{c.body}
+{c.author}
+From 30cf82ac3e970a6a3c0f07db1340dd7152d1c35d Mon Sep 17 00:00:00 2001
From: Robert van Hoesel
Locale: {getLocaleByPath("en")}
Locale: {getLocaleByPath("fr")}
-Locale: {getLocaleByPath("portugues")}
+Locale: {getLocaleByPath("portuguese")}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro index 40298d125f5c..76e36dbdfab0 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro @@ -3,7 +3,7 @@ import { ViewTransitions } from 'astro:transitions'; // For the test fixture, we import the script but we don't use theComment out working example and uncomment non working exmaples
+Comment out working example and uncomment non working examples
` element. */ export async function highlightCodeBlocks(tree: Root, highlighter: Highlighter) { diff --git a/packages/markdown/remark/src/import-plugin-default.ts b/packages/markdown/remark/src/import-plugin-default.ts index 8c1d01b56906..1b6778f753ff 100644 --- a/packages/markdown/remark/src/import-plugin-default.ts +++ b/packages/markdown/remark/src/import-plugin-default.ts @@ -6,7 +6,7 @@ import type * as unified from 'unified'; let cwdUrlStr: string | undefined; -// In non-browser enviroments, we can try to resolve from the filesystem too +// In non-browser environments, we can try to resolve from the filesystem too export async function importPlugin(p: string): Promise{ // Try import from this package first try { diff --git a/packages/upgrade/src/actions/verify.ts b/packages/upgrade/src/actions/verify.ts index 8d4c6f1e7cbb..c3f4b7af2af2 100644 --- a/packages/upgrade/src/actions/verify.ts +++ b/packages/upgrade/src/actions/verify.ts @@ -177,7 +177,7 @@ async function resolveTargetVersion(packageInfo: PackageInfo, registry: string): packageInfo.changelogTitle = 'CHANGELOG'; } else { // Dependency updates should not include the specific dist-tag - // since they are just for compatability + // since they are just for compatibility packageInfo.tag = undefined; } } diff --git a/scripts/cmd/build.js b/scripts/cmd/build.js index 1830cddcf1ff..5dd2006432c7 100644 --- a/scripts/cmd/build.js +++ b/scripts/cmd/build.js @@ -179,7 +179,7 @@ async function getWorkspacePackageVersion(packageName) { const version = deps[packageName]; if (!version) { throw new Error( - `Unable to resolve "${packageName}". Is it a depdendency of the workspace root?` + `Unable to resolve "${packageName}". Is it a dependency of the workspace root?` ); } return version.replace(/^\D+/, ''); From 85f407efaec72b7af2d684a75f7cd4c96a90a494 Mon Sep 17 00:00:00 2001 From: Yugo Ogura <15419227+Spice-Z@users.noreply.github.com> Date: Tue, 30 Apr 2024 23:08:53 -0700 Subject: [PATCH 030/151] docs: fix typo in CONTRIBUTING.md (#10910) --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc891d36986a..c74cd375fba6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,9 @@ We welcome contributions of any size and skill level. As an open source project, we believe in giving back to our contributors and are happy to help with guidance on PRs, technical writing, and turning any feature idea into a reality. -> [!Tip] > **For new contributors:** Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing +> [!Tip] +> +> **For new contributors:** Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing ## Quick Guide From dd1d9376218ebc03bd8e25daa066dc8e3df77ee4 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Wed, 1 May 2024 15:14:36 +0900 Subject: [PATCH 031/151] fix: typo in build.js (#10915) From a0c77fc7164662ea62b65c51fd1bd4c2f6028bc1 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Wed, 1 May 2024 14:03:55 +0200 Subject: [PATCH 032/151] chore: changeset (#10925) --- .changeset/olive-cars-run.md | 5 +++++ packages/db/src/runtime/index.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/olive-cars-run.md diff --git a/.changeset/olive-cars-run.md b/.changeset/olive-cars-run.md new file mode 100644 index 000000000000..603ba0f7c185 --- /dev/null +++ b/.changeset/olive-cars-run.md @@ -0,0 +1,5 @@ +--- +"@astrojs/db": patch +--- + +Fixes `ASTRO_DATABASE_FILE` not correctly resolving relative paths (e.g. `ASTRO_DATABASE_FILE=./api/database.db` diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 06d08a8793f9..11a6758034bb 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -139,7 +139,8 @@ export function normalizeDatabaseUrl(envDbUrl: string | undefined, defaultDbUrl: if (envDbUrl.startsWith('file://')) { return envDbUrl; } - return new URL(envDbUrl, pathToFileURL(process.cwd())).toString(); + + return new URL(envDbUrl, pathToFileURL(process.cwd()) + "/").toString(); } else { // This is going to be a file URL always, return defaultDbUrl; From 77e206d7e50ee0e784c509e18e29db2498524678 Mon Sep 17 00:00:00 2001 From: Erika Date: Wed, 1 May 2024 12:04:47 +0000 Subject: [PATCH 033/151] [ci] format --- packages/db/src/runtime/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 11a6758034bb..544df372a503 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -140,7 +140,7 @@ export function normalizeDatabaseUrl(envDbUrl: string | undefined, defaultDbUrl: return envDbUrl; } - return new URL(envDbUrl, pathToFileURL(process.cwd()) + "/").toString(); + return new URL(envDbUrl, pathToFileURL(process.cwd()) + '/').toString(); } else { // This is going to be a file URL always, return defaultDbUrl; From 5248ed1fb35bc1407660a189f23155812787f9e4 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Wed, 1 May 2024 23:06:00 +0800 Subject: [PATCH 034/151] Delete unused changeset (#10927) --- .changeset/lorem-ipsum-dolor | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/lorem-ipsum-dolor diff --git a/.changeset/lorem-ipsum-dolor b/.changeset/lorem-ipsum-dolor deleted file mode 100644 index 15182e8f6cd5..000000000000 --- a/.changeset/lorem-ipsum-dolor +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes static builds when `config.outDir` is located outside of the astro project From 4ce5ced44d490f4c6df771995aef14e11910ec57 Mon Sep 17 00:00:00 2001 From: ktym4a Date: Thu, 2 May 2024 15:58:40 +0900 Subject: [PATCH 035/151] change: NotificationPayload type (#10931) * change: NotificationPayload type * Create silent-zoos-work.md --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> --- .changeset/silent-zoos-work.md | 5 +++++ packages/astro/src/runtime/client/dev-toolbar/helpers.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/silent-zoos-work.md diff --git a/.changeset/silent-zoos-work.md b/.changeset/silent-zoos-work.md new file mode 100644 index 000000000000..b5ec9ac11e5b --- /dev/null +++ b/.changeset/silent-zoos-work.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes `toggleNotification()`'s parameter type for the notification level not using the proper levels diff --git a/packages/astro/src/runtime/client/dev-toolbar/helpers.ts b/packages/astro/src/runtime/client/dev-toolbar/helpers.ts index 9fcb5af798c3..849c8cb1018a 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/helpers.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/helpers.ts @@ -1,7 +1,7 @@ type NotificationPayload = | { state: true; - level?: 'error' | 'warn' | 'info'; + level?: 'error' | 'warning' | 'info'; } | { state: false; From 007d17fee072955d4acb846a06d9eb666e908ef6 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Thu, 2 May 2024 10:31:19 +0200 Subject: [PATCH 036/151] Fixes toggleState (#10933) --- .changeset/lovely-feet-greet.md | 5 +++++ packages/astro/src/runtime/client/dev-toolbar/helpers.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/lovely-feet-greet.md diff --git a/.changeset/lovely-feet-greet.md b/.changeset/lovely-feet-greet.md new file mode 100644 index 000000000000..9ba35a15522b --- /dev/null +++ b/.changeset/lovely-feet-greet.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes `app.toggleState` not working correctly diff --git a/packages/astro/src/runtime/client/dev-toolbar/helpers.ts b/packages/astro/src/runtime/client/dev-toolbar/helpers.ts index 849c8cb1018a..c205d0003d83 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/helpers.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/helpers.ts @@ -46,7 +46,7 @@ export class ToolbarAppEventTarget extends EventTarget { */ toggleState(options: AppStatePayload) { this.dispatchEvent( - new CustomEvent('app-toggled', { + new CustomEvent('toggle-app', { detail: { state: options.state, } satisfies AppStatePayload, From 9a231a4dd7e14ecb9cb95842060a4a24e4c03f02 Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Thu, 2 May 2024 04:59:20 -0700 Subject: [PATCH 037/151] [ci] release (#10890) Co-authored-by: github-actions[bot] --- .changeset/afraid-mirrors-tease.md | 5 -- .changeset/angry-lemons-tie.md | 7 --- .changeset/curly-spoons-pull.md | 5 -- .changeset/happy-pumpkins-rescue.md | 5 -- .changeset/lovely-feet-greet.md | 5 -- .changeset/olive-cars-run.md | 5 -- .changeset/silent-zoos-work.md | 5 -- .changeset/twelve-bulldogs-raise.md | 5 -- examples/basics/package.json | 2 +- examples/blog/package.json | 2 +- examples/component/package.json | 2 +- examples/framework-alpine/package.json | 2 +- examples/framework-lit/package.json | 2 +- examples/framework-multiple/package.json | 4 +- examples/framework-preact/package.json | 2 +- examples/framework-react/package.json | 4 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 2 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 2 +- examples/integration/package.json | 2 +- examples/middleware/package.json | 2 +- examples/minimal/package.json | 2 +- examples/non-html-pages/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 2 +- examples/starlog/package.json | 2 +- examples/toolbar-app/package.json | 2 +- examples/view-transitions/package.json | 2 +- examples/with-markdoc/package.json | 2 +- examples/with-markdown-plugins/package.json | 2 +- examples/with-markdown-shiki/package.json | 2 +- examples/with-mdx/package.json | 2 +- examples/with-nanostores/package.json | 2 +- examples/with-tailwindcss/package.json | 2 +- examples/with-vitest/package.json | 2 +- packages/astro/CHANGELOG.md | 12 +++++ packages/astro/package.json | 2 +- packages/db/CHANGELOG.md | 12 +++++ packages/db/package.json | 2 +- packages/integrations/react/CHANGELOG.md | 6 +++ packages/integrations/react/package.json | 2 +- pnpm-lock.yaml | 60 ++++++++++----------- 43 files changed, 93 insertions(+), 105 deletions(-) delete mode 100644 .changeset/afraid-mirrors-tease.md delete mode 100644 .changeset/angry-lemons-tie.md delete mode 100644 .changeset/curly-spoons-pull.md delete mode 100644 .changeset/happy-pumpkins-rescue.md delete mode 100644 .changeset/lovely-feet-greet.md delete mode 100644 .changeset/olive-cars-run.md delete mode 100644 .changeset/silent-zoos-work.md delete mode 100644 .changeset/twelve-bulldogs-raise.md diff --git a/.changeset/afraid-mirrors-tease.md b/.changeset/afraid-mirrors-tease.md deleted file mode 100644 index 5d0c318ef836..000000000000 --- a/.changeset/afraid-mirrors-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Skips adding CSS dependencies of CSS Vite modules as style tags in the HTML diff --git a/.changeset/angry-lemons-tie.md b/.changeset/angry-lemons-tie.md deleted file mode 100644 index 91e0978f4953..000000000000 --- a/.changeset/angry-lemons-tie.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@astrojs/db": patch ---- - -Improves the typing of the `asDrizzleTable()` utility - -Fixes a type error when passing the output of `defineTable()` to the utility and returns a more detailed type inferred from the columns of the passed table config. diff --git a/.changeset/curly-spoons-pull.md b/.changeset/curly-spoons-pull.md deleted file mode 100644 index 7d0d085fe475..000000000000 --- a/.changeset/curly-spoons-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Detects overlapping navigation and view transitions and automatically aborts all but the most recent one. diff --git a/.changeset/happy-pumpkins-rescue.md b/.changeset/happy-pumpkins-rescue.md deleted file mode 100644 index c43a15751107..000000000000 --- a/.changeset/happy-pumpkins-rescue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/db": patch ---- - -Provide a better error message when app token is missing in CI diff --git a/.changeset/lovely-feet-greet.md b/.changeset/lovely-feet-greet.md deleted file mode 100644 index 9ba35a15522b..000000000000 --- a/.changeset/lovely-feet-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Fixes `app.toggleState` not working correctly diff --git a/.changeset/olive-cars-run.md b/.changeset/olive-cars-run.md deleted file mode 100644 index 603ba0f7c185..000000000000 --- a/.changeset/olive-cars-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/db": patch ---- - -Fixes `ASTRO_DATABASE_FILE` not correctly resolving relative paths (e.g. `ASTRO_DATABASE_FILE=./api/database.db` diff --git a/.changeset/silent-zoos-work.md b/.changeset/silent-zoos-work.md deleted file mode 100644 index b5ec9ac11e5b..000000000000 --- a/.changeset/silent-zoos-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Fixes `toggleNotification()`'s parameter type for the notification level not using the proper levels diff --git a/.changeset/twelve-bulldogs-raise.md b/.changeset/twelve-bulldogs-raise.md deleted file mode 100644 index d0acf5c09429..000000000000 --- a/.changeset/twelve-bulldogs-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/react": patch ---- - -Removes using deprecated `ReactDOMServer.renderToStaticNodeStream` API diff --git a/examples/basics/package.json b/examples/basics/package.json index 432fb69c4a4a..5c3e3c591953 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 2b291959f2b4..2dc4327a21f7 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -14,6 +14,6 @@ "@astrojs/mdx": "^2.3.1", "@astrojs/rss": "^4.0.5", "@astrojs/sitemap": "^3.1.4", - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/component/package.json b/examples/component/package.json index b48d3284f9a4..5d58f7aad047 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 079c9b37e43a..9bc2f9a22307 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -14,6 +14,6 @@ "@astrojs/alpinejs": "^0.4.0", "@types/alpinejs": "^3.13.5", "alpinejs": "^3.13.3", - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index b545d307c1f8..6ce890180864 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/lit": "^4.0.1", "@webcomponents/template-shadowroot": "^0.2.1", - "astro": "^4.7.0", + "astro": "^4.7.1", "lit": "^3.1.2" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 96ebae4a3974..2a3855175085 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -12,13 +12,13 @@ }, "dependencies": { "@astrojs/preact": "^3.2.0", - "@astrojs/react": "^3.3.1", + "@astrojs/react": "^3.3.2", "@astrojs/solid-js": "^4.1.0", "@astrojs/svelte": "^5.4.0", "@astrojs/vue": "^4.1.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "astro": "^4.7.0", + "astro": "^4.7.1", "preact": "^10.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 6f2b14f30689..c771cfaebe9d 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.2.0", "@preact/signals": "^1.2.1", - "astro": "^4.7.0", + "astro": "^4.7.1", "preact": "^10.19.2" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 6f75d049036c..154d81ff638d 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -11,10 +11,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/react": "^3.3.1", + "@astrojs/react": "^3.3.2", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "astro": "^4.7.0", + "astro": "^4.7.1", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 7937d208aefe..c73286b7fc77 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^4.1.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "solid-js": "^1.8.5" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 9889bd7d2318..d976d3bac0b7 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/svelte": "^5.4.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "svelte": "^4.2.5" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 000cd5320368..20d24b1bdf3a 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^4.1.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "vue": "^3.3.8" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 44c2cd938a97..0fb6bb59a37d 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/node": "^8.2.5", - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index 5ffce1c86a02..bb18d81ca471 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index fae6a84565d9..760ea6f2abd6 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@astrojs/node": "^8.2.5", - "astro": "^4.7.0", + "astro": "^4.7.1", "html-minifier": "^4.0.0" }, "devDependencies": { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index cb282c938f65..530023511b7a 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index 5c609d8dd793..6f0420779b42 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 9fe9d5f11cd0..07ac5a14094e 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 37302f7c84e5..c93536811ad6 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -14,7 +14,7 @@ "dependencies": { "@astrojs/node": "^8.2.5", "@astrojs/svelte": "^5.4.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "svelte": "^4.2.5" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index baba24ec9419..8a16cd5e2458 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.7.0", + "astro": "^4.7.1", "sass": "^1.69.5", "sharp": "^0.33.3" } diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index ada6648fe54b..22d073256d05 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -15,6 +15,6 @@ "./app": "./dist/app.js" }, "devDependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index 1485fbf52924..95898e2685c2 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -12,6 +12,6 @@ "devDependencies": { "@astrojs/tailwind": "^5.1.0", "@astrojs/node": "^8.2.5", - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index c592e4cd861b..7d48e13db452 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/markdoc": "^0.11.0", - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 33827e16c99d..061f5e20b3a5 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/markdown-remark": "^5.1.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "hast-util-select": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index bda2955f5bf3..0bb88803cdcd 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.7.0" + "astro": "^4.7.1" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 3f6938c86d8b..5eb9f7a6f626 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^2.3.1", "@astrojs/preact": "^3.2.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "preact": "^10.19.2" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index fd2ca0add385..e27d6d1efa86 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.2.0", "@nanostores/preact": "^0.5.0", - "astro": "^4.7.0", + "astro": "^4.7.1", "nanostores": "^0.9.5", "preact": "^10.19.2" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 171e7d54dd18..6bb09d0d4fbb 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -14,7 +14,7 @@ "@astrojs/mdx": "^2.3.1", "@astrojs/tailwind": "^5.1.0", "@types/canvas-confetti": "^1.6.3", - "astro": "^4.7.0", + "astro": "^4.7.1", "autoprefixer": "^10.4.15", "canvas-confetti": "^1.9.1", "postcss": "^8.4.28", diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 432db1b04c99..5a5e254e9459 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^4.7.0", + "astro": "^4.7.1", "vitest": "^1.5.0" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 33e47f6d2a65..5073c34a91e7 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,17 @@ # astro +## 4.7.1 + +### Patch Changes + +- [#10911](https://github.com/withastro/astro/pull/10911) [`a86dc9d`](https://github.com/withastro/astro/commit/a86dc9d269fc4409c458cfa05dcfaeee12bade2f) Thanks [@bluwy](https://github.com/bluwy)! - Skips adding CSS dependencies of CSS Vite modules as style tags in the HTML + +- [#10900](https://github.com/withastro/astro/pull/10900) [`36bb3b6`](https://github.com/withastro/astro/commit/36bb3b6025eb51f6e027a76a514cc7ebb29deb10) Thanks [@martrapp](https://github.com/martrapp)! - Detects overlapping navigation and view transitions and automatically aborts all but the most recent one. + +- [#10933](https://github.com/withastro/astro/pull/10933) [`007d17f`](https://github.com/withastro/astro/commit/007d17fee072955d4acb846a06d9eb666e908ef6) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes `app.toggleState` not working correctly + +- [#10931](https://github.com/withastro/astro/pull/10931) [`4ce5ced`](https://github.com/withastro/astro/commit/4ce5ced44d490f4c6df771995aef14e11910ec57) Thanks [@ktym4a](https://github.com/ktym4a)! - Fixes `toggleNotification()`'s parameter type for the notification level not using the proper levels + ## 4.7.0 ### Minor Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 07cb5f6361bf..6c3bcfeddbf3 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "4.7.0", + "version": "4.7.1", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/db/CHANGELOG.md b/packages/db/CHANGELOG.md index 2b89cae73ccc..8193ebc83547 100644 --- a/packages/db/CHANGELOG.md +++ b/packages/db/CHANGELOG.md @@ -1,5 +1,17 @@ # @astrojs/db +## 0.10.7 + +### Patch Changes + +- [#10882](https://github.com/withastro/astro/pull/10882) [`cf58d1e`](https://github.com/withastro/astro/commit/cf58d1ed56c671d0ee077dfecc286002b4bae5ed) Thanks [@delucis](https://github.com/delucis)! - Improves the typing of the `asDrizzleTable()` utility + + Fixes a type error when passing the output of `defineTable()` to the utility and returns a more detailed type inferred from the columns of the passed table config. + +- [#10918](https://github.com/withastro/astro/pull/10918) [`ca605f4`](https://github.com/withastro/astro/commit/ca605f4dd8fcd070d3d5a5ca2f7080d921801e17) Thanks [@matthewp](https://github.com/matthewp)! - Provide a better error message when app token is missing in CI + +- [#10925](https://github.com/withastro/astro/pull/10925) [`a0c77fc`](https://github.com/withastro/astro/commit/a0c77fc7164662ea62b65c51fd1bd4c2f6028bc1) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes `ASTRO_DATABASE_FILE` not correctly resolving relative paths (e.g. `ASTRO_DATABASE_FILE=./api/database.db` + ## 0.10.6 ### Patch Changes diff --git a/packages/db/package.json b/packages/db/package.json index e83e407905af..24d7f350287a 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/db", - "version": "0.10.6", + "version": "0.10.7", "description": "Add libSQL and Astro Studio support to your Astro site", "license": "MIT", "repository": { diff --git a/packages/integrations/react/CHANGELOG.md b/packages/integrations/react/CHANGELOG.md index b1664f60123c..af63e5c5cbd5 100644 --- a/packages/integrations/react/CHANGELOG.md +++ b/packages/integrations/react/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/react +## 3.3.2 + +### Patch Changes + +- [#10893](https://github.com/withastro/astro/pull/10893) [`fd7a9ed`](https://github.com/withastro/astro/commit/fd7a9ed3379a123f02f297b69fa5da0053e84a89) Thanks [@Angrigo](https://github.com/Angrigo)! - Removes using deprecated `ReactDOMServer.renderToStaticNodeStream` API + ## 3.3.1 ### Patch Changes diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index ba7bbae814e8..90eaba4c3f3d 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/react", "description": "Use React components within Astro", - "version": "3.3.1", + "version": "3.3.2", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce5f061d9cb8..cb460a49c147 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,7 +128,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/blog: @@ -143,13 +143,13 @@ importers: specifier: ^3.1.4 version: link:../../packages/integrations/sitemap astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/component: devDependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/framework-alpine: @@ -164,7 +164,7 @@ importers: specifier: ^3.13.3 version: 3.13.8 astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/framework-lit: @@ -176,7 +176,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro lit: specifier: ^3.1.2 @@ -188,7 +188,7 @@ importers: specifier: ^3.2.0 version: link:../../packages/integrations/preact '@astrojs/react': - specifier: ^3.3.1 + specifier: ^3.3.2 version: link:../../packages/integrations/react '@astrojs/solid-js': specifier: ^4.1.0 @@ -206,7 +206,7 @@ importers: specifier: ^18.2.15 version: 18.2.25 astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -236,7 +236,7 @@ importers: specifier: ^1.2.1 version: 1.2.1(preact@10.20.2) astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -245,7 +245,7 @@ importers: examples/framework-react: dependencies: '@astrojs/react': - specifier: ^3.3.1 + specifier: ^3.3.2 version: link:../../packages/integrations/react '@types/react': specifier: ^18.2.37 @@ -254,7 +254,7 @@ importers: specifier: ^18.2.15 version: 18.2.25 astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro react: specifier: ^18.2.0 @@ -269,7 +269,7 @@ importers: specifier: ^4.1.0 version: link:../../packages/integrations/solid astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro solid-js: specifier: ^1.8.5 @@ -281,7 +281,7 @@ importers: specifier: ^5.4.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -293,7 +293,7 @@ importers: specifier: ^4.1.0 version: link:../../packages/integrations/vue astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro vue: specifier: ^3.3.8 @@ -305,13 +305,13 @@ importers: specifier: ^8.2.5 version: link:../../packages/integrations/node astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/middleware: @@ -320,7 +320,7 @@ importers: specifier: ^8.2.5 version: link:../../packages/integrations/node astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro html-minifier: specifier: ^4.0.0 @@ -333,19 +333,19 @@ importers: examples/minimal: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/non-html-pages: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/ssr: @@ -357,7 +357,7 @@ importers: specifier: ^5.4.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -366,7 +366,7 @@ importers: examples/starlog: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro sass: specifier: ^1.69.5 @@ -378,7 +378,7 @@ importers: examples/toolbar-app: devDependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/view-transitions: @@ -390,7 +390,7 @@ importers: specifier: ^5.1.0 version: link:../../packages/integrations/tailwind astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/with-markdoc: @@ -399,7 +399,7 @@ importers: specifier: ^0.11.0 version: link:../../packages/integrations/markdoc astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/with-markdown-plugins: @@ -408,7 +408,7 @@ importers: specifier: ^5.1.0 version: link:../../packages/markdown/remark astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro hast-util-select: specifier: ^6.0.2 @@ -429,7 +429,7 @@ importers: examples/with-markdown-shiki: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro examples/with-mdx: @@ -441,7 +441,7 @@ importers: specifier: ^3.2.0 version: link:../../packages/integrations/preact astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -456,7 +456,7 @@ importers: specifier: ^0.5.0 version: 0.5.1(nanostores@0.9.5)(preact@10.20.2) astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro nanostores: specifier: ^0.9.5 @@ -477,7 +477,7 @@ importers: specifier: ^1.6.3 version: 1.6.4 astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro autoprefixer: specifier: ^10.4.15 @@ -495,7 +495,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^4.7.0 + specifier: ^4.7.1 version: link:../../packages/astro vitest: specifier: ^1.5.0 From 44bafa989af0cc380696bb6381048fc1ee55dd5b Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 3 May 2024 11:08:50 -0400 Subject: [PATCH 038/151] [db] Fix duplicate calls to recreate tables on startup (#10919) * fix: move recreateTables() to integration hooks * feat: recreate and seed at load, not in virtual runtime * feat: eager build db on startup and seed file change * fix: respect database_file in dbUrl * chore: remove duplicate recreateTables call * chore: remove now self-explanatory comments * fix: remove invalidateModule call for eager loading * feat: respect seed package paths * fix: remove duplicate recreateTables() call * refactor: move recreateTables() to vite-plugin-db * refactor: move queries.ts from runtime/ to core/ * fix: update test import to core/queries * refactor: move executeSeedFile to vite-plugin-db * refactor: extract seeding and recreating to helper fns * chore: changeset * chore: revert connectToStudio refactor * wip: log db url * fix(test): normalize astro_database_file flag for windows * Revert "wip: log db url" This reverts commit 558e2de67a09a611377929b625127c649b8504d6. * Revert "Revert "wip: log db url"" This reverts commit ffd004e00dff485b7bc5ddde0278dde6ff058b9e. * fix: correctly resolve relative paths with unit test * chore: remove unused dbDirPath Co-authored-by: Chris Swithinbank * chore: remove unused import Co-authored-by: Chris Swithinbank * chore: remove unused type Co-authored-by: Chris Swithinbank * fix: remove bad import * [db] Load seed files with vite dev server (#10941) * feat: load seed files with full vite dev server * chore: remove unused export --------- Co-authored-by: Chris Swithinbank --- .changeset/young-pots-brake.md | 6 + .../db/src/core/cli/commands/execute/index.ts | 4 +- .../db/src/core/cli/commands/shell/index.ts | 10 +- packages/db/src/core/cli/migration-queries.ts | 2 +- packages/db/src/core/integration/index.ts | 131 ++++++++++------ .../db/src/core/integration/vite-plugin-db.ts | 140 ++++++++++-------- packages/db/src/core/load-file.ts | 16 +- packages/db/src/{runtime => core}/queries.ts | 8 +- packages/db/src/core/utils.ts | 5 + packages/db/src/runtime/index.ts | 1 - packages/db/src/runtime/seed-local.ts | 59 -------- packages/db/src/runtime/utils.ts | 2 +- packages/db/test/local-prod.test.js | 7 +- packages/db/test/test-utils.js | 2 +- 14 files changed, 211 insertions(+), 182 deletions(-) create mode 100644 .changeset/young-pots-brake.md rename packages/db/src/{runtime => core}/queries.ts (97%) delete mode 100644 packages/db/src/runtime/seed-local.ts diff --git a/.changeset/young-pots-brake.md b/.changeset/young-pots-brake.md new file mode 100644 index 000000000000..33b5e7fd68be --- /dev/null +++ b/.changeset/young-pots-brake.md @@ -0,0 +1,6 @@ +--- +"@astrojs/db": minor +--- + +- Fix duplicate table recreations when you start your dev server. +- Remove eager re-seeding when updating your seed file in development. Seeding still runs on dev server startup for SQLite inspector tools. diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index 05a04ad61e85..b596d4c0d3b6 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -15,7 +15,7 @@ import { } from '../../../integration/vite-plugin-db.js'; import { bundleFile, importBundledFile } from '../../../load-file.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; -import { type DBConfig } from '../../../types.js'; +import type { DBConfig } from '../../../types.js'; export async function cmd({ astroConfig, @@ -51,8 +51,6 @@ export async function cmd({ virtualModContents = getLocalVirtualModContents({ tables: dbConfig.tables ?? {}, root: astroConfig.root, - shouldSeed: false, - seedFiles: [], }); } const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl }); diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 57fb6e959e40..0c1883cd20a6 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -9,7 +9,8 @@ import { DB_PATH } from '../../../consts.js'; import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; import type { DBConfigInput } from '../../../types.js'; -import { getRemoteDatabaseUrl } from '../../../utils.js'; +import { getAstroEnv, getRemoteDatabaseUrl } from '../../../utils.js'; +import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; export async function cmd({ flags, @@ -31,7 +32,12 @@ export async function cmd({ await appToken.destroy(); console.log(result); } else { - const db = createLocalDatabaseClient({ dbUrl: new URL(DB_PATH, astroConfig.root).href }); + const { ASTRO_DATABASE_FILE } = getAstroEnv(); + const dbUrl = normalizeDatabaseUrl( + ASTRO_DATABASE_FILE, + new URL(DB_PATH, astroConfig.root).href + ); + const db = createLocalDatabaseClient({ dbUrl }); const result = await db.run(sql.raw(query)); console.log(result); } diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index d5fe959cb116..94674f9d197b 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -12,7 +12,7 @@ import { getReferencesConfig, hasDefault, schemaTypeToSqlType, -} from '../../runtime/queries.js'; +} from '../queries.js'; import { isSerializedSQL } from '../../runtime/types.js'; import { safeFetch } from '../../runtime/utils.js'; import { MIGRATION_VERSION } from '../consts.js'; diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 2e3c1f2ea174..403d1901b3f7 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -4,9 +4,16 @@ import { fileURLToPath } from 'url'; import type { AstroConfig, AstroIntegration } from 'astro'; import { mkdir, writeFile } from 'fs/promises'; import { blue, yellow } from 'kleur/colors'; -import { loadEnv } from 'vite'; +import { + createServer, + loadEnv, + mergeConfig, + type HMRPayload, + type UserConfig, + type ViteDevServer, +} from 'vite'; import parseArgs from 'yargs-parser'; -import { SEED_DEV_FILE_NAME } from '../../runtime/queries.js'; +import { SEED_DEV_FILE_NAME } from '../queries.js'; import { AstroDbError } from '../../runtime/utils.js'; import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js'; import { resolveDbConfig } from '../load-file.js'; @@ -14,14 +21,24 @@ import { type ManagedAppToken, getManagedAppTokenOrExit } from '../tokens.js'; import { type VitePlugin, getDbDirectoryUrl } from '../utils.js'; import { fileURLIntegration } from './file-url.js'; import { typegenInternal } from './typegen.js'; -import { type LateSeedFiles, type LateTables, resolved, vitePluginDb } from './vite-plugin-db.js'; +import { + type LateSeedFiles, + type LateTables, + vitePluginDb, + type SeedHandler, + resolved, +} from './vite-plugin-db.js'; import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; +import { LibsqlError } from '@libsql/client'; +import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js'; function astroDBIntegration(): AstroIntegration { let connectToStudio = false; let configFileDependencies: string[] = []; let root: URL; let appToken: ManagedAppToken | undefined; + // Used during production builds to load seed files. + let tempViteServer: ViteDevServer | undefined; // Make table loading "late" to pass to plugins from `config:setup`, // but load during `config:done` to wait for integrations to settle. @@ -35,6 +52,13 @@ function astroDBIntegration(): AstroIntegration { throw new Error('[astro:db] INTERNAL Seed files not loaded yet'); }, }; + let seedHandler: SeedHandler = { + execute: () => { + throw new Error('[astro:db] INTERNAL Seed handler not loaded yet'); + }, + inProgress: false, + }; + let command: 'dev' | 'build' | 'preview'; let output: AstroConfig['output'] = 'server'; return { @@ -60,6 +84,7 @@ function astroDBIntegration(): AstroIntegration { root: config.root, srcDir: config.srcDir, output: config.output, + seedHandler, }); } else { dbPlugin = vitePluginDb({ @@ -69,6 +94,8 @@ function astroDBIntegration(): AstroIntegration { root: config.root, srcDir: config.srcDir, output: config.output, + logger, + seedHandler, }); } @@ -98,6 +125,9 @@ function astroDBIntegration(): AstroIntegration { await typegenInternal({ tables: tables.get() ?? {}, root: config.root }); }, 'astro:server:setup': async ({ server, logger }) => { + seedHandler.execute = async (fileUrl) => { + await executeSeedFile({ fileUrl, viteServer: server }); + }; const filesToWatch = [ ...CONFIG_FILE_NAMES.map((c) => new URL(c, getDbDirectoryUrl(root))), ...configFileDependencies.map((c) => new URL(c, root)), @@ -118,46 +148,11 @@ function astroDBIntegration(): AstroIntegration { const localSeedPaths = SEED_DEV_FILE_NAME.map( (name) => new URL(name, getDbDirectoryUrl(root)) ); - let seedInFlight = false; - // Load seed file on dev server startup. + // Eager load astro:db module on startup if (seedFiles.get().length || localSeedPaths.find((path) => existsSync(path))) { - loadSeedModule(); - } - const eagerReloadIntegrationSeedPaths = seedFiles - .get() - // Map integration seed paths to URLs, if possible. - // Module paths like `@example/seed` will be ignored - // from eager reloading. - .map((s) => (typeof s === 'string' && s.startsWith('.') ? new URL(s, root) : s)) - .filter((s): s is URL => s instanceof URL); - const eagerReloadSeedPaths = [...eagerReloadIntegrationSeedPaths, ...localSeedPaths]; - server.watcher.on('all', (event, relativeEntry) => { - if (event === 'unlink' || event === 'unlinkDir') return; - // When a seed file changes, load manually - // to track when seeding finishes and log a message. - const entry = new URL(relativeEntry, root); - if (eagerReloadSeedPaths.find((path) => entry.href === path.href)) { - loadSeedModule(); - } - }); - - function loadSeedModule() { - if (seedInFlight) return; - - seedInFlight = true; - const mod = server.moduleGraph.getModuleById(resolved.seedVirtual); - if (mod) server.moduleGraph.invalidateModule(mod); - server - .ssrLoadModule(resolved.seedVirtual) - .then(() => { - logger.info('Seeded database.'); - }) - .catch((e) => { - logger.error(e instanceof Error ? e.message : String(e)); - }) - .finally(() => { - seedInFlight = false; - }); + server.ssrLoadModule(resolved.module).catch((e) => { + logger.error(e instanceof Error ? e.message : String(e)); + }); } }, 100); }, @@ -175,8 +170,15 @@ function astroDBIntegration(): AstroIntegration { logger.info('database: ' + (connectToStudio ? yellow('remote') : blue('local database.'))); }, + 'astro:build:setup': async ({ vite }) => { + tempViteServer = await getTempViteServer({ viteConfig: vite }); + seedHandler.execute = async (fileUrl) => { + await executeSeedFile({ fileUrl, viteServer: tempViteServer! }); + }; + }, 'astro:build:done': async ({}) => { await appToken?.destroy(); + await tempViteServer?.close(); }, }, }; @@ -190,3 +192,48 @@ function databaseFileEnvDefined() { export function integration(): AstroIntegration[] { return [astroDBIntegration(), fileURLIntegration()]; } + +async function executeSeedFile({ + fileUrl, + viteServer, +}: { + fileUrl: URL; + viteServer: ViteDevServer; +}) { + const mod = await viteServer.ssrLoadModule(fileUrl.pathname); + if (typeof mod.default !== 'function') { + throw new AstroDbError(EXEC_DEFAULT_EXPORT_ERROR(fileURLToPath(fileUrl))); + } + try { + await mod.default(); + } catch (e) { + if (e instanceof LibsqlError) { + throw new AstroDbError(EXEC_ERROR(e.message)); + } + throw e; + } +} + +/** + * Inspired by Astro content collection config loader. + */ +async function getTempViteServer({ viteConfig }: { viteConfig: UserConfig }) { + const tempViteServer = await createServer( + mergeConfig(viteConfig, { + server: { middlewareMode: true, hmr: false, watch: null }, + optimizeDeps: { noDiscovery: true }, + ssr: { external: [] }, + logLevel: 'silent', + }) + ); + + const hotSend = tempViteServer.hot.send; + tempViteServer.hot.send = (payload: HMRPayload) => { + if (payload.type === 'error') { + throw payload.err; + } + return hotSend(payload); + }; + + return tempViteServer; +} diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index df31d527673b..865fcd168086 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -1,16 +1,19 @@ import { fileURLToPath } from 'node:url'; -import type { AstroConfig } from 'astro'; -import { normalizePath } from 'vite'; -import { SEED_DEV_FILE_NAME } from '../../runtime/queries.js'; +import type { AstroConfig, AstroIntegrationLogger } from 'astro'; +import { SEED_DEV_FILE_NAME, getCreateIndexQueries, getCreateTableQuery } from '../queries.js'; import { DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; import type { DBTables } from '../types.js'; -import { type VitePlugin, getDbDirectoryUrl, getRemoteDatabaseUrl } from '../utils.js'; - -const WITH_SEED_VIRTUAL_MODULE_ID = 'astro:db:seed'; +import { type VitePlugin, getDbDirectoryUrl, getRemoteDatabaseUrl, getAstroEnv } from '../utils.js'; +import { createLocalDatabaseClient } from '../../runtime/db-client.js'; +import { type SQL, sql } from 'drizzle-orm'; +import { existsSync } from 'node:fs'; +import { normalizeDatabaseUrl } from '../../runtime/index.js'; +import { getResolvedFileUrl } from '../load-file.js'; +import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; export const resolved = { - virtual: '\0' + VIRTUAL_MODULE_ID, - seedVirtual: '\0' + WITH_SEED_VIRTUAL_MODULE_ID, + module: '\0' + VIRTUAL_MODULE_ID, + importedFromSeedFile: '\0' + VIRTUAL_MODULE_ID + ':seed', }; export type LateTables = { @@ -19,6 +22,10 @@ export type LateTables = { export type LateSeedFiles = { get: () => Array ; }; +export type SeedHandler = { + inProgress: boolean; + execute: (fileUrl: URL) => Promise ; +}; type VitePluginDBParams = | { @@ -27,7 +34,9 @@ type VitePluginDBParams = seedFiles: LateSeedFiles; srcDir: URL; root: URL; + logger?: AstroIntegrationLogger; output: AstroConfig['output']; + seedHandler: SeedHandler; } | { connectToStudio: true; @@ -36,11 +45,10 @@ type VitePluginDBParams = srcDir: URL; root: URL; output: AstroConfig['output']; + seedHandler: SeedHandler; }; export function vitePluginDb(params: VitePluginDBParams): VitePlugin { - const srcDirPath = normalizePath(fileURLToPath(params.srcDir)); - const dbDirPath = normalizePath(fileURLToPath(getDbDirectoryUrl(params.root))); let command: 'build' | 'serve' = 'build'; return { name: 'astro:db', @@ -48,22 +56,15 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { configResolved(resolvedConfig) { command = resolvedConfig.command; }, - async resolveId(id, rawImporter) { + async resolveId(id) { if (id !== VIRTUAL_MODULE_ID) return; - if (params.connectToStudio) return resolved.virtual; - - const importer = rawImporter ? await this.resolve(rawImporter) : null; - if (!importer) return resolved.virtual; - - if (importer.id.startsWith(srcDirPath) && !importer.id.startsWith(dbDirPath)) { - // Seed only if the importer is in the src directory. - // Otherwise, we may get recursive seed calls (ex. import from db/seed.ts). - return resolved.seedVirtual; + if (params.seedHandler.inProgress) { + return resolved.importedFromSeedFile; } - return resolved.virtual; + return resolved.module; }, async load(id) { - if (id !== resolved.virtual && id !== resolved.seedVirtual) return; + if (id !== resolved.module && id !== resolved.importedFromSeedFile) return; if (params.connectToStudio) { return getStudioVirtualModContents({ @@ -73,11 +74,35 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { output: params.output, }); } + + // When seeding, we resolved to a different virtual module. + // this prevents an infinite loop attempting to rerun seed files. + // Short circuit with the module contents in this case. + if (id === resolved.importedFromSeedFile) { + return getLocalVirtualModContents({ + root: params.root, + tables: params.tables.get(), + }); + } + + await recreateTables(params); + const seedFiles = getResolvedSeedFiles(params); + for await (const seedFile of seedFiles) { + // Use `addWatchFile()` to invalidate the `astro:db` module + // when a seed file changes. + this.addWatchFile(fileURLToPath(seedFile)); + if (existsSync(seedFile)) { + params.seedHandler.inProgress = true; + await params.seedHandler.execute(seedFile); + } + } + if (params.seedHandler.inProgress) { + (params.logger ?? console).info('Seeded database.'); + params.seedHandler.inProgress = false; + } return getLocalVirtualModContents({ root: params.root, tables: params.tables.get(), - seedFiles: params.seedFiles.get(), - shouldSeed: id === resolved.seedVirtual, }); }, }; @@ -90,53 +115,17 @@ export function getConfigVirtualModContents() { export function getLocalVirtualModContents({ tables, root, - seedFiles, - shouldSeed, }: { tables: DBTables; - seedFiles: Array ; root: URL; - shouldSeed: boolean; }) { - const userSeedFilePaths = SEED_DEV_FILE_NAME.map( - // Format as /db/[name].ts - // for Vite import.meta.glob - (name) => new URL(name, getDbDirectoryUrl('file:///')).pathname - ); - const resolveId = (id: string) => - id.startsWith('.') ? normalizePath(fileURLToPath(new URL(id, root))) : id; - // Use top-level imports to correctly resolve `astro:db` within seed files. - // Dynamic imports cause a silent build failure, - // potentially because of circular module references. - const integrationSeedImportStatements: string[] = []; - const integrationSeedImportNames: string[] = []; - seedFiles.forEach((pathOrUrl, index) => { - const path = typeof pathOrUrl === 'string' ? resolveId(pathOrUrl) : pathOrUrl.pathname; - const importName = 'integration_seed_' + index; - integrationSeedImportStatements.push(`import ${importName} from ${JSON.stringify(path)};`); - integrationSeedImportNames.push(importName); - }); - const dbUrl = new URL(DB_PATH, root); return ` import { asDrizzleTable, createLocalDatabaseClient, normalizeDatabaseUrl } from ${RUNTIME_IMPORT}; -${shouldSeed ? `import { seedLocal } from ${RUNTIME_IMPORT};` : ''} -${shouldSeed ? integrationSeedImportStatements.join('\n') : ''} const dbUrl = normalizeDatabaseUrl(import.meta.env.ASTRO_DATABASE_FILE, ${JSON.stringify(dbUrl)}); export const db = createLocalDatabaseClient({ dbUrl }); -${ - shouldSeed - ? `await seedLocal({ - db, - tables: ${JSON.stringify(tables)}, - userSeedGlob: import.meta.glob(${JSON.stringify(userSeedFilePaths)}, { eager: true }), - integrationSeedFunctions: [${integrationSeedImportNames.join(',')}], -});` - : '' -} - export * from ${RUNTIME_VIRTUAL_IMPORT}; ${getStringifiedTableExports(tables)}`; @@ -194,3 +183,34 @@ function getStringifiedTableExports(tables: DBTables) { ) .join('\n'); } + +const sqlite = new SQLiteAsyncDialect(); + +async function recreateTables({ tables, root }: { tables: LateTables; root: URL }) { + const { ASTRO_DATABASE_FILE } = getAstroEnv(); + const dbUrl = normalizeDatabaseUrl(ASTRO_DATABASE_FILE, new URL(DB_PATH, root).href); + const db = createLocalDatabaseClient({ dbUrl }); + const setupQueries: SQL[] = []; + for (const [name, table] of Object.entries(tables.get() ?? {})) { + const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`); + const createQuery = sql.raw(getCreateTableQuery(name, table)); + const indexQueries = getCreateIndexQueries(name, table); + setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s))); + } + await db.batch([ + db.run(sql`pragma defer_foreign_keys=true;`), + ...setupQueries.map((q) => db.run(q)), + ]); +} + +function getResolvedSeedFiles({ + root, + seedFiles, +}: { + root: URL; + seedFiles: LateSeedFiles; +}) { + const localSeedFiles = SEED_DEV_FILE_NAME.map((name) => new URL(name, getDbDirectoryUrl(root))); + const integrationSeedFiles = seedFiles.get().map((s) => getResolvedFileUrl(root, s)); + return [...integrationSeedFiles, ...localSeedFiles]; +} diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts index 7bc7387c824c..dd48df928cbd 100644 --- a/packages/db/src/core/load-file.ts +++ b/packages/db/src/core/load-file.ts @@ -10,7 +10,7 @@ import { errorMap } from './integration/error-map.js'; import { getConfigVirtualModContents } from './integration/vite-plugin-db.js'; import { dbConfigSchema } from './schemas.js'; import { type AstroDbIntegration } from './types.js'; -import { getDbDirectoryUrl } from './utils.js'; +import { getAstroEnv, getDbDirectoryUrl } from './utils.js'; const isDbIntegration = (integration: AstroIntegration): integration is AstroDbIntegration => 'astro:db:setup' in integration.hooks; @@ -85,15 +85,17 @@ async function loadUserConfigFile( return await loadAndBundleDbConfigFile({ root, fileUrl: configFileUrl }); } -async function loadIntegrationConfigFile(root: URL, filePathOrUrl: string | URL) { - let fileUrl: URL; +export function getResolvedFileUrl(root: URL, filePathOrUrl: string | URL): URL { if (typeof filePathOrUrl === 'string') { const { resolve } = createRequire(root); const resolvedFilePath = resolve(filePathOrUrl); - fileUrl = pathToFileURL(resolvedFilePath); - } else { - fileUrl = filePathOrUrl; + return pathToFileURL(resolvedFilePath); } + return filePathOrUrl; +} + +async function loadIntegrationConfigFile(root: URL, filePathOrUrl: string | URL) { + const fileUrl = getResolvedFileUrl(root, filePathOrUrl); return await loadAndBundleDbConfigFile({ root, fileUrl }); } @@ -133,6 +135,7 @@ export async function bundleFile({ root: URL; virtualModContents: string; }) { + const { ASTRO_DATABASE_FILE } = getAstroEnv(); const result = await esbuild({ absWorkingDir: process.cwd(), entryPoints: [fileURLToPath(fileUrl)], @@ -147,6 +150,7 @@ export async function bundleFile({ metafile: true, define: { 'import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL': 'undefined', + 'import.meta.env.ASTRO_DATABASE_FILE': JSON.stringify(ASTRO_DATABASE_FILE ?? ''), }, plugins: [ { diff --git a/packages/db/src/runtime/queries.ts b/packages/db/src/core/queries.ts similarity index 97% rename from packages/db/src/runtime/queries.ts rename to packages/db/src/core/queries.ts index f58cb1ce233f..ff4d56a5d124 100644 --- a/packages/db/src/runtime/queries.ts +++ b/packages/db/src/core/queries.ts @@ -10,15 +10,15 @@ import type { JsonColumn, NumberColumn, TextColumn, -} from '../core/types.js'; +} from './types.js'; import { FOREIGN_KEY_DNE_ERROR, FOREIGN_KEY_REFERENCES_EMPTY_ERROR, FOREIGN_KEY_REFERENCES_LENGTH_ERROR, REFERENCE_DNE_ERROR, -} from './errors.js'; -import { hasPrimaryKey } from './index.js'; -import { isSerializedSQL } from './types.js'; +} from '../runtime/errors.js'; +import { hasPrimaryKey } from '../runtime/index.js'; +import { isSerializedSQL } from '../runtime/types.js'; const sqlite = new SQLiteAsyncDialect(); diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index ebc2547b3e2e..d9f1e032386e 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -9,6 +9,11 @@ export function getAstroStudioEnv(envMode = ''): Record<`ASTRO_STUDIO_${string}` return env; } +export function getAstroEnv(envMode = ''): Record<`ASTRO_${string}`, string> { + const env = loadEnv(envMode, process.cwd(), 'ASTRO_'); + return env; +} + export function getRemoteDatabaseUrl(): string { const env = getAstroStudioEnv(); return env.ASTRO_STUDIO_REMOTE_DB_URL || 'https://db.services.astro.build'; diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 544df372a503..ba157f01da40 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -14,7 +14,6 @@ import { pathToFileURL } from './utils.js'; export type { Table } from './types.js'; export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js'; -export { seedLocal } from './seed-local.js'; export function hasPrimaryKey(column: DBColumn) { return 'primaryKey' in column.schema && !!column.schema.primaryKey; diff --git a/packages/db/src/runtime/seed-local.ts b/packages/db/src/runtime/seed-local.ts deleted file mode 100644 index 3fb6c61d0c6b..000000000000 --- a/packages/db/src/runtime/seed-local.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { LibsqlError } from '@libsql/client'; -import { type SQL, sql } from 'drizzle-orm'; -import type { LibSQLDatabase } from 'drizzle-orm/libsql'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; -import { type DBTables } from '../core/types.js'; -import { SEED_DEFAULT_EXPORT_ERROR } from './errors.js'; -import { getCreateIndexQueries, getCreateTableQuery } from './queries.js'; -import { AstroDbError } from './utils.js'; - -const sqlite = new SQLiteAsyncDialect(); - -export async function seedLocal({ - db, - tables, - // Glob all potential seed files to catch renames and deletions. - userSeedGlob, - integrationSeedFunctions, -}: { - db: LibSQLDatabase; - tables: DBTables; - userSeedGlob: Record Promise }>; - integrationSeedFunctions: Array<() => Promise >; -}) { - await recreateTables({ db, tables }); - const seedFunctions: Array<() => Promise > = []; - const seedFilePath = Object.keys(userSeedGlob)[0]; - if (seedFilePath) { - const mod = userSeedGlob[seedFilePath]; - if (!mod.default) throw new AstroDbError(SEED_DEFAULT_EXPORT_ERROR(seedFilePath)); - seedFunctions.push(mod.default); - } - for (const seedFn of integrationSeedFunctions) { - seedFunctions.push(seedFn); - } - for (const seed of seedFunctions) { - try { - await seed(); - } catch (e) { - if (e instanceof LibsqlError) { - throw new AstroDbError(`Failed to seed database:\n${e.message}`); - } - throw e; - } - } -} - -async function recreateTables({ db, tables }: { db: LibSQLDatabase; tables: DBTables }) { - const setupQueries: SQL[] = []; - for (const [name, table] of Object.entries(tables)) { - const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`); - const createQuery = sql.raw(getCreateTableQuery(name, table)); - const indexQueries = getCreateIndexQueries(name, table); - setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s))); - } - await db.batch([ - db.run(sql`pragma defer_foreign_keys=true;`), - ...setupQueries.map((q) => db.run(q)), - ]); -} diff --git a/packages/db/src/runtime/utils.ts b/packages/db/src/runtime/utils.ts index 97ad21ccdfdd..2fe837d8fad6 100644 --- a/packages/db/src/runtime/utils.ts +++ b/packages/db/src/runtime/utils.ts @@ -25,7 +25,7 @@ export class AstroDbError extends AstroError { name = 'Astro DB Error'; } -export default function slash(path: string) { +function slash(path: string) { const isExtendedLengthPath = path.startsWith('\\\\?\\'); if (isExtendedLengthPath) { diff --git a/packages/db/test/local-prod.test.js b/packages/db/test/local-prod.test.js index 3a2793392e8d..ff3b2c633ca5 100644 --- a/packages/db/test/local-prod.test.js +++ b/packages/db/test/local-prod.test.js @@ -2,6 +2,7 @@ import { fileURLToPath } from 'url'; import { expect } from 'chai'; import testAdapter from '../../astro/test/test-adapter.js'; import { loadFixture } from '../../astro/test/test-utils.js'; +import { relative } from 'path'; describe('astro:db local database', () => { let fixture; @@ -32,8 +33,10 @@ describe('astro:db local database', () => { }); }); - describe('build (not remote) with DATABASE_FILE env (file path)', () => { - const prodDbPath = fileURLToPath(new URL('./fixtures/basics/dist/astro.db', import.meta.url)); + describe('build (not remote) with DATABASE_FILE env (relative file path)', () => { + const absoluteFileUrl = new URL('./fixtures/basics/dist/astro.db', import.meta.url); + const prodDbPath = relative(process.cwd(), fileURLToPath(absoluteFileUrl)); + before(async () => { process.env.ASTRO_DATABASE_FILE = prodDbPath; await fixture.build(); diff --git a/packages/db/test/test-utils.js b/packages/db/test/test-utils.js index 2ff59e552e44..8be80a879a7e 100644 --- a/packages/db/test/test-utils.js +++ b/packages/db/test/test-utils.js @@ -3,7 +3,7 @@ import { createClient } from '@libsql/client'; import { z } from 'zod'; import { cli } from '../dist/core/cli/index.js'; import { resolveDbConfig } from '../dist/core/load-file.js'; -import { getCreateIndexQueries, getCreateTableQuery } from '../dist/runtime/queries.js'; +import { getCreateIndexQueries, getCreateTableQuery } from '../dist/core/queries.js'; const singleQuerySchema = z.object({ sql: z.string(), From befbda7fa3d712388789a5a9be1e0597834f86db Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 3 May 2024 15:09:39 +0000 Subject: [PATCH 039/151] [ci] format --- packages/db/src/core/cli/commands/shell/index.ts | 2 +- packages/db/src/core/cli/migration-queries.ts | 8 ++++---- packages/db/src/core/integration/index.ts | 14 +++++++------- .../db/src/core/integration/vite-plugin-db.ts | 14 +++++++------- packages/db/src/core/queries.ts | 16 ++++++++-------- packages/db/test/local-prod.test.js | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 0c1883cd20a6..c93a127fa99c 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -5,12 +5,12 @@ import { createLocalDatabaseClient, createRemoteDatabaseClient, } from '../../../../runtime/db-client.js'; +import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; import { DB_PATH } from '../../../consts.js'; import { SHELL_QUERY_MISSING_ERROR } from '../../../errors.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; import type { DBConfigInput } from '../../../types.js'; import { getAstroEnv, getRemoteDatabaseUrl } from '../../../utils.js'; -import { normalizeDatabaseUrl } from '../../../../runtime/index.js'; export async function cmd({ flags, diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index 94674f9d197b..f00d194bf71f 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -4,6 +4,10 @@ import * as color from 'kleur/colors'; import { customAlphabet } from 'nanoid'; import stripAnsi from 'strip-ansi'; import { hasPrimaryKey } from '../../runtime/index.js'; +import { isSerializedSQL } from '../../runtime/types.js'; +import { safeFetch } from '../../runtime/utils.js'; +import { MIGRATION_VERSION } from '../consts.js'; +import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js'; import { getCreateIndexQueries, getCreateTableQuery, @@ -13,10 +17,6 @@ import { hasDefault, schemaTypeToSqlType, } from '../queries.js'; -import { isSerializedSQL } from '../../runtime/types.js'; -import { safeFetch } from '../../runtime/utils.js'; -import { MIGRATION_VERSION } from '../consts.js'; -import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js'; import { columnSchema } from '../schemas.js'; import { type BooleanColumn, diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 403d1901b3f7..99b69bed6551 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -1,22 +1,24 @@ import { existsSync } from 'fs'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; +import { LibsqlError } from '@libsql/client'; import type { AstroConfig, AstroIntegration } from 'astro'; import { mkdir, writeFile } from 'fs/promises'; import { blue, yellow } from 'kleur/colors'; import { - createServer, - loadEnv, - mergeConfig, type HMRPayload, type UserConfig, type ViteDevServer, + createServer, + loadEnv, + mergeConfig, } from 'vite'; import parseArgs from 'yargs-parser'; -import { SEED_DEV_FILE_NAME } from '../queries.js'; import { AstroDbError } from '../../runtime/utils.js'; import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js'; +import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js'; import { resolveDbConfig } from '../load-file.js'; +import { SEED_DEV_FILE_NAME } from '../queries.js'; import { type ManagedAppToken, getManagedAppTokenOrExit } from '../tokens.js'; import { type VitePlugin, getDbDirectoryUrl } from '../utils.js'; import { fileURLIntegration } from './file-url.js'; @@ -24,13 +26,11 @@ import { typegenInternal } from './typegen.js'; import { type LateSeedFiles, type LateTables, - vitePluginDb, type SeedHandler, resolved, + vitePluginDb, } from './vite-plugin-db.js'; import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; -import { LibsqlError } from '@libsql/client'; -import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js'; function astroDBIntegration(): AstroIntegration { let connectToStudio = false; diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 865fcd168086..cd334b4deb7c 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -1,15 +1,15 @@ +import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import type { AstroConfig, AstroIntegrationLogger } from 'astro'; -import { SEED_DEV_FILE_NAME, getCreateIndexQueries, getCreateTableQuery } from '../queries.js'; -import { DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; -import type { DBTables } from '../types.js'; -import { type VitePlugin, getDbDirectoryUrl, getRemoteDatabaseUrl, getAstroEnv } from '../utils.js'; -import { createLocalDatabaseClient } from '../../runtime/db-client.js'; import { type SQL, sql } from 'drizzle-orm'; -import { existsSync } from 'node:fs'; +import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; +import { createLocalDatabaseClient } from '../../runtime/db-client.js'; import { normalizeDatabaseUrl } from '../../runtime/index.js'; +import { DB_PATH, RUNTIME_IMPORT, RUNTIME_VIRTUAL_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; import { getResolvedFileUrl } from '../load-file.js'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; +import { SEED_DEV_FILE_NAME, getCreateIndexQueries, getCreateTableQuery } from '../queries.js'; +import type { DBTables } from '../types.js'; +import { type VitePlugin, getAstroEnv, getDbDirectoryUrl, getRemoteDatabaseUrl } from '../utils.js'; export const resolved = { module: '\0' + VIRTUAL_MODULE_ID, diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts index ff4d56a5d124..200476c14843 100644 --- a/packages/db/src/core/queries.ts +++ b/packages/db/src/core/queries.ts @@ -1,6 +1,14 @@ import { type SQL } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { bold } from 'kleur/colors'; +import { + FOREIGN_KEY_DNE_ERROR, + FOREIGN_KEY_REFERENCES_EMPTY_ERROR, + FOREIGN_KEY_REFERENCES_LENGTH_ERROR, + REFERENCE_DNE_ERROR, +} from '../runtime/errors.js'; +import { hasPrimaryKey } from '../runtime/index.js'; +import { isSerializedSQL } from '../runtime/types.js'; import type { BooleanColumn, ColumnType, @@ -11,14 +19,6 @@ import type { NumberColumn, TextColumn, } from './types.js'; -import { - FOREIGN_KEY_DNE_ERROR, - FOREIGN_KEY_REFERENCES_EMPTY_ERROR, - FOREIGN_KEY_REFERENCES_LENGTH_ERROR, - REFERENCE_DNE_ERROR, -} from '../runtime/errors.js'; -import { hasPrimaryKey } from '../runtime/index.js'; -import { isSerializedSQL } from '../runtime/types.js'; const sqlite = new SQLiteAsyncDialect(); diff --git a/packages/db/test/local-prod.test.js b/packages/db/test/local-prod.test.js index ff3b2c633ca5..4796bfc9beb4 100644 --- a/packages/db/test/local-prod.test.js +++ b/packages/db/test/local-prod.test.js @@ -1,8 +1,8 @@ +import { relative } from 'path'; import { fileURLToPath } from 'url'; import { expect } from 'chai'; import testAdapter from '../../astro/test/test-adapter.js'; import { loadFixture } from '../../astro/test/test-utils.js'; -import { relative } from 'path'; describe('astro:db local database', () => { let fixture; From a37d76a42ac00697be3acd575f3f7163129ea75c Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 3 May 2024 17:40:53 +0200 Subject: [PATCH 040/151] Add web-vitals integration (#10883) --- .changeset/great-swans-punch.md | 5 + packages/integrations/web-vitals/README.md | 59 +++++++++ packages/integrations/web-vitals/package.json | 49 ++++++++ .../web-vitals/src/client-script.ts | 36 ++++++ .../integrations/web-vitals/src/constants.ts | 1 + .../integrations/web-vitals/src/db-config.ts | 22 ++++ .../integrations/web-vitals/src/endpoint.ts | 23 ++++ packages/integrations/web-vitals/src/env.d.ts | 1 + packages/integrations/web-vitals/src/index.ts | 42 +++++++ .../integrations/web-vitals/src/middleware.ts | 60 +++++++++ .../integrations/web-vitals/src/schemas.ts | 32 +++++ .../web-vitals/test/basics.test.js | 118 ++++++++++++++++++ .../test/fixtures/basics/astro.config.mjs | 14 +++ .../test/fixtures/basics/package.json | 16 +++ .../fixtures/basics/src/pages/[dynamic].astro | 19 +++ .../fixtures/basics/src/pages/index.astro | 11 ++ .../web-vitals/test/test-utils.js | 16 +++ .../integrations/web-vitals/tsconfig.json | 7 ++ pnpm-lock.yaml | 34 +++++ 19 files changed, 565 insertions(+) create mode 100644 .changeset/great-swans-punch.md create mode 100644 packages/integrations/web-vitals/README.md create mode 100644 packages/integrations/web-vitals/package.json create mode 100644 packages/integrations/web-vitals/src/client-script.ts create mode 100644 packages/integrations/web-vitals/src/constants.ts create mode 100644 packages/integrations/web-vitals/src/db-config.ts create mode 100644 packages/integrations/web-vitals/src/endpoint.ts create mode 100644 packages/integrations/web-vitals/src/env.d.ts create mode 100644 packages/integrations/web-vitals/src/index.ts create mode 100644 packages/integrations/web-vitals/src/middleware.ts create mode 100644 packages/integrations/web-vitals/src/schemas.ts create mode 100644 packages/integrations/web-vitals/test/basics.test.js create mode 100644 packages/integrations/web-vitals/test/fixtures/basics/astro.config.mjs create mode 100644 packages/integrations/web-vitals/test/fixtures/basics/package.json create mode 100644 packages/integrations/web-vitals/test/fixtures/basics/src/pages/[dynamic].astro create mode 100644 packages/integrations/web-vitals/test/fixtures/basics/src/pages/index.astro create mode 100644 packages/integrations/web-vitals/test/test-utils.js create mode 100644 packages/integrations/web-vitals/tsconfig.json diff --git a/.changeset/great-swans-punch.md b/.changeset/great-swans-punch.md new file mode 100644 index 000000000000..60725a224ce0 --- /dev/null +++ b/.changeset/great-swans-punch.md @@ -0,0 +1,5 @@ +--- +"@astrojs/web-vitals": minor +--- + +Adds a new web-vitals integration powered by Astro DB diff --git a/packages/integrations/web-vitals/README.md b/packages/integrations/web-vitals/README.md new file mode 100644 index 000000000000..3a4c5fb0141d --- /dev/null +++ b/packages/integrations/web-vitals/README.md @@ -0,0 +1,59 @@ +# @astrojs/web-vitals (experimental) ⏱️ + +This **[Astro integration][astro-integration]** enables tracking real-world website performance and storing the data in [Astro DB][db]. + +## Pre-requisites + +- [Astro DB](https://astro.build/db) — `@astrojs/web-vitals` will store performance data in Astro DB in production +- [An SSR adapter](https://docs.astro.build/en/guides/server-side-rendering/) — `@astrojs/web-vitals` injects a server endpoint to manage saving data to Astro DB + +## Installation + +1. Install and configure the Web Vitals integration using `astro add`: + + ```sh + npx astro add web-vitals + ``` + +2. Push the tables added by the Web Vitals integration to Astro Studio: + + ```sh + npx astro db push + ``` + +3. Redeploy your site. + +4. Visit your project dashboard at https://studio.astro.build to see the data collected. + +Learn more about [Astro DB](https://docs.astro.build/en/guides/astro-db/) and [deploying with Astro Studio](https://docs.astro.build/en/guides/astro-db/#astro-studio) in the Astro docs. + +## Support + +- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! + +- Check our [Astro Integration Documentation][astro-integration] for more on integrations. + +- Submit bug reports and feature requests as [GitHub issues][issues]. + +## Contributing + +This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started: + +- [Contributor Manual][contributing] +- [Code of Conduct][coc] +- [Community Guide][community] + +## License + +MIT + +Copyright (c) 2023–present [Astro][astro] + +[astro]: https://astro.build/ +[db]: https://astro.build/db/ +[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md +[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md +[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md +[discord]: https://astro.build/chat/ +[issues]: https://github.com/withastro/astro/issues +[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ diff --git a/packages/integrations/web-vitals/package.json b/packages/integrations/web-vitals/package.json new file mode 100644 index 000000000000..1719f423ee0f --- /dev/null +++ b/packages/integrations/web-vitals/package.json @@ -0,0 +1,49 @@ +{ + "name": "@astrojs/web-vitals", + "description": "Track your website’s performance with Astro DB", + "version": "0.0.0", + "type": "module", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/astro.git", + "directory": "packages/integrations/web-vitals" + }, + "keywords": [ + "withastro", + "astro-integration" + ], + "bugs": "https://github.com/withastro/astro/issues", + "exports": { + ".": "./dist/index.js", + "./middleware": "./dist/middleware.js", + "./endpoint": "./dist/endpoint.js", + "./client-script": "./dist/client-script.js", + "./db-config": "./dist/db-config.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "astro-scripts build \"src/**/*.ts\" && tsc", + "build:ci": "astro-scripts build \"src/**/*.ts\"", + "dev": "astro-scripts dev \"src/**/*.ts\"", + "test": "astro-scripts test --timeout 50000 \"test/**/*.test.js\"" + }, + "dependencies": { + "web-vitals": "^3.5.2" + }, + "peerDependencies": { + "@astrojs/db": "^0.11.0" + }, + "devDependencies": { + "@astrojs/db": "workspace:*", + "astro": "workspace:*", + "astro-scripts": "workspace:*", + "linkedom": "^0.16.11" + }, + "publishConfig": { + "provenance": true + } +} diff --git a/packages/integrations/web-vitals/src/client-script.ts b/packages/integrations/web-vitals/src/client-script.ts new file mode 100644 index 000000000000..b69fa6772eb9 --- /dev/null +++ b/packages/integrations/web-vitals/src/client-script.ts @@ -0,0 +1,36 @@ +import { type Metric, onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals'; +import { WEB_VITALS_ENDPOINT_PATH } from './constants.js'; +import type { ClientMetric } from './schemas.js'; + +const pathname = location.pathname.replace(/(?<=.)\/$/, ''); +const route = + document + .querySelector ('meta[name="x-astro-vitals-route"]') + ?.getAttribute('content') || pathname; + +const queue = new Set (); +const addToQueue = (metric: Metric) => queue.add(metric); +function flushQueue() { + if (!queue.size) return; + const rawBody: ClientMetric[] = [...queue].map(({ name, id, value, rating }) => ({ + pathname, + route, + name, + id, + value, + rating, + })); + const body = JSON.stringify(rawBody); + if (navigator.sendBeacon) navigator.sendBeacon(WEB_VITALS_ENDPOINT_PATH, body); + else fetch(WEB_VITALS_ENDPOINT_PATH, { body, method: 'POST', keepalive: true }); + queue.clear(); +} + +for (const listener of [onCLS, onLCP, onINP, onFID, onFCP, onTTFB]) { + listener(addToQueue); +} + +addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') flushQueue(); +}); +addEventListener('pagehide', flushQueue); diff --git a/packages/integrations/web-vitals/src/constants.ts b/packages/integrations/web-vitals/src/constants.ts new file mode 100644 index 000000000000..7df5bb8b6572 --- /dev/null +++ b/packages/integrations/web-vitals/src/constants.ts @@ -0,0 +1 @@ +export const WEB_VITALS_ENDPOINT_PATH = '/_web-vitals' diff --git a/packages/integrations/web-vitals/src/db-config.ts b/packages/integrations/web-vitals/src/db-config.ts new file mode 100644 index 000000000000..918850f63580 --- /dev/null +++ b/packages/integrations/web-vitals/src/db-config.ts @@ -0,0 +1,22 @@ +import { column, defineDb, defineTable } from 'astro:db'; +import { asDrizzleTable } from '@astrojs/db/utils'; + +const Metric = defineTable({ + columns: { + pathname: column.text(), + route: column.text(), + name: column.text(), + id: column.text({ primaryKey: true }), + value: column.number(), + rating: column.text(), + timestamp: column.date(), + }, +}); + +export const AstrojsWebVitals_Metric = asDrizzleTable('AstrojsWebVitals_Metric', Metric); + +export default defineDb({ + tables: { + AstrojsWebVitals_Metric: Metric, + }, +}); diff --git a/packages/integrations/web-vitals/src/endpoint.ts b/packages/integrations/web-vitals/src/endpoint.ts new file mode 100644 index 000000000000..10dea1ca88aa --- /dev/null +++ b/packages/integrations/web-vitals/src/endpoint.ts @@ -0,0 +1,23 @@ +import { db, sql } from 'astro:db'; +import type { APIRoute } from 'astro'; +import { AstrojsWebVitals_Metric } from './db-config.js'; +import { ServerMetricSchema } from './schemas.js'; + +export const prerender = false; + +export const ALL: APIRoute = async ({ request }) => { + try { + const rawBody = await request.json(); + const body = ServerMetricSchema.array().parse(rawBody); + await db + .insert(AstrojsWebVitals_Metric) + .values(body) + .onConflictDoUpdate({ + target: AstrojsWebVitals_Metric.id, + set: { value: sql`excluded.value` }, + }); + } catch (error) { + console.error(error); + } + return new Response(); +}; diff --git a/packages/integrations/web-vitals/src/env.d.ts b/packages/integrations/web-vitals/src/env.d.ts new file mode 100644 index 000000000000..18ef3554e128 --- /dev/null +++ b/packages/integrations/web-vitals/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/integrations/web-vitals/src/index.ts b/packages/integrations/web-vitals/src/index.ts new file mode 100644 index 000000000000..f8a5ad433496 --- /dev/null +++ b/packages/integrations/web-vitals/src/index.ts @@ -0,0 +1,42 @@ +import { defineDbIntegration } from '@astrojs/db/utils'; +import { AstroError } from 'astro/errors'; +import { WEB_VITALS_ENDPOINT_PATH } from './constants.js'; + +export default function webVitals() { + return defineDbIntegration({ + name: '@astrojs/web-vitals', + hooks: { + 'astro:db:setup'({ extendDb }) { + extendDb({ configEntrypoint: '@astrojs/web-vitals/db-config' }); + }, + + 'astro:config:setup'({ addMiddleware, config, injectRoute, injectScript }) { + if (!config.integrations.find(({ name }) => name === 'astro:db')) { + throw new AstroError( + 'Astro DB integration not found.', + 'Run `npx astro add db` to install `@astrojs/db` and add it to your Astro config.' + ); + } + + if (config.output !== 'hybrid' && config.output !== 'server') { + throw new AstroError( + 'No SSR adapter found.', + '`@astrojs/web-vitals` requires your site to be built with `hybrid` or `server` output.\n' + + 'Please add an SSR adapter: https://docs.astro.build/en/guides/server-side-rendering/' + ); + } + + // Middleware that adds a `` tag to each page. + addMiddleware({ entrypoint: '@astrojs/web-vitals/middleware', order: 'post' }); + // Endpoint that collects metrics and inserts them in Astro DB. + injectRoute({ + entrypoint: '@astrojs/web-vitals/endpoint', + pattern: WEB_VITALS_ENDPOINT_PATH, + prerender: false, + }); + // Client-side performance measurement script. + injectScript('page', `import '@astrojs/web-vitals/client-script';`); + }, + }, + }); +} diff --git a/packages/integrations/web-vitals/src/middleware.ts b/packages/integrations/web-vitals/src/middleware.ts new file mode 100644 index 000000000000..b4994c902c1c --- /dev/null +++ b/packages/integrations/web-vitals/src/middleware.ts @@ -0,0 +1,60 @@ +import type { MiddlewareHandler } from 'astro'; + +/** + * Middleware which adds the web vitals `` tag to each page’s ``. + * + * @example + * + */ +export const onRequest: MiddlewareHandler = async ({ params, url }, next) => { + const response = await next(); + const contentType = response.headers.get('Content-Type'); + if (contentType !== 'text/html') return response; + const webVitalsMetaTag = getMetaTag(url, params); + return new Response( + response.body + ?.pipeThrough(new TextDecoderStream()) + .pipeThrough(HeadInjectionTransformStream(webVitalsMetaTag)) + .pipeThrough(new TextEncoderStream()), + response + ); +}; + +/** TransformStream which injects the passed HTML just before the closing tag. */ +function HeadInjectionTransformStream(htmlToInject: string) { + let hasInjected = false; + return new TransformStream({ + transform: (chunk, controller) => { + if (!hasInjected) { + const headCloseIndex = chunk.indexOf(''); + if (headCloseIndex > -1) { + chunk = chunk.slice(0, headCloseIndex) + htmlToInject + chunk.slice(headCloseIndex); + hasInjected = true; + } + } + controller.enqueue(chunk); + }, + }); +} + +/** Get a `` tag to identify the current Astro route. */ +function getMetaTag(url: URL, params: Record ) { + let route = url.pathname; + for (const [key, value] of Object.entries(params)) { + if (value) route = route.replace(value, `[${key}]`); + } + route = miniEncodeAttribute(stripTrailingSlash(route)); + return ``; +} + +function stripTrailingSlash(str: string) { + return str.length > 1 && str.at(-1) === '/' ? str.slice(0, -1) : str; +} + +function miniEncodeAttribute(str: string) { + return str + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); +} diff --git a/packages/integrations/web-vitals/src/schemas.ts b/packages/integrations/web-vitals/src/schemas.ts new file mode 100644 index 000000000000..7a2050bd53ae --- /dev/null +++ b/packages/integrations/web-vitals/src/schemas.ts @@ -0,0 +1,32 @@ +import { z } from 'astro/zod'; + +export const RatingSchema = z.enum(['good', 'needs-improvement', 'poor']); +const MetricTypeSchema = z.enum(['CLS', 'INP', 'LCP', 'FCP', 'FID', 'TTFB']); + +/** `web-vitals` generated ID, transformed to reduce data resolution. */ +const MetricIdSchema = z + .string() + // Match https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/generateUniqueID.ts + .regex(/^v3-\d{13}-\d{13}$/) + // Avoid collecting higher resolution timestamp in ID. + // Transforms `'v3-1711484350895-3748043125387'` to `'v3-17114843-3748043125387'` + .transform((id) => id.replace(/^(v3-\d{8})\d{5}(-\d{13})$/, '$1$2')); + +/** Shape of the data submitted from clients to the collection API. */ +const ClientMetricSchema = z.object({ + pathname: z.string(), + route: z.string(), + name: MetricTypeSchema, + id: MetricIdSchema, + value: z.number().gte(0), + rating: RatingSchema, +}); + +/** Transformed client data with added timestamp. */ +export const ServerMetricSchema = ClientMetricSchema.transform((metric) => { + const timestamp = new Date(); + timestamp.setMinutes(0, 0, 0); + return { ...metric, timestamp }; +}); + +export type ClientMetric = z.input ; diff --git a/packages/integrations/web-vitals/test/basics.test.js b/packages/integrations/web-vitals/test/basics.test.js new file mode 100644 index 000000000000..937619b483d4 --- /dev/null +++ b/packages/integrations/web-vitals/test/basics.test.js @@ -0,0 +1,118 @@ +// @ts-check + +import * as assert from 'node:assert/strict'; +import { after, before, beforeEach, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from './test-utils.js'; + +/** + * @template {Record void>} T + * @template {keyof T} K + */ +class MockFunction { + /** @type {Parameters []} */ + calls = []; + + /** + * @param {T} object + * @param {K} property + */ + constructor(object, property) { + this.object = object; + this.property = property; + this.original = object[property]; + object[property] = /** @param {Parameters } args */ (...args) => { + this.calls.push(args); + }; + } + restore() { + this.object[this.property] = this.original; + } + reset() { + this.calls = []; + } +} + +describe('Web Vitals integration basics', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + /** @type {import('./test-utils').DevServer} */ + let devServer; + /** @type {MockFunction } */ + let consoleErrorMock; + + before(async () => { + consoleErrorMock = new MockFunction(console, 'error'); + fixture = await loadFixture({ root: './fixtures/basics/' }); + devServer = await fixture.startDevServer({}); + }); + + after(async () => { + consoleErrorMock.restore(); + await devServer.stop(); + }); + + beforeEach(() => { + consoleErrorMock.reset(); + }); + + it('adds a meta tag to the page', async () => { + const html = await fixture.fetch('/', {}).then((res) => res.text()); + const { document } = parseHTML(html); + const meta = document.querySelector('head > meta[name="x-astro-vitals-route"]'); + assert.ok(meta); + assert.equal(meta.getAttribute('content'), '/'); + }); + + it('adds a meta tag using the route pattern to the page', async () => { + const html = await fixture.fetch('/test', {}).then((res) => res.text()); + const { document } = parseHTML(html); + const meta = document.querySelector('head > meta[name="x-astro-vitals-route"]'); + assert.ok(meta); + assert.equal(meta.getAttribute('content'), '/[dynamic]'); + }); + + it('returns a 200 response even when bad data is sent to the injected endpoint', async () => { + { + // bad data + const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: 'garbage' }); + assert.equal(res.status, 200); + } + { + // no data + const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: '[]' }); + assert.equal(res.status, 200); + } + assert.equal(consoleErrorMock.calls.length, 2); + }); + + it('validates data sent to the injected endpoint with Zod', async () => { + const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: '[{}]' }); + assert.equal(res.status, 200); + const call = consoleErrorMock.calls[0][0]; + assert.ok(call instanceof Error); + assert.equal(call.name, 'ZodError'); + }); + + it('inserts data via the injected endpoint', async () => { + const res = await fixture.fetch('/_web-vitals', { + method: 'POST', + body: JSON.stringify([ + { + pathname: '/', + route: '/', + name: 'CLS', + id: 'v3-1711484350895-3748043125387', + value: 0, + rating: 'good', + }, + ]), + }); + assert.equal(res.status, 200); + assert.equal( + consoleErrorMock.calls.length, + 0, + 'Endpoint logged errors:\n' + consoleErrorMock.calls[0]?.join(' ') + ); + }); +}); diff --git a/packages/integrations/web-vitals/test/fixtures/basics/astro.config.mjs b/packages/integrations/web-vitals/test/fixtures/basics/astro.config.mjs new file mode 100644 index 000000000000..42bfa6f6693f --- /dev/null +++ b/packages/integrations/web-vitals/test/fixtures/basics/astro.config.mjs @@ -0,0 +1,14 @@ +import db from '@astrojs/db'; +import node from '@astrojs/node'; +import webVitals from '@astrojs/web-vitals'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [db(), webVitals()], + output: 'hybrid', + adapter: node({ mode: 'standalone' }), + devToolbar: { + enabled: false, + }, +}); diff --git a/packages/integrations/web-vitals/test/fixtures/basics/package.json b/packages/integrations/web-vitals/test/fixtures/basics/package.json new file mode 100644 index 000000000000..25ab0abc1b75 --- /dev/null +++ b/packages/integrations/web-vitals/test/fixtures/basics/package.json @@ -0,0 +1,16 @@ +{ + "name": "@test/web-vitals", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/db": "workspace:*", + "@astrojs/node": "workspace:*", + "@astrojs/web-vitals": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/web-vitals/test/fixtures/basics/src/pages/[dynamic].astro b/packages/integrations/web-vitals/test/fixtures/basics/src/pages/[dynamic].astro new file mode 100644 index 000000000000..36c7c50e6276 --- /dev/null +++ b/packages/integrations/web-vitals/test/fixtures/basics/src/pages/[dynamic].astro @@ -0,0 +1,19 @@ +--- +import type { GetStaticPaths } from "astro"; +export const getStaticPaths = (() => { + return [{ params: { dynamic: 'test' } }]; +}) satisfies GetStaticPaths; +--- + + + + + + + Web Vitals basics — dynamic route test + + +Web Vitals basics
+Dynamic route test
+ + diff --git a/packages/integrations/web-vitals/test/fixtures/basics/src/pages/index.astro b/packages/integrations/web-vitals/test/fixtures/basics/src/pages/index.astro new file mode 100644 index 000000000000..06ddd6565db9 --- /dev/null +++ b/packages/integrations/web-vitals/test/fixtures/basics/src/pages/index.astro @@ -0,0 +1,11 @@ + + + + + +Web Vitals basics test + + +Web Vitals basics test
+ + diff --git a/packages/integrations/web-vitals/test/test-utils.js b/packages/integrations/web-vitals/test/test-utils.js new file mode 100644 index 000000000000..8dd4d970bdd5 --- /dev/null +++ b/packages/integrations/web-vitals/test/test-utils.js @@ -0,0 +1,16 @@ +import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; + +/** @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ +/** @typedef {import('../../../astro/test/test-utils').DevServer} DevServer */ + +/** @type {typeof import('../../../astro/test/test-utils.js')['loadFixture']} */ +export function loadFixture(inlineConfig) { + if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); + + // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath + // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` + return baseLoadFixture({ + ...inlineConfig, + root: new URL(inlineConfig.root, import.meta.url).toString(), + }); +} diff --git a/packages/integrations/web-vitals/tsconfig.json b/packages/integrations/web-vitals/tsconfig.json new file mode 100644 index 000000000000..1504b4b6dfa4 --- /dev/null +++ b/packages/integrations/web-vitals/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb460a49c147..9e7fd73d2047 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5327,6 +5327,40 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/web-vitals: + dependencies: + web-vitals: + specifier: ^3.5.2 + version: 3.5.2 + devDependencies: + '@astrojs/db': + specifier: workspace:* + version: link:../../db + astro: + specifier: workspace:* + version: link:../../astro + astro-scripts: + specifier: workspace:* + version: link:../../../scripts + linkedom: + specifier: ^0.16.11 + version: 0.16.11 + + packages/integrations/web-vitals/test/fixtures/basics: + dependencies: + '@astrojs/db': + specifier: workspace:* + version: link:../../../../../db + '@astrojs/node': + specifier: workspace:* + version: link:../../../../node + '@astrojs/web-vitals': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/internal-helpers: devDependencies: astro-scripts: From 6c603bcc97e70dc03f137d16f714f36421191a85 Mon Sep 17 00:00:00 2001 From: Chris SwithinbankDate: Fri, 3 May 2024 15:41:58 +0000 Subject: [PATCH 041/151] [ci] format --- packages/integrations/web-vitals/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/web-vitals/src/constants.ts b/packages/integrations/web-vitals/src/constants.ts index 7df5bb8b6572..a76b42a2333c 100644 --- a/packages/integrations/web-vitals/src/constants.ts +++ b/packages/integrations/web-vitals/src/constants.ts @@ -1 +1 @@ -export const WEB_VITALS_ENDPOINT_PATH = '/_web-vitals' +export const WEB_VITALS_ENDPOINT_PATH = '/_web-vitals'; From 2dcbcdb8570d9c943ed2f2b4500bcc24998a25f7 Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Fri, 3 May 2024 08:55:43 -0700 Subject: [PATCH 042/151] [ci] release (#10943) Co-authored-by: github-actions[bot] --- .changeset/great-swans-punch.md | 5 ----- .changeset/young-pots-brake.md | 6 ------ packages/db/CHANGELOG.md | 7 +++++++ packages/db/package.json | 2 +- packages/integrations/web-vitals/CHANGELOG.md | 7 +++++++ packages/integrations/web-vitals/package.json | 16 ++++++++-------- 6 files changed, 23 insertions(+), 20 deletions(-) delete mode 100644 .changeset/great-swans-punch.md delete mode 100644 .changeset/young-pots-brake.md create mode 100644 packages/integrations/web-vitals/CHANGELOG.md diff --git a/.changeset/great-swans-punch.md b/.changeset/great-swans-punch.md deleted file mode 100644 index 60725a224ce0..000000000000 --- a/.changeset/great-swans-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/web-vitals": minor ---- - -Adds a new web-vitals integration powered by Astro DB diff --git a/.changeset/young-pots-brake.md b/.changeset/young-pots-brake.md deleted file mode 100644 index 33b5e7fd68be..000000000000 --- a/.changeset/young-pots-brake.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@astrojs/db": minor ---- - -- Fix duplicate table recreations when you start your dev server. -- Remove eager re-seeding when updating your seed file in development. Seeding still runs on dev server startup for SQLite inspector tools. diff --git a/packages/db/CHANGELOG.md b/packages/db/CHANGELOG.md index 8193ebc83547..177c5380e3cb 100644 --- a/packages/db/CHANGELOG.md +++ b/packages/db/CHANGELOG.md @@ -1,5 +1,12 @@ # @astrojs/db +## 0.11.0 + +### Minor Changes + +- [#10919](https://github.com/withastro/astro/pull/10919) [`44bafa9`](https://github.com/withastro/astro/commit/44bafa989af0cc380696bb6381048fc1ee55dd5b) Thanks [@bholmesdev](https://github.com/bholmesdev)! - - Fix duplicate table recreations when you start your dev server. + - Remove eager re-seeding when updating your seed file in development. Seeding still runs on dev server startup for SQLite inspector tools. + ## 0.10.7 ### Patch Changes diff --git a/packages/db/package.json b/packages/db/package.json index 24d7f350287a..0604ac93474e 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/db", - "version": "0.10.7", + "version": "0.11.0", "description": "Add libSQL and Astro Studio support to your Astro site", "license": "MIT", "repository": { diff --git a/packages/integrations/web-vitals/CHANGELOG.md b/packages/integrations/web-vitals/CHANGELOG.md new file mode 100644 index 000000000000..23945307b9a7 --- /dev/null +++ b/packages/integrations/web-vitals/CHANGELOG.md @@ -0,0 +1,7 @@ +# @astrojs/web-vitals + +## 0.1.0 + +### Minor Changes + +- [#10883](https://github.com/withastro/astro/pull/10883) [`a37d76a`](https://github.com/withastro/astro/commit/a37d76a42ac00697be3acd575f3f7163129ea75c) Thanks [@delucis](https://github.com/delucis)! - Adds a new web-vitals integration powered by Astro DB diff --git a/packages/integrations/web-vitals/package.json b/packages/integrations/web-vitals/package.json index 1719f423ee0f..dde7c502b011 100644 --- a/packages/integrations/web-vitals/package.json +++ b/packages/integrations/web-vitals/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/web-vitals", "description": "Track your website’s performance with Astro DB", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "author": "withastro", "license": "MIT", @@ -31,18 +31,18 @@ "dev": "astro-scripts dev \"src/**/*.ts\"", "test": "astro-scripts test --timeout 50000 \"test/**/*.test.js\"" }, - "dependencies": { - "web-vitals": "^3.5.2" - }, + "dependencies": { + "web-vitals": "^3.5.2" + }, "peerDependencies": { "@astrojs/db": "^0.11.0" }, - "devDependencies": { - "@astrojs/db": "workspace:*", - "astro": "workspace:*", + "devDependencies": { + "@astrojs/db": "workspace:*", + "astro": "workspace:*", "astro-scripts": "workspace:*", "linkedom": "^0.16.11" - }, + }, "publishConfig": { "provenance": true } From 3412535be4a0ec94cea18c5d186b7ffbd6f8209c Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Fri, 3 May 2024 21:01:25 +0200 Subject: [PATCH 043/151] fix: don't include port twice from x-forwarded-host and x-forwarded-port headers (#10917) * fix: don't include port twice from x-forwarded-host and x-forwarded-port headers * add changeset * add test for port both in forwarded host and forwarded port * don't include port if undefined * Update .changeset/forty-wolves-turn.md Co-authored-by: Florian Lefebvre --------- Co-authored-by: Florian Lefebvre --- .changeset/forty-wolves-turn.md | 5 +++++ packages/astro/src/core/app/node.ts | 7 ++++++- packages/integrations/node/test/url.test.js | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/forty-wolves-turn.md diff --git a/.changeset/forty-wolves-turn.md b/.changeset/forty-wolves-turn.md new file mode 100644 index 000000000000..87f6e50226c5 --- /dev/null +++ b/.changeset/forty-wolves-turn.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes a case where the local server would crash when the host also contained the port, eg. with `X-Forwarded-Host: hostname:8080` and `X-Forwarded-Port: 8080` headers diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index b90a9ec8e789..c28fa8754d7c 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -66,7 +66,12 @@ export class NodeApp extends App { const hostname = req.headers['x-forwarded-host'] ?? req.headers.host ?? req.headers[':authority']; const port = req.headers['x-forwarded-port']; - const url = `${protocol}://${hostname}${port ? `:${port}` : ''}${req.url}`; + + const portInHostname = + typeof hostname === 'string' && typeof port === 'string' && hostname.endsWith(port); + const hostnamePort = portInHostname ? hostname : hostname + (port ? `:${port}` : ''); + + const url = `${protocol}://${hostnamePort}${req.url}`; const options: RequestInit = { method: req.method || 'GET', headers: makeRequestHeaders(req), diff --git a/packages/integrations/node/test/url.test.js b/packages/integrations/node/test/url.test.js index 6fc008236613..77ca45836638 100644 --- a/packages/integrations/node/test/url.test.js +++ b/packages/integrations/node/test/url.test.js @@ -92,4 +92,24 @@ describe('URL', () => { assert.equal($('body').text(), 'https://abc.xyz:444/'); }); + + it('accepts port in forwarded host and forwarded port', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + let { req, res, text } = createRequestAndResponse({ + headers: { + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'abc.xyz:444', + 'X-Forwarded-Port': '444', + }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + const $ = cheerio.load(html); + + assert.equal($('body').text(), 'https://abc.xyz:444/'); + }); }); From fd508a0fbb5148aafc180f1b14d3e47974777248 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Fri, 3 May 2024 21:21:37 +0200 Subject: [PATCH 044/151] feat(preact): add support for devtools (#10938) * feat(preact): add support for devtools * Update little-dryers-stare.md --- .changeset/little-dryers-stare.md | 18 ++++++++++++++++++ packages/integrations/preact/src/index.ts | 13 ++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 .changeset/little-dryers-stare.md diff --git a/.changeset/little-dryers-stare.md b/.changeset/little-dryers-stare.md new file mode 100644 index 000000000000..c1880d311ffb --- /dev/null +++ b/.changeset/little-dryers-stare.md @@ -0,0 +1,18 @@ +--- +"@astrojs/preact": minor +--- + +Adds a `devtools` option + +You can enable [Preact devtools](https://preactjs.github.io/preact-devtools/) in development by setting `devtools: true` in your `preact()` integration config: + +```js +import { defineConfig } from "astro/config" +import preact from "@astrojs/preact" + +export default defineConfig({ + integrations: [ + preact({ devtools: true }) + ] +}) +``` diff --git a/packages/integrations/preact/src/index.ts b/packages/integrations/preact/src/index.ts index 8de24d56548a..bcca01dd0b33 100644 --- a/packages/integrations/preact/src/index.ts +++ b/packages/integrations/preact/src/index.ts @@ -12,13 +12,16 @@ function getRenderer(development: boolean): AstroRenderer { }; } -export type Options = Pick & { compat?: boolean }; +export interface Options extends Pick { + compat?: boolean; + devtools?: boolean; +} -export default function ({ include, exclude, compat }: Options = {}): AstroIntegration { +export default function ({ include, exclude, compat, devtools }: Options = {}): AstroIntegration { return { name: '@astrojs/preact', hooks: { - 'astro:config:setup': ({ addRenderer, updateConfig, command }) => { + 'astro:config:setup': ({ addRenderer, updateConfig, command, injectScript }) => { const preactPlugin = preact({ reactAliasesEnabled: compat ?? false, include, @@ -56,6 +59,10 @@ export default function ({ include, exclude, compat }: Options = {}): AstroInteg updateConfig({ vite: viteConfig, }); + + if (command === 'dev' && devtools) { + injectScript('page', 'import "preact/debug";'); + } }, }, }; From 11c58a9c5a33a44756435e2acede68cb274fb05b Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Mon, 6 May 2024 15:55:37 +0200 Subject: [PATCH 045/151] Adds missing readonly (#10956) --- packages/astro/src/transitions/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/transitions/events.ts b/packages/astro/src/transitions/events.ts index 969a12139943..a35fd72cb6f3 100644 --- a/packages/astro/src/transitions/events.ts +++ b/packages/astro/src/transitions/events.ts @@ -25,7 +25,7 @@ class BeforeEvent extends Event { readonly sourceElement: Element | undefined; readonly info: any; newDocument: Document; - signal: AbortSignal; + readonly signal: AbortSignal; constructor( type: string, From 082abb82d574a200f9168ee5ae92c65c71e29eda Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Mon, 6 May 2024 16:02:06 +0200 Subject: [PATCH 046/151] feat(vue): add support for devtools (#10929) Co-authored-by: Sarah Rainsberger --- .changeset/thin-rabbits-wait.md | 18 ++ packages/integrations/vue/package.json | 3 +- packages/integrations/vue/src/index.ts | 35 ++-- pnpm-lock.yaml | 243 ++++++++++++++++++++++++- 4 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 .changeset/thin-rabbits-wait.md diff --git a/.changeset/thin-rabbits-wait.md b/.changeset/thin-rabbits-wait.md new file mode 100644 index 000000000000..1556e0147fc9 --- /dev/null +++ b/.changeset/thin-rabbits-wait.md @@ -0,0 +1,18 @@ +--- +"@astrojs/vue": minor +--- + +Adds a `devtools` option + +You can enable the [official Vue DevTools](https://devtools-next.vuejs.org/) while working in development mode by setting `devtools:true` in your `vue()` integration config: + +```js +import { defineConfig } from "astro/config" +import vue from "@astrojs/vue" + +export default defineConfig({ + integrations: [ + vue({ devtools: true }) + ] +}) +``` \ No newline at end of file diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json index 102d102ba5db..5fcf280a49b4 100644 --- a/packages/integrations/vue/package.json +++ b/packages/integrations/vue/package.json @@ -42,7 +42,8 @@ "dependencies": { "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", - "@vue/compiler-sfc": "^3.4.26" + "@vue/compiler-sfc": "^3.4.26", + "vite-plugin-vue-devtools": "^7.1.3" }, "devDependencies": { "astro": "workspace:*", diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts index 803095c19c4b..f61b35a09d6b 100644 --- a/packages/integrations/vue/src/index.ts +++ b/packages/integrations/vue/src/index.ts @@ -3,12 +3,16 @@ import type { Options as VueOptions } from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue'; import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx'; import { MagicString } from '@vue/compiler-sfc'; -import type { AstroIntegration, AstroRenderer } from 'astro'; +import type { AstroIntegration, AstroRenderer, HookParameters } from 'astro'; import type { Plugin, UserConfig } from 'vite'; +const VIRTUAL_MODULE_ID = 'virtual:@astrojs/vue/app'; +const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`; + interface Options extends VueOptions { jsx?: boolean | VueJsxOptions; appEntrypoint?: string; + devtools?: boolean; } function getRenderer(): AstroRenderer { @@ -28,9 +32,6 @@ function getJsxRenderer(): AstroRenderer { } function virtualAppEntrypoint(options?: Options): Plugin { - const virtualModuleId = 'virtual:@astrojs/vue/app'; - const resolvedVirtualModuleId = '\0' + virtualModuleId; - let isBuild: boolean; let root: string; let appEntrypoint: string | undefined; @@ -49,12 +50,12 @@ function virtualAppEntrypoint(options?: Options): Plugin { } }, resolveId(id: string) { - if (id == virtualModuleId) { - return resolvedVirtualModuleId; + if (id == VIRTUAL_MODULE_ID) { + return RESOLVED_VIRTUAL_MODULE_ID; } }, load(id: string) { - if (id === resolvedVirtualModuleId) { + if (id === RESOLVED_VIRTUAL_MODULE_ID) { if (appEntrypoint) { return `\ import * as mod from ${JSON.stringify(appEntrypoint)}; @@ -93,7 +94,10 @@ export const setup = async (app) => { }; } -async function getViteConfiguration(options?: Options): Promise { +async function getViteConfiguration( + command: HookParameters<'astro:config:setup'>['command'], + options?: Options +): Promise { let vueOptions = { ...options, template: { @@ -105,7 +109,7 @@ async function getViteConfiguration(options?: Options): Promise { const config: UserConfig = { optimizeDeps: { include: ['@astrojs/vue/client.js', 'vue'], - exclude: ['@astrojs/vue/server.js', 'virtual:@astrojs/vue/app'], + exclude: ['@astrojs/vue/server.js', VIRTUAL_MODULE_ID], }, plugins: [vue(vueOptions), virtualAppEntrypoint(vueOptions)], ssr: { @@ -119,6 +123,15 @@ async function getViteConfiguration(options?: Options): Promise { config.plugins?.push(vueJsx(jsxOptions)); } + if (command === 'dev' && options?.devtools) { + const vueDevTools = (await import('vite-plugin-vue-devtools')).default; + config.plugins?.push( + vueDevTools({ + appendTo: VIRTUAL_MODULE_ID, + }) + ); + } + return config; } @@ -126,12 +139,12 @@ export default function (options?: Options): AstroIntegration { return { name: '@astrojs/vue', hooks: { - 'astro:config:setup': async ({ addRenderer, updateConfig }) => { + 'astro:config:setup': async ({ addRenderer, updateConfig, command }) => { addRenderer(getRenderer()); if (options?.jsx) { addRenderer(getJsxRenderer()); } - updateConfig({ vite: await getViteConfiguration(options) }); + updateConfig({ vite: await getViteConfiguration(command, options) }); }, }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e7fd73d2047..0649f9fa1f17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5235,6 +5235,9 @@ importers: '@vue/compiler-sfc': specifier: ^3.4.26 version: 3.4.26 + vite-plugin-vue-devtools: + specifier: ^7.1.3 + version: 7.1.3(vite@5.2.10)(vue@3.4.26) devDependencies: astro: specifier: workspace:* @@ -5584,6 +5587,10 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + /@antfu/utils@0.7.7: + resolution: {integrity: sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==} + dev: false + /@asamuzakjp/dom-selector@2.0.2: resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==} dependencies: @@ -5787,7 +5794,7 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-split-export-declaration': 7.24.5 semver: 6.3.1 dev: false @@ -5897,13 +5904,6 @@ packages: '@babel/types': 7.24.5 dev: false - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: false - /@babel/helper-split-export-declaration@7.24.5: resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} engines: {node: '>=6.9.0'} @@ -5955,6 +5955,59 @@ packages: dependencies: '@babel/types': 7.24.5 + /@babel/plugin-proposal-decorators@7.24.1(@babel/core@7.24.5): + resolution: {integrity: sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.4(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.0 + '@babel/plugin-syntax-decorators': 7.24.1(@babel/core@7.24.5) + dev: false + + /@babel/plugin-syntax-decorators@7.24.1(@babel/core@7.24.5): + resolution: {integrity: sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.0 + dev: false + + /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.5): + resolution: {integrity: sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.0 + dev: false + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.0 + dev: false + /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} engines: {node: '>=6.9.0'} @@ -7780,6 +7833,10 @@ packages: playwright: 1.43.1 dev: true + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + dev: false + /@preact/preset-vite@2.8.2(preact@10.20.2): resolution: {integrity: sha512-m3tl+M8IO8jgiHnk+7LSTFl8axdPXloewi7iGVLdmCwf34XOzEUur0bZVewW4DUbUipFjTS2CXu27+5f/oexBA==} peerDependencies: @@ -7873,6 +7930,20 @@ packages: picomatch: 2.3.1 dev: false + /@rollup/pluginutils@5.1.0: + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: false + /@rollup/rollup-android-arm-eabi@4.17.1: resolution: {integrity: sha512-P6Wg856Ou/DLpR+O0ZLneNmrv7QpqBg+hK4wE05ijbC/t349BRfMfx+UFj5Ha3fCFopIa6iSZlpdaB4agkWp2Q==} cpu: [arm] @@ -8982,6 +9053,39 @@ packages: '@vue/compiler-dom': 3.4.26 '@vue/shared': 3.4.26 + /@vue/devtools-core@7.1.3(vite@5.2.10)(vue@3.4.26): + resolution: {integrity: sha512-pVbWi8pf2Z/fZPioYOIgu+cv9pQG55k4D8bL31ec+Wfe+pQR0ImFDu0OhHfch1Ra8uvLLrAZTF4IKeGAkmzD4A==} + dependencies: + '@vue/devtools-kit': 7.1.3(vue@3.4.26) + '@vue/devtools-shared': 7.1.3 + mitt: 3.0.1 + nanoid: 3.3.7 + pathe: 1.1.2 + vite-hot-client: 0.2.3(vite@5.2.10) + transitivePeerDependencies: + - vite + - vue + dev: false + + /@vue/devtools-kit@7.1.3(vue@3.4.26): + resolution: {integrity: sha512-NFskFSJMVCBXTkByuk2llzI3KD3Blcm7WqiRorWjD6nClHPgkH5BobDH08rfulqq5ocRt5xV+3qOT1Q9FXJrwQ==} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@vue/devtools-shared': 7.1.3 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + vue: 3.4.26(typescript@5.4.5) + dev: false + + /@vue/devtools-shared@7.1.3: + resolution: {integrity: sha512-KJ3AfgjTn3tJz/XKF+BlVShNPecim3G21oHRue+YQOsooW+0s+qXvm09U09aO7yBza5SivL1QgxSrzAbiKWjhQ==} + dependencies: + rfdc: 1.3.1 + dev: false + /@vue/reactivity@3.1.5: resolution: {integrity: sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==} dependencies: @@ -10594,6 +10698,10 @@ packages: is-arrayish: 0.2.1 dev: true + /error-stack-parser-es@0.1.1: + resolution: {integrity: sha512-g/9rfnvnagiNf+DRMHEVGuGuIBlCIMDFoTA616HaP2l9PlCjGjVhD98PNbVSJvmK4TttqT5mV5tInMhoFgi+aA==} + dev: false + /es-abstract@1.23.3: resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} engines: {node: '>= 0.4'} @@ -11711,6 +11819,10 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + /hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true @@ -13351,6 +13463,10 @@ packages: rimraf: 5.0.5 dev: false + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: false + /mixme@0.5.10: resolution: {integrity: sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==} engines: {node: '>= 8.0.0'} @@ -13945,6 +14061,10 @@ packages: engines: {node: '>= 14.16'} dev: true + /perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + dev: false + /periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} dependencies: @@ -14995,6 +15115,10 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + dev: false + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -15363,6 +15487,15 @@ packages: is-arrayish: 0.3.2 dev: false + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: false + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: false @@ -15496,6 +15629,11 @@ packages: resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} dev: true + /speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + dev: false + /speech-rule-engine@4.0.7: resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==} hasBin: true @@ -15965,6 +16103,11 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: false + /tough-cookie@4.1.3: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} @@ -16510,6 +16653,17 @@ packages: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 + /vite-hot-client@0.2.3(vite@5.2.10): + resolution: {integrity: sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + vite: 5.2.10(@types/node@18.19.31)(sass@1.75.0) + dev: false + /vite-node@1.5.0(@types/node@18.19.31): resolution: {integrity: sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -16531,6 +16685,33 @@ packages: - terser dev: false + /vite-plugin-inspect@0.8.4(vite@5.2.10): + resolution: {integrity: sha512-G0N3rjfw+AiiwnGw50KlObIHYWfulVwaCBUBLh2xTW9G1eM9ocE5olXkEYUbwyTmX+azM8duubi+9w5awdCz+g==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + vite: + optional: true + dependencies: + '@antfu/utils': 0.7.7 + '@rollup/pluginutils': 5.1.0 + debug: 4.3.4(supports-color@8.1.1) + error-stack-parser-es: 0.1.1 + fs-extra: 11.2.0 + open: 10.1.0 + perfect-debounce: 1.0.0 + picocolors: 1.0.0 + sirv: 2.0.4 + vite: 5.2.10(@types/node@18.19.31)(sass@1.75.0) + transitivePeerDependencies: + - rollup + - supports-color + dev: false + /vite-plugin-solid@2.10.2(solid-js@1.8.17): resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} peerDependencies: @@ -16554,6 +16735,52 @@ packages: - supports-color dev: false + /vite-plugin-vue-devtools@7.1.3(vite@5.2.10)(vue@3.4.26): + resolution: {integrity: sha512-qv8Z4yok9RYo6TEs89WnIAlmTHby/+XTim8tlSnMs3lAPcQqqcl/wGRY8gAeYrGCANngOqO+VuabW3Jb1HZtyw==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^3.1.0 || ^4.0.0-0 || ^5.0.0-0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + '@vue/devtools-core': 7.1.3(vite@5.2.10)(vue@3.4.26) + '@vue/devtools-kit': 7.1.3(vue@3.4.26) + '@vue/devtools-shared': 7.1.3 + execa: 8.0.1 + sirv: 2.0.4 + vite: 5.2.10(@types/node@18.19.31)(sass@1.75.0) + vite-plugin-inspect: 0.8.4(vite@5.2.10) + vite-plugin-vue-inspector: 5.0.1(vite@5.2.10) + transitivePeerDependencies: + - '@nuxt/kit' + - rollup + - supports-color + - vue + dev: false + + /vite-plugin-vue-inspector@5.0.1(vite@5.2.10): + resolution: {integrity: sha512-R93P8iFa6BPODhc/aOtO04A8FFMMyFIfm8ZVSmN+8vU1TgwsHya734APGpX4fVHSPX2aVwYyiezXBUYQ0Opsqw==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + '@babel/core': 7.24.5 + '@babel/plugin-proposal-decorators': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-transform-typescript': 7.24.4(@babel/core@7.24.5) + '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.24.5) + '@vue/compiler-dom': 3.4.26 + kolorist: 1.8.0 + magic-string: 0.30.10 + vite: 5.2.10(@types/node@18.19.31)(sass@1.75.0) + transitivePeerDependencies: + - supports-color + dev: false + /vite-svg-loader@4.0.0: resolution: {integrity: sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==} peerDependencies: From 4d905ccef663f728fc981181f5bb9f1d157184ff Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 6 May 2024 11:27:07 -0400 Subject: [PATCH 047/151] Preserve content modules properly in cache (#10889) * Wait until after build to preserve content modules * Properly build hoisted scripts * Add changeset * Fix tests hitting each other * Global state, the shame * Move the file copying over to earlier in the process --- .changeset/spicy-keys-own.md | 5 + packages/astro/content-module.template.mjs | 2 + packages/astro/src/assets/build/generate.ts | 2 +- .../astro/src/assets/vite-plugin-assets.ts | 2 +- packages/astro/src/content/runtime.ts | 3 +- .../content/vite-plugin-content-imports.ts | 2 +- .../vite-plugin-content-virtual-mod.ts | 3 +- packages/astro/src/core/build/generate.ts | 4 +- packages/astro/src/core/build/index.ts | 3 +- packages/astro/src/core/build/internal.ts | 3 + packages/astro/src/core/build/pipeline.ts | 3 +- .../src/core/build/plugins/plugin-analyzer.ts | 91 +++++--- .../src/core/build/plugins/plugin-content.ts | 51 +++-- .../build/plugins/plugin-hoisted-scripts.ts | 53 +++-- .../src/core/build/plugins/plugin-ssr.ts | 2 +- packages/astro/src/core/build/static-build.ts | 9 +- packages/astro/src/core/build/types.ts | 4 +- packages/astro/src/core/config/config.ts | 5 - packages/astro/src/core/util.ts | 12 +- packages/astro/src/integrations/hooks.ts | 2 +- packages/astro/src/prerender/utils.ts | 5 +- .../src/vite-plugin-astro-server/pipeline.ts | 3 +- .../astro/src/vite-plugin-scanner/index.ts | 4 +- ...collections-css-inline-stylesheets.test.js | 61 +---- ...imental-content-collections-render.test.js | 208 ++---------------- .../css-inline-stylesheets-2/package.json | 8 + .../src/components/Button.astro | 86 ++++++++ .../src/content/en/endeavour.md | 15 ++ .../css-inline-stylesheets-2/src/imported.css | 15 ++ .../src/layouts/Layout.astro | 35 +++ .../src/pages/index.astro | 17 ++ packages/astro/test/test-utils.js | 6 +- pnpm-lock.yaml | 6 + 33 files changed, 385 insertions(+), 345 deletions(-) create mode 100644 .changeset/spicy-keys-own.md create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/package.json create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/components/Button.astro create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/content/en/endeavour.md create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/imported.css create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/layouts/Layout.astro create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/pages/index.astro diff --git a/.changeset/spicy-keys-own.md b/.changeset/spicy-keys-own.md new file mode 100644 index 000000000000..066d764a740c --- /dev/null +++ b/.changeset/spicy-keys-own.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Preserve content modules properly in cache diff --git a/packages/astro/content-module.template.mjs b/packages/astro/content-module.template.mjs index 35c6a55c9b2a..f246678a25ec 100644 --- a/packages/astro/content-module.template.mjs +++ b/packages/astro/content-module.template.mjs @@ -48,10 +48,12 @@ const collectionToRenderEntryMap = createCollectionToGlobResultMap({ contentDir, }); +const cacheEntriesByCollection = new Map(); export const getCollection = createGetCollection({ contentCollectionToEntryMap, dataCollectionToEntryMap, getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap), + cacheEntriesByCollection, }); export const getEntryBySlug = createGetEntryBySlug({ diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts index f209345387a7..c363a6349786 100644 --- a/packages/astro/src/assets/build/generate.ts +++ b/packages/astro/src/assets/build/generate.ts @@ -10,7 +10,7 @@ import { AstroError } from '../../core/errors/errors.js'; import { AstroErrorData } from '../../core/errors/index.js'; import type { Logger } from '../../core/logger/core.js'; import { isRemotePath, removeLeadingForwardSlash } from '../../core/path.js'; -import { isServerLikeOutput } from '../../prerender/utils.js'; +import { isServerLikeOutput } from '../../core/util.js'; import type { MapValue } from '../../type-utils.js'; import { getConfiguredImageService } from '../internal.js'; import type { LocalImageService } from '../services/service.js'; diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 6114e7bf9bdc..eda4b6cfbc2a 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -12,7 +12,7 @@ import { removeBase, removeQueryString, } from '../core/path.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; +import { isServerLikeOutput } from '../core/util.js'; import { VALID_INPUT_FORMATS, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js'; import { emitESMImage } from './utils/emitAsset.js'; import { getAssetsPrefix } from './utils/getAssetsPrefix.js'; diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index c4fc8573910b..21af34e1d280 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -44,15 +44,16 @@ export function createCollectionToGlobResultMap({ return collectionToGlobResultMap; } -const cacheEntriesByCollection = new Map (); export function createGetCollection({ contentCollectionToEntryMap, dataCollectionToEntryMap, getRenderEntryImport, + cacheEntriesByCollection, }: { contentCollectionToEntryMap: CollectionToEntryMap; dataCollectionToEntryMap: CollectionToEntryMap; getRenderEntryImport: GetEntryImport; + cacheEntriesByCollection: Map ; }) { return async function getCollection(collection: string, filter?: (entry: any) => unknown) { let type: 'content' | 'data'; diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 6540d483ae5b..2589a9629444 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -15,7 +15,7 @@ import type { import { getProxyCode } from '../assets/utils/proxy.js'; import { AstroError } from '../core/errors/errors.js'; import { AstroErrorData } from '../core/errors/index.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; +import { isServerLikeOutput } from '../core/util.js'; import { CONTENT_FLAG, DATA_FLAG } from './consts.js'; import { type ContentConfig, diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index 474e96235d6d..bd3362343daf 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -8,8 +8,7 @@ import type { AstroSettings } from '../@types/astro.js'; import { encodeName } from '../core/build/util.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { appendForwardSlash, removeFileExtension } from '../core/path.js'; -import { rootRelativePath } from '../core/util.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; +import { rootRelativePath, isServerLikeOutput } from '../core/util.js'; import type { AstroPluginMetadata } from '../vite-plugin-astro/index.js'; import { CONTENT_FLAG, diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 5c6c302db773..ffe799f6e7e4 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -31,7 +31,7 @@ import { } from '../../core/path.js'; import { toRoutingStrategy } from '../../i18n/utils.js'; import { runHookBuildGenerated } from '../../integrations/hooks.js'; -import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js'; +import { getOutputDirectory } from '../../prerender/utils.js'; import type { SSRManifestI18n } from '../app/types.js'; import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; @@ -45,7 +45,7 @@ import { RenderContext } from '../render-context.js'; import { callGetStaticPaths } from '../render/route-cache.js'; import { createRequest } from '../request.js'; import { matchRoute } from '../routing/match.js'; -import { getOutputFilename } from '../util.js'; +import { getOutputFilename, isServerLikeOutput } from '../util.js'; import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; import { cssOrder, diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index ea1b715bbd57..2dbf8967287c 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -19,7 +19,6 @@ import { runHookConfigDone, runHookConfigSetup, } from '../../integrations/hooks.js'; -import { isServerLikeOutput } from '../../prerender/utils.js'; import { resolveConfig } from '../config/config.js'; import { createNodeLogger } from '../config/logging.js'; import { createSettings } from '../config/settings.js'; @@ -28,7 +27,7 @@ import type { Logger } from '../logger/core.js'; import { levels, timerMessage } from '../logger/core.js'; import { apply as applyPolyfill } from '../polyfill.js'; import { createRouteManifest } from '../routing/index.js'; -import { ensureProcessNodeEnv } from '../util.js'; +import { ensureProcessNodeEnv, isServerLikeOutput } from '../util.js'; import { collectPagesData } from './page-data.js'; import { staticBuild, viteBuild } from './static-build.js'; import type { StaticBuildOptions } from './types.js'; diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index a5e456627155..a7ff537dc8a4 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -25,6 +25,8 @@ export interface BuildInternals { hoistedScriptIdToHoistedMap: Map >; // A mapping of hoisted script ids back to the pages which reference it hoistedScriptIdToPagesMap: Map >; + // A mapping of hoisted script ids back to the content which reference it + hoistedScriptIdToContentMap: Map >; /** * Used by the `directRenderScript` option. If script is inlined, its id and @@ -123,6 +125,7 @@ export function createBuildInternals(): BuildInternals { cssModuleToChunkIdMap: new Map(), hoistedScriptIdToHoistedMap, hoistedScriptIdToPagesMap, + hoistedScriptIdToContentMap: new Map(), inlinedScripts: new Map(), entrySpecifierToBundleMap: new Map (), pageToBundleMap: new Map (), diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index a733d4c6fe16..5df7cb1e3061 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,5 +1,5 @@ import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../@types/astro.js'; -import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js'; +import { getOutputDirectory } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import type { SSRManifest } from '../app/types.js'; import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js'; @@ -21,6 +21,7 @@ import { getVirtualModulePageNameFromPath } from './plugins/util.js'; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { PageBuildData, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; +import { isServerLikeOutput } from '../util.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index cb1f4078b44f..c72026e0f1fd 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -6,6 +6,7 @@ import type { AstroBuildPlugin } from '../plugin.js'; import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { prependForwardSlash } from '../../../core/path.js'; +import { isContentCollectionsCacheEnabled } from '../../../core/util.js'; import { getParentModuleInfos, getTopLevelPageModuleInfos, @@ -31,6 +32,7 @@ export function vitePluginAnalyzer( const pageScripts = new Map< string, { + type: 'page' | 'content'; hoistedSet: Set ; propagatedMapByImporter: Map >; } @@ -51,20 +53,38 @@ export function vitePluginAnalyzer( if (hoistedScripts.size) { for (const parentInfo of getParentModuleInfos(from, this, isPropagatedAsset)) { if (isPropagatedAsset(parentInfo.id)) { - for (const nestedParentInfo of getParentModuleInfos(from, this)) { - if (moduleIsTopLevelPage(nestedParentInfo)) { - for (const hid of hoistedScripts) { - if (!pageScripts.has(nestedParentInfo.id)) { - pageScripts.set(nestedParentInfo.id, { - hoistedSet: new Set(), - propagatedMapByImporter: new Map(), - }); - } - const entry = pageScripts.get(nestedParentInfo.id)!; - if (!entry.propagatedMapByImporter.has(parentInfo.id)) { - entry.propagatedMapByImporter.set(parentInfo.id, new Set()); + if(isContentCollectionsCacheEnabled(options.settings.config)) { + if (!pageScripts.has(parentInfo.id)) { + pageScripts.set(parentInfo.id, { + type: 'content', + hoistedSet: new Set(), + propagatedMapByImporter: new Map(), + }); + } + const propagaters = pageScripts.get(parentInfo.id)!.propagatedMapByImporter; + for(const hid of hoistedScripts) { + if(!propagaters.has(parentInfo.id)) { + propagaters.set(parentInfo.id, new Set()); + } + propagaters.get(parentInfo.id)!.add(hid); + } + } else { + for (const nestedParentInfo of getParentModuleInfos(from, this)) { + if (moduleIsTopLevelPage(nestedParentInfo)) { + for (const hid of hoistedScripts) { + if (!pageScripts.has(nestedParentInfo.id)) { + pageScripts.set(nestedParentInfo.id, { + type: 'page', + hoistedSet: new Set(), + propagatedMapByImporter: new Map(), + }); + } + const entry = pageScripts.get(nestedParentInfo.id)!; + if (!entry.propagatedMapByImporter.has(parentInfo.id)) { + entry.propagatedMapByImporter.set(parentInfo.id, new Set()); + } + entry.propagatedMapByImporter.get(parentInfo.id)!.add(hid); } - entry.propagatedMapByImporter.get(parentInfo.id)!.add(hid); } } } @@ -72,6 +92,7 @@ export function vitePluginAnalyzer( for (const hid of hoistedScripts) { if (!pageScripts.has(parentInfo.id)) { pageScripts.set(parentInfo.id, { + type: 'page', hoistedSet: new Set(), propagatedMapByImporter: new Map(), }); @@ -84,12 +105,21 @@ export function vitePluginAnalyzer( }, finalize() { - for (const [pageId, { hoistedSet, propagatedMapByImporter }] of pageScripts) { - const pageData = getPageDataByViteID(internals, pageId); - if (!pageData) continue; + for (const [pageId, { hoistedSet, propagatedMapByImporter, type }] of pageScripts) { + let astroModuleId: string; + if(type === 'page') { + const pageData = getPageDataByViteID(internals, pageId); + if (!pageData) { + continue; + }; + const { component } = pageData; + astroModuleId = prependForwardSlash(component); - const { component } = pageData; - const astroModuleId = prependForwardSlash(component); + // Keep track of the importers + pageData.propagatedScripts = propagatedMapByImporter; + } else { + astroModuleId = pageId; + } const uniqueHoistedId = JSON.stringify(Array.from(hoistedSet).sort()); let moduleId: string; @@ -104,8 +134,6 @@ export function vitePluginAnalyzer( } internals.discoveredScripts.add(moduleId); - pageData.propagatedScripts = propagatedMapByImporter; - // Add propagated scripts to client build, // but DON'T add to pages -> hoisted script map. for (const propagatedScripts of propagatedMapByImporter.values()) { @@ -114,13 +142,24 @@ export function vitePluginAnalyzer( } } - // Make sure to track that this page uses this set of hoisted scripts - if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { - const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); - pages!.add(astroModuleId); + if(type === 'page') { + // Make sure to track that this page uses this set of hoisted scripts + if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { + const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); + pages!.add(astroModuleId); + } else { + internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId])); + internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); + } } else { - internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId])); - internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); + // For content collections save to hoistedScriptIdToContentMap instead + if (internals.hoistedScriptIdToContentMap.has(moduleId)) { + const contentModules = internals.hoistedScriptIdToContentMap.get(moduleId); + contentModules!.add(astroModuleId); + } else { + internals.hoistedScriptIdToContentMap.set(moduleId, new Set([astroModuleId])); + internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); + } } } }, diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts index 24512f8ebc8e..c94f01f3fc98 100644 --- a/packages/astro/src/core/build/plugins/plugin-content.ts +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -9,7 +9,6 @@ import { generateContentEntryFile, generateLookupMap, } from '../../../content/vite-plugin-content-virtual-mod.js'; -import { isServerLikeOutput } from '../../../prerender/utils.js'; import { configPaths } from '../../config/index.js'; import { emptyDir } from '../../fs/index.js'; import { @@ -26,6 +25,8 @@ import { copyFiles } from '../static-build.js'; import type { StaticBuildOptions } from '../types.js'; import { encodeName } from '../util.js'; import { extendManualChunks } from './util.js'; +import { isContentCollectionsCacheEnabled } from '../../util.js'; +import type { AstroConfig } from '../../../@types/astro.js'; const CONTENT_CACHE_DIR = './content/'; const CONTENT_MANIFEST_FILE = './manifest.json'; @@ -69,6 +70,10 @@ function createContentManifest(): ContentManifest { }; } +const getContentRoot = (config: AstroConfig) => new URL('./content/', config.outDir); +const getContentCacheDir = (config: AstroConfig) => new URL(CONTENT_CACHE_DIR, config.cacheDir); +const getCacheTmp = (contentCacheDir: URL) => new URL('./.tmp/', contentCacheDir); + function vitePluginContent( opts: StaticBuildOptions, lookupMap: ContentLookupMap, @@ -76,12 +81,9 @@ function vitePluginContent( cachedBuildOutput: Array<{ cached: URL; dist: URL }> ): VitePlugin { const { config } = opts.settings; - const { cacheDir } = config; - const distRoot = config.outDir; - const distContentRoot = new URL('./content/', distRoot); - const contentCacheDir = new URL(CONTENT_CACHE_DIR, cacheDir); + const distContentRoot = getContentRoot(config); + const contentCacheDir = getContentCacheDir(config); const contentManifestFile = new URL(CONTENT_MANIFEST_FILE, contentCacheDir); - const cacheTmp = new URL('./.tmp/', contentCacheDir); let oldManifest = createContentManifest(); let newManifest = createContentManifest(); let entries: ContentEntries; @@ -151,6 +153,11 @@ function vitePluginContent( await copyFiles(cached, dist, true); } } + // Copy over the content cache now so that new files override it + const cacheExists = fsMod.existsSync(contentCacheDir); + if (cacheExists) { + await copyFiles(contentCacheDir, distContentRoot, false); + } } // If nothing needs to be rebuilt, we inject a fake entrypoint to appease Rollup @@ -242,7 +249,6 @@ function vitePluginContent( ...oldManifest.clientEntries, ...internals.discoveredHydratedComponents.keys(), ...internals.discoveredClientOnlyComponents.keys(), - ...internals.discoveredScripts, ]); // Likewise, these are server modules that might not be referenced // once the cached items are excluded from the build process @@ -263,13 +269,6 @@ function vitePluginContent( await fsMod.promises.writeFile(contentManifestFile, JSON.stringify(newManifest), { encoding: 'utf8', }); - await fsMod.promises.mkdir(cacheTmp, { recursive: true }); - await copyFiles(distContentRoot, cacheTmp, true); - if (cacheExists && currentManifestState === 'valid') { - await copyFiles(contentCacheDir, distContentRoot, false); - } - await copyFiles(cacheTmp, contentCacheDir); - await fsMod.promises.rm(cacheTmp, { recursive: true, force: true }); }, }; } @@ -319,6 +318,7 @@ function getEntriesFromManifests( entries.buildFromSource.push(entry); } } + return entries; } @@ -438,6 +438,19 @@ function collectionTypeToFlag(type: 'content' | 'data') { return `astro${name}CollectionEntry`; } +export async function copyContentToCache(opts: StaticBuildOptions) { + const { config } = opts.settings; + const distContentRoot = getContentRoot(config); + const contentCacheDir = getContentCacheDir(config); + const cacheTmp = getCacheTmp(contentCacheDir); + + await fsMod.promises.mkdir(cacheTmp, { recursive: true }); + await copyFiles(distContentRoot, cacheTmp, true); + + await copyFiles(cacheTmp, contentCacheDir); + await fsMod.promises.rm(cacheTmp, { recursive: true, force: true }); +} + export function pluginContent( opts: StaticBuildOptions, internals: BuildInternals @@ -456,10 +469,7 @@ export function pluginContent( targets: ['server'], hooks: { async 'build:before'() { - if (!opts.settings.config.experimental.contentCollectionCache) { - return { vitePlugin: undefined }; - } - if (isServerLikeOutput(opts.settings.config)) { + if (!isContentCollectionsCacheEnabled(opts.settings.config)) { return { vitePlugin: undefined }; } const lookupMap = await generateLookupMap({ settings: opts.settings, fs: fsMod }); @@ -469,10 +479,7 @@ export function pluginContent( }, async 'build:post'() { - if (!opts.settings.config.experimental.contentCollectionCache) { - return; - } - if (isServerLikeOutput(opts.settings.config)) { + if(!isContentCollectionsCacheEnabled(opts.settings.config)) { return; } // Cache build output of chunks and assets diff --git a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts index 2ed3c7fa746d..641665b84c1a 100644 --- a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -1,6 +1,6 @@ import type { BuildOptions, Plugin as VitePlugin } from 'vite'; import type { AstroSettings } from '../../../@types/astro.js'; -import { viteID } from '../../util.js'; +import { isContentCollectionsCacheEnabled, viteID } from '../../util.js'; import type { BuildInternals } from '../internal.js'; import { getPageDataByViteID } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; @@ -11,6 +11,7 @@ function virtualHoistedEntry(id: string) { return id.startsWith('/astro/hoisted.js?q='); } + export function vitePluginHoistedScripts( settings: AstroSettings, internals: BuildInternals @@ -72,23 +73,41 @@ export function vitePluginHoistedScripts( output.dynamicImports.length === 0 && shouldInlineAsset(output.code, output.fileName, assetsInlineLimit); let removeFromBundle = false; + const facadeId = output.facadeModuleId!; - const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; - for (const pathname of pages) { - const vid = viteID(new URL('.' + pathname, settings.config.root)); - const pageInfo = getPageDataByViteID(internals, vid); - if (pageInfo) { - if (canBeInlined) { - pageInfo.hoistedScript = { - type: 'inline', - value: output.code, - }; - removeFromBundle = true; - } else { - pageInfo.hoistedScript = { - type: 'external', - value: id, - }; + + // Pages + if(internals.hoistedScriptIdToPagesMap.has(facadeId)) { + const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; + for (const pathname of pages) { + const vid = viteID(new URL('.' + pathname, settings.config.root)); + + const pageInfo = getPageDataByViteID(internals, vid); + if (pageInfo) { + if (canBeInlined) { + pageInfo.hoistedScript = { + type: 'inline', + value: output.code, + }; + removeFromBundle = true; + } else { + pageInfo.hoistedScript = { + type: 'external', + value: id, + }; + } + } + } + } + // Content collection entries + else { + const contentModules = internals.hoistedScriptIdToContentMap.get(facadeId)!; + for(const contentId of contentModules) { + if(isContentCollectionsCacheEnabled(settings.config)) { + const scripts = internals.propagatedScriptsMap.get(contentId) ?? + internals.propagatedScriptsMap.set(contentId, new Set()).get(contentId)!; + + scripts.add(facadeId); } } } diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 5ccb8746364f..1d8ea677f4b2 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -2,8 +2,8 @@ import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin as VitePlugin } from 'vite'; import type { AstroAdapter } from '../../../@types/astro.js'; +import { isServerLikeOutput } from '../../util.js'; import { isFunctionPerRouteEnabled } from '../../../integrations/hooks.js'; -import { isServerLikeOutput } from '../../../prerender/utils.js'; import { routeIsRedirect } from '../../redirects/index.js'; import { addRollupInput } from '../add-rollup-input.js'; import type { BuildInternals } from '../internal.js'; diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 6aa1eca901ba..91d17e37431c 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -16,9 +16,9 @@ import { } from '../../core/build/internal.js'; import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js'; import { appendForwardSlash, prependForwardSlash, removeFileExtension } from '../../core/path.js'; -import { isModeServerWithNoAdapter } from '../../core/util.js'; +import { isModeServerWithNoAdapter, isServerLikeOutput } from '../../core/util.js'; import { runHookBuildSetup } from '../../integrations/hooks.js'; -import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js'; +import { getOutputDirectory } from '../../prerender/utils.js'; import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { routeIsRedirect } from '../redirects/index.js'; @@ -35,6 +35,7 @@ import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plug import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { StaticBuildOptions } from './types.js'; import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js'; +import { copyContentToCache } from './plugins/plugin-content.js'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -107,7 +108,9 @@ export async function viteBuild(opts: StaticBuildOptions) { const ssrOutputs = viteBuildReturnToRollupOutputs(ssrOutput); const clientOutputs = viteBuildReturnToRollupOutputs(clientOutput ?? []); await runPostBuildHooks(container, ssrOutputs, clientOutputs); - + if(opts.settings.config.experimental.contentCollectionCache) { + await copyContentToCache(opts); + } settings.timer.end('Client build'); // Free up memory diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 9608ba04c524..9b418f7bc56d 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -20,13 +20,15 @@ export type StylesheetAsset = | { type: 'inline'; content: string } | { type: 'external'; src: string }; +export type HoistedScriptAsset = { type: 'inline' | 'external'; value: string }; + export interface PageBuildData { component: ComponentPath; route: RouteData; moduleSpecifier: string; propagatedStyles: Map >; propagatedScripts: Map >; - hoistedScript: { type: 'inline' | 'external'; value: string } | undefined; + hoistedScript: HoistedScriptAsset | undefined; styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>; hasSharedModules: boolean; } diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 5bb2eda77287..d0cfd0e49702 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -45,11 +45,6 @@ export async function validateConfig( throw e; } - // TODO: fix inlineStylesheets behavior with content collection cache - if (result.build.inlineStylesheets !== 'auto' && result.experimental.contentCollectionCache) { - result.experimental.contentCollectionCache = false; - } - // If successful, return the result as a verified AstroConfig object. return result; } diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 4db3549a2a6d..405479030a2f 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -3,7 +3,6 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { normalizePath } from 'vite'; import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js'; import type { ModuleLoader } from './module-loader/index.js'; import { prependForwardSlash, removeTrailingForwardSlash, slash } from './path.js'; @@ -148,10 +147,21 @@ export function isEndpoint(file: URL, settings: AstroSettings): boolean { return !endsWithPageExt(file, settings); } +export function isServerLikeOutput(config: AstroConfig) { + return config.output === 'server' || config.output === 'hybrid'; +} + export function isModeServerWithNoAdapter(settings: AstroSettings): boolean { return isServerLikeOutput(settings.config) && !settings.adapter; } + +export function isContentCollectionsCacheEnabled(config: AstroConfig): boolean { + return config.experimental.contentCollectionCache && + // contentCollectionsCache is an SSG only feature + !isServerLikeOutput(config); +} + export function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string) { let id: string; if (typeof idOrUrl !== 'string') { diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index 51f600ae85f6..ae97802eebb0 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -19,7 +19,7 @@ import type { PageBuildData } from '../core/build/types.js'; import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js'; import { mergeConfig } from '../core/config/index.js'; import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; +import { isServerLikeOutput } from '../core/util.js'; import { validateSupportedFeatures } from './features-validation.js'; async function withTakingALongTimeMsg ({ diff --git a/packages/astro/src/prerender/utils.ts b/packages/astro/src/prerender/utils.ts index c671a68e06af..4097b66b1f6d 100644 --- a/packages/astro/src/prerender/utils.ts +++ b/packages/astro/src/prerender/utils.ts @@ -1,9 +1,6 @@ import type { AstroConfig } from '../@types/astro.js'; import { getOutDirWithinCwd } from '../core/build/common.js'; - -export function isServerLikeOutput(config: AstroConfig) { - return config.output === 'server' || config.output === 'hybrid'; -} +import { isServerLikeOutput } from '../core/util.js'; export function getPrerenderDefault(config: AstroConfig) { return config.output !== 'server'; diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 09366d885cc7..2dfaaac55f07 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -16,8 +16,7 @@ import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js import type { Logger } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; import { Pipeline, loadRenderer } from '../core/render/index.js'; -import { isPage, resolveIdToUrl, viteID } from '../core/util.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; +import { isPage, resolveIdToUrl, viteID, isServerLikeOutput } from '../core/util.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; import { getStylesForURL } from './css.js'; import { getComponentMetadata } from './metadata.js'; diff --git a/packages/astro/src/vite-plugin-scanner/index.ts b/packages/astro/src/vite-plugin-scanner/index.ts index fb1ce4f7b5ff..b0065f1ab67a 100644 --- a/packages/astro/src/vite-plugin-scanner/index.ts +++ b/packages/astro/src/vite-plugin-scanner/index.ts @@ -4,8 +4,8 @@ import type { Plugin as VitePlugin } from 'vite'; import { normalizePath } from 'vite'; import type { AstroSettings } from '../@types/astro.js'; import { type Logger } from '../core/logger/core.js'; -import { isEndpoint, isPage, rootRelativePath } from '../core/util.js'; -import { getPrerenderDefault, isServerLikeOutput } from '../prerender/utils.js'; +import { isEndpoint, isPage, rootRelativePath, isServerLikeOutput } from '../core/util.js'; +import { getPrerenderDefault } from '../prerender/utils.js'; import { scan } from './scan.js'; export interface AstroPluginScannerOptions { diff --git a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js index a3e4cf113276..a4aede4966a5 100644 --- a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js +++ b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js @@ -13,7 +13,7 @@ describe('Experimental Content Collections cache inlineStylesheets', () => { // to bust cache and prevent modules and their state // from being reused site: 'https://test.dev/', - root: './fixtures/css-inline-stylesheets/', + root: './fixtures/css-inline-stylesheets-2/', output: 'static', build: { inlineStylesheets: 'never', @@ -145,65 +145,6 @@ describe('Experimental Content Collections cache - inlineStylesheets to auto in }); }); -describe('Setting inlineStylesheets to auto in server output', () => { - let app; - let fixture; - - before(async () => { - fixture = await loadFixture({ - // inconsequential config that differs between tests - // to bust cache and prevent modules and their state - // from being reused - site: 'https://test.info/', - root: './fixtures/css-inline-stylesheets/', - output: 'server', - adapter: testAdapter(), - build: { - inlineStylesheets: 'auto', - }, - vite: { - build: { - assetsInlineLimit: 512, - }, - }, - experimental: { - contentCollectionCache: true, - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - after(async () => await fixture.clean()); - - it( - 'Renders some \ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-2/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/content/en/endeavour.md new file mode 100644 index 000000000000..240eeeae3993 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/content/en/endeavour.md @@ -0,0 +1,15 @@ +--- +title: Endeavour +description: 'Learn about the Endeavour NASA space shuttle.' +publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)' +layout: '../../layouts/Layout.astro' +tags: [space, 90s] +--- + +**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) + +Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly. + +The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986. + +NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly. \ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-2/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/imported.css new file mode 100644 index 000000000000..3959523ff16e --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/imported.css @@ -0,0 +1,15 @@ +.bg-skyblue { + background: skyblue; +} + +.bg-lightcoral { + background: lightcoral; +} + +.red { + color: darkred; +} + +.blue { + color: royalblue; +} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-2/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/layouts/Layout.astro new file mode 100644 index 000000000000..0a26655189f5 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/layouts/Layout.astro @@ -0,0 +1,35 @@ +--- +import Button from '../components/Button.astro'; +import '../imported.css'; + +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + {title} + + + ++ + + diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-2/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/pages/index.astro new file mode 100644 index 000000000000..2aecfb0f2eb7 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-2/src/pages/index.astro @@ -0,0 +1,17 @@ +--- +import { getEntryBySlug } from 'astro:content'; +import Button from '../components/Button.astro'; + +const entry = await getEntryBySlug('en', 'endeavour'); +const { Content } = await entry.render(); +--- + + + diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index d72680c2fd8d..e0eb8c2b71d2 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -204,9 +204,9 @@ export async function loadFixture(inlineConfig) { recursive: true, force: true, }); - const contentCache = new URL('./node_modules/.astro/content', config.root); - if (fs.existsSync(contentCache)) { - await fs.promises.rm(contentCache, { + const astroCache = new URL('./node_modules/.astro', config.root); + if (fs.existsSync(astroCache)) { + await fs.promises.rm(astroCache, { maxRetries: 10, recursive: true, force: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0649f9fa1f17..be362ddc2641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2606,6 +2606,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/css-inline-stylesheets-2: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/css-no-code-split: dependencies: astro: From 4efe519456de50ae9636ac7dce684af1830afccc Mon Sep 17 00:00:00 2001 From: Matthew PhillipsWelcome to Astro
++ + Date: Mon, 6 May 2024 15:28:05 +0000 Subject: [PATCH 048/151] [ci] format --- .../src/content/vite-plugin-content-virtual-mod.ts | 2 +- packages/astro/src/core/build/pipeline.ts | 2 +- .../astro/src/core/build/plugins/plugin-analyzer.ts | 12 ++++++------ .../astro/src/core/build/plugins/plugin-content.ts | 6 +++--- .../src/core/build/plugins/plugin-hoisted-scripts.ts | 12 ++++++------ packages/astro/src/core/build/plugins/plugin-ssr.ts | 2 +- packages/astro/src/core/build/static-build.ts | 4 ++-- packages/astro/src/core/util.ts | 7 ++++--- .../astro/src/vite-plugin-astro-server/pipeline.ts | 2 +- packages/astro/src/vite-plugin-scanner/index.ts | 2 +- .../experimental-content-collections-render.test.js | 4 ++-- 11 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index bd3362343daf..de50edeaaad5 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -8,7 +8,7 @@ import type { AstroSettings } from '../@types/astro.js'; import { encodeName } from '../core/build/util.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { appendForwardSlash, removeFileExtension } from '../core/path.js'; -import { rootRelativePath, isServerLikeOutput } from '../core/util.js'; +import { isServerLikeOutput, rootRelativePath } from '../core/util.js'; import type { AstroPluginMetadata } from '../vite-plugin-astro/index.js'; import { CONTENT_FLAG, diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index 5df7cb1e3061..a78c8eaf893c 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -9,6 +9,7 @@ import { createModuleScriptsSet, createStylesheetElementSet, } from '../render/ssr-element.js'; +import { isServerLikeOutput } from '../util.js'; import { type BuildInternals, cssOrder, @@ -21,7 +22,6 @@ import { getVirtualModulePageNameFromPath } from './plugins/util.js'; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { PageBuildData, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; -import { isServerLikeOutput } from '../util.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index c72026e0f1fd..5bc0c53e04ea 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -53,7 +53,7 @@ export function vitePluginAnalyzer( if (hoistedScripts.size) { for (const parentInfo of getParentModuleInfos(from, this, isPropagatedAsset)) { if (isPropagatedAsset(parentInfo.id)) { - if(isContentCollectionsCacheEnabled(options.settings.config)) { + if (isContentCollectionsCacheEnabled(options.settings.config)) { if (!pageScripts.has(parentInfo.id)) { pageScripts.set(parentInfo.id, { type: 'content', @@ -62,8 +62,8 @@ export function vitePluginAnalyzer( }); } const propagaters = pageScripts.get(parentInfo.id)!.propagatedMapByImporter; - for(const hid of hoistedScripts) { - if(!propagaters.has(parentInfo.id)) { + for (const hid of hoistedScripts) { + if (!propagaters.has(parentInfo.id)) { propagaters.set(parentInfo.id, new Set()); } propagaters.get(parentInfo.id)!.add(hid); @@ -107,11 +107,11 @@ export function vitePluginAnalyzer( finalize() { for (const [pageId, { hoistedSet, propagatedMapByImporter, type }] of pageScripts) { let astroModuleId: string; - if(type === 'page') { + if (type === 'page') { const pageData = getPageDataByViteID(internals, pageId); if (!pageData) { continue; - }; + } const { component } = pageData; astroModuleId = prependForwardSlash(component); @@ -142,7 +142,7 @@ export function vitePluginAnalyzer( } } - if(type === 'page') { + if (type === 'page') { // Make sure to track that this page uses this set of hoisted scripts if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts index c94f01f3fc98..7210dd4f184b 100644 --- a/packages/astro/src/core/build/plugins/plugin-content.ts +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -3,6 +3,7 @@ import fsMod from 'node:fs'; import { fileURLToPath } from 'node:url'; import pLimit from 'p-limit'; import { type Plugin as VitePlugin, normalizePath } from 'vite'; +import type { AstroConfig } from '../../../@types/astro.js'; import { CONTENT_RENDER_FLAG, PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { type ContentLookupMap, hasContentFlag } from '../../../content/utils.js'; import { @@ -17,6 +18,7 @@ import { removeFileExtension, removeLeadingForwardSlash, } from '../../path.js'; +import { isContentCollectionsCacheEnabled } from '../../util.js'; import { addRollupInput } from '../add-rollup-input.js'; import { CHUNKS_PATH } from '../consts.js'; import { type BuildInternals } from '../internal.js'; @@ -25,8 +27,6 @@ import { copyFiles } from '../static-build.js'; import type { StaticBuildOptions } from '../types.js'; import { encodeName } from '../util.js'; import { extendManualChunks } from './util.js'; -import { isContentCollectionsCacheEnabled } from '../../util.js'; -import type { AstroConfig } from '../../../@types/astro.js'; const CONTENT_CACHE_DIR = './content/'; const CONTENT_MANIFEST_FILE = './manifest.json'; @@ -479,7 +479,7 @@ export function pluginContent( }, async 'build:post'() { - if(!isContentCollectionsCacheEnabled(opts.settings.config)) { + if (!isContentCollectionsCacheEnabled(opts.settings.config)) { return; } // Cache build output of chunks and assets diff --git a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts index 641665b84c1a..80bfa6a6e964 100644 --- a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -11,7 +11,6 @@ function virtualHoistedEntry(id: string) { return id.startsWith('/astro/hoisted.js?q='); } - export function vitePluginHoistedScripts( settings: AstroSettings, internals: BuildInternals @@ -77,7 +76,7 @@ export function vitePluginHoistedScripts( const facadeId = output.facadeModuleId!; // Pages - if(internals.hoistedScriptIdToPagesMap.has(facadeId)) { + if (internals.hoistedScriptIdToPagesMap.has(facadeId)) { const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; for (const pathname of pages) { const vid = viteID(new URL('.' + pathname, settings.config.root)); @@ -102,11 +101,12 @@ export function vitePluginHoistedScripts( // Content collection entries else { const contentModules = internals.hoistedScriptIdToContentMap.get(facadeId)!; - for(const contentId of contentModules) { - if(isContentCollectionsCacheEnabled(settings.config)) { - const scripts = internals.propagatedScriptsMap.get(contentId) ?? + for (const contentId of contentModules) { + if (isContentCollectionsCacheEnabled(settings.config)) { + const scripts = + internals.propagatedScriptsMap.get(contentId) ?? internals.propagatedScriptsMap.set(contentId, new Set()).get(contentId)!; - + scripts.add(facadeId); } } diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 1d8ea677f4b2..07cba1d57cd2 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -2,9 +2,9 @@ import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin as VitePlugin } from 'vite'; import type { AstroAdapter } from '../../../@types/astro.js'; -import { isServerLikeOutput } from '../../util.js'; import { isFunctionPerRouteEnabled } from '../../../integrations/hooks.js'; import { routeIsRedirect } from '../../redirects/index.js'; +import { isServerLikeOutput } from '../../util.js'; import { addRollupInput } from '../add-rollup-input.js'; import type { BuildInternals } from '../internal.js'; import { eachPageFromAllPages } from '../internal.js'; diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 91d17e37431c..e2acc4ea56ca 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -28,6 +28,7 @@ import { generatePages } from './generate.js'; import { trackPageData } from './internal.js'; import { type AstroBuildPluginContainer, createPluginContainer } from './plugin.js'; import { registerAllPlugins } from './plugins/index.js'; +import { copyContentToCache } from './plugins/plugin-content.js'; import { RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugins/plugin-manifest.js'; import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js'; @@ -35,7 +36,6 @@ import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plug import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { StaticBuildOptions } from './types.js'; import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js'; -import { copyContentToCache } from './plugins/plugin-content.js'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -108,7 +108,7 @@ export async function viteBuild(opts: StaticBuildOptions) { const ssrOutputs = viteBuildReturnToRollupOutputs(ssrOutput); const clientOutputs = viteBuildReturnToRollupOutputs(clientOutput ?? []); await runPostBuildHooks(container, ssrOutputs, clientOutputs); - if(opts.settings.config.experimental.contentCollectionCache) { + if (opts.settings.config.experimental.contentCollectionCache) { await copyContentToCache(opts); } settings.timer.end('Client build'); diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 405479030a2f..a48a85d16b35 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -155,11 +155,12 @@ export function isModeServerWithNoAdapter(settings: AstroSettings): boolean { return isServerLikeOutput(settings.config) && !settings.adapter; } - export function isContentCollectionsCacheEnabled(config: AstroConfig): boolean { - return config.experimental.contentCollectionCache && + return ( + config.experimental.contentCollectionCache && // contentCollectionsCache is an SSG only feature - !isServerLikeOutput(config); + !isServerLikeOutput(config) + ); } export function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string) { diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 2dfaaac55f07..7ccc63638284 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -16,7 +16,7 @@ import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js import type { Logger } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; import { Pipeline, loadRenderer } from '../core/render/index.js'; -import { isPage, resolveIdToUrl, viteID, isServerLikeOutput } from '../core/util.js'; +import { isPage, isServerLikeOutput, resolveIdToUrl, viteID } from '../core/util.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; import { getStylesForURL } from './css.js'; import { getComponentMetadata } from './metadata.js'; diff --git a/packages/astro/src/vite-plugin-scanner/index.ts b/packages/astro/src/vite-plugin-scanner/index.ts index b0065f1ab67a..78069b937a3a 100644 --- a/packages/astro/src/vite-plugin-scanner/index.ts +++ b/packages/astro/src/vite-plugin-scanner/index.ts @@ -4,7 +4,7 @@ import type { Plugin as VitePlugin } from 'vite'; import { normalizePath } from 'vite'; import type { AstroSettings } from '../@types/astro.js'; import { type Logger } from '../core/logger/core.js'; -import { isEndpoint, isPage, rootRelativePath, isServerLikeOutput } from '../core/util.js'; +import { isEndpoint, isPage, isServerLikeOutput, rootRelativePath } from '../core/util.js'; import { getPrerenderDefault } from '../prerender/utils.js'; import { scan } from './scan.js'; diff --git a/packages/astro/test/experimental-content-collections-render.test.js b/packages/astro/test/experimental-content-collections-render.test.js index cf7a22bad172..19349dc39764 100644 --- a/packages/astro/test/experimental-content-collections-render.test.js +++ b/packages/astro/test/experimental-content-collections-render.test.js @@ -114,10 +114,10 @@ if (!isWindows) { it('Includes CSS for rendered entry', async () => { const html = await fixture.readFile('/launch-week/index.html'); const $ = cheerio.load(html); - + // Renders content assert.equal($('ul li').length, 3); - + // Includes styles assert.equal($('link[rel=stylesheet]').length, 1); }); From 2978287f92dbd135f5c3efc6a037ea1756064d35 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Mon, 6 May 2024 17:35:17 +0200 Subject: [PATCH 049/151] fix(astro): handle AstroUserError during sync and exports types (#10955) Co-authored-by: Luiz Ferraz --- .changeset/strong-peaches-learn.md | 5 +++++ packages/astro/src/core/sync/index.ts | 4 +++- packages/astro/types/content.d.ts | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/strong-peaches-learn.md diff --git a/.changeset/strong-peaches-learn.md b/.changeset/strong-peaches-learn.md new file mode 100644 index 000000000000..263746d6be38 --- /dev/null +++ b/.changeset/strong-peaches-learn.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Handles `AstroUserError`s thrown while syncing content collections and exports `BaseSchema` and `CollectionConfig` types diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index a5763b68f370..b5cfc945b772 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -17,7 +17,7 @@ import { createNodeLogger } from '../config/logging.js'; import { createSettings } from '../config/settings.js'; import { createVite } from '../create-vite.js'; import { collectErrorMetadata } from '../errors/dev/utils.js'; -import { AstroError, AstroErrorData, createSafeError, isAstroError } from '../errors/index.js'; +import { AstroError, AstroErrorData, AstroUserError, createSafeError, isAstroError } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; import { formatErrorMessage } from '../messages.js'; import { ensureProcessNodeEnv } from '../util.js'; @@ -159,9 +159,11 @@ export async function syncContentCollections( if (isAstroError(e)) { throw e; } + const hint = AstroUserError.is(e) ? e.hint : AstroErrorData.GenerateContentTypesError.hint; throw new AstroError( { ...AstroErrorData.GenerateContentTypesError, + hint, message: AstroErrorData.GenerateContentTypesError.message(safeError.message), }, { cause: e } diff --git a/packages/astro/types/content.d.ts b/packages/astro/types/content.d.ts index 45f0d4af79b2..e09bda61844b 100644 --- a/packages/astro/types/content.d.ts +++ b/packages/astro/types/content.d.ts @@ -26,7 +26,7 @@ declare module 'astro:content' { | import('astro/zod').ZodDiscriminatedUnion | import('astro/zod').ZodIntersection ; - type BaseSchema = + export type BaseSchema = | BaseSchemaWithoutEffects | import('astro/zod').ZodEffects ; @@ -42,7 +42,7 @@ declare module 'astro:content' { schema?: S | ((context: SchemaContext) => S); }; - type CollectionConfig = + export type CollectionConfig= | ContentCollectionConfig| DataCollectionConfig; From 23fb790f40fa642392092489c79da4fc372c1182 Mon Sep 17 00:00:00 2001 From: Florian LefebvreDate: Mon, 6 May 2024 15:36:13 +0000 Subject: [PATCH 050/151] [ci] format --- packages/astro/src/core/sync/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index b5cfc945b772..0b4bd1af46d1 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -17,7 +17,13 @@ import { createNodeLogger } from '../config/logging.js'; import { createSettings } from '../config/settings.js'; import { createVite } from '../create-vite.js'; import { collectErrorMetadata } from '../errors/dev/utils.js'; -import { AstroError, AstroErrorData, AstroUserError, createSafeError, isAstroError } from '../errors/index.js'; +import { + AstroError, + AstroErrorData, + AstroUserError, + createSafeError, + isAstroError, +} from '../errors/index.js'; import type { Logger } from '../logger/core.js'; import { formatErrorMessage } from '../messages.js'; import { ensureProcessNodeEnv } from '../util.js'; From e63e96bf32bce270926da6e65c9a331cf9e462d4 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Mon, 6 May 2024 22:02:54 +0200 Subject: [PATCH 051/151] Import metric table from astro:db (#10947) --- .changeset/tough-numbers-build.md | 5 +++++ packages/integrations/web-vitals/src/db-config.ts | 4 ++-- packages/integrations/web-vitals/src/endpoint.ts | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/tough-numbers-build.md diff --git a/.changeset/tough-numbers-build.md b/.changeset/tough-numbers-build.md new file mode 100644 index 000000000000..fdb7eea33c28 --- /dev/null +++ b/.changeset/tough-numbers-build.md @@ -0,0 +1,5 @@ +--- +"@astrojs/web-vitals": patch +--- + +Fixes a runtime issue where Vite was unintentionally pulled into the server code diff --git a/packages/integrations/web-vitals/src/db-config.ts b/packages/integrations/web-vitals/src/db-config.ts index 918850f63580..b7969b14bff2 100644 --- a/packages/integrations/web-vitals/src/db-config.ts +++ b/packages/integrations/web-vitals/src/db-config.ts @@ -1,5 +1,5 @@ import { column, defineDb, defineTable } from 'astro:db'; -import { asDrizzleTable } from '@astrojs/db/utils'; +// import { asDrizzleTable } from '@astrojs/db/utils'; const Metric = defineTable({ columns: { @@ -13,7 +13,7 @@ const Metric = defineTable({ }, }); -export const AstrojsWebVitals_Metric = asDrizzleTable('AstrojsWebVitals_Metric', Metric); +// export const AstrojsWebVitals_Metric = asDrizzleTable('AstrojsWebVitals_Metric', Metric); export default defineDb({ tables: { diff --git a/packages/integrations/web-vitals/src/endpoint.ts b/packages/integrations/web-vitals/src/endpoint.ts index 10dea1ca88aa..b0347d7d30c4 100644 --- a/packages/integrations/web-vitals/src/endpoint.ts +++ b/packages/integrations/web-vitals/src/endpoint.ts @@ -1,6 +1,6 @@ -import { db, sql } from 'astro:db'; +// @ts-expect-error — AstrojsWebVitals requires type-gen which we can’t use. +import { db, sql, AstrojsWebVitals_Metric } from 'astro:db'; import type { APIRoute } from 'astro'; -import { AstrojsWebVitals_Metric } from './db-config.js'; import { ServerMetricSchema } from './schemas.js'; export const prerender = false; From 4b693c0476f04a6956208a5df42c52a70a559fda Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Mon, 6 May 2024 20:03:49 +0000 Subject: [PATCH 052/151] [ci] format --- packages/integrations/web-vitals/src/endpoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/web-vitals/src/endpoint.ts b/packages/integrations/web-vitals/src/endpoint.ts index b0347d7d30c4..d3bf214bb5af 100644 --- a/packages/integrations/web-vitals/src/endpoint.ts +++ b/packages/integrations/web-vitals/src/endpoint.ts @@ -1,5 +1,5 @@ // @ts-expect-error — AstrojsWebVitals requires type-gen which we can’t use. -import { db, sql, AstrojsWebVitals_Metric } from 'astro:db'; +import { AstrojsWebVitals_Metric, db, sql } from 'astro:db'; import type { APIRoute } from 'astro'; import { ServerMetricSchema } from './schemas.js'; From d47baa466aaeedde9c79ed5375d0be34762ac8b6 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 7 May 2024 13:13:03 -0400 Subject: [PATCH 053/151] Support React 19 (#10942) * Support React 19 * Fix lint * Update .changeset/short-phones-breathe.md * fix: update types peer dep --------- Co-authored-by: bholmesdev --- .changeset/short-phones-breathe.md | 5 ++ packages/integrations/react/package.json | 8 +-- packages/integrations/react/src/index.ts | 67 +++++++++++++++++------- 3 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 .changeset/short-phones-breathe.md diff --git a/.changeset/short-phones-breathe.md b/.changeset/short-phones-breathe.md new file mode 100644 index 000000000000..d27d015afbec --- /dev/null +++ b/.changeset/short-phones-breathe.md @@ -0,0 +1,5 @@ +--- +"@astrojs/react": patch +--- + +Updates package to support React 19 beta diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 90eaba4c3f3d..e088be21d8cc 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -60,10 +60,10 @@ "vite": "^5.2.10" }, "peerDependencies": { - "@types/react": "^17.0.50 || ^18.0.21", - "@types/react-dom": "^17.0.17 || ^18.0.6", - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" + "@types/react": "^17.0.50 || ^18.0.21 || npm:types-react@beta", + "@types/react-dom": "^17.0.17 || ^18.0.6 || npm:types-react-dom@beta", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0-beta" }, "engines": { "node": "^18.17.1 || ^20.3.0 || >=21.0.0" diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index e0149e8e76c1..62803e788c8f 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -12,15 +12,44 @@ export type ReactIntegrationOptions = Pick< const FAST_REFRESH_PREAMBLE = react.preambleCode; -function getRenderer() { +const versionsConfig = { + 17: { + server: '@astrojs/react/server-v17.js', + client: '@astrojs/react/client-v17.js', + externals: ['react-dom/server.js', 'react-dom/client.js'], + }, + 18: { + server: '@astrojs/react/server.js', + client: '@astrojs/react/client.js', + externals: ['react-dom/server', 'react-dom/client'] + }, + 19: { + server: '@astrojs/react/server.js', + client: '@astrojs/react/client.js', + externals: ['react-dom/server', 'react-dom/client'] + } +}; + +type SupportedReactVersion = keyof (typeof versionsConfig); +type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion]; + +function getReactMajorVersion(): number { + const matches = /\d+\./.exec(ReactVersion); + if(!matches) { + return NaN; + } + return Number(matches[0]); +} + +function isUnsupportedVersion(majorVersion: number) { + return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion); +} + +function getRenderer(reactConfig: ReactVersionConfig) { return { name: '@astrojs/react', - clientEntrypoint: ReactVersion.startsWith('18.') - ? '@astrojs/react/client.js' - : '@astrojs/react/client-v17.js', - serverEntrypoint: ReactVersion.startsWith('18.') - ? '@astrojs/react/server.js' - : '@astrojs/react/server-v17.js', + clientEntrypoint: reactConfig.client, + serverEntrypoint: reactConfig.server, }; } @@ -51,22 +80,18 @@ function getViteConfiguration({ exclude, babel, experimentalReactChildren, -}: ReactIntegrationOptions = {}) { +}: ReactIntegrationOptions = {}, reactConfig: ReactVersionConfig) { return { optimizeDeps: { include: [ - ReactVersion.startsWith('18.') - ? '@astrojs/react/client.js' - : '@astrojs/react/client-v17.js', + reactConfig.client, 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', ], exclude: [ - ReactVersion.startsWith('18.') - ? '@astrojs/react/server.js' - : '@astrojs/react/server-v17.js', + reactConfig.server, ], }, plugins: [react({ include, exclude, babel }), optionsPlugin(!!experimentalReactChildren)], @@ -74,9 +99,7 @@ function getViteConfiguration({ dedupe: ['react', 'react-dom', 'react-dom/server'], }, ssr: { - external: ReactVersion.startsWith('18.') - ? ['react-dom/server', 'react-dom/client'] - : ['react-dom/server.js', 'react-dom/client.js'], + external: reactConfig.externals, noExternal: [ // These are all needed to get mui to work. '@mui/material', @@ -95,13 +118,19 @@ export default function ({ babel, experimentalReactChildren, }: ReactIntegrationOptions = {}): AstroIntegration { + const majorVersion = getReactMajorVersion(); + if(isUnsupportedVersion(majorVersion)) { + throw new Error(`Unsupported React version: ${majorVersion}.`); + } + const versionConfig = versionsConfig[majorVersion as SupportedReactVersion]; + return { name: '@astrojs/react', hooks: { 'astro:config:setup': ({ command, addRenderer, updateConfig, injectScript }) => { - addRenderer(getRenderer()); + addRenderer(getRenderer(versionConfig)); updateConfig({ - vite: getViteConfiguration({ include, exclude, babel, experimentalReactChildren }), + vite: getViteConfiguration({ include, exclude, babel, experimentalReactChildren }, versionConfig), }); if (command === 'dev') { const preamble = FAST_REFRESH_PREAMBLE.replace(`__BASE__`, '/'); From cceeafb62adf96b6f52b87024d774a30adf7f376 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 7 May 2024 17:13:57 +0000 Subject: [PATCH 054/151] [ci] format --- packages/integrations/react/src/index.ts | 31 ++++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index 62803e788c8f..838640239ee3 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -21,21 +21,21 @@ const versionsConfig = { 18: { server: '@astrojs/react/server.js', client: '@astrojs/react/client.js', - externals: ['react-dom/server', 'react-dom/client'] + externals: ['react-dom/server', 'react-dom/client'], }, 19: { server: '@astrojs/react/server.js', client: '@astrojs/react/client.js', - externals: ['react-dom/server', 'react-dom/client'] - } + externals: ['react-dom/server', 'react-dom/client'], + }, }; -type SupportedReactVersion = keyof (typeof versionsConfig); +type SupportedReactVersion = keyof typeof versionsConfig; type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion]; function getReactMajorVersion(): number { const matches = /\d+\./.exec(ReactVersion); - if(!matches) { + if (!matches) { return NaN; } return Number(matches[0]); @@ -75,12 +75,10 @@ function optionsPlugin(experimentalReactChildren: boolean): vite.Plugin { }; } -function getViteConfiguration({ - include, - exclude, - babel, - experimentalReactChildren, -}: ReactIntegrationOptions = {}, reactConfig: ReactVersionConfig) { +function getViteConfiguration( + { include, exclude, babel, experimentalReactChildren }: ReactIntegrationOptions = {}, + reactConfig: ReactVersionConfig +) { return { optimizeDeps: { include: [ @@ -90,9 +88,7 @@ function getViteConfiguration({ 'react/jsx-dev-runtime', 'react-dom', ], - exclude: [ - reactConfig.server, - ], + exclude: [reactConfig.server], }, plugins: [react({ include, exclude, babel }), optionsPlugin(!!experimentalReactChildren)], resolve: { @@ -119,7 +115,7 @@ export default function ({ experimentalReactChildren, }: ReactIntegrationOptions = {}): AstroIntegration { const majorVersion = getReactMajorVersion(); - if(isUnsupportedVersion(majorVersion)) { + if (isUnsupportedVersion(majorVersion)) { throw new Error(`Unsupported React version: ${majorVersion}.`); } const versionConfig = versionsConfig[majorVersion as SupportedReactVersion]; @@ -130,7 +126,10 @@ export default function ({ 'astro:config:setup': ({ command, addRenderer, updateConfig, injectScript }) => { addRenderer(getRenderer(versionConfig)); updateConfig({ - vite: getViteConfiguration({ include, exclude, babel, experimentalReactChildren }, versionConfig), + vite: getViteConfiguration( + { include, exclude, babel, experimentalReactChildren }, + versionConfig + ), }); if (command === 'dev') { const preamble = FAST_REFRESH_PREAMBLE.replace(`__BASE__`, '/'); From 685fc22bc6247be69a34c3f6945dec058c19fd71 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Wed, 8 May 2024 17:24:47 +0800 Subject: [PATCH 055/151] Improve content collection styles and scripts build perf (#10959) * Improve content collection styles and scripts build perf * Update test It was actually a bug. There was an empty module script injected. * Skip test * Fix test not matching non-ccc behaviour * Workaround bug to make test pass * Update .changeset/grumpy-pillows-develop.md Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Sarah Rainsberger --- .changeset/grumpy-pillows-develop.md | 5 + .../src/content/vite-plugin-content-assets.ts | 59 ++++------- packages/astro/src/core/build/internal.ts | 11 ++- packages/astro/src/core/build/page-data.ts | 4 - .../src/core/build/plugins/plugin-analyzer.ts | 99 ++++--------------- .../src/core/build/plugins/plugin-content.ts | 14 ++- .../src/core/build/plugins/plugin-css.ts | 68 ++++--------- .../build/plugins/plugin-hoisted-scripts.ts | 53 ++++------ packages/astro/src/core/build/types.ts | 2 - ...collections-css-inline-stylesheets.test.js | 5 +- ...imental-content-collections-render.test.js | 2 +- .../css-inline-stylesheets-3/package.json | 8 ++ .../src/components/Button.astro | 86 ++++++++++++++++ .../src/content/en/endeavour.md | 15 +++ .../css-inline-stylesheets-3/src/imported.css | 15 +++ .../src/layouts/Layout.astro | 35 +++++++ .../src/pages/index.astro | 17 ++++ .../mdx/test/css-head-mdx.test.js | 2 +- pnpm-lock.yaml | 6 ++ 19 files changed, 287 insertions(+), 219 deletions(-) create mode 100644 .changeset/grumpy-pillows-develop.md create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/package.json create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro create mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro diff --git a/.changeset/grumpy-pillows-develop.md b/.changeset/grumpy-pillows-develop.md new file mode 100644 index 000000000000..bba2a6fdcc97 --- /dev/null +++ b/.changeset/grumpy-pillows-develop.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Refactors internal handling of styles and scripts for content collections to improve build performance diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index 591cad3c70f6..d3228270a6dc 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -3,8 +3,7 @@ import { pathToFileURL } from 'node:url'; import type { Plugin, Rollup } from 'vite'; import type { AstroSettings, SSRElement } from '../@types/astro.js'; import { getAssetsPrefix } from '../assets/utils/getAssetsPrefix.js'; -import { getParentModuleInfos, moduleIsTopLevelPage } from '../core/build/graph.js'; -import { type BuildInternals, getPageDataByViteID } from '../core/build/internal.js'; +import type { BuildInternals } from '../core/build/internal.js'; import type { AstroBuildPlugin } from '../core/build/plugin.js'; import type { StaticBuildOptions } from '../core/build/types.js'; import type { ModuleLoader } from '../core/module-loader/loader.js'; @@ -163,49 +162,25 @@ export function astroConfigBuildPlugin( chunk.type === 'chunk' && (chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER)) ) { - let entryStyles = new Set (); - let entryLinks = new Set (); - let entryScripts = new Set (); + const entryStyles = new Set (); + const entryLinks = new Set (); + const entryScripts = new Set (); - if (options.settings.config.experimental.contentCollectionCache) { - // TODO: hoisted scripts are still handled on the pageData rather than the asset propagation point - for (const id of chunk.moduleIds) { - const _entryCss = internals.propagatedStylesMap.get(id); - const _entryScripts = internals.propagatedScriptsMap.get(id); - if (_entryCss) { - for (const value of _entryCss) { - if (value.type === 'inline') entryStyles.add(value.content); - if (value.type === 'external') entryLinks.add(value.src); - } - } - if (_entryScripts) { - for (const value of _entryScripts) { - entryScripts.add(value); - } + for (const id of chunk.moduleIds) { + const _entryCss = internals.propagatedStylesMap.get(id); + const _entryScripts = internals.propagatedScriptsMap.get(id); + if (_entryCss) { + // TODO: Separating styles and links this way is not ideal. The `entryCss` list is order-sensitive + // and splitting them into two sets causes the order to be lost, because styles are rendered after + // links. Refactor this away in the future. + for (const value of _entryCss) { + if (value.type === 'inline') entryStyles.add(value.content); + if (value.type === 'external') entryLinks.add(value.src); } } - } else { - for (const id of Object.keys(chunk.modules)) { - for (const pageInfo of getParentModuleInfos(id, ssrPluginContext!)) { - if (moduleIsTopLevelPage(pageInfo)) { - const pageViteID = pageInfo.id; - const pageData = getPageDataByViteID(internals, pageViteID); - if (!pageData) continue; - - const _entryCss = pageData.propagatedStyles?.get(id); - const _entryScripts = pageData.propagatedScripts?.get(id); - if (_entryCss) { - for (const value of _entryCss) { - if (value.type === 'inline') entryStyles.add(value.content); - if (value.type === 'external') entryLinks.add(value.src); - } - } - if (_entryScripts) { - for (const value of _entryScripts) { - entryScripts.add(value); - } - } - } + if (_entryScripts) { + for (const value of _entryScripts) { + entryScripts.add(value); } } } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index a7ff537dc8a4..a2c74271f496 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -25,8 +25,6 @@ export interface BuildInternals { hoistedScriptIdToHoistedMap: Map >; // A mapping of hoisted script ids back to the pages which reference it hoistedScriptIdToPagesMap: Map >; - // A mapping of hoisted script ids back to the content which reference it - hoistedScriptIdToContentMap: Map >; /** * Used by the `directRenderScript` option. If script is inlined, its id and @@ -93,7 +91,15 @@ export interface BuildInternals { cachedClientEntries: string[]; cacheManifestUsed: boolean; + /** + * Map of propagated module ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) + * to a set of stylesheets that it uses. + */ propagatedStylesMap: Map >; + /** + * Map of propagated module ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) + * to a set of hoisted scripts that it uses. + */ propagatedScriptsMap: Map >; // A list of all static files created during the build. Used for SSR. @@ -125,7 +131,6 @@ export function createBuildInternals(): BuildInternals { cssModuleToChunkIdMap: new Map(), hoistedScriptIdToHoistedMap, hoistedScriptIdToPagesMap, - hoistedScriptIdToContentMap: new Map(), inlinedScripts: new Map(), entrySpecifierToBundleMap: new Map (), pageToBundleMap: new Map (), diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index ce9e60622ec4..a151bae2c6f3 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -53,8 +53,6 @@ export async function collectPagesData( route, moduleSpecifier: '', styles: [], - propagatedStyles: new Map(), - propagatedScripts: new Map(), hoistedScript: undefined, hasSharedModules: false, }; @@ -78,8 +76,6 @@ export async function collectPagesData( route, moduleSpecifier: '', styles: [], - propagatedStyles: new Map(), - propagatedScripts: new Map(), hoistedScript: undefined, hasSharedModules: false, }; diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index 5bc0c53e04ea..06ba6fe0025c 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -6,7 +6,6 @@ import type { AstroBuildPlugin } from '../plugin.js'; import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; import { prependForwardSlash } from '../../../core/path.js'; -import { isContentCollectionsCacheEnabled } from '../../../core/util.js'; import { getParentModuleInfos, getTopLevelPageModuleInfos, @@ -32,9 +31,7 @@ export function vitePluginAnalyzer( const pageScripts = new Map< string, { - type: 'page' | 'content'; hoistedSet: Set ; - propagatedMapByImporter: Map >; } >(); @@ -53,48 +50,12 @@ export function vitePluginAnalyzer( if (hoistedScripts.size) { for (const parentInfo of getParentModuleInfos(from, this, isPropagatedAsset)) { if (isPropagatedAsset(parentInfo.id)) { - if (isContentCollectionsCacheEnabled(options.settings.config)) { - if (!pageScripts.has(parentInfo.id)) { - pageScripts.set(parentInfo.id, { - type: 'content', - hoistedSet: new Set(), - propagatedMapByImporter: new Map(), - }); - } - const propagaters = pageScripts.get(parentInfo.id)!.propagatedMapByImporter; - for (const hid of hoistedScripts) { - if (!propagaters.has(parentInfo.id)) { - propagaters.set(parentInfo.id, new Set()); - } - propagaters.get(parentInfo.id)!.add(hid); - } - } else { - for (const nestedParentInfo of getParentModuleInfos(from, this)) { - if (moduleIsTopLevelPage(nestedParentInfo)) { - for (const hid of hoistedScripts) { - if (!pageScripts.has(nestedParentInfo.id)) { - pageScripts.set(nestedParentInfo.id, { - type: 'page', - hoistedSet: new Set(), - propagatedMapByImporter: new Map(), - }); - } - const entry = pageScripts.get(nestedParentInfo.id)!; - if (!entry.propagatedMapByImporter.has(parentInfo.id)) { - entry.propagatedMapByImporter.set(parentInfo.id, new Set()); - } - entry.propagatedMapByImporter.get(parentInfo.id)!.add(hid); - } - } - } - } + internals.propagatedScriptsMap.set(parentInfo.id, hoistedScripts); } else if (moduleIsTopLevelPage(parentInfo)) { for (const hid of hoistedScripts) { if (!pageScripts.has(parentInfo.id)) { pageScripts.set(parentInfo.id, { - type: 'page', hoistedSet: new Set(), - propagatedMapByImporter: new Map(), }); } pageScripts.get(parentInfo.id)?.hoistedSet.add(hid); @@ -105,21 +66,20 @@ export function vitePluginAnalyzer( }, finalize() { - for (const [pageId, { hoistedSet, propagatedMapByImporter, type }] of pageScripts) { - let astroModuleId: string; - if (type === 'page') { - const pageData = getPageDataByViteID(internals, pageId); - if (!pageData) { - continue; - } - const { component } = pageData; - astroModuleId = prependForwardSlash(component); - - // Keep track of the importers - pageData.propagatedScripts = propagatedMapByImporter; - } else { - astroModuleId = pageId; + // Add propagated scripts to client build, + // but DON'T add to pages -> hoisted script map. + for (const propagatedScripts of internals.propagatedScriptsMap.values()) { + for (const propagatedScript of propagatedScripts) { + internals.discoveredScripts.add(propagatedScript); } + } + + for (const [pageId, { hoistedSet }] of pageScripts) { + const pageData = getPageDataByViteID(internals, pageId); + if (!pageData) continue; + + const { component } = pageData; + const astroModuleId = prependForwardSlash(component); const uniqueHoistedId = JSON.stringify(Array.from(hoistedSet).sort()); let moduleId: string; @@ -134,32 +94,13 @@ export function vitePluginAnalyzer( } internals.discoveredScripts.add(moduleId); - // Add propagated scripts to client build, - // but DON'T add to pages -> hoisted script map. - for (const propagatedScripts of propagatedMapByImporter.values()) { - for (const propagatedScript of propagatedScripts) { - internals.discoveredScripts.add(propagatedScript); - } - } - - if (type === 'page') { - // Make sure to track that this page uses this set of hoisted scripts - if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { - const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); - pages!.add(astroModuleId); - } else { - internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId])); - internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); - } + // Make sure to track that this page uses this set of hoisted scripts + if (internals.hoistedScriptIdToPagesMap.has(moduleId)) { + const pages = internals.hoistedScriptIdToPagesMap.get(moduleId); + pages!.add(astroModuleId); } else { - // For content collections save to hoistedScriptIdToContentMap instead - if (internals.hoistedScriptIdToContentMap.has(moduleId)) { - const contentModules = internals.hoistedScriptIdToContentMap.get(moduleId); - contentModules!.add(astroModuleId); - } else { - internals.hoistedScriptIdToContentMap.set(moduleId, new Set([astroModuleId])); - internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); - } + internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId])); + internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet); } } }, diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts index 7210dd4f184b..b6843e52b351 100644 --- a/packages/astro/src/core/build/plugins/plugin-content.ts +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -171,6 +171,7 @@ function vitePluginContent( outputOptions(outputOptions) { const rootPath = normalizePath(fileURLToPath(opts.settings.config.root)); const srcPath = normalizePath(fileURLToPath(opts.settings.config.srcDir)); + const entryCache = new Map (); extendManualChunks(outputOptions, { before(id, meta) { if (id.startsWith(srcPath) && id.slice(srcPath.length).startsWith('content')) { @@ -186,7 +187,11 @@ function vitePluginContent( return resultId; } const [srcRelativePath, flag] = id.replace(rootPath, '/').split('?'); - const collectionEntry = findEntryFromSrcRelativePath(lookupMap, srcRelativePath); + const collectionEntry = findEntryFromSrcRelativePath( + lookupMap, + srcRelativePath, + entryCache + ); if (collectionEntry) { let suffix = '.mjs'; if (flag === PROPAGATED_ASSET_FLAG) { @@ -273,8 +278,11 @@ function vitePluginContent( }; } -const entryCache = new Map (); -function findEntryFromSrcRelativePath(lookupMap: ContentLookupMap, srcRelativePath: string) { +function findEntryFromSrcRelativePath( + lookupMap: ContentLookupMap, + srcRelativePath: string, + entryCache: Map +) { let value = entryCache.get(srcRelativePath); if (value) return value; for (const collection of Object.values(lookupMap)) { diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index e0dce339f769..c50951e0b081 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -5,7 +5,6 @@ import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin, BuildTarget } from '../plugin.js'; import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js'; -import { RESOLVED_VIRTUAL_MODULE_ID as ASTRO_CONTENT_VIRTUAL_MODULE_ID } from '../../../content/consts.js'; import { hasAssetPropagationFlag } from '../../../content/index.js'; import type { AstroPluginCssMetadata } from '../../../vite-plugin-astro/index.js'; import * as assetName from '../css-asset-name.js'; @@ -63,11 +62,8 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { // stylesheet filenames are kept in here until "post", when they are rendered and ready to be inlined const pagesToCss: Record > = {}; - const pagesToPropagatedCss: Record >> = {}; - - const isContentCollectionCache = - options.buildOptions.settings.config.output === 'static' && - options.buildOptions.settings.config.experimental.contentCollectionCache; + // Map of module Ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) to its imported CSS + const moduleIdToPropagatedCss: Record > = {}; const cssBuildPlugin: VitePlugin = { name: 'astro:rollup-plugin-build-css', @@ -141,20 +137,9 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const parentModuleInfos = getParentExtendedModuleInfos(id, this, hasAssetPropagationFlag); for (const { info: pageInfo, depth, order } of parentModuleInfos) { if (hasAssetPropagationFlag(pageInfo.id)) { - const walkId = isContentCollectionCache ? ASTRO_CONTENT_VIRTUAL_MODULE_ID : id; - for (const parentInfo of getParentModuleInfos(walkId, this)) { - if (moduleIsTopLevelPage(parentInfo) === false) continue; - - const pageViteID = parentInfo.id; - const pageData = getPageDataByViteID(internals, pageViteID); - if (pageData === undefined) continue; - - for (const css of meta.importedCss) { - const propagatedStyles = (pagesToPropagatedCss[pageData.moduleSpecifier] ??= {}); - const existingCss = (propagatedStyles[pageInfo.id] ??= new Set()); - - existingCss.add(css); - } + const propagatedCss = (moduleIdToPropagatedCss[pageInfo.id] ??= new Set()); + for (const css of meta.importedCss) { + propagatedCss.add(css); } } else if (moduleIsTopLevelPage(pageInfo)) { const pageViteID = pageInfo.id; @@ -251,41 +236,30 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { ? { type: 'inline', content: stylesheet.source } : { type: 'external', src: stylesheet.fileName }; - const pages = Array.from(eachPageData(internals)); let sheetAddedToPage = false; - pages.forEach((pageData) => { + // Apply `pagesToCss` information to the respective `pageData.styles` + for (const pageData of eachPageData(internals)) { const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName]; if (orderingInfo !== undefined) { pageData.styles.push({ ...orderingInfo, sheet }); sheetAddedToPage = true; - return; } + } - const propagatedPaths = pagesToPropagatedCss[pageData.moduleSpecifier]; - if (propagatedPaths === undefined) return; - Object.entries(propagatedPaths).forEach(([pageInfoId, css]) => { - // return early if sheet does not need to be propagated - if (css.has(stylesheet.fileName) !== true) return; - - // return early if the stylesheet needing propagation has already been included - if (pageData.styles.some((s) => s.sheet === sheet)) return; - - let propagatedStyles: Set ; - if (isContentCollectionCache) { - propagatedStyles = - internals.propagatedStylesMap.get(pageInfoId) ?? - internals.propagatedStylesMap.set(pageInfoId, new Set()).get(pageInfoId)!; - } else { - propagatedStyles = - pageData.propagatedStyles.get(pageInfoId) ?? - pageData.propagatedStyles.set(pageInfoId, new Set()).get(pageInfoId)!; - } - - propagatedStyles.add(sheet); - sheetAddedToPage = true; - }); - }); + // Apply `moduleIdToPropagatedCss` information to `internals.propagatedStylesMap`. + // NOTE: It's pretty much a copy over to `internals.propagatedStylesMap` as it should be + // completely empty. The whole propagation handling could be better refactored in the future. + for (const moduleId in moduleIdToPropagatedCss) { + if (!moduleIdToPropagatedCss[moduleId].has(stylesheet.fileName)) continue; + let propagatedStyles = internals.propagatedStylesMap.get(moduleId); + if (!propagatedStyles) { + propagatedStyles = new Set(); + internals.propagatedStylesMap.set(moduleId, propagatedStyles); + } + propagatedStyles.add(sheet); + sheetAddedToPage = true; + } if (toBeInlined && sheetAddedToPage) { // CSS is already added to all used pages, we can delete it from the bundle diff --git a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts index 80bfa6a6e964..2ed3c7fa746d 100644 --- a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -1,6 +1,6 @@ import type { BuildOptions, Plugin as VitePlugin } from 'vite'; import type { AstroSettings } from '../../../@types/astro.js'; -import { isContentCollectionsCacheEnabled, viteID } from '../../util.js'; +import { viteID } from '../../util.js'; import type { BuildInternals } from '../internal.js'; import { getPageDataByViteID } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; @@ -72,42 +72,23 @@ export function vitePluginHoistedScripts( output.dynamicImports.length === 0 && shouldInlineAsset(output.code, output.fileName, assetsInlineLimit); let removeFromBundle = false; - const facadeId = output.facadeModuleId!; - - // Pages - if (internals.hoistedScriptIdToPagesMap.has(facadeId)) { - const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; - for (const pathname of pages) { - const vid = viteID(new URL('.' + pathname, settings.config.root)); - - const pageInfo = getPageDataByViteID(internals, vid); - if (pageInfo) { - if (canBeInlined) { - pageInfo.hoistedScript = { - type: 'inline', - value: output.code, - }; - removeFromBundle = true; - } else { - pageInfo.hoistedScript = { - type: 'external', - value: id, - }; - } - } - } - } - // Content collection entries - else { - const contentModules = internals.hoistedScriptIdToContentMap.get(facadeId)!; - for (const contentId of contentModules) { - if (isContentCollectionsCacheEnabled(settings.config)) { - const scripts = - internals.propagatedScriptsMap.get(contentId) ?? - internals.propagatedScriptsMap.set(contentId, new Set()).get(contentId)!; - - scripts.add(facadeId); + const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!; + for (const pathname of pages) { + const vid = viteID(new URL('.' + pathname, settings.config.root)); + const pageInfo = getPageDataByViteID(internals, vid); + if (pageInfo) { + if (canBeInlined) { + pageInfo.hoistedScript = { + type: 'inline', + value: output.code, + }; + removeFromBundle = true; + } else { + pageInfo.hoistedScript = { + type: 'external', + value: id, + }; } } } diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 9b418f7bc56d..4b502c353c5a 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -26,8 +26,6 @@ export interface PageBuildData { component: ComponentPath; route: RouteData; moduleSpecifier: string; - propagatedStyles: Map >; - propagatedScripts: Map >; hoistedScript: HoistedScriptAsset | undefined; styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>; hasSharedModules: boolean; diff --git a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js index a4aede4966a5..d6c509de5b64 100644 --- a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js +++ b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js @@ -154,7 +154,10 @@ describe('Setting inlineStylesheets to always in static output', () => { // to bust cache and prevent modules and their state // from being reused site: 'https://test.net/', - root: './fixtures/css-inline-stylesheets/', + // TODO: Uses -3 variant to bust ESM module cache when rendering the pages. Particularly in + // `node_modules/.astro/content/entry.mjs` and `import('./en/endeavour.mjs')`. Ideally this + // should be solved in core, but using this workaround for now. + root: './fixtures/css-inline-stylesheets-3/', output: 'static', build: { inlineStylesheets: 'always', diff --git a/packages/astro/test/experimental-content-collections-render.test.js b/packages/astro/test/experimental-content-collections-render.test.js index 19349dc39764..9e1dcdb4614e 100644 --- a/packages/astro/test/experimental-content-collections-render.test.js +++ b/packages/astro/test/experimental-content-collections-render.test.js @@ -72,7 +72,7 @@ if (!isWindows) { // Includes hoisted script assert.notEqual( - [...allScripts].find((script) => $(script).attr('src')?.includes('hoisted')), + [...allScripts].find((script) => $(script).attr('src')?.includes('/_astro/WithScripts')), undefined, 'hoisted script missing from head.' ); diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/package.json b/packages/astro/test/fixtures/css-inline-stylesheets-3/package.json new file mode 100644 index 000000000000..00e58c587604 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/css-inline-stylesheets-3", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro new file mode 100644 index 000000000000..3f25cbd3e3ae --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro @@ -0,0 +1,86 @@ +--- +const { class: className = '', style, href } = Astro.props; +const { variant = 'primary' } = Astro.props; +--- + + + + + + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md new file mode 100644 index 000000000000..240eeeae3993 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md @@ -0,0 +1,15 @@ +--- +title: Endeavour +description: 'Learn about the Endeavour NASA space shuttle.' +publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)' +layout: '../../layouts/Layout.astro' +tags: [space, 90s] +--- + +**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) + +Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly. + +The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986. + +NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly. \ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css new file mode 100644 index 000000000000..3959523ff16e --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css @@ -0,0 +1,15 @@ +.bg-skyblue { + background: skyblue; +} + +.bg-lightcoral { + background: lightcoral; +} + +.red { + color: darkred; +} + +.blue { + color: royalblue; +} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro new file mode 100644 index 000000000000..0a26655189f5 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro @@ -0,0 +1,35 @@ +--- +import Button from '../components/Button.astro'; +import '../imported.css'; + +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + {title} + + + ++ + + diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro new file mode 100644 index 000000000000..2aecfb0f2eb7 --- /dev/null +++ b/packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro @@ -0,0 +1,17 @@ +--- +import { getEntryBySlug } from 'astro:content'; +import Button from '../components/Button.astro'; + +const entry = await getEntryBySlug('en', 'endeavour'); +const { Content } = await entry.render(); +--- + + + diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index 5caab3d059b8..083348015c54 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -50,7 +50,7 @@ describe('Head injection w/ MDX', () => { assert.equal(links.length, 1); const scripts = document.querySelectorAll('head script[type=module]'); - assert.equal(scripts.length, 2); + assert.equal(scripts.length, 1); }); it('Using component using slots.render() API', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be362ddc2641..225e8e10ec66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2612,6 +2612,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/css-inline-stylesheets-3: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/css-no-code-split: dependencies: astro: From ddd8e49d1a179bec82310fb471f822a1567a6610 Mon Sep 17 00:00:00 2001 From: Bjorn LuWelcome to Astro
++ + Date: Wed, 8 May 2024 17:25:27 +0800 Subject: [PATCH 056/151] MDX integration v3 (#10935) * fix(mdx): convert remark-images-to-component plugin to a rehype plugin (#10697) * Remove fs read for MDX transform (#10866) * Tag MDX component for faster checks when rendering (#10864) * Use unified plugin only for MDX transform (#10869) * Only traverse children and handle mdxJsxTextElement when optimizing (#10885) * Rename to `optimize.ignoreComponentNames` in MDX (#10884) * Allow remark/rehype plugins added after mdx to work (#10877) * Improve MDX optimize with sibling nodes (#10887) * Improve types in rehype-optimize-static.ts * Rename `ignoreComponentNames` to `ignoreElementNames` I think this better reflects what it's actually doing * Simplify plain MDX nodes in optimize option (#10934) * Format code * Minimize diff changes * Update .changeset/slimy-cobras-end.md Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Armand Philippot <59021693+ArmandPhilippot@users.noreply.github.com> Co-authored-by: Sarah Rainsberger --- .changeset/blue-geese-visit.md | 5 + .changeset/chilly-items-help.md | 5 + .changeset/fresh-masks-agree.md | 5 + .changeset/friendly-plants-leave.md | 5 + .changeset/large-glasses-jam.md | 5 + .changeset/slimy-cobras-end.md | 7 + .changeset/small-oranges-report.md | 5 + .changeset/smart-rats-mate.md | 5 + .changeset/sweet-goats-own.md | 5 + .changeset/tame-avocados-relax.md | 5 + .changeset/violet-snails-call.md | 5 + .changeset/young-chicken-exercise.md | 5 + packages/astro/package.json | 2 + packages/astro/src/jsx/babel.ts | 3 + packages/astro/src/jsx/rehype.ts | 320 ++++++++++++++++++ packages/astro/src/jsx/server.ts | 38 ++- packages/astro/src/jsx/transform-options.ts | 3 + packages/astro/src/vite-plugin-mdx/index.ts | 4 +- packages/astro/src/vite-plugin-mdx/tag.ts | 2 + .../src/vite-plugin-mdx/transform-jsx.ts | 3 + .../units/dev/collections-renderentry.test.js | 12 - packages/integrations/mdx/package.json | 4 +- packages/integrations/mdx/src/README.md | 39 ++- packages/integrations/mdx/src/index.ts | 29 +- packages/integrations/mdx/src/plugins.ts | 16 +- .../mdx/src/rehype-images-to-component.ts | 166 +++++++++ .../mdx/src/rehype-optimize-static.ts | 251 ++++++++++++-- .../mdx/src/remark-images-to-component.ts | 156 --------- .../mdx/src/vite-plugin-mdx-postprocess.ts | 75 ++-- .../integrations/mdx/src/vite-plugin-mdx.ts | 50 +-- .../fixtures/mdx-optimize/astro.config.mjs | 2 +- .../integrations/mdx/test/mdx-plugins.test.js | 24 ++ .../test/units/rehype-optimize-static.test.js | 89 +++++ pnpm-lock.yaml | 12 + 34 files changed, 1081 insertions(+), 281 deletions(-) create mode 100644 .changeset/blue-geese-visit.md create mode 100644 .changeset/chilly-items-help.md create mode 100644 .changeset/fresh-masks-agree.md create mode 100644 .changeset/friendly-plants-leave.md create mode 100644 .changeset/large-glasses-jam.md create mode 100644 .changeset/slimy-cobras-end.md create mode 100644 .changeset/small-oranges-report.md create mode 100644 .changeset/smart-rats-mate.md create mode 100644 .changeset/sweet-goats-own.md create mode 100644 .changeset/tame-avocados-relax.md create mode 100644 .changeset/violet-snails-call.md create mode 100644 .changeset/young-chicken-exercise.md create mode 100644 packages/astro/src/jsx/rehype.ts create mode 100644 packages/integrations/mdx/src/rehype-images-to-component.ts delete mode 100644 packages/integrations/mdx/src/remark-images-to-component.ts create mode 100644 packages/integrations/mdx/test/units/rehype-optimize-static.test.js diff --git a/.changeset/blue-geese-visit.md b/.changeset/blue-geese-visit.md new file mode 100644 index 000000000000..408386d046c3 --- /dev/null +++ b/.changeset/blue-geese-visit.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Simplifies plain MDX components as hast element nodes to further improve HTML string inlining for the `optimize` option diff --git a/.changeset/chilly-items-help.md b/.changeset/chilly-items-help.md new file mode 100644 index 000000000000..7e868474e32c --- /dev/null +++ b/.changeset/chilly-items-help.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Improves the error message when failed to render MDX components diff --git a/.changeset/fresh-masks-agree.md b/.changeset/fresh-masks-agree.md new file mode 100644 index 000000000000..08fc812c8841 --- /dev/null +++ b/.changeset/fresh-masks-agree.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": major +--- + +Refactors the MDX transformation to rely only on the unified pipeline. Babel and esbuild transformations are removed, which should result in faster build times. The refactor requires using Astro v4.8.0 but no other changes are necessary. diff --git a/.changeset/friendly-plants-leave.md b/.changeset/friendly-plants-leave.md new file mode 100644 index 000000000000..c972fa42c4db --- /dev/null +++ b/.changeset/friendly-plants-leave.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Exports `astro/jsx/rehype.js` with utilities to generate an Astro metadata object diff --git a/.changeset/large-glasses-jam.md b/.changeset/large-glasses-jam.md new file mode 100644 index 000000000000..885471d82fba --- /dev/null +++ b/.changeset/large-glasses-jam.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Allows Vite plugins to transform `.mdx` files before the MDX plugin transforms it diff --git a/.changeset/slimy-cobras-end.md b/.changeset/slimy-cobras-end.md new file mode 100644 index 000000000000..58f22ac07c12 --- /dev/null +++ b/.changeset/slimy-cobras-end.md @@ -0,0 +1,7 @@ +--- +"@astrojs/mdx": major +--- + +Allows integrations after the MDX integration to update `markdown.remarkPlugins` and `markdown.rehypePlugins`, and have the plugins work in MDX too. + +If your integration relies on Astro's previous behavior that prevents integrations from adding remark/rehype plugins for MDX, you will now need to configure `@astrojs/mdx` with `extendMarkdownConfig: false` and explicitly specify any `remarkPlugins` and `rehypePlugins` options instead. diff --git a/.changeset/small-oranges-report.md b/.changeset/small-oranges-report.md new file mode 100644 index 000000000000..8d0906e0530b --- /dev/null +++ b/.changeset/small-oranges-report.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": major +--- + +Renames the `optimize.customComponentNames` option to `optimize.ignoreElementNames` to better reflect its usecase. Its behaviour is not changed and should continue to work as before. diff --git a/.changeset/smart-rats-mate.md b/.changeset/smart-rats-mate.md new file mode 100644 index 000000000000..b779a86c8a5b --- /dev/null +++ b/.changeset/smart-rats-mate.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Updates the `optimize` option to group static sibling nodes as a ` `. This reduces the number of AST nodes and simplifies runtime rendering of MDX pages. diff --git a/.changeset/sweet-goats-own.md b/.changeset/sweet-goats-own.md new file mode 100644 index 000000000000..6689246c33b3 --- /dev/null +++ b/.changeset/sweet-goats-own.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": major +--- + +Replaces the internal `remark-images-to-component` plugin with `rehype-images-to-component` to let users use additional rehype plugins for images diff --git a/.changeset/tame-avocados-relax.md b/.changeset/tame-avocados-relax.md new file mode 100644 index 000000000000..9b6a36881c03 --- /dev/null +++ b/.changeset/tame-avocados-relax.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Tags the MDX component export for quicker component checks while rendering diff --git a/.changeset/violet-snails-call.md b/.changeset/violet-snails-call.md new file mode 100644 index 000000000000..b7f06a7b9321 --- /dev/null +++ b/.changeset/violet-snails-call.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Fixes `export const components` keys detection for the `optimize` option diff --git a/.changeset/young-chicken-exercise.md b/.changeset/young-chicken-exercise.md new file mode 100644 index 000000000000..04b7417bbe21 --- /dev/null +++ b/.changeset/young-chicken-exercise.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Improves `optimize` handling for MDX components with attributes and inline MDX components diff --git a/packages/astro/package.json b/packages/astro/package.json index 6c3bcfeddbf3..572d5a9863f8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -209,6 +209,8 @@ "astro-scripts": "workspace:*", "cheerio": "1.0.0-rc.12", "eol": "^0.9.1", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.1.2", "memfs": "^4.9.1", "node-mocks-http": "^1.14.1", "parse-srcset": "^1.0.2", diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts index e8f9da87e2e1..d5fc0ccd30b0 100644 --- a/packages/astro/src/jsx/babel.ts +++ b/packages/astro/src/jsx/babel.ts @@ -134,6 +134,9 @@ function addClientOnlyMetadata( } } +/** + * @deprecated This plugin is no longer used. Remove in Astro 5.0 + */ export default function astroJSX(): PluginObj { return { visitor: { diff --git a/packages/astro/src/jsx/rehype.ts b/packages/astro/src/jsx/rehype.ts new file mode 100644 index 000000000000..40a8359cbe5c --- /dev/null +++ b/packages/astro/src/jsx/rehype.ts @@ -0,0 +1,320 @@ +import type { RehypePlugin } from '@astrojs/markdown-remark'; +import type { RootContent } from 'hast'; +import type { + MdxJsxAttribute, + MdxJsxFlowElementHast, + MdxJsxTextElementHast, +} from 'mdast-util-mdx-jsx'; +import { visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; +import { AstroError } from '../core/errors/errors.js'; +import { AstroErrorData } from '../core/errors/index.js'; +import { resolvePath } from '../core/util.js'; +import type { PluginMetadata } from '../vite-plugin-astro/types.js'; + +// This import includes ambient types for hast to include mdx nodes +import type {} from 'mdast-util-mdx'; + +const ClientOnlyPlaceholder = 'astro-client-only'; + +export const rehypeAnalyzeAstroMetadata: RehypePlugin = () => { + return (tree, file) => { + // Initial metadata for this MDX file, it will be mutated as we traverse the tree + const metadata: PluginMetadata['astro'] = { + clientOnlyComponents: [], + hydratedComponents: [], + scripts: [], + containsHead: false, + propagation: 'none', + pageOptions: {}, + }; + + // Parse imports in this file. This is used to match components with their import source + const imports = parseImports(tree.children); + + visit(tree, (node) => { + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return; + + const tagName = node.name; + if (!tagName || !isComponent(tagName) || !hasClientDirective(node)) return; + + // From this point onwards, `node` is confirmed to be an island component + + // Match this component with its import source + const matchedImport = findMatchingImport(tagName, imports); + if (!matchedImport) { + throw new AstroError({ + ...AstroErrorData.NoMatchingImport, + message: AstroErrorData.NoMatchingImport.message(node.name!), + }); + } + + // If this is an Astro component, that means the `client:` directive is misused as it doesn't + // work on Astro components as it's server-side only. Warn the user about this. + if (matchedImport.path.endsWith('.astro')) { + const clientAttribute = node.attributes.find( + (attr) => attr.type === 'mdxJsxAttribute' && attr.name.startsWith('client:') + ) as MdxJsxAttribute | undefined; + if (clientAttribute) { + // eslint-disable-next-line + console.warn( + `You are attempting to render <${node.name!} ${ + clientAttribute.name + } />, but ${node.name!} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.` + ); + } + } + + const resolvedPath = resolvePath(matchedImport.path, file.path); + + if (hasClientOnlyDirective(node)) { + // Add this component to the metadata + metadata.clientOnlyComponents.push({ + exportName: matchedImport.name, + specifier: tagName, + resolvedPath, + }); + // Mutate node with additional island attributes + addClientOnlyMetadata(node, matchedImport, resolvedPath); + } else { + // Add this component to the metadata + metadata.hydratedComponents.push({ + exportName: '*', + specifier: tagName, + resolvedPath, + }); + // Mutate node with additional island attributes + addClientMetadata(node, matchedImport, resolvedPath); + } + }); + + // Attach final metadata here, which can later be retrieved by `getAstroMetadata` + file.data.__astroMetadata = metadata; + }; +}; + +export function getAstroMetadata(file: VFile) { + return file.data.__astroMetadata as PluginMetadata['astro'] | undefined; +} + +type ImportSpecifier = { local: string; imported: string }; + +/** + * ``` + * import Foo from './Foo.jsx' + * import { Bar } from './Bar.jsx' + * import { Baz as Wiz } from './Bar.jsx' + * import * as Waz from './BaWazz.jsx' + * + * // => Map { + * // "./Foo.jsx" => Set { { local: "Foo", imported: "default" } }, + * // "./Bar.jsx" => Set { + * // { local: "Bar", imported: "Bar" } + * // { local: "Wiz", imported: "Baz" }, + * // }, + * // "./Waz.jsx" => Set { { local: "Waz", imported: "*" } }, + * // } + * ``` + */ +function parseImports(children: RootContent[]) { + // Map of import source to its imported specifiers + const imports = new Map >(); + + for (const child of children) { + if (child.type !== 'mdxjsEsm') continue; + + const body = child.data?.estree?.body; + if (!body) continue; + + for (const ast of body) { + if (ast.type !== 'ImportDeclaration') continue; + + const source = ast.source.value as string; + const specs: ImportSpecifier[] = ast.specifiers.map((spec) => { + switch (spec.type) { + case 'ImportDefaultSpecifier': + return { local: spec.local.name, imported: 'default' }; + case 'ImportNamespaceSpecifier': + return { local: spec.local.name, imported: '*' }; + case 'ImportSpecifier': + return { local: spec.local.name, imported: spec.imported.name }; + default: + throw new Error('Unknown import declaration specifier: ' + spec); + } + }); + + // Get specifiers set from source or initialize a new one + let specSet = imports.get(source); + if (!specSet) { + specSet = new Set(); + imports.set(source, specSet); + } + + for (const spec of specs) { + specSet.add(spec); + } + } + } + + return imports; +} + +function isComponent(tagName: string) { + return ( + (tagName[0] && tagName[0].toLowerCase() !== tagName[0]) || + tagName.includes('.') || + /[^a-zA-Z]/.test(tagName[0]) + ); +} + +function hasClientDirective(node: MdxJsxFlowElementHast | MdxJsxTextElementHast) { + return node.attributes.some( + (attr) => attr.type === 'mdxJsxAttribute' && attr.name.startsWith('client:') + ); +} + +function hasClientOnlyDirective(node: MdxJsxFlowElementHast | MdxJsxTextElementHast) { + return node.attributes.some( + (attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'client:only' + ); +} + +type MatchedImport = { name: string; path: string }; + +/** + * ``` + * import Button from './Button.jsx' + * + * // => { name: "default", path: "./Button.jsx" } + * + * import { Button } from './Button.jsx' + * + * // => { name: "Button", path: "./Button.jsx" } + * + * import * as buttons from './Button.jsx' + * + * // => { name: "Foo.Bar", path: "./Button.jsx" } + * + * import { buttons } from './Button.jsx' + * + * // => { name: "buttons.Foo.Bar", path: "./Button.jsx" } + * + * import buttons from './Button.jsx' + * + * // => { name: "default.Foo.Bar", path: "./Button.jsx" } + * ``` + */ +function findMatchingImport( + tagName: string, + imports: Map > +): MatchedImport | undefined { + const tagSpecifier = tagName.split('.')[0]; + for (const [source, specs] of imports) { + for (const { imported, local } of specs) { + if (local === tagSpecifier) { + // If tagName access properties, we need to make sure the returned `name` + // properly access the properties from `path` + if (tagSpecifier !== tagName) { + switch (imported) { + // Namespace import: " " => name: "Foo.Bar" + case '*': { + const accessPath = tagName.slice(tagSpecifier.length + 1); + return { name: accessPath, path: source }; + } + // Default import: " " => name: "default.Foo.Bar" + case 'default': { + // "buttons.Foo.Bar" => "Foo.Bar" + const accessPath = tagName.slice(tagSpecifier.length + 1); + return { name: `default.${accessPath}`, path: source }; + } + // Named import: " " => name: "buttons.Foo.Bar" + default: { + return { name: tagName, path: source }; + } + } + } + + return { name: imported, path: source }; + } + } + } +} + +function addClientMetadata( + node: MdxJsxFlowElementHast | MdxJsxTextElementHast, + meta: MatchedImport, + resolvedPath: string +) { + const attributeNames = node.attributes + .map((attr) => (attr.type === 'mdxJsxAttribute' ? attr.name : null)) + .filter(Boolean); + + if (!attributeNames.includes('client:component-path')) { + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:component-path', + value: resolvedPath, + }); + } + if (!attributeNames.includes('client:component-export')) { + if (meta.name === '*') { + meta.name = node.name!.split('.').slice(1).join('.')!; + } + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:component-export', + value: meta.name, + }); + } + if (!attributeNames.includes('client:component-hydration')) { + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:component-hydration', + value: null, + }); + } +} + +function addClientOnlyMetadata( + node: MdxJsxFlowElementHast | MdxJsxTextElementHast, + meta: { path: string; name: string }, + resolvedPath: string +) { + const attributeNames = node.attributes + .map((attr) => (attr.type === 'mdxJsxAttribute' ? attr.name : null)) + .filter(Boolean); + + if (!attributeNames.includes('client:display-name')) { + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:display-name', + value: node.name, + }); + } + if (!attributeNames.includes('client:component-hydpathation')) { + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:component-path', + value: resolvedPath, + }); + } + if (!attributeNames.includes('client:component-export')) { + if (meta.name === '*') { + meta.name = node.name!.split('.').slice(1).join('.')!; + } + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:component-export', + value: meta.name, + }); + } + if (!attributeNames.includes('client:component-hydration')) { + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'client:component-hydration', + value: null, + }); + } + + node.name = ClientOnlyPlaceholder; +} diff --git a/packages/astro/src/jsx/server.ts b/packages/astro/src/jsx/server.ts index d445ee3a559b..2ed308c37e82 100644 --- a/packages/astro/src/jsx/server.ts +++ b/packages/astro/src/jsx/server.ts @@ -4,6 +4,8 @@ import { renderJSX } from '../runtime/server/jsx.js'; const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); +// NOTE: In practice, MDX components are always tagged with `__astro_tag_component__`, so the right renderer +// is used directly, and this check is not often used to return true. export async function check( Component: any, props: any, @@ -19,18 +21,7 @@ export async function check( const result = await Component({ ...props, ...slots, children }); return result[AstroJSX]; } catch (e) { - const error = e as Error; - // if the exception is from an mdx component - // throw an error - if (Component[Symbol.for('mdx-component')]) { - throw new AstroError({ - message: error.message, - title: error.name, - hint: `This issue often occurs when your MDX component encounters runtime errors.`, - name: error.name, - stack: error.stack, - }); - } + throwEnhancedErrorIfMdxComponent(e as Error, Component); } return false; } @@ -48,8 +39,27 @@ export async function renderToStaticMarkup( } const { result } = this; - const html = await renderJSX(result, jsx(Component, { ...props, ...slots, children })); - return { html }; + try { + const html = await renderJSX(result, jsx(Component, { ...props, ...slots, children })); + return { html }; + } catch (e) { + throwEnhancedErrorIfMdxComponent(e as Error, Component); + throw e; + } +} + +function throwEnhancedErrorIfMdxComponent(error: Error, Component: any) { + // if the exception is from an mdx component + // throw an error + if (Component[Symbol.for('mdx-component')]) { + throw new AstroError({ + message: error.message, + title: error.name, + hint: `This issue often occurs when your MDX component encounters runtime errors.`, + name: error.name, + stack: error.stack, + }); + } } export default { diff --git a/packages/astro/src/jsx/transform-options.ts b/packages/astro/src/jsx/transform-options.ts index 4b51d85b8b04..ca1d50a6a131 100644 --- a/packages/astro/src/jsx/transform-options.ts +++ b/packages/astro/src/jsx/transform-options.ts @@ -1,5 +1,8 @@ import type { JSXTransformConfig } from '../@types/astro.js'; +/** + * @deprecated This function is no longer used. Remove in Astro 5.0 + */ export async function jsxTransformOptions(): Promise { // @ts-expect-error types not found const plugin = await import('@babel/plugin-transform-react-jsx'); diff --git a/packages/astro/src/vite-plugin-mdx/index.ts b/packages/astro/src/vite-plugin-mdx/index.ts index 7e86aed288f4..1c8e2ced6d81 100644 --- a/packages/astro/src/vite-plugin-mdx/index.ts +++ b/packages/astro/src/vite-plugin-mdx/index.ts @@ -9,7 +9,9 @@ const SPECIAL_QUERY_REGEX = new RegExp( `[?&](?:worker|sharedworker|raw|url|${CONTENT_FLAG}|${PROPAGATED_ASSET_FLAG})\\b` ); -// TODO: Move this Vite plugin into `@astrojs/mdx` in Astro 5 +/** + * @deprecated This plugin is no longer used. Remove in Astro 5.0 + */ export default function mdxVitePlugin(): Plugin { return { name: 'astro:jsx', diff --git a/packages/astro/src/vite-plugin-mdx/tag.ts b/packages/astro/src/vite-plugin-mdx/tag.ts index 3b774a0a238d..0bf9722a56f2 100644 --- a/packages/astro/src/vite-plugin-mdx/tag.ts +++ b/packages/astro/src/vite-plugin-mdx/tag.ts @@ -11,6 +11,8 @@ const rendererName = astroJsxRenderer.name; * * This plugin crawls each export in the file and "tags" each export with a given `rendererName`. * This allows us to automatically match a component to a renderer and skip the usual `check()` calls. + * + * @deprecated This plugin is no longer used. Remove in Astro 5.0 */ export const tagExportsPlugin: PluginObj = { visitor: { diff --git a/packages/astro/src/vite-plugin-mdx/transform-jsx.ts b/packages/astro/src/vite-plugin-mdx/transform-jsx.ts index 07eb87d0465e..e2e8e97bc6b1 100644 --- a/packages/astro/src/vite-plugin-mdx/transform-jsx.ts +++ b/packages/astro/src/vite-plugin-mdx/transform-jsx.ts @@ -5,6 +5,9 @@ import { jsxTransformOptions } from '../jsx/transform-options.js'; import type { PluginMetadata } from '../vite-plugin-astro/types.js'; import { tagExportsPlugin } from './tag.js'; +/** + * @deprecated This function is no longer used. Remove in Astro 5.0 + */ export async function transformJSX( code: string, id: string, diff --git a/packages/astro/test/units/dev/collections-renderentry.test.js b/packages/astro/test/units/dev/collections-renderentry.test.js index 4c3849577629..3fa872289404 100644 --- a/packages/astro/test/units/dev/collections-renderentry.test.js +++ b/packages/astro/test/units/dev/collections-renderentry.test.js @@ -84,18 +84,6 @@ _describe('Content Collections - render()', () => { it('can be used in a layout component', async () => { const fs = createFsWithFallback( { - // Loading the content config with `astro:content` oddly - // causes this test to fail. Spoof a different src/content entry - // to ensure `existsSync` checks pass. - // TODO: revisit after addressing this issue - // https://github.com/withastro/astro/issues/6121 - '/src/content/blog/promo/launch-week.mdx': `--- -title: Launch Week -description: Astro is launching this week! ---- -# Launch Week -- [x] Launch Astro -- [ ] Celebrate`, '/src/components/Layout.astro': ` --- import { getCollection } from 'astro:content'; diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 8eb0bce21da1..5ea24d1609a7 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -50,10 +50,11 @@ "vfile": "^6.0.1" }, "peerDependencies": { - "astro": "^4.0.0" + "astro": "^4.8.0" }, "devDependencies": { "@types/estree": "^1.0.5", + "@types/hast": "^3.0.3", "@types/mdast": "^4.0.3", "@types/yargs-parser": "^21.0.3", "astro": "workspace:*", @@ -61,6 +62,7 @@ "cheerio": "1.0.0-rc.12", "linkedom": "^0.16.11", "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.1.2", "mdast-util-to-string": "^4.0.0", "reading-time": "^1.5.0", "rehype-mathjax": "^6.0.0", diff --git a/packages/integrations/mdx/src/README.md b/packages/integrations/mdx/src/README.md index bbbc6075c8af..3fc991b77c91 100644 --- a/packages/integrations/mdx/src/README.md +++ b/packages/integrations/mdx/src/README.md @@ -30,12 +30,7 @@ After: ```jsx function _createMdxContent() { - return ( - <> - My MDX Content
- - > - ); + return; } ``` @@ -49,15 +44,20 @@ The next section explains the algorithm, which you can follow along by pairing w ### How it works -Two variables: +The flow can be divided into a "scan phase" and a "mutation phase". The scan phase searches for nodes that can be optimized, and the mutation phase applies the optimization on the `hast` nodes. + +#### Scan phase + +Variables: - `allPossibleElements`: A set of subtree roots where we can add a new `set:html` property with its children as value. - `elementStack`: The stack of elements (that could be subtree roots) while traversing the `hast` (node ancestors). +- `elementMetadatas`: A weak map to store the metadata used only by the mutation phase later. Flow: 1. Walk the `hast` tree. -2. For each `node` we enter, if the `node` is static (`type` is `element` or `mdxJsxFlowElement`), record in `allPossibleElements` and push to `elementStack`. +2. For each `node` we enter, if the `node` is static (`type` is `element` or starts with `mdx`), record in `allPossibleElements` and push to `elementStack`. We also record additional metadata in `elementMetadatas` for the mutation phase later. - Q: Why do we record `mdxJsxFlowElement`, it's MDX?
A: Because we're looking for nodes whose children are static. The node itself doesn't need to be static. - Q: Are we sure this is the subtree root node in `allPossibleElements`?
@@ -71,8 +71,25 @@ Flow: - Q: Why before step 2's `node` enter handling?
A: If we find a non-static `node`, the `node` should still be considered in `allPossibleElements` as its children could be static. 5. Walk done. This leaves us with `allPossibleElements` containing only subtree roots that can be optimized. -6. Add the `set:html` property to the `hast` node, and remove its children. -7. 🎉 The rest of the MDX pipeline will do its thing and generate the desired JSX like above. +6. Proceed to the mutation phase. + +#### Mutation phase + +Inputs: + +- `allPossibleElements` from the scan phase. +- `elementMetadatas` from the scan phase. + +Flow: + +1. Before we mutate the `hast` tree, each element in `allPossibleElements` may have siblings that can be optimized together. Sibling elements are grouped with the `findElementGroups()` function, which returns an array of element groups (new variable `elementGroups`) and mutates `allPossibleElements` to remove elements that are already part of a group. + + - Q: How does `findElementGroups()` work?
+ A: For each elements in `allPossibleElements` that are non-static, we're able to take the element metadata from `elementMetadatas` and guess the next sibling node. If the next sibling node is static and is an element in `allPossibleElements`, we group them together for optimization. It continues to guess until it hits a non-static node or an element not in `allPossibleElements`, which it'll finalize the group as part of the returned result. + +2. For each elements in `allPossibleElements`, we serailize them as HTML and add it to the `set:html` property of the `hast` node, and remove its children. +3. For each element group in `elementGroups`, we serialize the group children as HTML and add it to a new `` node, and replace the group children with the new ` ` node. +4. 🎉 The rest of the MDX pipeline will do its thing and generate the desired JSX like above. ### Extra @@ -82,7 +99,7 @@ Astro's MDX implementation supports specifying `export const components` in the #### Further optimizations -In [How it works](#how-it-works) step 4, +In [Scan phase](#scan-phase) step 4, > we remove all the elements in `elementStack` from `allPossibleElements` diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index fc1d92da48ca..3aaed8787585 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -29,6 +29,10 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & { }; export default function mdx(partialMdxOptions: Partial = {}): AstroIntegration { + // @ts-expect-error Temporarily assign an empty object here, which will be re-assigned by the + // `astro:config:done` hook later. This is so that `vitePluginMdx` can get hold of a reference earlier. + let mdxOptions: MdxOptions = {}; + return { name: '@astrojs/mdx', hooks: { @@ -58,21 +62,30 @@ export default function mdx(partialMdxOptions: Partial = {}): AstroI handlePropagation: true, }); + updateConfig({ + vite: { + plugins: [vitePluginMdx(mdxOptions), vitePluginMdxPostprocess(config)], + }, + }); + }, + 'astro:config:done': ({ config }) => { + // We resolve the final MDX options here so that other integrations have a chance to modify + // `config.markdown` before we access it const extendMarkdownConfig = partialMdxOptions.extendMarkdownConfig ?? defaultMdxOptions.extendMarkdownConfig; - const mdxOptions = applyDefaultOptions({ + const resolvedMdxOptions = applyDefaultOptions({ options: partialMdxOptions, defaults: markdownConfigToMdxOptions( extendMarkdownConfig ? config.markdown : markdownConfigDefaults ), }); - updateConfig({ - vite: { - plugins: [vitePluginMdx(config, mdxOptions), vitePluginMdxPostprocess(config)], - }, - }); + // Mutate `mdxOptions` so that `vitePluginMdx` can reference the actual options + Object.assign(mdxOptions, resolvedMdxOptions); + // @ts-expect-error After we assign, we don't need to reference `mdxOptions` in this context anymore. + // Re-assign it so that the garbage can be collected later. + mdxOptions = {}; }, }, }; @@ -81,7 +94,8 @@ export default function mdx(partialMdxOptions: Partial = {}): AstroI const defaultMdxOptions = { extendMarkdownConfig: true, recmaPlugins: [], -}; + optimize: false, +} satisfies Partial ; function markdownConfigToMdxOptions(markdownConfig: typeof markdownConfigDefaults): MdxOptions { return { @@ -90,7 +104,6 @@ function markdownConfigToMdxOptions(markdownConfig: typeof markdownConfigDefault remarkPlugins: ignoreStringPlugins(markdownConfig.remarkPlugins), rehypePlugins: ignoreStringPlugins(markdownConfig.rehypePlugins), remarkRehype: (markdownConfig.remarkRehype as any) ?? {}, - optimize: false, }; } diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index 99d0c70b2756..3978e5325435 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -5,6 +5,7 @@ import { remarkCollectImages, } from '@astrojs/markdown-remark'; import { createProcessor, nodeTypes } from '@mdx-js/mdx'; +import { rehypeAnalyzeAstroMetadata } from 'astro/jsx/rehype.js'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import remarkSmartypants from 'remark-smartypants'; @@ -13,9 +14,9 @@ import type { PluggableList } from 'unified'; import type { MdxOptions } from './index.js'; import { rehypeApplyFrontmatterExport } from './rehype-apply-frontmatter-export.js'; import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js'; +import { rehypeImageToComponent } from './rehype-images-to-component.js'; import rehypeMetaString from './rehype-meta-string.js'; import { rehypeOptimizeStatic } from './rehype-optimize-static.js'; -import { remarkImageToComponent } from './remark-images-to-component.js'; // Skip nonessential plugins during performance benchmark runs const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK); @@ -30,7 +31,6 @@ export function createMdxProcessor(mdxOptions: MdxOptions, extraOptions: MdxProc rehypePlugins: getRehypePlugins(mdxOptions), recmaPlugins: mdxOptions.recmaPlugins, remarkRehypeOptions: mdxOptions.remarkRehype, - jsx: true, jsxImportSource: 'astro', // Note: disable `.md` (and other alternative extensions for markdown files like `.markdown`) support format: 'mdx', @@ -52,7 +52,7 @@ function getRemarkPlugins(mdxOptions: MdxOptions): PluggableList { } } - remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages, remarkImageToComponent); + remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages); return remarkPlugins; } @@ -74,7 +74,7 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { } } - rehypePlugins.push(...mdxOptions.rehypePlugins); + rehypePlugins.push(...mdxOptions.rehypePlugins, rehypeImageToComponent); if (!isPerformanceBenchmark) { // getHeadings() is guaranteed by TS, so this must be included. @@ -82,8 +82,12 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { rehypePlugins.push(rehypeHeadingIds, rehypeInjectHeadingsExport); } - // computed from `astro.data.frontmatter` in VFile data - rehypePlugins.push(rehypeApplyFrontmatterExport); + rehypePlugins.push( + // Render info from `vfile.data.astro.data.frontmatter` as JS + rehypeApplyFrontmatterExport, + // Analyze MDX nodes and attach to `vfile.data.__astroMetadata` + rehypeAnalyzeAstroMetadata + ); if (mdxOptions.optimize) { // Convert user `optimize` option to compatible `rehypeOptimizeStatic` option diff --git a/packages/integrations/mdx/src/rehype-images-to-component.ts b/packages/integrations/mdx/src/rehype-images-to-component.ts new file mode 100644 index 000000000000..6c797fda235f --- /dev/null +++ b/packages/integrations/mdx/src/rehype-images-to-component.ts @@ -0,0 +1,166 @@ +import type { MarkdownVFile } from '@astrojs/markdown-remark'; +import type { Properties, Root } from 'hast'; +import type { MdxJsxAttribute, MdxjsEsm } from 'mdast-util-mdx'; +import type { MdxJsxFlowElementHast } from 'mdast-util-mdx-jsx'; +import { visit } from 'unist-util-visit'; +import { jsToTreeNode } from './utils.js'; + +export const ASTRO_IMAGE_ELEMENT = 'astro-image'; +export const ASTRO_IMAGE_IMPORT = '__AstroImage__'; +export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage'; + +function createArrayAttribute(name: string, values: (string | number)[]): MdxJsxAttribute { + return { + type: 'mdxJsxAttribute', + name: name, + value: { + type: 'mdxJsxAttributeValueExpression', + value: name, + data: { + estree: { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: values.map((value) => ({ + type: 'Literal', + value: value, + raw: String(value), + })), + }, + }, + ], + sourceType: 'module', + comments: [], + }, + }, + }, + }; +} + +/** + * Convert the element properties (except `src`) to MDX JSX attributes. + * + * @param {Properties} props - The element properties + * @returns {MdxJsxAttribute[]} The MDX attributes + */ +function getImageComponentAttributes(props: Properties): MdxJsxAttribute[] { + const attrs: MdxJsxAttribute[] = []; + + for (const [prop, value] of Object.entries(props)) { + if (prop === 'src') continue; + + /* + * component expects an array for those attributes but the + * received properties are sanitized as strings. So we need to convert them + * back to an array. + */ + if (prop === 'widths' || prop === 'densities') { + attrs.push(createArrayAttribute(prop, String(value).split(' '))); + } else { + attrs.push({ + name: prop, + type: 'mdxJsxAttribute', + value: String(value), + }); + } + } + + return attrs; +} + +export function rehypeImageToComponent() { + return function (tree: Root, file: MarkdownVFile) { + if (!file.data.imagePaths) return; + + const importsStatements: MdxjsEsm[] = []; + const importedImages = new Map (); + + visit(tree, 'element', (node, index, parent) => { + if (!file.data.imagePaths || node.tagName !== 'img' || !node.properties.src) return; + + const src = decodeURI(String(node.properties.src)); + + if (!file.data.imagePaths.has(src)) return; + + let importName = importedImages.get(src); + + if (!importName) { + importName = `__${importedImages.size}_${src.replace(/\W/g, '_')}__`; + + importsStatements.push({ + type: 'mdxjsEsm', + value: '', + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ImportDeclaration', + source: { + type: 'Literal', + value: src, + raw: JSON.stringify(src), + }, + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name: importName }, + }, + ], + }, + ], + }, + }, + }); + importedImages.set(src, importName); + } + + // Build a component that's equivalent to + const componentElement: MdxJsxFlowElementHast = { + name: ASTRO_IMAGE_ELEMENT, + type: 'mdxJsxFlowElement', + attributes: [ + ...getImageComponentAttributes(node.properties), + { + name: 'src', + type: 'mdxJsxAttribute', + value: { + type: 'mdxJsxAttributeValueExpression', + value: importName, + data: { + estree: { + type: 'Program', + sourceType: 'module', + comments: [], + body: [ + { + type: 'ExpressionStatement', + expression: { type: 'Identifier', name: importName }, + }, + ], + }, + }, + }, + }, + ], + children: [], + }; + + parent!.children.splice(index!, 1, componentElement); + }); + + // Add all the import statements to the top of the file for the images + tree.children.unshift(...importsStatements); + + tree.children.unshift( + jsToTreeNode(`import { Image as ${ASTRO_IMAGE_IMPORT} } from "astro:assets";`) + ); + // Export `__usesAstroImage` to pick up `astro:assets` usage in the module graph. + // @see the '@astrojs/mdx-postprocess' plugin + tree.children.push(jsToTreeNode(`export const ${USES_ASTRO_IMAGE_FLAG} = true`)); + }; +} diff --git a/packages/integrations/mdx/src/rehype-optimize-static.ts b/packages/integrations/mdx/src/rehype-optimize-static.ts index 573af317e99c..ebedb753e1cf 100644 --- a/packages/integrations/mdx/src/rehype-optimize-static.ts +++ b/packages/integrations/mdx/src/rehype-optimize-static.ts @@ -1,11 +1,26 @@ -import { visit } from 'estree-util-visit'; +import type { RehypePlugin } from '@astrojs/markdown-remark'; +import { SKIP, visit } from 'estree-util-visit'; +import type { Element, RootContent, RootContentMap } from 'hast'; import { toHtml } from 'hast-util-to-html'; +import type { MdxJsxFlowElementHast, MdxJsxTextElementHast } from 'mdast-util-mdx-jsx'; -// accessing untyped hast and mdx types -type Node = any; +// This import includes ambient types for hast to include mdx nodes +import type {} from 'mdast-util-mdx'; + +// Alias as the main hast node +type Node = RootContent; +// Nodes that have the `children` property +type ParentNode = Element | MdxJsxFlowElementHast | MdxJsxTextElementHast; +// Nodes that can have its children optimized as a single HTML string +type OptimizableNode = Element | MdxJsxFlowElementHast | MdxJsxTextElementHast; export interface OptimizeOptions { - customComponentNames?: string[]; + ignoreElementNames?: string[]; +} + +interface ElementMetadata { + parent: ParentNode; + index: number; } const exportConstComponentsRe = /export\s+const\s+components\s*=/; @@ -17,44 +32,57 @@ const exportConstComponentsRe = /export\s+const\s+components\s*=/; * This optimization reduces the JS output as more content are represented as a * string instead, which also reduces the AST size that Rollup holds in memory. */ -export function rehypeOptimizeStatic(options?: OptimizeOptions) { - return (tree: any) => { +export const rehypeOptimizeStatic: RehypePlugin<[OptimizeOptions?]> = (options) => { + return (tree) => { // A set of non-static components to avoid collapsing when walking the tree // as they need to be preserved as JSX to be rendered dynamically. - const customComponentNames = new Set (options?.customComponentNames); + const ignoreElementNames = new Set (options?.ignoreElementNames); // Find `export const components = { ... }` and get it's object's keys to be - // populated into `customComponentNames`. This configuration is used to render + // populated into `ignoreElementNames`. This configuration is used to render // some HTML elements as custom components, and we also want to avoid collapsing them. for (const child of tree.children) { if (child.type === 'mdxjsEsm' && exportConstComponentsRe.test(child.value)) { - // Try to loosely get the object property nodes - const objectPropertyNodes = child.data.estree.body[0]?.declarations?.[0]?.init?.properties; - if (objectPropertyNodes) { - for (const objectPropertyNode of objectPropertyNodes) { - const componentName = objectPropertyNode.key?.name ?? objectPropertyNode.key?.value; - if (componentName) { - customComponentNames.add(componentName); - } + const keys = getExportConstComponentObjectKeys(child); + if (keys) { + for (const key of keys) { + ignoreElementNames.add(key); } } + break; } } // All possible elements that could be the root of a subtree - const allPossibleElements = new Set (); + const allPossibleElements = new Set (); // The current collapsible element stack while traversing the tree const elementStack: Node[] = []; + // Metadata used by `findElementGroups` later + const elementMetadatas = new WeakMap (); + + /** + * A non-static node causes all its parents to be non-optimizable + */ + const isNodeNonStatic = (node: Node) => { + // @ts-expect-error Access `.tagName` naively for perf + return node.type.startsWith('mdx') || ignoreElementNames.has(node.tagName); + }; + + visit(tree as any, { + // @ts-expect-error Force coerce node as hast node + enter(node: Node, key, index, parents: ParentNode[]) { + // `estree-util-visit` may traverse in MDX `attributes`, we don't want that. Only continue + // if it's traversing the root, or the `children` key. + if (key != null && key !== 'children') return SKIP; + + // Mutate `node` as a normal hast element node if it's a plain MDX node, e.g. `something` + simplifyPlainMdxComponentNode(node, ignoreElementNames); - visit(tree, { - enter(node) { - // @ts-expect-error read tagName naively - const isCustomComponent = node.tagName && customComponentNames.has(node.tagName); - // For nodes that can't be optimized, eliminate all elements in the - // `elementStack` from the `allPossibleElements` set. - if (node.type.startsWith('mdx') || isCustomComponent) { + // For nodes that are not static, eliminate all elements in the `elementStack` from the + // `allPossibleElements` set. + if (isNodeNonStatic(node)) { for (const el of elementStack) { - allPossibleElements.delete(el); + allPossibleElements.delete(el as OptimizableNode); } // Micro-optimization: While this destroys the meaning of an element // stack for this node, things will still work but we won't repeatedly @@ -64,17 +92,25 @@ export function rehypeOptimizeStatic(options?: OptimizeOptions) { } // For possible subtree root nodes, record them in `elementStack` and // `allPossibleElements` to be used in the "leave" hook below. - // @ts-expect-error MDX types for `.type` is not enhanced because MDX isn't used directly - if (node.type === 'element' || node.type === 'mdxJsxFlowElement') { + if (node.type === 'element' || isMdxComponentNode(node)) { elementStack.push(node); allPossibleElements.add(node); + + if (index != null && node.type === 'element') { + // Record metadata for element node to be used for grouping analysis later + elementMetadatas.set(node, { parent: parents[parents.length - 1], index }); + } } }, - leave(node, _, __, parents) { + // @ts-expect-error Force coerce node as hast node + leave(node: Node, key, _, parents: ParentNode[]) { + // `estree-util-visit` may traverse in MDX `attributes`, we don't want that. Only continue + // if it's traversing the root, or the `children` key. + if (key != null && key !== 'children') return SKIP; + // Do the reverse of the if condition above, popping the `elementStack`, // and consolidating `allPossibleElements` as a subtree root. - // @ts-expect-error MDX types for `.type` is not enhanced because MDX isn't used directly - if (node.type === 'element' || node.type === 'mdxJsxFlowElement') { + if (node.type === 'element' || isMdxComponentNode(node)) { elementStack.pop(); // Many possible elements could be part of a subtree, in order to find // the root, we check the parent of the element we're popping. If the @@ -89,10 +125,18 @@ export function rehypeOptimizeStatic(options?: OptimizeOptions) { }, }); + // Within `allPossibleElements`, element nodes are often siblings and instead of setting `set:html` + // on each of the element node, we can create a ` ` element that includes + // all element nodes instead, simplifying the output. + const elementGroups = findElementGroups(allPossibleElements, elementMetadatas, isNodeNonStatic); + // For all possible subtree roots, collapse them into `set:html` and // strip of their children for (const el of allPossibleElements) { - if (el.type === 'mdxJsxFlowElement') { + // Avoid adding empty `set:html` attributes if there's no children + if (el.children.length === 0) continue; + + if (isMdxComponentNode(el)) { el.attributes.push({ type: 'mdxJsxAttribute', name: 'set:html', @@ -103,5 +147,150 @@ export function rehypeOptimizeStatic(options?: OptimizeOptions) { } el.children = []; } + + // For each element group, we create a new ` ` MDX node with `set:html` of the children + // serialized as HTML. We insert this new fragment, replacing all the group children nodes. + // We iterate in reverse to avoid changing the index of groups of the same parent. + for (let i = elementGroups.length - 1; i >= 0; i--) { + const group = elementGroups[i]; + const fragmentNode: MdxJsxFlowElementHast = { + type: 'mdxJsxFlowElement', + name: 'Fragment', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'set:html', + value: toHtml(group.children), + }, + ], + children: [], + }; + group.parent.children.splice(group.startIndex, group.children.length, fragmentNode); + } }; +}; + +interface ElementGroup { + parent: ParentNode; + startIndex: number; + children: Node[]; +} + +/** + * Iterate through `allPossibleElements` and find elements that are siblings, and return them. `allPossibleElements` + * will be mutated to exclude these grouped elements. + */ +function findElementGroups( + allPossibleElements: Set , + elementMetadatas: WeakMap , + isNodeNonStatic: (node: Node) => boolean +): ElementGroup[] { + const elementGroups: ElementGroup[] = []; + + for (const el of allPossibleElements) { + // Non-static nodes can't be grouped. It can only optimize its static children. + if (isNodeNonStatic(el)) continue; + + // Get the metadata for the element node, this should always exist + const metadata = elementMetadatas.get(el); + if (!metadata) { + throw new Error( + 'Internal MDX error: rehype-optimize-static should have metadata for element node' + ); + } + + // For this element, iterate through the next siblings and add them to this array + // if they are text nodes or elements that are in `allPossibleElements` (optimizable). + // If one of the next siblings don't match the criteria, break the loop as others are no longer siblings. + const groupableElements: Node[] = [el]; + for (let i = metadata.index + 1; i < metadata.parent.children.length; i++) { + const node = metadata.parent.children[i]; + + // If the node is non-static, we can't group it with the current element + if (isNodeNonStatic(node)) break; + + if (node.type === 'element') { + // This node is now (presumably) part of a group, remove it from `allPossibleElements` + const existed = allPossibleElements.delete(node); + // If this node didn't exist in `allPossibleElements`, it's likely that one of its children + // are non-static, hence this node can also not be grouped. So we break out here. + if (!existed) break; + } + + groupableElements.push(node); + } + + // If group elements are more than one, add them to the `elementGroups`. + // Grouping is most effective if there's multiple elements in it. + if (groupableElements.length > 1) { + elementGroups.push({ + parent: metadata.parent, + startIndex: metadata.index, + children: groupableElements, + }); + // The `el` is also now part of a group, remove it from `allPossibleElements` + allPossibleElements.delete(el); + } + } + + return elementGroups; +} + +function isMdxComponentNode(node: Node): node is MdxJsxFlowElementHast | MdxJsxTextElementHast { + return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement'; +} + +/** + * Get the object keys from `export const components` + * + * @example + * `export const components = { foo, bar: Baz }`, returns `['foo', 'bar']` + */ +function getExportConstComponentObjectKeys(node: RootContentMap['mdxjsEsm']) { + const exportNamedDeclaration = node.data?.estree?.body[0]; + if (exportNamedDeclaration?.type !== 'ExportNamedDeclaration') return; + + const variableDeclaration = exportNamedDeclaration.declaration; + if (variableDeclaration?.type !== 'VariableDeclaration') return; + + const variableInit = variableDeclaration.declarations[0]?.init; + if (variableInit?.type !== 'ObjectExpression') return; + + const keys: string[] = []; + for (const propertyNode of variableInit.properties) { + if (propertyNode.type === 'Property' && propertyNode.key.type === 'Identifier') { + keys.push(propertyNode.key.name); + } + } + return keys; +} + +/** + * Some MDX nodes are simply `something` which isn't needed to be completely treated + * as an MDX node. This function tries to mutate this node as a simple hast element node if so. + */ +function simplifyPlainMdxComponentNode(node: Node, ignoreElementNames: Set ) { + if ( + !isMdxComponentNode(node) || + // Attributes could be dynamic, so bail if so. + node.attributes.length > 0 || + // Fragments are also dynamic + !node.name || + // Ignore if the node name is in the ignore list + ignoreElementNames.has(node.name) || + // If the node name has uppercase characters, it's likely an actual MDX component + node.name.toLowerCase() !== node.name + ) { + return; + } + + // Mutate as hast element node + const newNode = node as unknown as Element; + newNode.type = 'element'; + newNode.tagName = node.name; + newNode.properties = {}; + + // @ts-expect-error Delete mdx-specific properties + node.attributes = undefined; + node.data = undefined; } diff --git a/packages/integrations/mdx/src/remark-images-to-component.ts b/packages/integrations/mdx/src/remark-images-to-component.ts deleted file mode 100644 index 46d04d443341..000000000000 --- a/packages/integrations/mdx/src/remark-images-to-component.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { MarkdownVFile } from '@astrojs/markdown-remark'; -import type { Image, Parent } from 'mdast'; -import type { MdxJsxAttribute, MdxJsxFlowElement, MdxjsEsm } from 'mdast-util-mdx'; -import { visit } from 'unist-util-visit'; -import { jsToTreeNode } from './utils.js'; - -export const ASTRO_IMAGE_ELEMENT = 'astro-image'; -export const ASTRO_IMAGE_IMPORT = '__AstroImage__'; -export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage'; - -export function remarkImageToComponent() { - return function (tree: any, file: MarkdownVFile) { - if (!file.data.imagePaths) return; - - const importsStatements: MdxjsEsm[] = []; - const importedImages = new Map (); - - visit(tree, 'image', (node: Image, index: number | undefined, parent: Parent | null) => { - // Use the imagePaths set from the remark-collect-images so we don't have to duplicate the logic for - // checking if an image should be imported or not - if (file.data.imagePaths?.has(node.url)) { - let importName = importedImages.get(node.url); - - // If we haven't already imported this image, add an import statement - if (!importName) { - importName = `__${importedImages.size}_${node.url.replace(/\W/g, '_')}__`; - importsStatements.push({ - type: 'mdxjsEsm', - value: '', - data: { - estree: { - type: 'Program', - sourceType: 'module', - body: [ - { - type: 'ImportDeclaration', - source: { - type: 'Literal', - value: node.url, - raw: JSON.stringify(node.url), - }, - specifiers: [ - { - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name: importName }, - }, - ], - }, - ], - }, - }, - }); - importedImages.set(node.url, importName); - } - - // Build a component that's equivalent to - const componentElement: MdxJsxFlowElement = { - name: ASTRO_IMAGE_ELEMENT, - type: 'mdxJsxFlowElement', - attributes: [ - { - name: 'src', - type: 'mdxJsxAttribute', - value: { - type: 'mdxJsxAttributeValueExpression', - value: importName, - data: { - estree: { - type: 'Program', - sourceType: 'module', - comments: [], - body: [ - { - type: 'ExpressionStatement', - expression: { type: 'Identifier', name: importName }, - }, - ], - }, - }, - }, - }, - { name: 'alt', type: 'mdxJsxAttribute', value: node.alt || '' }, - ], - children: [], - }; - - if (node.title) { - componentElement.attributes.push({ - type: 'mdxJsxAttribute', - name: 'title', - value: node.title, - }); - } - - if (node.data && node.data.hProperties) { - const createArrayAttribute = (name: string, values: string[]): MdxJsxAttribute => { - return { - type: 'mdxJsxAttribute', - name: name, - value: { - type: 'mdxJsxAttributeValueExpression', - value: name, - data: { - estree: { - type: 'Program', - body: [ - { - type: 'ExpressionStatement', - expression: { - type: 'ArrayExpression', - elements: values.map((value) => ({ - type: 'Literal', - value: value, - raw: String(value), - })), - }, - }, - ], - sourceType: 'module', - comments: [], - }, - }, - }, - }; - }; - // Go through every hProperty and add it as an attribute of the - Object.entries(node.data.hProperties as Record ).forEach( - ([key, value]) => { - if (Array.isArray(value)) { - componentElement.attributes.push(createArrayAttribute(key, value)); - } else { - componentElement.attributes.push({ - name: key, - type: 'mdxJsxAttribute', - value: String(value), - }); - } - } - ); - } - - parent!.children.splice(index!, 1, componentElement); - } - }); - - // Add all the import statements to the top of the file for the images - tree.children.unshift(...importsStatements); - - tree.children.unshift( - jsToTreeNode(`import { Image as ${ASTRO_IMAGE_IMPORT} } from "astro:assets";`) - ); - // Export `__usesAstroImage` to pick up `astro:assets` usage in the module graph. - // @see the '@astrojs/mdx-postprocess' plugin - tree.children.push(jsToTreeNode(`export const ${USES_ASTRO_IMAGE_FLAG} = true`)); - }; -} diff --git a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts index c60504be6c9c..7661c0ecf874 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts @@ -5,24 +5,27 @@ import { ASTRO_IMAGE_ELEMENT, ASTRO_IMAGE_IMPORT, USES_ASTRO_IMAGE_FLAG, -} from './remark-images-to-component.js'; +} from './rehype-images-to-component.js'; import { type FileInfo, getFileInfo } from './utils.js'; +const underscoreFragmentImportRegex = /[\s,{]_Fragment[\s,}]/; +const astroTagComponentImportRegex = /[\s,{]__astro_tag_component__[\s,}]/; + // These transforms must happen *after* JSX runtime transformations export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { return { name: '@astrojs/mdx-postprocess', - transform(code, id) { + transform(code, id, opts) { if (!id.endsWith('.mdx')) return; const fileInfo = getFileInfo(id, astroConfig); const [imports, exports] = parse(code); // Call a series of functions that transform the code - code = injectFragmentImport(code, imports); + code = injectUnderscoreFragmentImport(code, imports); code = injectMetadataExports(code, exports, fileInfo); code = transformContentExport(code, exports); - code = annotateContentExport(code, id); + code = annotateContentExport(code, id, !!opts?.ssr, imports); // The code transformations above are append-only, so the line/column mappings are the same // and we can omit the sourcemap for performance. @@ -31,23 +34,12 @@ export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { }; } -const fragmentImportRegex = /[\s,{](?:Fragment,|Fragment\s*\})/; - /** - * Inject `Fragment` identifier import if not already present. It should already be injected, - * but check just to be safe. - * - * TODO: Double-check if we no longer need this function. + * Inject `Fragment` identifier import if not already present. */ -function injectFragmentImport(code: string, imports: readonly ImportSpecifier[]) { - const importsFromJSXRuntime = imports - .filter(({ n }) => n === 'astro/jsx-runtime') - .map(({ ss, se }) => code.substring(ss, se)); - const hasFragmentImport = importsFromJSXRuntime.some((statement) => - fragmentImportRegex.test(statement) - ); - if (!hasFragmentImport) { - code = `import { Fragment } from "astro/jsx-runtime"\n` + code; +function injectUnderscoreFragmentImport(code: string, imports: readonly ImportSpecifier[]) { + if (!isSpecifierImported(code, imports, underscoreFragmentImportRegex, 'astro/jsx-runtime')) { + code += `\nimport { Fragment as _Fragment } from 'astro/jsx-runtime';`; } return code; } @@ -81,7 +73,9 @@ function transformContentExport(code: string, exports: readonly ExportSpecifier[ const usesAstroImage = exports.find(({ n }) => n === USES_ASTRO_IMAGE_FLAG); // Generate code for the `components` prop passed to `MDXContent` - let componentsCode = `{ Fragment${hasComponents ? ', ...components' : ''}, ...props.components,`; + let componentsCode = `{ Fragment: _Fragment${ + hasComponents ? ', ...components' : '' + }, ...props.components,`; if (usesAstroImage) { componentsCode += ` ${JSON.stringify(ASTRO_IMAGE_ELEMENT)}: ${ hasComponents ? 'components.img ?? ' : '' @@ -103,7 +97,12 @@ export default Content;`; /** * Add properties to the `Content` export. */ -function annotateContentExport(code: string, id: string) { +function annotateContentExport( + code: string, + id: string, + ssr: boolean, + imports: readonly ImportSpecifier[] +) { // Mark `Content` as MDX component code += `\nContent[Symbol.for('mdx-component')] = true`; // Ensure styles and scripts are injected into a `` when a layout is not applied @@ -111,5 +110,39 @@ function annotateContentExport(code: string, id: string) { // Assign the `moduleId` metadata to `Content` code += `\nContent.moduleId = ${JSON.stringify(id)};`; + // Tag the `Content` export as "astro:jsx" so it's quicker to identify how to render this component + if (ssr) { + if ( + !isSpecifierImported( + code, + imports, + astroTagComponentImportRegex, + 'astro/runtime/server/index.js' + ) + ) { + code += `\nimport { __astro_tag_component__ } from 'astro/runtime/server/index.js';`; + } + code += `\n__astro_tag_component__(Content, 'astro:jsx');`; + } + return code; } + +/** + * Check whether the `specifierRegex` matches for an import of `source` in the `code`. + */ +function isSpecifierImported( + code: string, + imports: readonly ImportSpecifier[], + specifierRegex: RegExp, + source: string +) { + for (const imp of imports) { + if (imp.n !== source) continue; + + const importStatement = code.slice(imp.ss, imp.se); + if (specifierRegex.test(importStatement)) return true; + } + + return false; +} diff --git a/packages/integrations/mdx/src/vite-plugin-mdx.ts b/packages/integrations/mdx/src/vite-plugin-mdx.ts index 6f2ec2cc487a..1b966ecd2a30 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx.ts @@ -1,13 +1,13 @@ -import fs from 'node:fs/promises'; import { setVfileFrontmatter } from '@astrojs/markdown-remark'; -import type { AstroConfig, SSRError } from 'astro'; +import type { SSRError } from 'astro'; +import { getAstroMetadata } from 'astro/jsx/rehype.js'; import { VFile } from 'vfile'; import type { Plugin } from 'vite'; import type { MdxOptions } from './index.js'; import { createMdxProcessor } from './plugins.js'; -import { getFileInfo, parseFrontmatter } from './utils.js'; +import { parseFrontmatter } from './utils.js'; -export function vitePluginMdx(astroConfig: AstroConfig, mdxOptions: MdxOptions): Plugin { +export function vitePluginMdx(mdxOptions: MdxOptions): Plugin { let processor: ReturnType | undefined; return { @@ -17,21 +17,19 @@ export function vitePluginMdx(astroConfig: AstroConfig, mdxOptions: MdxOptions): processor = undefined; }, configResolved(resolved) { + // `mdxOptions` should be populated at this point, but `astro sync` doesn't call `astro:config:done` :( + // Workaround this for now by skipping here. `astro sync` shouldn't call the `transform()` hook here anyways. + if (Object.keys(mdxOptions).length === 0) return; + processor = createMdxProcessor(mdxOptions, { sourcemap: !!resolved.build.sourcemap, }); - // HACK: move ourselves before Astro's JSX plugin to transform things in the right order + // HACK: Remove the `astro:jsx` plugin if defined as we handle the JSX transformation ourselves const jsxPluginIndex = resolved.plugins.findIndex((p) => p.name === 'astro:jsx'); if (jsxPluginIndex !== -1) { - const myPluginIndex = resolved.plugins.findIndex((p) => p.name === '@mdx-js/rollup'); - if (myPluginIndex !== -1) { - const myPlugin = resolved.plugins[myPluginIndex]; - // @ts-ignore-error ignore readonly annotation - resolved.plugins.splice(myPluginIndex, 1); - // @ts-ignore-error ignore readonly annotation - resolved.plugins.splice(jsxPluginIndex, 0, myPlugin); - } + // @ts-ignore-error ignore readonly annotation + resolved.plugins.splice(jsxPluginIndex, 1); } }, async resolveId(source, importer, options) { @@ -43,13 +41,9 @@ export function vitePluginMdx(astroConfig: AstroConfig, mdxOptions: MdxOptions): }, // Override transform to alter code before MDX compilation // ex. inject layouts - async transform(_, id) { + async transform(code, id) { if (!id.endsWith('.mdx')) return; - // Read code from file manually to prevent Vite from parsing `import.meta.env` expressions - const { fileId } = getFileInfo(id, astroConfig); - const code = await fs.readFile(fileId, 'utf-8'); - const { data: frontmatter, content: pageContent } = parseFrontmatter(code, id); const vfile = new VFile({ value: pageContent, path: id }); @@ -70,13 +64,14 @@ export function vitePluginMdx(astroConfig: AstroConfig, mdxOptions: MdxOptions): return { code: String(compiled.value), map: compiled.map, + meta: getMdxMeta(vfile), }; } catch (e: any) { const err: SSRError = e; // For some reason MDX puts the error location in the error's name, not very useful for us. err.name = 'MDXError'; - err.loc = { file: fileId, line: e.line, column: e.column }; + err.loc = { file: id, line: e.line, column: e.column }; // For another some reason, MDX doesn't include a stack trace. Weird Error.captureStackTrace(err); @@ -86,3 +81,20 @@ export function vitePluginMdx(astroConfig: AstroConfig, mdxOptions: MdxOptions): }, }; } + +function getMdxMeta(vfile: VFile): Record { + const astroMetadata = getAstroMetadata(vfile); + if (!astroMetadata) { + throw new Error( + 'Internal MDX error: Astro metadata is not set by rehype-analyze-astro-metadata' + ); + } + return { + astro: astroMetadata, + vite: { + // Setting this vite metadata to `ts` causes Vite to resolve .js + // extensions to .ts files. + lang: 'ts', + }, + }; +} diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs index b92b48617c28..204549479f5d 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs +++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs @@ -3,7 +3,7 @@ import mdx from '@astrojs/mdx'; export default { integrations: [mdx({ optimize: { - customComponentNames: ['strong'] + ignoreElementNames: ['strong'] } })] } diff --git a/packages/integrations/mdx/test/mdx-plugins.test.js b/packages/integrations/mdx/test/mdx-plugins.test.js index 6b15884fb712..6bc8e096c268 100644 --- a/packages/integrations/mdx/test/mdx-plugins.test.js +++ b/packages/integrations/mdx/test/mdx-plugins.test.js @@ -64,6 +64,30 @@ describe('MDX plugins', () => { assert.notEqual(selectRehypeExample(document), null); }); + it('supports custom rehype plugins from integrations', async () => { + const fixture = await buildFixture({ + integrations: [ + mdx(), + { + name: 'test', + hooks: { + 'astro:config:setup': ({ updateConfig }) => { + updateConfig({ + markdown: { + rehypePlugins: [rehypeExamplePlugin], + }, + }); + }, + }, + }, + ], + }); + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + assert.notEqual(selectRehypeExample(document), null); + }); + it('supports custom rehype plugins with namespaced attributes', async () => { const fixture = await buildFixture({ integrations: [ diff --git a/packages/integrations/mdx/test/units/rehype-optimize-static.test.js b/packages/integrations/mdx/test/units/rehype-optimize-static.test.js new file mode 100644 index 000000000000..132f3849f5bf --- /dev/null +++ b/packages/integrations/mdx/test/units/rehype-optimize-static.test.js @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { compile as _compile } from '@mdx-js/mdx'; +import { rehypeOptimizeStatic } from '../../dist/rehype-optimize-static.js'; + +/** + * @param {string} mdxCode + * @param {Readonly } options + */ +async function compile(mdxCode, options) { + const result = await _compile(mdxCode, { + jsx: true, + rehypePlugins: [rehypeOptimizeStatic], + ...options, + }); + const code = result.toString(); + // Capture the returned JSX code for testing + const jsx = code.match(/return (.+);\n\}\nexport default function MDXContent/s)?.[1]; + if (jsx == null) throw new Error('Could not find JSX code in compiled MDX'); + return dedent(jsx); +} + +function dedent(str) { + const lines = str.split('\n'); + if (lines.length <= 1) return str; + // Get last line indent, and dedent this amount for the other lines + const lastLineIndent = lines[lines.length - 1].match(/^\s*/)[0].length; + return lines.map((line, i) => (i === 0 ? line : line.slice(lastLineIndent))).join('\n'); +} + +describe('rehype-optimize-static', () => { + it('works', async () => { + const jsx = await compile(`# hello`); + assert.equal( + jsx, + `\ +<_components.h1 {...{ + "set:html": "hello" +}} />` + ); + }); + + it('groups sibling nodes as a single Fragment', async () => { + const jsx = await compile(`\ +# hello + +foo bar +`); + assert.equal( + jsx, + `\ + ` + ); + }); + + it('skips optimization of components', async () => { + const jsx = await compile(`\ +import Comp from './Comp.jsx'; + +# hello + +This is a +`); + assert.equal( + jsx, + `\ +<> <_components.p>{"This is a "} >` + ); + }); + + it('optimizes explicit html elements', async () => { + const jsx = await compile(`\ +# hello + +foo bar baz + +qux +`); + assert.equal( + jsx, + `\ + ` + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 225e8e10ec66..43aa94a54d14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -775,6 +775,12 @@ importers: eol: specifier: ^0.9.1 version: 0.9.1 + mdast-util-mdx: + specifier: ^3.0.0 + version: 3.0.0 + mdast-util-mdx-jsx: + specifier: ^3.1.2 + version: 3.1.2 memfs: specifier: ^4.9.1 version: 4.9.1 @@ -4426,6 +4432,9 @@ importers: '@types/estree': specifier: ^1.0.5 version: 1.0.5 + '@types/hast': + specifier: ^3.0.3 + version: 3.0.4 '@types/mdast': specifier: ^4.0.3 version: 4.0.3 @@ -4447,6 +4456,9 @@ importers: mdast-util-mdx: specifier: ^3.0.0 version: 3.0.0 + mdast-util-mdx-jsx: + specifier: ^3.1.2 + version: 3.1.2 mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 From 7bbd66459dd29a338ac1dfae0e4c984cb08f73b3 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Wed, 8 May 2024 11:26:00 +0200 Subject: [PATCH 057/151] feat: Add new component to toolbar library (#10906) * feat: progress * chore: changeset --- .changeset/metal-crabs-applaud.md | 5 + .changeset/twelve-dolphins-roll.md | 5 + packages/astro/src/@types/astro.ts | 2 + .../runtime/client/dev-toolbar/entrypoint.ts | 2 + .../client/dev-toolbar/ui-library/button.ts | 35 ++++- .../client/dev-toolbar/ui-library/index.ts | 1 + .../dev-toolbar/ui-library/radio-checkbox.ts | 121 ++++++++++++++++++ 7 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 .changeset/metal-crabs-applaud.md create mode 100644 .changeset/twelve-dolphins-roll.md create mode 100644 packages/astro/src/runtime/client/dev-toolbar/ui-library/radio-checkbox.ts diff --git a/.changeset/metal-crabs-applaud.md b/.changeset/metal-crabs-applaud.md new file mode 100644 index 000000000000..c7cb3a26a7f6 --- /dev/null +++ b/.changeset/metal-crabs-applaud.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Adds a new radio checkbox component to the dev toolbar UI library (`astro-dev-toolbar-radio-checkbox`) diff --git a/.changeset/twelve-dolphins-roll.md b/.changeset/twelve-dolphins-roll.md new file mode 100644 index 000000000000..baf944281d70 --- /dev/null +++ b/.changeset/twelve-dolphins-roll.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Adds a new `buttonBorderRadius` property to the `astro-dev-toolbar-button` component for the dev toolbar component library. This property can be useful to make a fully rounded button with an icon in the center. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0cff203cf227..af839ae61fda 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -33,6 +33,7 @@ import type { DevToolbarCard, DevToolbarHighlight, DevToolbarIcon, + DevToolbarRadioCheckbox, DevToolbarSelect, DevToolbarToggle, DevToolbarTooltip, @@ -3087,6 +3088,7 @@ declare global { 'astro-dev-toolbar-icon': DevToolbarIcon; 'astro-dev-toolbar-card': DevToolbarCard; 'astro-dev-toolbar-select': DevToolbarSelect; + 'astro-dev-toolbar-radio-checkbox': DevToolbarRadioCheckbox; // Deprecated names // TODO: Remove in Astro 5.0 diff --git a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts index dc8d043a252b..2558a4db2339 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts @@ -25,6 +25,7 @@ document.addEventListener('DOMContentLoaded', async () => { DevToolbarBadge, DevToolbarIcon, DevToolbarSelect, + DevToolbarRadioCheckbox, }, ] = await Promise.all([ loadDevToolbarApps() as DevToolbarAppDefinition[], @@ -48,6 +49,7 @@ document.addEventListener('DOMContentLoaded', async () => { customElements.define('astro-dev-toolbar-badge', DevToolbarBadge); customElements.define('astro-dev-toolbar-icon', DevToolbarIcon); customElements.define('astro-dev-toolbar-select', DevToolbarSelect); + customElements.define('astro-dev-toolbar-radio-checkbox', DevToolbarRadioCheckbox); // Add deprecated names // TODO: Remove in Astro 5.0 diff --git a/packages/astro/src/runtime/client/dev-toolbar/ui-library/button.ts b/packages/astro/src/runtime/client/dev-toolbar/ui-library/button.ts index 9a370581773d..067a1cf2ac90 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/ui-library/button.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/ui-library/button.ts @@ -2,13 +2,16 @@ import { settings } from '../settings.js'; const sizes = ['small', 'medium', 'large'] as const; const styles = ['ghost', 'outline', 'purple', 'gray', 'red', 'green', 'yellow', 'blue'] as const; +const borderRadii = ['normal', 'rounded'] as const; type ButtonSize = (typeof sizes)[number]; type ButtonStyle = (typeof styles)[number]; +type ButtonBorderRadius = (typeof borderRadii)[number]; export class DevToolbarButton extends HTMLElement { _size: ButtonSize = 'small'; _buttonStyle: ButtonStyle = 'purple'; + _buttonBorderRadius: ButtonBorderRadius = 'normal'; get size() { return this._size; @@ -40,7 +43,22 @@ export class DevToolbarButton extends HTMLElement { this.updateStyle(); } - static observedAttributes = ['button-style', 'size']; + get buttonBorderRadius() { + return this._buttonBorderRadius; + } + + set buttonBorderRadius(value) { + if (!borderRadii.includes(value)) { + settings.logger.error( + `Invalid border-radius: ${value}, expected one of ${borderRadii.join(', ')}, got ${value}.` + ); + return; + } + this._buttonBorderRadius = value; + this.updateStyle(); + } + + static observedAttributes = ['button-style', 'size', 'button-border-radius']; shadowRoot: ShadowRoot; @@ -88,8 +106,14 @@ export class DevToolbarButton extends HTMLElement { --small-font-size: 12px; --large-padding: 12px 16px; + --large-rounded-padding: 12px 12px; --medium-padding: 8px 12px; + --medium-rounded-padding: 8px 8px; --small-padding: 4px 8px; + --small-rounded-padding: 4px 4px; + + --normal-border-radius: 4px; + --rounded-border-radius: 9999px; border: 1px solid var(--border); padding: var(--padding); @@ -97,7 +121,7 @@ export class DevToolbarButton extends HTMLElement { background: var(--background); color: var(--text-color); - border-radius: 4px; + border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; @@ -137,8 +161,13 @@ export class DevToolbarButton extends HTMLElement { --background: var(--${this.buttonStyle}-background); --border: var(--${this.buttonStyle}-border); --font-size: var(--${this.size}-font-size); - --padding: var(--${this.size}-padding); --text-color: var(--${this.buttonStyle}-text); + ${ + this.buttonBorderRadius === 'normal' + ? '--padding: var(--' + this.size + '-padding);' + : '--padding: var(--' + this.size + '-rounded-padding);' + } + --border-radius: var(--${this.buttonBorderRadius}-border-radius); }`; } } diff --git a/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts b/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts index 7b1197ab70c2..56765c4ca3ed 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/ui-library/index.ts @@ -7,3 +7,4 @@ export { DevToolbarSelect } from './select.js'; export { DevToolbarToggle } from './toggle.js'; export { DevToolbarTooltip } from './tooltip.js'; export { DevToolbarWindow } from './window.js'; +export { DevToolbarRadioCheckbox } from './radio-checkbox.js'; diff --git a/packages/astro/src/runtime/client/dev-toolbar/ui-library/radio-checkbox.ts b/packages/astro/src/runtime/client/dev-toolbar/ui-library/radio-checkbox.ts new file mode 100644 index 000000000000..a223bf1a848b --- /dev/null +++ b/packages/astro/src/runtime/client/dev-toolbar/ui-library/radio-checkbox.ts @@ -0,0 +1,121 @@ +const styles = ['purple', 'gray', 'red', 'green', 'yellow', 'blue'] as const; + +type RadioStyle = (typeof styles)[number]; + +export class DevToolbarRadioCheckbox extends HTMLElement { + private _radioStyle: RadioStyle = 'purple'; + input: HTMLInputElement; + + shadowRoot: ShadowRoot; + + get radioStyle() { + return this._radioStyle; + } + + set radioStyle(value) { + if (!styles.includes(value)) { + console.error(`Invalid style: ${value}, expected one of ${styles.join(', ')}.`); + return; + } + this._radioStyle = value; + this.updateStyle(); + } + + static observedAttributes = ['radio-style', 'checked', 'disabled', 'name', 'value']; + + constructor() { + super(); + this.shadowRoot = this.attachShadow({ mode: 'open' }); + + this.shadowRoot.innerHTML = ` + + + `; + this.input = document.createElement('input'); + this.input.type = 'radio'; + this.shadowRoot.append(this.input); + } + + connectedCallback() { + this.updateInputState(); + this.updateStyle(); + } + + updateStyle() { + const styleElement = this.shadowRoot.querySelector ('#selected-style'); + + if (styleElement) { + styleElement.innerHTML = ` + :host { + --unchecked-color: var(--${this._radioStyle}-unchecked); + --checked-color: var(--${this._radioStyle}-checked); + } + `; + } + } + + updateInputState() { + this.input.checked = this.hasAttribute('checked'); + this.input.disabled = this.hasAttribute('disabled'); + this.input.name = this.getAttribute('name') || ''; + this.input.value = this.getAttribute('value') || ''; + } + + attributeChangedCallback() { + if (this.hasAttribute('radio-style')) { + this.radioStyle = this.getAttribute('radio-style') as RadioStyle; + } + + this.updateInputState(); + } +} From 47877a75404ccc8786bbea2171015fb088dc01a1 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 8 May 2024 10:26:31 +0100 Subject: [PATCH 058/151] feat: rewriting (experimental) (#10867) * feat: implement reroute in dev (#10818) * chore: implement reroute in dev * chore: revert naming change * chore: conditionally create the new request * chore: handle error * remove only * remove only * chore: add tests and remove logs * chore: fix regression * chore: fix regression route matching * chore: remove unwanted test * feat: reroute in SSG (#10843) * feat: rerouting in ssg * linting * feat: reroute for SSR (#10845) * feat: implement reroute in dev (#10818) * chore: implement reroute in dev * chore: revert naming change * chore: conditionally create the new request * chore: handle error * remove only * remove only * chore: add tests and remove logs * chore: fix regression * chore: fix regression route matching * chore: remove unwanted test * feat: reroute in SSG (#10843) * feat: rerouting in ssg * linting * feat: rerouting in ssg * linting * feat: reroute for SSR * fix rebase * fix merge issue * feat: rerouting in the middleware (#10853) * feat: implement reroute in dev (#10818) * chore: implement reroute in dev * chore: revert naming change * chore: conditionally create the new request * chore: handle error * remove only * remove only * chore: add tests and remove logs * chore: fix regression * chore: fix regression route matching * chore: remove unwanted test * feat: reroute in SSG (#10843) * feat: rerouting in ssg * linting * feat: rerouting in ssg * linting * feat: reroute for SSR * fix rebase * fix merge issue * feat: implement the `next(payload)` feature for rerouting * chore: revert code * chore: fix code * Apply suggestions from code review Co-authored-by: Bjorn Lu --------- Co-authored-by: Bjorn Lu * feat: rerouting * chore: rename to `rewrite` * chore: better error message * chore: update the chageset * Apply suggestions from code review Co-authored-by: Sarah Rainsberger * chore: update docs based on feedback * lock file * Apply suggestions from code review Co-authored-by: Sarah Rainsberger Co-authored-by: Matthew Phillips Co-authored-by: Ben Holmes * feedback * rename * add tests for 404 * revert change * fix regression * Update .changeset/pink-ligers-share.md Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Bjorn Lu Co-authored-by: Sarah Rainsberger Co-authored-by: Matthew Phillips Co-authored-by: Ben Holmes --- .changeset/pink-ligers-share.md | 49 ++++ packages/astro/src/@types/astro.ts | 104 +++++++- packages/astro/src/core/app/index.ts | 52 +--- packages/astro/src/core/app/pipeline.ts | 113 +++++++-- packages/astro/src/core/app/types.ts | 2 + packages/astro/src/core/base-pipeline.ts | 19 ++ packages/astro/src/core/build/generate.ts | 78 +----- packages/astro/src/core/build/pipeline.ts | 145 +++++++++++- .../src/core/build/plugins/plugin-manifest.ts | 1 + packages/astro/src/core/config/schema.ts | 2 + packages/astro/src/core/errors/errors-data.ts | 12 + .../src/core/middleware/callMiddleware.ts | 29 ++- packages/astro/src/core/middleware/index.ts | 14 +- .../astro/src/core/middleware/sequence.ts | 31 ++- packages/astro/src/core/render-context.ts | 148 ++++++++++-- packages/astro/src/prerender/routing.ts | 2 +- .../src/vite-plugin-astro-server/pipeline.ts | 94 ++++++-- .../src/vite-plugin-astro-server/plugin.ts | 4 +- .../src/vite-plugin-astro-server/route.ts | 67 +++--- .../middleware-virtual/astro.config.mjs | 3 + .../fixtures/middleware-virtual/package.json | 8 + .../middleware-virtual/src/middleware.js | 6 + .../middleware-virtual/src/pages/index.astro | 13 + .../test/fixtures/reroute/astro.config.mjs | 9 + .../astro/test/fixtures/reroute/package.json | 8 + .../test/fixtures/reroute/src/middleware.js | 33 +++ .../reroute/src/pages/auth/base.astro | 10 + .../reroute/src/pages/auth/dashboard.astro | 10 + .../reroute/src/pages/auth/settings.astro | 10 + .../reroute/src/pages/blog/hello/index.astro | 11 + .../reroute/src/pages/blog/oops.astro | 11 + .../reroute/src/pages/blog/salut/index.astro | 11 + .../reroute/src/pages/dynamic/[id].astro | 21 ++ .../fixtures/reroute/src/pages/index.astro | 12 + .../fixtures/reroute/src/pages/reroute.astro | 11 + .../reroute/src/pages/spread/[...id].astro | 20 ++ .../astro/test/i18n-routing-manual.test.js | 2 - packages/astro/test/rewrite.test.js | 223 ++++++++++++++++++ .../test/units/routing/route-matching.test.js | 2 +- .../vite-plugin-astro-server/request.test.js | 2 +- pnpm-lock.yaml | 12 + 41 files changed, 1192 insertions(+), 222 deletions(-) create mode 100644 .changeset/pink-ligers-share.md create mode 100644 packages/astro/test/fixtures/middleware-virtual/astro.config.mjs create mode 100644 packages/astro/test/fixtures/middleware-virtual/package.json create mode 100644 packages/astro/test/fixtures/middleware-virtual/src/middleware.js create mode 100644 packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/reroute/astro.config.mjs create mode 100644 packages/astro/test/fixtures/reroute/package.json create mode 100644 packages/astro/test/fixtures/reroute/src/middleware.js create mode 100644 packages/astro/test/fixtures/reroute/src/pages/auth/base.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/reroute.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro create mode 100644 packages/astro/test/rewrite.test.js diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md new file mode 100644 index 000000000000..e7923350fb5b --- /dev/null +++ b/.changeset/pink-ligers-share.md @@ -0,0 +1,49 @@ +--- +"astro": minor +--- + +Adds experimental rewriting in Astro with a new `rewrite()` function and the middleware `next()` function. + +The feature is available via an experimental flag in `astro.config.mjs`: + +```js +export default defineConfig({ + experimental: { + rewriting: true + } +}) +``` + +When enabled, you can use `rewrite()` to **render** another page without changing the URL of the browser in Astro pages and endpoints. + +```astro +--- +// src/pages/dashboard.astro +if (!Astro.props.allowed) { + return Astro.rewrite("/") +} +--- +``` + +```js +// src/pages/api.js +export function GET(ctx) { + if (!ctx.locals.allowed) { + return ctx.rewrite("/") + } +} +``` + +The middleware `next()` function now accepts a parameter with the same type as the `rewrite()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`. + +```js +// src/middleware.js +export function onRequest(ctx, next) { + if (!ctx.cookies.get("allowed")) { + return next("/") // new signature + } + return next(); +} +``` + +> **NOTE**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index af839ae61fda..cb39ec785ded 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -251,6 +251,19 @@ export interface AstroGlobal< * [Astro reference](https://docs.astro.build/en/guides/server-side-rendering/) */ redirect: AstroSharedContext['redirect']; + /** + * It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rewritten URL passed as argument. + * + * ## Example + * + * ```js + * if (pageIsNotEnabled) { + * return Astro.rewrite('/fallback-page') + * } + * ``` + */ + rewrite: AstroSharedContext['rewrite']; /** * The element allows a component to reference itself recursively. * @@ -1642,7 +1655,7 @@ export interface AstroUserConfig { domains?: Record ; }; - /** ⚠️ WARNING: SUBJECT TO CHANGE */ + /** ! WARNING: SUBJECT TO CHANGE */ db?: Config.Database; /** @@ -1923,6 +1936,62 @@ export interface AstroUserConfig { origin?: boolean; }; }; + + /** + * @docs + * @name experimental.rewriting + * @type {boolean} + * @default `false` + * @version 4.8.0 + * @description + * + * Enables a routing feature for rewriting requests in Astro pages, endpoints and Astro middleware, giving you programmatic control over your routes. + * + * ```js + * { + * experimental: { + * rewriting: true, + * }, + * } + * ``` + * + * Use `Astro.rewrite` in your `.astro` files to reroute to a different page: + * + * ```astro "rewrite" + * --- + * // src/pages/dashboard.astro + * if (!Astro.props.allowed) { + * return Astro.rewrite("/") + * } + * --- + * ``` + * + * Use `context.rewrite` in your endpoint files to reroute to a different page: + * + * ```js + * // src/pages/api.js + * export function GET(ctx) { + * if (!ctx.locals.allowed) { + * return ctx.rewrite("/") + * } + * } + * ``` + * + * Use `next("/")` in your middleware file to reroute to a different page, and then call the next middleware function: + * + * ```js + * // src/middleware.js + * export function onRequest(ctx, next) { + * if (!ctx.cookies.get("allowed")) { + * return next("/") // new signature + * } + * return next(); + * } + * ``` + * + * For a complete overview, and to give feedback on this experimental API, see the [Rerouting RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md). + */ + rewriting: boolean; }; } @@ -2492,6 +2561,20 @@ interface AstroSharedContext< */ redirect(path: string, status?: ValidRedirectStatus): Response; + /** + * It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rerouted URL passed as argument. + * + * ## Example + * + * ```js + * if (pageIsNotEnabled) { + * return Astro.rewrite('/fallback-page') + * } + * ``` + */ + rewrite(rewritePayload: RewritePayload): Promise ; + /** * Object accessed via Astro middleware */ @@ -2606,6 +2689,21 @@ export interface APIContext< */ redirect: AstroSharedContext['redirect']; + /** + * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rerouted URL passed as argument. + * + * ## Example + * + * ```ts + * // src/pages/secret.ts + * export function GET(ctx) { + * return ctx.rewrite(new URL("../"), ctx.url); + * } + * ``` + */ + rewrite: AstroSharedContext['rewrite']; + /** * An object that middlewares can use to store extra information related to the request. * @@ -2800,7 +2898,9 @@ export interface AstroIntegration { }; } -export type MiddlewareNext = () => Promise ; +export type RewritePayload = string | URL | Request; + +export type MiddlewareNext = (rewritePayload?: RewritePayload) => Promise ; export type MiddlewareHandler = ( context: APIContext, next: MiddlewareNext diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 116151610e1c..1ba5d9479833 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,13 +1,6 @@ -import type { - ComponentInstance, - ManifestData, - RouteData, - SSRManifest, -} from '../../@types/astro.js'; +import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js'; import { normalizeTheLocale } from '../../i18n/index.js'; -import type { SinglePageBuiltModule } from '../build/types.js'; import { - DEFAULT_404_COMPONENT, REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER, clientAddressSymbol, @@ -26,7 +19,6 @@ import { prependForwardSlash, removeTrailingForwardSlash, } from '../path.js'; -import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { RenderContext } from '../render-context.js'; import { createAssetLink } from '../render/ssr-element.js'; import { ensure404Route } from '../routing/astro-designed-error-pages.js'; @@ -96,7 +88,7 @@ export class App { routes: manifest.routes.map((route) => route.routeData), }); this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base); - this.#pipeline = this.#createPipeline(streaming); + this.#pipeline = this.#createPipeline(this.#manifestData, streaming); this.#adapterLogger = new AstroIntegrationLogger( this.#logger.options, this.#manifest.adapterName @@ -110,10 +102,11 @@ export class App { /** * Creates a pipeline by reading the stored manifest * + * @param manifestData * @param streaming * @private */ - #createPipeline(streaming = false) { + #createPipeline(manifestData: ManifestData, streaming = false) { if (this.#manifest.checkOrigin) { this.#manifest.middleware = sequence( createOriginCheckMiddleware(), @@ -121,7 +114,7 @@ export class App { ); } - return AppPipeline.create({ + return AppPipeline.create(manifestData, { logger: this.#logger, manifest: this.#manifest, mode: 'production', @@ -309,7 +302,7 @@ export class App { } const pathname = this.#getPathnameFromRequest(request); const defaultStatus = this.#getDefaultStatusCode(routeData, pathname); - const mod = await this.#getModuleForRoute(routeData); + const mod = await this.#pipeline.getModuleForRoute(routeData); let response; try { @@ -405,7 +398,7 @@ export class App { return this.#mergeResponses(response, originalResponse, override); } - const mod = await this.#getModuleForRoute(errorRouteData); + const mod = await this.#pipeline.getModuleForRoute(errorRouteData); try { const renderContext = RenderContext.create({ locals, @@ -493,35 +486,4 @@ export class App { if (route.endsWith('/500')) return 500; return 200; } - - async #getModuleForRoute(route: RouteData): Promise { - if (route.component === DEFAULT_404_COMPONENT) { - return { - page: async () => - ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance, - renderers: [], - }; - } - if (route.type === 'redirect') { - return RedirectSinglePageBuiltModule; - } else { - if (this.#manifest.pageMap) { - const importComponentInstance = this.#manifest.pageMap.get(route.component); - if (!importComponentInstance) { - throw new Error( - `Unexpectedly unable to find a component instance for route ${route.route}` - ); - } - const pageModule = await importComponentInstance(); - return pageModule; - } else if (this.#manifest.pageModule) { - const importComponentInstance = this.#manifest.pageModule; - return importComponentInstance; - } else { - throw new Error( - "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue." - ); - } - } - } } diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index b1c615a1eb36..77d2f80b24f2 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,21 +1,46 @@ -import type { RouteData, SSRElement, SSRResult } from '../../@types/astro.js'; +import type { + ManifestData, + RouteData, + SSRElement, + SSRResult, + ComponentInstance, + RewritePayload, +} from '../../@types/astro.js'; import { Pipeline } from '../base-pipeline.js'; +import { DEFAULT_404_COMPONENT } from '../constants.js'; +import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js'; +import type { SinglePageBuiltModule } from '../build/types.js'; export class AppPipeline extends Pipeline { - static create({ - logger, - manifest, - mode, - renderers, - resolve, - serverLike, - streaming, - }: Pick< - AppPipeline, - 'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming' - >) { - return new AppPipeline(logger, manifest, mode, renderers, resolve, serverLike, streaming); + #manifestData: ManifestData | undefined; + + static create( + manifestData: ManifestData, + { + logger, + manifest, + mode, + renderers, + resolve, + serverLike, + streaming, + }: Pick< + AppPipeline, + 'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming' + > + ) { + const pipeline = new AppPipeline( + logger, + manifest, + mode, + renderers, + resolve, + serverLike, + streaming + ); + pipeline.#manifestData = manifestData; + return pipeline; } headElements(routeData: RouteData): Pick { @@ -41,4 +66,64 @@ export class AppPipeline extends Pipeline { } componentMetadata() {} + async getComponentByRoute(routeData: RouteData): Promise { + const module = await this.getModuleForRoute(routeData); + return module.page(); + } + + async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { + let foundRoute; + + for (const route of this.#manifestData!.routes) { + if (payload instanceof URL) { + if (route.pattern.test(payload.pathname)) { + foundRoute = route; + break; + } + } else if (payload instanceof Request) { + const url = new URL(payload.url); + if (route.pattern.test(url.pathname)) { + foundRoute = route; + break; + } + } else if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; + } + } + + if (foundRoute) { + const componentInstance = await this.getComponentByRoute(foundRoute); + return [foundRoute, componentInstance]; + } + throw new Error('Route not found'); + } + + async getModuleForRoute(route: RouteData): Promise { + if (route.component === DEFAULT_404_COMPONENT) { + return { + page: async () => + ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance, + renderers: [], + }; + } + if (route.type === 'redirect') { + return RedirectSinglePageBuiltModule; + } else { + if (this.manifest.pageMap) { + const importComponentInstance = this.manifest.pageMap.get(route.component); + if (!importComponentInstance) { + throw new Error( + `Unexpectedly unable to find a component instance for route ${route.route}` + ); + } + return await importComponentInstance(); + } else if (this.manifest.pageModule) { + return this.manifest.pageModule; + } + throw new Error( + "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue." + ); + } + } } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index fd56c6f1068f..30134252ef9d 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -65,6 +65,8 @@ export type SSRManifest = { i18n: SSRManifestI18n | undefined; middleware: MiddlewareHandler; checkOrigin: boolean; + // TODO: remove once the experimental flag is removed + rewritingEnabled: boolean; }; export type SSRManifestI18n = { diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 832823db35fa..11cff7c809f5 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -1,5 +1,7 @@ import type { + ComponentInstance, MiddlewareHandler, + RewritePayload, RouteData, RuntimeMode, SSRLoadedRenderer, @@ -59,6 +61,23 @@ export abstract class Pipeline { abstract headElements(routeData: RouteData): Promise | HeadElements; abstract componentMetadata(routeData: RouteData): Promise | void; + + /** + * It attempts to retrieve the `RouteData` that matches the input `url`, and the component that belongs to the `RouteData`. + * + * ## Errors + * + * - if not `RouteData` is found + * + * @param {RewritePayload} rewritePayload + */ + abstract tryRewrite(rewritePayload: RewritePayload): Promise<[RouteData, ComponentInstance]>; + + /** + * Tells the pipeline how to retrieve a component give a `RouteData` + * @param routeData + */ + abstract getComponentByRoute(routeData: RouteData): Promise ; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index ffe799f6e7e4..355d551eaa6c 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -35,24 +35,14 @@ import { getOutputDirectory } from '../../prerender/utils.js'; import type { SSRManifestI18n } from '../app/types.js'; import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; -import { routeIsFallback } from '../redirects/helpers.js'; -import { - RedirectSinglePageBuiltModule, - getRedirectLocationOrThrow, - routeIsRedirect, -} from '../redirects/index.js'; +import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js'; import { RenderContext } from '../render-context.js'; import { callGetStaticPaths } from '../render/route-cache.js'; import { createRequest } from '../request.js'; import { matchRoute } from '../routing/match.js'; import { getOutputFilename, isServerLikeOutput } from '../util.js'; import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; -import { - cssOrder, - getEntryFilePathFromComponentPath, - getPageDataByComponent, - mergeInlineCss, -} from './internal.js'; +import { cssOrder, getPageDataByComponent, mergeInlineCss } from './internal.js'; import { BuildPipeline } from './pipeline.js'; import type { PageBuildData, @@ -66,46 +56,6 @@ function createEntryURL(filePath: string, outFolder: URL) { return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); } -async function getEntryForRedirectRoute( - route: RouteData, - internals: BuildInternals, - outFolder: URL -): Promise { - if (route.type !== 'redirect') { - throw new Error(`Expected a redirect route.`); - } - if (route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); - if (filePath) { - const url = createEntryURL(filePath, outFolder); - const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); - return ssrEntryPage; - } - } - - return RedirectSinglePageBuiltModule; -} - -async function getEntryForFallbackRoute( - route: RouteData, - internals: BuildInternals, - outFolder: URL -): Promise { - if (route.type !== 'fallback') { - throw new Error(`Expected a redirect route.`); - } - if (route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); - if (filePath) { - const url = createEntryURL(filePath, outFolder); - const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); - return ssrEntryPage; - } - } - - return RedirectSinglePageBuiltModule; -} - // Gives back a facadeId that is relative to the root. // ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): string { @@ -185,14 +135,15 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil }); } - const ssrEntryURLPage = createEntryURL(filePath, outFolder); - const ssrEntryPage = await import(ssrEntryURLPage.toString()); + const ssrEntryPage = await pipeline.retrieveSsrEntry(pageData.route, filePath); if (options.settings.adapter?.adapterFeatures?.functionPerRoute) { // forcing to use undefined, so we fail in an expected way if the module is not even there. + // @ts-expect-error When building for `functionPerRoute`, the module exports a `pageModule` function instead const ssrEntry = ssrEntryPage?.pageModule; if (ssrEntry) { await generatePage(pageData, ssrEntry, builtPaths, pipeline); } else { + const ssrEntryURLPage = createEntryURL(filePath, outFolder); throw new Error( `Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.` ); @@ -205,18 +156,8 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil } } else { for (const [pageData, filePath] of pagesToGenerate) { - if (routeIsRedirect(pageData.route)) { - const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder); - await generatePage(pageData, entry, builtPaths, pipeline); - } else if (routeIsFallback(pageData.route)) { - const entry = await getEntryForFallbackRoute(pageData.route, internals, outFolder); - await generatePage(pageData, entry, builtPaths, pipeline); - } else { - const ssrEntryURLPage = createEntryURL(filePath, outFolder); - const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString()); - - await generatePage(pageData, entry, builtPaths, pipeline); - } + const entry = await pipeline.retrieveSsrEntry(pageData.route, filePath); + await generatePage(pageData, entry, builtPaths, pipeline); } } logger.info( @@ -232,12 +173,12 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil .map((x) => x.transforms.size) .reduce((a, b) => a + b, 0); const cpuCount = os.cpus().length; - const assetsCreationpipeline = await prepareAssetsGenerationEnv(pipeline, totalCount); + const assetsCreationPipeline = await prepareAssetsGenerationEnv(pipeline, totalCount); const queue = new PQueue({ concurrency: Math.max(cpuCount, 1) }); const assetsTimer = performance.now(); for (const [originalPath, transforms] of staticImageList) { - await generateImagesForPath(originalPath, transforms, assetsCreationpipeline, queue); + await generateImagesForPath(originalPath, transforms, assetsCreationPipeline, queue); } await queue.onIdle(); @@ -615,6 +556,7 @@ function createBuildManifest( i18n: i18nManifest, buildFormat: settings.config.build.format, middleware, + rewritingEnabled: settings.config.experimental.rewriting, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, }; } diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index a78c8eaf893c..532759f1e426 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,4 +1,10 @@ -import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../@types/astro.js'; +import type { + ComponentInstance, + RewritePayload, + RouteData, + SSRLoadedRenderer, + SSRResult, +} from '../../@types/astro.js'; import { getOutputDirectory } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import type { SSRManifest } from '../app/types.js'; @@ -13,20 +19,44 @@ import { isServerLikeOutput } from '../util.js'; import { type BuildInternals, cssOrder, + getEntryFilePathFromComponentPath, getPageDataByComponent, mergeInlineCss, } from './internal.js'; import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js'; -import { getVirtualModulePageNameFromPath } from './plugins/util.js'; -import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; -import type { PageBuildData, StaticBuildOptions } from './types.js'; +import { + ASTRO_PAGE_EXTENSION_POST_PATTERN, + getVirtualModulePageNameFromPath, +} from './plugins/util.js'; +import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; +import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; +import { getOutDirWithinCwd } from './common.js'; +import { RouteNotFound } from '../errors/errors-data.js'; +import { AstroError } from '../errors/index.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. */ export class BuildPipeline extends Pipeline { + #componentsInterner: WeakMap = new WeakMap< + RouteData, + SinglePageBuiltModule + >(); + /** + * This cache is needed to map a single `RouteData` to its file path. + * @private + */ + #routesByFilePath: WeakMap = new WeakMap (); + + get outFolder() { + const ssr = isServerLikeOutput(this.settings.config); + return ssr + ? this.settings.config.build.server + : getOutDirWithinCwd(this.settings.config.outDir); + } + private constructor( readonly internals: BuildInternals, readonly manifest: SSRManifest, @@ -225,6 +255,113 @@ export class BuildPipeline extends Pipeline { } } + for (const [buildData, filePath] of pages.entries()) { + this.#routesByFilePath.set(buildData.route, filePath); + } + return pages; } + + async getComponentByRoute(routeData: RouteData): Promise { + if (this.#componentsInterner.has(routeData)) { + // SAFETY: checked before + const entry = this.#componentsInterner.get(routeData)!; + return await entry.page(); + } else { + // SAFETY: the pipeline calls `retrieveRoutesToGenerate`, which is in charge to fill the cache. + const filePath = this.#routesByFilePath.get(routeData)!; + const module = await this.retrieveSsrEntry(routeData, filePath); + return module.page(); + } + } + + async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { + let foundRoute: RouteData | undefined; + // options.manifest is the actual type that contains the information + for (const route of this.options.manifest.routes) { + if (payload instanceof URL) { + if (route.pattern.test(payload.pathname)) { + foundRoute = route; + break; + } + } else if (payload instanceof Request) { + const url = new URL(payload.url); + if (route.pattern.test(url.pathname)) { + foundRoute = route; + break; + } + } else if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; + } + } + if (foundRoute) { + const componentInstance = await this.getComponentByRoute(foundRoute); + return [foundRoute, componentInstance]; + } else { + throw new AstroError(RouteNotFound); + } + } + + async retrieveSsrEntry(route: RouteData, filePath: string): Promise { + if (this.#componentsInterner.has(route)) { + // SAFETY: it is checked inside the if + return this.#componentsInterner.get(route)!; + } + let entry; + if (routeIsRedirect(route)) { + entry = await this.#getEntryForRedirectRoute(route, this.internals, this.outFolder); + } else if (routeIsFallback(route)) { + entry = await this.#getEntryForFallbackRoute(route, this.internals, this.outFolder); + } else { + const ssrEntryURLPage = createEntryURL(filePath, this.outFolder); + entry = await import(ssrEntryURLPage.toString()); + } + this.#componentsInterner.set(route, entry); + return entry; + } + + async #getEntryForFallbackRoute( + route: RouteData, + internals: BuildInternals, + outFolder: URL + ): Promise { + if (route.type !== 'fallback') { + throw new Error(`Expected a redirect route.`); + } + if (route.redirectRoute) { + const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); + if (filePath) { + const url = createEntryURL(filePath, outFolder); + const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); + return ssrEntryPage; + } + } + + return RedirectSinglePageBuiltModule; + } + + async #getEntryForRedirectRoute( + route: RouteData, + internals: BuildInternals, + outFolder: URL + ): Promise { + if (route.type !== 'redirect') { + throw new Error(`Expected a redirect route.`); + } + if (route.redirectRoute) { + const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); + if (filePath) { + const url = createEntryURL(filePath, outFolder); + const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); + return ssrEntryPage; + } + } + + return RedirectSinglePageBuiltModule; + } +} + +function createEntryURL(filePath: string, outFolder: URL) { + return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); } diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 498ccdbb544b..5bb6ddab038a 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -277,5 +277,6 @@ function buildManifest( i18n: i18nManifest, buildFormat: settings.config.build.format, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + rewritingEnabled: settings.config.experimental.rewriting, }; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 303846f7608f..0fd4c58e66fd 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -87,6 +87,7 @@ const ASTRO_CONFIG_DEFAULTS = { globalRoutePriority: false, i18nDomains: false, security: {}, + rewriting: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -525,6 +526,7 @@ export const AstroConfigSchema = z.object({ .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.security), i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains), + rewriting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rewriting), }) .strict( `Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.` diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 180160064ab1..7ebc3a3831a1 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1483,6 +1483,18 @@ export const UnsupportedConfigTransformError = { hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue', } satisfies ErrorData; +/** + * @docs + * @description + * + * Astro couldn't find a route matching the one provided by the user + */ +export const RouteNotFound = { + name: 'RouteNotFound', + title: 'Route not found.', + message: `Astro could find a route that matches the one you requested.`, +} satisfies ErrorData; + // Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip. export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData; diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 0133c13d032d..b92e0f3cb19b 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -1,5 +1,11 @@ -import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro.js'; +import type { + APIContext, + MiddlewareHandler, + MiddlewareNext, + RewritePayload, +} from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; +import type { Logger } from '../logger/core.js'; /** * Utility function that is in charge of calling the middleware. @@ -38,13 +44,28 @@ import { AstroError, AstroErrorData } from '../errors/index.js'; export async function callMiddleware( onRequest: MiddlewareHandler, apiContext: APIContext, - responseFunction: () => Promise | Response + responseFunction: ( + apiContext: APIContext, + rewritePayload?: RewritePayload + ) => Promise | Response, + // TODO: remove these two arguments once rerouting goes out of experimental + enableRerouting: boolean, + logger: Logger ): Promise { let nextCalled = false; let responseFunctionPromise: Promise | Response | undefined = undefined; - const next: MiddlewareNext = async () => { + const next: MiddlewareNext = async (payload) => { nextCalled = true; - responseFunctionPromise = responseFunction(); + if (enableRerouting) { + responseFunctionPromise = responseFunction(apiContext, payload); + } else { + logger.warn( + 'router', + 'The rewrite API is experimental. To use this feature, add the `rewriting` flag to the `experimental` object in your Astro config.' + ); + responseFunctionPromise = responseFunction(apiContext); + } + // We need to pass the APIContext pass to `callMiddleware` because it can be mutated across middleware functions return responseFunctionPromise; }; diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index cb9304bffbe1..17c206d6e9f5 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,17 +1,14 @@ -import type { APIContext, MiddlewareHandler, Params } from '../../@types/astro.js'; +import type { APIContext, MiddlewareHandler, Params, RewritePayload } from '../../@types/astro.js'; import { computeCurrentLocale, computePreferredLocale, computePreferredLocaleList, } from '../../i18n/utils.js'; -import { ASTRO_VERSION } from '../constants.js'; +import { ASTRO_VERSION, clientLocalsSymbol, clientAddressSymbol } from '../constants.js'; import { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { sequence } from './sequence.js'; -const clientAddressSymbol = Symbol.for('astro.clientAddress'); -const clientLocalsSymbol = Symbol.for('astro.locals'); - function defineMiddleware(fn: MiddlewareHandler) { return fn; } @@ -49,6 +46,12 @@ function createContext({ const url = new URL(request.url); const route = url.pathname; + // TODO verify that this function works in an edge middleware environment + const reroute = (_reroutePayload: RewritePayload) => { + // return dummy response + return Promise.resolve(new Response(null)); + }; + return { cookies: new AstroCookies(request), request, @@ -56,6 +59,7 @@ function createContext({ site: undefined, generator: `Astro v${ASTRO_VERSION}`, props: {}, + rewrite: reroute, redirect(path, status) { return new Response(null, { status: status || 302, diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 9a68963945ec..ef27d03c2cb4 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,5 +1,6 @@ -import type { APIContext, MiddlewareHandler } from '../../@types/astro.js'; +import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js'; import { defineMiddleware } from './index.js'; +import { AstroCookies } from '../cookies/cookies.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js /** @@ -10,13 +11,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { const filtered = handlers.filter((h) => !!h); const length = filtered.length; if (!length) { - const handler: MiddlewareHandler = defineMiddleware((context, next) => { + return defineMiddleware((_context, next) => { return next(); }); - return handler; } return defineMiddleware((context, next) => { + /** + * This variable is used to carry the rerouting payload across middleware functions. + */ + let carriedPayload: RewritePayload | undefined = undefined; return applyHandle(0, context); function applyHandle(i: number, handleContext: APIContext) { @@ -24,11 +28,28 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { // @ts-expect-error // SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`. - const result = handle(handleContext, async () => { + const result = handle(handleContext, async (payload: RewritePayload) => { if (i < length - 1) { + if (payload) { + let newRequest; + if (payload instanceof Request) { + newRequest = payload; + } else if (payload instanceof URL) { + newRequest = new Request(payload, handleContext.request); + } else { + newRequest = new Request( + new URL(payload, handleContext.url.origin), + handleContext.request + ); + } + carriedPayload = payload; + handleContext.request = newRequest; + handleContext.url = new URL(newRequest.url); + handleContext.cookies = new AstroCookies(newRequest); + } return applyHandle(i + 1, handleContext); } else { - return next(); + return next(payload ?? carriedPayload); } }); return result; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 5cfc8ef2ede3..279745ac19e4 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -4,6 +4,8 @@ import type { AstroGlobalPartial, ComponentInstance, MiddlewareHandler, + MiddlewareNext, + RewritePayload, RouteData, SSRResult, } from '../@types/astro.js'; @@ -39,14 +41,23 @@ export class RenderContext { public locals: App.Locals, readonly middleware: MiddlewareHandler, readonly pathname: string, - readonly request: Request, - readonly routeData: RouteData, + public request: Request, + public routeData: RouteData, public status: number, - readonly cookies = new AstroCookies(request), - readonly params = getParams(routeData, pathname), - readonly url = new URL(request.url) + protected cookies = new AstroCookies(request), + public params = getParams(routeData, pathname), + protected url = new URL(request.url) ) {} + /** + * A flag that tells the render content if the rewriting was triggered + */ + isRewriting = false; + /** + * A safety net in case of loops + */ + counter = 0; + static create({ locals = {}, middleware, @@ -56,7 +67,7 @@ export class RenderContext { routeData, status = 200, }: Pick & - Partial >) { + Partial >): RenderContext { return new RenderContext( pipeline, locals, @@ -80,11 +91,11 @@ export class RenderContext { * - fallback */ async render(componentInstance: ComponentInstance | undefined): Promise { - const { cookies, middleware, pathname, pipeline, routeData } = this; + const { cookies, middleware, pathname, pipeline } = this; const { logger, routeCache, serverLike, streaming } = pipeline; const props = await getProps({ mod: componentInstance, - routeData, + routeData: this.routeData, routeCache, pathname, logger, @@ -92,10 +103,40 @@ export class RenderContext { }); const apiContext = this.createAPIContext(props); - const lastNext = async () => { - switch (routeData.type) { + this.counter++; + if (this.counter === 4) { + return new Response('Loop Detected', { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508 + status: 508, + statusText: + 'Astro detected a loop where you tried to call the rewriting logic more than four times.', + }); + } + const lastNext = async (ctx: APIContext, payload?: RewritePayload) => { + if (payload) { + if (this.pipeline.manifest.rewritingEnabled) { + try { + const [routeData, component] = await pipeline.tryRewrite(payload); + this.routeData = routeData; + componentInstance = component; + } catch (e) { + return new Response('Not found', { + status: 404, + statusText: 'Not found', + }); + } finally { + this.isRewriting = true; + } + } else { + this.pipeline.logger.warn( + 'router', + 'The rewrite API is experimental. To use this feature, add the `rewriting` flag to the `experimental` object in your Astro config.' + ); + } + } + switch (this.routeData.type) { case 'endpoint': - return renderEndpoint(componentInstance as any, apiContext, serverLike, logger); + return renderEndpoint(componentInstance as any, ctx, serverLike, logger); case 'redirect': return renderRedirect(this); case 'page': { @@ -108,7 +149,7 @@ export class RenderContext { props, {}, streaming, - routeData + this.routeData ); } catch (e) { // If there is an error in the page's frontmatter or instantiation of the RenderTemplate fails midway, @@ -119,7 +160,11 @@ export class RenderContext { // Signal to the i18n middleware to maybe act on this response response.headers.set(ROUTE_TYPE_HEADER, 'page'); // Signal to the error-page-rerouting infra to let this response pass through to avoid loops - if (routeData.route === '/404' || routeData.route === '/500') { + if ( + this.routeData.route === '/404' || + this.routeData.route === '/500' || + this.isRewriting + ) { response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); } return response; @@ -130,7 +175,13 @@ export class RenderContext { } }; - const response = await callMiddleware(middleware, apiContext, lastNext); + const response = await callMiddleware( + middleware, + apiContext, + lastNext, + this.pipeline.manifest.rewritingEnabled, + this.pipeline.logger + ); if (response.headers.get(ROUTE_TYPE_HEADER)) { response.headers.delete(ROUTE_TYPE_HEADER); } @@ -143,10 +194,38 @@ export class RenderContext { createAPIContext(props: APIContext['props']): APIContext { const renderContext = this; - const { cookies, params, pipeline, request, url } = this; + const { cookies, params, pipeline, url } = this; const generator = `Astro v${ASTRO_VERSION}`; const redirect = (path: string, status = 302) => new Response(null, { status, headers: { Location: path } }); + + const rewrite = async (reroutePayload: RewritePayload) => { + pipeline.logger.debug('router', 'Called rewriting to:', reroutePayload); + try { + const [routeData, component] = await pipeline.tryRewrite(reroutePayload); + this.routeData = routeData; + if (reroutePayload instanceof Request) { + this.request = reroutePayload; + } else { + this.request = new Request( + new URL(routeData.pathname ?? routeData.route, this.url.origin), + this.request + ); + } + this.url = new URL(this.request.url); + this.cookies = new AstroCookies(this.request); + this.params = getParams(routeData, url.toString()); + this.isRewriting = true; + return await this.render(component); + } catch (e) { + pipeline.logger.debug('router', 'Rewrite failed.', e); + return new Response('Not found', { + status: 404, + statusText: 'Not found', + }); + } + }; + return { cookies, get clientAddress() { @@ -167,7 +246,7 @@ export class RenderContext { renderContext.locals = val; // we also put it on the original Request object, // where the adapter might be expecting to read it after the response. - Reflect.set(request, clientLocalsSymbol, val); + Reflect.set(this.request, clientLocalsSymbol, val); } }, params, @@ -179,7 +258,8 @@ export class RenderContext { }, props, redirect, - request, + rewrite, + request: this.request, site: pipeline.site, url, }; @@ -294,11 +374,11 @@ export class RenderContext { astroStaticPartial: AstroGlobalPartial ): Omit { const renderContext = this; - const { cookies, locals, params, pipeline, request, url } = this; + const { cookies, locals, params, pipeline, url } = this; const { response } = result; const redirect = (path: string, status = 302) => { // If the response is already sent, error as we cannot proceed with the redirect. - if ((request as any)[responseSentSymbol]) { + if ((this.request as any)[responseSentSymbol]) { throw new AstroError({ ...AstroErrorData.ResponseSentError, }); @@ -306,6 +386,33 @@ export class RenderContext { return new Response(null, { status, headers: { Location: path } }); }; + const rewrite = async (reroutePayload: RewritePayload) => { + try { + pipeline.logger.debug('router', 'Calling rewrite: ', reroutePayload); + const [routeData, component] = await pipeline.tryRewrite(reroutePayload); + this.routeData = routeData; + if (reroutePayload instanceof Request) { + this.request = reroutePayload; + } else { + this.request = new Request( + new URL(routeData.pathname ?? routeData.route, this.url.origin), + this.request + ); + } + this.url = new URL(this.request.url); + this.cookies = new AstroCookies(this.request); + this.params = getParams(routeData, url.toString()); + this.isRewriting = true; + return await this.render(component); + } catch (e) { + pipeline.logger.debug('router', 'Rerouting failed, returning a 404.', e); + return new Response('Not found', { + status: 404, + statusText: 'Not found', + }); + } + }; + return { generator: astroStaticPartial.generator, glob: astroStaticPartial.glob, @@ -325,7 +432,8 @@ export class RenderContext { }, locals, redirect, - request, + rewrite, + request: this.request, response, site: pipeline.site, url, diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts index e6c09dd70279..cbdddff5c8cb 100644 --- a/packages/astro/src/prerender/routing.ts +++ b/packages/astro/src/prerender/routing.ts @@ -54,7 +54,7 @@ async function preloadAndSetPrerenderStatus({ continue; } - const preloadedComponent = await pipeline.preload(filePath); + const preloadedComponent = await pipeline.preload(route, filePath); // gets the prerender metadata set by the `astro:scanner` vite plugin const prerenderStatus = getPrerenderStatus({ diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 7ccc63638284..685d13f570c4 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -1,8 +1,10 @@ -import url from 'node:url'; +import { fileURLToPath } from 'node:url'; import type { AstroSettings, ComponentInstance, DevToolbarMetadata, + ManifestData, + RewritePayload, RouteData, SSRElement, SSRLoadedRenderer, @@ -12,10 +14,10 @@ import { getInfoOutput } from '../cli/info/index.js'; import type { HeadElements } from '../core/base-pipeline.js'; import { ASTRO_VERSION, DEFAULT_404_COMPONENT } from '../core/constants.js'; import { enhanceViteSSRError } from '../core/errors/dev/index.js'; -import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js'; +import { AggregateError, AstroError, CSSError, MarkdownError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; -import { Pipeline, loadRenderer } from '../core/render/index.js'; +import { loadRenderer, Pipeline } from '../core/render/index.js'; import { isPage, isServerLikeOutput, resolveIdToUrl, viteID } from '../core/util.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; import { getStylesForURL } from './css.js'; @@ -23,12 +25,20 @@ import { getComponentMetadata } from './metadata.js'; import { createResolve } from './resolve.js'; import { default404Page } from './response.js'; import { getScriptsForURL } from './scripts.js'; +import { RouteNotFound } from '../core/errors/errors-data.js'; export class DevPipeline extends Pipeline { // renderers are loaded on every request, // so it needs to be mutable here unlike in other environments override renderers = new Array (); + manifestData: ManifestData | undefined; + + componentInterner: WeakMap = new WeakMap< + RouteData, + ComponentInstance + >(); + private constructor( readonly loader: ModuleLoader, readonly logger: Logger, @@ -43,13 +53,18 @@ export class DevPipeline extends Pipeline { super(logger, manifest, mode, [], resolve, serverLike, streaming); } - static create({ - loader, - logger, - manifest, - settings, - }: Pick ) { - return new DevPipeline(loader, logger, manifest, settings); + static create( + manifestData: ManifestData, + { + loader, + logger, + manifest, + settings, + }: Pick + ) { + const pipeline = new DevPipeline(loader, logger, manifest, settings); + pipeline.manifestData = manifestData; + return pipeline; } async headElements(routeData: RouteData): Promise { @@ -59,7 +74,7 @@ export class DevPipeline extends Pipeline { mode, settings, } = this; - const filePath = new URL(`./${routeData.component}`, root); + const filePath = new URL(`${routeData.component}`, root); // Add hoisted script tags, skip if direct rendering with `directRenderScript` const { scripts } = settings.config.experimental.directRenderScript ? { scripts: new Set () } @@ -80,7 +95,7 @@ export class DevPipeline extends Pipeline { scripts.add({ props: { type: 'module', src }, children: '' }); const additionalMetadata: DevToolbarMetadata['__astro_dev_toolbar__'] = { - root: url.fileURLToPath(settings.config.root), + root: fileURLToPath(settings.config.root), version: ASTRO_VERSION, latestAstroVersion: settings.latestAstroVersion, debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }), @@ -131,11 +146,11 @@ export class DevPipeline extends Pipeline { config: { root }, loader, } = this; - const filePath = new URL(`./${routeData.component}`, root); + const filePath = new URL(`${routeData.component}`, root); return getComponentMetadata(filePath, loader); } - async preload(filePath: URL) { + async preload(routeData: RouteData, filePath: URL) { const { loader } = this; if (filePath.href === new URL(DEFAULT_404_COMPONENT, this.config.root).href) { return { default: default404Page } as any as ComponentInstance; @@ -148,7 +163,9 @@ export class DevPipeline extends Pipeline { try { // Load the module from the Vite SSR Runtime. - return (await loader.import(viteID(filePath))) as ComponentInstance; + const componentInstance = (await loader.import(viteID(filePath))) as ComponentInstance; + this.componentInterner.set(routeData, componentInstance); + return componentInstance; } catch (error) { // If the error came from Markdown or CSS, we already handled it and there's no need to enhance it if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) { @@ -161,5 +178,52 @@ export class DevPipeline extends Pipeline { clearRouteCache() { this.routeCache.clearAll(); + this.componentInterner = new WeakMap (); + } + + async getComponentByRoute(routeData: RouteData): Promise { + const component = this.componentInterner.get(routeData); + if (component) { + return component; + } else { + const filePath = new URL(`${routeData.component}`, this.config.root); + return await this.preload(routeData, filePath); + } + } + + async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { + let foundRoute; + if (!this.manifestData) { + throw new Error('Missing manifest data. This is an internal error, please file an issue.'); + } + + for (const route of this.manifestData.routes) { + if (payload instanceof URL) { + if (route.pattern.test(payload.pathname)) { + foundRoute = route; + break; + } + } else if (payload instanceof Request) { + const url = new URL(payload.url); + if (route.pattern.test(url.pathname)) { + foundRoute = route; + break; + } + } else if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; + } + } + + if (foundRoute) { + const componentInstance = await this.getComponentByRoute(foundRoute); + return [foundRoute, componentInstance]; + } else { + throw new AstroError(RouteNotFound); + } + } + + setManifestData(manifestData: ManifestData) { + this.manifestData = manifestData; } } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 082de6bcebf0..3c6f06ee9a05 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -35,10 +35,10 @@ export default function createVitePluginAstroServer({ configureServer(viteServer) { const loader = createViteLoader(viteServer); const manifest = createDevelopmentManifest(settings); - const pipeline = DevPipeline.create({ loader, logger, manifest, settings }); let manifestData: ManifestData = ensure404Route( createRouteManifest({ settings, fsMod }, logger) ); + const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings }); const controller = createController({ loader }); const localStorage = new AsyncLocalStorage(); @@ -47,6 +47,7 @@ export default function createVitePluginAstroServer({ pipeline.clearRouteCache(); if (needsManifestRebuild) { manifestData = ensure404Route(createRouteManifest({ settings }, logger)); + pipeline.setManifestData(manifestData); } } // Rebuild route manifest on file change, if needed. @@ -144,6 +145,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest inlinedScripts: new Map(), i18n: i18nManifest, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + rewritingEnabled: settings.config.experimental.rewriting, middleware(_, next) { return next(); }, diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 21053420a754..85bf969f9db9 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -114,7 +114,7 @@ export async function matchRoute( if (custom404) { const filePath = new URL(`./${custom404.component}`, config.root); - const preloadedComponent = await pipeline.preload(filePath); + const preloadedComponent = await pipeline.preload(custom404, filePath); return { route: custom404, @@ -197,40 +197,39 @@ export async function handleRoute({ if (!pathNameHasLocale && pathname !== '/') { return handle404Response(origin, incomingRequest, incomingResponse); } - request = createRequest({ - base: config.base, - url, - headers: incomingRequest.headers, - logger, - // no route found, so we assume the default for rendering the 404 page - staticLike: config.output === 'static' || config.output === 'hybrid', - }); - route = { - component: '', - generate(_data: any): string { - return ''; - }, - params: [], - // Disable eslint as we only want to generate an empty RegExp - // eslint-disable-next-line prefer-regex-literals - pattern: new RegExp(''), - prerender: false, - segments: [], - type: 'fallback', - route: '', - fallbackRoutes: [], - isIndex: false, - }; - renderContext = RenderContext.create({ - pipeline: pipeline, - pathname, - middleware, - request, - routeData: route, - }); - } else { - return handle404Response(origin, incomingRequest, incomingResponse); } + request = createRequest({ + base: config.base, + url, + headers: incomingRequest.headers, + logger, + // no route found, so we assume the default for rendering the 404 page + staticLike: config.output === 'static' || config.output === 'hybrid', + }); + route = { + component: '', + generate(_data: any): string { + return ''; + }, + params: [], + // Disable eslint as we only want to generate an empty RegExp + // eslint-disable-next-line prefer-regex-literals + pattern: new RegExp(''), + prerender: false, + segments: [], + type: 'fallback', + route: '', + fallbackRoutes: [], + isIndex: false, + }; + + renderContext = RenderContext.create({ + pipeline: pipeline, + pathname, + middleware, + request, + routeData: route, + }); } else { const filePath: URL | undefined = matchedRoute.filePath; const { preloadedComponent } = matchedRoute; diff --git a/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs b/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs new file mode 100644 index 000000000000..bc095ecddb69 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from "astro/config"; + +export default defineConfig({}) diff --git a/packages/astro/test/fixtures/middleware-virtual/package.json b/packages/astro/test/fixtures/middleware-virtual/package.json new file mode 100644 index 000000000000..7cfbeb721047 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/middleware-virtual", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/middleware-virtual/src/middleware.js b/packages/astro/test/fixtures/middleware-virtual/src/middleware.js new file mode 100644 index 000000000000..55004a00cfdb --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/src/middleware.js @@ -0,0 +1,6 @@ +import { defineMiddleware } from 'astro:middleware'; + +export const onRequest = defineMiddleware(async (context, next) => { + console.log('[MIDDLEWARE] in ' + context.url.toString()); + return next(); +}); diff --git a/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro b/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro new file mode 100644 index 000000000000..9bd31f5fde27 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro @@ -0,0 +1,13 @@ +--- +const data = Astro.locals; +--- + + + + Index + + + +Index + + diff --git a/packages/astro/test/fixtures/reroute/astro.config.mjs b/packages/astro/test/fixtures/reroute/astro.config.mjs new file mode 100644 index 000000000000..af13ef19b477 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/astro.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + experimental: { + rewriting: true + }, + site: "https://example.com" +}); diff --git a/packages/astro/test/fixtures/reroute/package.json b/packages/astro/test/fixtures/reroute/package.json new file mode 100644 index 000000000000..ed64e57a97e0 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/reroute", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/reroute/src/middleware.js b/packages/astro/test/fixtures/reroute/src/middleware.js new file mode 100644 index 000000000000..4d7c2a7956c8 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/middleware.js @@ -0,0 +1,33 @@ +import { sequence } from 'astro:middleware'; + +let contextReroute = false; + +export const first = async (context, next) => { + if (context.url.pathname.includes('/auth')) { + } + + return next(); +}; + +export const second = async (context, next) => { + if (context.url.pathname.includes('/auth')) { + if (context.url.pathname.includes('/auth/dashboard')) { + contextReroute = true; + return await context.rewrite('/'); + } + if (context.url.pathname.includes('/auth/base')) { + return await next('/'); + } + } + return next(); +}; + +export const third = async (context, next) => { + // just making sure that we are testing the change in context coming from `next()` + if (context.url.pathname.startsWith('/') && contextReroute === false) { + context.locals.auth = 'Third function called'; + } + return next(); +}; + +export const onRequest = sequence(first, second, third); diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro new file mode 100644 index 000000000000..be31dfb14141 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro @@ -0,0 +1,10 @@ +--- +--- + + +Base + + +Base
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro new file mode 100644 index 000000000000..bfa006aa01a7 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro @@ -0,0 +1,10 @@ +--- +--- + + +Dashboard + + +Dashboard
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro new file mode 100644 index 000000000000..9eee5fe95149 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro @@ -0,0 +1,10 @@ +--- +--- + + +Settings + + +Settings
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro new file mode 100644 index 000000000000..8c38e518a7b7 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro @@ -0,0 +1,11 @@ +--- +return Astro.rewrite(new URL("../../", Astro.url)) +--- + + +Blog hello + + +Blog hello
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro new file mode 100644 index 000000000000..df1f1f76a331 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro @@ -0,0 +1,11 @@ +--- +return Astro.rewrite("/404") +--- + + +Blog hello + + +Blog hello
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro new file mode 100644 index 000000000000..89d35ce2564d --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro @@ -0,0 +1,11 @@ +--- +return Astro.rewrite(new Request(new URL("../../", Astro.url))) +--- + + +Blog hello + + +Blog hello
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro new file mode 100644 index 000000000000..8d849de160bf --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro @@ -0,0 +1,21 @@ +--- + +export function getStaticPaths() { + return [ + { params: { id: 'hello' } }, + ]; +} + + +return Astro.rewrite("/") + +--- + + + +Dynamic [id].astro + + +/dynamic/[id].astro
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/index.astro b/packages/astro/test/fixtures/reroute/src/pages/index.astro new file mode 100644 index 000000000000..91a6fd0fb0fc --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +const auth = Astro.locals.auth; +--- + + +Index + + +Index
+ {auth ?Called auth
: ""} + + diff --git a/packages/astro/test/fixtures/reroute/src/pages/reroute.astro b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro new file mode 100644 index 000000000000..dbc7a6ae628a --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro @@ -0,0 +1,11 @@ +--- +return Astro.rewrite("/") +--- + + +Reroute + + +Reroute
+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro new file mode 100644 index 000000000000..0bab88d0f7b1 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro @@ -0,0 +1,20 @@ +--- +export function getStaticPaths() { + return [ + { params: { id: 'hello' } }, + ]; +} + +return Astro.rewrite("/") + +--- + + + + +Spread [...id].astro + + +/spread/[...id].astro
+ + diff --git a/packages/astro/test/i18n-routing-manual.test.js b/packages/astro/test/i18n-routing-manual.test.js index d664b3797889..1feaf963348c 100644 --- a/packages/astro/test/i18n-routing-manual.test.js +++ b/packages/astro/test/i18n-routing-manual.test.js @@ -58,8 +58,6 @@ describe('Dev server manual routing', () => { describe('SSG manual routing', () => { /** @type {import('./test-utils').Fixture} */ let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js new file mode 100644 index 000000000000..39ff084a4c80 --- /dev/null +++ b/packages/astro/test/rewrite.test.js @@ -0,0 +1,223 @@ +import { describe, it, before, after } from 'node:test'; +import { loadFixture } from './test-utils.js'; +import { load as cheerioLoad } from 'cheerio'; +import assert from 'node:assert/strict'; +import testAdapter from './test-adapter.js'; + +describe('Dev reroute', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('should render the index page when navigating /reroute ', async () => { + const html = await fixture.fetch('/reroute').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating /blog/hello ', async () => { + const html = await fixture.fetch('/blog/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating /blog/salut ', async () => { + const html = await fixture.fetch('/blog/salut').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => { + const html = await fixture.fetch('/dynamic/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating spread route /spread/[...spread] ', async () => { + const html = await fixture.fetch('/spread/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the 404 built-in page', async () => { + const html = await fixture.fetch('/blog/oops').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), '404: Not found'); + }); +}); + +describe('Build reroute', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + await fixture.build(); + }); + + it('should render the index page when navigating /reroute ', async () => { + const html = await fixture.readFile('/reroute/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating /blog/hello ', async () => { + const html = await fixture.readFile('/blog/hello/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating /blog/salut ', async () => { + const html = await fixture.readFile('/blog/salut/index.html'); + + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => { + const html = await fixture.readFile('/dynamic/hello/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating spread route /spread/[...spread] ', async () => { + const html = await fixture.readFile('/spread/hello/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the 404 built-in page', async () => { + try { + const html = await fixture.readFile('/spread/oops/index.html'); + assert.fail('Not found'); + } catch { + assert.ok; + } + }); +}); + +describe('SSR reroute', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let app; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + output: 'server', + adapter: testAdapter(), + }); + await fixture.build(); + app = await fixture.loadTestAdapterApp(); + }); + + it('should render the index page when navigating /reroute ', async () => { + const request = new Request('http://example.com/reroute'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating /blog/hello ', async () => { + const request = new Request('http://example.com/blog/hello'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating /blog/salut ', async () => { + const request = new Request('http://example.com/blog/salut'); + const response = await app.render(request); + const html = await response.text(); + + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => { + const request = new Request('http://example.com/dynamic/hello'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the index page when navigating spread route /spread/[...spread] ', async () => { + const request = new Request('http://example.com/spread/hello'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('should render the 404 built-in page', async () => { + const request = new Request('http://example.com/blog/oops'); + const response = await app.render(request); + const html = await response.text(); + assert.equal(html, 'Not found'); + }); +}); + +describe('Middleware', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('should render a locals populated in the third middleware function, because we use next("/")', async () => { + const html = await fixture.fetch('/auth/base').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + assert.equal($('p').text(), 'Called auth'); + }); + + it('should NOT render locals populated in the third middleware function, because we use ctx.reroute("/")', async () => { + const html = await fixture.fetch('/auth/dashboard').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + assert.equal($('p').text(), ''); + }); +}); diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js index b2f27d8c9f80..5eafa6c80aea 100644 --- a/packages/astro/test/units/routing/route-matching.test.js +++ b/packages/astro/test/units/routing/route-matching.test.js @@ -146,7 +146,7 @@ describe('Route matching', () => { const loader = createViteLoader(container.viteServer); const manifest = createDevelopmentManifest(container.settings); - pipeline = DevPipeline.create({ loader, logger: defaultLogger, manifest, settings }); + pipeline = DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings }); manifestData = createRouteManifest( { cwd: fileURLToPath(root), diff --git a/packages/astro/test/units/vite-plugin-astro-server/request.test.js b/packages/astro/test/units/vite-plugin-astro-server/request.test.js index 7ea587f97e2f..f976a9d30b50 100644 --- a/packages/astro/test/units/vite-plugin-astro-server/request.test.js +++ b/packages/astro/test/units/vite-plugin-astro-server/request.test.js @@ -22,7 +22,7 @@ async function createDevPipeline(overrides = {}) { const loader = overrides.loader ?? createLoader(); const manifest = createDevelopmentManifest(settings); - return DevPipeline.create({ loader, logger: defaultLogger, manifest, settings }); + return DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings }); } describe('vite-plugin-astro-server', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43aa94a54d14..98b0868de4b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3126,6 +3126,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/middleware-virtual: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/minification-html: dependencies: astro: @@ -3324,6 +3330,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/reroute: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/root-srcdir-css: dependencies: astro: From 4436398642d03695220b594e0b2a1b751a9b972c Mon Sep 17 00:00:00 2001 From: Emanuele StoppaDate: Wed, 8 May 2024 09:27:28 +0000 Subject: [PATCH 059/151] [ci] format --- packages/astro/src/core/app/pipeline.ts | 6 +++--- packages/astro/src/core/build/pipeline.ts | 8 ++++---- packages/astro/src/core/middleware/index.ts | 2 +- packages/astro/src/core/middleware/sequence.ts | 2 +- packages/astro/src/vite-plugin-astro-server/pipeline.ts | 4 ++-- packages/astro/test/rewrite.test.js | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 77d2f80b24f2..f62dc84ed983 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,16 +1,16 @@ import type { + ComponentInstance, ManifestData, + RewritePayload, RouteData, SSRElement, SSRResult, - ComponentInstance, - RewritePayload, } from '../../@types/astro.js'; import { Pipeline } from '../base-pipeline.js'; +import type { SinglePageBuiltModule } from '../build/types.js'; import { DEFAULT_404_COMPONENT } from '../constants.js'; import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js'; -import type { SinglePageBuiltModule } from '../build/types.js'; export class AppPipeline extends Pipeline { #manifestData: ManifestData | undefined; diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index 532759f1e426..daae6940e046 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -8,7 +8,10 @@ import type { import { getOutputDirectory } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import type { SSRManifest } from '../app/types.js'; +import { RouteNotFound } from '../errors/errors-data.js'; +import { AstroError } from '../errors/index.js'; import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js'; +import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { Pipeline } from '../render/index.js'; import { createAssetLink, @@ -16,6 +19,7 @@ import { createStylesheetElementSet, } from '../render/ssr-element.js'; import { isServerLikeOutput } from '../util.js'; +import { getOutDirWithinCwd } from './common.js'; import { type BuildInternals, cssOrder, @@ -31,10 +35,6 @@ import { } from './plugins/util.js'; import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; -import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; -import { getOutDirWithinCwd } from './common.js'; -import { RouteNotFound } from '../errors/errors-data.js'; -import { AstroError } from '../errors/index.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index 17c206d6e9f5..358cf31fe6f3 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -4,7 +4,7 @@ import { computePreferredLocale, computePreferredLocaleList, } from '../../i18n/utils.js'; -import { ASTRO_VERSION, clientLocalsSymbol, clientAddressSymbol } from '../constants.js'; +import { ASTRO_VERSION, clientAddressSymbol, clientLocalsSymbol } from '../constants.js'; import { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { sequence } from './sequence.js'; diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index ef27d03c2cb4..8b2c2b49c572 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,6 +1,6 @@ import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js'; -import { defineMiddleware } from './index.js'; import { AstroCookies } from '../cookies/cookies.js'; +import { defineMiddleware } from './index.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js /** diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 685d13f570c4..797c5d29abdd 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -14,10 +14,11 @@ import { getInfoOutput } from '../cli/info/index.js'; import type { HeadElements } from '../core/base-pipeline.js'; import { ASTRO_VERSION, DEFAULT_404_COMPONENT } from '../core/constants.js'; import { enhanceViteSSRError } from '../core/errors/dev/index.js'; +import { RouteNotFound } from '../core/errors/errors-data.js'; import { AggregateError, AstroError, CSSError, MarkdownError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; -import { loadRenderer, Pipeline } from '../core/render/index.js'; +import { Pipeline, loadRenderer } from '../core/render/index.js'; import { isPage, isServerLikeOutput, resolveIdToUrl, viteID } from '../core/util.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; import { getStylesForURL } from './css.js'; @@ -25,7 +26,6 @@ import { getComponentMetadata } from './metadata.js'; import { createResolve } from './resolve.js'; import { default404Page } from './response.js'; import { getScriptsForURL } from './scripts.js'; -import { RouteNotFound } from '../core/errors/errors-data.js'; export class DevPipeline extends Pipeline { // renderers are loaded on every request, diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js index 39ff084a4c80..1c76ce10af74 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.js @@ -1,8 +1,8 @@ -import { describe, it, before, after } from 'node:test'; -import { loadFixture } from './test-utils.js'; -import { load as cheerioLoad } from 'cheerio'; import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { load as cheerioLoad } from 'cheerio'; import testAdapter from './test-adapter.js'; +import { loadFixture } from './test-utils.js'; describe('Dev reroute', () => { /** @type {import('./test-utils').Fixture} */ From 61f47a684235a049cbfc4f2cbb5edff3befeced7 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Wed, 8 May 2024 12:19:54 +0200 Subject: [PATCH 060/151] Support passing inline Astro config to getViteConfig() (#10963) * Support passing inline Astro config to `getViteConfig()` * Add changeset --- .changeset/nervous-waves-shop.md | 22 ++++++++++++++++++++++ packages/astro/src/config/index.ts | 6 +++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 .changeset/nervous-waves-shop.md diff --git a/.changeset/nervous-waves-shop.md b/.changeset/nervous-waves-shop.md new file mode 100644 index 000000000000..2abf43d1527b --- /dev/null +++ b/.changeset/nervous-waves-shop.md @@ -0,0 +1,22 @@ +--- +"astro": minor +--- + +Adds support for passing an inline Astro configuration object to `getViteConfig()` + +If you are using `getViteConfig()` to configure the Vitest test runner, you can now pass a second argument to control how Astro is configured. This makes it possible to configure unit tests with different Astro options when using [Vitest’s workspaces](https://vitest.dev/guide/workspace.html) feature. + +```js +// vitest.config.ts +import { getViteConfig } from 'astro/config'; + +export default getViteConfig( + /* Vite configuration */ + { test: {} }, + /* Astro configuration */ + { + site: 'https://example.com', + trailingSlash: 'never', + }, +); +``` diff --git a/packages/astro/src/config/index.ts b/packages/astro/src/config/index.ts index 3f4652f20419..7950990ec7f7 100644 --- a/packages/astro/src/config/index.ts +++ b/packages/astro/src/config/index.ts @@ -1,12 +1,12 @@ import type { UserConfig } from 'vite'; -import type { AstroUserConfig } from '../@types/astro.js'; +import type { AstroInlineConfig, AstroUserConfig } from '../@types/astro.js'; import { Logger } from '../core/logger/core.js'; export function defineConfig(config: AstroUserConfig) { return config; } -export function getViteConfig(inlineConfig: UserConfig) { +export function getViteConfig(inlineConfig: UserConfig, inlineAstroConfig: AstroInlineConfig = {}) { // Return an async Vite config getter which exposes a resolved `mode` and `command` return async ({ mode, command }: { mode: string; command: 'serve' | 'build' }) => { // Vite `command` is `serve | build`, but Astro uses `dev | build` @@ -34,7 +34,7 @@ export function getViteConfig(inlineConfig: UserConfig) { dest: nodeLogDestination, level: 'info', }); - const { astroConfig: config } = await resolveConfig({}, cmd); + const { astroConfig: config } = await resolveConfig(inlineAstroConfig, cmd); let settings = await createSettings(config, inlineConfig.root); settings = await runHookConfigSetup({ settings, command: cmd, logger }); const viteConfig = await createVite( From 698c2d9bb51e20b38de405b6076fd6488ddb5c2b Mon Sep 17 00:00:00 2001 From: Goulven CLEC'H Date: Wed, 8 May 2024 12:33:19 +0200 Subject: [PATCH 061/151] fix(page-data): add key to allPages (#10625) * fix(page-data): add key to allPages * add fryuni's test * replaced object.entries(allpages) * tmp: change pagesByComponents by pagesByKeys * fix pagesByKeys.get() in plugin-ssr & plugin-manifest * remove logs * remove useless generators * another useless generator * use null byte in key * tmp function in pipeline.ts * refactor getVirtualModulePageName * refactor getPageKeyFromVirtualModulePageName * clean & comments * better key and fix build * utils: add makePageDataKey * fix(pipeline): retrieveRoutesToGenerate for ssr * internals: getPageData function * tmp(ssr-split-manifest): fix test ? * fix?: ssr clean static output * internals: getPageDatasWithPublicKey * internals: getPageDatasByHoistedScriptId & getPagesDatasByComponent * remove broken & useless virtualModuleNameFromResolvedId * chore: changeset * fix: sanitize slashes in filepaths * Revert "fix: sanitize slashes in filepaths" This reverts commit 5c3a75fac8ab9a3be8e4560f0aeb7e4c3a114d7f. * fix?: remove route from virtual module name * fix: concat & array.from * update changeset * clean unnecessary change * remove unnecessary pageInfo * add return types to utils functions * revert a comment deletion * fix cleanStaticOutput * changes from ematipico review * moving a todo outside jsdoc (cc @ematipico ) * Update .changeset/great-turtles-greet.md Co-authored-by: Sarah Rainsberger * Update .changeset/great-turtles-greet.md Co-authored-by: Sarah Rainsberger * chore: fix merge conflicts * fix: incorrect function * remove logs * revert: codepoint change --------- Co-authored-by: Princesseuh <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: Sarah Rainsberger Co-authored-by: Emanuele Stoppa --- .changeset/great-turtles-greet.md | 5 + packages/astro/src/core/build/generate.ts | 7 +- packages/astro/src/core/build/internal.ts | 150 ++++++++++-------- packages/astro/src/core/build/page-data.ts | 11 +- packages/astro/src/core/build/pipeline.ts | 71 +++++---- .../src/core/build/plugins/plugin-css.ts | 11 +- .../src/core/build/plugins/plugin-manifest.ts | 3 +- .../src/core/build/plugins/plugin-pages.ts | 20 +-- .../src/core/build/plugins/plugin-ssr.ts | 31 ++-- packages/astro/src/core/build/plugins/util.ts | 60 +++++-- packages/astro/src/core/build/static-build.ts | 44 ++--- packages/astro/src/core/build/types.ts | 1 + .../astro.config.mjs | 34 ++++ .../reuse-injected-entrypoint/package.json | 8 + .../reuse-injected-entrypoint/src/[id].astro | 20 +++ .../src/pages/index.astro | 12 ++ .../src/to-inject.astro | 12 ++ .../test/reuse-injected-entrypoint.test.js | 135 ++++++++++++++++ packages/astro/test/test-utils.js | 4 +- pnpm-lock.yaml | 126 ++++++++++----- 20 files changed, 549 insertions(+), 216 deletions(-) create mode 100644 .changeset/great-turtles-greet.md create mode 100644 packages/astro/test/fixtures/reuse-injected-entrypoint/astro.config.mjs create mode 100644 packages/astro/test/fixtures/reuse-injected-entrypoint/package.json create mode 100644 packages/astro/test/fixtures/reuse-injected-entrypoint/src/[id].astro create mode 100644 packages/astro/test/fixtures/reuse-injected-entrypoint/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/reuse-injected-entrypoint/src/to-inject.astro create mode 100644 packages/astro/test/reuse-injected-entrypoint.test.js diff --git a/.changeset/great-turtles-greet.md b/.changeset/great-turtles-greet.md new file mode 100644 index 000000000000..7043e0e239d4 --- /dev/null +++ b/.changeset/great-turtles-greet.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Adds the ability for multiple pages to use the same component as an `entrypoint` when building an Astro integration. This change is purely internal, and aligns the build process with the behaviour in the development server. \ No newline at end of file diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 355d551eaa6c..dbd1e915df96 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -42,7 +42,7 @@ import { createRequest } from '../request.js'; import { matchRoute } from '../routing/match.js'; import { getOutputFilename, isServerLikeOutput } from '../util.js'; import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; -import { cssOrder, getPageDataByComponent, mergeInlineCss } from './internal.js'; +import { cssOrder, mergeInlineCss } from './internal.js'; import { BuildPipeline } from './pipeline.js'; import type { PageBuildData, @@ -51,6 +51,8 @@ import type { StylesheetAsset, } from './types.js'; import { getTimeStat, shouldAppendForwardSlash } from './util.js'; +import { getVirtualModulePageName } from './plugins/util.js'; +import { ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages.js'; function createEntryURL(filePath: string, outFolder: URL) { return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); @@ -200,7 +202,6 @@ async function generatePage( // prepare information we need const { config, internals, logger } = pipeline; const pageModulePromise = ssrEntry.page; - const pageInfo = getPageDataByComponent(internals, pageData.route.component); // Calculate information of the page, like scripts, links and styles const styles = pageData.styles @@ -209,7 +210,7 @@ async function generatePage( .reduce(mergeInlineCss, []); // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc. const linkIds: [] = []; - const scripts = pageInfo?.hoistedScript ?? null; + const scripts = pageData.hoistedScript ?? null; if (!pageModulePromise) { throw new Error( `Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.` diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index a2c74271f496..24389353ba07 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -3,13 +3,8 @@ import type { RouteData, SSRResult } from '../../@types/astro.js'; import type { PageOptions } from '../../vite-plugin-astro/types.js'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; -import { - ASTRO_PAGE_RESOLVED_MODULE_ID, - getVirtualModulePageIdFromPath, -} from './plugins/plugin-pages.js'; -import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js'; -import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; -import type { AllPagesData, PageBuildData, StylesheetAsset, ViteID } from './types.js'; +import type { PageBuildData, StylesheetAsset, ViteID } from './types.js'; +import { makePageDataKey } from './plugins/util.js'; export interface BuildInternals { /** @@ -45,7 +40,7 @@ export interface BuildInternals { /** * A map for page-specific information. */ - pagesByComponent: Map ; + pagesByKeys: Map ; /** * A map for page-specific output. @@ -134,7 +129,7 @@ export function createBuildInternals(): BuildInternals { inlinedScripts: new Map(), entrySpecifierToBundleMap: new Map (), pageToBundleMap: new Map (), - pagesByComponent: new Map(), + pagesByKeys: new Map(), pageOptionsByPage: new Map(), pagesByViteID: new Map(), pagesByClientOnly: new Map(), @@ -161,7 +156,7 @@ export function trackPageData( componentURL: URL ): void { pageData.moduleSpecifier = componentModuleId; - internals.pagesByComponent.set(component, pageData); + internals.pagesByKeys.set(pageData.key, pageData); internals.pagesByViteID.set(viteID(componentURL), pageData); } @@ -229,16 +224,77 @@ export function* getPageDatasByClientOnlyID( } } -export function getPageDataByComponent( +/** + * From its route and component, get the page data from the build internals. + * @param internals Build Internals with all the pages + * @param route The route of the page, used to identify the page + * @param component The component of the page, used to identify the page + */ +export function getPageData( internals: BuildInternals, + route: string, component: string ): PageBuildData | undefined { - if (internals.pagesByComponent.has(component)) { - return internals.pagesByComponent.get(component); - } + let pageData = internals.pagesByKeys.get(makePageDataKey(route, component)); + if (pageData) { return pageData;} return undefined; } +/** + * Get all pages datas from the build internals, using a specific component. + * @param internals Build Internals with all the pages + * @param component path to the component, used to identify related pages + */ +export function getPagesDatasByComponent( + internals: BuildInternals, + component: string +): PageBuildData[] { + const pageDatas: PageBuildData[] = []; + internals.pagesByKeys.forEach((pageData) => { + if (component === pageData.component) pageDatas.push(pageData); + }) + return pageDatas; +} + +// TODO: Should be removed in the future. (Astro 5?) +/** + * Map internals.pagesByKeys to a new map with the public key instead of the internal key. + * This function is only used to avoid breaking changes in the Integrations API, after we changed the way + * we identify pages, from the entrypoint component to an internal key. + * If the page component is unique -> the public key is the component path. (old behavior) + * If the page component is shared -> the public key is the internal key. (new behavior) + * The new behavior on shared entrypoint it's not a breaking change, because it was not supported before. + * @param pagesByKeys A map of all page data by their internal key + */ +export function getPageDatasWithPublicKey(pagesByKeys: Map ): Map { + // Create a map to store the pages with the public key, mimicking internal.pagesByKeys + const pagesWithPublicKey = new Map (); + + const pagesByComponentsArray = Array.from(pagesByKeys.values()).map((pageData) => { + return { component: pageData.component, pageData: pageData }; + }); + + // Get pages with unique component, and set the public key to the component. + const pagesWithUniqueComponent = pagesByComponentsArray.filter((page) => { + return pagesByComponentsArray.filter((p) => p.component === page.component).length === 1; + }); + + pagesWithUniqueComponent.forEach((page) => { + pagesWithPublicKey.set(page.component, page.pageData); + }); + + // Get pages with shared component, and set the public key to the internal key. + const pagesWithSharedComponent = pagesByComponentsArray.filter((page) => { + return pagesByComponentsArray.filter((p) => p.component === page.component).length > 1; + }); + + pagesWithSharedComponent.forEach((page) => { + pagesWithPublicKey.set(page.pageData.key, page.pageData); + }); + + return pagesWithPublicKey; +} + export function getPageDataByViteID( internals: BuildInternals, viteid: ViteID @@ -253,44 +309,8 @@ export function hasPageDataByViteID(internals: BuildInternals, viteid: ViteID): return internals.pagesByViteID.has(viteid); } -export function* eachPageData(internals: BuildInternals) { - yield* internals.pagesByComponent.values(); -} - -export function* eachPageFromAllPages(allPages: AllPagesData): Generator<[string, PageBuildData]> { - for (const [path, pageData] of Object.entries(allPages)) { - yield [path, pageData]; - } -} - -export function* eachPageDataFromEntryPoint( - internals: BuildInternals -): Generator<[PageBuildData, string]> { - for (const [entrypoint, filePath] of internals.entrySpecifierToBundleMap) { - // virtual pages can be emitted with different prefixes: - // - the classic way are pages emitted with prefix ASTRO_PAGE_RESOLVED_MODULE_ID -> plugin-pages - // - pages emitted using `build.split`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID - if ( - entrypoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) || - entrypoint.includes(RESOLVED_SPLIT_MODULE_ID) - ) { - const [, pageName] = entrypoint.split(':'); - const pageData = internals.pagesByComponent.get( - `${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}` - ); - if (!pageData) { - throw new Error( - "Build failed. Astro couldn't find the emitted page from " + pageName + ' pattern' - ); - } - - yield [pageData, filePath]; - } - } -} - export function hasPrerenderedPages(internals: BuildInternals) { - for (const pageData of eachPageData(internals)) { + for (const pageData of internals.pagesByKeys.values()) { if (pageData.route.prerender) { return true; } @@ -350,27 +370,23 @@ export function mergeInlineCss( return acc; } -export function isHoistedScript(internals: BuildInternals, id: string): boolean { - return internals.hoistedScriptIdToPagesMap.has(id); -} - -export function* getPageDatasByHoistedScriptId( +/** + * Get all pages data from the build internals, using a specific hoisted script id. + * @param internals Build Internals with all the pages + * @param id Hoisted script id, used to identify the pages using it + */ +export function getPageDatasByHoistedScriptId( internals: BuildInternals, id: string -): Generator { +): PageBuildData[]{ const set = internals.hoistedScriptIdToPagesMap.get(id); + const pageDatas: PageBuildData[] = []; if (set) { for (const pageId of set) { - const pageData = getPageDataByComponent(internals, pageId.slice(1)); - if (pageData) { - yield pageData; - } + getPagesDatasByComponent(internals, pageId.slice(1)).forEach((pageData) => { + pageDatas.push(pageData); + }); } } -} - -// From a component path such as pages/index.astro find the entrypoint module -export function getEntryFilePathFromComponentPath(internals: BuildInternals, path: string) { - const id = getVirtualModulePageIdFromPath(path); - return internals.entrySpecifierToBundleMap.get(id); + return pageDatas; } diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index a151bae2c6f3..6358a6f55364 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -4,6 +4,7 @@ import type { AllPagesData } from './types.js'; import * as colors from 'kleur/colors'; import { debug } from '../logger/core.js'; +import { makePageDataKey } from './plugins/util.js'; export interface CollectPagesDataOptions { settings: AstroSettings; @@ -35,6 +36,8 @@ export async function collectPagesData( // and is then cached across all future SSR builds. In the past, we've had trouble // with parallelized builds without guaranteeing that this is called first. for (const route of manifest.routes) { + // Generate a unique key to identify each page in the build process. + const key = makePageDataKey(route.route, route.component); // static route: if (route.pathname) { const routeCollectionLogTimeout = setInterval(() => { @@ -47,8 +50,8 @@ export async function collectPagesData( clearInterval(routeCollectionLogTimeout); }, 10000); builtPaths.add(route.pathname); - - allPages[route.component] = { + allPages[key] = { + key: key, component: route.component, route, moduleSpecifier: '', @@ -70,8 +73,8 @@ export async function collectPagesData( continue; } // dynamic route: - - allPages[route.component] = { + allPages[key] = { + key: key, component: route.component, route, moduleSpecifier: '', diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index daae6940e046..cfedc1f2d916 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -20,20 +20,11 @@ import { } from '../render/ssr-element.js'; import { isServerLikeOutput } from '../util.js'; import { getOutDirWithinCwd } from './common.js'; -import { - type BuildInternals, - cssOrder, - getEntryFilePathFromComponentPath, - getPageDataByComponent, - mergeInlineCss, -} from './internal.js'; +import { type BuildInternals, cssOrder, getPageData, mergeInlineCss } from './internal.js'; import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js'; -import { - ASTRO_PAGE_EXTENSION_POST_PATTERN, - getVirtualModulePageNameFromPath, -} from './plugins/util.js'; import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; +import { getPagesFromVirtualModulePageName, getVirtualModulePageName } from './plugins/util.js'; import { i18nHasFallback } from './util.js'; /** @@ -163,7 +154,7 @@ export class BuildPipeline extends Pipeline { settings, } = this; const links = new Set (); - const pageBuildData = getPageDataByComponent(internals, routeData.component); + const pageBuildData = getPageData(internals, routeData.route, routeData.component); const scripts = createModuleScriptsSet( pageBuildData?.hoistedScript ? [pageBuildData.hoistedScript] : [], base, @@ -203,37 +194,47 @@ export class BuildPipeline extends Pipeline { /** * It collects the routes to generate during the build. - * * It returns a map of page information and their relative entry point as a string. */ retrieveRoutesToGenerate(): Map { const pages = new Map (); - for (const [entrypoint, filePath] of this.internals.entrySpecifierToBundleMap) { + for (const [virtualModulePageName, filePath] of this.internals.entrySpecifierToBundleMap) { // virtual pages can be emitted with different prefixes: // - the classic way are pages emitted with prefix ASTRO_PAGE_RESOLVED_MODULE_ID -> plugin-pages - // - pages emitted using `build.split`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID + // - pages emitted using `functionPerRoute`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID if ( - entrypoint.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) || - entrypoint.includes(RESOLVED_SPLIT_MODULE_ID) + virtualModulePageName.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) || + virtualModulePageName.includes(RESOLVED_SPLIT_MODULE_ID) ) { - const [, pageName] = entrypoint.split(':'); - const pageData = this.internals.pagesByComponent.get( - `${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}` - ); - if (!pageData) { - throw new Error( - "Build failed. Astro couldn't find the emitted page from " + pageName + ' pattern' + let pageDatas: PageBuildData[] = []; + if (virtualModulePageName.includes(ASTRO_PAGE_RESOLVED_MODULE_ID)) { + pageDatas.push( + ...getPagesFromVirtualModulePageName( + this.internals, + ASTRO_PAGE_RESOLVED_MODULE_ID, + virtualModulePageName + ) ); } - - pages.set(pageData, filePath); + if (virtualModulePageName.includes(RESOLVED_SPLIT_MODULE_ID)) { + pageDatas.push( + ...getPagesFromVirtualModulePageName( + this.internals, + RESOLVED_SPLIT_MODULE_ID, + virtualModulePageName + ) + ); + } + for (const pageData of pageDatas) { + pages.set(pageData, filePath); + } } } - for (const [path, pageData] of this.internals.pagesByComponent.entries()) { + for (const pageData of this.internals.pagesByKeys.values()) { if (routeIsRedirect(pageData.route)) { - pages.set(pageData, path); + pages.set(pageData, pageData.component); } else if ( routeIsFallback(pageData.route) && (i18nHasFallback(this.config) || @@ -245,7 +246,7 @@ export class BuildPipeline extends Pipeline { // The values of the map are the actual `.mjs` files that are generated during the build // Here, we take the component path and transform it in the virtual module name - const moduleSpecifier = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); + const moduleSpecifier = getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component); // We retrieve the original JS module const filePath = this.internals.entrySpecifierToBundleMap.get(moduleSpecifier); if (filePath) { @@ -330,7 +331,7 @@ export class BuildPipeline extends Pipeline { throw new Error(`Expected a redirect route.`); } if (route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); + const filePath = getEntryFilePath(this.internals, route.redirectRoute); if (filePath) { const url = createEntryURL(filePath, outFolder); const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); @@ -350,7 +351,7 @@ export class BuildPipeline extends Pipeline { throw new Error(`Expected a redirect route.`); } if (route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); + const filePath = getEntryFilePath(this.internals, route.redirectRoute); if (filePath) { const url = createEntryURL(filePath, outFolder); const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); @@ -365,3 +366,11 @@ export class BuildPipeline extends Pipeline { function createEntryURL(filePath: string, outFolder: URL) { return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); } + +/** + * For a given pageData, returns the entry file path—aka a resolved virtual module in our internals' specifiers. + */ +function getEntryFilePath(internals: BuildInternals, pageData: RouteData) { + const id = '\x00' + getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component); + return internals.entrySpecifierToBundleMap.get(id); +} diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index c50951e0b081..c9961560219c 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -14,11 +14,9 @@ import { moduleIsTopLevelPage, } from '../graph.js'; import { - eachPageData, getPageDataByViteID, getPageDatasByClientOnlyID, getPageDatasByHoistedScriptId, - isHoistedScript, } from '../internal.js'; import { extendManualChunks, shouldInlineAsset } from './util.js'; @@ -147,7 +145,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { if (pageData) { appendCSSToPage(pageData, meta, pagesToCss, depth, order); } - } else if (options.target === 'client' && isHoistedScript(internals, pageInfo.id)) { + } else if (options.target === 'client' && internals.hoistedScriptIdToPagesMap.has(pageInfo.id)) { for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) { appendCSSToPage(pageData, meta, pagesToCss, -1, order); } @@ -199,7 +197,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { (chunk) => chunk.type === 'asset' && chunk.name === 'style.css' ); if (cssChunk === undefined) return; - for (const pageData of eachPageData(internals)) { + for (const pageData of internals.pagesByKeys.values()) { const cssToInfoMap = (pagesToCss[pageData.moduleSpecifier] ??= {}); cssToInfoMap[cssChunk.fileName] = { depth: -1, order: -1 }; } @@ -238,14 +236,13 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { let sheetAddedToPage = false; - // Apply `pagesToCss` information to the respective `pageData.styles` - for (const pageData of eachPageData(internals)) { + internals.pagesByKeys.forEach((pageData) => { const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName]; if (orderingInfo !== undefined) { pageData.styles.push({ ...orderingInfo, sheet }); sheetAddedToPage = true; } - } + }) // Apply `moduleIdToPropagatedCss` information to `internals.propagatedStylesMap`. // NOTE: It's pretty much a copy over to `internals.propagatedStylesMap` as it should be diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 5bb6ddab038a..6afd521be449 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -19,6 +19,7 @@ import { getOutFile, getOutFolder } from '../common.js'; import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import type { StaticBuildOptions } from '../types.js'; +import { makePageDataKey } from './util.js'; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, 'g'); @@ -189,7 +190,7 @@ function buildManifest( } for (const route of opts.manifest.routes) { - const pageData = internals.pagesByComponent.get(route.component); + const pageData = internals.pagesByKeys.get(makePageDataKey(route.route, route.component)); if (route.prerender || !pageData) continue; const scripts: SerializedRouteInfo['scripts'] = []; if (pageData.hoistedScript) { diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index dd488a97d91a..71195a2e2996 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -1,20 +1,15 @@ import type { Plugin as VitePlugin } from 'vite'; import { routeIsRedirect } from '../../redirects/index.js'; import { addRollupInput } from '../add-rollup-input.js'; -import { type BuildInternals, eachPageFromAllPages } from '../internal.js'; +import { type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import type { StaticBuildOptions } from '../types.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; -import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js'; +import { getPagesFromVirtualModulePageName, getVirtualModulePageName } from './util.js'; export const ASTRO_PAGE_MODULE_ID = '@astro-page:'; export const ASTRO_PAGE_RESOLVED_MODULE_ID = '\0' + ASTRO_PAGE_MODULE_ID; -export function getVirtualModulePageIdFromPath(path: string) { - const name = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); - return '\x00' + name; -} - function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { name: '@astro/plugin-build-pages', @@ -22,11 +17,13 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (opts.settings.config.output === 'static') { const inputs = new Set (); - for (const [path, pageData] of eachPageFromAllPages(opts.allPages)) { + for (const pageData of Object.values(opts.allPages)) { if (routeIsRedirect(pageData.route)) { continue; } - inputs.add(getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path)); + inputs.add( + getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component) + ); } return addRollupInput(options, Array.from(inputs)); @@ -41,9 +38,8 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (id.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { const imports: string[] = []; const exports: string[] = []; - const pageName = getPathFromVirtualModulePageName(ASTRO_PAGE_RESOLVED_MODULE_ID, id); - const pageData = internals.pagesByComponent.get(pageName); - if (pageData) { + const pageDatas = getPagesFromVirtualModulePageName(internals, ASTRO_PAGE_RESOLVED_MODULE_ID, id); + for (const pageData of pageDatas) { const resolvedPage = await this.resolve(pageData.moduleSpecifier); if (resolvedPage) { imports.push(`const page = () => import(${JSON.stringify(pageData.moduleSpecifier)});`); diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 07cba1d57cd2..97c7ea1cbe3b 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -7,14 +7,13 @@ import { routeIsRedirect } from '../../redirects/index.js'; import { isServerLikeOutput } from '../../util.js'; import { addRollupInput } from '../add-rollup-input.js'; import type { BuildInternals } from '../internal.js'; -import { eachPageFromAllPages } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import type { StaticBuildOptions } from '../types.js'; import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; -import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js'; +import { getComponentFromVirtualModulePageName, getVirtualModulePageName } from './util.js'; export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID; @@ -44,18 +43,21 @@ function vitePluginSSR( let i = 0; const pageMap: string[] = []; - for (const [path, pageData] of eachPageFromAllPages(allPages)) { + for (const pageData of Object.values(allPages)) { if (routeIsRedirect(pageData.route)) { continue; } - const virtualModuleName = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); + const virtualModuleName = getVirtualModulePageName( + ASTRO_PAGE_MODULE_ID, + pageData.component + ); let module = await this.resolve(virtualModuleName); if (module) { const variable = `_page${i}`; // we need to use the non-resolved ID in order to resolve correctly the virtual module imports.push(`const ${variable} = () => import("${virtualModuleName}");`); - const pageData2 = internals.pagesByComponent.get(path); + const pageData2 = internals.pagesByKeys.get(pageData.key); if (pageData2) { pageMap.push(`[${JSON.stringify(pageData2.component)}, ${variable}]`); } @@ -147,11 +149,13 @@ function vitePluginSSRSplit( if (functionPerRouteEnabled) { const inputs = new Set (); - for (const [path, pageData] of eachPageFromAllPages(options.allPages)) { + for (const pageData of Object.values(options.allPages)) { if (routeIsRedirect(pageData.route)) { continue; } - inputs.add(getVirtualModulePageNameFromPath(SPLIT_MODULE_ID, path)); + inputs.add( + getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component) + ); } return addRollupInput(opts, Array.from(inputs)); @@ -167,9 +171,8 @@ function vitePluginSSRSplit( const imports: string[] = []; const contents: string[] = []; const exports: string[] = []; - - const path = getPathFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, id); - const virtualModuleName = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); + const componentPath = getComponentFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, id); + const virtualModuleName = getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, componentPath); let module = await this.resolve(virtualModuleName); if (module) { // we need to use the non-resolved ID in order to resolve correctly the virtual module @@ -284,7 +287,7 @@ if (_start in serverEntrypointModule) { * we can't use `writeBundle` hook to get the final file name of the entry point written on disk. * We use this hook instead. * - * We retrieve the {@link RouteData} that belongs the current moduleKey + * We retrieve all the {@link RouteData} that have the same component as the one we are processing. */ function storeEntryPoint( moduleKey: string, @@ -292,9 +295,9 @@ function storeEntryPoint( internals: BuildInternals, fileName: string ) { - const componentPath = getPathFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, moduleKey); - for (const [page, pageData] of eachPageFromAllPages(options.allPages)) { - if (componentPath == page) { + const componentPath = getComponentFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, moduleKey); + for (const pageData of Object.values(options.allPages)) { + if (componentPath == pageData.component) { const publicPath = fileURLToPath(options.settings.config.build.server); internals.entryPoints.set(pageData.route, pathToFileURL(join(publicPath, fileName))); } diff --git a/packages/astro/src/core/build/plugins/util.ts b/packages/astro/src/core/build/plugins/util.ts index d1bd266cd4ab..c1552599d90e 100644 --- a/packages/astro/src/core/build/plugins/util.ts +++ b/packages/astro/src/core/build/plugins/util.ts @@ -1,5 +1,7 @@ import { extname } from 'node:path'; import type { BuildOptions, Rollup, Plugin as VitePlugin } from 'vite'; +import type { BuildInternals } from '../internal.js'; +import type { PageBuildData } from '../types.js'; // eslint-disable-next-line @typescript-eslint/ban-types type OutputOptionsHook = Extract ; @@ -40,19 +42,28 @@ export function extendManualChunks(outputOptions: OutputOptions, hooks: ExtendMa }; } -// This is an arbitrary string that we are going to replace the dot of the extension +// This is an arbitrary string that we use to replace the dot of the extension. export const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; +// This is an arbitrary string that we use to make a pageData key +// Has to be a invalid character for a route, to avoid conflicts. +export const ASTRO_PAGE_KEY_SEPARATOR = '&'; + +/** + * Generate a unique key to identify each page in the build process. + * @param route Usually pageData.route.route + * @param componentPath Usually pageData.component + */ +export function makePageDataKey(route: string, componentPath: string): string { + return route + ASTRO_PAGE_KEY_SEPARATOR + componentPath; +} /** * Prevents Rollup from triggering other plugins in the process by masking the extension (hence the virtual file). - * - * 1. We add a fixed prefix, which is used as virtual module naming convention - * 2. If the path has an extension (at the end of the path), we replace the dot that belongs to the extension with an arbitrary string. - * - * @param virtualModulePrefix - * @param path + * Inverse function of getComponentFromVirtualModulePageName() below. + * @param virtualModulePrefix The prefix used to create the virtual module + * @param path Page component path */ -export function getVirtualModulePageNameFromPath(virtualModulePrefix: string, path: string) { +export function getVirtualModulePageName(virtualModulePrefix: string, path: string): string { const extension = extname(path); return ( virtualModulePrefix + @@ -63,13 +74,34 @@ export function getVirtualModulePageNameFromPath(virtualModulePrefix: string, pa } /** - * - * @param virtualModulePrefix - * @param id + * From the VirtualModulePageName, and the internals, get all pageDatas that use this + * component as their entry point. + * @param virtualModulePrefix The prefix used to create the virtual module + * @param id Virtual module name + */ +export function getPagesFromVirtualModulePageName(internals: BuildInternals, virtualModulePrefix: string, id: string): PageBuildData[] +{ + const path = getComponentFromVirtualModulePageName(virtualModulePrefix, id); + + const pages: PageBuildData[] = []; + internals.pagesByKeys.forEach(pageData => { + if (pageData.component === path) { + pages.push(pageData); + } + }); + + return pages; +} + +/** + * From the VirtualModulePageName, get the component path. + * Remember that the component can be use by multiple routes. + * Inverse function of getVirtualModulePageName() above. + * @param virtualModulePrefix The prefix at the beginning of the virtual module + * @param id Virtual module name */ -export function getPathFromVirtualModulePageName(virtualModulePrefix: string, id: string) { - const pageName = id.slice(virtualModulePrefix.length); - return pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); +export function getComponentFromVirtualModulePageName(virtualModulePrefix: string, id: string): string { + return id.slice(virtualModulePrefix.length).replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); } export function shouldInlineAsset( diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index e2acc4ea56ca..a73835f68186 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -9,11 +9,7 @@ import * as vite from 'vite'; import type { RouteData } from '../../@types/astro.js'; import { PROPAGATED_ASSET_FLAG } from '../../content/consts.js'; import { hasAnyContentFlag } from '../../content/utils.js'; -import { - type BuildInternals, - createBuildInternals, - eachPageData, -} from '../../core/build/internal.js'; +import { type BuildInternals, createBuildInternals, getPageDatasWithPublicKey } from '../../core/build/internal.js'; import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js'; import { appendForwardSlash, prependForwardSlash, removeFileExtension } from '../../core/path.js'; import { isModeServerWithNoAdapter, isServerLikeOutput } from '../../core/util.js'; @@ -39,7 +35,6 @@ import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from './util. export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; - // Make sure we have an adapter before building if (isModeServerWithNoAdapter(opts.settings)) { throw new AstroError(AstroErrorData.NoAdapterInstalled); @@ -48,17 +43,18 @@ export async function viteBuild(opts: StaticBuildOptions) { settings.timer.start('SSR build'); // The pages to be built for rendering purposes. + // (comment above may be outdated ?) const pageInput = new Set (); // Build internals needed by the CSS plugin const internals = createBuildInternals(); - for (const [component, pageData] of Object.entries(allPages)) { - const astroModuleURL = new URL('./' + component, settings.config.root); - const astroModuleId = prependForwardSlash(component); + for (const pageData of Object.values(allPages)) { + const astroModuleURL = new URL('./' + pageData.component, settings.config.root); + const astroModuleId = prependForwardSlash(pageData.component); // Track the page data in internals - trackPageData(internals, component, pageData, astroModuleId, astroModuleURL); + trackPageData(internals, pageData.component, pageData, astroModuleId, astroModuleURL); if (!routeIsRedirect(pageData.route)) { pageInput.add(astroModuleId); @@ -75,7 +71,6 @@ export async function viteBuild(opts: StaticBuildOptions) { // Register plugins const container = createPluginContainer(opts, internals); registerAllPlugins(container); - // Build your project (SSR application code, assets, client JS, etc.) const ssrTime = performance.now(); opts.logger.info('build', `Building ${settings.config.output} entrypoints...`); @@ -275,7 +270,7 @@ async function ssrBuild( const updatedViteBuildConfig = await runHookBuildSetup({ config: settings.config, - pages: internals.pagesByComponent, + pages: getPageDatasWithPublicKey(internals.pagesByKeys), vite: viteBuildConfig, target: 'server', logger: opts.logger, @@ -336,7 +331,7 @@ async function clientBuild( await runHookBuildSetup({ config: settings.config, - pages: internals.pagesByComponent, + pages: getPageDatasWithPublicKey(internals.pagesByKeys), vite: viteBuildConfig, target: 'client', logger: opts.logger, @@ -370,19 +365,26 @@ async function runPostBuildHooks( /** * For each statically prerendered page, replace their SSR file with a noop. * This allows us to run the SSR build only once, but still remove dependencies for statically rendered routes. + * If a component is shared between a statically rendered route and a SSR route, it will still be included in the SSR build. */ async function cleanStaticOutput( opts: StaticBuildOptions, internals: BuildInternals, ssrOutputChunkNames: string[] ) { - const allStaticFiles = new Set(); - for (const pageData of eachPageData(internals)) { - if (pageData.route.prerender && !pageData.hasSharedModules) { - const { moduleSpecifier } = pageData; - const pageBundleId = internals.pageToBundleMap.get(moduleSpecifier); - const entryBundleId = internals.entrySpecifierToBundleMap.get(moduleSpecifier); - allStaticFiles.add(pageBundleId ?? entryBundleId); + const prerenderedFiles = new Set(); + const onDemandsFiles = new Set(); + for (const pageData of internals.pagesByKeys.values()) { + const { moduleSpecifier } = pageData; + const bundleId = internals.pageToBundleMap.get(moduleSpecifier) ?? internals.entrySpecifierToBundleMap.get(moduleSpecifier); + if (pageData.route.prerender && !pageData.hasSharedModules && !onDemandsFiles.has(bundleId)) { + prerenderedFiles.add(bundleId); + } else { + onDemandsFiles.add(bundleId); + // Check if the component was not previously added to the static build by a statically rendered route + if (prerenderedFiles.has(bundleId)) { + prerenderedFiles.delete(bundleId); + } } } const ssr = isServerLikeOutput(opts.settings.config); @@ -400,7 +402,7 @@ async function cleanStaticOutput( // These chunks should only contain prerendering logic, so they are safe to modify. await Promise.all( files.map(async (filename) => { - if (!allStaticFiles.has(filename)) { + if (!prerenderedFiles.has(filename)) { return; } const url = new URL(filename, out); diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 4b502c353c5a..53c6dcb93350 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -23,6 +23,7 @@ export type StylesheetAsset = export type HoistedScriptAsset = { type: 'inline' | 'external'; value: string }; export interface PageBuildData { + key: string; component: ComponentPath; route: RouteData; moduleSpecifier: string; diff --git a/packages/astro/test/fixtures/reuse-injected-entrypoint/astro.config.mjs b/packages/astro/test/fixtures/reuse-injected-entrypoint/astro.config.mjs new file mode 100644 index 000000000000..266e31c07f93 --- /dev/null +++ b/packages/astro/test/fixtures/reuse-injected-entrypoint/astro.config.mjs @@ -0,0 +1,34 @@ +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + { + name: 'astropi', + hooks: { + 'astro:config:setup': async ({ injectRoute }) => { + injectRoute({ + pattern: `/injected-a`, + entrypoint: './src/to-inject.astro', + prerender: true, + }); + injectRoute({ + pattern: `/injected-b`, + entrypoint: './src/to-inject.astro', + prerender: true, + }); + injectRoute({ + pattern: `/dynamic-a/[id]`, + entrypoint: './src/[id].astro', + prerender: true, + }); + injectRoute({ + pattern: `/dynamic-b/[id]`, + entrypoint: './src/[id].astro', + prerender: true, + }); + }, + }, + }, + ], +}); \ No newline at end of file diff --git a/packages/astro/test/fixtures/reuse-injected-entrypoint/package.json b/packages/astro/test/fixtures/reuse-injected-entrypoint/package.json new file mode 100644 index 000000000000..c0ca107ccc41 --- /dev/null +++ b/packages/astro/test/fixtures/reuse-injected-entrypoint/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/reuse-injected-entrypoint", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/reuse-injected-entrypoint/src/[id].astro b/packages/astro/test/fixtures/reuse-injected-entrypoint/src/[id].astro new file mode 100644 index 000000000000..151fcfc5e555 --- /dev/null +++ b/packages/astro/test/fixtures/reuse-injected-entrypoint/src/[id].astro @@ -0,0 +1,20 @@ +--- +export async function getStaticPaths() { + return [ + { params: { id: 'id-1' } }, + { params: { id: 'id-2' } } + ]; +} +const { id } = Astro.params; +--- + + + + + Routing + + +[id].astro
+{id}
+ + \ No newline at end of file diff --git a/packages/astro/test/fixtures/reuse-injected-entrypoint/src/pages/index.astro b/packages/astro/test/fixtures/reuse-injected-entrypoint/src/pages/index.astro new file mode 100644 index 000000000000..4c057d51407a --- /dev/null +++ b/packages/astro/test/fixtures/reuse-injected-entrypoint/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +--- + + + + +Routing + + +index.astro
+ + \ No newline at end of file diff --git a/packages/astro/test/fixtures/reuse-injected-entrypoint/src/to-inject.astro b/packages/astro/test/fixtures/reuse-injected-entrypoint/src/to-inject.astro new file mode 100644 index 000000000000..13d5bac25d98 --- /dev/null +++ b/packages/astro/test/fixtures/reuse-injected-entrypoint/src/to-inject.astro @@ -0,0 +1,12 @@ +--- +--- + + + + +Routing + + +to-inject.astro
+ + \ No newline at end of file diff --git a/packages/astro/test/reuse-injected-entrypoint.test.js b/packages/astro/test/reuse-injected-entrypoint.test.js new file mode 100644 index 000000000000..18723f16cec8 --- /dev/null +++ b/packages/astro/test/reuse-injected-entrypoint.test.js @@ -0,0 +1,135 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +const routes = [ + { + description: 'matches / to index.astro', + url: '/', + h1: 'index.astro', + }, + { + description: 'matches /injected-a to to-inject.astro', + url: '/injected-a', + h1: 'to-inject.astro', + }, + { + description: 'matches /injected-b to to-inject.astro', + url: '/injected-b', + h1: 'to-inject.astro', + }, + { + description: 'matches /dynamic-a/id-1 to [id].astro', + url: '/dynamic-a/id-1', + h1: '[id].astro', + p: 'id-1', + }, + { + description: 'matches /dynamic-a/id-2 to [id].astro', + url: '/dynamic-a/id-2', + h1: '[id].astro', + p: 'id-2', + }, + { + description: 'matches /dynamic-b/id-1 to [id].astro', + url: '/dynamic-b/id-1', + h1: '[id].astro', + p: 'id-1', + }, + { + description: 'matches /dynamic-b/id-2 to [id].astro', + url: '/dynamic-b/id-2', + h1: '[id].astro', + p: 'id-2', + }, +]; + +function appendForwardSlash(path) { + return path.endsWith('/') ? path : path + '/'; +} + +describe('Reuse injected entrypoint', () => { + describe('build', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reuse-injected-entrypoint/', + }); + await fixture.build(); + }); + + routes.forEach(({ description, url, fourOhFour, h1, p, htmlMatch }) => { + const isEndpoint = htmlMatch && !h1 && !p; + + it(description, async () => { + const htmlFile = isEndpoint ? url : `${appendForwardSlash(url)}index.html`; + + if (fourOhFour) { + assert.equal(fixture.pathExists(htmlFile), false); + return; + } + + const html = await fixture.readFile(htmlFile); + const $ = cheerioLoad(html); + + if (h1) { + assert.equal($('h1').text(), h1); + } + + if (p) { + assert.equal($('p').text(), p); + } + + if (htmlMatch) { + assert.equal(html, htmlMatch); + } + }); + }); + }); + + describe('dev', () => { + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reuse-injected-entrypoint/', + }); + + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + routes.forEach(({ description, url, fourOhFour, h1, p, htmlMatch }) => { + const isEndpoint = htmlMatch && !h1 && !p; + + // checks URLs as written above + it(description, async () => { + const html = await fixture.fetch(url).then((res) => res.text()); + const $ = cheerioLoad(html); + + if (fourOhFour) { + assert.equal($('title').text(), '404: Not Found'); + return; + } + + if (h1) { + assert.equal($('h1').text(), h1); + } + + if (p) { + assert.equal($('p').text(), p); + } + + if (htmlMatch) { + assert.equal(html, htmlMatch); + } + }); + }); + }); +}); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index e0eb8c2b71d2..bd7e1f9035ec 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -8,7 +8,7 @@ import stripAnsi from 'strip-ansi'; import { check } from '../dist/cli/check/index.js'; import build from '../dist/core/build/index.js'; import { RESOLVED_SPLIT_MODULE_ID } from '../dist/core/build/plugins/plugin-ssr.js'; -import { getVirtualModulePageNameFromPath } from '../dist/core/build/plugins/util.js'; +import { getVirtualModulePageName } from '../dist/core/build/plugins/util.js'; import { makeSplitEntryPointFileName } from '../dist/core/build/static-build.js'; import { mergeConfig, resolveConfig } from '../dist/core/config/index.js'; import { dev, preview } from '../dist/core/index.js'; @@ -221,7 +221,7 @@ export async function loadFixture(inlineConfig) { return app; }, loadEntryPoint: async (pagePath, routes, streaming) => { - const virtualModule = getVirtualModulePageNameFromPath(RESOLVED_SPLIT_MODULE_ID, pagePath); + const virtualModule = getVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, pagePath); const filePath = makeSplitEntryPointFileName(virtualModule, routes); const url = new URL(`./server/${filePath}?id=${fixtureId}`, config.outDir); const { createApp, manifest } = await import(url); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98b0868de4b4..d3bb10a16be2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3336,6 +3336,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/reuse-injected-entrypoint: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/root-srcdir-css: dependencies: astro: @@ -5623,8 +5629,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@antfu/utils@0.7.7: - resolution: {integrity: sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==} + /@antfu/utils@0.7.8: + resolution: {integrity: sha512-rWQkqXRESdjXtc+7NRfK9lASQjpXJu1ayp7qi1d23zZorY+wBHVLHHoVcMsEnkqEBWTFqbztO7/QdJFzyEcLTg==} dev: false /@asamuzakjp/dom-selector@2.0.2: @@ -5830,7 +5836,7 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 + '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 dev: false @@ -5940,6 +5946,13 @@ packages: '@babel/types': 7.24.5 dev: false + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.5 + dev: false + /@babel/helper-split-export-declaration@7.24.5: resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} engines: {node: '>=6.9.0'} @@ -7445,8 +7458,8 @@ packages: resolution: {integrity: sha512-n5JEf16Wr4mdkRMZ8wMP/wN9/sHmTjRPbouXjJH371mZ2LEGDl72t8tEsMRNFerQN/QJtivOxqK1frdGa4QK5Q==} engines: {node: '>=10'} - /@jsonjoy.com/base64@1.1.1(tslib@2.6.2): - resolution: {integrity: sha512-LnFjVChaGY8cZVMwAIMjvA1XwQjZ/zIXHyh28IyJkyNkzof4Dkm1+KN9UIm3lHhREH4vs7XwZ0NpkZKnwOtEfg==} + /@jsonjoy.com/base64@1.1.2(tslib@2.6.2): + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -7457,8 +7470,8 @@ packages: tslib: 2.6.2 dev: true - /@jsonjoy.com/json-pack@1.0.2(tslib@2.6.2): - resolution: {integrity: sha512-4KMApTgb1Hvjz9Ue7unziJ1xNy3k6d2erp0hz1iXryXsf6LEM3KwN6YrfbqT0vqkUO8Tu+CSnvMia9cWX6YGVw==} + /@jsonjoy.com/json-pack@1.0.4(tslib@2.6.2): + resolution: {integrity: sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -7466,15 +7479,15 @@ packages: tslib: optional: true dependencies: - '@jsonjoy.com/base64': 1.1.1(tslib@2.6.2) - '@jsonjoy.com/util': 1.1.0(tslib@2.6.2) + '@jsonjoy.com/base64': 1.1.2(tslib@2.6.2) + '@jsonjoy.com/util': 1.1.3(tslib@2.6.2) hyperdyperid: 1.2.0 thingies: 1.21.0(tslib@2.6.2) tslib: 2.6.2 dev: true - /@jsonjoy.com/util@1.1.0(tslib@2.6.2): - resolution: {integrity: sha512-Yz+xITJ3Y/w0DBISwPkBETP5/cITHXscjgQNZIkfrVz1V7/ahJY8vw+T+LZy/KtXgKuUWqu4GALAQ3bhGt9J8A==} + /@jsonjoy.com/util@1.1.3(tslib@2.6.2): + resolution: {integrity: sha512-g//kkF4kOwUjemValCtOc/xiYzmwMRmWq3Bn+YnzOzuZLHq2PpMOxxIayN3cKbo7Ko2Np65t6D9H81IvXbXhqg==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -7482,7 +7495,6 @@ packages: tslib: optional: true dependencies: - hyperdyperid: 1.2.0 tslib: 2.6.2 dev: true @@ -9008,6 +9020,16 @@ packages: estree-walker: 2.0.2 source-map-js: 1.2.0 + /@vue/compiler-core@3.4.27: + resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} + dependencies: + '@babel/parser': 7.24.5 + '@vue/shared': 3.4.27 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 + dev: false + /@vue/compiler-dom@3.4.21: resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} dependencies: @@ -9028,6 +9050,13 @@ packages: '@vue/compiler-core': 3.4.26 '@vue/shared': 3.4.26 + /@vue/compiler-dom@3.4.27: + resolution: {integrity: sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==} + dependencies: + '@vue/compiler-core': 3.4.27 + '@vue/shared': 3.4.27 + dev: false + /@vue/compiler-sfc@3.4.21: resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==} dependencies: @@ -9042,32 +9071,32 @@ packages: source-map-js: 1.2.0 dev: false - /@vue/compiler-sfc@3.4.24: - resolution: {integrity: sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==} + /@vue/compiler-sfc@3.4.26: + resolution: {integrity: sha512-It1dp+FAOCgluYSVYlDn5DtZBxk1NCiJJfu2mlQqa/b+k8GL6NG/3/zRbJnHdhV2VhxFghaDq5L4K+1dakW6cw==} dependencies: '@babel/parser': 7.24.5 - '@vue/compiler-core': 3.4.24 - '@vue/compiler-dom': 3.4.24 - '@vue/compiler-ssr': 3.4.24 - '@vue/shared': 3.4.24 + '@vue/compiler-core': 3.4.26 + '@vue/compiler-dom': 3.4.26 + '@vue/compiler-ssr': 3.4.26 + '@vue/shared': 3.4.26 estree-walker: 2.0.2 magic-string: 0.30.10 postcss: 8.4.38 source-map-js: 1.2.0 - dev: false - /@vue/compiler-sfc@3.4.26: - resolution: {integrity: sha512-It1dp+FAOCgluYSVYlDn5DtZBxk1NCiJJfu2mlQqa/b+k8GL6NG/3/zRbJnHdhV2VhxFghaDq5L4K+1dakW6cw==} + /@vue/compiler-sfc@3.4.27: + resolution: {integrity: sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==} dependencies: '@babel/parser': 7.24.5 - '@vue/compiler-core': 3.4.26 - '@vue/compiler-dom': 3.4.26 - '@vue/compiler-ssr': 3.4.26 - '@vue/shared': 3.4.26 + '@vue/compiler-core': 3.4.27 + '@vue/compiler-dom': 3.4.27 + '@vue/compiler-ssr': 3.4.27 + '@vue/shared': 3.4.27 estree-walker: 2.0.2 magic-string: 0.30.10 postcss: 8.4.38 source-map-js: 1.2.0 + dev: false /@vue/compiler-ssr@3.4.21: resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} @@ -9076,19 +9105,19 @@ packages: '@vue/shared': 3.4.21 dev: false - /@vue/compiler-ssr@3.4.24: - resolution: {integrity: sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==} - dependencies: - '@vue/compiler-dom': 3.4.24 - '@vue/shared': 3.4.24 - dev: false - /@vue/compiler-ssr@3.4.26: resolution: {integrity: sha512-FNwLfk7LlEPRY/g+nw2VqiDKcnDTVdCfBREekF8X74cPLiWHUX6oldktf/Vx28yh4STNy7t+/yuLoMBBF7YDiQ==} dependencies: '@vue/compiler-dom': 3.4.26 '@vue/shared': 3.4.26 + /@vue/compiler-ssr@3.4.27: + resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} + dependencies: + '@vue/compiler-dom': 3.4.27 + '@vue/shared': 3.4.27 + dev: false + /@vue/devtools-core@7.1.3(vite@5.2.10)(vue@3.4.26): resolution: {integrity: sha512-pVbWi8pf2Z/fZPioYOIgu+cv9pQG55k4D8bL31ec+Wfe+pQR0ImFDu0OhHfch1Ra8uvLLrAZTF4IKeGAkmzD4A==} dependencies: @@ -9201,6 +9230,10 @@ packages: /@vue/shared@3.4.26: resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==} + /@vue/shared@3.4.27: + resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} + dev: false + /@webcomponents/template-shadowroot@0.2.1: resolution: {integrity: sha512-fXL/vIUakyZL62hyvUh+EMwbVoTc0hksublmRz6ai6et8znHkJa6gtqMUZo1oc7dIz46exHSIImml9QTdknMHg==} dev: false @@ -13014,9 +13047,9 @@ packages: resolution: {integrity: sha512-36cVYFMaa9HNEYyvkyKCwker8DBmOdjWLrfekE/cHEKJ806fCfKNVhOJNvoyV/CrGSZDtfQPbhn0Zid0gbH0Hw==} engines: {node: '>= 4.0.0'} dependencies: - '@jsonjoy.com/json-pack': 1.0.2(tslib@2.6.2) - '@jsonjoy.com/util': 1.1.0(tslib@2.6.2) - sonic-forest: 1.0.0(tslib@2.6.2) + '@jsonjoy.com/json-pack': 1.0.4(tslib@2.6.2) + '@jsonjoy.com/util': 1.1.3(tslib@2.6.2) + sonic-forest: 1.0.3(tslib@2.6.2) tslib: 2.6.2 dev: true @@ -15608,8 +15641,8 @@ packages: solid-js: 1.8.17 dev: false - /sonic-forest@1.0.0(tslib@2.6.2): - resolution: {integrity: sha512-yFO2N4uTUFtgKLw03WWFpN1iEwZySweMsa18XN3Kt0yYrlmVHunC2ZgM+437zDoKISAJHcH3Cg18U7d6tuSgSQ==} + /sonic-forest@1.0.3(tslib@2.6.2): + resolution: {integrity: sha512-dtwajos6IWMEWXdEbW1IkEkyL2gztCAgDplRIX+OT5aRKnEd5e7r7YCxRgXZdhRP1FBdOBf8axeTPhzDv8T4wQ==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -15617,6 +15650,7 @@ packages: tslib: optional: true dependencies: + tree-dump: 1.0.1(tslib@2.6.2) tslib: 2.6.2 dev: true @@ -16164,6 +16198,18 @@ packages: punycode: 2.3.1 dev: true + /tree-dump@1.0.1(tslib@2.6.2): + resolution: {integrity: sha512-WCkcRBVPSlHHq1dc/px9iOfqklvzCbdRwvlNfxGZsrHqf6aZttfPrd7DJTt6oR10dwUfpFFQeVTkPbBIZxX/YA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + peerDependenciesMeta: + tslib: + optional: true + dependencies: + tslib: 2.6.2 + dev: true + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -16733,7 +16779,7 @@ packages: vite: optional: true dependencies: - '@antfu/utils': 0.7.7 + '@antfu/utils': 0.7.8 '@rollup/pluginutils': 5.1.0 debug: 4.3.4(supports-color@8.1.1) error-stack-parser-es: 0.1.1 @@ -16809,7 +16855,7 @@ packages: '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) '@babel/plugin-transform-typescript': 7.24.4(@babel/core@7.24.5) '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.24.5) - '@vue/compiler-dom': 3.4.26 + '@vue/compiler-dom': 3.4.24 kolorist: 1.8.0 magic-string: 0.30.10 vite: 5.2.10(@types/node@18.19.31)(sass@1.75.0) @@ -16825,7 +16871,7 @@ packages: vue: optional: true dependencies: - '@vue/compiler-sfc': 3.4.24 + '@vue/compiler-sfc': 3.4.27 svgo: 3.2.0 dev: false From 6382d7d238311d5cd2ef2ccf79d209149c5df2f9 Mon Sep 17 00:00:00 2001 From: Goulven CLEC'HDate: Wed, 8 May 2024 10:34:18 +0000 Subject: [PATCH 062/151] [ci] format --- packages/astro/src/core/build/generate.ts | 4 ++-- packages/astro/src/core/build/internal.ts | 22 +++++++++++-------- packages/astro/src/core/build/pipeline.ts | 2 +- .../src/core/build/plugins/plugin-css.ts | 7 ++++-- .../src/core/build/plugins/plugin-pages.ts | 12 +++++----- .../src/core/build/plugins/plugin-ssr.ts | 4 +--- packages/astro/src/core/build/plugins/util.ts | 16 +++++++++----- packages/astro/src/core/build/static-build.ts | 10 +++++++-- 8 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index dbd1e915df96..1963962ff2b6 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -44,6 +44,8 @@ import { getOutputFilename, isServerLikeOutput } from '../util.js'; import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; import { cssOrder, mergeInlineCss } from './internal.js'; import { BuildPipeline } from './pipeline.js'; +import { ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages.js'; +import { getVirtualModulePageName } from './plugins/util.js'; import type { PageBuildData, SinglePageBuiltModule, @@ -51,8 +53,6 @@ import type { StylesheetAsset, } from './types.js'; import { getTimeStat, shouldAppendForwardSlash } from './util.js'; -import { getVirtualModulePageName } from './plugins/util.js'; -import { ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages.js'; function createEntryURL(filePath: string, outFolder: URL) { return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 24389353ba07..2c593fa8cf1a 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -3,8 +3,8 @@ import type { RouteData, SSRResult } from '../../@types/astro.js'; import type { PageOptions } from '../../vite-plugin-astro/types.js'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; -import type { PageBuildData, StylesheetAsset, ViteID } from './types.js'; import { makePageDataKey } from './plugins/util.js'; +import type { PageBuildData, StylesheetAsset, ViteID } from './types.js'; export interface BuildInternals { /** @@ -232,11 +232,13 @@ export function* getPageDatasByClientOnlyID( */ export function getPageData( internals: BuildInternals, - route: string, + route: string, component: string ): PageBuildData | undefined { let pageData = internals.pagesByKeys.get(makePageDataKey(route, component)); - if (pageData) { return pageData;} + if (pageData) { + return pageData; + } return undefined; } @@ -250,23 +252,25 @@ export function getPagesDatasByComponent( component: string ): PageBuildData[] { const pageDatas: PageBuildData[] = []; - internals.pagesByKeys.forEach((pageData) => { - if (component === pageData.component) pageDatas.push(pageData); - }) + internals.pagesByKeys.forEach((pageData) => { + if (component === pageData.component) pageDatas.push(pageData); + }); return pageDatas; } // TODO: Should be removed in the future. (Astro 5?) /** * Map internals.pagesByKeys to a new map with the public key instead of the internal key. - * This function is only used to avoid breaking changes in the Integrations API, after we changed the way + * This function is only used to avoid breaking changes in the Integrations API, after we changed the way * we identify pages, from the entrypoint component to an internal key. * If the page component is unique -> the public key is the component path. (old behavior) * If the page component is shared -> the public key is the internal key. (new behavior) * The new behavior on shared entrypoint it's not a breaking change, because it was not supported before. * @param pagesByKeys A map of all page data by their internal key */ -export function getPageDatasWithPublicKey(pagesByKeys: Map ): Map { +export function getPageDatasWithPublicKey( + pagesByKeys: Map +): Map { // Create a map to store the pages with the public key, mimicking internal.pagesByKeys const pagesWithPublicKey = new Map (); @@ -378,7 +382,7 @@ export function mergeInlineCss( export function getPageDatasByHoistedScriptId( internals: BuildInternals, id: string -): PageBuildData[]{ +): PageBuildData[] { const set = internals.hoistedScriptIdToPagesMap.get(id); const pageDatas: PageBuildData[] = []; if (set) { diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index cfedc1f2d916..7661b6fc4d66 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -23,8 +23,8 @@ import { getOutDirWithinCwd } from './common.js'; import { type BuildInternals, cssOrder, getPageData, mergeInlineCss } from './internal.js'; import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js'; -import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; import { getPagesFromVirtualModulePageName, getVirtualModulePageName } from './plugins/util.js'; +import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; /** diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index c9961560219c..718f81f879d6 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -145,7 +145,10 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { if (pageData) { appendCSSToPage(pageData, meta, pagesToCss, depth, order); } - } else if (options.target === 'client' && internals.hoistedScriptIdToPagesMap.has(pageInfo.id)) { + } else if ( + options.target === 'client' && + internals.hoistedScriptIdToPagesMap.has(pageInfo.id) + ) { for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) { appendCSSToPage(pageData, meta, pagesToCss, -1, order); } @@ -242,7 +245,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { pageData.styles.push({ ...orderingInfo, sheet }); sheetAddedToPage = true; } - }) + }); // Apply `moduleIdToPropagatedCss` information to `internals.propagatedStylesMap`. // NOTE: It's pretty much a copy over to `internals.propagatedStylesMap` as it should be diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 71195a2e2996..9b7faf07c6bb 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -21,9 +21,7 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (routeIsRedirect(pageData.route)) { continue; } - inputs.add( - getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component) - ); + inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component)); } return addRollupInput(options, Array.from(inputs)); @@ -38,8 +36,12 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (id.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { const imports: string[] = []; const exports: string[] = []; - const pageDatas = getPagesFromVirtualModulePageName(internals, ASTRO_PAGE_RESOLVED_MODULE_ID, id); - for (const pageData of pageDatas) { + const pageDatas = getPagesFromVirtualModulePageName( + internals, + ASTRO_PAGE_RESOLVED_MODULE_ID, + id + ); + for (const pageData of pageDatas) { const resolvedPage = await this.resolve(pageData.moduleSpecifier); if (resolvedPage) { imports.push(`const page = () => import(${JSON.stringify(pageData.moduleSpecifier)});`); diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 97c7ea1cbe3b..0a10110cd217 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -153,9 +153,7 @@ function vitePluginSSRSplit( if (routeIsRedirect(pageData.route)) { continue; } - inputs.add( - getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component) - ); + inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component)); } return addRollupInput(opts, Array.from(inputs)); diff --git a/packages/astro/src/core/build/plugins/util.ts b/packages/astro/src/core/build/plugins/util.ts index c1552599d90e..f1e2bf244aeb 100644 --- a/packages/astro/src/core/build/plugins/util.ts +++ b/packages/astro/src/core/build/plugins/util.ts @@ -74,17 +74,20 @@ export function getVirtualModulePageName(virtualModulePrefix: string, path: stri } /** - * From the VirtualModulePageName, and the internals, get all pageDatas that use this + * From the VirtualModulePageName, and the internals, get all pageDatas that use this * component as their entry point. * @param virtualModulePrefix The prefix used to create the virtual module * @param id Virtual module name */ -export function getPagesFromVirtualModulePageName(internals: BuildInternals, virtualModulePrefix: string, id: string): PageBuildData[] -{ +export function getPagesFromVirtualModulePageName( + internals: BuildInternals, + virtualModulePrefix: string, + id: string +): PageBuildData[] { const path = getComponentFromVirtualModulePageName(virtualModulePrefix, id); const pages: PageBuildData[] = []; - internals.pagesByKeys.forEach(pageData => { + internals.pagesByKeys.forEach((pageData) => { if (pageData.component === path) { pages.push(pageData); } @@ -100,7 +103,10 @@ export function getPagesFromVirtualModulePageName(internals: BuildInternals, vir * @param virtualModulePrefix The prefix at the beginning of the virtual module * @param id Virtual module name */ -export function getComponentFromVirtualModulePageName(virtualModulePrefix: string, id: string): string { +export function getComponentFromVirtualModulePageName( + virtualModulePrefix: string, + id: string +): string { return id.slice(virtualModulePrefix.length).replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index a73835f68186..bdd50d3bfbae 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -9,7 +9,11 @@ import * as vite from 'vite'; import type { RouteData } from '../../@types/astro.js'; import { PROPAGATED_ASSET_FLAG } from '../../content/consts.js'; import { hasAnyContentFlag } from '../../content/utils.js'; -import { type BuildInternals, createBuildInternals, getPageDatasWithPublicKey } from '../../core/build/internal.js'; +import { + type BuildInternals, + createBuildInternals, + getPageDatasWithPublicKey, +} from '../../core/build/internal.js'; import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js'; import { appendForwardSlash, prependForwardSlash, removeFileExtension } from '../../core/path.js'; import { isModeServerWithNoAdapter, isServerLikeOutput } from '../../core/util.js'; @@ -376,7 +380,9 @@ async function cleanStaticOutput( const onDemandsFiles = new Set(); for (const pageData of internals.pagesByKeys.values()) { const { moduleSpecifier } = pageData; - const bundleId = internals.pageToBundleMap.get(moduleSpecifier) ?? internals.entrySpecifierToBundleMap.get(moduleSpecifier); + const bundleId = + internals.pageToBundleMap.get(moduleSpecifier) ?? + internals.entrySpecifierToBundleMap.get(moduleSpecifier); if (pageData.route.prerender && !pageData.hasSharedModules && !onDemandsFiles.has(bundleId)) { prerenderedFiles.add(bundleId); } else { From c0c509b6bf3f55562d22297fdcc2b3e57969734d Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Wed, 8 May 2024 07:53:17 -0400 Subject: [PATCH 063/151] Actions experimental release (#10858) * feat: port astro-actions poc * feat: basic blog example * feat: basic validationError class * feat: standard error types and safe() wrapper * refactor: move enhanceProps to astro:actions * fix: throw internal server errors * chore: refine enhance: true error message * fix: remove FormData fallback from route * refactor: clarify what enhance: true allows * feat: progressively enhanced comments * chore: changeset * refactor: enhance -> acceptFormData * wip: migrate actions to core * feat: working actions demo from astro core! * chore: changeset * chore: delete old changeset * fix: Function type lint * refactor: expose defineAction from `astro:actions` * fix: add null check to experimental * fix: export `types/actions.d.ts` * feat: more robust form data parsing * feat: support formData from rpc call * feat: remove acceptFormData flag requirement * feat: add actions.d.ts type reference on startup * refactor: actionNameProps -> getNameProps * fix: actions type import * chore: expose zod from `astro:actions` * fix: zod export path * feat: add explicit `accept` property * Use zod package instead of relative path outside of src * feat: clean up error throwing and handling flow * fix: make `accept` optional * docs: beef up actions experimental docs * fix: defineAction type narrowing on `accept` * fix: bad `getNameProps()` arg type * refactor: move to single `error` object + `isInputError()` util * fix: move res.json() parse to avoid double parse * feat: support async zod schemas * feat: serialize and expose zod properties on input error * feat: test input error in comment example * fix: remove ZodError import * fix: add actions-module to files export * fix: use workspace for test pkg versions * refactor: default export -> server export * fix: type inference for json vs. form * refactor: accept form -> defineFormAction * refactor: better callSafely signature * feat: block action calls from the server with RFC link * feat: move getActionResult to global * refactor: getNameProps -> getActionProps * refactor: body.toString() * edit: capitAl Co-authored-by: Sarah Rainsberger * edit: highlight `actions` Co-authored-by: Sarah Rainsberger * edit: add actions file name Co-authored-by: Sarah Rainsberger * edit: not you can. You DO Co-authored-by: Sarah Rainsberger * edit: declare with feeling Co-authored-by: Sarah Rainsberger * edit: clarify what the `handler` does * edit: schema -> input * edit: add FormData mdn reference * edit: add defineFormAction() explainer * refactor: inline getDotAstroTypeRefs * edit: yeah yeah maybe * fix: existsSync test mock * refactor: use callSafely in middleware * test: upgradeFormData() * chore: stray console log * refactor: extract helper functions * fix: include status in error response * fix: return `undefined` when there's no action result * fix: content-type * test: e2e like button action * test: comment e2e * fix: existsSync mock for other sync test * test: action dev server raw fetch * test: build preview * chore: fix lock * fix: add dotAstroDir to existsSync * chore: slim down e2e fixture * chore: remove unneeded disabled test * refactor: better api context error * fix: return `false` for envDts * refactor: defineFormAction -> defineAction with accept * fix: check FormData on getActionProps * edit: uppercase Co-authored-by: Sarah Rainsberger * fix: add switch default for 500 Co-authored-by: Emanuele Stoppa * fix: add `toLowerCase()` on content-type check Co-authored-by: Emanuele Stoppa * chore: use VIRTUAL_MODULE_ID for plugin * fix: remove incorrect ts-ignore * chore: remove unneeded POST method check * refactor: route callSafely * refactor: error switch case to map * chore: add link to trpc error code table * fix: add readable error on failed json.stringify * refactor: add param -> callerParam with comment * feat: always return safe from getActionResult() * refactor: move actions module to templates/ * refactor: remove unneeded existsSync on dotAstro * fix: hasContentType util for toLowerCase() * chore: comment on 415 code * refactor: upgradeFormData -> formDataToObj * fix: avoid leaking stack in production * refactor: defineProperty with write false * fix: revert package.json back to spaces * edit: use config docs for changeset * refactor: stringifiedActionsPath -> stringifiedActionsImport * fix: avoid double-handling for route * fix: support zero arg actions * refactor: move actionHandler to helper fn * fix: restore mdast deps * docs: add `output` to config --------- Co-authored-by: Sarah Rainsberger Co-authored-by: Emanuele Stoppa Co-authored-by: bholmesdev --- .changeset/shaggy-moons-peel.md | 95 ++++++++++ packages/astro/client.d.ts | 1 + packages/astro/e2e/actions-blog.test.js | 58 ++++++ .../fixtures/actions-blog/astro.config.mjs | 17 ++ .../e2e/fixtures/actions-blog/db/config.ts | 21 +++ .../e2e/fixtures/actions-blog/db/seed.ts | 15 ++ .../e2e/fixtures/actions-blog/package.json | 24 +++ .../actions-blog/src/actions/index.ts | 45 +++++ .../src/components/BaseHead.astro | 47 +++++ .../actions-blog/src/components/Footer.astro | 62 +++++++ .../src/components/FormattedDate.astro | 17 ++ .../actions-blog/src/components/Header.astro | 83 +++++++++ .../src/components/HeaderLink.astro | 25 +++ .../actions-blog/src/components/Like.tsx | 22 +++ .../src/components/PostComment.tsx | 66 +++++++ .../e2e/fixtures/actions-blog/src/consts.ts | 5 + .../src/content/blog/first-post.md | 15 ++ .../actions-blog/src/content/config.ts | 16 ++ .../actions-blog/src/layouts/BlogPost.astro | 85 +++++++++ .../src/pages/blog/[...slug].astro | 61 ++++++ .../actions-blog/src/pages/blog/index.astro | 111 +++++++++++ .../actions-blog/src/styles/global.css | 140 ++++++++++++++ .../e2e/fixtures/actions-blog/tsconfig.json | 8 + packages/astro/package.json | 4 +- packages/astro/src/@types/astro.ts | 126 +++++++++++++ packages/astro/src/actions/consts.ts | 3 + packages/astro/src/actions/index.ts | 81 ++++++++ .../astro/src/actions/runtime/middleware.ts | 52 ++++++ packages/astro/src/actions/runtime/route.ts | 39 ++++ packages/astro/src/actions/runtime/store.ts | 18 ++ packages/astro/src/actions/runtime/utils.ts | 27 +++ .../src/actions/runtime/virtual/client.ts | 18 ++ .../src/actions/runtime/virtual/server.ts | 172 +++++++++++++++++ .../src/actions/runtime/virtual/shared.ts | 151 +++++++++++++++ packages/astro/src/actions/utils.ts | 20 ++ packages/astro/src/content/index.ts | 7 +- packages/astro/src/content/utils.ts | 11 +- packages/astro/src/core/config/schema.ts | 2 + packages/astro/src/core/middleware/index.ts | 11 +- packages/astro/src/core/render-context.ts | 12 +- packages/astro/src/integrations/hooks.ts | 4 + .../src/vite-plugin-inject-env-ts/index.ts | 56 ++++-- packages/astro/templates/actions.mjs | 61 ++++++ packages/astro/test/actions.test.js | 173 ++++++++++++++++++ packages/astro/test/astro-sync.test.js | 43 +++-- .../test/fixtures/actions/astro.config.mjs | 9 + .../astro/test/fixtures/actions/package.json | 8 + .../fixtures/actions/src/actions/index.ts | 32 ++++ .../units/actions/form-data-to-object.test.js | 138 ++++++++++++++ packages/astro/types/actions.d.ts | 3 + pnpm-lock.yaml | 52 +++++- 51 files changed, 2320 insertions(+), 52 deletions(-) create mode 100644 .changeset/shaggy-moons-peel.md create mode 100644 packages/astro/e2e/actions-blog.test.js create mode 100644 packages/astro/e2e/fixtures/actions-blog/astro.config.mjs create mode 100644 packages/astro/e2e/fixtures/actions-blog/db/config.ts create mode 100644 packages/astro/e2e/fixtures/actions-blog/db/seed.ts create mode 100644 packages/astro/e2e/fixtures/actions-blog/package.json create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/BaseHead.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/Footer.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/FormattedDate.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/Header.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/HeaderLink.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/consts.ts create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/content/blog/first-post.md create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/content/config.ts create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/layouts/BlogPost.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/pages/blog/index.astro create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/styles/global.css create mode 100644 packages/astro/e2e/fixtures/actions-blog/tsconfig.json create mode 100644 packages/astro/src/actions/consts.ts create mode 100644 packages/astro/src/actions/index.ts create mode 100644 packages/astro/src/actions/runtime/middleware.ts create mode 100644 packages/astro/src/actions/runtime/route.ts create mode 100644 packages/astro/src/actions/runtime/store.ts create mode 100644 packages/astro/src/actions/runtime/utils.ts create mode 100644 packages/astro/src/actions/runtime/virtual/client.ts create mode 100644 packages/astro/src/actions/runtime/virtual/server.ts create mode 100644 packages/astro/src/actions/runtime/virtual/shared.ts create mode 100644 packages/astro/src/actions/utils.ts create mode 100644 packages/astro/templates/actions.mjs create mode 100644 packages/astro/test/actions.test.js create mode 100644 packages/astro/test/fixtures/actions/astro.config.mjs create mode 100644 packages/astro/test/fixtures/actions/package.json create mode 100644 packages/astro/test/fixtures/actions/src/actions/index.ts create mode 100644 packages/astro/test/units/actions/form-data-to-object.test.js create mode 100644 packages/astro/types/actions.d.ts diff --git a/.changeset/shaggy-moons-peel.md b/.changeset/shaggy-moons-peel.md new file mode 100644 index 000000000000..db500d5e5c71 --- /dev/null +++ b/.changeset/shaggy-moons-peel.md @@ -0,0 +1,95 @@ +--- +"astro": minor +--- + +Adds experimental support for the Actions API. Actions let you define type-safe endpoints you can query from client components with progressive enhancement built in. + + +Actions help you write type-safe backend functions you can call from anywhere. Enable server rendering [using the `output` property](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered) and add the `actions` flag to the `experimental` object: + +```js +{ + output: 'hybrid', // or 'server' + experimental: { + actions: true, + }, +} +``` + +Declare all your actions in `src/actions/index.ts`. This file is the global actions handler. + +Define an action using the `defineAction()` utility from the `astro:actions` module. These accept the `handler` property to define your server-side request handler. If your action accepts arguments, apply the `input` property to validate parameters with Zod. + +This example defines two actions: `like` and `comment`. The `like` action accepts a JSON object with a `postId` string, while the `comment` action accepts [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) with `postId`, `author`, and `body` strings. Each `handler` updates your database and return a type-safe response. + +```ts +// src/actions/index.ts +import { defineAction, z } from "astro:actions"; + +export const server = { + like: defineAction({ + input: z.object({ postId: z.string() }), + handler: async ({ postId }, context) => { + // update likes in db + + return likes; + }, + }), + comment: defineAction({ + accept: 'form', + input: z.object({ + postId: z.string(), + author: z.string(), + body: z.string(), + }), + handler: async ({ postId }, context) => { + // insert comments in db + + return comment; + }, + }), +}; +``` + +Then, call an action from your client components using the `actions` object from `astro:actions`. You can pass a type-safe object when using JSON, or a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) object when using `accept: 'form'` in your action definition: + +```tsx "actions" +// src/components/blog.tsx +import { actions } from "astro:actions"; +import { useState } from "preact/hooks"; + +export function Like({ postId }: { postId: string }) { + const [likes, setLikes] = useState(0); + return ( + + ); +} + +export function Comment({ postId }: { postId: string }) { + return ( + + ); +} +``` + +For a complete overview, and to give feedback on this experimental API, see the [Actions RFC](https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md). diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index 9128e9dd0ce7..f81d652e3808 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -1,5 +1,6 @@ /// /// +/// // eslint-disable-next-line @typescript-eslint/no-namespace declare namespace App { diff --git a/packages/astro/e2e/actions-blog.test.js b/packages/astro/e2e/actions-blog.test.js new file mode 100644 index 000000000000..b98f74143ebd --- /dev/null +++ b/packages/astro/e2e/actions-blog.test.js @@ -0,0 +1,58 @@ +import { expect } from '@playwright/test'; +import { testFactory } from './test-utils.js'; + +const test = testFactory({ root: './fixtures/actions-blog/' }); + +let devServer; + +test.beforeAll(async ({ astro }) => { + devServer = await astro.startDevServer(); +}); + +test.afterAll(async () => { + await devServer.stop(); +}); + +test.describe('Astro Actions - Blog', () => { + test('Like action', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/blog/first-post/')); + + const likeButton = page.getByLabel('Like'); + await expect(likeButton, 'like button starts with 10 likes').toContainText('10'); + await likeButton.click(); + await expect(likeButton, 'like button should increment likes').toContainText('11'); + }); + + test('Comment action - validation error', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/blog/first-post/')); + + const authorInput = page.locator('input[name="author"]'); + const bodyInput = page.locator('textarea[name="body"]'); + + await authorInput.fill('Ben'); + await bodyInput.fill('Too short'); + + const submitButton = page.getByLabel('Post comment'); + await submitButton.click(); + + await expect(page.locator('p[data-error="body"]')).toBeVisible(); + }); + + test('Comment action - success', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/blog/first-post/')); + + const authorInput = page.locator('input[name="author"]'); + const bodyInput = page.locator('textarea[name="body"]'); + + const body = 'This should be long enough.'; + await authorInput.fill('Ben'); + await bodyInput.fill(body); + + const submitButton = page.getByLabel('Post comment'); + await submitButton.click(); + + const comment = await page.getByTestId('comment'); + await expect(comment).toBeVisible(); + await expect(comment).toContainText(body); + }); +}); diff --git a/packages/astro/e2e/fixtures/actions-blog/astro.config.mjs b/packages/astro/e2e/fixtures/actions-blog/astro.config.mjs new file mode 100644 index 000000000000..acbed1768b3c --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/astro.config.mjs @@ -0,0 +1,17 @@ +import { defineConfig } from 'astro/config'; +import db from '@astrojs/db'; +import react from '@astrojs/react'; +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + site: 'https://example.com', + integrations: [db(), react()], + output: 'hybrid', + adapter: node({ + mode: 'standalone', + }), + experimental: { + actions: true, + }, +}); diff --git a/packages/astro/e2e/fixtures/actions-blog/db/config.ts b/packages/astro/e2e/fixtures/actions-blog/db/config.ts new file mode 100644 index 000000000000..da005471e10c --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/db/config.ts @@ -0,0 +1,21 @@ +import { column, defineDb, defineTable } from "astro:db"; + +const Comment = defineTable({ + columns: { + postId: column.text(), + author: column.text(), + body: column.text(), + }, +}); + +const Likes = defineTable({ + columns: { + postId: column.text(), + likes: column.number(), + }, +}); + +// https://astro.build/db/config +export default defineDb({ + tables: { Comment, Likes }, +}); diff --git a/packages/astro/e2e/fixtures/actions-blog/db/seed.ts b/packages/astro/e2e/fixtures/actions-blog/db/seed.ts new file mode 100644 index 000000000000..11dc55f7fe00 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/db/seed.ts @@ -0,0 +1,15 @@ +import { db, Likes, Comment } from "astro:db"; + +// https://astro.build/db/seed +export default async function seed() { + await db.insert(Likes).values({ + postId: "first-post.md", + likes: 10, + }); + + await db.insert(Comment).values({ + postId: "first-post.md", + author: "Alice", + body: "Great post!", + }); +} diff --git a/packages/astro/e2e/fixtures/actions-blog/package.json b/packages/astro/e2e/fixtures/actions-blog/package.json new file mode 100644 index 000000000000..0c69e9a98be5 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/package.json @@ -0,0 +1,24 @@ +{ + "name": "@e2e/astro-actions-basics", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/check": "^0.5.10", + "@astrojs/db": "workspace:*", + "@astrojs/node": "workspace:*", + "@astrojs/react": "workspace:*", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "astro": "workspace:*", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "typescript": "^5.4.5" + } +} diff --git a/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts b/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts new file mode 100644 index 000000000000..4574caaaf50b --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts @@ -0,0 +1,45 @@ +import { db, Comment, Likes, eq, sql } from 'astro:db'; +import { defineAction, z } from 'astro:actions'; + +export const server = { + blog: { + like: defineAction({ + input: z.object({ postId: z.string() }), + handler: async ({ postId }) => { + await new Promise((r) => setTimeout(r, 200)); + + const { likes } = await db + .update(Likes) + .set({ + likes: sql`likes + 1`, + }) + .where(eq(Likes.postId, postId)) + .returning() + .get(); + + return likes; + }, + }), + + comment: defineAction({ + accept: 'form', + input: z.object({ + postId: z.string(), + author: z.string(), + body: z.string().min(10), + }), + handler: async ({ postId, author, body }) => { + const comment = await db + .insert(Comment) + .values({ + postId, + body, + author, + }) + .returning() + .get(); + return comment; + }, + }), + }, +}; diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/BaseHead.astro b/packages/astro/e2e/fixtures/actions-blog/src/components/BaseHead.astro new file mode 100644 index 000000000000..344124012bbf --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/BaseHead.astro @@ -0,0 +1,47 @@ +--- +// Import the global.css file here so that it is included on +// all pages through the use of the component. +import '../styles/global.css'; + +interface Props { + title: string; + description: string; + image?: string; +} + +const canonicalURL = new URL(Astro.url.pathname, Astro.site); + +const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props; +--- + + + + + + + + + + + + + + + + {title} + + + + + + + + + + + + + + + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/Footer.astro b/packages/astro/e2e/fixtures/actions-blog/src/components/Footer.astro new file mode 100644 index 000000000000..96c2fce91208 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/Footer.astro @@ -0,0 +1,62 @@ +--- +const today = new Date(); +--- + + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/FormattedDate.astro b/packages/astro/e2e/fixtures/actions-blog/src/components/FormattedDate.astro new file mode 100644 index 000000000000..1bcce73a2b67 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/FormattedDate.astro @@ -0,0 +1,17 @@ +--- +interface Props { + date: Date; +} + +const { date } = Astro.props; +--- + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/Header.astro b/packages/astro/e2e/fixtures/actions-blog/src/components/Header.astro new file mode 100644 index 000000000000..71b8cdc55c58 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/Header.astro @@ -0,0 +1,83 @@ +--- +import HeaderLink from './HeaderLink.astro'; +import { SITE_TITLE } from '../consts'; +--- + ++ + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/HeaderLink.astro b/packages/astro/e2e/fixtures/actions-blog/src/components/HeaderLink.astro new file mode 100644 index 000000000000..bb600fb65ac3 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/HeaderLink.astro @@ -0,0 +1,25 @@ +--- +import type { HTMLAttributes } from 'astro/types'; + +type Props = HTMLAttributes<'a'>; + +const { href, class: className, ...props } = Astro.props; + +const { pathname } = Astro.url; +const subpath = pathname.match(/[^\/]+/g); +const isActive = href === pathname || href === '/' + subpath?.[0]; +--- + + ++ + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx b/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx new file mode 100644 index 000000000000..7d4e6a53d161 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx @@ -0,0 +1,22 @@ +import { actions } from 'astro:actions'; +import { useState } from 'react'; + +export function Like({ postId, initial }: { postId: string; initial: number }) { + const [likes, setLikes] = useState(initial); + const [pending, setPending] = useState(false); + + return ( + + ); +} diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx b/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx new file mode 100644 index 000000000000..1b0d10a063fc --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx @@ -0,0 +1,66 @@ +import { getActionProps, actions, isInputError } from 'astro:actions'; +import { useState } from 'react'; + +export function PostComment({ + postId, + serverBodyError, +}: { + postId: string; + serverBodyError?: string; +}) { + const [comments, setComments] = useState<{ author: string; body: string }[]>([]); + const [bodyError, setBodyError] = useState (serverBodyError); + + return ( + <> + + {comments.map((c) => ( + + + ))} + > + ); +} diff --git a/packages/astro/e2e/fixtures/actions-blog/src/consts.ts b/packages/astro/e2e/fixtures/actions-blog/src/consts.ts new file mode 100644 index 000000000000..0df8a61f4cf7 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/consts.ts @@ -0,0 +1,5 @@ +// Place any global data in this file. +// You can import this data from anywhere in your site by using the `import` keyword. + +export const SITE_TITLE = 'Astro Blog'; +export const SITE_DESCRIPTION = 'Welcome to my website!'; diff --git a/packages/astro/e2e/fixtures/actions-blog/src/content/blog/first-post.md b/packages/astro/e2e/fixtures/actions-blog/src/content/blog/first-post.md new file mode 100644 index 000000000000..ee51f1541097 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/content/blog/first-post.md @@ -0,0 +1,15 @@ +--- +title: 'First post' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 08 2022' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/packages/astro/e2e/fixtures/actions-blog/src/content/config.ts b/packages/astro/e2e/fixtures/actions-blog/src/content/config.ts new file mode 100644 index 000000000000..667a31cc7391 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/content/config.ts @@ -0,0 +1,16 @@ +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + type: 'content', + // Type-check frontmatter using a schema + schema: z.object({ + title: z.string(), + description: z.string(), + // Transform string to Date object + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + heroImage: z.string().optional(), + }), +}); + +export const collections = { blog }; diff --git a/packages/astro/e2e/fixtures/actions-blog/src/layouts/BlogPost.astro b/packages/astro/e2e/fixtures/actions-blog/src/layouts/BlogPost.astro new file mode 100644 index 000000000000..e67b2b30f859 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/layouts/BlogPost.astro @@ -0,0 +1,85 @@ +--- +import type { CollectionEntry } from 'astro:content'; +import BaseHead from '../components/BaseHead.astro'; +import Header from '../components/Header.astro'; +import Footer from '../components/Footer.astro'; +import FormattedDate from '../components/FormattedDate.astro'; + +type Props = CollectionEntry<'blog'>['data']; + +const { title, description, pubDate, updatedDate, heroImage } = Astro.props; +--- + + + +{c.body}
+{c.author}
++ + + + + + + + + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro new file mode 100644 index 000000000000..a571a7cbe61f --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro @@ -0,0 +1,61 @@ +--- +import { type CollectionEntry, getCollection, getEntry } from 'astro:content'; +import BlogPost from '../../layouts/BlogPost.astro'; +import { db, eq, Comment, Likes } from 'astro:db'; +import { Like } from '../../components/Like'; +import { PostComment } from '../../components/PostComment'; +import { actions } from 'astro:actions'; +import { isInputError } from 'astro:actions'; + +export const prerender = false; + +export async function getStaticPaths() { + const posts = await getCollection('blog'); + return posts.map((post) => ({ + params: { slug: post.slug }, + props: post, + })); +} + +type Props = CollectionEntry<'blog'>; + +const post = await getEntry('blog', Astro.params.slug)!; +const { Content } = await post.render(); + +const comment = Astro.getActionResult(actions.blog.comment); + +const comments = await db.select().from(Comment).where(eq(Comment.postId, post.id)); + +const initialLikes = await db.select().from(Likes).where(eq(Likes.postId, post.id)).get(); +--- + ++ ++ {heroImage && } +++++++++ { + updatedDate && ( + + Last updated on+ ) + } ++ {title}
+
++ + + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/index.astro b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/index.astro new file mode 100644 index 000000000000..ebbcd304272f --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/index.astro @@ -0,0 +1,111 @@ +--- +import BaseHead from '../../components/BaseHead.astro'; +import Header from '../../components/Header.astro'; +import Footer from '../../components/Footer.astro'; +import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts'; +import { getCollection } from 'astro:content'; +import FormattedDate from '../../components/FormattedDate.astro'; + +const posts = (await getCollection('blog')).sort( + (a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf() +); +--- + + + + ++ + + + Comments
++ + { + comments.map((c) => ( +++ + )) + } +{c.body}
+{c.author}
++ + + + + + + + + diff --git a/packages/astro/e2e/fixtures/actions-blog/src/styles/global.css b/packages/astro/e2e/fixtures/actions-blog/src/styles/global.css new file mode 100644 index 000000000000..757a8a07f4c6 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/styles/global.css @@ -0,0 +1,140 @@ +/* + The CSS in this style tag is based off of Bear Blog's default CSS. + https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css + License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md + */ + +:root { + --accent: #2337ff; + --accent-dark: #000d8a; + --black: 15, 18, 25; + --gray: 96, 115, 159; + --gray-light: 229, 233, 240; + --gray-dark: 34, 41, 57; + --gray-gradient: rgba(var(--gray-light), 50%), #fff; + --box-shadow: 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%), + 0 16px 32px rgba(var(--gray), 33%); +} +body { + font-family: sans-serif; + margin: 0; + padding: 0; + text-align: left; + background: linear-gradient(var(--gray-gradient)) no-repeat; + background-size: 100% 600px; + word-wrap: break-word; + overflow-wrap: break-word; + color: rgb(var(--gray-dark)); + font-size: 20px; + line-height: 1.7; +} +main { + width: 720px; + max-width: calc(100% - 2em); + margin: auto; + padding: 3em 1em; +} +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0 0 0.5rem 0; + color: rgb(var(--black)); + line-height: 1.2; +} +h1 { + font-size: 3.052em; +} +h2 { + font-size: 2.441em; +} +h3 { + font-size: 1.953em; +} +h4 { + font-size: 1.563em; +} +h5 { + font-size: 1.25em; +} +strong, +b { + font-weight: 700; +} +a { + color: var(--accent); +} +a:hover { + color: var(--accent); +} +p { + margin-bottom: 1em; +} +.prose p { + margin-bottom: 2em; +} +textarea { + width: 100%; + font-size: 16px; +} +input { + font-size: 16px; +} +table { + width: 100%; +} +img { + max-width: 100%; + height: auto; + border-radius: 8px; +} +code { + padding: 2px 5px; + background-color: rgb(var(--gray-light)); + border-radius: 2px; +} +pre { + padding: 1.5em; + border-radius: 8px; +} +pre > code { + all: unset; +} +blockquote { + border-left: 4px solid var(--accent); + padding: 0 0 0 20px; + margin: 0px; + font-size: 1.333em; +} +hr { + border: none; + border-top: 1px solid rgb(var(--gray-light)); +} +@media (max-width: 720px) { + body { + font-size: 18px; + } + main { + padding: 1em; + } +} + +.sr-only { + border: 0; + padding: 0; + margin: 0; + position: absolute !important; + height: 1px; + width: 1px; + overflow: hidden; + /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */ + clip: rect(1px 1px 1px 1px); + /* maybe deprecated but we need to support legacy browsers */ + clip: rect(1px, 1px, 1px, 1px); + /* modern browsers, clip-path works inwards from each corner */ + clip-path: inset(50%); + /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */ + white-space: nowrap; +} diff --git a/packages/astro/e2e/fixtures/actions-blog/tsconfig.json b/packages/astro/e2e/fixtures/actions-blog/tsconfig.json new file mode 100644 index 000000000000..6bed1f7a51c0 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "strictNullChecks": true, + "jsx": "react-jsx", + "jsxImportSource": "react" + } +} \ No newline at end of file diff --git a/packages/astro/package.json b/packages/astro/package.json index 572d5a9863f8..aa5ef4a3d8c8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -54,6 +54,7 @@ "./components": "./components/index.ts", "./components/*": "./components/*", "./toolbar": "./dist/toolbar/index.js", + "./actions/runtime/*": "./dist/actions/runtime/*", "./assets": "./dist/assets/index.js", "./assets/utils": "./dist/assets/utils/index.js", "./assets/endpoint/*": "./dist/assets/endpoint/*.js", @@ -85,6 +86,7 @@ "components", "tsconfigs", "dist", + "types", "astro.js", "index.d.ts", "config.d.ts", @@ -96,8 +98,8 @@ "jsx-runtime.d.ts", "content-types.template.d.ts", "content-module.template.mjs", + "templates", "astro-jsx.d.ts", - "types/content.d.ts", "types.d.ts", "README.md", "vendor" diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index cb39ec785ded..9e45bfd23c36 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -46,6 +46,7 @@ import type { } from '../transitions/events.js'; import type { DeepPartial, OmitIndexSignature, Simplify, WithRequired } from '../type-utils.js'; import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'; +import type { Accept, ActionClient, InputSchema } from '../actions/runtime/virtual/server.js'; export type { AstroIntegrationLogger, ToolbarServerHelpers }; @@ -239,6 +240,22 @@ export interface AstroGlobal< response: ResponseInit & { readonly headers: Headers; }; + /** + * Get an action result on the server when using a form POST. + * Expects the action function as a parameter. + * Returns a type-safe result with the action data when + * a matching POST request is received + * and `undefined` otherwise. + * + * Example usage: + * + * ```typescript + * import { actions } from 'astro:actions'; + * + * const result = await Astro.getActionResult(actions.myAction); + * ``` + */ + getActionResult: AstroSharedContext['getActionResult']; /** Redirect to another page (**SSR Only**) * * Example usage: @@ -1705,6 +1722,105 @@ export interface AstroUserConfig { */ directRenderScript?: boolean; + /** + * @docs + * @name experimental.actions + * @type {boolean} + * @default `false` + * @version 4.7.0 + * @description + * + * Actions help you write type-safe backend functions you can call from anywhere. Enable server rendering [using the `output` property](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered) and add the `actions` flag to the `experimental` object: + * + * ```js + * { + * output: 'hybrid', // or 'server' + * experimental: { + * actions: true, + * }, + * } + * ``` + * + * Declare all your actions in `src/actions/index.ts`. This file is the global actions handler. + * + * Define an action using the `defineAction()` utility from the `astro:actions` module. These accept the `handler` property to define your server-side request handler. If your action accepts arguments, apply the `input` property to validate parameters with Zod. + * + * This example defines two actions: `like` and `comment`. The `like` action accepts a JSON object with a `postId` string, while the `comment` action accepts [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) with `postId`, `author`, and `body` strings. Each `handler` updates your database and return a type-safe response. + * + * ```ts + * // src/actions/index.ts + * import { defineAction, z } from "astro:actions"; + * + * export const server = { + * like: defineAction({ + * input: z.object({ postId: z.string() }), + * handler: async ({ postId }, context) => { + * // update likes in db + * + * return likes; + * }, + * }), + * comment: defineAction({ + * accept: 'form', + * input: z.object({ + * postId: z.string(), + * author: z.string(), + * body: z.string(), + * }), + * handler: async ({ postId }, context) => { + * // insert comments in db + * + * return comment; + * }, + * }), + * }; + * ``` + * + * Then, call an action from your client components using the `actions` object from `astro:actions`. You can pass a type-safe object when using JSON, or a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) object when using `accept: 'form'` in your action definition: + * + * ```tsx "actions" + * // src/components/blog.tsx + * import { actions } from "astro:actions"; + * import { useState } from "preact/hooks"; + * + * export function Like({ postId }: { postId: string }) { + * const [likes, setLikes] = useState(0); + * return ( + * + * ); + * } + * + * export function Comment({ postId }: { postId: string }) { + * return ( + * + * ); + * } + * ``` + * + * For a complete overview, and to give feedback on this experimental API, see the [Actions RFC](https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md). + */ + actions?: boolean; + /** * @docs * @name experimental.contentCollectionCache @@ -2548,6 +2664,16 @@ interface AstroSharedContext< * A full URL object of the request URL. */ url: URL; + /** + * Get action result on the server when using a form POST. + */ + getActionResult: < + TAccept extends Accept, + TInputSchema extends InputSchema+ ++ { + posts.map((post) => ( +
+- + + +
+ )) + } +{post.data.title}
++
+ ++ , + TAction extends ActionClient , + >( + action: TAction + ) => Awaited > | undefined; /** * Route parameters for this request if this is a dynamic route. */ diff --git a/packages/astro/src/actions/consts.ts b/packages/astro/src/actions/consts.ts new file mode 100644 index 000000000000..ef6b87ca83fc --- /dev/null +++ b/packages/astro/src/actions/consts.ts @@ -0,0 +1,3 @@ +export const VIRTUAL_MODULE_ID = 'astro:actions'; +export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; +export const ACTIONS_TYPES_FILE = 'actions.d.ts'; diff --git a/packages/astro/src/actions/index.ts b/packages/astro/src/actions/index.ts new file mode 100644 index 000000000000..c2c3ebbf6c7a --- /dev/null +++ b/packages/astro/src/actions/index.ts @@ -0,0 +1,81 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import type { AstroIntegration } from '../@types/astro.js'; +import { ACTIONS_TYPES_FILE, RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID } from './consts.js'; +import type { Plugin as VitePlugin } from 'vite'; + +export default function astroActions(): AstroIntegration { + return { + name: VIRTUAL_MODULE_ID, + hooks: { + async 'astro:config:setup'(params) { + const stringifiedActionsImport = JSON.stringify( + new URL('actions', params.config.srcDir).pathname + ); + params.updateConfig({ + vite: { + define: { + 'import.meta.env.ACTIONS_PATH': stringifiedActionsImport, + }, + plugins: [vitePluginActions], + }, + }); + + params.injectRoute({ + pattern: '/_actions/[...path]', + entrypoint: 'astro/actions/runtime/route.js', + prerender: false, + }); + + params.addMiddleware({ + entrypoint: 'astro/actions/runtime/middleware.js', + order: 'pre', + }); + + await typegen({ + stringifiedActionsImport, + root: params.config.root, + }); + }, + }, + }; +} + +const vitePluginActions: VitePlugin = { + name: VIRTUAL_MODULE_ID, + enforce: 'pre', + resolveId(id) { + if (id === VIRTUAL_MODULE_ID) { + return RESOLVED_VIRTUAL_MODULE_ID; + } + }, + async load(id, opts) { + if (id !== RESOLVED_VIRTUAL_MODULE_ID) return; + + let code = await readFile(new URL('../../templates/actions.mjs', import.meta.url), 'utf-8'); + if (opts?.ssr) { + code += `\nexport * from 'astro/actions/runtime/virtual/server.js';`; + } else { + code += `\nexport * from 'astro/actions/runtime/virtual/client.js';`; + } + return code; + }, +}; + +async function typegen({ + stringifiedActionsImport, + root, +}: { + stringifiedActionsImport: string; + root: URL; +}) { + const content = `declare module "astro:actions" { + type Actions = typeof import(${stringifiedActionsImport})["server"]; + + export const actions: Actions; +}`; + + const dotAstroDir = new URL('.astro/', root); + + await mkdir(dotAstroDir, { recursive: true }); + await writeFile(new URL(ACTIONS_TYPES_FILE, dotAstroDir), content); +} diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts new file mode 100644 index 000000000000..c0d90f26feb8 --- /dev/null +++ b/packages/astro/src/actions/runtime/middleware.ts @@ -0,0 +1,52 @@ +import { defineMiddleware } from '../../core/middleware/index.js'; +import { ApiContextStorage } from './store.js'; +import { formContentTypes, getAction, hasContentType } from './utils.js'; +import { callSafely } from './virtual/shared.js'; +import type { APIContext, MiddlewareNext } from '../../@types/astro.js'; + +export type Locals = { + _actionsInternal: { + getActionResult: APIContext['getActionResult']; + }; +}; + +export const onRequest = defineMiddleware(async (context, next) => { + const locals = context.locals as Locals; + const { request, url } = context; + const contentType = request.headers.get('Content-Type'); + + // Avoid double-handling with middleware when calling actions directly. + if (url.pathname.startsWith('/_actions')) return nextWithLocalsStub(next, locals); + + if (!contentType || !hasContentType(contentType, formContentTypes)) + return nextWithLocalsStub(next, locals); + + const formData = await request.clone().formData(); + const actionPath = formData.get('_astroAction'); + if (typeof actionPath !== 'string') return nextWithLocalsStub(next, locals); + + const actionPathKeys = actionPath.replace('/_actions/', '').split('.'); + const action = await getAction(actionPathKeys); + const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData))); + + const actionsInternal: Locals['_actionsInternal'] = { + getActionResult: (actionFn) => { + if (actionFn.toString() !== actionPath) return Promise.resolve(undefined); + // The `action` uses type `unknown` since we can't infer the user's action type. + // Cast to `any` to satisfy `getActionResult()` type. + return result as any; + }, + }; + Object.defineProperty(locals, '_actionsInternal', { writable: false, value: actionsInternal }); + return next(); +}); + +function nextWithLocalsStub(next: MiddlewareNext, locals: Locals) { + Object.defineProperty(locals, '_actionsInternal', { + writable: false, + value: { + getActionResult: () => undefined, + }, + }); + return next(); +} diff --git a/packages/astro/src/actions/runtime/route.ts b/packages/astro/src/actions/runtime/route.ts new file mode 100644 index 000000000000..59b5aaf290b7 --- /dev/null +++ b/packages/astro/src/actions/runtime/route.ts @@ -0,0 +1,39 @@ +import type { APIRoute } from '../../@types/astro.js'; +import { ApiContextStorage } from './store.js'; +import { formContentTypes, getAction, hasContentType } from './utils.js'; +import { callSafely } from './virtual/shared.js'; + +export const POST: APIRoute = async (context) => { + const { request, url } = context; + const actionPathKeys = url.pathname.replace('/_actions/', '').split('.'); + const action = await getAction(actionPathKeys); + const contentType = request.headers.get('Content-Type'); + let args: unknown; + if (contentType && hasContentType(contentType, formContentTypes)) { + args = await request.clone().formData(); + } else if (contentType && hasContentType(contentType, ['application/json'])) { + args = await request.clone().json(); + } else { + // 415: Unsupported media type + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 + return new Response(null, { status: 415 }); + } + const result = await ApiContextStorage.run(context, () => callSafely(() => action(args))); + if (result.error) { + if (import.meta.env.PROD) { + // Avoid leaking stack trace in production + result.error.stack = undefined; + } + return new Response(JSON.stringify(result.error), { + status: result.error.status, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + return new Response(JSON.stringify(result.data), { + headers: { + 'Content-Type': 'application/json', + }, + }); +}; diff --git a/packages/astro/src/actions/runtime/store.ts b/packages/astro/src/actions/runtime/store.ts new file mode 100644 index 000000000000..596177601231 --- /dev/null +++ b/packages/astro/src/actions/runtime/store.ts @@ -0,0 +1,18 @@ +import type { APIContext } from '../../@types/astro.js'; +import { AstroError } from '../../core/errors/errors.js'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +export type ActionAPIContext = Omit ; +export const ApiContextStorage = new AsyncLocalStorage (); + +export function getApiContext(): ActionAPIContext { + const context = ApiContextStorage.getStore(); + if (!context) { + throw new AstroError({ + name: 'AstroActionError', + message: 'Unable to get API context.', + hint: 'If you attempted to call this action from server code, trying using `Astro.getActionResult()` instead.', + }); + } + return context; +} diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts new file mode 100644 index 000000000000..8beb43a5a180 --- /dev/null +++ b/packages/astro/src/actions/runtime/utils.ts @@ -0,0 +1,27 @@ +export const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; + +export function hasContentType(contentType: string, expected: string[]) { + // Split off parameters like charset or boundary + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms + const type = contentType.split(';')[0].toLowerCase(); + + return expected.some((t) => type === t); +} + +export type MaybePromise = T | Promise ; + +export async function getAction( + pathKeys: string[] +): Promise<(param: unknown) => MaybePromise > { + let { server: actionLookup } = await import(import.meta.env.ACTIONS_PATH); + for (const key of pathKeys) { + if (!(key in actionLookup)) { + throw new Error('Action not found'); + } + actionLookup = actionLookup[key]; + } + if (typeof actionLookup !== 'function') { + throw new Error('Action not found'); + } + return actionLookup; +} diff --git a/packages/astro/src/actions/runtime/virtual/client.ts b/packages/astro/src/actions/runtime/virtual/client.ts new file mode 100644 index 000000000000..3c81e19cb861 --- /dev/null +++ b/packages/astro/src/actions/runtime/virtual/client.ts @@ -0,0 +1,18 @@ +export * from './shared.js'; + +export function defineAction() { + throw new Error('[astro:action] `defineAction()` unexpectedly used on the client.'); +} + +export function getApiContext() { + throw new Error('[astro:action] `getApiContext()` unexpectedly used on the client.'); +} + +export const z = new Proxy( + {}, + { + get() { + throw new Error('[astro:action] `z` unexpectedly used on the client.'); + }, + } +); diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts new file mode 100644 index 000000000000..c9c73481c471 --- /dev/null +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -0,0 +1,172 @@ +import { z } from 'zod'; +import { getApiContext } from '../store.js'; +import { hasContentType, type MaybePromise } from '../utils.js'; +import { + ActionError, + ActionInputError, + callSafely, + type ErrorInferenceObject, + type SafeResult, +} from './shared.js'; + +export * from './shared.js'; + +export { z } from 'zod'; + +export { getApiContext } from '../store.js'; + +export type Accept = 'form' | 'json'; +export type InputSchema = T extends 'form' + ? z.AnyZodObject | z.ZodType + : z.ZodType; + +type Handler = TInputSchema extends z.ZodType + ? (input: z.infer ) => MaybePromise + : (input?: any) => MaybePromise ; + +export type ActionClient< + TOutput, + TAccept extends Accept, + TInputSchema extends InputSchema | undefined, +> = TInputSchema extends z.ZodType + ? (( + input: TAccept extends 'form' ? FormData : z.input + ) => Promise >) & { + safe: ( + input: TAccept extends 'form' ? FormData : z.input + ) => Promise< + SafeResult< + z.input extends ErrorInferenceObject + ? z.input + : ErrorInferenceObject, + Awaited + > + >; + } + : ((input?: any) => Promise >) & { + safe: (input?: any) => Promise >>; + }; + +export function defineAction< + TOutput, + TAccept extends Accept = 'json', + TInputSchema extends InputSchema | undefined = TAccept extends 'form' + ? // If `input` is omitted, default to `FormData` for forms and `any` for JSON. + z.ZodType + : undefined, +>({ + accept, + input: inputSchema, + handler, +}: { + input?: TInputSchema; + accept?: TAccept; + handler: Handler ; +}): ActionClient { + const serverHandler = + accept === 'form' + ? getFormServerHandler(handler, inputSchema) + : getJsonServerHandler(handler, inputSchema); + + Object.assign(serverHandler, { + safe: async (unparsedInput: unknown) => { + return callSafely(() => serverHandler(unparsedInput)); + }, + }); + return serverHandler as ActionClient ; +} + +function getFormServerHandler >( + handler: Handler , + inputSchema?: TInputSchema +) { + return async (unparsedInput: unknown): Promise > => { + if (!(unparsedInput instanceof FormData)) { + throw new ActionError({ + code: 'UNSUPPORTED_MEDIA_TYPE', + message: 'This action only accepts FormData.', + }); + } + + if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput); + + const parsed = await inputSchema.safeParseAsync(formDataToObject(unparsedInput, inputSchema)); + if (!parsed.success) { + throw new ActionInputError(parsed.error.issues); + } + return await handler(parsed.data); + }; +} + +function getJsonServerHandler >( + handler: Handler , + inputSchema?: TInputSchema +) { + return async (unparsedInput: unknown): Promise > => { + const context = getApiContext(); + const contentType = context.request.headers.get('content-type'); + if (!contentType || !hasContentType(contentType, ['application/json'])) { + throw new ActionError({ + code: 'UNSUPPORTED_MEDIA_TYPE', + message: 'This action only accepts JSON.', + }); + } + + if (!inputSchema) return await handler(unparsedInput); + const parsed = await inputSchema.safeParseAsync(unparsedInput); + if (!parsed.success) { + throw new ActionInputError(parsed.error.issues); + } + return await handler(parsed.data); + }; +} + +/** Transform form data to an object based on a Zod schema. */ +export function formDataToObject ( + formData: FormData, + schema: T +): Record { + const obj: Record