diff --git a/.changeset/mighty-trees-teach.md b/.changeset/mighty-trees-teach.md new file mode 100644 index 000000000000..289a41816154 --- /dev/null +++ b/.changeset/mighty-trees-teach.md @@ -0,0 +1,5 @@ +--- +'@astrojs/mdx': patch +--- + +Fixes stack trace location when failed to parse an MDX file with frontmatter diff --git a/.changeset/poor-frogs-dream.md b/.changeset/poor-frogs-dream.md new file mode 100644 index 000000000000..fdb3daa85284 --- /dev/null +++ b/.changeset/poor-frogs-dream.md @@ -0,0 +1,7 @@ +--- +'astro': major +--- + +Refactor the exported types from the `astro` module. There should normally be no breaking changes, but if you relied on some previously deprecated types, these might now have been fully removed. + +In most cases, updating your code to move away from previously deprecated APIs in previous versions of Astro should be enough to fix any issues. diff --git a/.changeset/pre.json b/.changeset/pre.json index 8a327161067b..f239e523d5cc 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,6 +1,6 @@ { "mode": "pre", - "tag": "beta", + "tag": "alpha", "initialVersions": { "astro": "4.13.1", "@astrojs/prism": "3.1.0", diff --git a/.changeset/small-ties-sort.md b/.changeset/small-ties-sort.md new file mode 100644 index 000000000000..e3f3d67eb4f8 --- /dev/null +++ b/.changeset/small-ties-sort.md @@ -0,0 +1,50 @@ +--- +'astro': major +--- + +Fixes attribute rendering for non-[boolean HTML attributes](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML) with boolean values to match proper attribute handling in browsers. + +Previously, non-boolean attributes may not have included their values when rendered to HTML. In Astro v5.0, the values are now explicitly rendered as `="true"` or `="false"` + +In the following `.astro` examples, only `allowfullscreen` is a boolean attribute: + +```astro + + +

+

+ + +

+

+ + +

+

+``` + +Astro v5.0 now preserves the full data attribute with its value when rendering the HTML of non-boolean attributes: + +```diff +

+

+ +

+-

++

+ +-

++

+-

++

+``` + +If you rely on attribute values, for example to locate elements or to conditionally render, update your code to match the new non-boolean attribute values: + +```diff +- el.getAttribute('inherit') === '' ++ el.getAttribute('inherit') === 'false' + +- el.hasAttribute('data-light') ++ el.dataset.light === 'true' +``` diff --git a/.changeset/spicy-houses-fry.md b/.changeset/spicy-houses-fry.md new file mode 100644 index 000000000000..41f376a3546f --- /dev/null +++ b/.changeset/spicy-houses-fry.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Simplifies path operations of `astro sync` diff --git a/.github/labeler.yml b/.github/labeler.yml index c1025ad117d1..b0d27800ba97 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -40,5 +40,5 @@ - packages/integrations/vue/** 'docs pr': -- packages/astro/src/@types/astro.ts +- packages/astro/src/types/public/** - packages/astro/src/core/errors/errors-data.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c1b4919901ab..2ce9ac1dbd53 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -11,7 +11,7 @@ on: - "examples/**" - ".github/workflows/check.yml" - "scripts/smoke/check.js" - - "packages/astro/src/@types/astro.ts" + - "packages/astro/src/types/public/**" - "pnpm-lock.yaml" - "packages/astro/types.d.ts" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79962c07e1ce..d516ab47a93f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,7 +229,7 @@ jobs: filters: | docs: - 'packages/integrations/*/README.md' - - 'packages/astro/src/@types/astro.ts' + - "packages/astro/src/types/public/**" - 'packages/astro/src/core/errors/errors-data.ts' - name: Build autogenerated docs pages from current astro branch diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index caaa9e726e10..6a657068d1df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -289,7 +289,7 @@ Server-side rendering (SSR) can be complicated. The Astro package (`packages/ast - `components/`: Built-in components to use in your project (e.g. `import Code from 'astro/components/Code.astro'`) - `src/`: Astro source - - `@types/`: TypeScript types. These are centralized to cut down on circular dependencies + - `types/`: TypeScript types. These are centralized to cut down on circular dependencies - `cli/`: Code that powers the `astro` CLI command - `core/`: Code that executes **in the top-level scope** (in Node). Within, you’ll find code that powers the `astro build` and `astro dev` commands, as well as top-level SSR code. - `runtime/`: Code that executes **in different scopes** (i.e. not in a pure Node context). You’ll have to think about code differently here. diff --git a/README.md b/README.md index 8cc1dd3780c6..62589bac80ce 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ Join us on [Discord](https://astro.build/chat) to meet other maintainers. We'll | [@astrojs/alpinejs](packages/integrations/alpinejs) | [![@astrojs/alpinejs version](https://img.shields.io/npm/v/@astrojs/alpinejs.svg?label=%20)](packages/integrations/alpinejs/CHANGELOG.md) | | [@astrojs/mdx](packages/integrations/mdx) | [![@astrojs/mdx version](https://img.shields.io/npm/v/@astrojs/mdx.svg?label=%20)](packages/integrations/mdx/CHANGELOG.md) | | [@astrojs/db](packages/db) | [![@astrojs/db version](https://img.shields.io/npm/v/@astrojs/db.svg?label=%20)](packages/db/CHANGELOG.md) | -| [@astrojs/rss](packages/integrations/astro-rss) | [![@astrojs/rss version](https://img.shields.io/npm/v/@astrojs/rss.svg?label=%20)](packages/astro-rss/CHANGELOG.md) | +| [@astrojs/rss](packages/astro-rss) | [![@astrojs/rss version](https://img.shields.io/npm/v/@astrojs/rss.svg?label=%20)](packages/astro-rss/CHANGELOG.md) | +| [@astrojs/netlify](https://github.com/withastro/adapters/blob/main/packages/netlify) | [![@astrojs/netlify version](https://img.shields.io/npm/v/@astrojs/netlify.svg?label=%20)](https://github.com/withastro/adapters/blob/main/packages/netlify/CHANGELOG.md) | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6178/badge)](https://bestpractices.coreinfrastructure.org/projects/6178) diff --git a/benchmark/bench/memory.js b/benchmark/bench/memory.js index 34a4972f78b1..3a437b3fe357 100644 --- a/benchmark/bench/memory.js +++ b/benchmark/bench/memory.js @@ -18,7 +18,7 @@ export async function run(projectDir, outputFile) { const outputFilePath = fileURLToPath(outputFile); console.log('Building and benchmarking...'); - await execaCommand(`node --expose-gc --max_old_space_size=256 ${astroBin} build`, { + await execaCommand(`node --expose-gc --max_old_space_size=10000 ${astroBin} build --silent`, { cwd: root, stdio: 'inherit', env: { diff --git a/benchmark/make-project/image.jpg b/benchmark/make-project/image.jpg new file mode 100644 index 000000000000..80b8ea67b8e4 Binary files /dev/null and b/benchmark/make-project/image.jpg differ diff --git a/benchmark/make-project/markdown-cc1.js b/benchmark/make-project/markdown-cc1.js new file mode 100644 index 000000000000..6c83959601e3 --- /dev/null +++ b/benchmark/make-project/markdown-cc1.js @@ -0,0 +1,63 @@ +import fs from 'node:fs/promises'; +import { loremIpsumMd } from './_util.js'; + +/** + * @param {URL} projectDir + */ +export async function run(projectDir) { + await fs.rm(projectDir, { recursive: true, force: true }); + await fs.mkdir(new URL('./src/pages/blog', projectDir), { recursive: true }); + await fs.mkdir(new URL('./src/content/blog', projectDir), { recursive: true }); + await fs.copyFile(new URL('./image.jpg', import.meta.url), new URL('./src/image.jpg', projectDir)); + + const promises = []; + + + for (let i = 0; i < 10000; i++) { + const content = `\ +# Article ${i} + +${loremIpsumMd} + +![image ${i}](../../image.jpg) + + +`; + promises.push( + fs.writeFile(new URL(`./src/content/blog/article-${i}.md`, projectDir), content, 'utf-8') + ); + } + + + await fs.writeFile( + new URL(`./src/pages/blog/[...slug].astro`, projectDir), + `\ +--- +import { getCollection } from 'astro:content'; +export async function getStaticPaths() { + const blogEntries = await getCollection('blog'); + return blogEntries.map(entry => ({ + params: { slug: entry.slug }, props: { entry }, + })); +} +const { entry } = Astro.props; +const { Content } = await entry.render(); +--- +

{entry.data.title}

+ +`, + 'utf-8' + ); + + await Promise.all(promises); + + await fs.writeFile( + new URL('./astro.config.js', projectDir), + `\ +import { defineConfig } from 'astro/config'; + +export default defineConfig({ +});`, + 'utf-8' + ); +} diff --git a/benchmark/make-project/markdown-cc2.js b/benchmark/make-project/markdown-cc2.js new file mode 100644 index 000000000000..73c6afe7a8d0 --- /dev/null +++ b/benchmark/make-project/markdown-cc2.js @@ -0,0 +1,80 @@ +import fs from 'node:fs/promises'; +import { loremIpsumMd } from './_util.js'; + +/** + * @param {URL} projectDir + */ +export async function run(projectDir) { + await fs.rm(projectDir, { recursive: true, force: true }); + await fs.mkdir(new URL('./src/pages/blog', projectDir), { recursive: true }); + await fs.mkdir(new URL('./data/blog', projectDir), { recursive: true }); + await fs.mkdir(new URL('./src/content', projectDir), { recursive: true }); + await fs.copyFile(new URL('./image.jpg', import.meta.url), new URL('./image.jpg', projectDir)); + + const promises = []; + + for (let i = 0; i < 10000; i++) { + const content = `\ +# Article ${i} + +${loremIpsumMd} + +![image ${i}](../../image.jpg) + +`; + promises.push( + fs.writeFile(new URL(`./data/blog/article-${i}.md`, projectDir), content, 'utf-8') + ); + } + + await fs.writeFile( + new URL(`./src/content/config.ts`, projectDir), + /*ts */ ` + import { defineCollection, z } from 'astro:content'; + import { glob } from 'astro/loaders'; + + const blog = defineCollection({ + loader: glob({ pattern: '*', base: './data/blog' }), + }); + + export const collections = { blog } + + ` + ); + + await fs.writeFile( + new URL(`./src/pages/blog/[...slug].astro`, projectDir), + `\ +--- +import { getCollection, render } from 'astro:content'; +export async function getStaticPaths() { + const blogEntries = await getCollection('blog'); + return blogEntries.map(entry => ({ + params: { slug: entry.id }, props: { entry }, + })); +} +const { entry } = Astro.props; +const { Content } = await render(entry); + +--- +

{entry.data.title}

+ +`, + 'utf-8' + ); + + await Promise.all(promises); + + await fs.writeFile( + new URL('./astro.config.js', projectDir), + `\ +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + experimental: { + contentLayer: true + } +});`, + 'utf-8' + ); +} diff --git a/benchmark/make-project/mdx-cc1.js b/benchmark/make-project/mdx-cc1.js new file mode 100644 index 000000000000..98e1495d13e1 --- /dev/null +++ b/benchmark/make-project/mdx-cc1.js @@ -0,0 +1,66 @@ +import fs from 'node:fs/promises'; +import { loremIpsumMd } from './_util.js'; + +/** + * @param {URL} projectDir + */ +export async function run(projectDir) { + await fs.rm(projectDir, { recursive: true, force: true }); + await fs.mkdir(new URL('./src/pages/blog', projectDir), { recursive: true }); + await fs.mkdir(new URL('./src/content/blog', projectDir), { recursive: true }); + await fs.copyFile(new URL('./image.jpg', import.meta.url), new URL('./src/image.jpg', projectDir)); + + const promises = []; + + + for (let i = 0; i < 10000; i++) { + const content = `\ +# Article ${i} + +${loremIpsumMd} + +![image ${i}](../../image.jpg) + + +`; + promises.push( + fs.writeFile(new URL(`./src/content/blog/article-${i}.mdx`, projectDir), content, 'utf-8') + ); + } + + + await fs.writeFile( + new URL(`./src/pages/blog/[...slug].astro`, projectDir), + `\ +--- +import { getCollection } from 'astro:content'; +export async function getStaticPaths() { + const blogEntries = await getCollection('blog'); + return blogEntries.map(entry => ({ + params: { slug: entry.slug }, props: { entry }, + })); +} +const { entry } = Astro.props; +const { Content } = await entry.render(); +--- +

{entry.data.title}

+ +`, + 'utf-8' + ); + + await Promise.all(promises); + + await fs.writeFile( + new URL('./astro.config.js', projectDir), + `\ +import { defineConfig } from 'astro/config'; + +import mdx from '@astrojs/mdx'; + +export default defineConfig({ + integrations: [mdx()], +});`, + 'utf-8' + ); +} diff --git a/benchmark/make-project/mdx-cc2.js b/benchmark/make-project/mdx-cc2.js new file mode 100644 index 000000000000..c08c2fb9fe23 --- /dev/null +++ b/benchmark/make-project/mdx-cc2.js @@ -0,0 +1,83 @@ +import fs from 'node:fs/promises'; +import { loremIpsumMd } from './_util.js'; + +/** + * @param {URL} projectDir + */ +export async function run(projectDir) { + await fs.rm(projectDir, { recursive: true, force: true }); + await fs.mkdir(new URL('./src/pages/blog', projectDir), { recursive: true }); + await fs.mkdir(new URL('./data/blog', projectDir), { recursive: true }); + await fs.mkdir(new URL('./src/content', projectDir), { recursive: true }); + await fs.copyFile(new URL('./image.jpg', import.meta.url), new URL('./image.jpg', projectDir)); + + const promises = []; + + for (let i = 0; i < 10000; i++) { + const content = `\ +# Article ${i} + +${loremIpsumMd} + +![image ${i}](../../image.jpg) + +`; + promises.push( + fs.writeFile(new URL(`./data/blog/article-${i}.mdx`, projectDir), content, 'utf-8') + ); + } + + await fs.writeFile( + new URL(`./src/content/config.ts`, projectDir), + /*ts */ ` + import { defineCollection, z } from 'astro:content'; + import { glob } from 'astro/loaders'; + + const blog = defineCollection({ + loader: glob({ pattern: '*', base: './data/blog' }), + }); + + export const collections = { blog } + + ` + ); + + await fs.writeFile( + new URL(`./src/pages/blog/[...slug].astro`, projectDir), + `\ +--- +import { getCollection, render } from 'astro:content'; +export async function getStaticPaths() { + const blogEntries = await getCollection('blog'); + return blogEntries.map(entry => ({ + params: { slug: entry.id }, props: { entry }, + })); +} +const { entry } = Astro.props; +const { Content } = await render(entry); + +--- +

{entry.data.title}

+ +`, + 'utf-8' + ); + + await Promise.all(promises); + + await fs.writeFile( + new URL('./astro.config.js', projectDir), + `\ +import { defineConfig } from 'astro/config'; + +import mdx from '@astrojs/mdx'; + +export default defineConfig({ + integrations: [mdx()], + experimental: { + contentLayer: true + } +});`, + 'utf-8' + ); +} diff --git a/benchmark/package.json b/benchmark/package.json index 5f7206242b45..d56a6d1b0673 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -16,6 +16,7 @@ "markdown-table": "^3.0.3", "mri": "^1.2.0", "port-authority": "^2.0.1", - "pretty-bytes": "^6.1.1" + "pretty-bytes": "^6.1.1", + "sharp": "^0.33.3" } } diff --git a/biome.json b/biome.json index 8effd9cfceb8..67e2a4620e0f 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.8.1/schema.json", + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "files": { "ignore": [ "vendor", @@ -26,7 +26,15 @@ "organizeImports": { "enabled": true }, - "linter": { "enabled": false }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "style": { + "useNodejsImportProtocol": "error" + } + } + }, "javascript": { "formatter": { "trailingCommas": "all", diff --git a/examples/basics/package.json b/examples/basics/package.json index b523cad78ba0..be4bf81ad1c6 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/basics/src/env.d.ts b/examples/basics/src/env.d.ts index f964fe0cffd8..9bc5cb41c24e 100644 --- a/examples/basics/src/env.d.ts +++ b/examples/basics/src/env.d.ts @@ -1 +1 @@ -/// +/// \ No newline at end of file diff --git a/examples/blog/package.json b/examples/blog/package.json index 21a14e62cf87..2b248d3a8d29 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -14,6 +14,6 @@ "@astrojs/mdx": "^3.1.3", "@astrojs/rss": "^4.0.7", "@astrojs/sitemap": "^3.1.6", - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/blog/src/env.d.ts b/examples/blog/src/env.d.ts index acef35f175aa..e16c13c6952a 100644 --- a/examples/blog/src/env.d.ts +++ b/examples/blog/src/env.d.ts @@ -1,2 +1 @@ /// -/// diff --git a/examples/component/package.json b/examples/component/package.json index 711f47ab6ccc..3d2e2bead498 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json index 59f4d937ff9c..1c673234054c 100644 --- a/examples/container-with-vitest/package.json +++ b/examples/container-with-vitest/package.json @@ -12,14 +12,14 @@ "test": "vitest run" }, "dependencies": { - "astro": "^4.13.3", + "astro": "^4.14.2", "@astrojs/react": "^3.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", "vitest": "^2.0.5" }, "devDependencies": { - "@types/react-dom": "^18.3.0", - "@types/react": "^18.3.3" + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0" } } diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 6cd4bb07f8a6..03cfff656dbf 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.10", "alpinejs": "^3.14.1", - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/framework-alpine/src/env.d.ts b/examples/framework-alpine/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-alpine/src/env.d.ts +++ b/examples/framework-alpine/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 49e0ce9712b5..626962d02e99 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -18,12 +18,12 @@ "@astrojs/vue": "^4.5.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "astro": "^4.13.3", + "astro": "^4.14.2", "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/examples/framework-multiple/src/env.d.ts b/examples/framework-multiple/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-multiple/src/env.d.ts +++ b/examples/framework-multiple/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 577bf5a514d5..d7169d6cc2e9 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.5.1", "@preact/signals": "^1.3.0", - "astro": "^4.13.3", + "astro": "^4.14.2", "preact": "^10.23.1" } } diff --git a/examples/framework-preact/src/env.d.ts b/examples/framework-preact/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-preact/src/env.d.ts +++ b/examples/framework-preact/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 5ee3222ad928..4fbde09c6476 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -14,7 +14,7 @@ "@astrojs/react": "^3.6.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "astro": "^4.13.3", + "astro": "^4.14.2", "react": "^18.3.1", "react-dom": "^18.3.1" } diff --git a/examples/framework-react/src/env.d.ts b/examples/framework-react/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-react/src/env.d.ts +++ b/examples/framework-react/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index ae8ff13f124b..cc583f052e6e 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^4.4.1", - "astro": "^4.13.3", - "solid-js": "^1.8.19" + "astro": "^4.14.2", + "solid-js": "^1.8.20" } } diff --git a/examples/framework-solid/src/env.d.ts b/examples/framework-solid/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-solid/src/env.d.ts +++ b/examples/framework-solid/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index c8095dd91725..42a99cd2473a 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/svelte": "^5.7.0", - "astro": "^4.13.3", + "astro": "^4.14.2", "svelte": "^4.2.18" } } diff --git a/examples/framework-svelte/src/env.d.ts b/examples/framework-svelte/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-svelte/src/env.d.ts +++ b/examples/framework-svelte/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index df136b65e7a9..1bff3df25d21 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^4.5.0", - "astro": "^4.13.3", - "vue": "^3.4.35" + "astro": "^4.14.2", + "vue": "^3.4.37" } } diff --git a/examples/framework-vue/src/env.d.ts b/examples/framework-vue/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-vue/src/env.d.ts +++ b/examples/framework-vue/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index e47cf63d7caf..bc5ee46b2c2b 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/node": "^8.3.2", - "astro": "^4.13.3" + "@astrojs/node": "^8.3.3", + "astro": "^4.14.2" } } diff --git a/examples/hackernews/src/env.d.ts b/examples/hackernews/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/hackernews/src/env.d.ts +++ b/examples/hackernews/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/integration/package.json b/examples/integration/package.json index cd735e915fc9..d8225462a805 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 3a20bc13a485..db80ba2fe233 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -12,8 +12,8 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^8.3.2", - "astro": "^4.13.3", + "@astrojs/node": "^8.3.3", + "astro": "^4.14.2", "html-minifier": "^4.0.0" }, "devDependencies": { diff --git a/examples/middleware/src/env.d.ts b/examples/middleware/src/env.d.ts index 44f67965a3cc..74b9019e5746 100644 --- a/examples/middleware/src/env.d.ts +++ b/examples/middleware/src/env.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace App { interface Locals { user: { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index cb9e5b9f6891..129a0e1c68d6 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/minimal/src/env.d.ts b/examples/minimal/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/minimal/src/env.d.ts +++ b/examples/minimal/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index c84aae0b1f8e..14750d84118e 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/non-html-pages/src/env.d.ts b/examples/non-html-pages/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/non-html-pages/src/env.d.ts +++ b/examples/non-html-pages/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index faec95f81403..22e75e1419ea 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/portfolio/src/env.d.ts b/examples/portfolio/src/env.d.ts index acef35f175aa..e16c13c6952a 100644 --- a/examples/portfolio/src/env.d.ts +++ b/examples/portfolio/src/env.d.ts @@ -1,2 +1 @@ /// -/// diff --git a/examples/server-islands/package.json b/examples/server-islands/package.json index 899f4d4af0ee..a3bf732943f2 100644 --- a/examples/server-islands/package.json +++ b/examples/server-islands/package.json @@ -10,17 +10,17 @@ "astro": "astro" }, "devDependencies": { - "@astrojs/node": "^8.3.2", + "@astrojs/node": "^8.3.3", "@astrojs/react": "^3.6.2", "@astrojs/tailwind": "^5.1.0", "@fortawesome/fontawesome-free": "^6.6.0", "@tailwindcss/forms": "^0.5.7", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "astro": "^4.13.3", - "postcss": "^8.4.40", + "astro": "^4.14.2", + "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", - "tailwindcss": "^3.4.7" + "tailwindcss": "^3.4.9" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 530d4c9293ef..d35df0ec4c56 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -12,9 +12,9 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^8.3.2", + "@astrojs/node": "^8.3.3", "@astrojs/svelte": "^5.7.0", - "astro": "^4.13.3", + "astro": "^4.14.2", "svelte": "^4.2.18" } } diff --git a/examples/ssr/src/env.d.ts b/examples/ssr/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/ssr/src/env.d.ts +++ b/examples/ssr/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/starlog/package.json b/examples/starlog/package.json index cfcc76527b25..905df4a69dfe 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.13.3", + "astro": "^4.14.2", "sass": "^1.77.8", "sharp": "^0.33.3" } diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index 1c0d67f7f940..d08c29bca229 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -15,6 +15,6 @@ "./app": "./dist/app.js" }, "devDependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index 42b1401b05b2..7448b9ec0a8f 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@astrojs/tailwind": "^5.1.0", - "@astrojs/node": "^8.3.2", - "astro": "^4.13.3" + "@astrojs/node": "^8.3.3", + "astro": "^4.14.2" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index dbc01d6ba3d9..1af3bc63c152 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/markdoc": "^0.11.3", - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/with-markdoc/src/env.d.ts b/examples/with-markdoc/src/env.d.ts index acef35f175aa..e16c13c6952a 100644 --- a/examples/with-markdoc/src/env.d.ts +++ b/examples/with-markdoc/src/env.d.ts @@ -1,2 +1 @@ /// -/// diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 5dfb45ea0f3e..61f1253458f9 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/markdown-remark": "^5.2.0", - "astro": "^4.13.3", + "astro": "^4.14.2", "hast-util-select": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", diff --git a/examples/with-markdown-plugins/src/env.d.ts b/examples/with-markdown-plugins/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-markdown-plugins/src/env.d.ts +++ b/examples/with-markdown-plugins/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index 511e370f540a..e1486a376a88 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.13.3" + "astro": "^4.14.2" } } diff --git a/examples/with-markdown-shiki/src/env.d.ts b/examples/with-markdown-shiki/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-markdown-shiki/src/env.d.ts +++ b/examples/with-markdown-shiki/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 20bb7349c9a5..934959975cd8 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^3.1.3", "@astrojs/preact": "^3.5.1", - "astro": "^4.13.3", + "astro": "^4.14.2", "preact": "^10.23.1" } } diff --git a/examples/with-mdx/src/env.d.ts b/examples/with-mdx/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-mdx/src/env.d.ts +++ b/examples/with-mdx/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 8933b0bac495..b8db777b4225 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,8 +13,8 @@ "dependencies": { "@astrojs/preact": "^3.5.1", "@nanostores/preact": "^0.5.2", - "astro": "^4.13.3", - "nanostores": "^0.11.0", + "astro": "^4.14.2", + "nanostores": "^0.11.2", "preact": "^10.23.1" } } diff --git a/examples/with-nanostores/src/env.d.ts b/examples/with-nanostores/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-nanostores/src/env.d.ts +++ b/examples/with-nanostores/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index ae2329dd69bc..80ba691d86c0 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -14,10 +14,10 @@ "@astrojs/mdx": "^3.1.3", "@astrojs/tailwind": "^5.1.0", "@types/canvas-confetti": "^1.6.4", - "astro": "^4.13.3", + "astro": "^4.14.2", "autoprefixer": "^10.4.20", "canvas-confetti": "^1.9.3", - "postcss": "^8.4.40", - "tailwindcss": "^3.4.7" + "postcss": "^8.4.41", + "tailwindcss": "^3.4.9" } } diff --git a/examples/with-tailwindcss/src/env.d.ts b/examples/with-tailwindcss/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-tailwindcss/src/env.d.ts +++ b/examples/with-tailwindcss/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 738dac867d18..88956a94eb43 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^4.13.3", + "astro": "^4.14.2", "vitest": "^2.0.5" } } diff --git a/package.json b/package.json index 8417847d811f..850816b5e115 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "test:e2e:match": "cd packages/astro && pnpm playwright install chromium firefox && pnpm run test:e2e:match", "test:e2e:hosts": "turbo run test:hosted", "benchmark": "astro-benchmark", - "lint": "eslint . --report-unused-disable-directives", + "lint": "biome lint && eslint . --report-unused-disable-directives", "version": "changeset version && node ./scripts/deps/update-example-versions.js && pnpm install --no-frozen-lockfile && pnpm run format", "preinstall": "npx only-allow pnpm" }, @@ -52,20 +52,20 @@ "astro-benchmark": "workspace:*" }, "devDependencies": { - "@astrojs/check": "^0.9.1", - "@biomejs/biome": "1.8.1", + "@astrojs/check": "^0.9.2", + "@biomejs/biome": "1.8.3", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.7", "@types/node": "^18.17.8", "esbuild": "^0.21.5", - "eslint": "^9.8.0", + "eslint": "^9.9.0", "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-regexp": "^2.6.0", "globby": "^14.0.2", "only-allow": "^1.2.1", "prettier": "^3.3.3", "prettier-plugin-astro": "^0.14.1", - "turbo": "^1.13.4", + "turbo": "^2.0.12", "typescript": "~5.5.4", "typescript-eslint": "^8.0.1" }, diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index c183b7afbb4a..5c93709a4b30 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,261 @@ # astro +## 4.14.2 + +### Patch Changes + +- [#11733](https://github.com/withastro/astro/pull/11733) [`391324d`](https://github.com/withastro/astro/commit/391324df969db71d1c7ca25c2ed14c9eb6eea5ee) Thanks [@bluwy](https://github.com/bluwy)! - Reverts back to `yargs-parser` package for CLI argument parsing + +## 4.14.1 + +### Patch Changes + +- [#11725](https://github.com/withastro/astro/pull/11725) [`6c1560f`](https://github.com/withastro/astro/commit/6c1560fb0d19ce659bc9f9090f8050254d5c03f3) Thanks [@ascorbic](https://github.com/ascorbic)! - Prevents content layer importing node builtins in runtime + +- [#11692](https://github.com/withastro/astro/pull/11692) [`35af73a`](https://github.com/withastro/astro/commit/35af73aace97a7cc898b9aa5040db8bc2ac62687) Thanks [@matthewp](https://github.com/matthewp)! - Prevent errant HTML from crashing server islands + + When an HTML minifier strips away the server island comment, the script can't correctly know where the end of the fallback content is. This makes it so that it simply doesn't remove any DOM in that scenario. This means the fallback isn't removed, but it also doesn't crash the browser. + +- [#11727](https://github.com/withastro/astro/pull/11727) [`3c2f93b`](https://github.com/withastro/astro/commit/3c2f93b66c6b8e9d2ab58e2cbe941c14ffab89b5) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a type issue when using the Content Layer in dev + +## 4.14.0 + +### Minor Changes + +- [#11657](https://github.com/withastro/astro/pull/11657) [`a23c69d`](https://github.com/withastro/astro/commit/a23c69d0d0bed229bee52a32e61f135f9ebf9122) Thanks [@bluwy](https://github.com/bluwy)! - Deprecates the option for route-generating files to export a dynamic value for `prerender`. Only static values are now supported (e.g. `export const prerender = true` or `= false`). This allows for better treeshaking and bundling configuration in the future. + + Adds a new [`"astro:route:setup"` hook](https://docs.astro.build/en/reference/integrations-reference/#astroroutesetup) to the Integrations API to allow you to dynamically set options for a route at build or request time through an integration, such as enabling [on-demand server rendering](https://docs.astro.build/en/guides/server-side-rendering/#opting-in-to-pre-rendering-in-server-mode). + + To migrate from a dynamic export to the new hook, update or remove any dynamic `prerender` exports from individual routing files: + + ```diff + // src/pages/blog/[slug].astro + - export const prerender = import.meta.env.PRERENDER + ``` + + Instead, create an integration with the `"astro:route:setup"` hook and update the route's `prerender` option: + + ```js + // astro.config.mjs + import { defineConfig } from 'astro/config'; + import { loadEnv } from 'vite'; + + export default defineConfig({ + integrations: [setPrerender()], + }); + + function setPrerender() { + const { PRERENDER } = loadEnv(process.env.NODE_ENV, process.cwd(), ''); + + return { + name: 'set-prerender', + hooks: { + 'astro:route:setup': ({ route }) => { + if (route.component.endsWith('/blog/[slug].astro')) { + route.prerender = PRERENDER; + } + }, + }, + }; + } + ``` + +- [#11360](https://github.com/withastro/astro/pull/11360) [`a79a8b0`](https://github.com/withastro/astro/commit/a79a8b0230b06ed32ce1802f2a5f84a6cf92dbe7) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds a new [`injectTypes()` utility](https://docs.astro.build/en/reference/integrations-reference/#injecttypes-options) to the Integration API and refactors how type generation works + + Use `injectTypes()` in the `astro:config:done` hook to inject types into your user's project by adding a new a `*.d.ts` file. + + The `filename` property will be used to generate a file at `/.astro/integrations//.d.ts` and must end with `".d.ts"`. + + The `content` property will create the body of the file, and must be valid TypeScript. + + Additionally, `injectTypes()` returns a URL to the normalized path so you can overwrite its content later on, or manipulate it in any way you want. + + ```js + // my-integration/index.js + export default { + name: 'my-integration', + 'astro:config:done': ({ injectTypes }) => { + injectTypes({ + filename: 'types.d.ts', + content: "declare module 'virtual:my-integration' {}", + }); + }, + }; + ``` + + Codegen has been refactored. Although `src/env.d.ts` will continue to work as is, we recommend you update it: + + ```diff + - /// + + /// + - /// + - /// + ``` + +- [#11605](https://github.com/withastro/astro/pull/11605) [`d3d99fb`](https://github.com/withastro/astro/commit/d3d99fba269da9e812e748539a11dfed785ef8a4) Thanks [@jcayzac](https://github.com/jcayzac)! - Adds a new property `meta` to Astro's [built-in `` component](https://docs.astro.build/en/reference/api-reference/#code-). + + This allows you to provide a value for [Shiki's `meta` attribute](https://shiki.style/guide/transformers#meta) to pass options to transformers. + + The following example passes an option to highlight lines 1 and 3 to Shiki's `tranformerMetaHighlight`: + + ```astro + --- + // src/components/Card.astro + import { Code } from 'astro:components'; + import { transformerMetaHighlight } from '@shikijs/transformers'; + --- + + + ``` + +- [#11360](https://github.com/withastro/astro/pull/11360) [`a79a8b0`](https://github.com/withastro/astro/commit/a79a8b0230b06ed32ce1802f2a5f84a6cf92dbe7) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds support for Intellisense features (e.g. code completion, quick hints) for your content collection entries in compatible editors under the `experimental.contentIntellisense` flag. + + ```js + import { defineConfig } from 'astro'; + + export default defineConfig({ + experimental: { + contentIntellisense: true, + }, + }); + ``` + + When enabled, this feature will generate and add JSON schemas to the `.astro` directory in your project. These files can be used by the Astro language server to provide Intellisense inside content files (`.md`, `.mdx`, `.mdoc`). + + Note that at this time, this also require enabling the `astro.content-intellisense` option in your editor, or passing the `contentIntellisense: true` initialization parameter to the Astro language server for editors using it directly. + + See the [experimental content Intellisense docs](https://docs.astro.build/en/reference/configuration-reference/#experimentalcontentintellisense) for more information updates as this feature develops. + +- [#11360](https://github.com/withastro/astro/pull/11360) [`a79a8b0`](https://github.com/withastro/astro/commit/a79a8b0230b06ed32ce1802f2a5f84a6cf92dbe7) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds experimental support for the Content Layer API. + + The new Content Layer API builds upon content collections, taking them beyond local files in `src/content/` and allowing you to fetch content from anywhere, including remote APIs. These new collections work alongside your existing content collections, and you can migrate them to the new API at your own pace. There are significant improvements to performance with large collections of local files. + + ### Getting started + + To try out the new Content Layer API, enable it in your Astro config: + + ```js + import { defineConfig } from 'astro'; + + export default defineConfig({ + experimental: { + contentLayer: true, + }, + }); + ``` + + You can then create collections in your `src/content/config.ts` using the Content Layer API. + + ### Loading your content + + The core of the new Content Layer API is the loader, a function that fetches content from a source and caches it in a local data store. Astro 4.14 ships with built-in `glob()` and `file()` loaders to handle your local Markdown, MDX, Markdoc, and JSON files: + + ```ts {3,7} + // src/content/config.ts + import { defineCollection, z } from 'astro:content'; + import { glob } from 'astro/loaders'; + + const blog = defineCollection({ + // The ID is a slug generated from the path of the file relative to `base` + loader: glob({ pattern: '**/*.md', base: './src/data/blog' }), + schema: z.object({ + title: z.string(), + description: z.string(), + publishDate: z.coerce.date(), + }), + }); + + export const collections = { blog }; + ``` + + You can then query using the existing content collections functions, and enjoy a simplified `render()` function to display your content: + + ```astro + --- + import { getEntry, render } from 'astro:content'; + + const post = await getEntry('blog', Astro.params.slug); + + const { Content } = await render(entry); + --- + + + ``` + + ### Creating a loader + + You're not restricted to the built-in loaders – we hope you'll try building your own. You can fetch content from anywhere and return an array of entries: + + ```ts + // src/content/config.ts + const countries = defineCollection({ + loader: async () => { + const response = await fetch('https://restcountries.com/v3.1/all'); + const data = await response.json(); + // Must return an array of entries with an id property, + // or an object with IDs as keys and entries as values + return data.map((country) => ({ + id: country.cca3, + ...country, + })); + }, + // optionally add a schema to validate the data and make it type-safe for users + // schema: z.object... + }); + + export const collections = { countries }; + ``` + + For more advanced loading logic, you can define an object loader. This allows incremental updates and conditional loading, and gives full access to the data store. It also allows a loader to define its own schema, including generating it dynamically based on the source API. See the [the Content Layer API RFC](https://github.com/withastro/roadmap/blob/content-layer/proposals/0047-content-layer.md#loaders) for more details. + + ### Sharing your loaders + + Loaders are better when they're shared. You can create a package that exports a loader and publish it to npm, and then anyone can use it on their site. We're excited to see what the community comes up with! To get started, [take a look at some examples](https://github.com/ascorbic/astro-loaders/). Here's how to load content using an RSS/Atom feed loader: + + ```ts + // src/content/config.ts + import { defineCollection } from 'astro:content'; + import { feedLoader } from '@ascorbic/feed-loader'; + + const podcasts = defineCollection({ + loader: feedLoader({ + url: 'https://feeds.99percentinvisible.org/99percentinvisible', + }), + }); + + export const collections = { podcasts }; + ``` + + ### Learn more + + To find out more about using the Content Layer API, check out [the Content Layer RFC](https://github.com/withastro/roadmap/blob/content-layer/proposals/0047-content-layer.md) and [share your feedback](https://github.com/withastro/roadmap/pull/982). + +### Patch Changes + +- [#11716](https://github.com/withastro/astro/pull/11716) [`f4057c1`](https://github.com/withastro/astro/commit/f4057c18c91f969e3e508545fb988aff94c3ff08) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes content types sync in dev + +- [#11645](https://github.com/withastro/astro/pull/11645) [`849e4c6`](https://github.com/withastro/astro/commit/849e4c6c23e61f7fa59f583419048b998bef2475) Thanks [@bluwy](https://github.com/bluwy)! - Refactors internally to use `node:util` `parseArgs` instead of `yargs-parser` + +- [#11712](https://github.com/withastro/astro/pull/11712) [`791d809`](https://github.com/withastro/astro/commit/791d809cbc22ed30dda1195ca026daa46a54b551) Thanks [@matthewp](https://github.com/matthewp)! - Fix mixed use of base + trailingSlash in Server Islands + +- [#11709](https://github.com/withastro/astro/pull/11709) [`3d8ae76`](https://github.com/withastro/astro/commit/3d8ae767fd4952af7332542b58fe98886eb2e99e) Thanks [@matthewp](https://github.com/matthewp)! - Fix adapter causing Netlify to break + +## 4.13.4 + +### Patch Changes + +- [#11678](https://github.com/withastro/astro/pull/11678) [`34da907`](https://github.com/withastro/astro/commit/34da907f3b4fb411024e6d28fdb291fa78116950) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a case where omitting a semicolon and line ending with carriage return - CRLF - in the `prerender` option could throw an error. + +- [#11535](https://github.com/withastro/astro/pull/11535) [`932bd2e`](https://github.com/withastro/astro/commit/932bd2eb07f1d7cb2c91e7e7d31fe84c919e302b) Thanks [@matthewp](https://github.com/matthewp)! - Encrypt server island props + + Server island props are now encrypted with a key generated at build-time. This is intended to prevent accidentally leaking secrets caused by exposing secrets through prop-passing. This is not intended to allow a server island to be trusted to skip authentication, or to protect against any other vulnerabilities other than secret leakage. + + See the RFC for an explanation: https://github.com/withastro/roadmap/blob/server-islands/proposals/server-islands.md#props-serialization + +- [#11655](https://github.com/withastro/astro/pull/11655) [`dc0a297`](https://github.com/withastro/astro/commit/dc0a297e2a4bea3db8310cc98c51b2f94ede5fde) Thanks [@billy-le](https://github.com/billy-le)! - Fixes Astro Actions `input` validation when using `default` values with a form input. + +- [#11689](https://github.com/withastro/astro/pull/11689) [`c7bda4c`](https://github.com/withastro/astro/commit/c7bda4cd672864babc3cebd19a2dd2e1af85c087) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue in the Astro actions, where the size of the generated cookie was exceeding the size permitted by the `Set-Cookie` header. + ## 4.13.3 ### Patch Changes @@ -7562,7 +7818,7 @@ ## 2.0.0 > **Note** -> This is a detailed changelog of all changes in Astro v2. +> This is a detailed changelog of all changes in Astro v2. > See our [upgrade guide](https://docs.astro.build/en/guides/upgrade-to/v2/) for an overview of steps needed to upgrade an existing project. ### Major Changes diff --git a/packages/astro/astro-jsx.d.ts b/packages/astro/astro-jsx.d.ts index b26507d1db58..128709dfc528 100644 --- a/packages/astro/astro-jsx.d.ts +++ b/packages/astro/astro-jsx.d.ts @@ -17,23 +17,21 @@ declare namespace astroHTML.JSX { children: {}; } - interface IntrinsicAttributes - extends AstroBuiltinProps, - AstroBuiltinAttributes, - AstroClientDirectives { + interface IntrinsicAttributes extends AstroComponentDirectives, AstroBuiltinAttributes { slot?: string | undefined | null; children?: Children; } - type AstroBuiltinProps = import('./dist/@types/astro.js').AstroBuiltinProps; - type AstroClientDirectives = import('./dist/@types/astro.js').AstroClientDirectives; - type AstroBuiltinAttributes = import('./dist/@types/astro.js').AstroBuiltinAttributes; - type AstroDefineVarsAttribute = import('./dist/@types/astro.js').AstroDefineVarsAttribute; - type AstroScriptAttributes = import('./dist/@types/astro.js').AstroScriptAttributes & + type AstroComponentDirectives = + import('./dist/types/public/elements.js').AstroComponentDirectives; + type AstroBuiltinAttributes = import('./dist/types/public/elements.js').AstroBuiltinAttributes; + type AstroDefineVarsAttribute = + import('./dist/types/public/elements.js').AstroDefineVarsAttribute; + type AstroScriptAttributes = import('./dist/types/public/elements.js').AstroScriptAttributes & AstroDefineVarsAttribute; - type AstroStyleAttributes = import('./dist/@types/astro.js').AstroStyleAttributes & + type AstroStyleAttributes = import('./dist/types/public/elements.js').AstroStyleAttributes & AstroDefineVarsAttribute; - type AstroSlotAttributes = import('./dist/@types/astro.js').AstroSlotAttributes; + type AstroSlotAttributes = import('./dist/types/public/elements.js').AstroSlotAttributes; // This is an unfortunate use of `any`, but unfortunately we can't make a type that works for every framework // without importing every single framework's types (which comes with its own set of problems). diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index ed5c1b894d28..796bf3f33deb 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -2,12 +2,6 @@ /// /// -// eslint-disable-next-line @typescript-eslint/no-namespace -declare namespace App { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Locals {} -} - interface ImportMetaEnv { /** * The prefix for Astro-generated asset links if the build.assetsPrefix config option is set. This can be used to create asset links not handled by Astro. @@ -52,7 +46,7 @@ declare module 'astro:assets' { getImage: ( options: import('./dist/assets/types.js').UnresolvedImageTransform, ) => Promise; - imageConfig: import('./dist/@types/astro.js').AstroConfig['image']; + imageConfig: import('./dist/types/public/config.js').AstroConfig['image']; getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService; inferRemoteSize: typeof import('./dist/assets/utils/index.js').inferRemoteSize; Image: typeof import('./components/Image.astro').default; @@ -172,7 +166,7 @@ declare module 'astro:components' { export * from 'astro/components'; } -type MD = import('./dist/@types/astro.js').MarkdownInstance>; +type MD = import('./dist/types/public/content.js').MarkdownInstance>; interface ExportedMarkdownModuleEntities { frontmatter: MD['frontmatter']; file: MD['file']; @@ -191,7 +185,6 @@ declare module '*.md' { file, url, getHeadings, - getHeaders, Content, rawContent, compiledContent, @@ -206,7 +199,6 @@ declare module '*.markdown' { file, url, getHeadings, - getHeaders, Content, rawContent, compiledContent, @@ -221,7 +213,6 @@ declare module '*.mkdn' { file, url, getHeadings, - getHeaders, Content, rawContent, compiledContent, @@ -236,7 +227,6 @@ declare module '*.mkd' { file, url, getHeadings, - getHeaders, Content, rawContent, compiledContent, @@ -251,7 +241,6 @@ declare module '*.mdwn' { file, url, getHeadings, - getHeaders, Content, rawContent, compiledContent, @@ -266,7 +255,6 @@ declare module '*.mdown' { file, url, getHeadings, - getHeaders, Content, rawContent, compiledContent, @@ -275,7 +263,7 @@ declare module '*.mdown' { } declare module '*.mdx' { - type MDX = import('./dist/@types/astro.js').MDXInstance>; + type MDX = import('./dist/types/public/content.js').MDXInstance>; export const frontmatter: MDX['frontmatter']; export const file: MDX['file']; @@ -288,7 +276,7 @@ declare module '*.mdx' { } declare module 'astro:ssr-manifest' { - export const manifest: import('./dist/@types/astro.js').SSRManifest; + export const manifest: import('./dist/types/public/internal.js').SSRManifest; } // Everything below are Vite's types (apart from image types, which are in `client.d.ts`) diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index 0cc639d7d577..8818b2ae0d27 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -23,6 +23,13 @@ interface Props extends Omit, 'lang'> { * @default "plaintext" */ lang?: BuiltinLanguage | SpecialLanguage | LanguageRegistration; + /** + * A metastring to pass to the highlighter. + * Allows passing information to transformers: https://shiki.style/guide/transformers#meta + * + * @default undefined + */ + meta?: string; /** * The styling theme. * Supports all themes listed here: https://shiki.style/themes @@ -72,6 +79,7 @@ interface Props extends Omit, 'lang'> { const { code, lang = 'plaintext', + meta, theme = 'github-dark', themes = {}, defaultColor = 'light', @@ -110,6 +118,7 @@ const highlighter = await getCachedHighlighter({ const html = await highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, { inline, + meta, attributes: rest as any, }); --- diff --git a/packages/astro/components/Picture.astro b/packages/astro/components/Picture.astro index c85548404d29..6686faf15bbe 100644 --- a/packages/astro/components/Picture.astro +++ b/packages/astro/components/Picture.astro @@ -1,7 +1,7 @@ --- import { type LocalImageProps, type RemoteImageProps, getImage } from 'astro:assets'; import * as mime from 'mrmime'; -import type { GetImageResult, ImageOutputFormat } from '../dist/@types/astro'; +import type { GetImageResult, ImageOutputFormat } from '../dist/types/public/index.js'; import { isESMImportedImage, resolveSrc } from '../dist/assets/utils/imageKind'; import { AstroError, AstroErrorData } from '../dist/core/errors/index.js'; import type { HTMLAttributes } from '../types'; diff --git a/packages/astro/config.d.ts b/packages/astro/config.d.ts index 9f4c6bfd112d..675c783a191d 100644 --- a/packages/astro/config.d.ts +++ b/packages/astro/config.d.ts @@ -1,8 +1,8 @@ type ViteUserConfig = import('vite').UserConfig; type ViteUserConfigFn = import('vite').UserConfigFn; -type AstroUserConfig = import('./dist/@types/astro.js').AstroUserConfig; -type AstroInlineConfig = import('./dist/@types/astro.js').AstroInlineConfig; -type ImageServiceConfig = import('./dist/@types/astro.js').ImageServiceConfig; +type AstroUserConfig = import('./dist/types/public/config.js').AstroUserConfig; +type AstroInlineConfig = import('./dist/types/public/config.js').AstroInlineConfig; +type ImageServiceConfig = import('./dist/types/public/config.js').ImageServiceConfig; type SharpImageServiceConfig = import('./dist/assets/services/sharp.js').SharpImageServiceConfig; type EnvField = typeof import('./dist/env/config.js').envField; @@ -42,4 +42,4 @@ export function passthroughImageService(): ImageServiceConfig; /** * Return a valid env field to use in this Astro config for `experimental.env.schema`. */ -export const envField: EnvField; +export declare const envField: EnvField; diff --git a/packages/astro/e2e/fixtures/actions-blog/package.json b/packages/astro/e2e/fixtures/actions-blog/package.json index 35e036ee01ff..311b7a37884f 100644 --- a/packages/astro/e2e/fixtures/actions-blog/package.json +++ b/packages/astro/e2e/fixtures/actions-blog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/check": "^0.9.1", + "@astrojs/check": "^0.9.2", "@astrojs/db": "workspace:*", "@astrojs/node": "workspace:*", "@astrojs/react": "workspace:*", diff --git a/packages/astro/e2e/fixtures/actions-react-19/package.json b/packages/astro/e2e/fixtures/actions-react-19/package.json index d67b081e23ae..a0f446eb9591 100644 --- a/packages/astro/e2e/fixtures/actions-react-19/package.json +++ b/packages/astro/e2e/fixtures/actions-react-19/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/check": "^0.9.1", + "@astrojs/check": "^0.9.2", "@astrojs/db": "workspace:*", "@astrojs/node": "workspace:*", "@astrojs/react": "workspace:*", diff --git a/packages/astro/e2e/fixtures/astro-envs/package.json b/packages/astro/e2e/fixtures/astro-envs/package.json index a2e272e8b2db..bc11078b8c65 100644 --- a/packages/astro/e2e/fixtures/astro-envs/package.json +++ b/packages/astro/e2e/fixtures/astro-envs/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/vue": "workspace:*", "astro": "workspace:*", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/client-only/package.json b/packages/astro/e2e/fixtures/client-only/package.json index 6af924c3d4b7..2a1ce6d7f157 100644 --- a/packages/astro/e2e/fixtures/client-only/package.json +++ b/packages/astro/e2e/fixtures/client-only/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/errors/package.json b/packages/astro/e2e/fixtures/errors/package.json index e6216ea15b79..2bd6a5fd3bbb 100644 --- a/packages/astro/e2e/fixtures/errors/package.json +++ b/packages/astro/e2e/fixtures/errors/package.json @@ -13,8 +13,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "sass": "^1.77.8", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/package.json b/packages/astro/e2e/fixtures/multiple-frameworks/package.json index 5fd1905f5800..8bf507751f47 100644 --- a/packages/astro/e2e/fixtures/multiple-frameworks/package.json +++ b/packages/astro/e2e/fixtures/multiple-frameworks/package.json @@ -12,12 +12,12 @@ }, "dependencies": { "@webcomponents/template-shadowroot": "^0.2.1", - "lit": "^3.1.4", + "lit": "^3.2.0", "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/nested-in-preact/package.json b/packages/astro/e2e/fixtures/nested-in-preact/package.json index 37e20359ac36..855c013a997a 100644 --- a/packages/astro/e2e/fixtures/nested-in-preact/package.json +++ b/packages/astro/e2e/fixtures/nested-in-preact/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/nested-in-react/package.json b/packages/astro/e2e/fixtures/nested-in-react/package.json index 29d2a9e9a523..81f74f3ad4f9 100644 --- a/packages/astro/e2e/fixtures/nested-in-react/package.json +++ b/packages/astro/e2e/fixtures/nested-in-react/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/nested-in-solid/package.json b/packages/astro/e2e/fixtures/nested-in-solid/package.json index d2c44020704c..0d9b59c9d831 100644 --- a/packages/astro/e2e/fixtures/nested-in-solid/package.json +++ b/packages/astro/e2e/fixtures/nested-in-solid/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/package.json b/packages/astro/e2e/fixtures/nested-in-svelte/package.json index 31bec20c305f..9cc7bdb3063b 100644 --- a/packages/astro/e2e/fixtures/nested-in-svelte/package.json +++ b/packages/astro/e2e/fixtures/nested-in-svelte/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/nested-in-vue/package.json b/packages/astro/e2e/fixtures/nested-in-vue/package.json index 1985df63f95f..abbd45a703fb 100644 --- a/packages/astro/e2e/fixtures/nested-in-vue/package.json +++ b/packages/astro/e2e/fixtures/nested-in-vue/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/nested-recursive/package.json b/packages/astro/e2e/fixtures/nested-recursive/package.json index 7a39d16e2498..18a9d346aece 100644 --- a/packages/astro/e2e/fixtures/nested-recursive/package.json +++ b/packages/astro/e2e/fixtures/nested-recursive/package.json @@ -14,9 +14,9 @@ "preact": "^10.23.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.19", + "solid-js": "^1.8.20", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" }, "scripts": { "dev": "astro dev" diff --git a/packages/astro/e2e/fixtures/server-islands/astro.config.mjs b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs index 4bec97b9e1ad..2175a1bf8fe9 100644 --- a/packages/astro/e2e/fixtures/server-islands/astro.config.mjs +++ b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs @@ -9,7 +9,7 @@ export default defineConfig({ output: 'hybrid', adapter: nodejs({ mode: 'standalone' }), integrations: [react(), mdx()], - trailingSlash: 'always', + trailingSlash: process.env.TRAILING_SLASH ?? 'always', experimental: { serverIslands: true, } diff --git a/packages/astro/e2e/fixtures/server-islands/src/components/HTMLError.astro b/packages/astro/e2e/fixtures/server-islands/src/components/HTMLError.astro new file mode 100644 index 000000000000..91b1946539c4 --- /dev/null +++ b/packages/astro/e2e/fixtures/server-islands/src/components/HTMLError.astro @@ -0,0 +1 @@ +
diff --git a/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro index b7c376f517ad..5eab0dc4dfc3 100644 --- a/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro +++ b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro @@ -1,4 +1,6 @@ --- +const { secret } = Astro.props; ---

I am an island

+

{secret}

diff --git a/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro index 998d6c074082..eff5df25e940 100644 --- a/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro +++ b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro @@ -1,6 +1,7 @@ --- import Island from '../components/Island.astro'; import Self from '../components/Self.astro'; +import HTMLError from '../components/HTMLError.astro'; --- @@ -8,9 +9,20 @@ import Self from '../components/Self.astro'; - +

children

+ +
+ + + +
diff --git a/packages/astro/e2e/fixtures/solid-circular/package.json b/packages/astro/e2e/fixtures/solid-circular/package.json index 36bd98489293..4b52eaed797c 100644 --- a/packages/astro/e2e/fixtures/solid-circular/package.json +++ b/packages/astro/e2e/fixtures/solid-circular/package.json @@ -7,6 +7,6 @@ "astro": "workspace:*" }, "devDependencies": { - "solid-js": "^1.8.19" + "solid-js": "^1.8.20" } } diff --git a/packages/astro/e2e/fixtures/solid-component/package.json b/packages/astro/e2e/fixtures/solid-component/package.json index db9e2e995ba5..f7f4d2610e8f 100644 --- a/packages/astro/e2e/fixtures/solid-component/package.json +++ b/packages/astro/e2e/fixtures/solid-component/package.json @@ -6,6 +6,6 @@ "@astrojs/mdx": "workspace:*", "@astrojs/solid-js": "workspace:*", "astro": "workspace:*", - "solid-js": "^1.8.19" + "solid-js": "^1.8.20" } } diff --git a/packages/astro/e2e/fixtures/solid-recurse/package.json b/packages/astro/e2e/fixtures/solid-recurse/package.json index b099ccc50d23..fe2f588ee8c3 100644 --- a/packages/astro/e2e/fixtures/solid-recurse/package.json +++ b/packages/astro/e2e/fixtures/solid-recurse/package.json @@ -7,6 +7,6 @@ "astro": "workspace:*" }, "devDependencies": { - "solid-js": "^1.8.19" + "solid-js": "^1.8.20" } } diff --git a/packages/astro/e2e/fixtures/tailwindcss/package.json b/packages/astro/e2e/fixtures/tailwindcss/package.json index dc5b9c4b5a3a..0852f726143a 100644 --- a/packages/astro/e2e/fixtures/tailwindcss/package.json +++ b/packages/astro/e2e/fixtures/tailwindcss/package.json @@ -6,7 +6,7 @@ "@astrojs/tailwind": "workspace:*", "astro": "workspace:*", "autoprefixer": "^10.4.20", - "postcss": "^8.4.40", - "tailwindcss": "^3.4.7" + "postcss": "^8.4.41", + "tailwindcss": "^3.4.9" } } diff --git a/packages/astro/e2e/fixtures/view-transitions/package.json b/packages/astro/e2e/fixtures/view-transitions/package.json index 2223ccbb71a4..e50f5de5b7cc 100644 --- a/packages/astro/e2e/fixtures/view-transitions/package.json +++ b/packages/astro/e2e/fixtures/view-transitions/package.json @@ -11,6 +11,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "svelte": "^4.2.18", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/fixtures/vue-component/package.json b/packages/astro/e2e/fixtures/vue-component/package.json index aeaf4cef0a2f..dffb08551c56 100644 --- a/packages/astro/e2e/fixtures/vue-component/package.json +++ b/packages/astro/e2e/fixtures/vue-component/package.json @@ -6,6 +6,6 @@ "@astrojs/mdx": "workspace:*", "@astrojs/vue": "workspace:*", "astro": "workspace:*", - "vue": "^3.4.35" + "vue": "^3.4.37" } } diff --git a/packages/astro/e2e/server-islands.test.js b/packages/astro/e2e/server-islands.test.js index b036eaafa3f6..496cf229cc50 100644 --- a/packages/astro/e2e/server-islands.test.js +++ b/packages/astro/e2e/server-islands.test.js @@ -38,14 +38,49 @@ test.describe('Server islands', () => { await expect(el, 'element rendered').toBeVisible(); }); + test('Props are encrypted', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/base/')); + let el = page.locator('#secret'); + await expect(el).toHaveText('test'); + }); + test('Self imported module can server defer', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/base/')); let el = page.locator('.now'); await expect(el).toHaveCount(2); }); + + test("Missing server island start comment doesn't cause browser to lock up", async ({ + page, + astro, + }) => { + await page.goto(astro.resolveUrl('/base/')); + let el = page.locator('#first'); + await expect(el).toHaveCount(1); + }); }); + test.describe('Development - trailingSlash: ignore', () => { + let devServer; + + test.beforeAll(async ({ astro }) => { + process.env.TRAILING_SLASH = 'ignore'; + devServer = await astro.startDevServer(); + }); + + test.afterAll(async () => { + await devServer.stop(); + }); + + test('Load content from the server', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/base/')); + let el = page.locator('#island'); + + await expect(el, 'element rendered').toBeVisible(); + await expect(el, 'should have content').toHaveText('I am an island'); + }); + }); test.describe('Production', () => { let previewServer; @@ -69,5 +104,11 @@ test.describe('Server islands', () => { await expect(el, 'element rendered').toBeVisible(); await expect(el, 'should have content').toHaveText('I am an island'); }); + + test('Props are encrypted', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + let el = page.locator('#secret'); + await expect(el).toHaveText('test'); + }); }); }); diff --git a/packages/astro/env.d.ts b/packages/astro/env.d.ts index 876a29c60f49..d2a788db3d3f 100644 --- a/packages/astro/env.d.ts +++ b/packages/astro/env.d.ts @@ -4,7 +4,7 @@ // As such, if the typings you're trying to add should be available inside ex: React components, they should instead // be inside `client.d.ts` -type Astro = import('./dist/@types/astro.js').AstroGlobal; +type Astro = import('./dist/types/public/context.js').AstroGlobal; // We have to duplicate the description here because editors won't show the JSDoc comment from the imported type // However, they will for its properties, ex: Astro.request will show the AstroGlobal.request description diff --git a/packages/astro/index.d.ts b/packages/astro/index.d.ts index a9e679be17d9..7b7a236e7970 100644 --- a/packages/astro/index.d.ts +++ b/packages/astro/index.d.ts @@ -1,2 +1,2 @@ -export type * from './dist/@types/astro.js'; +export type * from './dist/types/public/index.js'; export * from './dist/core/index.js'; diff --git a/packages/astro/package.json b/packages/astro/package.json index 64ec709d59dc..9fac8c9b704a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "4.13.3", + "version": "4.14.2", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", @@ -68,6 +68,7 @@ "./assets/services/sharp": "./dist/assets/services/sharp.js", "./assets/services/squoosh": "./dist/assets/services/squoosh.js", "./assets/services/noop": "./dist/assets/services/noop.js", + "./loaders": "./dist/content/loaders/index.js", "./content/runtime": "./dist/content/runtime.js", "./content/runtime-assets": "./dist/content/runtime-assets.js", "./debug": "./components/Debug.astro", @@ -122,7 +123,7 @@ "test:node": "astro-scripts test \"test/**/*.test.js\"" }, "dependencies": { - "@astrojs/compiler": "^2.10.1", + "@astrojs/compiler": "^2.10.2", "@astrojs/internal-helpers": "workspace:*", "@astrojs/markdown-remark": "workspace:*", "@astrojs/telemetry": "workspace:*", @@ -132,6 +133,8 @@ "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", + "@oslojs/encoding": "^0.4.1", + "@rollup/pluginutils": "^5.1.0", "@types/babel__core": "^7.20.5", "@types/cookie": "^0.6.0", "acorn": "^8.12.1", @@ -162,7 +165,9 @@ "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.11", + "micromatch": "^4.0.7", "mrmime": "^2.0.0", + "neotraverse": "^0.6.9", "ora": "^8.0.1", "p-limit": "^6.1.0", "p-queue": "^8.0.1", @@ -177,19 +182,21 @@ "tsconfck": "^3.1.1", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2", - "vite": "^5.3.5", + "vite": "^5.4.0", "vitefu": "^0.2.5", "which-pm": "^3.0.0", + "xxhash-wasm": "^1.0.2", "yargs-parser": "^21.1.1", "zod": "^3.23.8", - "zod-to-json-schema": "^3.23.2" + "zod-to-json-schema": "^3.23.2", + "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "devDependencies": { - "@astrojs/check": "^0.9.1", - "@playwright/test": "^1.45.3", + "@astrojs/check": "^0.9.2", + "@playwright/test": "^1.46.0", "@types/aria-query": "^5.0.4", "@types/babel__generator": "^7.6.8", "@types/babel__traverse": "^7.20.6", @@ -203,11 +210,12 @@ "@types/html-escaper": "^3.0.2", "@types/http-cache-semantics": "^4.0.4", "@types/js-yaml": "^4.0.9", + "@types/micromatch": "^4.0.9", "@types/prompts": "^2.4.9", "@types/semver": "^7.5.8", "@types/yargs-parser": "^21.0.3", "astro-scripts": "workspace:*", - "cheerio": "1.0.0-rc.12", + "cheerio": "1.0.0", "eol": "^0.9.1", "expect-type": "^0.19.0", "mdast-util-mdx": "^3.0.0", @@ -221,7 +229,7 @@ "remark-code-titles": "^0.1.2", "rollup": "^4.20.0", "sass": "^1.77.8", - "undici": "^6.19.5", + "undici": "^6.19.7", "unified": "^11.0.5" }, "engines": { diff --git a/packages/astro/performance/content-benchmark.mjs b/packages/astro/performance/content-benchmark.mjs index a710bd762500..98ef5f0eac32 100644 --- a/packages/astro/performance/content-benchmark.mjs +++ b/packages/astro/performance/content-benchmark.mjs @@ -1,8 +1,8 @@ /* eslint-disable no-console */ import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; import { bold, cyan, dim } from 'kleur/colors'; -import yargs from 'yargs-parser'; import { loadFixture } from '../test/test-utils.js'; import { generatePosts } from './scripts/generate-posts.mjs'; @@ -40,7 +40,7 @@ async function benchmark({ fixtures, templates, numPosts }) { // Test the build performance for content collections across multiple file types (md, mdx, mdoc) (async function benchmarkAll() { try { - const flags = yargs(process.argv.slice(2)); + const { values: flags } = parseArgs({ strict: false }); const test = Array.isArray(flags.test) ? flags.test : typeof flags.test === 'string' diff --git a/packages/astro/performance/package.json b/packages/astro/performance/package.json index c0833b952288..36d62528176b 100644 --- a/packages/astro/performance/package.json +++ b/packages/astro/performance/package.json @@ -11,8 +11,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@types/yargs-parser": "^21.0.3", - "kleur": "^4.1.5", - "yargs-parser": "^21.1.1" + "kleur": "^4.1.5" } } diff --git a/packages/astro/src/@types/README.md b/packages/astro/src/@types/README.md deleted file mode 100644 index 397329eaf218..000000000000 --- a/packages/astro/src/@types/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# `@types/` - -TypeScript definitions and types for untyped modules. - -[See CONTRIBUTING.md](../../../../CONTRIBUTING.md) for a code overview. diff --git a/packages/astro/src/@types/app.d.ts b/packages/astro/src/@types/app.d.ts deleted file mode 100644 index 1c0908bb8072..000000000000 --- a/packages/astro/src/@types/app.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Shared interfaces throughout the application that can be overridden by the user. - */ -declare namespace App { - /** - * Used by middlewares to store information, that can be read by the user via the global `Astro.locals` - */ - interface Locals {} -} diff --git a/packages/astro/src/actions/consts.ts b/packages/astro/src/actions/consts.ts index 6a55386d869a..beb8c45b641d 100644 --- a/packages/astro/src/actions/consts.ts +++ b/packages/astro/src/actions/consts.ts @@ -1,6 +1,6 @@ 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'; +export const ACTIONS_TYPES_FILE = 'astro/actions.d.ts'; export const VIRTUAL_INTERNAL_MODULE_ID = 'astro:internal-actions'; export const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = '\0astro:internal-actions'; export const NOOP_ACTIONS = '\0noop-actions'; diff --git a/packages/astro/src/actions/index.ts b/packages/astro/src/actions/index.ts index 04a911856f9c..61f5a00cc3ab 100644 --- a/packages/astro/src/actions/index.ts +++ b/packages/astro/src/actions/index.ts @@ -1,9 +1,10 @@ import fsMod from 'node:fs'; import type { Plugin as VitePlugin } from 'vite'; -import type { AstroIntegration, AstroSettings } from '../@types/astro.js'; import { ActionsWithoutServerOutputError } from '../core/errors/errors-data.js'; import { AstroError } from '../core/errors/errors.js'; import { isServerLikeOutput, viteID } from '../core/util.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { AstroIntegration } from '../types/public/integrations.js'; import { ACTIONS_TYPES_FILE, NOOP_ACTIONS, @@ -30,9 +31,6 @@ export default function astroActions({ throw error; } - const stringifiedActionsImport = JSON.stringify( - viteID(new URL('./actions', params.config.srcDir)), - ); params.updateConfig({ vite: { plugins: [vitePluginUserActions({ settings }), vitePluginActions(fs)], @@ -49,11 +47,18 @@ export default function astroActions({ entrypoint: 'astro/actions/runtime/middleware.js', order: 'post', }); + }, + 'astro:config:done': (params) => { + const stringifiedActionsImport = JSON.stringify( + viteID(new URL('./actions', params.config.srcDir)), + ); + settings.injectedTypes.push({ + filename: ACTIONS_TYPES_FILE, + content: `declare module "astro:actions" { + type Actions = typeof import(${stringifiedActionsImport})["server"]; - await typegen({ - stringifiedActionsImport, - root: params.config.root, - fs, + export const actions: Actions; +}`, }); }, }, @@ -119,24 +124,3 @@ const vitePluginActions = (fs: typeof fsMod): VitePlugin => ({ return code; }, }); - -async function typegen({ - stringifiedActionsImport, - root, - fs, -}: { - stringifiedActionsImport: string; - root: URL; - fs: typeof fsMod; -}) { - const content = `declare module "astro:actions" { - type Actions = typeof import(${stringifiedActionsImport})["server"]; - - export const actions: Actions; -}`; - - const dotAstroDir = new URL('.astro/', root); - - await fs.promises.mkdir(dotAstroDir, { recursive: true }); - await fs.promises.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 index f3f01800a837..b6f3221b5700 100644 --- a/packages/astro/src/actions/runtime/middleware.ts +++ b/packages/astro/src/actions/runtime/middleware.ts @@ -1,8 +1,9 @@ import { yellow } from 'kleur/colors'; -import type { APIContext, MiddlewareNext } from '../../@types/astro.js'; import { ActionQueryStringInvalidError } from '../../core/errors/errors-data.js'; import { AstroError } from '../../core/errors/errors.js'; import { defineMiddleware } from '../../core/middleware/index.js'; +import type { MiddlewareNext } from '../../types/public/common.js'; +import type { APIContext } from '../../types/public/context.js'; import { ACTION_QUERY_PARAMS } from '../consts.js'; import { formContentTypes, hasContentType } from './utils.js'; import { getAction } from './virtual/get-action.js'; diff --git a/packages/astro/src/actions/runtime/route.ts b/packages/astro/src/actions/runtime/route.ts index e4e2ad1ce5b1..7279a093d07e 100644 --- a/packages/astro/src/actions/runtime/route.ts +++ b/packages/astro/src/actions/runtime/route.ts @@ -1,4 +1,4 @@ -import type { APIRoute } from '../../@types/astro.js'; +import type { APIRoute } from '../../types/public/common.js'; import { formContentTypes, hasContentType } from './utils.js'; import { getAction } from './virtual/get-action.js'; import { serializeActionResult } from './virtual/shared.js'; diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts index 776171aa2ac5..199809d4e87c 100644 --- a/packages/astro/src/actions/runtime/utils.ts +++ b/packages/astro/src/actions/runtime/utils.ts @@ -1,4 +1,4 @@ -import type { APIContext } from '../../@types/astro.js'; +import type { APIContext } from '../../types/public/context.js'; export const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index 7aea22b2fc7f..9bc387d6b860 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -134,11 +134,25 @@ export function formDataToObject( const obj: Record = {}; for (const [key, baseValidator] of Object.entries(schema.shape)) { let validator = baseValidator; - while (validator instanceof z.ZodOptional || validator instanceof z.ZodNullable) { + + while ( + validator instanceof z.ZodOptional || + validator instanceof z.ZodNullable || + validator instanceof z.ZodDefault + ) { + // use default value when key is undefined + if (validator instanceof z.ZodDefault && !formData.has(key)) { + obj[key] = validator._def.defaultValue(); + } validator = validator._def.innerType; } - if (validator instanceof z.ZodBoolean) { - obj[key] = formData.has(key); + + if (!formData.has(key) && key in obj) { + // continue loop if form input is not found and default value is set + continue; + } else if (validator instanceof z.ZodBoolean) { + const val = formData.get(key); + obj[key] = val === 'true' ? true : val === 'false' ? false : formData.has(key); } else if (validator instanceof z.ZodArray) { obj[key] = handleFormDataGetAll(key, formData, validator); } else { diff --git a/packages/astro/src/actions/runtime/virtual/shared.ts b/packages/astro/src/actions/runtime/virtual/shared.ts index 57dc404496a5..d792a9af55af 100644 --- a/packages/astro/src/actions/runtime/virtual/shared.ts +++ b/packages/astro/src/actions/runtime/virtual/shared.ts @@ -213,6 +213,9 @@ export type SerializedActionResult = export function serializeActionResult(res: SafeResult): SerializedActionResult { if (res.error) { + if (import.meta.env?.DEV) { + actionResultErrorStack.set(res.error.stack); + } return { type: 'error', status: res.error.status, @@ -220,7 +223,6 @@ export function serializeActionResult(res: SafeResult): SerializedActi body: JSON.stringify({ ...res.error, message: res.error.message, - stack: import.meta.env.PROD ? undefined : res.error.stack, }), }; } @@ -243,7 +245,16 @@ export function serializeActionResult(res: SafeResult): SerializedActi export function deserializeActionResult(res: SerializedActionResult): SafeResult { if (res.type === 'error') { - return { error: ActionError.fromJson(JSON.parse(res.body)), data: undefined }; + if (import.meta.env?.PROD) { + return { error: ActionError.fromJson(JSON.parse(res.body)), data: undefined }; + } else { + const error = ActionError.fromJson(JSON.parse(res.body)); + error.stack = actionResultErrorStack.get(); + return { + error, + data: undefined, + }; + } } if (res.type === 'empty') { return { data: undefined, error: undefined }; @@ -255,3 +266,16 @@ export function deserializeActionResult(res: SerializedActionResult): SafeResult error: undefined, }; } + +// in-memory singleton to save the stack trace +const actionResultErrorStack = (function actionResultErrorStackFn() { + let errorStack: string | undefined; + return { + set(stack: string | undefined) { + errorStack = stack; + }, + get() { + return errorStack; + }, + }; +})(); diff --git a/packages/astro/src/actions/utils.ts b/packages/astro/src/actions/utils.ts index 5cf78626600f..f1cab8e430f2 100644 --- a/packages/astro/src/actions/utils.ts +++ b/packages/astro/src/actions/utils.ts @@ -1,6 +1,6 @@ -import type { APIContext } from '../@types/astro.js'; +import type { APIContext } from '../types/public/context.js'; import type { Locals } from './runtime/middleware.js'; -import { type ActionAPIContext } from './runtime/utils.js'; +import type { ActionAPIContext } from './runtime/utils.js'; import { deserializeActionResult, getActionQueryString } from './runtime/virtual/shared.js'; export function hasActionPayload(locals: APIContext['locals']): locals is Locals { diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts index fcc19f4f53a9..1c4866592828 100644 --- a/packages/astro/src/assets/build/generate.ts +++ b/packages/astro/src/assets/build/generate.ts @@ -2,7 +2,6 @@ import fs, { readFileSync } from 'node:fs'; import { basename } from 'node:path/posix'; import { dim, green } from 'kleur/colors'; import type PQueue from 'p-queue'; -import type { AstroConfig } from '../../@types/astro.js'; import { getOutDirWithinCwd } from '../../core/build/common.js'; import type { BuildPipeline } from '../../core/build/pipeline.js'; import { getTimeStat } from '../../core/build/util.js'; @@ -12,6 +11,7 @@ import type { Logger } from '../../core/logger/core.js'; import { isRemotePath, removeLeadingForwardSlash } from '../../core/path.js'; import { isServerLikeOutput } from '../../core/util.js'; import type { MapValue } from '../../type-utils.js'; +import type { AstroConfig } from '../../types/public/config.js'; import { getConfiguredImageService } from '../internal.js'; import type { LocalImageService } from '../services/service.js'; import type { AssetsGlobalStaticImagesList, ImageMetadata, ImageTransform } from '../types.js'; diff --git a/packages/astro/src/assets/endpoint/config.ts b/packages/astro/src/assets/endpoint/config.ts index 07cfe8faecbb..ff9dcc79a807 100644 --- a/packages/astro/src/assets/endpoint/config.ts +++ b/packages/astro/src/assets/endpoint/config.ts @@ -1,4 +1,4 @@ -import type { AstroSettings } from '../../@types/astro.js'; +import type { AstroSettings } from '../../types/astro.js'; export function injectImageEndpoint(settings: AstroSettings, mode: 'dev' | 'build') { const endpointEntrypoint = diff --git a/packages/astro/src/assets/endpoint/generic.ts b/packages/astro/src/assets/endpoint/generic.ts index 5238f3721a4d..d453787fd87e 100644 --- a/packages/astro/src/assets/endpoint/generic.ts +++ b/packages/astro/src/assets/endpoint/generic.ts @@ -2,7 +2,7 @@ import { imageConfig } from 'astro:assets'; import { isRemotePath } from '@astrojs/internal-helpers/path'; import * as mime from 'mrmime'; -import type { APIRoute } from '../../@types/astro.js'; +import type { APIRoute } from '../../types/public/common.js'; import { getConfiguredImageService } from '../internal.js'; import { etag } from '../utils/etag.js'; import { isRemoteAllowed } from '../utils/remotePattern.js'; diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts index 4c50fe95fdb3..04275652cde2 100644 --- a/packages/astro/src/assets/endpoint/node.ts +++ b/packages/astro/src/assets/endpoint/node.ts @@ -1,3 +1,4 @@ +import { readFile } from 'node:fs/promises'; /* eslint-disable no-console */ import os from 'node:os'; import { isAbsolute } from 'node:path'; @@ -5,9 +6,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; // @ts-expect-error import { assetsDir, imageConfig, outDir } from 'astro:assets'; import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path'; -import { readFile } from 'fs/promises'; import * as mime from 'mrmime'; -import type { APIRoute } from '../../@types/astro.js'; +import type { APIRoute } from '../../types/public/common.js'; import { getConfiguredImageService } from '../internal.js'; import { etag } from '../utils/etag.js'; import { isRemoteAllowed } from '../utils/remotePattern.js'; diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index 38afbf19fc9b..9a5926e8d255 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -1,5 +1,5 @@ -import type { AstroConfig } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import type { AstroConfig } from '../types/public/config.js'; import { DEFAULT_HASH_PROPS } from './consts.js'; import { type ImageService, isLocalService } from './services/service.js'; import { diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index 9cd6d0ecba42..f798e1dfd142 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -1,6 +1,6 @@ -import type { AstroConfig } from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import { isRemotePath, joinPaths } from '../../core/path.js'; +import type { AstroConfig } from '../../types/public/config.js'; import { DEFAULT_HASH_PROPS, DEFAULT_OUTPUT_FORMAT, VALID_SUPPORTED_FORMATS } from '../consts.js'; import type { ImageOutputFormat, ImageTransform, UnresolvedSrcSetValue } from '../types.js'; import { isESMImportedImage } from '../utils/imageKind.js'; diff --git a/packages/astro/src/assets/utils/remotePattern.ts b/packages/astro/src/assets/utils/remotePattern.ts index 5feefb89fe73..d3e832573a7f 100644 --- a/packages/astro/src/assets/utils/remotePattern.ts +++ b/packages/astro/src/assets/utils/remotePattern.ts @@ -1,5 +1,5 @@ import { isRemotePath } from '@astrojs/internal-helpers/path'; -import type { AstroConfig } from '../../@types/astro.js'; +import type { AstroConfig } from '../../types/public/config.js'; export type RemotePattern = { hostname?: string; diff --git a/packages/astro/src/assets/utils/resolveImports.ts b/packages/astro/src/assets/utils/resolveImports.ts new file mode 100644 index 000000000000..8d147552ece5 --- /dev/null +++ b/packages/astro/src/assets/utils/resolveImports.ts @@ -0,0 +1,40 @@ +import { isRemotePath, removeBase } from '@astrojs/internal-helpers/path'; +import { CONTENT_IMAGE_FLAG, IMAGE_IMPORT_PREFIX } from '../../content/consts.js'; +import { shorthash } from '../../runtime/server/shorthash.js'; +import { VALID_INPUT_FORMATS } from '../consts.js'; + +/** + * Resolves an image src from a content file (such as markdown) to a module ID or import that can be resolved by Vite. + * + * @param imageSrc The src attribute of an image tag + * @param filePath The path to the file that contains the imagem relative to the site root + * @returns A module id of the image that can be rsolved by Vite, or undefined if it is not a local image + */ +export function imageSrcToImportId(imageSrc: string, filePath: string): string | undefined { + // If the import is coming from the data store it will have a special prefix to identify it + // as an image import. We remove this prefix so that we can resolve the image correctly. + imageSrc = removeBase(imageSrc, IMAGE_IMPORT_PREFIX); + + // We only care about local imports + if (isRemotePath(imageSrc) || imageSrc.startsWith('/')) { + return; + } + // We only care about images + const ext = imageSrc.split('.').at(-1) as (typeof VALID_INPUT_FORMATS)[number] | undefined; + if (!ext || !VALID_INPUT_FORMATS.includes(ext)) { + return; + } + + // The import paths are relative to the content (md) file, but when it's actually resolved it will + // be in a single assets file, so relative paths will no longer work. To deal with this we use + // a query parameter to store the original path to the file and append a query param flag. + // This allows our Vite plugin to intercept the import and resolve the path relative to the + // importer and get the correct full path for the imported image. + + const params = new URLSearchParams(CONTENT_IMAGE_FLAG); + params.set('importer', filePath); + return `${imageSrc}?${params.toString()}`; +} + +export const importIdToSymbolName = (importId: string) => + `__ASTRO_IMAGE_IMPORT_${shorthash(importId)}`; diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 4c9314e85fad..57bbf847a4ff 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -2,7 +2,6 @@ import { extname } from 'node:path'; import MagicString from 'magic-string'; import type * as vite from 'vite'; import { normalizePath } from 'vite'; -import type { AstroPluginOptions, AstroSettings, ImageTransform } from '../@types/astro.js'; import { extendManualChunks } from '../core/build/plugins/util.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { @@ -13,7 +12,9 @@ import { removeQueryString, } from '../core/path.js'; import { isServerLikeOutput } from '../core/util.js'; +import type { AstroPluginOptions, AstroSettings } from '../types/astro.js'; import { VALID_INPUT_FORMATS, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js'; +import type { ImageTransform } from './types.js'; import { getAssetsPrefix } from './utils/getAssetsPrefix.js'; import { isESMImportedImage } from './utils/imageKind.js'; import { emitESMImage } from './utils/node/emitAsset.js'; diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index 7d33fe33a5be..f710184d2cb9 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -9,7 +9,6 @@ import ora from 'ora'; import preferredPM from 'preferred-pm'; import prompts from 'prompts'; import maxSatisfying from 'semver/ranges/max-satisfying.js'; -import type yargs from 'yargs-parser'; import { loadTSConfig, resolveConfig, @@ -29,14 +28,14 @@ import { appendForwardSlash } from '../../core/path.js'; import { apply as applyPolyfill } from '../../core/polyfill.js'; import { ensureProcessNodeEnv, parseNpmName } from '../../core/util.js'; import { eventCliSession, telemetry } from '../../events/index.js'; -import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; import { fetchPackageJson, fetchPackageVersions } from '../install-package.js'; import { generate, parse, t, visit } from './babel.js'; import { ensureImport } from './imports.js'; import { wrapDefaultExport } from './wrapper.js'; interface AddOptions { - flags: yargs.Arguments; + flags: Flags; } interface IntegrationInfo { @@ -143,7 +142,7 @@ export async function add(names: string[], { flags }: AddOptions) { } // Some packages might have a common alias! We normalize those here. - const cwd = flags.root; + const cwd = inlineConfig.root; const logger = createLoggerFromFlags(flags); const integrationNames = names.map((name) => (ALIASES.has(name) ? ALIASES.get(name)! : name)); const integrations = await validateIntegrations(integrationNames); @@ -249,7 +248,7 @@ export async function add(names: string[], { flags }: AddOptions) { const rawConfigPath = await resolveConfigPath({ root: rootPath, - configFile: flags.config, + configFile: inlineConfig.configFile, fs: fsMod, }); let configURL = rawConfigPath ? pathToFileURL(rawConfigPath) : undefined; @@ -580,7 +579,7 @@ async function updateAstroConfig({ }: { configURL: URL; ast: t.File; - flags: yargs.Arguments; + flags: Flags; logger: Logger; logAdapterInstructions: boolean; }): Promise { @@ -717,7 +716,7 @@ async function tryToInstallIntegrations({ }: { integrations: IntegrationInfo[]; cwd?: string; - flags: yargs.Arguments; + flags: Flags; logger: Logger; }): Promise { const installCommand = await getInstallIntegrationsCommand({ integrations, cwd, logger }); @@ -893,7 +892,7 @@ async function updateTSConfig( cwd = process.cwd(), logger: Logger, integrationsInfo: IntegrationInfo[], - flags: yargs.Arguments, + flags: Flags, ): Promise { const integrations = integrationsInfo.map( (integration) => integration.id as frameworkWithTSSettings, @@ -996,7 +995,7 @@ function parseIntegrationName(spec: string) { return { scope, name, tag }; } -async function askToContinue({ flags }: { flags: yargs.Arguments }): Promise { +async function askToContinue({ flags }: { flags: Flags }): Promise { if (flags.yes || flags.y) return true; const response = await prompts({ @@ -1038,7 +1037,7 @@ function getDiffContent(input: string, output: string): string | null { async function setupIntegrationConfig(opts: { root: URL; logger: Logger; - flags: yargs.Arguments; + flags: Flags; integrationName: string; possibleConfigFiles: string[]; defaultConfigFile: string; diff --git a/packages/astro/src/cli/build/index.ts b/packages/astro/src/cli/build/index.ts index 15ff584317d9..dd44e6170165 100644 --- a/packages/astro/src/cli/build/index.ts +++ b/packages/astro/src/cli/build/index.ts @@ -1,10 +1,9 @@ -import type yargs from 'yargs-parser'; import _build from '../../core/build/index.js'; import { printHelp } from '../../core/messages.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; interface BuildOptions { - flags: yargs.Arguments; + flags: Flags; } export async function build({ flags }: BuildOptions) { @@ -15,6 +14,10 @@ export async function build({ flags }: BuildOptions) { tables: { Flags: [ ['--outDir ', `Specify the output directory for the build.`], + [ + '--force', + 'Clear the content layer and content collection cache, forcing a full rebuild.', + ], ['--help (-h)', 'See all available flags.'], ], }, @@ -25,5 +28,5 @@ export async function build({ flags }: BuildOptions) { const inlineConfig = flagsToAstroInlineConfig(flags); - await _build(inlineConfig, { force: flags.force ?? false }); + await _build(inlineConfig); } diff --git a/packages/astro/src/cli/check/index.ts b/packages/astro/src/cli/check/index.ts index a95e1074a59a..c93e3b2f4cff 100644 --- a/packages/astro/src/cli/check/index.ts +++ b/packages/astro/src/cli/check/index.ts @@ -1,13 +1,15 @@ import path from 'node:path'; -import type { Arguments } from 'yargs-parser'; import { ensureProcessNodeEnv } from '../../core/util.js'; -import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; import { getPackage } from '../install-package.js'; -export async function check(flags: Arguments) { +export async function check(flags: Flags) { ensureProcessNodeEnv('production'); const logger = createLoggerFromFlags(flags); - const getPackageOpts = { skipAsk: flags.yes || flags.y, cwd: flags.root }; + const getPackageOpts = { + skipAsk: !!flags.yes || !!flags.y, + cwd: flags.root, + }; const checkPackage = await getPackage( '@astrojs/check', logger, @@ -30,7 +32,7 @@ export async function check(flags: Arguments) { // For now, we run this once as usually `astro check --watch` is ran alongside `astro dev` which also calls `astro sync`. const { default: sync } = await import('../../core/sync/index.js'); try { - await sync({ inlineConfig: flagsToAstroInlineConfig(flags) }); + await sync(flagsToAstroInlineConfig(flags)); } catch (_) { return process.exit(1); } diff --git a/packages/astro/src/cli/db/index.ts b/packages/astro/src/cli/db/index.ts index dc6da36e1dff..7aaea302c173 100644 --- a/packages/astro/src/cli/db/index.ts +++ b/packages/astro/src/cli/db/index.ts @@ -1,5 +1,5 @@ import type { Arguments } from 'yargs-parser'; -import type { AstroConfig } from '../../@types/astro.js'; +import type { AstroConfig } from '../../types/public/config.js'; import { resolveConfig } from '../../core/config/config.js'; import { apply as applyPolyfill } from '../../core/polyfill.js'; import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; @@ -12,7 +12,10 @@ type DBPackage = { export async function db({ flags }: { flags: Arguments }) { applyPolyfill(); const logger = createLoggerFromFlags(flags); - const getPackageOpts = { skipAsk: flags.yes || flags.y, cwd: flags.root }; + const getPackageOpts = { + skipAsk: !!flags.yes || !!flags.y, + cwd: flags.root, + }; const dbPackage = await getPackage('@astrojs/db', logger, getPackageOpts, []); if (!dbPackage) { diff --git a/packages/astro/src/cli/dev/index.ts b/packages/astro/src/cli/dev/index.ts index 531cddde4ce9..fe9a1094c640 100644 --- a/packages/astro/src/cli/dev/index.ts +++ b/packages/astro/src/cli/dev/index.ts @@ -1,11 +1,10 @@ import { cyan } from 'kleur/colors'; -import type yargs from 'yargs-parser'; import devServer from '../../core/dev/index.js'; import { printHelp } from '../../core/messages.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; interface DevOptions { - flags: yargs.Arguments; + flags: Flags; } export async function dev({ flags }: DevOptions) { @@ -19,6 +18,7 @@ export async function dev({ flags }: DevOptions) { ['--host', `Listen on all addresses, including LAN and public addresses.`], ['--host ', `Expose on a network IP address at `], ['--open', 'Automatically open the app in the browser on server start'], + ['--force', 'Clear the content layer cache, forcing a full rebuild.'], ['--help (-h)', 'See all available flags.'], ], }, diff --git a/packages/astro/src/cli/docs/index.ts b/packages/astro/src/cli/docs/index.ts index cd6325577b37..afb5a1c622cf 100644 --- a/packages/astro/src/cli/docs/index.ts +++ b/packages/astro/src/cli/docs/index.ts @@ -1,9 +1,9 @@ -import type yargs from 'yargs-parser'; import { printHelp } from '../../core/messages.js'; +import type { Flags } from '../flags.js'; import { openInBrowser } from './open.js'; interface DocsOptions { - flags: yargs.Arguments; + flags: Flags; } export async function docs({ flags }: DocsOptions) { diff --git a/packages/astro/src/cli/flags.ts b/packages/astro/src/cli/flags.ts index 0af16806df44..e890b242cd08 100644 --- a/packages/astro/src/cli/flags.ts +++ b/packages/astro/src/cli/flags.ts @@ -1,7 +1,10 @@ -import type { Arguments as Flags } from 'yargs-parser'; -import type { AstroInlineConfig } from '../@types/astro.js'; +import type { Arguments } from 'yargs-parser'; import { type LogOptions, Logger } from '../core/logger/core.js'; import { nodeLogDestination } from '../core/logger/node.js'; +import type { AstroInlineConfig } from '../types/public/config.js'; + +// Alias for now, but allows easier migration to node's `parseArgs` in the future. +export type Flags = Arguments; export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig { return { @@ -9,6 +12,7 @@ export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig { configFile: typeof flags.config === 'string' ? flags.config : undefined, mode: typeof flags.mode === 'string' ? (flags.mode as AstroInlineConfig['mode']) : undefined, logLevel: flags.verbose ? 'debug' : flags.silent ? 'silent' : undefined, + force: flags.force ? true : undefined, // Astro user configs root: typeof flags.root === 'string' ? flags.root : undefined, diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 2d37132ac3b3..c767569fde51 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -201,8 +201,8 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { } /** The primary CLI action */ -export async function cli(args: string[]) { - const flags = yargs(args, { boolean: ['global'], alias: { g: 'global' } }); +export async function cli(argv: string[]) { + const flags = yargs(argv, { boolean: ['global'], alias: { g: 'global' } }); const cmd = resolveCommand(flags); try { await runCommand(cmd, flags); diff --git a/packages/astro/src/cli/info/index.ts b/packages/astro/src/cli/info/index.ts index cb61e45bfc06..9f25a253516a 100644 --- a/packages/astro/src/cli/info/index.ts +++ b/packages/astro/src/cli/info/index.ts @@ -3,15 +3,14 @@ import { arch, platform } from 'node:os'; /* eslint-disable no-console */ import * as colors from 'kleur/colors'; import prompts from 'prompts'; -import type yargs from 'yargs-parser'; -import type { AstroConfig, AstroUserConfig } from '../../@types/astro.js'; import { resolveConfig } from '../../core/config/index.js'; import { ASTRO_VERSION } from '../../core/constants.js'; import { apply as applyPolyfill } from '../../core/polyfill.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; +import type { AstroConfig, AstroUserConfig } from '../../types/public/config.js'; +import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; interface InfoOptions { - flags: yargs.Arguments; + flags: Flags; } export async function getInfoOutput({ diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index 5735a9b6c26e..bd60343c22c5 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -1,6 +1,5 @@ /* eslint-disable no-console */ -import type yargs from 'yargs-parser'; -import type { AstroSettings } from '../../@types/astro.js'; +import type { AstroSettings } from '../../types/astro.js'; import { fileURLToPath } from 'node:url'; import { bgGreen, black, bold, dim, yellow } from 'kleur/colors'; @@ -15,10 +14,10 @@ import * as msg from '../../core/messages.js'; import { apply as applyPolyfill } from '../../core/polyfill.js'; import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js'; import { type PreferenceKey, coerce, isValidKey } from '../../preferences/index.js'; -import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; interface PreferencesOptions { - flags: yargs.Arguments; + flags: Flags; } const PREFERENCES_SUBCOMMANDS = [ @@ -77,7 +76,7 @@ export async function preferences( const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root)); const opts: SubcommandOptions = { location: flags.global ? 'global' : undefined, - json: flags.json, + json: !!flags.json, }; if (subcommand === 'list') { diff --git a/packages/astro/src/cli/preview/index.ts b/packages/astro/src/cli/preview/index.ts index 387c1f241a49..468332ce3b97 100644 --- a/packages/astro/src/cli/preview/index.ts +++ b/packages/astro/src/cli/preview/index.ts @@ -1,11 +1,10 @@ import { cyan } from 'kleur/colors'; -import type yargs from 'yargs-parser'; import { printHelp } from '../../core/messages.js'; import previewServer from '../../core/preview/index.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; interface PreviewOptions { - flags: yargs.Arguments; + flags: Flags; } export async function preview({ flags }: PreviewOptions) { diff --git a/packages/astro/src/cli/sync/index.ts b/packages/astro/src/cli/sync/index.ts index 6849fee70844..7ffe662c5c10 100644 --- a/packages/astro/src/cli/sync/index.ts +++ b/packages/astro/src/cli/sync/index.ts @@ -1,10 +1,9 @@ -import type yargs from 'yargs-parser'; import { printHelp } from '../../core/messages.js'; import _sync from '../../core/sync/index.js'; -import { flagsToAstroInlineConfig } from '../flags.js'; +import { type Flags, flagsToAstroInlineConfig } from '../flags.js'; interface SyncOptions { - flags: yargs.Arguments; + flags: Flags; } export async function sync({ flags }: SyncOptions) { @@ -13,7 +12,10 @@ export async function sync({ flags }: SyncOptions) { commandName: 'astro sync', usage: '[...flags]', tables: { - Flags: [['--help (-h)', 'See all available flags.']], + Flags: [ + ['--force', 'Clear the content layer cache, forcing a full rebuild.'], + ['--help (-h)', 'See all available flags.'], + ], }, description: `Generates TypeScript types for all Astro modules.`, }); @@ -21,7 +23,7 @@ export async function sync({ flags }: SyncOptions) { } try { - await _sync({ inlineConfig: flagsToAstroInlineConfig(flags), telemetry: true }); + await _sync(flagsToAstroInlineConfig(flags), { telemetry: true }); return 0; } catch (_) { return 1; diff --git a/packages/astro/src/cli/telemetry/index.ts b/packages/astro/src/cli/telemetry/index.ts index 277b1cab6721..276f00ef1970 100644 --- a/packages/astro/src/cli/telemetry/index.ts +++ b/packages/astro/src/cli/telemetry/index.ts @@ -1,11 +1,10 @@ /* eslint-disable no-console */ -import type yargs from 'yargs-parser'; import * as msg from '../../core/messages.js'; import { telemetry } from '../../events/index.js'; -import { createLoggerFromFlags } from '../flags.js'; +import { type Flags, createLoggerFromFlags } from '../flags.js'; interface TelemetryOptions { - flags: yargs.Arguments; + flags: Flags; } export async function notify() { diff --git a/packages/astro/src/config/index.ts b/packages/astro/src/config/index.ts index 3c5faf2fa087..b31105ec7adc 100644 --- a/packages/astro/src/config/index.ts +++ b/packages/astro/src/config/index.ts @@ -1,6 +1,6 @@ import type { UserConfig as ViteUserConfig } from 'vite'; -import type { AstroInlineConfig, AstroUserConfig } from '../@types/astro.js'; import { Logger } from '../core/logger/core.js'; +import type { AstroInlineConfig, AstroUserConfig } from '../types/public/config.js'; export function defineConfig(config: AstroUserConfig) { return config; diff --git a/packages/astro/src/config/vite-plugin-content-listen.ts b/packages/astro/src/config/vite-plugin-content-listen.ts index 1b65c5bfbbb3..6c0408001954 100644 --- a/packages/astro/src/config/vite-plugin-content-listen.ts +++ b/packages/astro/src/config/vite-plugin-content-listen.ts @@ -1,8 +1,8 @@ import type fsMod from 'node:fs'; import type { Plugin, ViteDevServer } from 'vite'; -import type { AstroSettings } from '../@types/astro.js'; import { attachContentServerListeners } from '../content/server-listeners.js'; import type { Logger } from '../core/logger/core.js'; +import type { AstroSettings } from '../types/astro.js'; /** * Listen for Astro content directory changes and generate types. diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index 292b49ece101..8a8c83e07877 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -1,22 +1,8 @@ import { posix } from 'node:path'; -import type { - AstroConfig, - AstroUserConfig, - ComponentInstance, - ContainerImportRendererFn, - MiddlewareHandler, - NamedSSRLoadedRendererValue, - Props, - RouteData, - RouteType, - SSRLoadedRenderer, - SSRLoadedRendererValue, - SSRManifest, - SSRResult, -} from '../@types/astro.js'; import { getDefaultClientDirectives } from '../core/client-directive/index.js'; import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schema.js'; import { validateConfig } from '../core/config/validate.js'; +import { createKey } from '../core/encryption.js'; import { Logger } from '../core/logger/core.js'; import { nodeLogDestination } from '../core/logger/node.js'; import { removeLeadingForwardSlash } from '../core/path.js'; @@ -24,8 +10,32 @@ import { RenderContext } from '../core/render-context.js'; import { getParts, validateSegment } from '../core/routing/manifest/create.js'; import { getPattern } from '../core/routing/manifest/pattern.js'; import type { AstroComponentFactory } from '../runtime/server/index.js'; +import type { ComponentInstance } from '../types/astro.js'; +import type { MiddlewareHandler, Props } from '../types/public/common.js'; +import type { AstroConfig, AstroUserConfig } from '../types/public/config.js'; +import type { + NamedSSRLoadedRendererValue, + RouteData, + RouteType, + SSRLoadedRenderer, + SSRLoadedRendererValue, + SSRManifest, + SSRResult, +} from '../types/public/internal.js'; import { ContainerPipeline } from './pipeline.js'; +/** Public type, used for integrations to define a renderer for the container API */ +export type ContainerRenderer = { + /** + * The name of the renderer. + */ + name: string; + /** + * The entrypoint that is used to render a component on the server + */ + serverEntrypoint: string; +}; + /** * Options to be passed when rendering a route */ @@ -102,6 +112,10 @@ export type AddClientRenderer = { entrypoint: string; }; +type ContainerImportRendererFn = ( + containerRenderer: ContainerRenderer, +) => Promise; + function createManifest( manifest?: AstroContainerManifest, renderers?: SSRLoadedRenderer[], @@ -130,6 +144,7 @@ function createManifest( checkOrigin: false, middleware: manifest?.middleware ?? middleware ?? defaultMiddleware, experimentalEnvGetSecretEnabled: false, + key: createKey(), }; } diff --git a/packages/astro/src/container/pipeline.ts b/packages/astro/src/container/pipeline.ts index ff2718b8fbc6..73caa4ecd9a4 100644 --- a/packages/astro/src/container/pipeline.ts +++ b/packages/astro/src/container/pipeline.ts @@ -1,10 +1,3 @@ -import type { - ComponentInstance, - RewritePayload, - RouteData, - SSRElement, - SSRResult, -} from '../@types/astro.js'; import { type HeadElements, Pipeline } from '../core/base-pipeline.js'; import type { SinglePageBuiltModule } from '../core/build/types.js'; import { @@ -12,6 +5,9 @@ import { createStylesheetElementSet, } from '../core/render/ssr-element.js'; import { findRouteToRewrite } from '../core/routing/rewrite.js'; +import type { ComponentInstance } from '../types/astro.js'; +import type { RewritePayload } from '../types/public/common.js'; +import type { RouteData, SSRElement, SSRResult } from '../types/public/internal.js'; export class ContainerPipeline extends Pipeline { /** diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts index f65652453b60..ac619c2b5e78 100644 --- a/packages/astro/src/content/consts.ts +++ b/packages/astro/src/content/consts.ts @@ -2,18 +2,42 @@ export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets'; export const CONTENT_RENDER_FLAG = 'astroRenderContent'; export const CONTENT_FLAG = 'astroContentCollectionEntry'; export const DATA_FLAG = 'astroDataCollectionEntry'; +export const CONTENT_IMAGE_FLAG = 'astroContentImageFlag'; +export const CONTENT_MODULE_FLAG = 'astroContentModuleFlag'; export const VIRTUAL_MODULE_ID = 'astro:content'; export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; +export const DATA_STORE_VIRTUAL_ID = 'astro:data-layer-content'; +export const RESOLVED_DATA_STORE_VIRTUAL_ID = '\0' + DATA_STORE_VIRTUAL_ID; + +// Used by the content layer to create a virtual module that loads the `modules.mjs`, a file created by the content layer +// to map modules that are renderer at runtime +export const MODULES_MJS_ID = 'astro:content-module-imports'; +export const MODULES_MJS_VIRTUAL_ID = '\0' + MODULES_MJS_ID; + +export const DEFERRED_MODULE = 'astro:content-layer-deferred-module'; + +// Used by the content layer to create a virtual module that loads the `assets.mjs` +export const ASSET_IMPORTS_VIRTUAL_ID = 'astro:asset-imports'; +export const ASSET_IMPORTS_RESOLVED_STUB_ID = '\0' + ASSET_IMPORTS_VIRTUAL_ID; export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@'; export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@'; export const SCRIPTS_PLACEHOLDER = '@@ASTRO-SCRIPTS@@'; +export const IMAGE_IMPORT_PREFIX = '__ASTRO_IMAGE_'; export const CONTENT_FLAGS = [ CONTENT_FLAG, CONTENT_RENDER_FLAG, DATA_FLAG, PROPAGATED_ASSET_FLAG, + CONTENT_IMAGE_FLAG, + CONTENT_MODULE_FLAG, ] as const; -export const CONTENT_TYPES_FILE = 'types.d.ts'; +export const CONTENT_TYPES_FILE = 'astro/content.d.ts'; + +export const DATA_STORE_FILE = 'data-store.json'; +export const ASSET_IMPORTS_FILE = 'assets.mjs'; +export const MODULES_IMPORTS_FILE = 'modules.mjs'; + +export const CONTENT_LAYER_TYPE = 'content_layer'; diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts new file mode 100644 index 000000000000..c0ccf27c646e --- /dev/null +++ b/packages/astro/src/content/content-layer.ts @@ -0,0 +1,306 @@ +import { promises as fs, existsSync } from 'node:fs'; +import { isAbsolute } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { FSWatcher } from 'vite'; +import xxhash from 'xxhash-wasm'; +import { AstroUserError } from '../core/errors/errors.js'; +import type { Logger } from '../core/logger/core.js'; +import type { AstroSettings } from '../types/astro.js'; +import { + ASSET_IMPORTS_FILE, + CONTENT_LAYER_TYPE, + DATA_STORE_FILE, + MODULES_IMPORTS_FILE, +} from './consts.js'; +import type { LoaderContext } from './loaders/types.js'; +import type { MutableDataStore } from './mutable-data-store.js'; +import { getEntryDataAndImages, globalContentConfigObserver, posixRelative } from './utils.js'; + +export interface ContentLayerOptions { + store: MutableDataStore; + settings: AstroSettings; + logger: Logger; + watcher?: FSWatcher; +} + +export class ContentLayer { + #logger: Logger; + #store: MutableDataStore; + #settings: AstroSettings; + #watcher?: FSWatcher; + #lastConfigDigest?: string; + #unsubscribe?: () => void; + + #generateDigest?: (data: Record | string) => string; + + #loading = false; + constructor({ settings, logger, store, watcher }: ContentLayerOptions) { + // The default max listeners is 10, which can be exceeded when using a lot of loaders + watcher?.setMaxListeners(50); + + this.#logger = logger; + this.#store = store; + this.#settings = settings; + this.#watcher = watcher; + } + + /** + * Whether the content layer is currently loading content + */ + get loading() { + return this.#loading; + } + + /** + * Watch for changes to the content config and trigger a sync when it changes. + */ + watchContentConfig() { + this.#unsubscribe?.(); + this.#unsubscribe = globalContentConfigObserver.subscribe(async (ctx) => { + if ( + !this.#loading && + ctx.status === 'loaded' && + ctx.config.digest !== this.#lastConfigDigest + ) { + this.sync(); + } + }); + } + + unwatchContentConfig() { + this.#unsubscribe?.(); + } + + /** + * Run the `load()` method of each collection's loader, which will load the data and save it in the data store. + * The loader itself is responsible for deciding whether this will clear and reload the full collection, or + * perform an incremental update. After the data is loaded, the data store is written to disk. + */ + async sync() { + if (this.#loading) { + return; + } + this.#loading = true; + try { + await this.#doSync(); + } finally { + this.#loading = false; + } + } + + async #getGenerateDigest() { + if (this.#generateDigest) { + return this.#generateDigest; + } + // xxhash is a very fast non-cryptographic hash function that is used to generate a content digest + // It uses wasm, so we need to load it asynchronously. + const { h64ToString } = await xxhash(); + + this.#generateDigest = (data: Record | string) => { + const dataString = typeof data === 'string' ? data : JSON.stringify(data); + return h64ToString(dataString); + }; + + return this.#generateDigest; + } + + async #getLoaderContext({ + collectionName, + loaderName = 'content', + parseData, + }: { + collectionName: string; + loaderName: string; + parseData: LoaderContext['parseData']; + }): Promise { + return { + collection: collectionName, + store: this.#store.scopedStore(collectionName), + meta: this.#store.metaStore(collectionName), + logger: this.#logger.forkIntegrationLogger(loaderName), + settings: this.#settings, + parseData, + generateDigest: await this.#getGenerateDigest(), + watcher: this.#watcher, + }; + } + + async #doSync() { + const contentConfig = globalContentConfigObserver.get(); + const logger = this.#logger.forkIntegrationLogger('content'); + if (contentConfig?.status !== 'loaded') { + logger.debug('Content config not loaded, skipping sync'); + return; + } + if (!this.#settings.config.experimental.contentLayer) { + const contentLayerCollections = Object.entries(contentConfig.config.collections).filter( + ([_, collection]) => collection.type === CONTENT_LAYER_TYPE, + ); + if (contentLayerCollections.length > 0) { + throw new AstroUserError( + `The following collections have a loader defined, but the content layer is not enabled: ${contentLayerCollections.map(([title]) => title).join(', ')}.`, + 'To enable the Content Layer API, set `experimental: { contentLayer: true }` in your Astro config file.', + ); + } + return; + } + + logger.info('Syncing content'); + const { digest: currentConfigDigest } = contentConfig.config; + this.#lastConfigDigest = currentConfigDigest; + + const previousConfigDigest = await this.#store.metaStore().get('config-digest'); + if (currentConfigDigest && previousConfigDigest !== currentConfigDigest) { + logger.info('Content config changed, clearing cache'); + this.#store.clearAll(); + await this.#store.metaStore().set('config-digest', currentConfigDigest); + } + + await Promise.all( + Object.entries(contentConfig.config.collections).map(async ([name, collection]) => { + if (collection.type !== CONTENT_LAYER_TYPE) { + return; + } + + let { schema } = collection; + + if (!schema && typeof collection.loader === 'object') { + schema = collection.loader.schema; + if (typeof schema === 'function') { + schema = await schema(); + } + } + + const collectionWithResolvedSchema = { ...collection, schema }; + + const parseData: LoaderContext['parseData'] = async ({ id, data, filePath = '' }) => { + const { imageImports, data: parsedData } = await getEntryDataAndImages( + { + id, + collection: name, + unvalidatedData: data, + _internal: { + rawData: undefined, + filePath, + }, + }, + collectionWithResolvedSchema, + false, + ); + if (imageImports?.length) { + this.#store.addAssetImports( + imageImports, + // This path may already be relative, if we're re-parsing an existing entry + isAbsolute(filePath) + ? posixRelative(fileURLToPath(this.#settings.config.root), filePath) + : filePath, + ); + } + + return parsedData; + }; + + const context = await this.#getLoaderContext({ + collectionName: name, + parseData, + loaderName: collection.loader.name, + }); + + if (typeof collection.loader === 'function') { + return simpleLoader(collection.loader, context); + } + + if (!collection.loader.load) { + throw new Error(`Collection loader for ${name} does not have a load method`); + } + + return collection.loader.load(context); + }), + ); + if (!existsSync(this.#settings.config.cacheDir)) { + await fs.mkdir(this.#settings.config.cacheDir, { recursive: true }); + } + const cacheFile = new URL(DATA_STORE_FILE, this.#settings.config.cacheDir); + await this.#store.writeToDisk(cacheFile); + if (!existsSync(this.#settings.dotAstroDir)) { + await fs.mkdir(this.#settings.dotAstroDir, { recursive: true }); + } + const assetImportsFile = new URL(ASSET_IMPORTS_FILE, this.#settings.dotAstroDir); + await this.#store.writeAssetImports(assetImportsFile); + const modulesImportsFile = new URL(MODULES_IMPORTS_FILE, this.#settings.dotAstroDir); + await this.#store.writeModuleImports(modulesImportsFile); + logger.info('Synced content'); + if (this.#settings.config.experimental.contentIntellisense) { + await this.regenerateCollectionFileManifest(); + } + } + + async regenerateCollectionFileManifest() { + const collectionsManifest = new URL('collections/collections.json', this.#settings.dotAstroDir); + this.#logger.debug('content', 'Regenerating collection file manifest'); + if (existsSync(collectionsManifest)) { + try { + const collections = await fs.readFile(collectionsManifest, 'utf-8'); + const collectionsJson = JSON.parse(collections); + collectionsJson.entries ??= {}; + + for (const { hasSchema, name } of collectionsJson.collections) { + if (!hasSchema) { + continue; + } + const entries = this.#store.values(name); + if (!entries?.[0]?.filePath) { + continue; + } + for (const { filePath } of entries) { + if (!filePath) { + continue; + } + const key = new URL(filePath, this.#settings.config.root).href.toLowerCase(); + collectionsJson.entries[key] = name; + } + } + await fs.writeFile(collectionsManifest, JSON.stringify(collectionsJson, null, 2)); + } catch { + this.#logger.error('content', 'Failed to regenerate collection file manifest'); + } + } + this.#logger.debug('content', 'Regenerated collection file manifest'); + } +} + +export async function simpleLoader( + handler: () => Array | Promise>, + context: LoaderContext, +) { + const data = await handler(); + context.store.clear(); + for (const raw of data) { + const item = await context.parseData({ id: raw.id, data: raw }); + context.store.set({ id: raw.id, data: item }); + } +} + +function contentLayerSingleton() { + let instance: ContentLayer | null = null; + return { + initialized: () => Boolean(instance), + init: (options: ContentLayerOptions) => { + instance?.unwatchContentConfig(); + instance = new ContentLayer(options); + return instance; + }, + get: () => { + if (!instance) { + throw new Error('Content layer not initialized'); + } + return instance; + }, + dispose: () => { + instance?.unwatchContentConfig(); + instance = null; + }, + }; +} + +export const globalContentLayer = contentLayerSingleton(); diff --git a/packages/astro/src/content/data-store.ts b/packages/astro/src/content/data-store.ts new file mode 100644 index 000000000000..76cefc411a76 --- /dev/null +++ b/packages/astro/src/content/data-store.ts @@ -0,0 +1,122 @@ +import type { MarkdownHeading } from '@astrojs/markdown-remark'; +import * as devalue from 'devalue'; + +export interface RenderedContent { + /** Rendered HTML string. If present then `render(entry)` will return a component that renders this HTML. */ + html: string; + metadata?: { + /** Any images that are present in this entry. Relative to the {@link DataEntry} filePath. */ + imagePaths?: Array; + /** Any headings that are present in this file. */ + headings?: MarkdownHeading[]; + /** Raw frontmatter, parsed parsed from the file. This may include data from remark plugins. */ + frontmatter?: Record; + /** Any other metadata that is present in this file. */ + [key: string]: unknown; + }; +} + +export interface DataEntry = Record> { + /** The ID of the entry. Unique per collection. */ + id: string; + /** The parsed entry data */ + data: TData; + /** The file path of the content, if applicable. Relative to the site root. */ + filePath?: string; + /** The raw body of the content, if applicable. */ + body?: string; + /** An optional content digest, to check if the content has changed. */ + digest?: number | string; + /** The rendered content of the entry, if applicable. */ + rendered?: RenderedContent; + /** + * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase when calling `renderEntry`. + */ + deferredRender?: boolean; +} + +/** + * A read-only data store for content collections. This is used to retrieve data from the content layer at runtime. + * To add or modify data, use {@link MutableDataStore} instead. + */ + +export class DataStore { + protected _collections = new Map>(); + + constructor() { + this._collections = new Map(); + } + + get(collectionName: string, key: string): T | undefined { + return this._collections.get(collectionName)?.get(String(key)); + } + + entries(collectionName: string): Array<[id: string, T]> { + const collection = this._collections.get(collectionName) ?? new Map(); + return [...collection.entries()]; + } + + values(collectionName: string): Array { + const collection = this._collections.get(collectionName) ?? new Map(); + return [...collection.values()]; + } + + keys(collectionName: string): Array { + const collection = this._collections.get(collectionName) ?? new Map(); + return [...collection.keys()]; + } + + has(collectionName: string, key: string) { + const collection = this._collections.get(collectionName); + if (collection) { + return collection.has(String(key)); + } + return false; + } + + hasCollection(collectionName: string) { + return this._collections.has(collectionName); + } + + collections() { + return this._collections; + } + + /** + * Attempts to load a DataStore from the virtual module. + * This only works in Vite. + */ + static async fromModule() { + try { + // @ts-expect-error - this is a virtual module + const data = await import('astro:data-layer-content'); + const map = devalue.unflatten(data.default); + return DataStore.fromMap(map); + } catch {} + return new DataStore(); + } + + static async fromMap(data: Map>) { + const store = new DataStore(); + store._collections = data; + return store; + } +} + +function dataStoreSingleton() { + let instance: Promise | DataStore | undefined = undefined; + return { + get: async () => { + if (!instance) { + instance = DataStore.fromModule(); + } + return instance; + }, + set: (store: DataStore) => { + instance = store; + }, + }; +} + +/** @internal */ +export const globalDataStore = dataStoreSingleton(); diff --git a/packages/astro/src/content/loaders/file.ts b/packages/astro/src/content/loaders/file.ts new file mode 100644 index 000000000000..cbc684a99788 --- /dev/null +++ b/packages/astro/src/content/loaders/file.ts @@ -0,0 +1,83 @@ +import { promises as fs, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { posixRelative } from '../utils.js'; +import type { Loader, LoaderContext } from './types.js'; + +/** + * Loads entries from a JSON file. The file must contain an array of objects that contain unique `id` fields, or an object with string keys. + * @todo Add support for other file types, such as YAML, CSV etc. + * @param fileName The path to the JSON file to load, relative to the content directory. + */ +export function file(fileName: string): Loader { + if (fileName.includes('*')) { + // TODO: AstroError + throw new Error('Glob patterns are not supported in `file` loader. Use `glob` loader instead.'); + } + + async function syncData(filePath: string, { logger, parseData, store, settings }: LoaderContext) { + let json: Array>; + + try { + const data = await fs.readFile(filePath, 'utf-8'); + json = JSON.parse(data); + } catch (error: any) { + logger.error(`Error reading data from ${fileName}`); + logger.debug(error.message); + return; + } + + if (Array.isArray(json)) { + if (json.length === 0) { + logger.warn(`No items found in ${fileName}`); + } + logger.debug(`Found ${json.length} item array in ${fileName}`); + store.clear(); + for (const rawItem of json) { + const id = (rawItem.id ?? rawItem.slug)?.toString(); + if (!id) { + logger.error(`Item in ${fileName} is missing an id or slug field.`); + continue; + } + const data = await parseData({ id, data: rawItem, filePath }); + store.set({ + id, + data, + filePath: posixRelative(fileURLToPath(settings.config.root), filePath), + }); + } + } else if (typeof json === 'object') { + const entries = Object.entries>(json); + logger.debug(`Found object with ${entries.length} entries in ${fileName}`); + store.clear(); + for (const [id, rawItem] of entries) { + const data = await parseData({ id, data: rawItem, filePath }); + store.set({ id, data }); + } + } else { + logger.error(`Invalid data in ${fileName}. Must be an array or object.`); + } + } + + return { + name: 'file-loader', + load: async (options) => { + const { settings, logger, watcher } = options; + logger.debug(`Loading data from ${fileName}`); + const url = new URL(fileName, settings.config.root); + if (!existsSync(url)) { + logger.error(`File not found: ${fileName}`); + return; + } + const filePath = fileURLToPath(url); + + await syncData(filePath, options); + + watcher?.on('change', async (changedPath) => { + if (changedPath === filePath) { + logger.info(`Reloading data from ${fileName}`); + await syncData(filePath, options); + } + }); + }, + }; +} diff --git a/packages/astro/src/content/loaders/glob.ts b/packages/astro/src/content/loaders/glob.ts new file mode 100644 index 000000000000..27af34196ad0 --- /dev/null +++ b/packages/astro/src/content/loaders/glob.ts @@ -0,0 +1,296 @@ +import { promises as fs } from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import fastGlob from 'fast-glob'; +import { bold, green } from 'kleur/colors'; +import micromatch from 'micromatch'; +import pLimit from 'p-limit'; +import type { ContentEntryRenderFunction, ContentEntryType } from '../../types/public/content.js'; +import type { RenderedContent } from '../data-store.js'; +import { getContentEntryIdAndSlug, getEntryConfigByExtMap, posixRelative } from '../utils.js'; +import type { Loader } from './types.js'; + +export interface GenerateIdOptions { + /** The path to the entry file, relative to the base directory. */ + entry: string; + + /** The base directory URL. */ + base: URL; + /** The parsed, unvalidated data of the entry. */ + data: Record; +} + +export interface GlobOptions { + /** The glob pattern to match files, relative to the base directory */ + pattern: string; + /** The base directory to resolve the glob pattern from. Relative to the root directory, or an absolute file URL. Defaults to `.` */ + base?: string | URL; + /** + * Function that generates an ID for an entry. Default implementation generates a slug from the entry path. + * @returns The ID of the entry. Must be unique per collection. + **/ + generateId?: (options: GenerateIdOptions) => string; +} + +function generateIdDefault({ entry, base, data }: GenerateIdOptions): string { + if (data.slug) { + return data.slug as string; + } + const entryURL = new URL(entry, base); + const { slug } = getContentEntryIdAndSlug({ + entry: entryURL, + contentDir: base, + collection: '', + }); + return slug; +} + +/** + * Loads multiple entries, using a glob pattern to match files. + * @param pattern A glob pattern to match files, relative to the content directory. + */ +export function glob(globOptions: GlobOptions): Loader { + if (globOptions.pattern.startsWith('../')) { + throw new Error( + 'Glob patterns cannot start with `../`. Set the `base` option to a parent directory instead.', + ); + } + if (globOptions.pattern.startsWith('/')) { + throw new Error( + 'Glob patterns cannot start with `/`. Set the `base` option to a parent directory or use a relative path instead.', + ); + } + + const generateId = globOptions?.generateId ?? generateIdDefault; + + const fileToIdMap = new Map(); + + return { + name: 'glob-loader', + load: async ({ settings, logger, watcher, parseData, store, generateDigest }) => { + const renderFunctionByContentType = new WeakMap< + ContentEntryType, + ContentEntryRenderFunction + >(); + + const untouchedEntries = new Set(store.keys()); + + async function syncData(entry: string, base: URL, entryType?: ContentEntryType) { + if (!entryType) { + logger.warn(`No entry type found for ${entry}`); + return; + } + const fileUrl = new URL(entry, base); + const contents = await fs.readFile(fileUrl, 'utf-8').catch((err) => { + logger.error(`Error reading ${entry}: ${err.message}`); + return; + }); + + if (!contents) { + logger.warn(`No contents found for ${entry}`); + return; + } + + const { body, data } = await entryType.getEntryInfo({ + contents, + fileUrl, + }); + + const id = generateId({ entry, base, data }); + untouchedEntries.delete(id); + + const existingEntry = store.get(id); + + const digest = generateDigest(contents); + + if (existingEntry && existingEntry.digest === digest && existingEntry.filePath) { + if (existingEntry.deferredRender) { + store.addModuleImport(existingEntry.filePath); + } + + if (existingEntry.rendered?.metadata?.imagePaths?.length) { + // Add asset imports for existing entries + store.addAssetImports( + existingEntry.rendered.metadata.imagePaths, + existingEntry.filePath, + ); + } + // Re-parsing to resolve images and other effects + await parseData(existingEntry); + return; + } + + const filePath = fileURLToPath(fileUrl); + + const relativePath = posixRelative(fileURLToPath(settings.config.root), filePath); + + const parsedData = await parseData({ + id, + data, + filePath, + }); + if (entryType.getRenderFunction) { + let render = renderFunctionByContentType.get(entryType); + if (!render) { + render = await entryType.getRenderFunction(settings.config); + // Cache the render function for this content type, so it can re-use parsers and other expensive setup + renderFunctionByContentType.set(entryType, render); + } + let rendered: RenderedContent | undefined = undefined; + + try { + rendered = await render?.({ + id, + data: parsedData, + body, + filePath, + digest, + }); + } catch (error: any) { + logger.error(`Error rendering ${entry}: ${error.message}`); + } + + store.set({ + id, + data: parsedData, + body, + filePath: relativePath, + digest, + rendered, + }); + if (rendered?.metadata?.imagePaths?.length) { + store.addAssetImports(rendered.metadata.imagePaths, relativePath); + } + // todo: add an explicit way to opt in to deferred rendering + } else if ('contentModuleTypes' in entryType) { + store.set({ + id, + data: parsedData, + body, + filePath: relativePath, + digest, + deferredRender: true, + }); + } else { + store.set({ id, data: parsedData, body, filePath: relativePath, digest }); + } + + fileToIdMap.set(filePath, id); + } + + const entryConfigByExt = getEntryConfigByExtMap([ + ...settings.contentEntryTypes, + ...settings.dataEntryTypes, + ] as Array); + + const baseDir = globOptions.base + ? new URL(globOptions.base, settings.config.root) + : settings.config.root; + + if (!baseDir.pathname.endsWith('/')) { + baseDir.pathname = `${baseDir.pathname}/`; + } + + const files = await fastGlob(globOptions.pattern, { + cwd: fileURLToPath(baseDir), + }); + + function configForFile(file: string) { + const ext = file.split('.').at(-1); + if (!ext) { + logger.warn(`No extension found for ${file}`); + return; + } + return entryConfigByExt.get(`.${ext}`); + } + + const limit = pLimit(10); + const skippedFiles: Array = []; + + const contentDir = new URL('content/', settings.config.srcDir); + + function isInContentDir(file: string) { + const fileUrl = new URL(file, baseDir); + return fileUrl.href.startsWith(contentDir.href); + } + + const configFiles = new Set( + ['config.js', 'config.ts', 'config.mjs'].map((file) => new URL(file, contentDir).href), + ); + + function isConfigFile(file: string) { + const fileUrl = new URL(file, baseDir); + return configFiles.has(fileUrl.href); + } + + await Promise.all( + files.map((entry) => { + if (isConfigFile(entry)) { + return; + } + if (isInContentDir(entry)) { + skippedFiles.push(entry); + return; + } + return limit(async () => { + const entryType = configForFile(entry); + await syncData(entry, baseDir, entryType); + }); + }), + ); + + const skipCount = skippedFiles.length; + + if (skipCount > 0) { + logger.warn(`The glob() loader cannot be used for files in ${bold('src/content')}.`); + if (skipCount > 10) { + logger.warn( + `Skipped ${green(skippedFiles.length)} files that matched ${green(globOptions.pattern)}.`, + ); + } else { + logger.warn(`Skipped the following files that matched ${green(globOptions.pattern)}:`); + skippedFiles.forEach((file) => logger.warn(`• ${green(file)}`)); + } + } + + // Remove entries that were not found this time + untouchedEntries.forEach((id) => store.delete(id)); + + if (!watcher) { + return; + } + + const matcher: RegExp = micromatch.makeRe(globOptions.pattern); + + const matchesGlob = (entry: string) => !entry.startsWith('../') && matcher.test(entry); + + const basePath = fileURLToPath(baseDir); + + async function onChange(changedPath: string) { + const entry = posixRelative(basePath, changedPath); + if (!matchesGlob(entry)) { + return; + } + const entryType = configForFile(changedPath); + const baseUrl = pathToFileURL(basePath); + await syncData(entry, baseUrl, entryType); + logger.info(`Reloaded data from ${green(entry)}`); + } + + watcher.on('change', onChange); + + watcher.on('add', onChange); + + watcher.on('unlink', async (deletedPath) => { + const entry = posixRelative(basePath, deletedPath); + if (!matchesGlob(entry)) { + return; + } + const id = fileToIdMap.get(deletedPath); + if (id) { + store.delete(id); + fileToIdMap.delete(deletedPath); + } + }); + }, + }; +} diff --git a/packages/astro/src/content/loaders/index.ts b/packages/astro/src/content/loaders/index.ts new file mode 100644 index 000000000000..30b4bfbe5334 --- /dev/null +++ b/packages/astro/src/content/loaders/index.ts @@ -0,0 +1,3 @@ +export { file } from './file.js'; +export { glob } from './glob.js'; +export * from './types.js'; diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts new file mode 100644 index 000000000000..5edfb5ef71fe --- /dev/null +++ b/packages/astro/src/content/loaders/types.ts @@ -0,0 +1,44 @@ +import type { FSWatcher } from 'vite'; +import type { ZodSchema } from 'zod'; +import type { AstroIntegrationLogger } from '../../core/logger/core.js'; +import type { AstroSettings } from '../../types/astro.js'; +import type { MetaStore, ScopedDataStore } from '../mutable-data-store.js'; + +export interface ParseDataOptions> { + /** The ID of the entry. Unique per collection */ + id: string; + /** The raw, unvalidated data of the entry */ + data: TData; + /** An optional file path, where the entry represents a local file. */ + filePath?: string; +} + +export interface LoaderContext { + /** The unique name of the collection */ + collection: string; + /** A database abstraction to store the actual data */ + store: ScopedDataStore; + /** A simple KV store, designed for things like sync tokens */ + meta: MetaStore; + logger: AstroIntegrationLogger; + + settings: AstroSettings; + + /** Validates and parses the data according to the collection schema */ + parseData>(props: ParseDataOptions): Promise; + + /** Generates a non-cryptographic content digest. This can be used to check if the data has changed */ + generateDigest(data: Record | string): string; + + /** When running in dev, this is a filesystem watcher that can be used to trigger updates */ + watcher?: FSWatcher; +} + +export interface Loader { + /** Unique name of the loader, e.g. the npm package name */ + name: string; + /** Do the actual loading of the data */ + load: (context: LoaderContext) => Promise; + /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ + schema?: ZodSchema | Promise | (() => ZodSchema | Promise); +} diff --git a/packages/astro/src/content/mutable-data-store.ts b/packages/astro/src/content/mutable-data-store.ts new file mode 100644 index 000000000000..200951848046 --- /dev/null +++ b/packages/astro/src/content/mutable-data-store.ts @@ -0,0 +1,370 @@ +import { promises as fs, type PathLike, existsSync } from 'node:fs'; +import * as devalue from 'devalue'; +import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js'; +import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import { type DataEntry, DataStore, type RenderedContent } from './data-store.js'; +import { contentModuleToId } from './utils.js'; + +const SAVE_DEBOUNCE_MS = 500; + +/** + * Extends the DataStore with the ability to change entries and write them to disk. + * This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed. + */ +export class MutableDataStore extends DataStore { + #file?: PathLike; + + #assetsFile?: PathLike; + #modulesFile?: PathLike; + + #saveTimeout: NodeJS.Timeout | undefined; + #assetsSaveTimeout: NodeJS.Timeout | undefined; + #modulesSaveTimeout: NodeJS.Timeout | undefined; + + #dirty = false; + #assetsDirty = false; + #modulesDirty = false; + + #assetImports = new Set(); + #moduleImports = new Map(); + + set(collectionName: string, key: string, value: unknown) { + const collection = this._collections.get(collectionName) ?? new Map(); + collection.set(String(key), value); + this._collections.set(collectionName, collection); + this.#saveToDiskDebounced(); + } + + delete(collectionName: string, key: string) { + const collection = this._collections.get(collectionName); + if (collection) { + collection.delete(String(key)); + this.#saveToDiskDebounced(); + } + } + + clear(collectionName: string) { + this._collections.delete(collectionName); + this.#saveToDiskDebounced(); + } + + clearAll() { + this._collections.clear(); + this.#saveToDiskDebounced(); + } + + addAssetImport(assetImport: string, filePath: string) { + const id = imageSrcToImportId(assetImport, filePath); + if (id) { + this.#assetImports.add(id); + // We debounce the writes to disk because addAssetImport is called for every image in every file, + // and can be called many times in quick succession by a filesystem watcher. We only want to write + // the file once, after all the imports have been added. + this.#writeAssetsImportsDebounced(); + } + } + + addAssetImports(assets: Array, filePath: string) { + assets.forEach((asset) => this.addAssetImport(asset, filePath)); + } + + addModuleImport(fileName: string) { + const id = contentModuleToId(fileName); + if (id) { + this.#moduleImports.set(fileName, id); + // We debounce the writes to disk because addAssetImport is called for every image in every file, + // and can be called many times in quick succession by a filesystem watcher. We only want to write + // the file once, after all the imports have been added. + this.#writeModulesImportsDebounced(); + } + } + + async writeAssetImports(filePath: PathLike) { + this.#assetsFile = filePath; + + if (this.#assetImports.size === 0) { + try { + await fs.writeFile(filePath, 'export default new Map();'); + } catch (err) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); + } + } + + if (!this.#assetsDirty && existsSync(filePath)) { + return; + } + // Import the assets, with a symbol name that is unique to the import id. The import + // for each asset is an object with path, format and dimensions. + // We then export them all, mapped by the import id, so we can find them again in the build. + const imports: Array = []; + const exports: Array = []; + this.#assetImports.forEach((id) => { + const symbol = importIdToSymbolName(id); + imports.push(`import ${symbol} from '${id}';`); + exports.push(`[${JSON.stringify(id)}, ${symbol}]`); + }); + const code = /* js */ ` +${imports.join('\n')} +export default new Map([${exports.join(', ')}]); + `; + try { + await fs.writeFile(filePath, code); + } catch (err) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); + } + this.#assetsDirty = false; + } + + async writeModuleImports(filePath: PathLike) { + this.#modulesFile = filePath; + + if (this.#moduleImports.size === 0) { + try { + await fs.writeFile(filePath, 'export default new Map();'); + } catch (err) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); + } + } + + if (!this.#modulesDirty && existsSync(filePath)) { + return; + } + + // Import the assets, with a symbol name that is unique to the import id. The import + // for each asset is an object with path, format and dimensions. + // We then export them all, mapped by the import id, so we can find them again in the build. + const lines: Array = []; + for (const [fileName, specifier] of this.#moduleImports) { + lines.push(`['${fileName}', () => import('${specifier}')]`); + } + const code = ` +export default new Map([\n${lines.join(',\n')}]); + `; + try { + await fs.writeFile(filePath, code); + } catch (err) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); + } + this.#modulesDirty = false; + } + + #writeAssetsImportsDebounced() { + this.#assetsDirty = true; + if (this.#assetsFile) { + if (this.#assetsSaveTimeout) { + clearTimeout(this.#assetsSaveTimeout); + } + this.#assetsSaveTimeout = setTimeout(() => { + this.#assetsSaveTimeout = undefined; + this.writeAssetImports(this.#assetsFile!); + }, SAVE_DEBOUNCE_MS); + } + } + + #writeModulesImportsDebounced() { + this.#modulesDirty = true; + if (this.#modulesFile) { + if (this.#modulesSaveTimeout) { + clearTimeout(this.#modulesSaveTimeout); + } + this.#modulesSaveTimeout = setTimeout(() => { + this.#modulesSaveTimeout = undefined; + this.writeModuleImports(this.#modulesFile!); + }, SAVE_DEBOUNCE_MS); + } + } + + #saveToDiskDebounced() { + this.#dirty = true; + // Only save to disk if it has already been saved once + if (this.#file) { + if (this.#saveTimeout) { + clearTimeout(this.#saveTimeout); + } + this.#saveTimeout = setTimeout(() => { + this.#saveTimeout = undefined; + this.writeToDisk(this.#file!); + }, SAVE_DEBOUNCE_MS); + } + } + + scopedStore(collectionName: string): ScopedDataStore { + return { + get: = Record>(key: string) => + this.get>(collectionName, key), + entries: () => this.entries(collectionName), + values: () => this.values(collectionName), + keys: () => this.keys(collectionName), + set: ({ id: key, data, body, filePath, deferredRender, digest, rendered }) => { + if (!key) { + throw new Error(`ID must be a non-empty string`); + } + const id = String(key); + if (digest) { + const existing = this.get(collectionName, id); + if (existing && existing.digest === digest) { + return false; + } + } + const entry: DataEntry = { + id, + data, + }; + // We do it like this so we don't waste space stringifying + // the fields if they are not set + if (body) { + entry.body = body; + } + if (filePath) { + if (filePath.startsWith('/')) { + throw new Error(`File path must be relative to the site root. Got: ${filePath}`); + } + entry.filePath = filePath; + } + if (digest) { + entry.digest = digest; + } + if (rendered) { + entry.rendered = rendered; + } + if (deferredRender) { + entry.deferredRender = deferredRender; + if (filePath) { + this.addModuleImport(filePath); + } + } + this.set(collectionName, id, entry); + return true; + }, + delete: (key: string) => this.delete(collectionName, key), + clear: () => this.clear(collectionName), + has: (key: string) => this.has(collectionName, key), + addAssetImport: (assetImport: string, fileName: string) => + this.addAssetImport(assetImport, fileName), + addAssetImports: (assets: Array, fileName: string) => + this.addAssetImports(assets, fileName), + addModuleImport: (fileName: string) => this.addModuleImport(fileName), + }; + } + /** + * Returns a MetaStore for a given collection, or if no collection is provided, the default meta collection. + */ + metaStore(collectionName = ':meta'): MetaStore { + const collectionKey = `meta:${collectionName}`; + return { + get: (key: string) => this.get(collectionKey, key), + set: (key: string, data: string) => this.set(collectionKey, key, data), + delete: (key: string) => this.delete(collectionKey, key), + has: (key: string) => this.has(collectionKey, key), + }; + } + + toString() { + return devalue.stringify(this._collections); + } + + async writeToDisk(filePath: PathLike) { + if (!this.#dirty) { + return; + } + try { + await fs.writeFile(filePath, this.toString()); + this.#file = filePath; + this.#dirty = false; + } catch (err) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); + } + } + + /** + * Attempts to load a MutableDataStore from the virtual module. + * This only works in Vite. + */ + static async fromModule() { + try { + // @ts-expect-error - this is a virtual module + const data = await import('astro:data-layer-content'); + const map = devalue.unflatten(data.default); + return MutableDataStore.fromMap(map); + } catch {} + return new MutableDataStore(); + } + + static async fromMap(data: Map>) { + const store = new MutableDataStore(); + store._collections = data; + return store; + } + + static async fromString(data: string) { + const map = devalue.parse(data); + return MutableDataStore.fromMap(map); + } + + static async fromFile(filePath: string | URL) { + try { + if (existsSync(filePath)) { + const data = await fs.readFile(filePath, 'utf-8'); + return MutableDataStore.fromString(data); + } + } catch {} + return new MutableDataStore(); + } +} + +export interface ScopedDataStore { + get: = Record>( + key: string, + ) => DataEntry | undefined; + entries: () => Array<[id: string, DataEntry]>; + set: >(opts: { + /** The ID of the entry. Must be unique per collection. */ + id: string; + /** The data to store. */ + data: TData; + /** The raw body of the content, if applicable. */ + body?: string; + /** The file path of the content, if applicable. Relative to the site root. */ + filePath?: string; + /** A content digest, to check if the content has changed. */ + digest?: number | string; + /** The rendered content, if applicable. */ + rendered?: RenderedContent; + /** + * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase. + */ + deferredRender?: boolean; + }) => boolean; + values: () => Array; + keys: () => Array; + delete: (key: string) => void; + clear: () => void; + has: (key: string) => boolean; + /** + * @internal Adds asset imports to the store. This is used to track image imports for the build. This API is subject to change. + */ + addAssetImports: (assets: Array, fileName: string) => void; + /** + * @internal Adds an asset import to the store. This is used to track image imports for the build. This API is subject to change. + */ + addAssetImport: (assetImport: string, fileName: string) => void; + /** + * Adds a single asset to the store. This asset will be transformed + * by Vite, and the URL will be available in the final build. + * @param fileName + * @param specifier + * @returns + */ + addModuleImport: (fileName: string) => void; +} + +/** + * A key-value store for metadata strings. Useful for storing things like sync tokens. + */ + +export interface MetaStore { + get: (key: string) => string | undefined; + set: (key: string, value: string) => void; + has: (key: string) => boolean; + delete: (key: string) => void; +} diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 34d2f10e927d..489625532950 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -1,7 +1,10 @@ import type { MarkdownHeading } from '@astrojs/markdown-remark'; +import { Traverse } from 'neotraverse/modern'; import pLimit from 'p-limit'; -import { ZodIssueCode, string as zodString } from 'zod'; -import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import { ZodIssueCode, z } from 'zod'; +import type { GetImageResult, ImageMetadata } from '../assets/types.js'; +import { imageSrcToImportId } from '../assets/utils/resolveImports.js'; +import { AstroError, AstroErrorData, AstroUserError } from '../core/errors/index.js'; import { prependForwardSlash } from '../core/path.js'; import { type AstroComponentFactory, @@ -11,8 +14,11 @@ import { renderScriptElement, renderTemplate, renderUniqueStylesheet, + render as serverRender, unescapeHTML, } from '../runtime/server/index.js'; +import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from './consts.js'; +import { type DataEntry, globalDataStore } from './data-store.js'; import type { ContentLookupMap } from './utils.js'; type LazyImport = () => Promise; @@ -21,6 +27,15 @@ type CollectionToEntryMap = Record; type GetEntryImport = (collection: string, lookupId: string) => Promise; export function defineCollection(config: any) { + if ('loader' in config) { + if (config.type && config.type !== CONTENT_LAYER_TYPE) { + throw new AstroUserError( + 'Collections that use the Content Layer API must have a `loader` defined and no `type` set.', + "Check your collection definitions in `src/content/config.*`.'", + ); + } + config.type = CONTENT_LAYER_TYPE; + } if (!config.type) config.type = 'content'; return config; } @@ -56,11 +71,34 @@ export function createGetCollection({ cacheEntriesByCollection: Map; }) { return async function getCollection(collection: string, filter?: (entry: any) => unknown) { + const hasFilter = typeof filter === 'function'; + const store = await globalDataStore.get(); let type: 'content' | 'data'; if (collection in contentCollectionToEntryMap) { type = 'content'; } else if (collection in dataCollectionToEntryMap) { type = 'data'; + } else if (store.hasCollection(collection)) { + // @ts-expect-error virtual module + const { default: imageAssetMap } = await import('astro:asset-imports'); + + const result = []; + for (const rawEntry of store.values(collection)) { + const data = rawEntry.filePath + ? updateImageReferencesInData(rawEntry.data, rawEntry.filePath, imageAssetMap) + : rawEntry.data; + + const entry = { + ...rawEntry, + data, + collection, + }; + if (hasFilter && !filter(entry)) { + continue; + } + result.push(entry); + } + return result; } else { // eslint-disable-next-line no-console console.warn( @@ -70,6 +108,7 @@ export function createGetCollection({ ); return []; } + const lazyImports = Object.values( type === 'content' ? contentCollectionToEntryMap[collection] @@ -111,7 +150,7 @@ export function createGetCollection({ ); cacheEntriesByCollection.set(collection, entries); } - if (typeof filter === 'function') { + if (hasFilter) { return entries.filter(filter); } else { // Clone the array so users can safely mutate it. @@ -124,11 +163,27 @@ export function createGetCollection({ export function createGetEntryBySlug({ getEntryImport, getRenderEntryImport, + collectionNames, }: { getEntryImport: GetEntryImport; getRenderEntryImport: GetEntryImport; + collectionNames: Set; }) { return async function getEntryBySlug(collection: string, slug: string) { + const store = await globalDataStore.get(); + + if (!collectionNames.has(collection)) { + if (store.hasCollection(collection)) { + throw new AstroError({ + ...AstroErrorData.GetEntryDeprecationError, + message: AstroErrorData.GetEntryDeprecationError.message(collection, 'getEntryBySlug'), + }); + } + // eslint-disable-next-line no-console + console.warn(`The collection ${JSON.stringify(collection)} does not exist.`); + return undefined; + } + const entryImport = await getEntryImport(collection, slug); if (typeof entryImport !== 'function') return undefined; @@ -151,8 +206,28 @@ export function createGetEntryBySlug({ }; } -export function createGetDataEntryById({ getEntryImport }: { getEntryImport: GetEntryImport }) { +export function createGetDataEntryById({ + getEntryImport, + collectionNames, +}: { + getEntryImport: GetEntryImport; + collectionNames: Set; +}) { return async function getDataEntryById(collection: string, id: string) { + const store = await globalDataStore.get(); + + if (!collectionNames.has(collection)) { + if (store.hasCollection(collection)) { + throw new AstroError({ + ...AstroErrorData.GetEntryDeprecationError, + message: AstroErrorData.GetEntryDeprecationError.message(collection, 'getDataEntryById'), + }); + } + // eslint-disable-next-line no-console + console.warn(`The collection ${JSON.stringify(collection)} does not exist.`); + return undefined; + } + const lazyImport = await getEntryImport(collection, id); // TODO: AstroError @@ -187,9 +262,11 @@ type EntryLookupObject = { collection: string; id: string } | { collection: stri export function createGetEntry({ getEntryImport, getRenderEntryImport, + collectionNames, }: { getEntryImport: GetEntryImport; getRenderEntryImport: GetEntryImport; + collectionNames: Set; }) { return async function getEntry( // Can either pass collection and identifier as 2 positional args, @@ -216,6 +293,33 @@ export function createGetEntry({ : collectionOrLookupObject.slug; } + const store = await globalDataStore.get(); + + if (store.hasCollection(collection)) { + const entry = store.get(collection, lookupId); + if (!entry) { + // eslint-disable-next-line no-console + console.warn(`Entry ${collection} → ${lookupId} was not found.`); + return; + } + + if (entry.filePath) { + // @ts-expect-error virtual module + const { default: imageAssetMap } = await import('astro:asset-imports'); + entry.data = updateImageReferencesInData(entry.data, entry.filePath, imageAssetMap); + } + return { + ...entry, + collection, + } as DataEntryResult | ContentEntryResult; + } + + if (!collectionNames.has(collection)) { + // eslint-disable-next-line no-console + console.warn(`The collection ${JSON.stringify(collection)} does not exist.`); + return undefined; + } + const entryImport = await getEntryImport(collection, lookupId); if (typeof entryImport !== 'function') return undefined; @@ -261,6 +365,115 @@ type RenderResult = { remarkPluginFrontmatter: Record; }; +const CONTENT_LAYER_IMAGE_REGEX = /__ASTRO_IMAGE_="([^"]+)"/g; + +async function updateImageReferencesInBody(html: string, fileName: string) { + // @ts-expect-error Virtual module + const { default: imageAssetMap } = await import('astro:asset-imports'); + + const imageObjects = new Map(); + + // @ts-expect-error Virtual module resolved at runtime + const { getImage } = await import('astro:assets'); + + // First load all the images. This is done outside of the replaceAll + // function because getImage is async. + for (const [_full, imagePath] of html.matchAll(CONTENT_LAYER_IMAGE_REGEX)) { + try { + const decodedImagePath = JSON.parse(imagePath.replaceAll('"', '"')); + const id = imageSrcToImportId(decodedImagePath.src, fileName); + + const imported = imageAssetMap.get(id); + if (!id || imageObjects.has(id) || !imported) { + continue; + } + const image: GetImageResult = await getImage({ ...decodedImagePath, src: imported }); + imageObjects.set(imagePath, image); + } catch { + throw new Error(`Failed to parse image reference: ${imagePath}`); + } + } + + return html.replaceAll(CONTENT_LAYER_IMAGE_REGEX, (full, imagePath) => { + const image = imageObjects.get(imagePath); + + if (!image) { + return full; + } + + const { index, ...attributes } = image.attributes; + + return Object.entries({ + ...attributes, + src: image.src, + srcset: image.srcSet.attribute, + }) + .map(([key, value]) => (value ? `${key}=${JSON.stringify(String(value))}` : '')) + .join(' '); + }); +} + +function updateImageReferencesInData>( + data: T, + fileName: string, + imageAssetMap: Map, +): T { + return new Traverse(data).map(function (ctx, val) { + if (typeof val === 'string' && val.startsWith(IMAGE_IMPORT_PREFIX)) { + const src = val.replace(IMAGE_IMPORT_PREFIX, ''); + const id = imageSrcToImportId(src, fileName); + if (!id) { + ctx.update(src); + return; + } + const imported = imageAssetMap.get(id); + if (imported) { + ctx.update(imported); + } else { + ctx.update(src); + } + } + }); +} + +export async function renderEntry( + entry: DataEntry | { render: () => Promise<{ Content: AstroComponentFactory }> }, +) { + if (entry && 'render' in entry) { + // This is an old content collection entry, so we use its render method + return entry.render(); + } + + if (entry.deferredRender) { + try { + // @ts-expect-error virtual module + const { default: contentModules } = await import('astro:content-module-imports'); + const module = contentModules.get(entry.filePath); + const deferredMod = await module(); + return { + Content: deferredMod.Content, + headings: deferredMod.getHeadings?.() ?? [], + remarkPluginFrontmatter: deferredMod.frontmatter ?? {}, + }; + } catch (e) { + // eslint-disable-next-line + console.error(e); + } + } + + const html = + entry?.rendered?.metadata?.imagePaths?.length && entry.filePath + ? await updateImageReferencesInBody(entry.rendered.html, entry.filePath) + : entry?.rendered?.html; + + const Content = createComponent(() => serverRender`${unescapeHTML(html)}`); + return { + Content, + headings: entry?.rendered?.metadata?.headings ?? [], + remarkPluginFrontmatter: entry?.rendered?.metadata?.frontmatter ?? {}, + }; +} + async function render({ collection, id, @@ -357,36 +570,92 @@ async function render({ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap }) { return function reference(collection: string) { - return zodString().transform((lookupId: string, ctx) => { - const flattenedErrorPath = ctx.path.join('.'); - if (!lookupMap[collection]) { - ctx.addIssue({ - code: ZodIssueCode.custom, - message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`, - }); - return; - } - - const { type, entries } = lookupMap[collection]; - const entry = entries[lookupId]; - - if (!entry) { - ctx.addIssue({ - code: ZodIssueCode.custom, - message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${Object.keys( - entries, - ) - .map((c) => JSON.stringify(c)) - .join(' | ')}. Received ${JSON.stringify(lookupId)}.`, - }); - return; - } - // Content is still identified by slugs, so map to a `slug` key for consistency. - if (type === 'content') { - return { slug: lookupId, collection }; - } - return { id: lookupId, collection }; - }); + return z + .union([ + z.string(), + z.object({ + id: z.string(), + collection: z.string(), + }), + z.object({ + slug: z.string(), + collection: z.string(), + }), + ]) + .transform( + async ( + lookup: + | string + | { id: string; collection: string } + | { slug: string; collection: string }, + ctx, + ) => { + const flattenedErrorPath = ctx.path.join('.'); + const store = await globalDataStore.get(); + const collectionIsInStore = store.hasCollection(collection); + + if (typeof lookup === 'object') { + // If these don't match then something is wrong with the reference + if (lookup.collection !== collection) { + ctx.addIssue({ + code: ZodIssueCode.custom, + message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${collection}. Received ${lookup.collection}.`, + }); + return; + } + + // A reference object might refer to an invalid collection, because when we convert it we don't have access to the store. + // If it is an object then we're validating later in the pipeline, so we can check the collection at that point. + if (!lookupMap[collection] && !collectionIsInStore) { + ctx.addIssue({ + code: ZodIssueCode.custom, + message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`, + }); + return; + } + return lookup; + } + + if (collectionIsInStore) { + const entry = store.get(collection, lookup); + if (!entry) { + ctx.addIssue({ + code: ZodIssueCode.custom, + message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Entry ${lookup} does not exist.`, + }); + return; + } + return { id: lookup, collection }; + } + + if (!lookupMap[collection] && store.collections().size === 0) { + // If the collection is not in the lookup map or store, it may be a content layer collection and the store may not yet be populated. + // For now, we can't validate this reference, so we'll optimistically convert it to a reference object which we'll validate + // later in the pipeline when we do have access to the store. + return { id: lookup, collection }; + } + + const { type, entries } = lookupMap[collection]; + const entry = entries[lookup]; + + if (!entry) { + ctx.addIssue({ + code: ZodIssueCode.custom, + message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${Object.keys( + entries, + ) + .map((c) => JSON.stringify(c)) + .join(' | ')}. Received ${JSON.stringify(lookup)}.`, + }); + return; + } + // Content is still identified by slugs, so map to a `slug` key for consistency. + if (type === 'content') { + return { slug: lookup, collection }; + } + return { id: lookup, collection }; + }, + ); }; } diff --git a/packages/astro/src/content/server-listeners.ts b/packages/astro/src/content/server-listeners.ts index 5d7868d58935..28f5b16a83e0 100644 --- a/packages/astro/src/content/server-listeners.ts +++ b/packages/astro/src/content/server-listeners.ts @@ -3,10 +3,10 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { bold, cyan, underline } from 'kleur/colors'; import type { ViteDevServer } from 'vite'; -import type { AstroSettings } from '../@types/astro.js'; import { loadTSConfig } from '../core/config/tsconfig.js'; import type { Logger } from '../core/logger/core.js'; import { appendForwardSlash } from '../core/path.js'; +import type { AstroSettings } from '../types/astro.js'; import { createContentTypesGenerator } from './types-generator.js'; import { type ContentPaths, getContentPaths, globalContentConfigObserver } from './utils.js'; diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index ea0c3cc80e8d..9923a0c343d7 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -4,15 +4,18 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import glob from 'fast-glob'; import { bold, cyan } from 'kleur/colors'; import { type ViteDevServer, normalizePath } from 'vite'; -import { z } from 'zod'; +import { type ZodSchema, z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import type { AstroSettings, ContentEntryType } from '../@types/astro.js'; +import { printNode, zodToTs } from 'zod-to-ts'; import { AstroError } from '../core/errors/errors.js'; import { AstroErrorData } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import { isRelativePath } from '../core/path.js'; -import { CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { ContentEntryType } from '../types/public/content.js'; +import { CONTENT_LAYER_TYPE, CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js'; import { + type CollectionConfig, type ContentConfig, type ContentObservable, type ContentPaths, @@ -44,7 +47,7 @@ type CollectionEntryMap = { entries: Record; } | { - type: 'data'; + type: 'data' | typeof CONTENT_LAYER_TYPE; entries: Record; }; }; @@ -245,7 +248,7 @@ export async function createContentTypesGenerator({ collectionEntryMap[collectionKey] = { type: 'content', entries: { - ...collectionInfo.entries, + ...(collectionInfo.entries as Record), [entryKey]: { slug: addedSlug }, }, }; @@ -356,6 +359,51 @@ function normalizeConfigPath(from: string, to: string) { return `"${isRelativePath(configPath) ? '' : './'}${normalizedPath}"` as const; } +const schemaCache = new Map(); + +async function getContentLayerSchema( + collection: ContentConfig['collections'][T], + collectionKey: T, +): Promise { + const cached = schemaCache.get(collectionKey); + if (cached) { + return cached; + } + + if ( + collection?.type === CONTENT_LAYER_TYPE && + typeof collection.loader === 'object' && + collection.loader.schema + ) { + let schema = collection.loader.schema; + if (typeof schema === 'function') { + schema = await schema(); + } + if (schema) { + schemaCache.set(collectionKey, await schema); + return schema; + } + } +} + +async function typeForCollection( + collection: ContentConfig['collections'][T] | undefined, + collectionKey: T, +): Promise { + if (collection?.schema) { + return `InferEntrySchema<${collectionKey}>`; + } + + if (collection?.type === CONTENT_LAYER_TYPE) { + const schema = await getContentLayerSchema(collection, collectionKey); + if (schema) { + const ast = zodToTs(schema); + return printNode(ast.node); + } + } + return 'any'; +} + async function writeContentFiles({ fs, contentPaths, @@ -391,12 +439,15 @@ async function writeContentFiles({ entries: {}, }; } + + let contentCollectionsMap: CollectionEntryMap = {}; for (const collectionKey of Object.keys(collectionEntryMap).sort()) { const collectionConfig = contentConfig?.collections[JSON.parse(collectionKey)]; const collection = collectionEntryMap[collectionKey]; if ( collectionConfig?.type && collection.type !== 'unknown' && + collectionConfig.type !== CONTENT_LAYER_TYPE && collection.type !== collectionConfig.type ) { viteServer.hot.send({ @@ -419,7 +470,7 @@ async function writeContentFiles({ }); return; } - const resolvedType: 'content' | 'data' = + const resolvedType = collection.type === 'unknown' ? // Add empty / unknown collections to the data type map by default // This ensures `getCollection('empty-collection')` doesn't raise a type error @@ -427,7 +478,7 @@ async function writeContentFiles({ : collection.type; const collectionEntryKeys = Object.keys(collection.entries).sort(); - const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any'; + const dataType = await typeForCollection(collectionConfig, collectionKey); switch (resolvedType) { case 'content': if (collectionEntryKeys.length === 0) { @@ -446,6 +497,9 @@ async function writeContentFiles({ } contentTypesStr += `};\n`; break; + case CONTENT_LAYER_TYPE: + dataTypesStr += `${collectionKey}: Record;\n`; + break; case 'data': if (collectionEntryKeys.length === 0) { dataTypesStr += `${collectionKey}: Record;\n`; @@ -458,40 +512,60 @@ async function writeContentFiles({ } if (collectionConfig?.schema) { - let zodSchemaForJson = - typeof collectionConfig.schema === 'function' - ? collectionConfig.schema({ image: () => z.string() }) - : collectionConfig.schema; - if (zodSchemaForJson instanceof z.ZodObject) { - zodSchemaForJson = zodSchemaForJson.extend({ - $schema: z.string().optional(), - }); - } - try { - await fs.promises.writeFile( - new URL(`./${collectionKey.replace(/"/g, '')}.schema.json`, collectionSchemasDir), - JSON.stringify( - zodToJsonSchema(zodSchemaForJson, { - name: collectionKey.replace(/"/g, ''), - markdownDescription: true, - errorMessages: true, - // Fix for https://github.com/StefanTerdell/zod-to-json-schema/issues/110 - dateStrategy: ['format:date-time', 'format:date', 'integer'], - }), - null, - 2, - ), - ); - } catch (err) { - // This should error gracefully and not crash the dev server - logger.warn( - 'content', - `An error was encountered while creating the JSON schema for the ${collectionKey} collection. Proceeding without it. Error: ${err}`, - ); - } + await generateJSONSchema( + fs, + collectionConfig, + collectionKey, + collectionSchemasDir, + logger, + ); } break; } + + if ( + settings.config.experimental.contentIntellisense && + collectionConfig && + (collectionConfig.schema || (await getContentLayerSchema(collectionConfig, collectionKey))) + ) { + await generateJSONSchema(fs, collectionConfig, collectionKey, collectionSchemasDir, logger); + + contentCollectionsMap[collectionKey] = collection; + } + } + + if (settings.config.experimental.contentIntellisense) { + let contentCollectionManifest: { + collections: { hasSchema: boolean; name: string }[]; + entries: Record; + } = { + collections: [], + entries: {}, + }; + Object.entries(contentCollectionsMap).forEach(([collectionKey, collection]) => { + const collectionConfig = contentConfig?.collections[JSON.parse(collectionKey)]; + const key = JSON.parse(collectionKey); + + contentCollectionManifest.collections.push({ + hasSchema: Boolean(collectionConfig?.schema || schemaCache.has(collectionKey)), + name: key, + }); + + Object.keys(collection.entries).forEach((entryKey) => { + const entryPath = new URL( + JSON.parse(entryKey), + contentPaths.contentDir + `${key}/`, + ).toString(); + + // Save entry path in lower case to avoid case sensitivity issues between Windows and Unix + contentCollectionManifest.entries[entryPath.toLowerCase()] = key; + }); + }); + + await fs.promises.writeFile( + new URL('./collections.json', collectionSchemasDir), + JSON.stringify(contentCollectionManifest, null, 2), + ); } if (!fs.existsSync(settings.dotAstroDir)) { @@ -499,7 +573,7 @@ async function writeContentFiles({ } const configPathRelativeToCacheDir = normalizeConfigPath( - settings.dotAstroDir.pathname, + new URL('astro', settings.dotAstroDir).pathname, contentPaths.config.url.pathname, ); @@ -515,8 +589,60 @@ async function writeContentFiles({ contentConfig ? `typeof import(${configPathRelativeToCacheDir})` : 'never', ); - await fs.promises.writeFile( - new URL(CONTENT_TYPES_FILE, settings.dotAstroDir), - typeTemplateContent, - ); + // If it's the first time, we inject types the usual way. sync() will handle creating files and references. If it's not the first time, we just override the dts content + if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) { + const filePath = fileURLToPath(new URL(CONTENT_TYPES_FILE, settings.dotAstroDir)); + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); + await fs.promises.writeFile(filePath, typeTemplateContent, 'utf-8'); + } else { + settings.injectedTypes.push({ + filename: CONTENT_TYPES_FILE, + content: typeTemplateContent, + }); + } +} + +async function generateJSONSchema( + fsMod: typeof import('node:fs'), + collectionConfig: CollectionConfig, + collectionKey: string, + collectionSchemasDir: URL, + logger: Logger, +) { + let zodSchemaForJson = + typeof collectionConfig.schema === 'function' + ? collectionConfig.schema({ image: () => z.string() }) + : collectionConfig.schema; + + if (!zodSchemaForJson && collectionConfig.type === CONTENT_LAYER_TYPE) { + zodSchemaForJson = await getContentLayerSchema(collectionConfig, collectionKey); + } + + if (zodSchemaForJson instanceof z.ZodObject) { + zodSchemaForJson = zodSchemaForJson.extend({ + $schema: z.string().optional(), + }); + } + try { + await fsMod.promises.writeFile( + new URL(`./${collectionKey.replace(/"/g, '')}.schema.json`, collectionSchemasDir), + JSON.stringify( + zodToJsonSchema(zodSchemaForJson, { + name: collectionKey.replace(/"/g, ''), + markdownDescription: true, + errorMessages: true, + // Fix for https://github.com/StefanTerdell/zod-to-json-schema/issues/110 + dateStrategy: ['format:date-time', 'format:date', 'integer'], + }), + null, + 2, + ), + ); + } catch (err) { + // This should error gracefully and not crash the dev server + logger.warn( + 'content', + `An error was encountered while creating the JSON schema for the ${collectionKey} collection. Proceeding without it. Error: ${err}`, + ); + } } diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index ce6dc63ca808..27b0e1915f4a 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -5,17 +5,22 @@ import { slug as githubSlug } from 'github-slugger'; import matter from 'gray-matter'; import type { PluginContext } from 'rollup'; import { type ViteDevServer, normalizePath } from 'vite'; +import xxhash from 'xxhash-wasm'; import { z } from 'zod'; -import type { - AstroConfig, - AstroSettings, - ContentEntryType, - DataEntryType, -} from '../@types/astro.js'; import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js'; import { isYAMLException } from '../core/errors/utils.js'; import type { Logger } from '../core/logger/core.js'; -import { CONTENT_FLAGS, PROPAGATED_ASSET_FLAG } from './consts.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { AstroConfig } from '../types/public/config.js'; +import type { ContentEntryType, DataEntryType } from '../types/public/content.js'; +import { + CONTENT_FLAGS, + CONTENT_LAYER_TYPE, + CONTENT_MODULE_FLAG, + DEFERRED_MODULE, + IMAGE_IMPORT_PREFIX, + PROPAGATED_ASSET_FLAG, +} from './consts.js'; import { createImage } from './runtime-assets.js'; /** * Amap from a collection + slug to the local file path. @@ -35,6 +40,54 @@ const collectionConfigParser = z.union([ type: z.literal('data'), schema: z.any().optional(), }), + z.object({ + type: z.literal(CONTENT_LAYER_TYPE), + schema: z.any().optional(), + loader: z.union([ + z.function().returns( + z.union([ + z.array( + z + .object({ + id: z.string(), + }) + .catchall(z.unknown()), + ), + z.promise( + z.array( + z + .object({ + id: z.string(), + }) + .catchall(z.unknown()), + ), + ), + ]), + ), + z.object({ + name: z.string(), + load: z.function( + z.tuple( + [ + z.object({ + collection: z.string(), + store: z.any(), + meta: z.any(), + logger: z.any(), + settings: z.any(), + parseData: z.any(), + generateDigest: z.function(z.tuple([z.any()], z.string())), + watcher: z.any().optional(), + }), + ], + z.unknown(), + ), + ), + schema: z.any().optional(), + render: z.function(z.tuple([z.any()], z.unknown())).optional(), + }), + ]), + }), ]); const contentConfigParser = z.object({ @@ -42,7 +95,7 @@ const contentConfigParser = z.object({ }); export type CollectionConfig = z.infer; -export type ContentConfig = z.infer; +export type ContentConfig = z.infer & { digest?: string }; type EntryInternal = { rawData: string | undefined; filePath: string }; @@ -67,30 +120,46 @@ export function parseEntrySlug({ } } -export async function getEntryData( +export async function getEntryDataAndImages< + TInputData extends Record = Record, + TOutputData extends TInputData = TInputData, +>( entry: { id: string; collection: string; - unvalidatedData: Record; + unvalidatedData: TInputData; _internal: EntryInternal; }, collectionConfig: CollectionConfig, shouldEmitFile: boolean, - pluginContext: PluginContext, -) { - let data; - if (collectionConfig.type === 'data') { - data = entry.unvalidatedData; + pluginContext?: PluginContext, +): Promise<{ data: TOutputData; imageImports: Array }> { + let data: TOutputData; + if (collectionConfig.type === 'data' || collectionConfig.type === CONTENT_LAYER_TYPE) { + data = entry.unvalidatedData as TOutputData; } else { const { slug, ...unvalidatedData } = entry.unvalidatedData; - data = unvalidatedData; + data = unvalidatedData as TOutputData; } let schema = collectionConfig.schema; + + const imageImports = new Set(); + if (typeof schema === 'function') { - schema = schema({ - image: createImage(pluginContext, shouldEmitFile, entry._internal.filePath), - }); + if (pluginContext) { + schema = schema({ + image: createImage(pluginContext, shouldEmitFile, entry._internal.filePath), + }); + } else if (collectionConfig.type === CONTENT_LAYER_TYPE) { + schema = schema({ + image: () => + z.string().transform((val) => { + imageImports.add(val); + return `${IMAGE_IMPORT_PREFIX}${val}`; + }), + }); + } } if (schema) { @@ -119,7 +188,7 @@ export async function getEntryData( }, }); if (parsed.success) { - data = parsed.data as Record; + data = parsed.data as TOutputData; } else { if (!formattedError) { formattedError = new AstroError({ @@ -139,6 +208,27 @@ export async function getEntryData( throw formattedError; } } + + return { data, imageImports: Array.from(imageImports) }; +} + +export async function getEntryData( + entry: { + id: string; + collection: string; + unvalidatedData: Record; + _internal: EntryInternal; + }, + collectionConfig: CollectionConfig, + shouldEmitFile: boolean, + pluginContext?: PluginContext, +) { + const { data } = await getEntryDataAndImages( + entry, + collectionConfig, + shouldEmitFile, + pluginContext, + ); return data; } @@ -383,6 +473,11 @@ export function hasContentFlag(viteId: string, flag: (typeof CONTENT_FLAGS)[numb return flags.has(flag); } +export function isDeferredModule(viteId: string): boolean { + const flags = new URLSearchParams(viteId.split('?')[1] ?? ''); + return flags.has(CONTENT_MODULE_FLAG); +} + async function loadContentConfig({ fs, settings, @@ -402,7 +497,10 @@ async function loadContentConfig({ const config = contentConfigParser.safeParse(unparsedConfig); if (config.success) { - return config.data; + // Generate a digest of the config file so we can invalidate the cache if it changes + const hasher = await xxhash(); + const digest = await hasher.h64ToString(await fs.promises.readFile(configPathname, 'utf-8')); + return { ...config.data, digest }; } else { return undefined; } @@ -556,3 +654,24 @@ export function hasAssetPropagationFlag(id: string): boolean { return false; } } + +/** + * Convert a platform path to a posix path. + */ +export function posixifyPath(filePath: string) { + return filePath.split(path.sep).join('/'); +} + +/** + * Unlike `path.posix.relative`, this function will accept a platform path and return a posix path. + */ +export function posixRelative(from: string, to: string) { + return posixifyPath(path.relative(from, to)); +} + +export function contentModuleToId(fileName: string) { + const params = new URLSearchParams(DEFERRED_MODULE); + params.set('fileName', fileName); + params.set(CONTENT_MODULE_FLAG, 'true'); + return `${DEFERRED_MODULE}?${params.toString()}`; +} diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index dd6dacc7cad9..059cd92fa6af 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -1,7 +1,6 @@ import { extname } from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin } from 'vite'; -import type { AstroSettings, SSRElement } from '../@types/astro.js'; import { getAssetsPrefix } from '../assets/utils/getAssetsPrefix.js'; import type { BuildInternals } from '../core/build/internal.js'; import type { AstroBuildPlugin } from '../core/build/plugin.js'; @@ -9,9 +8,12 @@ import type { StaticBuildOptions } from '../core/build/types.js'; import type { ModuleLoader } from '../core/module-loader/loader.js'; import { createViteLoader } from '../core/module-loader/vite.js'; import { joinPaths, prependForwardSlash } from '../core/path.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { SSRElement } from '../types/public/internal.js'; import { getStylesForURL } from '../vite-plugin-astro-server/css.js'; import { getScriptsForURL } from '../vite-plugin-astro-server/scripts.js'; import { + CONTENT_IMAGE_FLAG, CONTENT_RENDER_FLAG, LINKS_PLACEHOLDER, PROPAGATED_ASSET_FLAG, @@ -32,6 +34,17 @@ export function astroContentAssetPropagationPlugin({ name: 'astro:content-asset-propagation', enforce: 'pre', async resolveId(id, importer, opts) { + if (hasContentFlag(id, CONTENT_IMAGE_FLAG)) { + const [base, query] = id.split('?'); + const params = new URLSearchParams(query); + const importerParam = params.get('importer'); + + const importerPath = importerParam + ? fileURLToPath(new URL(importerParam, settings.config.root)) + : importer; + + return this.resolve(base, importerPath, { skipSelf: true, ...opts }); + } if (hasContentFlag(id, CONTENT_RENDER_FLAG)) { const base = id.split('?')[0]; diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index de642329a3d8..62f129052a8d 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -4,19 +4,19 @@ import { pathToFileURL } from 'node:url'; import * as devalue from 'devalue'; import type { PluginContext } from 'rollup'; import type { Plugin } from 'vite'; -import type { - AstroConfig, - AstroSettings, - ContentEntryModule, - ContentEntryType, - DataEntryModule, - DataEntryType, -} from '../@types/astro.js'; import { getProxyCode } from '../assets/utils/proxy.js'; import { AstroError } from '../core/errors/errors.js'; import { AstroErrorData } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import { isServerLikeOutput } from '../core/util.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { AstroConfig } from '../types/public/config.js'; +import type { + ContentEntryModule, + ContentEntryType, + DataEntryModule, + DataEntryType, +} from '../types/public/content.js'; import { CONTENT_FLAG, DATA_FLAG } from './consts.js'; import { type ContentConfig, @@ -158,6 +158,7 @@ export const _internal = { // The content config could depend on collection entries via `reference()`. // Reload the config in case of changes. + // Changes to the config file itself are handled in types-generator.ts, so we skip them here if (entryType === 'content' || entryType === 'data') { await reloadContentConfigObserver({ fs, settings, viteServer }); } 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 8c5365368de8..832ca0b171ad 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -1,21 +1,31 @@ import nodeFs from 'node:fs'; import { extname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { dataToEsm } from '@rollup/pluginutils'; import glob from 'fast-glob'; import pLimit from 'p-limit'; import type { Plugin } from 'vite'; -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 { isServerLikeOutput } from '../core/util.js'; import { rootRelativePath } from '../core/viteUtils.js'; +import type { AstroSettings } from '../types/astro.js'; import type { AstroPluginMetadata } from '../vite-plugin-astro/index.js'; import { createDefaultAstroMetadata } from '../vite-plugin-astro/metadata.js'; import { + ASSET_IMPORTS_FILE, + ASSET_IMPORTS_RESOLVED_STUB_ID, + ASSET_IMPORTS_VIRTUAL_ID, CONTENT_FLAG, CONTENT_RENDER_FLAG, DATA_FLAG, + DATA_STORE_FILE, + DATA_STORE_VIRTUAL_ID, + MODULES_IMPORTS_FILE, + MODULES_MJS_ID, + MODULES_MJS_VIRTUAL_ID, + RESOLVED_DATA_STORE_VIRTUAL_ID, RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID, } from './consts.js'; @@ -30,6 +40,7 @@ import { getEntrySlug, getEntryType, getExtGlob, + isDeferredModule, } from './utils.js'; interface AstroContentVirtualModPluginParams { @@ -43,13 +54,14 @@ export function astroContentVirtualModPlugin({ }: AstroContentVirtualModPluginParams): Plugin { let IS_DEV = false; const IS_SERVER = isServerLikeOutput(settings.config); + const dataStoreFile = new URL(DATA_STORE_FILE, settings.config.cacheDir); return { name: 'astro-content-virtual-mod-plugin', enforce: 'pre', configResolved(config) { IS_DEV = config.mode === 'development'; }, - resolveId(id) { + async resolveId(id) { if (id === VIRTUAL_MODULE_ID) { if (!settings.config.experimental.contentCollectionCache) { return RESOLVED_VIRTUAL_MODULE_ID; @@ -61,6 +73,38 @@ export function astroContentVirtualModPlugin({ return { id: RESOLVED_VIRTUAL_MODULE_ID, external: true }; } } + if (id === DATA_STORE_VIRTUAL_ID) { + return RESOLVED_DATA_STORE_VIRTUAL_ID; + } + + if (isDeferredModule(id)) { + const [, query] = id.split('?'); + const params = new URLSearchParams(query); + const fileName = params.get('fileName'); + let importerPath = undefined; + if (fileName && URL.canParse(fileName, settings.config.root.toString())) { + importerPath = fileURLToPath(new URL(fileName, settings.config.root)); + } + if (importerPath) { + return await this.resolve(importerPath); + } + } + + if (id === MODULES_MJS_ID) { + const modules = new URL(MODULES_IMPORTS_FILE, settings.dotAstroDir); + if (fs.existsSync(modules)) { + return fileURLToPath(modules); + } + return MODULES_MJS_VIRTUAL_ID; + } + + if (id === ASSET_IMPORTS_VIRTUAL_ID) { + const assetImportsFile = new URL(ASSET_IMPORTS_FILE, settings.dotAstroDir); + if (fs.existsSync(assetImportsFile)) { + return fileURLToPath(assetImportsFile); + } + return ASSET_IMPORTS_RESOLVED_STUB_ID; + } }, async load(id, args) { if (id === RESOLVED_VIRTUAL_MODULE_ID) { @@ -87,6 +131,41 @@ export function astroContentVirtualModPlugin({ } satisfies AstroPluginMetadata, }; } + if (id === RESOLVED_DATA_STORE_VIRTUAL_ID) { + if (!fs.existsSync(dataStoreFile)) { + return 'export default new Map()'; + } + const jsonData = await fs.promises.readFile(dataStoreFile, 'utf-8'); + + try { + const parsed = JSON.parse(jsonData); + return { + code: dataToEsm(parsed, { + compact: true, + }), + map: { mappings: '' }, + }; + } catch (err) { + const message = 'Could not parse JSON file'; + this.error({ message, id, cause: err }); + } + } + + if (id === ASSET_IMPORTS_RESOLVED_STUB_ID) { + const assetImportsFile = new URL(ASSET_IMPORTS_FILE, settings.dotAstroDir); + if (!fs.existsSync(assetImportsFile)) { + return 'export default new Map()'; + } + return fs.readFileSync(assetImportsFile, 'utf-8'); + } + + if (id === MODULES_MJS_VIRTUAL_ID) { + const modules = new URL(MODULES_IMPORTS_FILE, settings.dotAstroDir); + if (!fs.existsSync(modules)) { + return 'export default new Map()'; + } + return fs.readFileSync(modules, 'utf-8'); + } }, renderChunk(code, chunk) { if (!settings.config.experimental.contentCollectionCache) { @@ -98,6 +177,31 @@ export function astroContentVirtualModPlugin({ return code.replaceAll(RESOLVED_VIRTUAL_MODULE_ID, `${prefix}content/entry.mjs`); } }, + + configureServer(server) { + const dataStorePath = fileURLToPath(dataStoreFile); + // Watch for changes to the data store file + if (Array.isArray(server.watcher.options.ignored)) { + // The data store file is in node_modules, so is ignored by default, + // so we need to un-ignore it. + server.watcher.options.ignored.push(`!${dataStorePath}`); + } + server.watcher.add(dataStorePath); + + server.watcher.on('change', (changedPath) => { + // If the datastore file changes, invalidate the virtual module + if (changedPath === dataStorePath) { + const module = server.moduleGraph.getModuleById(RESOLVED_DATA_STORE_VIRTUAL_ID); + if (module) { + server.moduleGraph.invalidateModule(module); + } + server.ws.send({ + type: 'full-reload', + path: '*', + }); + } + }); + }, }; } diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 19bbee19548d..7cfe1c5dd741 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -1,3 +1,4 @@ +import { decodeKey } from '../encryption.js'; import { deserializeRouteData } from '../routing/manifest/serialization.js'; import type { RouteInfo, SSRManifest, SerializedSSRManifest } from './types.js'; @@ -18,6 +19,7 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): const inlinedScripts = new Map(serializedManifest.inlinedScripts); const clientDirectives = new Map(serializedManifest.clientDirectives); const serverIslandNameMap = new Map(serializedManifest.serverIslandNameMap); + const key = decodeKey(serializedManifest.key); return { // in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts) @@ -31,5 +33,6 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): clientDirectives, routes, serverIslandNameMap, + key, }; } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index d19a4da7d314..3ffa5f144647 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,5 +1,6 @@ -import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js'; import { normalizeTheLocale } from '../../i18n/index.js'; +import type { ManifestData } from '../../types/astro.js'; +import type { RouteData, SSRManifest } from '../../types/public/internal.js'; import { REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER, @@ -416,13 +417,15 @@ export class App { `${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`, url, ); - const response = await fetch(statusURL.toString()); + if (statusURL.toString() !== request.url) { + const response = await fetch(statusURL.toString()); - // response for /404.html and 500.html is 200, which is not meaningful - // so we create an override - const override = { status }; + // response for /404.html and 500.html is 200, which is not meaningful + // so we create an override + const override = { status }; - return this.#mergeResponses(response, originalResponse, override); + return this.#mergeResponses(response, originalResponse, override); + } } const mod = await this.#pipeline.getModuleForRoute(errorRouteData); try { diff --git a/packages/astro/src/core/app/middlewares.ts b/packages/astro/src/core/app/middlewares.ts index 095158b42ba3..a60b6baf4081 100644 --- a/packages/astro/src/core/app/middlewares.ts +++ b/packages/astro/src/core/app/middlewares.ts @@ -1,4 +1,4 @@ -import type { MiddlewareHandler } from '../../@types/astro.js'; +import type { MiddlewareHandler } from '../../types/public/common.js'; import { defineMiddleware } from '../middleware/index.js'; /** diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index f9afa6189d5a..86f0f5f65c46 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import type { IncomingMessage, ServerResponse } from 'node:http'; -import type { RouteData } from '../../@types/astro.js'; +import type { RouteData } from '../../types/public/internal.js'; import { deserializeManifest } from './common.js'; import { createOutgoingHttpHeaders } from './createOutgoingHttpHeaders.js'; import { App } from './index.js'; diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index b784ba916464..d42472f50493 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,11 +1,6 @@ -import type { - ComponentInstance, - ManifestData, - RewritePayload, - RouteData, - SSRElement, - SSRResult, -} from '../../@types/astro.js'; +import type { ComponentInstance, ManifestData } from '../../types/astro.js'; +import type { RewritePayload } from '../../types/public/common.js'; +import type { RouteData, SSRElement, SSRResult } from '../../types/public/internal.js'; import { Pipeline } from '../base-pipeline.js'; import type { SinglePageBuiltModule } from '../build/types.js'; import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 2e4e8d8057e9..29cb00ed4d1f 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -1,14 +1,13 @@ +import type { RoutingStrategies } from '../../i18n/utils.js'; +import type { ComponentInstance, SerializedRouteData } from '../../types/astro.js'; +import type { MiddlewareHandler } from '../../types/public/common.js'; +import type { Locales } from '../../types/public/config.js'; import type { - ComponentInstance, - Locales, - MiddlewareHandler, RouteData, SSRComponentMetadata, SSRLoadedRenderer, SSRResult, - SerializedRouteData, -} from '../../@types/astro.js'; -import type { RoutingStrategies } from '../../i18n/utils.js'; +} from '../../types/public/internal.js'; import type { SinglePageBuiltModule } from '../build/types.js'; export type ComponentPath = string; @@ -66,6 +65,7 @@ export type SSRManifest = { pageMap?: Map; serverIslandMap?: Map Promise>; serverIslandNameMap?: Map; + key: Promise; i18n: SSRManifestI18n | undefined; middleware: MiddlewareHandler; checkOrigin: boolean; @@ -81,6 +81,7 @@ export type SSRManifestI18n = { domainLookupTable: Record; }; +/** Public type exposed through the `astro:build:ssr` integration hook */ export type SerializedSSRManifest = Omit< SSRManifest, | 'middleware' @@ -90,6 +91,7 @@ export type SerializedSSRManifest = Omit< | 'inlinedScripts' | 'clientDirectives' | 'serverIslandNameMap' + | 'key' > & { routes: SerializedRouteInfo[]; assets: string[]; @@ -97,4 +99,5 @@ export type SerializedSSRManifest = Omit< inlinedScripts: [string, string][]; clientDirectives: [string, string][]; serverIslandNameMap: [string, string][]; + key: string; }; diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 01e18bfa0356..562b651744b1 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -1,15 +1,14 @@ +import { setGetEnv } from '../env/runtime.js'; +import { createI18nMiddleware } from '../i18n/middleware.js'; +import type { ComponentInstance } from '../types/astro.js'; +import type { MiddlewareHandler, RewritePayload } from '../types/public/common.js'; +import type { RuntimeMode } from '../types/public/config.js'; import type { - ComponentInstance, - MiddlewareHandler, - RewritePayload, RouteData, - RuntimeMode, SSRLoadedRenderer, SSRManifest, SSRResult, -} from '../@types/astro.js'; -import { setGetEnv } from '../env/runtime.js'; -import { createI18nMiddleware } from '../i18n/middleware.js'; +} from '../types/public/internal.js'; import { AstroError } from './errors/errors.js'; import { AstroErrorData } from './errors/index.js'; import type { Logger } from './logger/core.js'; diff --git a/packages/astro/src/core/build/common.ts b/packages/astro/src/core/build/common.ts index a479aed390dd..f9ed45836181 100644 --- a/packages/astro/src/core/build/common.ts +++ b/packages/astro/src/core/build/common.ts @@ -1,7 +1,8 @@ import npath from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import type { AstroConfig, RouteData } from '../../@types/astro.js'; import { appendForwardSlash } from '../../core/path.js'; +import type { AstroConfig } from '../../types/public/config.js'; +import type { RouteData } from '../../types/public/internal.js'; const STATUS_CODE_PAGES = new Set(['/404', '/500']); const FALLBACK_OUT_DIR_NAME = './.astro/'; diff --git a/packages/astro/src/core/build/css-asset-name.ts b/packages/astro/src/core/build/css-asset-name.ts index fbee7f2e2008..57277c989c2a 100644 --- a/packages/astro/src/core/build/css-asset-name.ts +++ b/packages/astro/src/core/build/css-asset-name.ts @@ -4,7 +4,7 @@ import crypto from 'node:crypto'; import npath from 'node:path'; import { fileURLToPath } from 'node:url'; import { normalizePath } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; +import type { AstroSettings } from '../../types/astro.js'; import { viteID } from '../util.js'; import { getTopLevelPageModuleInfos } from './graph.js'; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 5897ba7e4dfc..1f71f2d8a55d 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -2,18 +2,6 @@ import fs from 'node:fs'; import os from 'node:os'; import { bgGreen, black, blue, bold, dim, green, magenta, red } from 'kleur/colors'; import PQueue from 'p-queue'; -import type { - AstroConfig, - AstroSettings, - ComponentInstance, - GetStaticPathsItem, - MiddlewareHandler, - RouteData, - RouteType, - SSRError, - SSRLoadedRenderer, - SSRManifest, -} from '../../@types/astro.js'; import { generateImagesForPath, getStaticImageList, @@ -29,7 +17,16 @@ import { import { toRoutingStrategy } from '../../i18n/utils.js'; import { runHookBuildGenerated } from '../../integrations/hooks.js'; import { getOutputDirectory } from '../../prerender/utils.js'; -import type { SSRManifestI18n } from '../app/types.js'; +import type { AstroSettings, ComponentInstance } from '../../types/astro.js'; +import type { GetStaticPathsItem, MiddlewareHandler } from '../../types/public/common.js'; +import type { AstroConfig } from '../../types/public/config.js'; +import type { + RouteData, + RouteType, + SSRError, + SSRLoadedRenderer, +} from '../../types/public/internal.js'; +import type { SSRManifest, SSRManifestI18n } from '../app/types.js'; import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js'; @@ -77,6 +74,7 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil internals, renderers.renderers as SSRLoadedRenderer[], middleware, + options.key, ); } const pipeline = BuildPipeline.create({ internals, manifest, options }); @@ -521,6 +519,7 @@ function createBuildManifest( internals: BuildInternals, renderers: SSRLoadedRenderer[], middleware: MiddlewareHandler, + key: Promise, ): SSRManifest { let i18nManifest: SSRManifestI18n | undefined = undefined; if (settings.config.i18n) { @@ -551,6 +550,7 @@ function createBuildManifest( buildFormat: settings.config.build.format, middleware, checkOrigin: settings.config.security?.checkOrigin ?? false, + key, experimentalEnvGetSecretEnabled: false, }; } diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 8df72d8b22c9..74a648304789 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -3,13 +3,6 @@ import { performance } from 'node:perf_hooks'; import { fileURLToPath } from 'node:url'; import { blue, bold, green } from 'kleur/colors'; import type * as vite from 'vite'; -import type { - AstroConfig, - AstroInlineConfig, - AstroSettings, - ManifestData, - RuntimeMode, -} from '../../@types/astro.js'; import { injectImageEndpoint } from '../../assets/endpoint/config.js'; import { telemetry } from '../../events/index.js'; import { eventCliSession } from '../../events/session.js'; @@ -19,21 +12,24 @@ import { runHookConfigDone, runHookConfigSetup, } from '../../integrations/hooks.js'; +import type { AstroSettings, ManifestData } from '../../types/astro.js'; +import type { AstroConfig, AstroInlineConfig, RuntimeMode } from '../../types/public/config.js'; import { resolveConfig } from '../config/config.js'; import { createNodeLogger } from '../config/logging.js'; import { createSettings } from '../config/settings.js'; import { createVite } from '../create-vite.js'; +import { createKey } from '../encryption.js'; 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 { getServerIslandRouteData } from '../server-islands/endpoint.js'; +import { clearContentLayerCache } from '../sync/index.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'; import { getTimeStat } from './util.js'; - export interface BuildOptions { /** * Teardown the compiler WASM instance after build. This can improve performance when @@ -43,14 +39,6 @@ export interface BuildOptions { * @default true */ teardownCompiler?: boolean; - - /** - * If `experimental.contentCollectionCache` is enabled, this flag will clear the cache before building - * - * @internal not part of our public api - * @default false - */ - force?: boolean; } /** @@ -68,13 +56,16 @@ export default async function build( const logger = createNodeLogger(inlineConfig); const { userConfig, astroConfig } = await resolveConfig(inlineConfig, 'build'); telemetry.record(eventCliSession('build', userConfig)); - if (astroConfig.experimental.contentCollectionCache && options.force) { - const contentCacheDir = new URL('./content/', astroConfig.cacheDir); - if (fs.existsSync(contentCacheDir)) { - logger.debug('content', 'clearing content cache'); - await fs.promises.rm(contentCacheDir, { force: true, recursive: true }); - logger.warn('content', 'content cache cleared (force)'); + if (inlineConfig.force) { + if (astroConfig.experimental.contentCollectionCache) { + const contentCacheDir = new URL('./content/', astroConfig.cacheDir); + if (fs.existsSync(contentCacheDir)) { + logger.debug('content', 'clearing content cache'); + await fs.promises.rm(contentCacheDir, { force: true, recursive: true }); + logger.warn('content', 'content cache cleared (force)'); + } } + await clearContentLayerCache({ astroConfig, logger, fs }); } const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root)); @@ -201,6 +192,7 @@ class AstroBuilder { pageNames, teardownCompiler: this.teardownCompiler, viteConfig, + key: createKey(), }; const { internals, ssrOutputChunkNames, contentFileNames } = await viteBuild(opts); @@ -241,18 +233,21 @@ class AstroBuilder { buildMode: this.settings.config.output, }); } - - // Benchmark results - this.settings.timer.writeStats(); } /** Build the given Astro project. */ async run() { + this.settings.timer.start('Total build'); + const setupData = await this.setup(); try { await this.build(setupData); } catch (_err) { throw _err; + } finally { + this.settings.timer.end('Total build'); + // Benchmark results + this.settings.timer.writeStats(); } } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 5c28a4d40ed9..8c2d928244f2 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -1,5 +1,5 @@ import type { Rollup } from 'vite'; -import type { RouteData, SSRResult } from '../../@types/astro.js'; +import type { RouteData, SSRResult } from '../../types/public/internal.js'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; import { makePageDataKey } from './plugins/util.js'; diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index 210b31a0e56f..06ed629f2ce6 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -1,4 +1,4 @@ -import type { AstroSettings, ManifestData } from '../../@types/astro.js'; +import type { AstroSettings, ManifestData } from '../../types/astro.js'; import type { Logger } from '../logger/core.js'; import type { AllPagesData } from './types.js'; diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index 4e0d94c57ea6..18357e5ae62b 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,11 +1,7 @@ -import type { - ComponentInstance, - RewritePayload, - RouteData, - SSRLoadedRenderer, - SSRResult, -} from '../../@types/astro.js'; import { getOutputDirectory } from '../../prerender/utils.js'; +import type { ComponentInstance } from '../../types/astro.js'; +import type { RewritePayload } from '../../types/public/common.js'; +import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../types/public/internal.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'; diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts index 9c6d5add0b8d..5fe0b6792036 100644 --- a/packages/astro/src/core/build/plugins/plugin-content.ts +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -4,13 +4,13 @@ import { fileURLToPath } from 'node:url'; import glob from 'fast-glob'; 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 { generateContentEntryFile, generateLookupMap, } from '../../../content/vite-plugin-content-virtual-mod.js'; +import type { AstroConfig } from '../../../types/public/config.js'; import { configPaths } from '../../config/index.js'; import { emptyDir } from '../../fs/index.js'; import { 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 7c378490938a..2992c0f2d91d 100644 --- a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -1,5 +1,5 @@ import type { BuildOptions, Rollup, Plugin as VitePlugin } from 'vite'; -import type { AstroSettings } from '../../../@types/astro.js'; +import type { AstroSettings } from '../../../types/astro.js'; import { viteID } from '../../util.js'; import type { BuildInternals } from '../internal.js'; import { getPageDataByViteID } from '../internal.js'; diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index bb1add5b4553..6b38758c38c4 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -12,6 +12,7 @@ import type { SerializedRouteInfo, SerializedSSRManifest, } from '../../app/types.js'; +import { encodeKey } from '../../encryption.js'; import { fileExtension, joinPaths, prependForwardSlash } from '../../path.js'; import { serializeRouteData } from '../../routing/index.js'; import { addRollupInput } from '../add-rollup-input.js'; @@ -132,7 +133,8 @@ async function createManifest( } const staticFiles = internals.staticFiles; - return buildManifest(buildOpts, internals, Array.from(staticFiles)); + const encodedKey = await encodeKey(await buildOpts.key); + return buildManifest(buildOpts, internals, Array.from(staticFiles), encodedKey); } /** @@ -150,6 +152,7 @@ function buildManifest( opts: StaticBuildOptions, internals: BuildInternals, staticFiles: string[], + encodedKey: string, ): SerializedSSRManifest { const { settings } = opts; @@ -277,6 +280,7 @@ function buildManifest( buildFormat: settings.config.build.format, checkOrigin: settings.config.security?.checkOrigin ?? false, serverIslandNameMap: Array.from(settings.serverIslandNameMap), + key: encodedKey, experimentalEnvGetSecretEnabled: settings.config.experimental.env !== undefined && (settings.adapter?.supportedAstroFeatures.envGetSecret ?? 'unsupported') !== 'unsupported', diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index f395141a0fe9..888a4d44b177 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -1,8 +1,9 @@ import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin as VitePlugin } from 'vite'; -import type { AstroAdapter, AstroSettings } from '../../../@types/astro.js'; import { isFunctionPerRouteEnabled } from '../../../integrations/hooks.js'; +import type { AstroSettings } from '../../../types/astro.js'; +import type { AstroAdapter } from '../../../types/public/integrations.js'; import { routeIsRedirect } from '../../redirects/index.js'; import { VIRTUAL_ISLAND_MAP_ID } from '../../server-islands/vite-plugin-server-islands.js'; import { isServerLikeOutput } from '../../util.js'; @@ -19,6 +20,26 @@ import { getComponentFromVirtualModulePageName, getVirtualModulePageName } from export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID; +const ADAPTER_VIRTUAL_MODULE_ID = '@astrojs-ssr-adapter'; +const RESOLVED_ADAPTER_VIRTUAL_MODULE_ID = '\0' + ADAPTER_VIRTUAL_MODULE_ID; + +function vitePluginAdapter(adapter: AstroAdapter): VitePlugin { + return { + name: '@astrojs/vite-plugin-astro-adapter', + enforce: 'post', + resolveId(id) { + if (id === ADAPTER_VIRTUAL_MODULE_ID) { + return RESOLVED_ADAPTER_VIRTUAL_MODULE_ID; + } + }, + async load(id) { + if (id === RESOLVED_ADAPTER_VIRTUAL_MODULE_ID) { + return `export * from '${adapter.serverEntrypoint}';`; + } + }, + }; +} + function vitePluginSSR( internals: BuildInternals, adapter: AstroAdapter, @@ -37,6 +58,11 @@ function vitePluginSSR( inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component)); } + const adapterServerEntrypoint = options.settings.adapter?.serverEntrypoint; + if (adapterServerEntrypoint) { + inputs.add(ADAPTER_VIRTUAL_MODULE_ID); + } + inputs.add(SSR_VIRTUAL_MODULE_ID); return addRollupInput(opts, Array.from(inputs)); }, @@ -114,14 +140,19 @@ export function pluginSSR( targets: ['server'], hooks: { 'build:before': () => { - let vitePlugin = + const adapter = options.settings.adapter!; + let ssrPlugin = ssr && functionPerRouteEnabled === false - ? vitePluginSSR(internals, options.settings.adapter!, options) + ? vitePluginSSR(internals, adapter, options) : undefined; + const vitePlugin = [vitePluginAdapter(adapter)]; + if (ssrPlugin) { + vitePlugin.unshift(ssrPlugin); + } return { enforce: 'after-user-plugins', - vitePlugin, + vitePlugin: vitePlugin, }; }, 'build:post': async () => { @@ -226,10 +257,15 @@ export function pluginSSRSplit( targets: ['server'], hooks: { 'build:before': () => { - let vitePlugin = + const adapter = options.settings.adapter!; + let ssrPlugin = ssr && functionPerRouteEnabled - ? vitePluginSSRSplit(internals, options.settings.adapter!, options) + ? vitePluginSSRSplit(internals, adapter, options) : undefined; + const vitePlugin = [vitePluginAdapter(adapter)]; + if (ssrPlugin) { + vitePlugin.unshift(ssrPlugin); + } return { enforce: 'after-user-plugins', @@ -246,8 +282,8 @@ function generateSSRCode(settings: AstroSettings, adapter: AstroAdapter, middlew const imports = [ `import { renderers } from '${RENDERERS_MODULE_ID}';`, + `import * as serverEntrypointModule from '${ADAPTER_VIRTUAL_MODULE_ID}';`, `import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`, - `import * as serverEntrypointModule from '${adapter.serverEntrypoint}';`, edgeMiddleware ? `` : `import { onRequest as middleware } from '${middlewareId}';`, settings.config.experimental.serverIslands ? `import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';` diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 8626019562e2..96feb85420df 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -5,7 +5,6 @@ import { teardown } from '@astrojs/compiler'; import glob from 'fast-glob'; import { bgGreen, bgMagenta, black, green } from 'kleur/colors'; import * as vite from 'vite'; -import type { RouteData } from '../../@types/astro.js'; import { PROPAGATED_ASSET_FLAG } from '../../content/consts.js'; import { getSymlinkedContentCollections, @@ -22,6 +21,7 @@ import { appendForwardSlash, prependForwardSlash, removeFileExtension } from '.. import { isModeServerWithNoAdapter, isServerLikeOutput } from '../../core/util.js'; import { runHookBuildSetup } from '../../integrations/hooks.js'; import { getOutputDirectory } from '../../prerender/utils.js'; +import type { RouteData } from '../../types/public/internal.js'; import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; @@ -255,6 +255,8 @@ async function ssrBuild( return 'renderers.mjs'; } else if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) { return 'manifest_[hash].mjs'; + } else if (chunkInfo.facadeModuleId === settings.adapter?.serverEntrypoint) { + return 'adapter_[hash].mjs'; } else if ( settings.config.experimental.contentCollectionCache && chunkInfo.facadeModuleId && diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 11724b82449e..f06f42501cf1 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -1,14 +1,9 @@ import type * as vite from 'vite'; import type { InlineConfig } from 'vite'; -import type { - AstroSettings, - ComponentInstance, - ManifestData, - MiddlewareHandler, - RouteData, - RuntimeMode, - SSRLoadedRenderer, -} from '../../@types/astro.js'; +import type { AstroSettings, ComponentInstance, ManifestData } from '../../types/astro.js'; +import type { MiddlewareHandler } from '../../types/public/common.js'; +import type { RuntimeMode } from '../../types/public/config.js'; +import type { RouteData, SSRLoadedRenderer } from '../../types/public/internal.js'; import type { Logger } from '../logger/core.js'; export type ComponentPath = string; @@ -20,6 +15,7 @@ export type StylesheetAsset = export type HoistedScriptAsset = { type: 'inline' | 'external'; value: string }; +/** Public type exposed through the `astro:build:setup` integration hook */ export interface PageBuildData { key: string; component: ComponentPath; @@ -42,6 +38,7 @@ export interface StaticBuildOptions { pageNames: string[]; viteConfig: InlineConfig; teardownCompiler: boolean; + key: Promise; } type ImportComponentInstance = () => Promise; diff --git a/packages/astro/src/core/build/util.ts b/packages/astro/src/core/build/util.ts index 9dc61f06fd8d..b6b313254379 100644 --- a/packages/astro/src/core/build/util.ts +++ b/packages/astro/src/core/build/util.ts @@ -1,5 +1,5 @@ import type { Rollup } from 'vite'; -import type { AstroConfig } from '../../@types/astro.js'; +import type { AstroConfig } from '../../types/public/config.js'; import type { ViteBuildReturn } from './types.js'; export function getTimeStat(timeStart: number, timeEnd: number) { diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts index c3e6e4bb87a8..6e49f5850c63 100644 --- a/packages/astro/src/core/compile/compile.ts +++ b/packages/astro/src/core/compile/compile.ts @@ -1,11 +1,11 @@ import type { TransformResult } from '@astrojs/compiler'; import type { ResolvedConfig } from 'vite'; -import type { AstroConfig } from '../../@types/astro.js'; import { fileURLToPath } from 'node:url'; import { transform } from '@astrojs/compiler'; import { normalizePath } from 'vite'; import type { AstroPreferences } from '../../preferences/index.js'; +import type { AstroConfig } from '../../types/public/config.js'; import type { AstroError } from '../errors/errors.js'; import { AggregateError, CompilerError } from '../errors/errors.js'; import { AstroErrorData } from '../errors/index.js'; diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 2e43661a437a..3e19db8015f8 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -2,16 +2,14 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import * as colors from 'kleur/colors'; -import type { Arguments as Flags } from 'yargs-parser'; import { ZodError } from 'zod'; +import { eventConfigError, telemetry } from '../../events/index.js'; import type { AstroConfig, AstroInlineConfig, AstroInlineOnlyConfig, AstroUserConfig, - CLIFlags, -} from '../../@types/astro.js'; -import { eventConfigError, telemetry } from '../../events/index.js'; +} from '../../types/public/config.js'; import { trackAstroConfigZodError } from '../errors/errors.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { formatConfigErrorMessage } from '../messages.js'; @@ -19,23 +17,6 @@ import { mergeConfig } from './merge.js'; import { validateConfig } from './validate.js'; import { loadConfigWithVite } from './vite-load.js'; -/** Convert the generic "yargs" flag object into our own, custom TypeScript object. */ -// NOTE: This function will be removed in a later PR. Use `flagsToAstroInlineConfig` instead. -// All CLI related flow should be located in the `packages/astro/src/cli` directory. -export function resolveFlags(flags: Partial): CLIFlags { - return { - root: typeof flags.root === 'string' ? flags.root : undefined, - site: typeof flags.site === 'string' ? flags.site : undefined, - base: typeof flags.base === 'string' ? flags.base : undefined, - port: typeof flags.port === 'number' ? flags.port : undefined, - config: typeof flags.config === 'string' ? flags.config : undefined, - host: - typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined, - open: - typeof flags.open === 'string' || typeof flags.open === 'boolean' ? flags.open : undefined, - }; -} - export function resolveRoot(cwd?: string | URL): string { if (cwd instanceof URL) { cwd = fileURLToPath(cwd); @@ -66,7 +47,7 @@ async function search(fsMod: typeof fs, root: string) { interface ResolveConfigPathOptions { root: string; - configFile?: string; + configFile?: string | false; fs: typeof fs; } diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts index 3beaa5663523..7ffc290141cc 100644 --- a/packages/astro/src/core/config/index.ts +++ b/packages/astro/src/core/config/index.ts @@ -2,7 +2,6 @@ export { configPaths, resolveConfig, resolveConfigPath, - resolveFlags, resolveRoot, } from './config.js'; export { createNodeLogger } from './logging.js'; diff --git a/packages/astro/src/core/config/logging.ts b/packages/astro/src/core/config/logging.ts index 004283f85f0c..bd72f8b5e978 100644 --- a/packages/astro/src/core/config/logging.ts +++ b/packages/astro/src/core/config/logging.ts @@ -1,4 +1,4 @@ -import type { AstroInlineConfig } from '../../@types/astro.js'; +import type { AstroInlineConfig } from '../../types/public/config.js'; import { Logger } from '../logger/core.js'; import { nodeLogDestination } from '../logger/node.js'; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 9ffb58934ba0..9db1117180b8 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -6,13 +6,13 @@ import type { } from '@astrojs/markdown-remark'; import { markdownConfigDefaults } from '@astrojs/markdown-remark'; import { type BuiltinTheme, bundledThemes } from 'shiki'; -import type { AstroUserConfig, ViteUserConfig } from '../../@types/astro.js'; import type { OutgoingHttpHeaders } from 'node:http'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { z } from 'zod'; import { EnvSchema } from '../../env/schema.js'; +import type { AstroUserConfig, ViteUserConfig } from '../../types/public/config.js'; import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from '../path.js'; // The below types are required boilerplate to workaround a Zod issue since v3.21.2. Since that version, @@ -89,9 +89,11 @@ export const ASTRO_CONFIG_DEFAULTS = { clientPrerender: false, globalRoutePriority: false, serverIslands: false, + contentIntellisense: false, env: { validateSecrets: false, }, + contentLayer: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -538,6 +540,11 @@ export const AstroConfigSchema = z.object({ .boolean() .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.serverIslands), + contentIntellisense: z + .boolean() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.experimental.contentIntellisense), + contentLayer: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.contentLayer), }) .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/config/settings.ts b/packages/astro/src/core/config/settings.ts index c3c62edd45a0..902ff7d03318 100644 --- a/packages/astro/src/core/config/settings.ts +++ b/packages/astro/src/core/config/settings.ts @@ -1,9 +1,10 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import yaml from 'js-yaml'; -import type { AstroConfig, AstroSettings } from '../../@types/astro.js'; import { getContentPaths } from '../../content/index.js'; import createPreferences from '../../preferences/index.js'; +import type { AstroSettings } from '../../types/astro.js'; +import type { AstroConfig } from '../../types/public/config.js'; import { markdownContentEntryType } from '../../vite-plugin-markdown/content-entry-type.js'; import { getDefaultClientDirectives } from '../client-directive/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; @@ -14,7 +15,8 @@ import { loadTSConfig } from './tsconfig.js'; export function createBaseSettings(config: AstroConfig): AstroSettings { const { contentDir } = getContentPaths(config); - const preferences = createPreferences(config); + const dotAstroDir = new URL('.astro/', config.root); + const preferences = createPreferences(config, dotAstroDir); return { config, preferences, @@ -106,8 +108,9 @@ export function createBaseSettings(config: AstroConfig): AstroSettings { watchFiles: [], devToolbarApps: [], timer: new AstroTimer(), - dotAstroDir: new URL('.astro/', config.root), + dotAstroDir, latestAstroVersion: undefined, // Will be set later if applicable when the dev server starts + injectedTypes: [], }; } diff --git a/packages/astro/src/core/config/validate.ts b/packages/astro/src/core/config/validate.ts index 4f4497b784d8..75a64f1f7d17 100644 --- a/packages/astro/src/core/config/validate.ts +++ b/packages/astro/src/core/config/validate.ts @@ -1,4 +1,4 @@ -import type { AstroConfig } from '../../@types/astro.js'; +import type { AstroConfig } from '../../types/public/config.js'; import { createRelativeSchema } from './schema.js'; /** Turn raw config values into normalized values */ diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 0570d9d5d584..f2931523a6ed 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'; import glob from 'fast-glob'; import * as vite from 'vite'; import { crawlFrameworkPkgs } from 'vitefu'; -import type { AstroSettings } from '../@types/astro.js'; import { getAssetsPrefix } from '../assets/utils/getAssetsPrefix.js'; import astroAssetsPlugin from '../assets/vite-plugin-assets.js'; import astroContainer from '../container/vite-plugin-container.js'; @@ -17,6 +16,7 @@ import astroInternationalization from '../i18n/vite-plugin-i18n.js'; import astroPrefetch from '../prefetch/vite-plugin-prefetch.js'; import astroDevToolbar from '../toolbar/vite-plugin-dev-toolbar.js'; import astroTransitions from '../transitions/vite-plugin-transitions.js'; +import type { AstroSettings } from '../types/astro.js'; import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js'; import { vitePluginAstroServer } from '../vite-plugin-astro-server/index.js'; import astroVitePlugin from '../vite-plugin-astro/index.js'; @@ -132,7 +132,7 @@ export async function createVite( // The server plugin is for dev only and having it run during the build causes // the build to run very slow as the filewatcher is triggered often. mode !== 'build' && vitePluginAstroServer({ settings, logger, fs }), - envVitePlugin({ settings }), + envVitePlugin({ settings, logger }), astroEnv({ settings, mode, fs, sync }), markdownVitePlugin({ settings, logger }), htmlVitePlugin(), @@ -185,7 +185,7 @@ export async function createVite( { // Typings are imported from 'astro' (e.g. import { Type } from 'astro') find: /^astro$/, - replacement: fileURLToPath(new URL('../@types/astro.js', import.meta.url)), + replacement: fileURLToPath(new URL('../types/public/index.js', import.meta.url)), }, { find: 'astro:middleware', diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts index fc401ca710cf..f89e331970c5 100644 --- a/packages/astro/src/core/dev/container.ts +++ b/packages/astro/src/core/dev/container.ts @@ -1,6 +1,6 @@ import type * as http from 'node:http'; import type { AddressInfo } from 'node:net'; -import type { AstroInlineConfig, AstroSettings } from '../../@types/astro.js'; +import type { AstroSettings } from '../../types/astro.js'; import nodeFs from 'node:fs'; import * as vite from 'vite'; @@ -11,6 +11,7 @@ import { runHookServerDone, runHookServerStart, } from '../../integrations/hooks.js'; +import type { AstroInlineConfig } from '../../types/public/config.js'; import { createVite } from '../create-vite.js'; import type { Logger } from '../logger/core.js'; import { apply as applyPolyfill } from '../polyfill.js'; @@ -97,6 +98,7 @@ export async function createContainer({ skip: { content: true, }, + force: inlineConfig?.force, }); const viteServer = await vite.createServer(viteConfig); diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts index 3d8424174f4c..73ec0fa713e2 100644 --- a/packages/astro/src/core/dev/dev.ts +++ b/packages/astro/src/core/dev/dev.ts @@ -1,13 +1,17 @@ -import fs from 'node:fs'; +import fs, { existsSync } from 'node:fs'; import type http from 'node:http'; import type { AddressInfo } from 'node:net'; +import { performance } from 'node:perf_hooks'; import { green } from 'kleur/colors'; -import { performance } from 'perf_hooks'; import { gt, major, minor, patch } from 'semver'; import type * as vite from 'vite'; -import type { AstroInlineConfig } from '../../@types/astro.js'; +import { DATA_STORE_FILE } from '../../content/consts.js'; +import { globalContentLayer } from '../../content/content-layer.js'; import { attachContentServerListeners } from '../../content/index.js'; +import { MutableDataStore } from '../../content/mutable-data-store.js'; +import { globalContentConfigObserver } from '../../content/utils.js'; import { telemetry } from '../../events/index.js'; +import type { AstroInlineConfig } from '../../types/public/config.js'; import * as msg from '../messages.js'; import { ensureProcessNodeEnv } from '../util.js'; import { startContainer } from './container.js'; @@ -102,6 +106,34 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise handleServerRestart(); - // Set up shortcuts, overriding Vite's default shortcuts so it works for Astro + // Set up shortcuts + + const customShortcuts: Array = [ + // Disable default Vite shortcuts that don't work well with Astro + { key: 'r', description: '' }, + { key: 'u', description: '' }, + { key: 'c', description: '' }, + ]; + + if (restart.container.settings.config.experimental.contentLayer) { + customShortcuts.push({ + key: 's', + description: 'sync content layer', + action: () => { + if (globalContentLayer.initialized()) { + globalContentLayer.get().sync(); + } + }, + }); + } restart.container.viteServer.bindCLIShortcuts({ - customShortcuts: [ - // Disable Vite's builtin "r" (restart server), "u" (print server urls) and "c" (clear console) shortcuts - { key: 'r', description: '' }, - { key: 'u', description: '' }, - { key: 'c', description: '' }, - ], + customShortcuts, }); } setupContainer(); diff --git a/packages/astro/src/core/encryption.ts b/packages/astro/src/core/encryption.ts new file mode 100644 index 000000000000..ccfc9bdd274f --- /dev/null +++ b/packages/astro/src/core/encryption.ts @@ -0,0 +1,88 @@ +import { decodeBase64, decodeHex, encodeBase64, encodeHexUpperCase } from '@oslojs/encoding'; + +// Chose this algorithm for no particular reason, can change. +// This algo does check against text manipulation though. See +// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#aes-gcm +const ALGORITHM = 'AES-GCM'; + +/** + * Creates a CryptoKey object that can be used to encrypt any string. + */ +export async function createKey() { + const key = await crypto.subtle.generateKey( + { + name: ALGORITHM, + length: 256, + }, + true, + ['encrypt', 'decrypt'], + ); + return key; +} + +/** + * Takes a key that has been serialized to an array of bytes and returns a CryptoKey + */ +export async function importKey(bytes: Uint8Array): Promise { + const key = await crypto.subtle.importKey('raw', bytes, ALGORITHM, true, ['encrypt', 'decrypt']); + return key; +} + +/** + * Encodes a CryptoKey to base64 string, so that it can be embedded in JSON / JavaScript + */ +export async function encodeKey(key: CryptoKey) { + const exported = await crypto.subtle.exportKey('raw', key); + const encodedKey = encodeBase64(new Uint8Array(exported)); + return encodedKey; +} + +/** + * Decodes a base64 string into bytes and then imports the key. + */ +export async function decodeKey(encoded: string): Promise { + const bytes = decodeBase64(encoded); + return crypto.subtle.importKey('raw', bytes, ALGORITHM, true, ['encrypt', 'decrypt']); +} + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +// The length of the initialization vector +// See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams +const IV_LENGTH = 24; + +/** + * Using a CryptoKey, encrypt a string into a base64 string. + */ +export async function encryptString(key: CryptoKey, raw: string) { + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH / 2)); + const data = encoder.encode(raw); + const buffer = await crypto.subtle.encrypt( + { + name: ALGORITHM, + iv, + }, + key, + data, + ); + // iv is 12, hex brings it to 24 + return encodeHexUpperCase(iv) + encodeBase64(new Uint8Array(buffer)); +} + +/** + * Takes a base64 encoded string, decodes it and returns the decrypted text. + */ +export async function decryptString(key: CryptoKey, encoded: string) { + const iv = decodeHex(encoded.slice(0, IV_LENGTH)); + const dataArray = decodeBase64(encoded.slice(IV_LENGTH)); + const decryptedBuffer = await crypto.subtle.decrypt( + { + name: ALGORITHM, + iv, + }, + key, + dataArray, + ); + const decryptedString = decoder.decode(decryptedBuffer); + return decryptedString; +} diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts index 341c815a6762..c082a81937de 100644 --- a/packages/astro/src/core/errors/dev/utils.ts +++ b/packages/astro/src/core/errors/dev/utils.ts @@ -6,7 +6,7 @@ import { bold, underline } from 'kleur/colors'; import stripAnsi from 'strip-ansi'; import type { ESBuildTransformResult } from 'vite'; import { normalizePath } from 'vite'; -import type { SSRError } from '../../../@types/astro.js'; +import type { SSRError } from '../../../types/public/internal.js'; import { removeLeadingForwardSlashWindows } from '../../path.js'; import { AggregateError, type ErrorWithMetadata } from '../errors.js'; import { AstroErrorData } from '../index.js'; diff --git a/packages/astro/src/core/errors/dev/vite.ts b/packages/astro/src/core/errors/dev/vite.ts index 56688877a861..c944ad78d686 100644 --- a/packages/astro/src/core/errors/dev/vite.ts +++ b/packages/astro/src/core/errors/dev/vite.ts @@ -3,11 +3,11 @@ import { fileURLToPath } from 'node:url'; import { codeToHtml, createCssVariablesTheme } from 'shiki'; import type { ShikiTransformer } from 'shiki'; import type { ErrorPayload } from 'vite'; +import type { SSRLoadedRenderer } from '../../../types/public/internal.js'; import type { ModuleLoader } from '../../module-loader/index.js'; import { FailedToLoadModuleSSR, InvalidGlob, MdxIntegrationMissingError } from '../errors-data.js'; import { AstroError, type ErrorWithMetadata } from '../errors.js'; import { createSafeError } from '../utils.js'; -import type { SSRLoadedRenderer } from './../../../@types/astro.js'; import { getDocsForError, renderErrorMarkdown } from './utils.js'; export function enhanceViteSSRError({ diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 8415a9c0a31e..e083ba2f54ae 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1,5 +1,5 @@ // BEFORE ADDING AN ERROR: Please look at the README.md in this folder for general guidelines on writing error messages -// Additionally, this code, much like `@types/astro.ts`, is used to generate documentation, so make sure to pass +// Additionally, this code, much like `types/public/config.ts`, is used to generate documentation, so make sure to pass // your changes by our wonderful docs team before merging! import type { ZodError } from 'zod'; @@ -1291,6 +1291,17 @@ export const RewriteWithBodyUsed = { 'Astro.rewrite() cannot be used if the request body has already been read. If you need to read the body, first clone the request.', } satisfies ErrorData; +/** + * @docs + * @description + * An unknown error occurred while reading or writing files to disk. It can be caused by many things, eg. missing permissions or a file not existing we attempt to read. + */ +export const UnknownFilesystemError = { + name: 'UnknownFilesystemError', + title: 'An unknown error occurred while reading or writing files to disk.', + hint: 'It can be caused by many things, eg. missing permissions or a file not existing we attempt to read. Check the error cause for more details.', +} satisfies ErrorData; + /** * @docs * @kind heading @@ -1469,6 +1480,20 @@ export const UnknownContentCollectionError = { name: 'UnknownContentCollectionError', title: 'Unknown Content Collection Error.', } satisfies ErrorData; + +/** + * @docs + * @description + * The `getDataEntryById` and `getEntryBySlug` functions are deprecated and cannot be used with content layer collections. Use the `getEntry` function instead. + */ +export const GetEntryDeprecationError = { + name: 'GetEntryDeprecationError', + title: 'Invalid use of `getDataEntryById` or `getEntryBySlug` function.', + message: (collection: string, method: string) => + `The \`${method}\` function is deprecated and cannot be used to query the "${collection}" collection. Use \`getEntry\` instead.`, + hint: 'Use the `getEntry` or `getCollection` functions to query content layer collections.', +} satisfies ErrorData; + /** * @docs * @message diff --git a/packages/astro/src/core/errors/utils.ts b/packages/astro/src/core/errors/utils.ts index 05dd49071a48..6754656b9134 100644 --- a/packages/astro/src/core/errors/utils.ts +++ b/packages/astro/src/core/errors/utils.ts @@ -1,6 +1,6 @@ import type { YAMLException } from 'js-yaml'; import type { ErrorPayload as ViteErrorPayload } from 'vite'; -import type { SSRError } from '../../@types/astro.js'; +import type { SSRError } from '../../types/public/internal.js'; /** * Get the line and character based on the offset diff --git a/packages/astro/src/core/index.ts b/packages/astro/src/core/index.ts index e0f9f6c82412..14a8c2f99af0 100644 --- a/packages/astro/src/core/index.ts +++ b/packages/astro/src/core/index.ts @@ -1,6 +1,6 @@ // This is the main entrypoint when importing the `astro` package. -import type { AstroInlineConfig } from '../@types/astro.js'; +import type { AstroInlineConfig } from '../types/public/config.js'; import { default as _build } from './build/index.js'; import { default as _sync } from './sync/index.js'; @@ -23,4 +23,4 @@ export const build = (inlineConfig: AstroInlineConfig) => _build(inlineConfig); * @experimental The JavaScript API is experimental */ // Wrap `_sync` to prevent exposing internal options -export const sync = (inlineConfig: AstroInlineConfig) => _sync({ inlineConfig }); +export const sync = (inlineConfig: AstroInlineConfig) => _sync(inlineConfig); diff --git a/packages/astro/src/core/logger/vite.ts b/packages/astro/src/core/logger/vite.ts index ed62adc8d047..f1ed49dce0d2 100644 --- a/packages/astro/src/core/logger/vite.ts +++ b/packages/astro/src/core/logger/vite.ts @@ -1,4 +1,4 @@ -import { fileURLToPath } from 'url'; +import { fileURLToPath } from 'node:url'; import stripAnsi from 'strip-ansi'; import type { LogLevel, Rollup, Logger as ViteLogger } from 'vite'; import { isAstroError } from '../errors/errors.js'; diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 3bdc6052da99..4cc7b6586685 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -1,9 +1,9 @@ import type { - APIContext, MiddlewareHandler, MiddlewareNext, RewritePayload, -} from '../../@types/astro.js'; +} from '../../types/public/common.js'; +import type { APIContext } from '../../types/public/context.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; /** diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index 56ce0b76c364..31988ae02eac 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,10 +1,11 @@ -import type { APIContext, MiddlewareHandler, Params, RewritePayload } from '../../@types/astro.js'; import { createCallAction, createGetActionResult } from '../../actions/utils.js'; import { computeCurrentLocale, computePreferredLocale, computePreferredLocaleList, } from '../../i18n/utils.js'; +import type { MiddlewareHandler, Params, RewritePayload } from '../../types/public/common.js'; +import type { APIContext } from '../../types/public/context.js'; import { ASTRO_VERSION, clientAddressSymbol, clientLocalsSymbol } from '../constants.js'; import { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index ee08381e6fe4..aefa66a9205d 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,4 +1,5 @@ -import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js'; +import type { MiddlewareHandler, RewritePayload } from '../../types/public/common.js'; +import type { APIContext } from '../../types/public/context.js'; import { AstroCookies } from '../cookies/cookies.js'; import { defineMiddleware } from './index.js'; diff --git a/packages/astro/src/core/middleware/vite-plugin.ts b/packages/astro/src/core/middleware/vite-plugin.ts index bb7b54e727d3..8a76bed921c9 100644 --- a/packages/astro/src/core/middleware/vite-plugin.ts +++ b/packages/astro/src/core/middleware/vite-plugin.ts @@ -1,7 +1,7 @@ import type { Plugin as VitePlugin } from 'vite'; import { normalizePath } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; import { getOutputDirectory } from '../../prerender/utils.js'; +import type { AstroSettings } from '../../types/astro.js'; import { addRollupInput } from '../build/add-rollup-input.js'; import type { BuildInternals } from '../build/internal.js'; import type { StaticBuildOptions } from '../build/types.js'; diff --git a/packages/astro/src/core/module-loader/loader.ts b/packages/astro/src/core/module-loader/loader.ts index 976354448acc..9973ae6577e8 100644 --- a/packages/astro/src/core/module-loader/loader.ts +++ b/packages/astro/src/core/module-loader/loader.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:events'; import type * as fs from 'node:fs'; -import type { TypedEventEmitter } from '../../@types/typed-emitter.js'; +import type { TypedEventEmitter } from '../../types/typed-emitter.js'; // This is a generic interface for a module loader. In the astro cli this is // fulfilled by Vite, see vite.ts diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts index b46cf1becc57..a8a533fe85b7 100644 --- a/packages/astro/src/core/preview/index.ts +++ b/packages/astro/src/core/preview/index.ts @@ -1,11 +1,12 @@ import fs from 'node:fs'; import { createRequire } from 'node:module'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import type { AstroInlineConfig, PreviewModule, PreviewServer } from '../../@types/astro.js'; import { AstroIntegrationLogger } from '../../core/logger/core.js'; import { telemetry } from '../../events/index.js'; import { eventCliSession } from '../../events/session.js'; import { runHookConfigDone, runHookConfigSetup } from '../../integrations/hooks.js'; +import type { AstroInlineConfig } from '../../types/public/config.js'; +import type { PreviewModule, PreviewServer } from '../../types/public/preview.js'; import { resolveConfig } from '../config/config.js'; import { createNodeLogger } from '../config/logging.js'; import { createSettings } from '../config/settings.js'; diff --git a/packages/astro/src/core/preview/static-preview-server.ts b/packages/astro/src/core/preview/static-preview-server.ts index 68ca3236b296..855506ef91ce 100644 --- a/packages/astro/src/core/preview/static-preview-server.ts +++ b/packages/astro/src/core/preview/static-preview-server.ts @@ -1,8 +1,8 @@ import type http from 'node:http'; +import { performance } from 'node:perf_hooks'; import { fileURLToPath } from 'node:url'; -import { performance } from 'perf_hooks'; import { type PreviewServer as VitePreviewServer, preview } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; +import type { AstroSettings } from '../../types/astro.js'; import type { Logger } from '../logger/core.js'; import * as msg from '../messages.js'; import { getResolvedHostForHttpServer } from './util.js'; diff --git a/packages/astro/src/core/preview/vite-plugin-astro-preview.ts b/packages/astro/src/core/preview/vite-plugin-astro-preview.ts index a425807dccd2..fd9bbae66c57 100644 --- a/packages/astro/src/core/preview/vite-plugin-astro-preview.ts +++ b/packages/astro/src/core/preview/vite-plugin-astro-preview.ts @@ -2,8 +2,8 @@ import fs from 'node:fs'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { fileURLToPath } from 'node:url'; import type { Connect, Plugin } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js'; +import type { AstroSettings } from '../../types/astro.js'; import { cleanUrl } from '../../vite-plugin-utils/index.js'; import { stripBase } from './util.js'; diff --git a/packages/astro/src/core/redirects/component.ts b/packages/astro/src/core/redirects/component.ts index 00b8d176c1d2..12b37ae0091b 100644 --- a/packages/astro/src/core/redirects/component.ts +++ b/packages/astro/src/core/redirects/component.ts @@ -1,4 +1,4 @@ -import type { ComponentInstance } from '../../@types/astro.js'; +import type { ComponentInstance } from '../../types/astro.js'; import type { SinglePageBuiltModule } from '../build/types.js'; // A stub of a component instance for a given route diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts index 80f18ae0f123..a2dc42df96e5 100644 --- a/packages/astro/src/core/redirects/helpers.ts +++ b/packages/astro/src/core/redirects/helpers.ts @@ -1,4 +1,8 @@ -import type { RedirectRouteData, RouteData } from '../../@types/astro.js'; +import type { RouteData } from '../../types/public/internal.js'; + +type RedirectRouteData = RouteData & { + redirect: string; +}; export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData { return route?.type === 'redirect'; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index a57221574495..ab0554d0c03d 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -1,14 +1,3 @@ -import type { - APIContext, - AstroGlobal, - AstroGlobalPartial, - ComponentInstance, - MiddlewareHandler, - Props, - RewritePayload, - RouteData, - SSRResult, -} from '../@types/astro.js'; import type { ActionAPIContext } from '../actions/runtime/utils.js'; import { deserializeActionResult } from '../actions/runtime/virtual/shared.js'; import { createCallAction, createGetActionResult, hasActionPayload } from '../actions/utils.js'; @@ -19,6 +8,10 @@ import { } from '../i18n/utils.js'; import { renderEndpoint } from '../runtime/server/endpoint.js'; import { renderPage } from '../runtime/server/index.js'; +import type { ComponentInstance } from '../types/astro.js'; +import type { MiddlewareHandler, Props, RewritePayload } from '../types/public/common.js'; +import type { APIContext, AstroGlobal, AstroGlobalPartial } from '../types/public/context.js'; +import type { RouteData, SSRResult } from '../types/public/internal.js'; import { ASTRO_VERSION, REROUTE_DIRECTIVE_HEADER, @@ -344,6 +337,7 @@ export class RenderContext { styles, actionResult, serverIslandNameMap: manifest.serverIslandNameMap ?? new Map(), + key: manifest.key, trailingSlash: manifest.trailingSlash, _metadata: { hasHydrationScript: false, diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts index db14701b0fde..b56a2eaf2cc0 100644 --- a/packages/astro/src/core/render/index.ts +++ b/packages/astro/src/core/render/index.ts @@ -1,4 +1,5 @@ -import type { ComponentInstance, RouteData } from '../../@types/astro.js'; +import type { ComponentInstance } from '../../types/astro.js'; +import type { RouteData } from '../../types/public/internal.js'; import type { Pipeline } from '../base-pipeline.js'; export { Pipeline } from '../base-pipeline.js'; export { getParams, getProps } from './params-and-props.js'; diff --git a/packages/astro/src/core/render/paginate.ts b/packages/astro/src/core/render/paginate.ts index e962d98d31f4..c462cd4b8646 100644 --- a/packages/astro/src/core/render/paginate.ts +++ b/packages/astro/src/core/render/paginate.ts @@ -4,8 +4,8 @@ import type { PaginateOptions, Params, Props, - RouteData, -} from '../../@types/astro.js'; +} from '../../types/public/common.js'; +import type { RouteData } from '../../types/public/internal.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; export function generatePaginateFunction( diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts index cf7d02d48360..a45fb16a8084 100644 --- a/packages/astro/src/core/render/params-and-props.ts +++ b/packages/astro/src/core/render/params-and-props.ts @@ -1,4 +1,6 @@ -import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro.js'; +import type { ComponentInstance } from '../../types/astro.js'; +import type { Params, Props } from '../../types/public/common.js'; +import type { RouteData } from '../../types/public/internal.js'; import { DEFAULT_404_COMPONENT } from '../constants.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; diff --git a/packages/astro/src/core/render/renderer.ts b/packages/astro/src/core/render/renderer.ts index 42cc8fd05d64..8daec8d53e5f 100644 --- a/packages/astro/src/core/render/renderer.ts +++ b/packages/astro/src/core/render/renderer.ts @@ -1,4 +1,5 @@ -import type { AstroRenderer, SSRLoadedRenderer } from '../../@types/astro.js'; +import type { AstroRenderer } from '../../types/public/integrations.js'; +import type { SSRLoadedRenderer } from '../../types/public/internal.js'; import type { ModuleLoader } from '../module-loader/index.js'; export async function loadRenderer( diff --git a/packages/astro/src/core/render/route-cache.ts b/packages/astro/src/core/render/route-cache.ts index 399795675e06..3329c42cd347 100644 --- a/packages/astro/src/core/render/route-cache.ts +++ b/packages/astro/src/core/render/route-cache.ts @@ -1,13 +1,13 @@ +import type { ComponentInstance } from '../../types/astro.js'; import type { - ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, PaginateFunction, Params, - RouteData, - RuntimeMode, -} from '../../@types/astro.js'; +} from '../../types/public/common.js'; +import type { RuntimeMode } from '../../types/public/config.js'; +import type { RouteData } from '../../types/public/internal.js'; import type { Logger } from '../logger/core.js'; import { stringifyParams } from '../routing/params.js'; diff --git a/packages/astro/src/core/render/slots.ts b/packages/astro/src/core/render/slots.ts index ab87204e465e..1c767083da71 100644 --- a/packages/astro/src/core/render/slots.ts +++ b/packages/astro/src/core/render/slots.ts @@ -1,8 +1,8 @@ -import type { SSRResult } from '../../@types/astro.js'; import { type ComponentSlots, renderSlotToString } from '../../runtime/server/index.js'; import { renderJSX } from '../../runtime/server/jsx.js'; import { chunkToString } from '../../runtime/server/render/index.js'; import { isRenderInstruction } from '../../runtime/server/render/instruction.js'; +import type { SSRResult } from '../../types/public/internal.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; diff --git a/packages/astro/src/core/render/ssr-element.ts b/packages/astro/src/core/render/ssr-element.ts index 7c766ee20e4a..827c8fddf40d 100644 --- a/packages/astro/src/core/render/ssr-element.ts +++ b/packages/astro/src/core/render/ssr-element.ts @@ -1,7 +1,7 @@ -import type { AssetsPrefix, SSRElement } from '../../@types/astro.js'; import { getAssetsPrefix } from '../../assets/utils/getAssetsPrefix.js'; import { fileExtension, joinPaths, prependForwardSlash, slash } from '../../core/path.js'; -import type { StylesheetAsset } from '../app/types.js'; +import type { SSRElement } from '../../types/public/internal.js'; +import type { AssetsPrefix, StylesheetAsset } from '../app/types.js'; export function createAssetLink(href: string, base?: string, assetsPrefix?: AssetsPrefix): string { if (assetsPrefix) { diff --git a/packages/astro/src/core/routing/astro-designed-error-pages.ts b/packages/astro/src/core/routing/astro-designed-error-pages.ts index 4e4b41b70cf7..2c1c1f77c5a3 100644 --- a/packages/astro/src/core/routing/astro-designed-error-pages.ts +++ b/packages/astro/src/core/routing/astro-designed-error-pages.ts @@ -1,5 +1,6 @@ -import type { ComponentInstance, ManifestData, RouteData } from '../../@types/astro.js'; import notFoundTemplate from '../../template/4xx.js'; +import type { ComponentInstance, ManifestData } from '../../types/astro.js'; +import type { RouteData } from '../../types/public/internal.js'; import { DEFAULT_404_COMPONENT, DEFAULT_500_COMPONENT } from '../constants.js'; export const DEFAULT_404_ROUTE: RouteData = { diff --git a/packages/astro/src/core/routing/default.ts b/packages/astro/src/core/routing/default.ts index dd3c8cc538c1..8bcd473d0081 100644 --- a/packages/astro/src/core/routing/default.ts +++ b/packages/astro/src/core/routing/default.ts @@ -1,4 +1,5 @@ -import type { ComponentInstance, ManifestData, SSRManifest } from '../../@types/astro.js'; +import type { ComponentInstance, ManifestData } from '../../types/astro.js'; +import type { SSRManifest } from '../app/types.js'; import { DEFAULT_404_COMPONENT } from '../constants.js'; import { SERVER_ISLAND_COMPONENT, diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index f9c19b9ed0fd..14980f63e5ad 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -1,20 +1,16 @@ -import type { - AstroConfig, - AstroSettings, - ManifestData, - RouteData, - RoutePart, - RoutePriorityOverride, -} from '../../../@types/astro.js'; +import type { AstroSettings, ManifestData } from '../../../types/astro.js'; import type { Logger } from '../../logger/core.js'; -import { createRequire } from 'module'; import nodeFs from 'node:fs'; +import { createRequire } from 'node:module'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { bold } from 'kleur/colors'; import { toRoutingStrategy } from '../../../i18n/utils.js'; import { getPrerenderDefault } from '../../../prerender/utils.js'; +import type { AstroConfig } from '../../../types/public/config.js'; +import type { RoutePriorityOverride } from '../../../types/public/integrations.js'; +import type { RouteData, RoutePart } from '../../../types/public/internal.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js'; import { MissingIndexForInternationalization } from '../../errors/errors-data.js'; import { AstroError } from '../../errors/index.js'; diff --git a/packages/astro/src/core/routing/manifest/generator.ts b/packages/astro/src/core/routing/manifest/generator.ts index 4ab635ec6608..e3565864d91d 100644 --- a/packages/astro/src/core/routing/manifest/generator.ts +++ b/packages/astro/src/core/routing/manifest/generator.ts @@ -1,6 +1,6 @@ -import type { AstroConfig, RoutePart } from '../../../@types/astro.js'; - import { compile } from 'path-to-regexp'; +import type { AstroConfig } from '../../../types/public/config.js'; +import type { RoutePart } from '../../../types/public/internal.js'; /** * Sanitizes the parameters object by normalizing string values and replacing certain characters with their URL-encoded equivalents. diff --git a/packages/astro/src/core/routing/manifest/pattern.ts b/packages/astro/src/core/routing/manifest/pattern.ts index 65f223aa0c20..8a9a9d27f9fc 100644 --- a/packages/astro/src/core/routing/manifest/pattern.ts +++ b/packages/astro/src/core/routing/manifest/pattern.ts @@ -1,4 +1,5 @@ -import type { AstroConfig, RoutePart } from '../../../@types/astro.js'; +import type { AstroConfig } from '../../../types/public/config.js'; +import type { RoutePart } from '../../../types/public/internal.js'; export function getPattern( segments: RoutePart[][], diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts index 852aa703bf9d..c0cf600f0b79 100644 --- a/packages/astro/src/core/routing/manifest/serialization.ts +++ b/packages/astro/src/core/routing/manifest/serialization.ts @@ -1,4 +1,6 @@ -import type { AstroConfig, RouteData, SerializedRouteData } from '../../../@types/astro.js'; +import type { SerializedRouteData } from '../../../types/astro.js'; +import type { AstroConfig } from '../../../types/public/config.js'; +import type { RouteData } from '../../../types/public/internal.js'; import { getRouteGenerator } from './generator.js'; diff --git a/packages/astro/src/core/routing/match.ts b/packages/astro/src/core/routing/match.ts index 40a8b95fba29..403b3fb9535b 100644 --- a/packages/astro/src/core/routing/match.ts +++ b/packages/astro/src/core/routing/match.ts @@ -1,4 +1,5 @@ -import type { ManifestData, RouteData } from '../../@types/astro.js'; +import type { ManifestData } from '../../types/astro.js'; +import type { RouteData } from '../../types/public/internal.js'; /** Find matching route from pathname */ export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined { diff --git a/packages/astro/src/core/routing/params.ts b/packages/astro/src/core/routing/params.ts index 43cd24cc13a7..802c39cc5043 100644 --- a/packages/astro/src/core/routing/params.ts +++ b/packages/astro/src/core/routing/params.ts @@ -1,4 +1,5 @@ -import type { GetStaticPathsItem, Params, RouteData } from '../../@types/astro.js'; +import type { GetStaticPathsItem, Params } from '../../types/public/common.js'; +import type { RouteData } from '../../types/public/internal.js'; import { trimSlashes } from '../path.js'; import { validateGetStaticPathsParameter } from './validation.js'; diff --git a/packages/astro/src/core/routing/priority.ts b/packages/astro/src/core/routing/priority.ts index 4082683970e5..dc1c665c6156 100644 --- a/packages/astro/src/core/routing/priority.ts +++ b/packages/astro/src/core/routing/priority.ts @@ -1,4 +1,4 @@ -import type { RouteData } from '../../@types/astro.js'; +import type { RouteData } from '../../types/public/internal.js'; /** * Comparator for sorting routes in resolution order. diff --git a/packages/astro/src/core/routing/rewrite.ts b/packages/astro/src/core/routing/rewrite.ts index f30caed08c7b..a6fce3354929 100644 --- a/packages/astro/src/core/routing/rewrite.ts +++ b/packages/astro/src/core/routing/rewrite.ts @@ -1,4 +1,6 @@ -import type { AstroConfig, RewritePayload, RouteData } from '../../@types/astro.js'; +import type { RewritePayload } from '../../types/public/common.js'; +import type { AstroConfig } from '../../types/public/config.js'; +import type { RouteData } from '../../types/public/internal.js'; import { shouldAppendForwardSlash } from '../build/util.js'; import { appendForwardSlash, removeTrailingForwardSlash } from '../path.js'; import { DEFAULT_404_ROUTE } from './astro-designed-error-pages.js'; diff --git a/packages/astro/src/core/routing/validation.ts b/packages/astro/src/core/routing/validation.ts index 1f11f55e6cd4..a2f9a25ba320 100644 --- a/packages/astro/src/core/routing/validation.ts +++ b/packages/astro/src/core/routing/validation.ts @@ -1,4 +1,6 @@ -import type { ComponentInstance, GetStaticPathsResult, RouteData } from '../../@types/astro.js'; +import type { ComponentInstance } from '../../types/astro.js'; +import type { GetStaticPathsResult } from '../../types/public/common.js'; +import type { RouteData } from '../../types/public/internal.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts index 638e22882952..73ed5717704d 100644 --- a/packages/astro/src/core/server-islands/endpoint.ts +++ b/packages/astro/src/core/server-islands/endpoint.ts @@ -1,9 +1,3 @@ -import type { - ComponentInstance, - ManifestData, - RouteData, - SSRManifest, -} from '../../@types/astro.js'; import { type AstroComponentFactory, type ComponentSlots, @@ -11,6 +5,9 @@ import { renderTemplate, } from '../../runtime/server/index.js'; import { createSlotValueFromString } from '../../runtime/server/render/slot.js'; +import type { ComponentInstance, ManifestData } from '../../types/astro.js'; +import type { RouteData, SSRManifest } from '../../types/public/internal.js'; +import { decryptString } from '../encryption.js'; import { getPattern } from '../routing/manifest/pattern.js'; export const SERVER_ISLAND_ROUTE = '/_server-islands/[name]'; @@ -48,7 +45,7 @@ export function ensureServerIslandRoute(config: ConfigFields, routeManifest: Man type RenderOptions = { componentExport: string; - props: Record; + encryptedProps: string; slots: Record; }; @@ -74,7 +71,11 @@ export function createEndpoint(manifest: SSRManifest) { }); } - const props = data.props; + const key = await manifest.key; + const encryptedProps = data.encryptedProps; + const propString = await decryptString(key, encryptedProps); + const props = JSON.parse(propString); + const componentModule = await imp(); const Component = (componentModule as any)[data.componentExport]; diff --git a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts index 573ae9cf9703..8bc79e087d56 100644 --- a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts +++ b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts @@ -1,5 +1,5 @@ import type { ConfigEnv, ViteDevServer, Plugin as VitePlugin } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; +import type { AstroSettings } from '../../types/astro.js'; import type { AstroPluginMetadata } from '../../vite-plugin-astro/index.js'; export const VIRTUAL_ISLAND_MAP_ID = '@astro-server-islands'; diff --git a/packages/astro/src/core/sync/constants.ts b/packages/astro/src/core/sync/constants.ts new file mode 100644 index 000000000000..7ff603105a75 --- /dev/null +++ b/packages/astro/src/core/sync/constants.ts @@ -0,0 +1,2 @@ +// TODO: use types.d.ts for backward compatibility. Use astro.d.ts in Astro 5.0 +export const REFERENCE_FILE = './types.d.ts'; diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index 7b0d3268a00c..02ab6bf9e137 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -1,16 +1,19 @@ -import fsMod from 'node:fs'; +import fsMod, { existsSync } from 'node:fs'; import { performance } from 'node:perf_hooks'; import { fileURLToPath } from 'node:url'; import { dim } from 'kleur/colors'; import { type HMRPayload, createServer } from 'vite'; -import type { AstroConfig, AstroInlineConfig, AstroSettings } from '../../@types/astro.js'; -import { getPackage } from '../../cli/install-package.js'; +import { CONTENT_TYPES_FILE, DATA_STORE_FILE } from '../../content/consts.js'; +import { globalContentLayer } from '../../content/content-layer.js'; import { createContentTypesGenerator } from '../../content/index.js'; -import { globalContentConfigObserver } from '../../content/utils.js'; +import { MutableDataStore } from '../../content/mutable-data-store.js'; +import { getContentPaths, globalContentConfigObserver } from '../../content/utils.js'; import { syncAstroEnv } from '../../env/sync.js'; import { telemetry } from '../../events/index.js'; import { eventCliSession } from '../../events/session.js'; -import { runHookConfigSetup } from '../../integrations/hooks.js'; +import { runHookConfigDone, runHookConfigSetup } from '../../integrations/hooks.js'; +import type { AstroSettings } from '../../types/astro.js'; +import type { AstroConfig, AstroInlineConfig } from '../../types/public/config.js'; import { getTimeStat } from '../build/util.js'; import { resolveConfig } from '../config/config.js'; import { createNodeLogger } from '../config/logging.js'; @@ -27,7 +30,7 @@ import { import type { Logger } from '../logger/core.js'; import { formatErrorMessage } from '../messages.js'; import { ensureProcessNodeEnv } from '../util.js'; -import { setUpEnvTs } from './setup-env-ts.js'; +import { writeFiles } from './write-files.js'; export type SyncOptions = { /** @@ -36,21 +39,17 @@ export type SyncOptions = { fs?: typeof fsMod; logger: Logger; settings: AstroSettings; + force?: boolean; skip?: { // Must be skipped in dev content?: boolean; }; }; -type DBPackage = { - typegen?: (args: Pick) => Promise; -}; - -export default async function sync({ - inlineConfig, - fs, - telemetry: _telemetry = false, -}: { inlineConfig: AstroInlineConfig; fs?: typeof fsMod; telemetry?: boolean }) { +export default async function sync( + inlineConfig: AstroInlineConfig, + { fs, telemetry: _telemetry = false }: { fs?: typeof fsMod; telemetry?: boolean } = {}, +) { ensureProcessNodeEnv('production'); const logger = createNodeLogger(inlineConfig); const { astroConfig, userConfig } = await resolveConfig(inlineConfig ?? {}, 'sync'); @@ -63,7 +62,24 @@ export default async function sync({ settings, logger, }); - return await syncInternal({ settings, logger, fs }); + await runHookConfigDone({ settings, logger }); + return await syncInternal({ settings, logger, fs, force: inlineConfig.force }); +} + +/** + * Clears the content layer and content collection cache, forcing a full rebuild. + */ +export async function clearContentLayerCache({ + astroConfig, + logger, + fs = fsMod, +}: { astroConfig: AstroConfig; logger: Logger; fs?: typeof fsMod }) { + const dataStore = new URL(DATA_STORE_FILE, astroConfig.cacheDir); + if (fs.existsSync(dataStore)) { + logger.debug('content', 'clearing data store'); + await fs.promises.rm(dataStore, { force: true }); + logger.warn('content', 'data store cleared (force)'); + } } /** @@ -77,28 +93,49 @@ export async function syncInternal({ fs = fsMod, settings, skip, + force, }: SyncOptions): Promise { - const cwd = fileURLToPath(settings.config.root); + if (force) { + await clearContentLayerCache({ astroConfig: settings.config, logger, fs }); + } const timerStart = performance.now(); - const dbPackage = await getPackage( - '@astrojs/db', - logger, - { - optional: true, - cwd, - }, - [], - ); try { - await dbPackage?.typegen?.(settings.config); if (!skip?.content) { await syncContentCollections(settings, { fs, logger }); + settings.timer.start('Sync content layer'); + let store: MutableDataStore | undefined; + try { + const dataStoreFile = new URL(DATA_STORE_FILE, settings.config.cacheDir); + if (existsSync(dataStoreFile)) { + store = await MutableDataStore.fromFile(dataStoreFile); + } + } catch (err: any) { + logger.error('content', err.message); + } + if (!store) { + store = new MutableDataStore(); + } + const contentLayer = globalContentLayer.init({ + settings, + logger, + store, + }); + await contentLayer.sync(); + settings.timer.end('Sync content layer'); + } else if (fs.existsSync(fileURLToPath(getContentPaths(settings.config, fs).contentDir))) { + // Content is synced after writeFiles. That means references are not created + // To work around it, we create a stub so the reference is created and content + // sync will override the empty file + settings.injectedTypes.push({ + filename: CONTENT_TYPES_FILE, + content: '', + }); } syncAstroEnv(settings, fs); - await setUpEnvTs({ settings, logger, fs }); + await writeFiles(settings, fs, logger); logger.info('types', `Generated ${dim(getTimeStat(timerStart, performance.now()))}`); } catch (err) { const error = createSafeError(err); diff --git a/packages/astro/src/core/sync/setup-env-ts.ts b/packages/astro/src/core/sync/setup-env-ts.ts deleted file mode 100644 index 39eb062e5bcd..000000000000 --- a/packages/astro/src/core/sync/setup-env-ts.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type fsMod from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { bold } from 'kleur/colors'; -import { normalizePath } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; -import { ACTIONS_TYPES_FILE } from '../../actions/consts.js'; -import { CONTENT_TYPES_FILE } from '../../content/consts.js'; -import { ENV_TYPES_FILE } from '../../env/constants.js'; -import { type Logger } from '../logger/core.js'; - -function getDotAstroTypeReference({ - settings, - filename, -}: { settings: AstroSettings; filename: string }) { - const relativePath = normalizePath( - path.relative( - fileURLToPath(settings.config.srcDir), - fileURLToPath(new URL(filename, settings.dotAstroDir)), - ), - ); - - return `/// `; -} - -type InjectedType = { filename: string; meetsCondition?: () => boolean | Promise }; - -export async function setUpEnvTs({ - settings, - logger, - fs, -}: { - settings: AstroSettings; - logger: Logger; - fs: typeof fsMod; -}) { - const envTsPath = new URL('env.d.ts', settings.config.srcDir); - const envTsPathRelativetoRoot = normalizePath( - path.relative(fileURLToPath(settings.config.root), fileURLToPath(envTsPath)), - ); - - const injectedTypes: Array = [ - { - filename: CONTENT_TYPES_FILE, - meetsCondition: () => fs.existsSync(new URL(CONTENT_TYPES_FILE, settings.dotAstroDir)), - }, - { - filename: ACTIONS_TYPES_FILE, - meetsCondition: () => fs.existsSync(new URL(ACTIONS_TYPES_FILE, settings.dotAstroDir)), - }, - ]; - if (settings.config.experimental.env) { - injectedTypes.push({ - filename: ENV_TYPES_FILE, - }); - } - - if (fs.existsSync(envTsPath)) { - const initialEnvContents = await fs.promises.readFile(envTsPath, 'utf-8'); - let typesEnvContents = initialEnvContents; - - for (const injectedType of injectedTypes) { - if (!injectedType.meetsCondition || (await injectedType.meetsCondition?.())) { - const expectedTypeReference = getDotAstroTypeReference({ - settings, - filename: injectedType.filename, - }); - - if (!typesEnvContents.includes(expectedTypeReference)) { - typesEnvContents = `${expectedTypeReference}\n${typesEnvContents}`; - } - } - } - - if (initialEnvContents !== typesEnvContents) { - logger.info('types', `Updated ${bold(envTsPathRelativetoRoot)} type declarations.`); - await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8'); - } - } else { - // Otherwise, inject the `env.d.ts` file - let referenceDefs: string[] = []; - referenceDefs.push('/// '); - - for (const injectedType of injectedTypes) { - if (!injectedType.meetsCondition || (await injectedType.meetsCondition?.())) { - referenceDefs.push(getDotAstroTypeReference({ settings, filename: injectedType.filename })); - } - } - - await fs.promises.mkdir(settings.config.srcDir, { recursive: true }); - await fs.promises.writeFile(envTsPath, referenceDefs.join('\n'), 'utf-8'); - logger.info('types', `Added ${bold(envTsPathRelativetoRoot)} type declarations`); - } -} diff --git a/packages/astro/src/core/sync/write-files.ts b/packages/astro/src/core/sync/write-files.ts new file mode 100644 index 000000000000..1f7d1d304608 --- /dev/null +++ b/packages/astro/src/core/sync/write-files.ts @@ -0,0 +1,76 @@ +import type fsMod from 'node:fs'; +import { dirname, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { bold } from 'kleur/colors'; +import { normalizePath } from 'vite'; +import type { AstroSettings } from '../../types/astro.js'; +import { AstroError, AstroErrorData } from '../errors/index.js'; +import type { Logger } from '../logger/core.js'; +import { REFERENCE_FILE } from './constants.js'; + +export async function writeFiles(settings: AstroSettings, fs: typeof fsMod, logger: Logger) { + try { + writeInjectedTypes(settings, fs); + await setUpEnvTs(settings, fs, logger); + } catch (e) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: e }); + } +} + +function getTsReference(type: 'path' | 'types', value: string) { + return `/// `; +} + +const CLIENT_TYPES_REFERENCE = getTsReference('types', 'astro/client'); + +function writeInjectedTypes(settings: AstroSettings, fs: typeof fsMod) { + const references: Array = []; + + for (const { filename, content } of settings.injectedTypes) { + const filepath = fileURLToPath(new URL(filename, settings.dotAstroDir)); + fs.mkdirSync(dirname(filepath), { recursive: true }); + fs.writeFileSync(filepath, content, 'utf-8'); + references.push(normalizePath(relative(fileURLToPath(settings.dotAstroDir), filepath))); + } + + const astroDtsContent = `${CLIENT_TYPES_REFERENCE}\n${references.map((reference) => getTsReference('path', reference)).join('\n')}`; + if (references.length === 0) { + fs.mkdirSync(settings.dotAstroDir, { recursive: true }); + } + fs.writeFileSync( + fileURLToPath(new URL(REFERENCE_FILE, settings.dotAstroDir)), + astroDtsContent, + 'utf-8', + ); +} + +async function setUpEnvTs(settings: AstroSettings, fs: typeof fsMod, logger: Logger) { + const envTsPath = fileURLToPath(new URL('env.d.ts', settings.config.srcDir)); + const envTsPathRelativetoRoot = relative(fileURLToPath(settings.config.root), envTsPath); + const relativePath = normalizePath( + relative( + fileURLToPath(settings.config.srcDir), + fileURLToPath(new URL(REFERENCE_FILE, settings.dotAstroDir)), + ), + ); + const expectedTypeReference = getTsReference('path', relativePath); + + if (fs.existsSync(envTsPath)) { + const initialEnvContents = await fs.promises.readFile(envTsPath, 'utf-8'); + let typesEnvContents = initialEnvContents; + + if (!typesEnvContents.includes(expectedTypeReference)) { + typesEnvContents = `${expectedTypeReference}\n${typesEnvContents}`; + } + + if (initialEnvContents !== typesEnvContents) { + logger.info('types', `Updated ${bold(envTsPathRelativetoRoot)} type declarations.`); + await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8'); + } + } else { + // Otherwise, inject the `env.d.ts` file + await fs.promises.mkdir(settings.config.srcDir, { recursive: true }); + await fs.promises.writeFile(envTsPath, expectedTypeReference, 'utf-8'); + logger.info('types', `Added ${bold(envTsPathRelativetoRoot)} type declarations`); + } +} diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 2dd4ddd3b411..497d54610262 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -1,7 +1,9 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { AstroConfig } from '../types/public/config.js'; +import type { RouteType } from '../types/public/internal.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js'; import { removeTrailingForwardSlash, slash } from './path.js'; @@ -153,7 +155,7 @@ export function isPage(file: URL, settings: AstroSettings): boolean { export function isEndpoint(file: URL, settings: AstroSettings): boolean { if (!isInPagesDir(file, settings.config)) return false; if (!isPublicRoute(file, settings.config)) return false; - return !endsWithPageExt(file, settings); + return !endsWithPageExt(file, settings) && !file.toString().includes('?astro'); } export function isServerLikeOutput(config: AstroConfig) { diff --git a/packages/astro/src/env/sync.ts b/packages/astro/src/env/sync.ts index 9ba11469ad7a..90a29685dc28 100644 --- a/packages/astro/src/env/sync.ts +++ b/packages/astro/src/env/sync.ts @@ -1,9 +1,9 @@ import fsMod from 'node:fs'; -import type { AstroSettings } from '../@types/astro.js'; -import { ENV_TYPES_FILE, TYPES_TEMPLATE_URL } from './constants.js'; +import type { AstroSettings } from '../types/astro.js'; +import { TYPES_TEMPLATE_URL } from './constants.js'; import { getEnvFieldType } from './validators.js'; -export function syncAstroEnv(settings: AstroSettings, fs = fsMod) { +export function syncAstroEnv(settings: AstroSettings, fs = fsMod): void { if (!settings.config.experimental.env) { return; } @@ -23,8 +23,10 @@ export function syncAstroEnv(settings: AstroSettings, fs = fsMod) { } const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8'); - const dts = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server); + const content = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server); - fs.mkdirSync(settings.dotAstroDir, { recursive: true }); - fs.writeFileSync(new URL(ENV_TYPES_FILE, settings.dotAstroDir), dts, 'utf-8'); + settings.injectedTypes.push({ + filename: 'astro/env.d.ts', + content, + }); } diff --git a/packages/astro/src/env/vite-plugin-env.ts b/packages/astro/src/env/vite-plugin-env.ts index fdcd6ce4015d..934ba79ca2ab 100644 --- a/packages/astro/src/env/vite-plugin-env.ts +++ b/packages/astro/src/env/vite-plugin-env.ts @@ -1,8 +1,8 @@ import type fsMod from 'node:fs'; import { fileURLToPath } from 'node:url'; import { type Plugin, loadEnv } from 'vite'; -import type { AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import type { AstroSettings } from '../types/astro.js'; import { MODULE_TEMPLATE_URL, VIRTUAL_MODULES_IDS, diff --git a/packages/astro/src/events/session.ts b/packages/astro/src/events/session.ts index 18049ac04dd0..6e919f127bee 100644 --- a/packages/astro/src/events/session.ts +++ b/packages/astro/src/events/session.ts @@ -1,5 +1,6 @@ -import type { AstroIntegration, AstroUserConfig } from '../@types/astro.js'; import { AstroConfigSchema } from '../core/config/schema.js'; +import type { AstroUserConfig } from '../types/public/config.js'; +import type { AstroIntegration } from '../types/public/integrations.js'; const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED'; diff --git a/packages/astro/src/i18n/index.ts b/packages/astro/src/i18n/index.ts index aa38b63bbe89..c7e676f75803 100644 --- a/packages/astro/src/i18n/index.ts +++ b/packages/astro/src/i18n/index.ts @@ -1,15 +1,11 @@ import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path'; -import type { - APIContext, - AstroConfig, - Locales, - SSRManifest, - ValidRedirectStatus, -} from '../@types/astro.js'; +import type { SSRManifest } from '../core/app/types.js'; import { shouldAppendForwardSlash } from '../core/build/util.js'; import { REROUTE_DIRECTIVE_HEADER } from '../core/constants.js'; import { MissingLocale, i18nNoLocaleFoundInPath } from '../core/errors/errors-data.js'; import { AstroError } from '../core/errors/index.js'; +import type { AstroConfig, Locales, ValidRedirectStatus } from '../types/public/config.js'; +import type { APIContext } from '../types/public/context.js'; import { createI18nMiddleware } from './middleware.js'; import type { RoutingStrategies } from './utils.js'; diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index 097332805759..7c2fc1406b2a 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -1,6 +1,7 @@ -import type { APIContext, MiddlewareHandler, SSRManifest } from '../@types/astro.js'; -import type { SSRManifestI18n } from '../core/app/types.js'; +import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js'; import { ROUTE_TYPE_HEADER } from '../core/constants.js'; +import type { MiddlewareHandler } from '../types/public/common.js'; +import type { APIContext } from '../types/public/context.js'; import { type MiddlewarePayload, normalizeTheLocale, diff --git a/packages/astro/src/i18n/utils.ts b/packages/astro/src/i18n/utils.ts index 052fe01fce65..98a44a19ca5e 100644 --- a/packages/astro/src/i18n/utils.ts +++ b/packages/astro/src/i18n/utils.ts @@ -1,4 +1,4 @@ -import type { AstroConfig, Locales } from '../@types/astro.js'; +import type { AstroConfig, Locales } from '../types/public/config.js'; import { normalizeTheLocale, toCodes } from './index.js'; type BrowserLocale = { diff --git a/packages/astro/src/i18n/vite-plugin-i18n.ts b/packages/astro/src/i18n/vite-plugin-i18n.ts index 7aa4d327b223..5ee79a8836a8 100644 --- a/packages/astro/src/i18n/vite-plugin-i18n.ts +++ b/packages/astro/src/i18n/vite-plugin-i18n.ts @@ -1,7 +1,8 @@ import type * as vite from 'vite'; -import type { AstroConfig, AstroSettings } from '../@types/astro.js'; import { AstroError } from '../core/errors/errors.js'; import { AstroErrorData } from '../core/errors/index.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { AstroConfig } from '../types/public/config.js'; const virtualModuleId = 'astro:i18n'; diff --git a/packages/astro/src/integrations/features-validation.ts b/packages/astro/src/integrations/features-validation.ts index 87de3592b726..f59a25b15c28 100644 --- a/packages/astro/src/integrations/features-validation.ts +++ b/packages/astro/src/integrations/features-validation.ts @@ -1,11 +1,11 @@ +import type { Logger } from '../core/logger/core.js'; +import type { AstroConfig } from '../types/public/config.js'; import type { + AdapterSupportsKind, + AstroAdapterFeatureMap, AstroAdapterFeatures, AstroAssetsFeature, - AstroConfig, - AstroFeatureMap, - SupportsKind, -} from '../@types/astro.js'; -import type { Logger } from '../core/logger/core.js'; +} from '../types/public/integrations.js'; const STABLE = 'stable'; const DEPRECATED = 'deprecated'; @@ -19,7 +19,7 @@ const UNSUPPORTED_ASSETS_FEATURE: AstroAssetsFeature = { }; type ValidationResult = { - [Property in keyof AstroFeatureMap]: boolean; + [Property in keyof AstroAdapterFeatureMap]: boolean; }; /** @@ -31,7 +31,7 @@ type ValidationResult = { */ export function validateSupportedFeatures( adapterName: string, - featureMap: AstroFeatureMap, + featureMap: AstroAdapterFeatureMap, config: AstroConfig, adapterFeatures: AstroAdapterFeatures | undefined, logger: Logger, @@ -101,7 +101,7 @@ export function validateSupportedFeatures( } function validateSupportKind( - supportKind: SupportsKind, + supportKind: AdapterSupportsKind, adapterName: string, logger: Logger, featureName: string, diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index 9b2859e48b3c..39d8ab69feb0 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -3,23 +3,23 @@ import type { AddressInfo } from 'node:net'; import { fileURLToPath } from 'node:url'; import { bold } from 'kleur/colors'; import type { InlineConfig, ViteDevServer } from 'vite'; -import type { - AstroAdapter, - AstroConfig, - AstroIntegration, - AstroRenderer, - AstroSettings, - ContentEntryType, - DataEntryType, - HookParameters, - RouteData, -} from '../@types/astro.js'; import type { SerializedSSRManifest } from '../core/app/types.js'; 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 '../core/util.js'; +import type { AstroSettings } from '../types/astro.js'; +import type { AstroConfig } from '../types/public/config.js'; +import type { ContentEntryType, DataEntryType } from '../types/public/content.js'; +import type { + AstroAdapter, + AstroIntegration, + AstroRenderer, + HookParameters, + RouteOptions, +} from '../types/public/integrations.js'; +import type { RouteData } from '../types/public/internal.js'; import { validateSupportedFeatures } from './features-validation.js'; async function withTakingALongTimeMsg({ @@ -100,6 +100,18 @@ export function getToolbarServerCommunicationHelpers(server: ViteDevServer) { }; } +// Will match any invalid characters (will be converted to _). We only allow a-zA-Z0-9.-_ +const SAFE_CHARS_RE = /[^\w.-]/g; + +export function normalizeInjectedTypeFilename(filename: string, integrationName: string): string { + if (!filename.endsWith('.d.ts')) { + throw new Error( + `Integration ${bold(integrationName)} is injecting a type that does not end with "${bold('.d.ts')}"`, + ); + } + return `./integrations/${integrationName.replace(SAFE_CHARS_RE, '_')}/${filename.replace(SAFE_CHARS_RE, '_')}`; +} + export async function runHookConfigSetup({ settings, command, @@ -185,10 +197,6 @@ export async function runHookConfigSetup({ addWatchFile: (path) => { updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path); }, - addDevOverlayPlugin: (entrypoint) => { - // TODO add a deprecation warning in Astro 5. - hooks.addDevToolbarApp(entrypoint); - }, addDevToolbarApp: (entrypoint) => { updatedSettings.devToolbarApps.push(entrypoint); }, @@ -327,6 +335,19 @@ export async function runHookConfigDone({ } settings.adapter = adapter; }, + injectTypes(injectedType) { + const normalizedFilename = normalizeInjectedTypeFilename( + injectedType.filename, + integration.name, + ); + + settings.injectedTypes.push({ + filename: normalizedFilename, + content: injectedType.content, + }); + + return new URL(normalizedFilename, settings.config.root); + }, logger: getLogger(integration, logger), }), logger, @@ -558,6 +579,47 @@ export async function runHookBuildDone({ } } +export async function runHookRouteSetup({ + route, + settings, + logger, +}: { + route: RouteOptions; + settings: AstroSettings; + logger: Logger; +}) { + const prerenderChangeLogs: { integrationName: string; value: boolean | undefined }[] = []; + + for (const integration of settings.config.integrations) { + if (integration?.hooks?.['astro:route:setup']) { + const originalRoute = { ...route }; + const integrationLogger = getLogger(integration, logger); + + await withTakingALongTimeMsg({ + name: integration.name, + hookName: 'astro:route:setup', + hookResult: integration.hooks['astro:route:setup']({ + route, + logger: integrationLogger, + }), + logger, + }); + + if (route.prerender !== originalRoute.prerender) { + prerenderChangeLogs.push({ integrationName: integration.name, value: route.prerender }); + } + } + } + + if (prerenderChangeLogs.length > 1) { + logger.debug( + 'router', + `The ${route.component} route's prerender option has been changed multiple times by integrations:\n` + + prerenderChangeLogs.map((log) => `- ${log.integrationName}: ${log.value}`).join('\n'), + ); + } +} + export function isFunctionPerRouteEnabled(adapter: AstroAdapter | undefined): boolean { if (adapter?.adapterFeatures?.functionPerRoute === true) { return true; diff --git a/packages/astro/src/jsx/renderer.ts b/packages/astro/src/jsx/renderer.ts index 413257faab97..86f4d018743a 100644 --- a/packages/astro/src/jsx/renderer.ts +++ b/packages/astro/src/jsx/renderer.ts @@ -1,11 +1,8 @@ -import type { AstroRenderer } from '../@types/astro.js'; -import { jsxTransformOptions } from './transform-options.js'; +import type { AstroRenderer } from '../types/public/integrations.js'; const renderer: AstroRenderer = { name: 'astro:jsx', serverEntrypoint: 'astro/jsx/server.js', - jsxImportSource: 'astro', - jsxTransformOptions, }; export default renderer; diff --git a/packages/astro/src/jsx/server.ts b/packages/astro/src/jsx/server.ts index 73b584baeaf7..bb71231c5813 100644 --- a/packages/astro/src/jsx/server.ts +++ b/packages/astro/src/jsx/server.ts @@ -1,7 +1,7 @@ -import type { NamedSSRLoadedRendererValue } from '../@types/astro.js'; import { AstroError, AstroUserError } from '../core/errors/errors.js'; import { AstroJSX, jsx } from '../jsx-runtime/index.js'; import { renderJSX } from '../runtime/server/jsx.js'; +import type { NamedSSRLoadedRendererValue } from '../types/public/internal.js'; const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); diff --git a/packages/astro/src/jsx/transform-options.ts b/packages/astro/src/jsx/transform-options.ts index ca1d50a6a131..e7405ddc0c2d 100644 --- a/packages/astro/src/jsx/transform-options.ts +++ b/packages/astro/src/jsx/transform-options.ts @@ -1,4 +1,4 @@ -import type { JSXTransformConfig } from '../@types/astro.js'; +import type { JSXTransformConfig } from '../types/astro.js'; /** * @deprecated This function is no longer used. Remove in Astro 5.0 diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts index 9318824bf67e..7c8779b43f4e 100644 --- a/packages/astro/src/preferences/index.ts +++ b/packages/astro/src/preferences/index.ts @@ -1,11 +1,10 @@ -import type { AstroConfig } from '../@types/astro.js'; - import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import dget from 'dlv'; +import type { AstroConfig } from '../types/public/config.js'; import { DEFAULT_PREFERENCES, type Preferences, type PublicPreferences } from './defaults.js'; import { PreferenceStore } from './store.js'; @@ -82,9 +81,9 @@ export function coerce(key: string, value: unknown) { return value as any; } -export default function createPreferences(config: AstroConfig): AstroPreferences { +export default function createPreferences(config: AstroConfig, dotAstroDir: URL): AstroPreferences { const global = new PreferenceStore(getGlobalPreferenceDir()); - const project = new PreferenceStore(fileURLToPath(new URL('./.astro/', config.root))); + const project = new PreferenceStore(fileURLToPath(dotAstroDir)); const stores: Record = { global, project }; return { diff --git a/packages/astro/src/prefetch/vite-plugin-prefetch.ts b/packages/astro/src/prefetch/vite-plugin-prefetch.ts index d64c6d5008b9..560895b15bfb 100644 --- a/packages/astro/src/prefetch/vite-plugin-prefetch.ts +++ b/packages/astro/src/prefetch/vite-plugin-prefetch.ts @@ -1,5 +1,5 @@ import type * as vite from 'vite'; -import type { AstroSettings } from '../@types/astro.js'; +import type { AstroSettings } from '../types/astro.js'; const virtualModuleId = 'astro:prefetch'; const resolvedVirtualModuleId = '\0' + virtualModuleId; diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts index cbdddff5c8cb..888b012e0b9d 100644 --- a/packages/astro/src/prerender/routing.ts +++ b/packages/astro/src/prerender/routing.ts @@ -1,6 +1,7 @@ -import type { AstroSettings, ComponentInstance, RouteData } from '../@types/astro.js'; import { RedirectComponentInstance, routeIsRedirect } from '../core/redirects/index.js'; import { routeComparator } from '../core/routing/priority.js'; +import type { AstroSettings, ComponentInstance } from '../types/astro.js'; +import type { RouteData } from '../types/public/internal.js'; import type { DevPipeline } from '../vite-plugin-astro-server/pipeline.js'; import { getPrerenderStatus } from './metadata.js'; diff --git a/packages/astro/src/prerender/utils.ts b/packages/astro/src/prerender/utils.ts index 4097b66b1f6d..e34e0d5fd024 100644 --- a/packages/astro/src/prerender/utils.ts +++ b/packages/astro/src/prerender/utils.ts @@ -1,6 +1,6 @@ -import type { AstroConfig } from '../@types/astro.js'; import { getOutDirWithinCwd } from '../core/build/common.js'; import { isServerLikeOutput } from '../core/util.js'; +import type { AstroConfig } from '../types/public/config.js'; export function getPrerenderDefault(config: AstroConfig) { return config.output !== 'server'; diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts index e63667a12d6f..12ab602a255e 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/astro.ts @@ -1,4 +1,4 @@ -import type { DevToolbarApp, DevToolbarMetadata } from '../../../../@types/astro.js'; +import type { DevToolbarApp, DevToolbarMetadata } from '../../../../types/public/toolbar.js'; import { type Icon, isDefinedIcon } from '../ui-library/icons.js'; import { colorForIntegration, iconForIntegration } from './utils/icons.js'; import { diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts index 6a6aba42d120..cc58411cf3c4 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts @@ -1,4 +1,4 @@ -import type { DevToolbarApp } from '../../../../../@types/astro.js'; +import type { DevToolbarApp } from '../../../../../types/public/toolbar.js'; import { settings } from '../../settings.js'; import type { DevToolbarHighlight } from '../../ui-library/highlight.js'; import { positionHighlight } from '../utils/highlight.js'; diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/ui/audit-ui.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/ui/audit-ui.ts index 38559e0f9adf..34adf4f01282 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/ui/audit-ui.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/ui/audit-ui.ts @@ -1,5 +1,5 @@ import { escape as escapeHTML } from 'html-escaper'; -import type { DevToolbarMetadata } from '../../../../../../@types/astro.js'; +import type { DevToolbarMetadata } from '../../../../../../types/public/toolbar.js'; import { attachTooltipToHighlight, createHighlight, diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts index 7ee42da84996..192e4509b504 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/settings.ts @@ -1,4 +1,4 @@ -import type { DevToolbarApp } from '../../../../@types/astro.js'; +import type { DevToolbarApp } from '../../../../types/public/toolbar.js'; import { type Settings, settings } from '../settings.js'; import { isValidPlacement, placements } from '../ui-library/window.js'; import { diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts index 2e56b0f244cf..fafc8e26d57e 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts @@ -1,5 +1,5 @@ import { escape as escapeHTML } from 'html-escaper'; -import type { DevToolbarApp, DevToolbarMetadata } from '../../../../@types/astro.js'; +import type { DevToolbarApp, DevToolbarMetadata } from '../../../../types/public/toolbar.js'; import type { DevToolbarHighlight } from '../ui-library/highlight.js'; import { attachTooltipToHighlight, diff --git a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts index 9731f3597fbb..86c616becb3a 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/entrypoint.ts @@ -1,6 +1,6 @@ // @ts-expect-error - This module is private and untyped import { loadDevToolbarApps } from 'astro:toolbar:internal'; -import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js'; +import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../types/public/toolbar.js'; import { ToolbarAppEventTarget } from './helpers.js'; import { settings } from './settings.js'; import type { AstroDevToolbar, DevToolbarApp } from './toolbar.js'; diff --git a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts index 08ea61cace5c..e24698d4337b 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js'; +import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../types/public/toolbar.js'; import { type ToolbarAppEventTarget, serverHelpers } from './helpers.js'; import { settings } from './settings.js'; import { type Icon, getIconElement, isDefinedIcon } from './ui-library/icons.js'; diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index 990d5da6ef10..e32b1a42f698 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -1,4 +1,4 @@ -import type { ClientDirective } from '../../@types/astro.js'; +import type { ClientDirective } from '../../types/public/integrations.js'; const idleDirective: ClientDirective = (load) => { const cb = async () => { diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index b603eecb3b49..98521181c528 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -1,4 +1,4 @@ -import type { ClientDirective } from '../../@types/astro.js'; +import type { ClientDirective } from '../../types/public/integrations.js'; const loadDirective: ClientDirective = async (load) => { const hydrate = await load(); diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index f894af31112d..0c6e497e3302 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -1,4 +1,4 @@ -import type { ClientDirective } from '../../@types/astro.js'; +import type { ClientDirective } from '../../types/public/integrations.js'; /** * Hydrate this component when a matching media query is found diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index 8e072b7a3fc0..c5fc488d4bd8 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -1,4 +1,4 @@ -import type { ClientDirective } from '../../@types/astro.js'; +import type { ClientDirective } from '../../types/public/integrations.js'; /** * Hydrate this component only on the client diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index 9be4d9b318a7..c7d858ffb0b4 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -1,4 +1,5 @@ -import type { ClientDirective, ClientVisibleOptions } from '../../@types/astro.js'; +import type { ClientVisibleOptions } from '../../types/public/elements.js'; +import type { ClientDirective } from '../../types/public/integrations.js'; /** * Hydrate this component when one of it's children becomes visible diff --git a/packages/astro/src/runtime/server/astro-component.ts b/packages/astro/src/runtime/server/astro-component.ts index 928d23ad82a5..20649b64dcb4 100644 --- a/packages/astro/src/runtime/server/astro-component.ts +++ b/packages/astro/src/runtime/server/astro-component.ts @@ -1,5 +1,5 @@ -import type { PropagationHint } from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; +import type { PropagationHint } from '../../types/public/internal.js'; import type { AstroComponentFactory } from './render/index.js'; function validateArgs(args: unknown[]): args is Parameters { diff --git a/packages/astro/src/runtime/server/astro-global.ts b/packages/astro/src/runtime/server/astro-global.ts index 5948c8ff3aff..74b32e331421 100644 --- a/packages/astro/src/runtime/server/astro-global.ts +++ b/packages/astro/src/runtime/server/astro-global.ts @@ -1,6 +1,6 @@ -import type { AstroGlobalPartial } from '../../@types/astro.js'; import { ASTRO_VERSION } from '../../core/constants.js'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; +import type { AstroGlobalPartial } from '../../types/public/context.js'; /** Create the Astro.glob() runtime function. */ function createAstroGlobFn() { diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts index 900d604fd569..674d6209343f 100644 --- a/packages/astro/src/runtime/server/endpoint.ts +++ b/packages/astro/src/runtime/server/endpoint.ts @@ -1,13 +1,16 @@ import { bold } from 'kleur/colors'; -import type { APIContext, EndpointHandler } from '../../@types/astro.js'; import { REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER } from '../../core/constants.js'; import { EndpointDidNotReturnAResponse } from '../../core/errors/errors-data.js'; import { AstroError } from '../../core/errors/errors.js'; import type { Logger } from '../../core/logger/core.js'; +import type { APIRoute } from '../../types/public/common.js'; +import type { APIContext } from '../../types/public/context.js'; /** Renders an endpoint request to completion, returning the body. */ export async function renderEndpoint( - mod: EndpointHandler, + mod: { + [method: string]: APIRoute; + }, context: APIContext, ssr: boolean, logger: Logger, diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index ab6396566b5e..982ff9977ecb 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -1,10 +1,10 @@ +import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import type { AstroComponentMetadata, SSRElement, SSRLoadedRenderer, SSRResult, -} from '../../@types/astro.js'; -import { AstroError, AstroErrorData } from '../../core/errors/index.js'; +} from '../../types/public/internal.js'; import { escapeHTML } from './escape.js'; import { serializeProps } from './serialize.js'; diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts index 2491ab58946e..7280e216c31c 100644 --- a/packages/astro/src/runtime/server/jsx.ts +++ b/packages/astro/src/runtime/server/jsx.ts @@ -1,5 +1,5 @@ -import type { SSRResult } from '../../@types/astro.js'; import { AstroJSX, type AstroVNode, isVNode } from '../../jsx-runtime/index.js'; +import type { SSRResult } from '../../types/public/internal.js'; import { HTMLString, escapeHTML, diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts index db319eb1aadd..dab33a031991 100644 --- a/packages/astro/src/runtime/server/render/astro/factory.ts +++ b/packages/astro/src/runtime/server/render/astro/factory.ts @@ -1,4 +1,4 @@ -import type { PropagationHint, SSRResult } from '../../../../@types/astro.js'; +import type { PropagationHint, SSRResult } from '../../../../types/public/internal.js'; import type { HeadAndContent } from './head-and-content.js'; import type { RenderTemplateResult } from './render-template.js'; diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts index 3246a7e1b294..029231a28d6d 100644 --- a/packages/astro/src/runtime/server/render/astro/instance.ts +++ b/packages/astro/src/runtime/server/render/astro/instance.ts @@ -1,7 +1,7 @@ -import type { SSRResult } from '../../../../@types/astro.js'; import type { ComponentSlots } from '../slot.js'; import type { AstroComponentFactory } from './factory.js'; +import type { SSRResult } from '../../../../types/public/internal.js'; import { isPromise } from '../../util.js'; import { renderChild } from '../any.js'; import type { RenderDestination } from '../common.js'; diff --git a/packages/astro/src/runtime/server/render/astro/render.ts b/packages/astro/src/runtime/server/render/astro/render.ts index 41845b7b9e16..adc335495da1 100644 --- a/packages/astro/src/runtime/server/render/astro/render.ts +++ b/packages/astro/src/runtime/server/render/astro/render.ts @@ -1,5 +1,5 @@ -import type { RouteData, SSRResult } from '../../../../@types/astro.js'; import { AstroError, AstroErrorData } from '../../../../core/errors/index.js'; +import type { RouteData, SSRResult } from '../../../../types/public/internal.js'; import { type RenderDestination, chunkToByteArray, chunkToString, encoder } from '../common.js'; import { promiseWithResolvers } from '../util.js'; import type { AstroComponentFactory } from './factory.js'; diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index 0845bdd65315..77f05dfccec6 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -1,6 +1,6 @@ -import type { SSRResult } from '../../../@types/astro.js'; import type { RenderInstruction } from './instruction.js'; +import type { SSRResult } from '../../../types/public/internal.js'; import type { HTMLBytes, HTMLString } from '../escape.js'; import { markHTMLString } from '../escape.js'; import { diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index dcad03c0628c..017594e75184 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -1,9 +1,3 @@ -import type { - AstroComponentMetadata, - RouteData, - SSRLoadedRenderer, - SSRResult, -} from '../../../@types/astro.js'; import { createRenderInstruction } from './instruction.js'; import { clsx } from 'clsx'; @@ -17,6 +11,12 @@ import { type AstroComponentFactory, isAstroComponentFactory } from './astro/fac import { renderTemplate } from './astro/index.js'; import { createAstroComponentInstance } from './astro/instance.js'; +import type { + AstroComponentMetadata, + RouteData, + SSRLoadedRenderer, + SSRResult, +} from '../../../types/public/internal.js'; import { Fragment, type RenderDestination, diff --git a/packages/astro/src/runtime/server/render/dom.ts b/packages/astro/src/runtime/server/render/dom.ts index e2b194df3451..f92b4889a38f 100644 --- a/packages/astro/src/runtime/server/render/dom.ts +++ b/packages/astro/src/runtime/server/render/dom.ts @@ -1,5 +1,4 @@ -import type { SSRResult } from '../../../@types/astro.js'; - +import type { SSRResult } from '../../../types/public/internal.js'; import { markHTMLString } from '../escape.js'; import { renderSlotToString } from './slot.js'; import { toAttributeString } from './util.js'; diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index 49dd5abed14a..01f11f21e760 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -1,5 +1,4 @@ -import type { SSRResult } from '../../../@types/astro.js'; - +import type { SSRResult } from '../../../types/public/internal.js'; import { markHTMLString } from '../escape.js'; import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js'; import { createRenderInstruction } from './instruction.js'; diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts index 35a7ec789947..0e0bcf295ad8 100644 --- a/packages/astro/src/runtime/server/render/page.ts +++ b/packages/astro/src/runtime/server/render/page.ts @@ -1,7 +1,7 @@ -import type { RouteData, SSRResult } from '../../../@types/astro.js'; import { type NonAstroPageComponent, renderComponentToString } from './component.js'; import type { AstroComponentFactory } from './index.js'; +import type { RouteData, SSRResult } from '../../../types/public/internal.js'; import { isAstroComponentFactory } from './astro/index.js'; import { renderToAsyncIterable, renderToReadableStream, renderToString } from './astro/render.js'; import { encoder } from './common.js'; diff --git a/packages/astro/src/runtime/server/render/script.ts b/packages/astro/src/runtime/server/render/script.ts index 1b9c5ce1b37e..6d9283790a06 100644 --- a/packages/astro/src/runtime/server/render/script.ts +++ b/packages/astro/src/runtime/server/render/script.ts @@ -1,4 +1,4 @@ -import type { SSRResult } from '../../../@types/astro.js'; +import type { SSRResult } from '../../../types/public/internal.js'; import { markHTMLString } from '../escape.js'; /** diff --git a/packages/astro/src/runtime/server/render/server-islands.ts b/packages/astro/src/runtime/server/render/server-islands.ts index c2263addaad5..fce702364df9 100644 --- a/packages/astro/src/runtime/server/render/server-islands.ts +++ b/packages/astro/src/runtime/server/render/server-islands.ts @@ -1,4 +1,5 @@ -import type { SSRResult } from '../../../@types/astro.js'; +import { encryptString } from '../../../core/encryption.js'; +import type { SSRResult } from '../../../types/public/internal.js'; import { renderChild } from './any.js'; import type { RenderInstance } from './common.js'; import { type ComponentSlots, renderSlotToString } from './slot.js'; @@ -59,8 +60,12 @@ export function renderServerIsland( } } + const key = await result.key; + const propsEncrypted = await encryptString(key, JSON.stringify(props)); + const hostId = crypto.randomUUID(); - const serverIslandUrl = `${result.base}_server-islands/${componentId}${result.trailingSlash === 'always' ? '/' : ''}`; + const slash = result.base.endsWith('/') ? '' : '/'; + const serverIslandUrl = `${result.base}${slash}_server-islands/${componentId}${result.trailingSlash === 'always' ? '/' : ''}`; destination.write(`