diff --git a/.changeset/fine-needles-send.md b/.changeset/fine-needles-send.md new file mode 100644 index 000000000000..047c45c81c19 --- /dev/null +++ b/.changeset/fine-needles-send.md @@ -0,0 +1,5 @@ +--- +'@astrojs/netlify': patch +--- + +Fixes an issue where the adapter didn't take into consideration the `outDir` configuration. diff --git a/.changeset/three-cooks-drive.md b/.changeset/three-cooks-drive.md new file mode 100644 index 000000000000..90d71321c792 --- /dev/null +++ b/.changeset/three-cooks-drive.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Replaces internal CSS chunking behavior for Astro components' scoped styles to use Vite's `cssScopeTo` feature. The feature is a port of Astro's implementation so this should not change the behavior. diff --git a/.changeset/wide-dodos-mate.md b/.changeset/wide-dodos-mate.md new file mode 100644 index 000000000000..5273b8e2600b --- /dev/null +++ b/.changeset/wide-dodos-mate.md @@ -0,0 +1,5 @@ +--- +'@astrojs/db': minor +--- + +Upgraded drizzle-orm to latest v0.42.0 diff --git a/.changeset/young-peaches-serve.md b/.changeset/young-peaches-serve.md deleted file mode 100644 index 2050dba6a0dd..000000000000 --- a/.changeset/young-peaches-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Improve autocomplete for session keys diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 345b6e46097c..fdb19edf45d6 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -37,7 +37,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" diff --git a/.github/workflows/check-merge.yml b/.github/workflows/check-merge.yml index 00e3671c011a..d4d659ea1930 100644 --- a/.github/workflows/check-merge.yml +++ b/.github/workflows/check-merge.yml @@ -41,7 +41,7 @@ jobs: - name: Get changed files in the .changeset folder id: changed-files - uses: tj-actions/changed-files@6cb76d07bee4c9772c6882c06c37837bf82a04d3 # v46.0.4 + uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 if: steps.blocked.outputs.result != 'true' with: files: | diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 2658d17177bb..aebb3a4fd819 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -34,7 +34,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17d609981ebb..7abb53acb95a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup node@${{ matrix.NODE_VERSION }} - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.NODE_VERSION }} cache: "pnpm" @@ -83,7 +83,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" @@ -92,7 +92,8 @@ jobs: run: pnpm install - name: Build Packages - run: pnpm run build + # The cache doesn't contain prebuild files and causes knip to fail + run: pnpm run build --force - name: Lint source code run: pnpm run lint:ci @@ -128,7 +129,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup node@${{ matrix.NODE_VERSION }} - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.NODE_VERSION }} cache: "pnpm" @@ -165,7 +166,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup node@${{ matrix.NODE_VERSION }} - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.NODE_VERSION }} cache: "pnpm" @@ -201,7 +202,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup node@${{ matrix.NODE_VERSION }} - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.NODE_VERSION }} cache: "pnpm" diff --git a/.github/workflows/continuous_benchmark.yml b/.github/workflows/continuous_benchmark.yml index 9e9d281ea572..9a5a3fef92e7 100644 --- a/.github/workflows/continuous_benchmark.yml +++ b/.github/workflows/continuous_benchmark.yml @@ -35,7 +35,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 90b58682522b..2fbf8d665d57 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -43,7 +43,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 829ab48f6c26..74f38a9aaf77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" @@ -46,7 +46,7 @@ jobs: - name: Create Release Pull Request or Publish id: changesets - uses: changesets/action@06245a4e0a36c064a573d4150030f5ec548e4fcc # v1.4.10 + uses: changesets/action@001cd79f0a536e733315164543a727bdf2d70aff # v1.5.1 with: # Note: pnpm install after versioning is necessary to refresh lockfile version: pnpm run version diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml index 72e4358f91ea..f031bbddb9d2 100644 --- a/.github/workflows/scripts.yml +++ b/.github/workflows/scripts.yml @@ -36,7 +36,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "pnpm" diff --git a/.github/workflows/test-hosts.yml b/.github/workflows/test-hosts.yml index a90b99f8a1c5..f12ed9b40aa0 100644 --- a/.github/workflows/test-hosts.yml +++ b/.github/workflows/test-hosts.yml @@ -25,7 +25,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Setup Node - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: 'pnpm' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db4c42d4b455..442d089f5f18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,20 +234,21 @@ This paragraph provides some guidance to the maintainers of the monorepo. The gu ```mermaid graph TD; - start{Followed issue\ntemplate?} - start --NO--> close1[Close and ask to\nfollow template] + start{Followed issue
template?} + start --NO--> close1[Close and ask to
follow template.] start --YES--> dupe{Is duplicate?} - dupe --YES--> close2[Close and point\nto duplicate] - dupe --NO--> repro{Has proper\nreproduction?} - repro --NO--> close3[Label: 'needs reproduction'\nbot will auto close if no update\nhas been made in 3 days] + dupe --YES--> close2[Close and point
to duplicate.] + dupe --NO--> repro{Has proper
reproduction?} + repro --NO--> close3[Add label: 'needs discussion'.
Bot will auto close if no
update was made in 3 days.] repro --YES--> real{Is actually a bug?} real --NO--> maybefeat{Is it a feature request?} - maybefeat -- YES --> roadmap[Close the issue.\n Point user to the roadmap.] - maybefeat -- NO --> intended{Is the intended\nbehaviour?} - intended --YES--> explain[Explain and close\npoint to docs if needed] - intended --NO--> open[Add label 'needs discussion'\nRemove 'needs triage' label] - real --YES--> real2["1. Remove 'needs triage' label\n2. Add related feature label if\napplicable (e.g. 'feat: ssr')\n3. Add priority and meta labels (see below)"] - real2 --> tolabel[Use the framework below to decide the priority of the issue,\nand choose the correct label] + maybefeat -- YES --> roadmap[Close the issue.
Point user to the roadmap.] + maybefeat -- NO --> intended{Is the intended
behaviour?} + intended --YES--> explain[Explain and close.
Point to docs if needed.] + intended --NO--> open[Add label: 'needs discussion'.
Remove label: 'needs triage'.] + real --YES--> real2["∙ Remove label: 'needs triage'.
∙ Add related feature label if
applicable. (e.g. 'feat: ssr')
∙ Add priority and meta labels. (see below)"] + style real2 text-wrap:balance + real2 --> tolabel[Use the framework below
to decide the priority
of the issue and choose
the correct label.] ``` diff --git a/benchmark/make-project/_template.js b/benchmark/make-project/_template.js index dd392138e91b..a99c5242853f 100644 --- a/benchmark/make-project/_template.js +++ b/benchmark/make-project/_template.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Create a new project in the `projectDir` directory. Make sure to clean up the * previous artifacts here before generating files. diff --git a/biome.jsonc b/biome.jsonc index 83c559f4d515..588d8f5f1f75 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -32,9 +32,9 @@ "noConsoleLog": "warn" }, "correctness": { - "noUnusedVariables": "info", - "noUnusedFunctionParameters": "info", - "noUnusedImports": "warn" + "noUnusedVariables": "error", + "noUnusedFunctionParameters": "error", + "noUnusedImports": "error" } } }, diff --git a/eslint.config.js b/eslint.config.js index c5fbf3fbc37c..c7a454ea5255 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,7 @@ export default [ 'packages/**/*.min.js', 'packages/**/dist/', 'packages/**/fixtures/', + 'packages/**/_temp-fixtures/', 'packages/astro/vendor/vite/', 'benchmark/**/dist/', 'examples/', @@ -54,7 +55,18 @@ export default [ 'no-console': 'off', // Todo: do we want these? - '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], '@typescript-eslint/array-type': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/class-literal-property-style': 'off', diff --git a/examples/basics/package.json b/examples/basics/package.json index 21e8d3e30c8d..c3bf17d142f4 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^5.7.0" + "astro": "^5.7.13" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index b57ab6c4a66c..06f67258de96 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^4.2.4", + "@astrojs/mdx": "^4.2.6", "@astrojs/rss": "^4.0.11", - "@astrojs/sitemap": "^3.3.0", - "astro": "^5.7.0" + "@astrojs/sitemap": "^3.4.0", + "astro": "^5.7.13" } } diff --git a/examples/component/package.json b/examples/component/package.json index fa1504200110..bdc2438bbb9e 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^5.7.0" + "astro": "^5.7.13" }, "peerDependencies": { "astro": "^4.0.0 || ^5.0.0" diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json index eb4870535ac2..53562bf0744a 100644 --- a/examples/container-with-vitest/package.json +++ b/examples/container-with-vitest/package.json @@ -11,8 +11,8 @@ "test": "vitest run" }, "dependencies": { - "@astrojs/react": "^4.2.4", - "astro": "^5.7.0", + "@astrojs/react": "^4.2.7", + "astro": "^5.7.13", "react": "^18.3.1", "react-dom": "^18.3.1", "vitest": "^3.1.1" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index c60f17a35ce3..dcbaa3ba7f4e 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/alpinejs": "^0.4.6", + "@astrojs/alpinejs": "^0.4.8", "@types/alpinejs": "^3.13.11", "alpinejs": "^3.14.9", - "astro": "^5.7.0" + "astro": "^5.7.13" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index f940ccdcdbb6..4895b5916909 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -10,14 +10,14 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^4.0.9", - "@astrojs/react": "^4.2.4", - "@astrojs/solid-js": "^5.0.8", - "@astrojs/svelte": "^7.0.10", - "@astrojs/vue": "^5.0.10", + "@astrojs/preact": "^4.0.11", + "@astrojs/react": "^4.2.7", + "@astrojs/solid-js": "^5.0.10", + "@astrojs/svelte": "^7.0.13", + "@astrojs/vue": "^5.0.13", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.6", - "astro": "^5.7.0", + "astro": "^5.7.13", "preact": "^10.26.5", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index b2876250cd1b..7a17a72f5d96 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^4.0.9", + "@astrojs/preact": "^4.0.11", "@preact/signals": "^2.0.3", - "astro": "^5.7.0", + "astro": "^5.7.13", "preact": "^10.26.5" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 1ca7c0337f13..fdd4c67e47f4 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -10,10 +10,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/react": "^4.2.4", + "@astrojs/react": "^4.2.7", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.6", - "astro": "^5.7.0", + "astro": "^5.7.13", "react": "^18.3.1", "react-dom": "^18.3.1" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 8da42ebb4064..5c9379b9f022 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -10,8 +10,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/solid-js": "^5.0.8", - "astro": "^5.7.0", + "@astrojs/solid-js": "^5.0.10", + "astro": "^5.7.13", "solid-js": "^1.9.5" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index bd462967e20e..e9cc78aecb90 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -10,8 +10,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/svelte": "^7.0.10", - "astro": "^5.7.0", + "@astrojs/svelte": "^7.0.13", + "astro": "^5.7.13", "svelte": "^5.25.7" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index a8f0513ea211..67fc005d62fa 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -10,8 +10,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/vue": "^5.0.10", - "astro": "^5.7.0", + "@astrojs/vue": "^5.0.13", + "astro": "^5.7.13", "vue": "^3.5.13" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index aac006273a2d..a305dfe3fdf7 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/node": "^9.2.0", - "astro": "^5.7.0" + "@astrojs/node": "^9.2.1", + "astro": "^5.7.13" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index 7a269d038189..5f2ecbc42a6b 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^5.7.0" + "astro": "^5.7.13" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/minimal/package.json b/examples/minimal/package.json index df816b83e1e0..9abf63557875 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^5.7.0" + "astro": "^5.7.13" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index b6b081cb0df5..d08dddf0e5f5 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^5.7.0" + "astro": "^5.7.13" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 8830c5f2fbc0..2850d032f62e 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -11,9 +11,9 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^9.2.0", - "@astrojs/svelte": "^7.0.10", - "astro": "^5.7.0", + "@astrojs/node": "^9.2.1", + "@astrojs/svelte": "^7.0.13", + "astro": "^5.7.13", "svelte": "^5.25.7" } } diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts index 74e09eb735f3..2d10a85f9efc 100644 --- a/examples/ssr/src/api.ts +++ b/examples/ssr/src/api.ts @@ -17,49 +17,42 @@ interface Cart { }>; } -async function get( +async function getJson( incomingReq: Request, endpoint: string, - cb: (response: Response) => Promise ): Promise { const origin = new URL(incomingReq.url).origin; - const response = await fetch(`${origin}${endpoint}`, { - credentials: 'same-origin', - headers: incomingReq.headers, - }); - if (!response.ok) { - // TODO make this better... - throw new Error('Fetch failed'); + try { + const response = await fetch(`${origin}${endpoint}`, { + credentials: 'same-origin', + headers: incomingReq.headers, + }); + if (!response.ok) { + throw new Error(`GET ${endpoint} failed: ${response.statusText}`); + } + return response.json() as Promise; + } catch (error) { + if (error instanceof DOMException || error instanceof TypeError) { + throw new Error(`GET ${endpoint} failed: ${error.message}`); + } + throw error; } - return cb(response); } export async function getProducts(incomingReq: Request): Promise { - return get(incomingReq, '/api/products', async (response) => { - const products: Product[] = await response.json(); - return products; - }); + return getJson(incomingReq, '/api/products'); } export async function getProduct(incomingReq: Request, id: number): Promise { - return get(incomingReq, `/api/products/${id}`, async (response) => { - const product: Product = await response.json(); - return product; - }); + return getJson(incomingReq, `/api/products/${id}`); } export async function getUser(incomingReq: Request): Promise { - return get(incomingReq, `/api/user`, async (response) => { - const user: User = await response.json(); - return user; - }); + return getJson(incomingReq, `/api/user`); } export async function getCart(incomingReq: Request): Promise { - return get(incomingReq, `/api/cart`, async (response) => { - const cart: Cart = await response.json(); - return cart; - }); + return getJson(incomingReq, `/api/cart`); } export async function addToUserCart(id: number | string, name: string): Promise { diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 4e886cb1b9b6..bd7eb4cd5f0d 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -9,7 +9,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^5.7.0", + "astro": "^5.7.13", "sass": "^1.86.3", "sharp": "^0.33.3" } diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index a4d2517d3fa2..7bb56813958a 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "@types/node": "^18.17.8", - "astro": "^5.7.0" + "astro": "^5.7.13" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 64a47aadad51..1d7635a67358 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/markdoc": "^0.14.0", - "astro": "^5.7.0" + "@astrojs/markdoc": "^0.14.2", + "astro": "^5.7.13" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 6f29fa981dc6..df646da67bcc 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^4.2.4", - "@astrojs/preact": "^4.0.9", - "astro": "^5.7.0", + "@astrojs/mdx": "^4.2.6", + "@astrojs/preact": "^4.0.11", + "astro": "^5.7.13", "preact": "^10.26.5" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index f916a33f49ea..0cbca8f15ad0 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^4.0.9", + "@astrojs/preact": "^4.0.11", "@nanostores/preact": "^0.5.2", - "astro": "^5.7.0", + "astro": "^5.7.13", "nanostores": "^0.11.4", "preact": "^10.26.5" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 7c3553b8e7cc..fd150b0b5090 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -10,10 +10,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^4.2.4", + "@astrojs/mdx": "^4.2.6", "@tailwindcss/vite": "^4.1.3", "@types/canvas-confetti": "^1.9.0", - "astro": "^5.7.0", + "astro": "^5.7.13", "canvas-confetti": "^1.9.3", "tailwindcss": "^4.1.3" } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 0aa6c92a0bdb..494232b5cc6b 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -11,7 +11,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^5.7.0", + "astro": "^5.7.13", "vitest": "^3.1.1" } } diff --git a/knip.js b/knip.js new file mode 100644 index 000000000000..5f543e1bd3f4 --- /dev/null +++ b/knip.js @@ -0,0 +1,77 @@ +// @ts-check +const testEntry = 'test/**/*.test.js'; + +/** @type {import('knip').KnipConfig} */ +export default { + ignore: ['**/test/**/{fixtures,_temp-fixtures}/**', '.github/scripts/**'], + tags: ['-lintignore'], + ignoreWorkspaces: [ + 'examples/**', + '**/{test,e2e}/**/{fixtures,_temp-fixtures}/**', + 'benchmark/**', + ], + workspaces: { + '.': { + ignoreDependencies: [ + '@astrojs/check', // Used by the build script but not as a standard module import + ], + // In smoke tests, we checkout to the docs repo so those binaries are not present in this project + ignoreBinaries: ['docgen', 'docgen:errors', 'playwright'], + }, + 'packages/*': { + entry: [testEntry], + }, + 'packages/astro': { + entry: [ + // Can't be detected automatically since it's only in package.json#files + 'templates/**/*', + testEntry, + 'test/types/**/*', + 'e2e/**/*.test.js', + 'test/units/teardown.js', + ], + ignore: ['**/e2e/**/{fixtures,_temp-fixtures}/**', 'performance/**/*'], + // Those deps are used in tests but only referenced as strings + ignoreDependencies: [ + 'rehype-autolink-headings', + 'rehype-slug', + 'rehype-toc', + 'remark-code-titles', + ], + }, + 'packages/integrations/*': { + entry: [testEntry], + }, + 'packages/integrations/cloudflare': { + entry: [testEntry], + // False positive because of cloudflare:workers + ignoreDependencies: ['cloudflare'], + }, + 'packages/integrations/mdx': { + entry: [testEntry], + // Required but not imported directly + ignoreDependencies: ['@types/*'], + }, + 'packages/integrations/netlify': { + entry: [testEntry], + ignore: ['test/hosted/**'], + }, + 'packages/integrations/solid': { + entry: [testEntry], + // It's an optional peer dep (triggers a warning) but it's fine in this case + ignoreDependencies: ['solid-devtools'], + }, + 'packages/integrations/vercel': { + entry: [testEntry, 'test/test-image-service.js'], + ignore: ['test/hosted/**'], + }, + 'packages/markdown/remark': { + entry: [testEntry], + // package.json#imports are not resolved at the moment + ignore: ['src/import-plugin-browser.ts'], + }, + 'packages/upgrade': { + entry: ['src/index.ts', testEntry], + }, + }, +}; diff --git a/package.json b/package.json index 11bf44115ba5..d79a0e38fa2e 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,13 @@ "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": "biome lint && eslint . --report-unused-disable-directives-severity=warn", - "lint:ci": "biome ci --formatter-enabled=false --organize-imports-enabled=false --reporter=github && eslint . --report-unused-disable-directives-severity=warn", + "lint": "biome lint && knip && eslint . --report-unused-disable-directives-severity=warn", + "lint:ci": "biome ci --formatter-enabled=false --organize-imports-enabled=false --reporter=github && eslint . --report-unused-disable-directives-severity=warn && knip", "lint:fix": "biome lint --write --unsafe", "publint": "pnpm -r --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --no-bail exec publint", "version": "changeset version && node ./scripts/deps/update-example-versions.js && pnpm install --no-frozen-lockfile && pnpm run format", - "preinstall": "npx only-allow pnpm" + "preinstall": "npx only-allow pnpm", + "knip": "knip" }, "workspaces": [ "packages/markdown/*", @@ -58,10 +59,11 @@ "@biomejs/biome": "1.9.4", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.28.1", - "@types/node": "^18.17.8", + "@types/node": "^18.19.50", "esbuild": "^0.25.0", "eslint": "^9.24.0", "eslint-plugin-regexp": "^2.7.0", + "knip": "5.50.5", "only-allow": "^1.2.1", "prettier": "^3.5.3", "prettier-plugin-astro": "^0.14.1", @@ -83,9 +85,6 @@ "workerd", "@biomejs/biome", "sharp" - ], - "patchedDependencies": { - "unifont@0.1.7": "patches/unifont@0.1.7.patch" - } + ] } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 8763c55f578d..91ad4b61a39f 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,210 @@ # astro +## 5.7.13 + +### Patch Changes + +- [#13761](https://github.com/withastro/astro/pull/13761) [`a2e8463`](https://github.com/withastro/astro/commit/a2e84631ad0a8dbc466d1301cc07a031334ffe5b) Thanks [@jp-knj](https://github.com/jp-knj)! - Adds new content collections errors + +- [#13788](https://github.com/withastro/astro/pull/13788) [`7d0b7ac`](https://github.com/withastro/astro/commit/7d0b7acb38d5140939d9660b2cf5718e9a8b2c15) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where an error would not be thrown when using the `` component from the experimental fonts API without adding fonts in the Astro config + +- [#13784](https://github.com/withastro/astro/pull/13784) [`d7a1889`](https://github.com/withastro/astro/commit/d7a188988427d1b157d27b789f918c208ece41f7) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes the experimental fonts API to correctly take `config.base`, `config.build.assets` and `config.build.assetsPrefix` into account + +- [#13777](https://github.com/withastro/astro/pull/13777) [`a56b8ea`](https://github.com/withastro/astro/commit/a56b8eaec486d26cbc61a7c94c152f4ee8cabc7a) Thanks [@L4Ph](https://github.com/L4Ph)! - Fixed an issue where looping GIF animation would stop when converted to WebP + +- [#13566](https://github.com/withastro/astro/pull/13566) [`0489d8f`](https://github.com/withastro/astro/commit/0489d8fe96fb8ee90284277358e38f55c8e0ab1d) Thanks [@TheOtterlord](https://github.com/TheOtterlord)! - Fix build errors being ignored when build.concurrency > 1 + +## 5.7.12 + +### Patch Changes + +- [#13752](https://github.com/withastro/astro/pull/13752) [`a079c21`](https://github.com/withastro/astro/commit/a079c21629ecf95b7539d9afdf90831266d00daf) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Improves handling of font URLs not ending with a file extension when using the experimental fonts API + +- [#13750](https://github.com/withastro/astro/pull/13750) [`7d3127d`](https://github.com/withastro/astro/commit/7d3127db9191556d2ead8a1ea35acb972ee67ec3) Thanks [@martrapp](https://github.com/martrapp)! - Allows the ClientRouter to open new tabs or windows when submitting forms by clicking while holding the Cmd, Ctrl, or Shift key. + +- [#13765](https://github.com/withastro/astro/pull/13765) [`d874fe0`](https://github.com/withastro/astro/commit/d874fe08f903a44cd8017313accbc02bcf9cb7d9) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where font sources with relative protocol URLs would fail when using the experimental fonts API + +- [#13640](https://github.com/withastro/astro/pull/13640) [`5e582e7`](https://github.com/withastro/astro/commit/5e582e7b4d56425d622c97ad933b1da0e7434155) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Allows inferring `weight` and `style` when using the local provider of the experimental fonts API + + If you want Astro to infer those properties directly from your local font files, leave them undefined: + + ```js + { + // No weight specified: infer + style: 'normal'; // Do not infer + } + ``` + +## 5.7.11 + +### Patch Changes + +- [#13734](https://github.com/withastro/astro/pull/13734) [`30aec73`](https://github.com/withastro/astro/commit/30aec7372b630649e1e484d9453842d3c36eaa26) Thanks [@ascorbic](https://github.com/ascorbic)! - Loosen content layer schema types + +- [#13751](https://github.com/withastro/astro/pull/13751) [`5816b8a`](https://github.com/withastro/astro/commit/5816b8a6d1295b297c9562ec245db6c60c37f1b1) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates `unifont` to support subsets when using the `google` provider with the experimental fonts API + +- [#13756](https://github.com/withastro/astro/pull/13756) [`d4547ba`](https://github.com/withastro/astro/commit/d4547bafef559b4f9ecd6e407d531aa51c46f7be) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds a terminal warning when a remote provider returns no data for a family when using the experimental fonts API + +- [#13742](https://github.com/withastro/astro/pull/13742) [`f599463`](https://github.com/withastro/astro/commit/f5994639120552e38e65c5d4d9688c1a3aa92f90) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes optimized fallback css generation to properly add a `src` when using the experimental fonts API + +- [#13740](https://github.com/withastro/astro/pull/13740) [`6935540`](https://github.com/withastro/astro/commit/6935540e44e5c75fd2106e3ae37add5e8ae7c67f) Thanks [@vixalien](https://github.com/vixalien)! - Fix cookies set after middleware did a rewrite with `next(url)` not being applied + +- [#13759](https://github.com/withastro/astro/pull/13759) [`4a56d0a`](https://github.com/withastro/astro/commit/4a56d0a44fb472ef2e3a9999c1b69a52da1afed3) Thanks [@jp-knj](https://github.com/jp-knj)! - Improved the error handling of certain error cases. + +## 5.7.10 + +### Patch Changes + +- [#13731](https://github.com/withastro/astro/pull/13731) [`c3e80c2`](https://github.com/withastro/astro/commit/c3e80c25b90c803e2798b752583a8e77cdad3146) Thanks [@jsparkdev](https://github.com/jsparkdev)! - update vite to latest version for fixing CVE + +## 5.7.9 + +### Patch Changes + +- [#13711](https://github.com/withastro/astro/pull/13711) [`2103991`](https://github.com/withastro/astro/commit/210399155a6004e8e975f9024ae6d7e9945ae9a9) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes height for responsive images + +## 5.7.8 + +### Patch Changes + +- [#13715](https://github.com/withastro/astro/pull/13715) [`b32dffa`](https://github.com/withastro/astro/commit/b32dffab6e16388c87fb5e8bb423ed02d88586bb) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates `unifont` to fix a case where a `unicodeRange` related error would be thrown when using the experimental fonts API + +## 5.7.7 + +### Patch Changes + +- [#13705](https://github.com/withastro/astro/pull/13705) [`28f8716`](https://github.com/withastro/astro/commit/28f8716ceef8b30ebb4da8c6ef32acc72405c1e6) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates unifont to latest and adds support for `fetch` options from remote providers when using the experimental fonts API + +- [#13692](https://github.com/withastro/astro/pull/13692) [`60d5be4`](https://github.com/withastro/astro/commit/60d5be4af49a72e3739f74424c3d5c423f98c133) Thanks [@Le0Developer](https://github.com/Le0Developer)! - Fixes a bug where Astro couldn't probably use `inferSize` for images that contain apostrophe `'` in their name. + +- [#13698](https://github.com/withastro/astro/pull/13698) [`ab98f88`](https://github.com/withastro/astro/commit/ab98f884f2f8639a8f385cdbc919bc829014f64d) Thanks [@sarah11918](https://github.com/sarah11918)! - Improves the configuration reference docs for the `adapter` entry with more relevant text and links. + +- [#13706](https://github.com/withastro/astro/pull/13706) [`b4929ae`](https://github.com/withastro/astro/commit/b4929ae9e77f74bde251e81abc0a80e160de774a) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes typechecking for content config schema + +- [#13653](https://github.com/withastro/astro/pull/13653) [`a7b2dc6`](https://github.com/withastro/astro/commit/a7b2dc60ca94f42a66575feb190e8b0f36b48e7c) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Reduces the amount of preloaded files for the local provider when using the experimental fonts API + +- [#13653](https://github.com/withastro/astro/pull/13653) [`a7b2dc6`](https://github.com/withastro/astro/commit/a7b2dc60ca94f42a66575feb190e8b0f36b48e7c) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where invalid CSS was emitted when using an experimental fonts API family name containing a space + +## 5.7.6 + +### Patch Changes + +- [#13703](https://github.com/withastro/astro/pull/13703) [`659904b`](https://github.com/withastro/astro/commit/659904bd999c6abdd62f18230954b7097dcbb7fe) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug where empty fallbacks could not be provided when using the experimental fonts API + +- [#13680](https://github.com/withastro/astro/pull/13680) [`18e1b97`](https://github.com/withastro/astro/commit/18e1b978f045f4c21d9cb4241a8c7fbb956d2efe) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Improves the `UnsupportedExternalRedirect` error message to include more details such as the concerned destination + +- [#13703](https://github.com/withastro/astro/pull/13703) [`659904b`](https://github.com/withastro/astro/commit/659904bd999c6abdd62f18230954b7097dcbb7fe) Thanks [@ascorbic](https://github.com/ascorbic)! - Simplifies styles for experimental responsive images + + :warning: **BREAKING CHANGE FOR EXPERIMENTAL RESPONSIVE IMAGES ONLY** :warning: + + The generated styles for image layouts are now simpler and easier to override. Previously the responsive image component used CSS to set the size and aspect ratio of the images, but this is no longer needed. Now the styles just include `object-fit` and `object-position` for all images, and sets `max-width: 100%` for constrained images and `width: 100%` for full-width images. + + This is an implementation change only, and most users will see no change. However, it may affect any custom styles you have added to your responsive images. Please check your rendered images to determine whether any change to your CSS is needed. + + The styles now use the [`:where()` pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:where), which has a [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity) of 0, meaning that it is easy to override with your own styles. You can now be sure that your own classes will always override the applied styles, as will global styles on `img`. + + An exception is Tailwind 4, which uses [cascade layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer), meaning the rules are always lower specificity. Astro supports browsers that do not support cascade layers, so we cannot use this. If you need to override the styles using Tailwind 4, you must use `!important` classes. Do check if this is needed though: there may be a layout that is more appropriate for your use case. + +- [#13703](https://github.com/withastro/astro/pull/13703) [`659904b`](https://github.com/withastro/astro/commit/659904bd999c6abdd62f18230954b7097dcbb7fe) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds warnings about using local font files in the `publicDir` when the experimental fonts API is enabled. + +- [#13703](https://github.com/withastro/astro/pull/13703) [`659904b`](https://github.com/withastro/astro/commit/659904bd999c6abdd62f18230954b7097dcbb7fe) Thanks [@ascorbic](https://github.com/ascorbic)! - Renames experimental responsive image layout option from "responsive" to "constrained" + + :warning: **BREAKING CHANGE FOR EXPERIMENTAL RESPONSIVE IMAGES ONLY** :warning: + + The layout option called `"responsive"` is renamed to `"constrained"` to better reflect its behavior. + + The previous name was causing confusion, because it is also the name of the feature. The `responsive` layout option is specifically for images that are displayed at the requested size, unless they do not fit the width of their container, at which point they would be scaled down to fit. They do not get scaled beyond the intrinsic size of the source image, or the `width` prop if provided. + + It became clear from user feedback that many people (understandably) thought that they needed to set `layout` to `responsive` if they wanted to use responsive images. They then struggled with overriding styles to make the image scale up for full-width hero images, for example, when they should have been using `full-width` layout. Renaming the layout to `constrained` should make it clearer that this layout is for when you want to constrain the maximum size of the image, but allow it to scale-down. + + ### Upgrading + + If you set a default `image.experimentalLayout` in your `astro.config.mjs`, or set it on a per-image basis using the `layout` prop, you will need to change all occurences to `constrained`: + + ```diff lang="ts" + // astro.config.mjs + export default { + image: { + - experimentalLayout: 'responsive', + + experimentalLayout: 'constrained', + }, + } + ``` + + ```diff lang="astro" + // src/pages/index.astro + --- + import { Image } from 'astro:assets'; + --- + - + + + ``` + + Please [give feedback on the RFC](https://github.com/withastro/roadmap/pull/1051) if you have any questions or comments about the responsive images API. + +## 5.7.5 + +### Patch Changes + +- [#13660](https://github.com/withastro/astro/pull/13660) [`620d15d`](https://github.com/withastro/astro/commit/620d15d8483dfb1822cd47833bc1653e0b704ccb) Thanks [@mingjunlu](https://github.com/mingjunlu)! - Adds `server.allowedHosts` docs comment to `AstroUserConfig` + +- [#13591](https://github.com/withastro/astro/pull/13591) [`5dd2d3f`](https://github.com/withastro/astro/commit/5dd2d3fde8a138ed611dedf39ffa5dfeeed315f8) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes unused code + +- [#13669](https://github.com/withastro/astro/pull/13669) [`73f24d4`](https://github.com/withastro/astro/commit/73f24d400acdc48462a7bc5277b8cee2bcf97580) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where `Astro.originPathname` wasn't returning the correct value when using rewrites. + +- [#13674](https://github.com/withastro/astro/pull/13674) [`42388b2`](https://github.com/withastro/astro/commit/42388b24d6eb866a3129118d22b2f6c71071d0bd) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where an experimental fonts API error would be thrown when using another `astro:assets` API + +- [#13654](https://github.com/withastro/astro/pull/13654) [`4931457`](https://github.com/withastro/astro/commit/49314575a76b52b43e491a0a33c0ccaf9cafb058) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes `fontProviders.google()` so it can forward options to the unifont provider, when using the experimental fonts API + +- Updated dependencies [[`5dd2d3f`](https://github.com/withastro/astro/commit/5dd2d3fde8a138ed611dedf39ffa5dfeeed315f8)]: + - @astrojs/telemetry@3.2.1 + +## 5.7.4 + +### Patch Changes + +- [#13647](https://github.com/withastro/astro/pull/13647) [`ffbe8f2`](https://github.com/withastro/astro/commit/ffbe8f27a3e897971432eed1fde566db328b540d) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug that caused a session error to be logged when using actions without sessions + +- [#13646](https://github.com/withastro/astro/pull/13646) [`6744842`](https://github.com/withastro/astro/commit/67448426fb4e2289ef8bc25d97bd617456b18b68) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where extra font sources were removed when using the experimental fonts API + +- [#13635](https://github.com/withastro/astro/pull/13635) [`d75cac4`](https://github.com/withastro/astro/commit/d75cac45de8790331aad134ae91bfeb1943cd458) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - The experimental fonts API now generates optimized fallbacks for every weight and style + +## 5.7.3 + +### Patch Changes + +- [#13643](https://github.com/withastro/astro/pull/13643) [`67b7493`](https://github.com/withastro/astro/commit/67b749391a9069ae1d94ef646b68a99973ef44d7) Thanks [@tanishqmanuja](https://github.com/tanishqmanuja)! - Fixes a case where the font face `src` format would be invalid when using the experimental fonts API + +- [#13639](https://github.com/withastro/astro/pull/13639) [`23410c6`](https://github.com/withastro/astro/commit/23410c644f5fc528ef630f2bcbe58c68dfe0c719) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where some font families would not be downloaded when using the same font provider several times, using the experimental fonts API + +## 5.7.2 + +### Patch Changes + +- [#13632](https://github.com/withastro/astro/pull/13632) [`cb05cfb`](https://github.com/withastro/astro/commit/cb05cfba12d1c6ea8cee98552c86a98bfb56794c) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Improves the optimized fallback name generated by the experimental Fonts API + +- [#13630](https://github.com/withastro/astro/pull/13630) [`3e7db4f`](https://github.com/withastro/astro/commit/3e7db4f802f69404ad2a3c3a3710452554ee40ec) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where fonts using a local provider would not work because of an invalid generated `src` + +- [#13634](https://github.com/withastro/astro/pull/13634) [`516de7d`](https://github.com/withastro/astro/commit/516de7dbe6d8aac20bb0ca8243c92cc7cbd730ce) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a regression where using `next('/')` didn't correctly return the requested route. + +- [#13632](https://github.com/withastro/astro/pull/13632) [`cb05cfb`](https://github.com/withastro/astro/commit/cb05cfba12d1c6ea8cee98552c86a98bfb56794c) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Improves the quality of optimized fallbacks generated by the experimental Fonts API + +- [#13616](https://github.com/withastro/astro/pull/13616) [`d475afc`](https://github.com/withastro/astro/commit/d475afcae7259204072e644e3d66e5479510f410) Thanks [@lfilho](https://github.com/lfilho)! - Fixes a regression where relative static redirects didn't work as expected. + +## 5.7.1 + +### Patch Changes + +- [#13594](https://github.com/withastro/astro/pull/13594) [`dc4a015`](https://github.com/withastro/astro/commit/dc4a015cf33c01b659e07b7d31dbd49f1c2ebfdf) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Reduces the number of font files downloaded + +- [#13627](https://github.com/withastro/astro/pull/13627) [`7f1a624`](https://github.com/withastro/astro/commit/7f1a62484ed17fe7a9be5d1e2bb71e2fd12b9fed) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where using the `` component would throw a Rollup error during the build + +- [#13626](https://github.com/withastro/astro/pull/13626) [`3838efe`](https://github.com/withastro/astro/commit/3838efe5028256e0e28bf823f868bcda6ef1e775) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates fallback font generation to always read font files returned by font providers + +- [#13625](https://github.com/withastro/astro/pull/13625) [`f1311d2`](https://github.com/withastro/astro/commit/f1311d2acb6dd7a75f7ea10eea3a02fbe674eb2a) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates the `` component so that preload links are generated before the style tag if `preload` is passed + +- [#13622](https://github.com/withastro/astro/pull/13622) [`a70d32a`](https://github.com/withastro/astro/commit/a70d32a4284ef18c3f93196f44c1fcf3ff56d3d5) Thanks [@ascorbic](https://github.com/ascorbic)! - Improve autocomplete for session keys + ## 5.7.0 ### Minor Changes diff --git a/packages/astro/components/ClientRouter.astro b/packages/astro/components/ClientRouter.astro index 8bda7b780fa0..df7a37da3d79 100644 --- a/packages/astro/components/ClientRouter.astro +++ b/packages/astro/components/ClientRouter.astro @@ -37,6 +37,7 @@ const { fallback = 'animate' } = Astro.props; import { init } from 'astro/virtual-modules/prefetch.js'; type Fallback = 'none' | 'animate' | 'swap'; + let lastClickedElementLeavingWindow: EventTarget | null = null; function getFallback(): Fallback { const el = document.querySelector('[name="astro-view-transitions-fallback"]'); @@ -50,6 +51,13 @@ const { fallback = 'animate' } = Astro.props; return el.dataset.astroReload !== undefined; } + const leavesWindow = (ev: MouseEvent) => + (ev.button && ev.button !== 0) || // left clicks only + ev.metaKey || // new tab (mac) + ev.ctrlKey || // new tab (windows) + ev.altKey || // download + ev.shiftKey; // new window + if (supportsViewTransitions || getFallback() !== 'none') { if (import.meta.env.DEV && window.matchMedia('(prefers-reduced-motion)').matches) { console.warn( @@ -58,6 +66,9 @@ const { fallback = 'animate' } = Astro.props; } document.addEventListener('click', (ev) => { let link = ev.target; + + lastClickedElementLeavingWindow = leavesWindow(ev) ? link : null; + if (ev.composed) { link = ev.composedPath()[0]; } @@ -82,11 +93,7 @@ const { fallback = 'animate' } = Astro.props; !link.href || (linkTarget && linkTarget !== '_self') || origin !== location.origin || - ev.button !== 0 || // left clicks only - ev.metaKey || // new tab (mac) - ev.ctrlKey || // new tab (windows) - ev.altKey || // download - ev.shiftKey || // new window + lastClickedElementLeavingWindow || ev.defaultPrevented ) { // No page transitions in these cases, @@ -102,11 +109,15 @@ const { fallback = 'animate' } = Astro.props; document.addEventListener('submit', (ev) => { let el = ev.target as HTMLElement; - if (el.tagName !== 'FORM' || ev.defaultPrevented || isReloadEl(el)) { + const submitter = ev.submitter; + + const clickedWithKeys = submitter && submitter === lastClickedElementLeavingWindow; + lastClickedElementLeavingWindow = null; + + if (el.tagName !== 'FORM' || ev.defaultPrevented || isReloadEl(el) || clickedWithKeys) { return; } const form = el as HTMLFormElement; - const submitter = ev.submitter; const formData = new FormData(form, submitter); // form.action and form.method can point to an or // in which case should fallback to the form attribute diff --git a/packages/astro/components/Font.astro b/packages/astro/components/Font.astro index 53d26ea361ad..a5bfba4d9534 100644 --- a/packages/astro/components/Font.astro +++ b/packages/astro/components/Font.astro @@ -1,10 +1,12 @@ --- +import * as mod from 'virtual:astro:assets/fonts/internal'; import { AstroError, AstroErrorData } from '../dist/core/errors/index.js'; -// TODO: remove dynamic import when fonts are stabilized -const { fontsData } = await import('virtual:astro:assets/fonts/internal').catch(() => { +// TODO: remove check when fonts are stabilized +const { fontsData } = mod; +if (!fontsData) { throw new AstroError(AstroErrorData.ExperimentalFontsNotEnabled); -}); +} interface Props { /** The `cssVariable` registered in your Astro configuration. */ @@ -13,7 +15,7 @@ interface Props { preload?: boolean; } -const { cssVariable, preload = false } = Astro.props; +const { cssVariable, preload = false } = Astro.props as Props; const data = fontsData.get(cssVariable); if (!data) { throw new AstroError({ @@ -23,10 +25,10 @@ if (!data) { } --- - { preload && data.preloadData.map(({ url, type }) => ( )) } + diff --git a/packages/astro/components/Picture.astro b/packages/astro/components/Picture.astro index 08d7e1cd3bc1..8ebb4995f96c 100644 --- a/packages/astro/components/Picture.astro +++ b/packages/astro/components/Picture.astro @@ -54,6 +54,7 @@ if (useResponsive) { for (const key in props) { if (key.startsWith('data-astro-cid')) { + // @ts-expect-error This is for island props so they're not properly typed pictureAttributes[key] = props[key]; } } diff --git a/packages/astro/components/env.d.ts b/packages/astro/components/env.d.ts new file mode 100644 index 000000000000..562dda037b8b --- /dev/null +++ b/packages/astro/components/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/packages/astro/components/image.css b/packages/astro/components/image.css index d748ba7d5d4f..6815ec126797 100644 --- a/packages/astro/components/image.css +++ b/packages/astro/components/image.css @@ -1,17 +1,11 @@ -[data-astro-image] { - width: 100%; - height: auto; +:where([data-astro-image]) { object-fit: var(--fit); object-position: var(--pos); - aspect-ratio: var(--w) / var(--h); + height: auto; } -/* Styles for responsive layout */ -[data-astro-image='responsive'] { - max-width: calc(var(--w) * 1px); - max-height: calc(var(--h) * 1px); +:where([data-astro-image='full-width']) { + width: 100%; } -/* Styles for fixed layout */ -[data-astro-image='fixed'] { - width: calc(var(--w) * 1px); - height: calc(var(--h) * 1px); +:where([data-astro-image='constrained']) { + max-width: 100%; } diff --git a/packages/astro/components/tsconfig.json b/packages/astro/components/tsconfig.json new file mode 100644 index 000000000000..71b1925eb046 --- /dev/null +++ b/packages/astro/components/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "emitDeclarationOnly": false, + "noEmit": true, + "jsx": "preserve" + } +} diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index 5a5420a95ced..54a6b365f80d 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -3,3 +3,7 @@ declare module 'virtual:astro:env/internal' { export const schema: import('./src/env/schema.js').EnvSchema; } + +declare module 'virtual:astro:assets/fonts/internal' { + export const fontsData: import('./src/assets/fonts/types.js').ConsumableMap; +} diff --git a/packages/astro/e2e/fixtures/view-transitions/public/favicon.ico b/packages/astro/e2e/fixtures/view-transitions/public/favicon.ico new file mode 100644 index 000000000000..578ad458b890 Binary files /dev/null and b/packages/astro/e2e/fixtures/view-transitions/public/favicon.ico differ diff --git a/packages/astro/e2e/test-utils.js b/packages/astro/e2e/test-utils.js index a2728f9627c2..d2858db0a630 100644 --- a/packages/astro/e2e/test-utils.js +++ b/packages/astro/e2e/test-utils.js @@ -4,10 +4,6 @@ import { fileURLToPath } from 'node:url'; import { expect, test as testBase } from '@playwright/test'; import { loadFixture as baseLoadFixture } from '../test/test-utils.js'; -export const isWindows = process.platform === 'win32'; - -export { silentLogging } from '../test/test-utils.js'; - // Get all test files in directory, assign unique port for each of them so they don't conflict const testFiles = await fs.readdir(new URL('.', import.meta.url)); const testFileToPort = new Map(); diff --git a/packages/astro/package.json b/packages/astro/package.json index 7586e36990a1..9c7c471913c4 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "5.7.0", + "version": "5.7.13", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", @@ -47,10 +47,7 @@ "./compiler-runtime": "./dist/runtime/compiler/index.js", "./runtime/*": "./dist/runtime/*", "./config": "./dist/config/entrypoint.js", - "./container": { - "types": "./dist/container/index.d.ts", - "default": "./dist/container/index.js" - }, + "./container": "./dist/container/index.js", "./app": "./dist/core/app/index.js", "./app/node": "./dist/core/app/node.js", "./client/*": "./dist/runtime/client/*", @@ -76,17 +73,14 @@ "default": "./zod.mjs" }, "./errors": "./dist/core/errors/userError.js", - "./middleware": { - "types": "./dist/core/middleware/index.d.ts", - "default": "./dist/core/middleware/index.js" - }, + "./middleware": "./dist/core/middleware/index.js", "./virtual-modules/*": "./dist/virtual-modules/*" }, "bin": { "astro": "astro.js" }, "files": [ - "components", + "components/*.{astro,css,ts}", "tsconfigs", "dist", "types", @@ -105,7 +99,7 @@ ], "scripts": { "prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"", - "build": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" --copy-wasm && tsc", + "build": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" --copy-wasm && tsc && astro-check --root ./components", "build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" --copy-wasm", "dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"", "test": "pnpm run test:unit && pnpm run test:integration && pnpm run test:types", @@ -123,7 +117,6 @@ "@astrojs/internal-helpers": "workspace:*", "@astrojs/markdown-remark": "workspace:*", "@astrojs/telemetry": "workspace:*", - "@capsizecss/metrics": "^3.5.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", @@ -146,6 +139,7 @@ "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", + "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", @@ -167,11 +161,11 @@ "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", - "unifont": "^0.1.7", + "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", - "vite": "^6.2.6", + "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", @@ -216,6 +210,7 @@ "remark-code-titles": "^0.1.2", "rollup": "^4.37.0", "sass": "^1.86.0", + "typescript": "^5.8.3", "undici": "^7.5.0", "unified": "^11.0.5", "vitest": "^3.0.9" diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index fe046701dbf3..e8c8cf71f651 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -300,13 +300,20 @@ export function getActionContext(context: APIContext): AstroActionContext { } throw e; } - const { - props: _props, - getActionResult: _getActionResult, - callAction: _callAction, - redirect: _redirect, - ...actionAPIContext - } = context; + + const omitKeys = ['props', 'getActionResult', 'callAction', 'redirect']; + + // Clones the context, preserving accessors and methods but omitting + // the properties that are not needed in the action handler. + const actionAPIContext = Object.create( + Object.getPrototypeOf(context), + Object.fromEntries( + Object.entries(Object.getOwnPropertyDescriptors(context)).filter( + ([key]) => !omitKeys.includes(key), + ), + ), + ); + Reflect.set(actionAPIContext, ACTION_API_CONTEXT_SYMBOL, true); const handler = baseAction.bind(actionAPIContext satisfies ActionAPIContext); return handler(input); diff --git a/packages/astro/src/assets/fonts/README.md b/packages/astro/src/assets/fonts/README.md index bd5a5cbdc788..dd630a729340 100644 --- a/packages/astro/src/assets/fonts/README.md +++ b/packages/astro/src/assets/fonts/README.md @@ -1,11 +1,20 @@ # fonts -The vite plugin orchestrates the fonts logic: +Here is an overview of the architecture of the fonts in Astro: -- Retrieves data from the config -- Initializes font providers -- Fetches fonts data -- In dev, serves a middleware that dynamically loads and caches fonts data -- In build, download fonts data (from cache if possible) - -The `` component is the only aspect not managed in the vite plugin, since it's exported from `astro:assets`. +- [`orchestrate()`](./orchestrate.ts) combines sub steps and takes care of getting useful data from the config + - It resolves font families (eg. import remote font providers) + - It prepares [`unifont`](https://github.com/unjs/unifont) providers + - It initializes `unifont` + - For each family, it resolves fonts data and normalizes them + - For each family, optimized fallbacks (and related CSS) are generated if applicable + - It returns the data +- [`/logic`](./logic/) contains the sub steps of `orchestrate()` so they can be easily tested +- The logic uses [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) to make it easily testable and swappable + - [`definitions.ts`](./definitions.ts) defines dependencies + - Those dependencies are implemented in [`/implementations`](./implementations/) +- [`fontsPlugin()`](./vite-plugin-fonts.ts) calls `orchestrate()` and using its result, setups anything required so that fonts can + - Be exposed to users (virual module) + - Be used in dev (middleware) + - Be used in build (copy) +- [``](../../../components/Font.astro) is managed in [`assets()`](../vite-plugin-assets.ts) so it can be imported from `astro:assets` and consumes the virtual module diff --git a/packages/astro/src/assets/fonts/config.ts b/packages/astro/src/assets/fonts/config.ts index 1f837862a459..73b28105bf64 100644 --- a/packages/astro/src/assets/fonts/config.ts +++ b/packages/astro/src/assets/fonts/config.ts @@ -2,7 +2,8 @@ import { z } from 'zod'; import { LOCAL_PROVIDER_NAME } from './constants.js'; const weightSchema = z.union([z.string(), z.number()]); -const styleSchema = z.enum(['normal', 'italic', 'oblique']); +export const styleSchema = z.enum(['normal', 'italic', 'oblique']); +const unicodeRangeSchema = z.array(z.string()).nonempty(); const familyPropertiesSchema = z.object({ /** @@ -12,21 +13,17 @@ const familyPropertiesSchema = z.object({ * weight: "100 900" * ``` */ - weight: weightSchema, + weight: weightSchema.optional(), /** * A [font style](https://developer.mozilla.org/en-US/docs/Web/CSS/font-style). */ - style: styleSchema, + style: styleSchema.optional(), /** * @default `"swap"` * * A [font display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display). */ display: z.enum(['auto', 'block', 'swap', 'fallback', 'optional']).optional(), - /** - * A [unicode range](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). - */ - unicodeRange: z.array(z.string()).nonempty().optional(), /** * A [font stretch](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-stretch). */ @@ -58,9 +55,9 @@ const fallbacksSchema = z.object({ * ``` * - * If the last font in the `fallbacks` array is a [generic family name](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#generic-name), an [optimized fallback](https://developer.chrome.com/blog/font-fallbacks) using font metrics will be generated. To disable this optimization, set `optimizedFallbacks` to false. + * If the last font in the `fallbacks` array is a [generic family name](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#generic-name), Astro will attempt to generate [optimized fallbacks](https://developer.chrome.com/blog/font-fallbacks) using font metrics will be generated. To disable this optimization, set `optimizedFallbacks` to false. */ - fallbacks: z.array(z.string()).nonempty().optional(), + fallbacks: z.array(z.string()).optional(), /** * @default `true` * @@ -69,7 +66,7 @@ const fallbacksSchema = z.object({ optimizedFallbacks: z.boolean().optional(), }); -export const requiredFamilyAttributesSchema = z.object({ +const requiredFamilyAttributesSchema = z.object({ /** * The font family name, as identified by your font provider. */ @@ -122,6 +119,10 @@ export const localFontFamilySchema = requiredFamilyAttributesSchema ]), ) .nonempty(), + /** + * A [unicode range](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). + */ + unicodeRange: unicodeRangeSchema.optional(), // TODO: find a way to support subsets (through fontkit?) }) .strict(), @@ -162,13 +163,16 @@ export const remoteFontFamilySchema = requiredFamilyAttributesSchema * An array of [font styles](https://developer.mozilla.org/en-US/docs/Web/CSS/font-style). */ styles: z.array(styleSchema).nonempty().optional(), - // TODO: better link /** * @default `["cyrillic-ext", "cyrillic", "greek-ext", "greek", "vietnamese", "latin-ext", "latin"]` * - * An array of [font subsets](https://fonts.google.com/knowledge/glossary/subsetting): + * An array of [font subsets](https://knaap.dev/posts/font-subsetting/): */ subsets: z.array(z.string()).nonempty().optional(), + /** + * A [unicode range](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). + */ + unicodeRange: unicodeRangeSchema.optional(), }), ) .strict(); diff --git a/packages/astro/src/assets/fonts/constants.ts b/packages/astro/src/assets/fonts/constants.ts index c12d31fbd9b6..33993ef9c0b3 100644 --- a/packages/astro/src/assets/fonts/constants.ts +++ b/packages/astro/src/assets/fonts/constants.ts @@ -1,40 +1,46 @@ -import type { ResolvedRemoteFontFamily } from './types.js'; +import type { Defaults, FontType } from './types.js'; export const LOCAL_PROVIDER_NAME = 'local'; -export const DEFAULTS = { +export const DEFAULTS: Defaults = { weights: ['400'], styles: ['normal', 'italic'], subsets: ['cyrillic-ext', 'cyrillic', 'greek-ext', 'greek', 'vietnamese', 'latin-ext', 'latin'], // Technically serif is the browser default but most websites these days use sans-serif fallbacks: ['sans-serif'], optimizedFallbacks: true, -} satisfies Partial; +}; export const VIRTUAL_MODULE_ID = 'virtual:astro:assets/fonts/internal'; export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; -// Requires a trailing slash -export const URL_PREFIX = '/_astro/fonts/'; +export const ASSETS_DIR = 'fonts'; export const CACHE_DIR = './fonts/'; export const FONT_TYPES = ['woff2', 'woff', 'otf', 'ttf', 'eot'] as const; -// Source: https://github.com/nuxt/fonts/blob/3a3eb6dfecc472242b3011b25f3fcbae237d0acc/src/module.ts#L55-L75 -export const DEFAULT_FALLBACKS: Record> = { - serif: ['Times New Roman'], - 'sans-serif': ['Arial'], - monospace: ['Courier New'], - cursive: [], - fantasy: [], - 'system-ui': ['BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial'], - 'ui-serif': ['Times New Roman'], - 'ui-sans-serif': ['Arial'], - 'ui-monospace': ['Courier New'], - 'ui-rounded': [], - emoji: [], - math: [], - fangsong: [], -}; +export const FONT_FORMATS: Array<{ type: FontType; format: string }> = [ + { type: 'woff2', format: 'woff2' }, + { type: 'woff', format: 'woff' }, + { type: 'otf', format: 'opentype' }, + { type: 'ttf', format: 'truetype' }, + { type: 'eot', format: 'embedded-opentype' }, +]; + +export const GENERIC_FALLBACK_NAMES = [ + 'serif', + 'sans-serif', + 'monospace', + 'cursive', + 'fantasy', + 'system-ui', + 'ui-serif', + 'ui-sans-serif', + 'ui-monospace', + 'ui-rounded', + 'emoji', + 'math', + 'fangsong', +] as const; export const FONTS_TYPES_FILE = 'fonts.d.ts'; diff --git a/packages/astro/src/assets/fonts/definitions.ts b/packages/astro/src/assets/fonts/definitions.ts new file mode 100644 index 000000000000..4c3b840c5d69 --- /dev/null +++ b/packages/astro/src/assets/fonts/definitions.ts @@ -0,0 +1,116 @@ +import type * as unifont from 'unifont'; +import type { CollectedFontForMetrics } from './logic/optimize-fallbacks.js'; +/* eslint-disable @typescript-eslint/no-empty-object-type */ +import type { + AstroFontProvider, + FontFileData, + FontType, + PreloadData, + ResolvedFontProvider, + Style, +} from './types.js'; +import type { FontFaceMetrics, GenericFallbackName } from './types.js'; + +export interface Hasher { + hashString: (input: string) => string; + hashObject: (input: Record) => string; +} + +export interface RemoteFontProviderModResolver { + resolve: (id: string) => Promise; +} + +export interface RemoteFontProviderResolver { + resolve: (provider: AstroFontProvider) => Promise; +} + +export interface LocalProviderUrlResolver { + resolve: (input: string) => string; +} + +type SingleErrorInput> = { + type: TType; + data: TData; + cause: unknown; +}; + +export type ErrorHandlerInput = + | SingleErrorInput< + 'cannot-load-font-provider', + { + entrypoint: string; + } + > + | SingleErrorInput<'unknown-fs-error', {}> + | SingleErrorInput<'cannot-fetch-font-file', { url: string }> + | SingleErrorInput<'cannot-extract-font-type', { url: string }> + | SingleErrorInput<'cannot-extract-data', { family: string; url: string }>; + +export interface ErrorHandler { + handle: (input: ErrorHandlerInput) => Error; +} + +export interface UrlProxy { + proxy: ( + input: Pick & { + type: FontType; + collectPreload: boolean; + data: Partial; + }, + ) => string; +} + +export interface UrlResolver { + resolve: (hash: string) => string; +} + +export interface UrlProxyContentResolver { + resolve: (url: string) => string; +} + +export interface DataCollector { + collect: ( + input: FontFileData & { + data: Partial; + preload: PreloadData | null; + }, + ) => void; +} + +export type CssProperties = Record; + +export interface CssRenderer { + generateFontFace: (family: string, properties: CssProperties) => string; + generateCssVariable: (key: string, values: Array) => string; +} + +export interface FontMetricsResolver { + getMetrics: (name: string, font: CollectedFontForMetrics) => Promise; + generateFontFace: (input: { + metrics: FontFaceMetrics; + fallbackMetrics: FontFaceMetrics; + name: string; + font: string; + properties: CssProperties; + }) => string; +} + +export interface SystemFallbacksProvider { + getLocalFonts: (fallback: GenericFallbackName) => Array | null; + getMetricsForLocalFont: (family: string) => FontFaceMetrics; +} + +export interface FontFetcher { + fetch: (input: FontFileData) => Promise; +} + +export interface FontTypeExtractor { + extract: (url: string) => FontType; +} + +export interface FontFileReader { + extract: (input: { family: string; url: string }) => { + weight: string; + style: Style; + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/css-renderer.ts b/packages/astro/src/assets/fonts/implementations/css-renderer.ts new file mode 100644 index 000000000000..3ae6d0393715 --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/css-renderer.ts @@ -0,0 +1,50 @@ +import type { CssProperties, CssRenderer } from '../definitions.js'; + +export function renderFontFace(properties: CssProperties, minify: boolean): string { + // Line feed + const lf = minify ? '' : `\n`; + // Space + const sp = minify ? '' : ' '; + + return `@font-face${sp}{${lf}${Object.entries(properties) + .filter(([, value]) => Boolean(value)) + .map(([key, value]) => `${sp}${sp}${key}:${sp}${value};`) + .join(lf)}${lf}}${lf}`; +} + +export function renderCssVariable(key: string, values: Array, minify: boolean): string { + // Line feed + const lf = minify ? '' : `\n`; + // Space + const sp = minify ? '' : ' '; + + return `:root${sp}{${lf}${sp}${sp}${key}:${sp}${values.map((v) => handleValueWithSpaces(v)).join(`,${sp}`)};${lf}}${lf}`; +} + +export function withFamily(family: string, properties: CssProperties): CssProperties { + return { + 'font-family': handleValueWithSpaces(family), + ...properties, + }; +} + +const SPACE_RE = /\s/; + +/** If the value contains spaces (which would be incorrectly interpreted), we wrap it in quotes. */ +export function handleValueWithSpaces(value: string): string { + if (SPACE_RE.test(value)) { + return JSON.stringify(value); + } + return value; +} + +export function createMinifiableCssRenderer({ minify }: { minify: boolean }): CssRenderer { + return { + generateFontFace(family, properties) { + return renderFontFace(withFamily(family, properties), minify); + }, + generateCssVariable(key, values) { + return renderCssVariable(key, values, minify); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/data-collector.ts b/packages/astro/src/assets/fonts/implementations/data-collector.ts new file mode 100644 index 000000000000..43f29b14f176 --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/data-collector.ts @@ -0,0 +1,21 @@ +import type { DataCollector } from '../definitions.js'; +import type { CreateUrlProxyParams } from '../types.js'; + +export function createDataCollector({ + hasUrl, + saveUrl, + savePreload, + saveFontData, +}: Omit): DataCollector { + return { + collect({ hash, url, init, preload, data }) { + if (!hasUrl(hash)) { + saveUrl({ hash, url, init }); + if (preload) { + savePreload(preload); + } + } + saveFontData({ hash, url, data, init }); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/error-handler.ts b/packages/astro/src/assets/fonts/implementations/error-handler.ts new file mode 100644 index 000000000000..a53ed4bc7eed --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/error-handler.ts @@ -0,0 +1,42 @@ +import { AstroError, AstroErrorData } from '../../../core/errors/index.js'; +import type { ErrorHandler, ErrorHandlerInput } from '../definitions.js'; + +function getProps(input: ErrorHandlerInput): ConstructorParameters[0] { + if (input.type === 'cannot-load-font-provider') { + return { + ...AstroErrorData.CannotLoadFontProvider, + message: AstroErrorData.CannotLoadFontProvider.message(input.data.entrypoint), + }; + } else if (input.type === 'unknown-fs-error') { + return AstroErrorData.UnknownFilesystemError; + } else if (input.type === 'cannot-fetch-font-file') { + return { + ...AstroErrorData.CannotFetchFontFile, + message: AstroErrorData.CannotFetchFontFile.message(input.data.url), + }; + } else if (input.type === 'cannot-extract-font-type') { + return { + ...AstroErrorData.CannotExtractFontType, + message: AstroErrorData.CannotExtractFontType.message(input.data.url), + }; + } else if (input.type === 'cannot-extract-data') { + return { + ...AstroErrorData.CannotDetermineWeightAndStyleFromFontFile, + message: AstroErrorData.CannotDetermineWeightAndStyleFromFontFile.message( + input.data.family, + input.data.url, + ), + }; + } + input satisfies never; + // Should never happen but TS isn't happy + return AstroErrorData.UnknownError; +} + +export function createAstroErrorHandler(): ErrorHandler { + return { + handle(input) { + return new AstroError(getProps(input), { cause: input.cause }); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/font-fetcher.ts b/packages/astro/src/assets/fonts/implementations/font-fetcher.ts new file mode 100644 index 000000000000..9bc2ae8ff0aa --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/font-fetcher.ts @@ -0,0 +1,39 @@ +import { isAbsolute } from 'node:path'; +import type { Storage } from 'unstorage'; +import type { ErrorHandler, FontFetcher } from '../definitions.js'; +import { cache } from '../utils.js'; + +export function createCachedFontFetcher({ + storage, + errorHandler, + fetch, + readFile, +}: { + storage: Storage; + errorHandler: ErrorHandler; + fetch: (url: string, init?: RequestInit) => Promise; + readFile: (url: string) => Promise; +}): FontFetcher { + return { + async fetch({ hash, url, init }) { + return await cache(storage, hash, async () => { + try { + if (isAbsolute(url)) { + return await readFile(url); + } + const response = await fetch(url, init ?? undefined); + if (!response.ok) { + throw new Error(`Response was not successful, received status code ${response.status}`); + } + return Buffer.from(await response.arrayBuffer()); + } catch (cause) { + throw errorHandler.handle({ + type: 'cannot-fetch-font-file', + data: { url }, + cause, + }); + } + }); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/font-file-reader.ts b/packages/astro/src/assets/fonts/implementations/font-file-reader.ts new file mode 100644 index 000000000000..f935288e76fa --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/font-file-reader.ts @@ -0,0 +1,26 @@ +import { readFileSync } from 'node:fs'; +import { fontace } from 'fontace'; +import type { ErrorHandler, FontFileReader } from '../definitions.js'; +import type { Style } from '../types.js'; + +export function createFontaceFontFileReader({ + errorHandler, +}: { errorHandler: ErrorHandler }): FontFileReader { + return { + extract({ family, url }) { + try { + const data = fontace(readFileSync(url)); + return { + weight: data.weight, + style: data.style as Style, + }; + } catch (cause) { + throw errorHandler.handle({ + type: 'cannot-extract-data', + data: { family, url }, + cause, + }); + } + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/font-metrics-resolver.ts b/packages/astro/src/assets/fonts/implementations/font-metrics-resolver.ts new file mode 100644 index 000000000000..2fe3baa442fb --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/font-metrics-resolver.ts @@ -0,0 +1,73 @@ +import { type Font, fromBuffer } from '@capsizecss/unpack'; +import type { CssRenderer, FontFetcher, FontMetricsResolver } from '../definitions.js'; +import type { FontFaceMetrics } from '../types.js'; +import { renderFontSrc } from '../utils.js'; + +// Source: https://github.com/unjs/fontaine/blob/main/src/metrics.ts +function filterRequiredMetrics({ + ascent, + descent, + lineGap, + unitsPerEm, + xWidthAvg, +}: Pick) { + return { + ascent, + descent, + lineGap, + unitsPerEm, + xWidthAvg, + }; +} + +// Source: https://github.com/unjs/fontaine/blob/f00f84032c5d5da72c8798eae4cd68d3ddfbf340/src/css.ts#L7 +function toPercentage(value: number, fractionDigits = 4) { + const percentage = value * 100; + return `${+percentage.toFixed(fractionDigits)}%`; +} + +export function createCapsizeFontMetricsResolver({ + fontFetcher, + cssRenderer, +}: { + fontFetcher: FontFetcher; + cssRenderer: CssRenderer; +}): FontMetricsResolver { + const cache: Record = {}; + + return { + async getMetrics(name, input) { + cache[name] ??= filterRequiredMetrics(await fromBuffer(await fontFetcher.fetch(input))); + return cache[name]; + }, + // Source: https://github.com/unjs/fontaine/blob/f00f84032c5d5da72c8798eae4cd68d3ddfbf340/src/css.ts#L170 + generateFontFace({ + metrics, + fallbackMetrics, + name: fallbackName, + font: fallbackFontName, + properties, + }) { + // Calculate size adjust + const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm; + const fallbackFontXAvgRatio = fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm; + const sizeAdjust = preferredFontXAvgRatio / fallbackFontXAvgRatio; + + const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust; + + // Calculate metric overrides for preferred font + const ascentOverride = metrics.ascent / adjustedEmSquare; + const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare; + const lineGapOverride = metrics.lineGap / adjustedEmSquare; + + return cssRenderer.generateFontFace(fallbackName, { + ...properties, + src: renderFontSrc([{ name: fallbackFontName }]), + 'size-adjust': toPercentage(sizeAdjust), + 'ascent-override': toPercentage(ascentOverride), + 'descent-override': toPercentage(descentOverride), + 'line-gap-override': toPercentage(lineGapOverride), + }); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/font-type-extractor.ts b/packages/astro/src/assets/fonts/implementations/font-type-extractor.ts new file mode 100644 index 000000000000..b61626bb7e4c --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/font-type-extractor.ts @@ -0,0 +1,21 @@ +import { extname } from 'node:path'; +import type { ErrorHandler, FontTypeExtractor } from '../definitions.js'; +import { isFontType } from '../utils.js'; + +export function createFontTypeExtractor({ + errorHandler, +}: { errorHandler: ErrorHandler }): FontTypeExtractor { + return { + extract(url) { + const extension = extname(url).slice(1); + if (!isFontType(extension)) { + throw errorHandler.handle({ + type: 'cannot-extract-font-type', + data: { url }, + cause: `Unexpected extension, got "${extension}"`, + }); + } + return extension; + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/hasher.ts b/packages/astro/src/assets/fonts/implementations/hasher.ts new file mode 100644 index 000000000000..2772284c4f79 --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/hasher.ts @@ -0,0 +1,13 @@ +import xxhash from 'xxhash-wasm'; +import type { Hasher } from '../definitions.js'; +import { sortObjectByKey } from '../utils.js'; + +export async function createXxHasher(): Promise { + const { h64ToString: hashString } = await xxhash(); + return { + hashString, + hashObject(input) { + return hashString(JSON.stringify(sortObjectByKey(input))); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/local-provider-url-resolver.ts b/packages/astro/src/assets/fonts/implementations/local-provider-url-resolver.ts new file mode 100644 index 000000000000..13f8c65acb7d --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/local-provider-url-resolver.ts @@ -0,0 +1,22 @@ +import { fileURLToPath } from 'node:url'; +import type { LocalProviderUrlResolver } from '../definitions.js'; +import { resolveEntrypoint } from '../utils.js'; + +export function createRequireLocalProviderUrlResolver({ + root, + intercept, +}: { + root: URL; + // TODO: remove when stabilizing + intercept?: (path: string) => void; +}): LocalProviderUrlResolver { + return { + resolve(input) { + // fileURLToPath is important so that the file can be read + // by createLocalUrlProxyContentResolver + const path = fileURLToPath(resolveEntrypoint(root, input)); + intercept?.(path); + return path; + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/remote-font-provider-mod-resolver.ts b/packages/astro/src/assets/fonts/implementations/remote-font-provider-mod-resolver.ts new file mode 100644 index 000000000000..7dc3df1e7fdd --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/remote-font-provider-mod-resolver.ts @@ -0,0 +1,20 @@ +import type { ViteDevServer } from 'vite'; +import type { RemoteFontProviderModResolver } from '../definitions.js'; + +export function createBuildRemoteFontProviderModResolver(): RemoteFontProviderModResolver { + return { + resolve(id) { + return import(id); + }, + }; +} + +export function createDevServerRemoteFontProviderModResolver({ + server, +}: { server: ViteDevServer }): RemoteFontProviderModResolver { + return { + resolve(id) { + return server.ssrLoadModule(id); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/remote-font-provider-resolver.ts b/packages/astro/src/assets/fonts/implementations/remote-font-provider-resolver.ts new file mode 100644 index 000000000000..f70a99fbc910 --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/remote-font-provider-resolver.ts @@ -0,0 +1,62 @@ +import type { + ErrorHandler, + RemoteFontProviderModResolver, + RemoteFontProviderResolver, +} from '../definitions.js'; +import type { ResolvedFontProvider } from '../types.js'; +import { resolveEntrypoint } from '../utils.js'; + +function validateMod({ + mod, + entrypoint, + errorHandler, +}: { mod: any; entrypoint: string; errorHandler: ErrorHandler }): Pick< + ResolvedFontProvider, + 'provider' +> { + // We do not throw astro errors directly to avoid duplication. Instead, we throw an error to be used as cause + try { + if (typeof mod !== 'object' || mod === null) { + throw new Error(`Expected an object for the module, but received ${typeof mod}.`); + } + + if (typeof mod.provider !== 'function') { + throw new Error(`Invalid provider export in module, expected a function.`); + } + + return { + provider: mod.provider, + }; + } catch (cause) { + throw errorHandler.handle({ + type: 'cannot-load-font-provider', + data: { + entrypoint, + }, + cause, + }); + } +} + +export function createRemoteFontProviderResolver({ + root, + modResolver, + errorHandler, +}: { + root: URL; + modResolver: RemoteFontProviderModResolver; + errorHandler: ErrorHandler; +}): RemoteFontProviderResolver { + return { + async resolve({ entrypoint, config }) { + const id = resolveEntrypoint(root, entrypoint.toString()).href; + const mod = await modResolver.resolve(id); + const { provider } = validateMod({ + mod, + entrypoint: id, + errorHandler, + }); + return { config, provider }; + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/storage.ts b/packages/astro/src/assets/fonts/implementations/storage.ts new file mode 100644 index 000000000000..ad0f6ae8cd6c --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/storage.ts @@ -0,0 +1,12 @@ +import { fileURLToPath } from 'node:url'; +import { type Storage, createStorage } from 'unstorage'; +import fsLiteDriver from 'unstorage/drivers/fs-lite'; + +export function createFsStorage({ base }: { base: URL }): Storage { + return createStorage({ + // Types are weirly exported + driver: (fsLiteDriver as unknown as typeof fsLiteDriver.default)({ + base: fileURLToPath(base), + }), + }); +} diff --git a/packages/astro/src/assets/fonts/implementations/system-fallbacks-provider.ts b/packages/astro/src/assets/fonts/implementations/system-fallbacks-provider.ts new file mode 100644 index 000000000000..3e2340c78bd7 --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/system-fallbacks-provider.ts @@ -0,0 +1,79 @@ +import type { SystemFallbacksProvider } from '../definitions.js'; +import type { FontFaceMetrics, GenericFallbackName } from '../types.js'; + +// Extracted from https://raw.githubusercontent.com/seek-oss/capsize/refs/heads/master/packages/metrics/src/entireMetricsCollection.json +const SYSTEM_METRICS = { + 'Times New Roman': { + ascent: 1825, + descent: -443, + lineGap: 87, + unitsPerEm: 2048, + xWidthAvg: 832, + }, + Arial: { + ascent: 1854, + descent: -434, + lineGap: 67, + unitsPerEm: 2048, + xWidthAvg: 913, + }, + 'Courier New': { + ascent: 1705, + descent: -615, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 1229, + }, + BlinkMacSystemFont: { + ascent: 1980, + descent: -432, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 853, + }, + 'Segoe UI': { + ascent: 2210, + descent: -514, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 908, + }, + Roboto: { + ascent: 1900, + descent: -500, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 911, + }, + 'Helvetica Neue': { + ascent: 952, + descent: -213, + lineGap: 28, + unitsPerEm: 1000, + xWidthAvg: 450, + }, +} satisfies Record; + +type FallbackName = keyof typeof SYSTEM_METRICS; + +// Source: https://github.com/nuxt/fonts/blob/3a3eb6dfecc472242b3011b25f3fcbae237d0acc/src/module.ts#L55-L75 +export const DEFAULT_FALLBACKS = { + serif: ['Times New Roman'], + 'sans-serif': ['Arial'], + monospace: ['Courier New'], + 'system-ui': ['BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial'], + 'ui-serif': ['Times New Roman'], + 'ui-sans-serif': ['Arial'], + 'ui-monospace': ['Courier New'], +} satisfies Partial>>; + +export function createSystemFallbacksProvider(): SystemFallbacksProvider { + return { + getLocalFonts(fallback) { + return DEFAULT_FALLBACKS[fallback as keyof typeof DEFAULT_FALLBACKS] ?? null; + }, + getMetricsForLocalFont(family) { + return SYSTEM_METRICS[family as FallbackName]; + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/url-proxy-content-resolver.ts b/packages/astro/src/assets/fonts/implementations/url-proxy-content-resolver.ts new file mode 100644 index 000000000000..2a0aa1d75988 --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/url-proxy-content-resolver.ts @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import type { ErrorHandler, UrlProxyContentResolver } from '../definitions.js'; + +export function createLocalUrlProxyContentResolver({ + errorHandler, +}: { errorHandler: ErrorHandler }): UrlProxyContentResolver { + return { + resolve(url) { + try { + // We use the url and the file content for the hash generation because: + // - The URL is not hashed unlike remote providers + // - A font file can renamed and swapped so we would incorrectly cache it + return url + readFileSync(url, 'utf-8'); + } catch (cause) { + throw errorHandler.handle({ + type: 'unknown-fs-error', + data: {}, + cause, + }); + } + }, + }; +} + +export function createRemoteUrlProxyContentResolver(): UrlProxyContentResolver { + return { + // Passthrough, the remote provider URL is enough + resolve: (url) => url, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/url-proxy.ts b/packages/astro/src/assets/fonts/implementations/url-proxy.ts new file mode 100644 index 000000000000..d41152c6b18a --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/url-proxy.ts @@ -0,0 +1,36 @@ +import type { + DataCollector, + Hasher, + UrlProxy, + UrlProxyContentResolver, + UrlResolver, +} from '../definitions.js'; + +export function createUrlProxy({ + contentResolver, + hasher, + dataCollector, + urlResolver, +}: { + contentResolver: UrlProxyContentResolver; + hasher: Hasher; + dataCollector: DataCollector; + urlResolver: UrlResolver; +}): UrlProxy { + return { + proxy({ url: originalUrl, type, data, collectPreload, init }) { + const hash = `${hasher.hashString(contentResolver.resolve(originalUrl))}.${type}`; + const url = urlResolver.resolve(hash); + + dataCollector.collect({ + url: originalUrl, + hash, + preload: collectPreload ? { url, type } : null, + data, + init, + }); + + return url; + }, + }; +} diff --git a/packages/astro/src/assets/fonts/implementations/url-resolver.ts b/packages/astro/src/assets/fonts/implementations/url-resolver.ts new file mode 100644 index 000000000000..d5290f348fbe --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/url-resolver.ts @@ -0,0 +1,27 @@ +import { fileExtension, joinPaths, prependForwardSlash } from '../../../core/path.js'; +import type { AssetsPrefix } from '../../../types/public/index.js'; +import { getAssetsPrefix } from '../../utils/getAssetsPrefix.js'; +import type { UrlResolver } from '../definitions.js'; + +export function createDevUrlResolver({ base }: { base: string }): UrlResolver { + return { + resolve(hash) { + return prependForwardSlash(joinPaths(base, hash)); + }, + }; +} + +export function createBuildUrlResolver({ + base, + assetsPrefix, +}: { base: string; assetsPrefix: AssetsPrefix }): UrlResolver { + return { + resolve(hash) { + const prefix = assetsPrefix ? getAssetsPrefix(fileExtension(hash), assetsPrefix) : undefined; + if (prefix) { + return joinPaths(prefix, base, hash); + } + return prependForwardSlash(joinPaths(base, hash)); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/load.ts b/packages/astro/src/assets/fonts/load.ts deleted file mode 100644 index ce228ec6dd1a..000000000000 --- a/packages/astro/src/assets/fonts/load.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { readFileSync } from 'node:fs'; -import * as unifont from 'unifont'; -import type { Storage } from 'unstorage'; -import { AstroError, AstroErrorData } from '../../core/errors/index.js'; -import { DEFAULTS, LOCAL_PROVIDER_NAME } from './constants.js'; -import type { generateFallbackFontFace } from './metrics.js'; -import { resolveLocalFont } from './providers/local.js'; -import type { PreloadData, ResolvedFontFamily } from './types.js'; -import { - type GetMetricsForFamily, - type GetMetricsForFamilyFont, - type ProxyURLOptions, - familiesToUnifontProviders, - generateFallbacksCSS, - generateFontFace, - proxyURL, -} from './utils.js'; - -interface Options { - base: string; - families: Array; - storage: Storage; - hashToUrlMap: Map; - resolvedMap: Map; - hashString: (value: string) => string; - log: (message: string) => void; - generateFallbackFontFace: typeof generateFallbackFontFace; - getMetricsForFamily: GetMetricsForFamily; -} - -export async function loadFonts({ - base, - families, - storage, - hashToUrlMap, - resolvedMap, - hashString, - generateFallbackFontFace, - getMetricsForFamily, - log, -}: Options): Promise { - const extractedProvidersResult = familiesToUnifontProviders({ families, hashString }); - families = extractedProvidersResult.families; - const { resolveFont } = await unifont.createUnifont(extractedProvidersResult.providers, { - storage, - }); - - for (const family of families) { - const preloadData: PreloadData = []; - let css = ''; - let fallbackFontData: GetMetricsForFamilyFont = null; - - // When going through the urls/filepaths returned by providers, - // We save the hash and the associated original value so we can use - // it in the vite middleware during development - const collect: ProxyURLOptions['collect'] = ({ hash, type, value }) => { - const url = base + hash; - if (!hashToUrlMap.has(hash)) { - hashToUrlMap.set(hash, value); - preloadData.push({ url, type }); - } - // If a family has fallbacks, we store the first url we get that may - // be used for the fallback generation, if capsize doesn't have this - // family in its built-in collection - if (family.fallbacks && family.fallbacks.length > 0) { - fallbackFontData ??= { - hash, - url: value, - }; - } - return url; - }; - - let fonts: Array; - - if (family.provider === LOCAL_PROVIDER_NAME) { - const result = resolveLocalFont({ - family, - proxyURL: (value) => { - return proxyURL({ - value, - // We hash based on the filepath and the contents, since the user could replace - // a given font file with completely different contents. - hashString: (v) => { - let content: string; - try { - content = readFileSync(value, 'utf-8'); - } catch (e) { - throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: e }); - } - return hashString(v + content); - }, - collect, - }); - }, - }); - fonts = result.fonts; - } else { - const result = await resolveFont( - family.name, - // We do not merge the defaults, we only provide defaults as a fallback - { - weights: family.weights ?? DEFAULTS.weights, - styles: family.styles ?? DEFAULTS.styles, - subsets: family.subsets ?? DEFAULTS.subsets, - fallbacks: family.fallbacks ?? DEFAULTS.fallbacks, - }, - // By default, unifont goes through all providers. We use a different approach - // where we specify a provider per font. - // Name has been set while extracting unifont providers from families (inside familiesToUnifontProviders) - [family.provider.name!], - ); - - fonts = result.fonts.map((font) => ({ - ...font, - src: font.src.map((source) => - 'name' in source - ? source - : { - ...source, - originalURL: source.url, - url: proxyURL({ - value: source.url, - // We only use the url for hashing since the service returns urls with a hash already - hashString, - collect, - }), - }, - ), - })); - } - - for (const data of fonts) { - // User settings override the generated font settings - css += generateFontFace(family.nameWithHash, { - src: data.src, - display: - (data.display ?? family.provider === LOCAL_PROVIDER_NAME) ? undefined : family.display, - unicodeRange: - (data.unicodeRange ?? family.provider === LOCAL_PROVIDER_NAME) - ? undefined - : family.unicodeRange, - weight: data.weight, - style: data.style, - stretch: - (data.stretch ?? family.provider === LOCAL_PROVIDER_NAME) ? undefined : family.stretch, - featureSettings: - (data.featureSettings ?? family.provider === LOCAL_PROVIDER_NAME) - ? undefined - : family.featureSettings, - variationSettings: - (data.variationSettings ?? family.provider === LOCAL_PROVIDER_NAME) - ? undefined - : family.variationSettings, - }); - } - - const fallbackData = await generateFallbacksCSS({ - family, - font: fallbackFontData, - fallbacks: family.fallbacks ?? DEFAULTS.fallbacks, - metrics: - (family.optimizedFallbacks ?? DEFAULTS.optimizedFallbacks) - ? { - getMetricsForFamily, - generateFontFace: generateFallbackFontFace, - } - : null, - }); - - const cssVarValues = [family.nameWithHash]; - - if (fallbackData) { - css += fallbackData.css; - cssVarValues.push(...fallbackData.fallbacks); - } - - css += `:root { ${family.cssVariable}: ${cssVarValues.join(', ')}; }`; - - resolvedMap.set(family.cssVariable, { preloadData, css }); - } - log('Fonts initialized'); -} diff --git a/packages/astro/src/assets/fonts/logic/extract-unifont-providers.ts b/packages/astro/src/assets/fonts/logic/extract-unifont-providers.ts new file mode 100644 index 000000000000..c6bdbac6dda4 --- /dev/null +++ b/packages/astro/src/assets/fonts/logic/extract-unifont-providers.ts @@ -0,0 +1,46 @@ +import type * as unifont from 'unifont'; +import { LOCAL_PROVIDER_NAME } from '../constants.js'; +import type { Hasher } from '../definitions.js'; +import type { ResolvedFontFamily } from '../types.js'; + +export function extractUnifontProviders({ + families, + hasher, +}: { + families: Array; + hasher: Hasher; +}): { + families: Array; + providers: Array; +} { + const hashes = new Set(); + const providers: Array = []; + + for (const { provider } of families) { + // The local provider logic happens outside of unifont + if (provider === LOCAL_PROVIDER_NAME) { + continue; + } + + const unifontProvider = provider.provider(provider.config); + const hash = hasher.hashObject({ + name: unifontProvider._name, + ...provider.config, + }); + // Makes sure every font uses the right instance of a given provider + // if this provider is provided several times with different options + // We have to mutate the unifont provider name because unifont deduplicates + // based on the name. + unifontProvider._name += `-${hash}`; + // We set the provider name so we can tell unifont what provider to use when + // resolving font faces + provider.name = unifontProvider._name; + + if (!hashes.has(hash)) { + hashes.add(hash); + providers.push(unifontProvider); + } + } + + return { families, providers }; +} diff --git a/packages/astro/src/assets/fonts/logic/normalize-remote-font-faces.ts b/packages/astro/src/assets/fonts/logic/normalize-remote-font-faces.ts new file mode 100644 index 000000000000..efcdb61a4404 --- /dev/null +++ b/packages/astro/src/assets/fonts/logic/normalize-remote-font-faces.ts @@ -0,0 +1,56 @@ +import type * as unifont from 'unifont'; +import { FONT_FORMATS } from '../constants.js'; +import type { FontTypeExtractor, UrlProxy } from '../definitions.js'; + +export function normalizeRemoteFontFaces({ + fonts, + urlProxy, + fontTypeExtractor, +}: { + fonts: Array; + urlProxy: UrlProxy; + fontTypeExtractor: FontTypeExtractor; +}): Array { + return ( + fonts + // Avoid getting too much font files + .filter((font) => (typeof font.meta?.priority === 'number' ? font.meta.priority === 0 : true)) + // Collect URLs + .map((font) => { + // The index keeps track of encountered URLs. We can't use the index on font.src.map + // below because it may contain sources without urls, which would prevent preloading completely + let index = 0; + return { + ...font, + src: font.src.map((source) => { + if ('name' in source) { + return source; + } + // We handle protocol relative URLs here, otherwise they're considered absolute by the font + // fetcher which will try to read them from the file system + const url = source.url.startsWith('//') ? `https:${source.url}` : source.url; + const proxied = { + ...source, + originalURL: url, + url: urlProxy.proxy({ + url, + type: + FONT_FORMATS.find((e) => e.format === source.format)?.type ?? + fontTypeExtractor.extract(source.url), + // We only collect the first URL to avoid preloading fallback sources (eg. we only + // preload woff2 if woff is available) + collectPreload: index === 0, + data: { + weight: font.weight, + style: font.style, + }, + init: font.meta?.init ?? null, + }), + }; + index++; + return proxied; + }), + }; + }) + ); +} diff --git a/packages/astro/src/assets/fonts/logic/optimize-fallbacks.ts b/packages/astro/src/assets/fonts/logic/optimize-fallbacks.ts new file mode 100644 index 000000000000..973534b09761 --- /dev/null +++ b/packages/astro/src/assets/fonts/logic/optimize-fallbacks.ts @@ -0,0 +1,78 @@ +import type * as unifont from 'unifont'; +import type { FontMetricsResolver, SystemFallbacksProvider } from '../definitions.js'; +import type { FontFileData, ResolvedFontFamily } from '../types.js'; +import { isGenericFontFamily, unifontFontFaceDataToProperties } from '../utils.js'; + +export interface CollectedFontForMetrics extends FontFileData { + data: Partial; +} + +export async function optimizeFallbacks({ + family, + fallbacks: _fallbacks, + collectedFonts, + enabled, + systemFallbacksProvider, + fontMetricsResolver, +}: { + family: Pick; + fallbacks: Array; + collectedFonts: Array; + enabled: boolean; + systemFallbacksProvider: SystemFallbacksProvider; + fontMetricsResolver: FontMetricsResolver; +}): Promise; +}> { + // We avoid mutating the original array + let fallbacks = [..._fallbacks]; + + if (fallbacks.length === 0 || !enabled || collectedFonts.length === 0) { + return null; + } + + // The last element of the fallbacks is usually a generic family name (eg. serif) + const lastFallback = fallbacks[fallbacks.length - 1]; + // If it's not a generic family name, we can't infer local fonts to be used as fallbacks + if (!isGenericFontFamily(lastFallback)) { + return null; + } + + // If it's a generic family name, we get the associated local fonts (eg. Arial) + const localFonts = systemFallbacksProvider.getLocalFonts(lastFallback); + // Some generic families do not have associated local fonts so we abort early + if (!localFonts || localFonts.length === 0) { + return null; + } + + // If the family is already a system font, no need to generate fallbacks + if (localFonts.includes(family.name)) { + return null; + } + + const localFontsMappings = localFonts.map((font) => ({ + font, + // We must't wrap in quote because that's handled by the CSS renderer + name: `${family.nameWithHash} fallback: ${font}`, + })); + + // We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided + fallbacks = [...localFontsMappings.map((m) => m.name), ...fallbacks]; + let css = ''; + + for (const { font, name } of localFontsMappings) { + for (const collected of collectedFonts) { + // We generate a fallback for each font collected, which is per weight and style + css += fontMetricsResolver.generateFontFace({ + metrics: await fontMetricsResolver.getMetrics(family.name, collected), + fallbackMetrics: systemFallbacksProvider.getMetricsForLocalFont(font), + font, + name, + properties: unifontFontFaceDataToProperties(collected.data), + }); + } + } + + return { css, fallbacks }; +} diff --git a/packages/astro/src/assets/fonts/logic/resolve-families.ts b/packages/astro/src/assets/fonts/logic/resolve-families.ts new file mode 100644 index 000000000000..17fd48799df4 --- /dev/null +++ b/packages/astro/src/assets/fonts/logic/resolve-families.ts @@ -0,0 +1,99 @@ +import { LOCAL_PROVIDER_NAME } from '../constants.js'; +import type { + Hasher, + LocalProviderUrlResolver, + RemoteFontProviderResolver, +} from '../definitions.js'; +import type { + FontFamily, + LocalFontFamily, + ResolvedFontFamily, + ResolvedLocalFontFamily, +} from '../types.js'; +import { dedupe, withoutQuotes } from '../utils.js'; + +function resolveVariants({ + variants, + localProviderUrlResolver, +}: { + variants: LocalFontFamily['variants']; + localProviderUrlResolver: LocalProviderUrlResolver; +}): ResolvedLocalFontFamily['variants'] { + return variants.map((variant) => ({ + ...variant, + weight: variant.weight?.toString(), + src: variant.src.map((value) => { + // A src can be a string or an object, we extract the value accordingly. + const isValue = typeof value === 'string' || value instanceof URL; + const url = (isValue ? value : value.url).toString(); + const tech = isValue ? undefined : value.tech; + return { + url: localProviderUrlResolver.resolve(url), + tech, + }; + }), + })); +} + +/** + * Dedupes properties if applicable and resolves entrypoints. + */ +export async function resolveFamily({ + family, + hasher, + remoteFontProviderResolver, + localProviderUrlResolver, +}: { + family: FontFamily; + hasher: Hasher; + remoteFontProviderResolver: RemoteFontProviderResolver; + localProviderUrlResolver: LocalProviderUrlResolver; +}): Promise { + // We remove quotes from the name so they can be properly resolved by providers. + const name = withoutQuotes(family.name); + // This will be used in CSS font faces. Quotes are added by the CSS renderer if + // this value contains a space. + const nameWithHash = `${name}-${hasher.hashObject(family)}`; + + if (family.provider === LOCAL_PROVIDER_NAME) { + return { + ...family, + name, + nameWithHash, + variants: resolveVariants({ variants: family.variants, localProviderUrlResolver }), + fallbacks: family.fallbacks ? dedupe(family.fallbacks) : undefined, + }; + } + + return { + ...family, + name, + nameWithHash, + weights: family.weights ? dedupe(family.weights.map((weight) => weight.toString())) : undefined, + styles: family.styles ? dedupe(family.styles) : undefined, + subsets: family.subsets ? dedupe(family.subsets) : undefined, + fallbacks: family.fallbacks ? dedupe(family.fallbacks) : undefined, + unicodeRange: family.unicodeRange ? dedupe(family.unicodeRange) : undefined, + // This will be Astro specific eventually + provider: await remoteFontProviderResolver.resolve(family.provider), + }; +} + +/** + * A function for convenience. The actual logic lives in resolveFamily + */ +export async function resolveFamilies({ + families, + ...dependencies +}: { families: Array } & Omit[0], 'family'>): Promise< + Array +> { + return await Promise.all( + families.map((family) => + resolveFamily({ + family, + ...dependencies, + }), + ), + ); +} diff --git a/packages/astro/src/assets/fonts/metrics.ts b/packages/astro/src/assets/fonts/metrics.ts deleted file mode 100644 index d7b8b5229319..000000000000 --- a/packages/astro/src/assets/fonts/metrics.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Adapted from https://github.com/unjs/fontaine/ -import { fontFamilyToCamelCase } from '@capsizecss/metrics'; -import { type Font, fromBuffer } from '@capsizecss/unpack'; - -const QUOTES_RE = /^["']|["']$/g; - -const withoutQuotes = (str: string) => str.trim().replace(QUOTES_RE, ''); - -export type FontFaceMetrics = Pick< - Font, - 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg' ->; - -const metricCache: Record = {}; - -function filterRequiredMetrics({ - ascent, - descent, - lineGap, - unitsPerEm, - xWidthAvg, -}: Pick) { - return { - ascent, - descent, - lineGap, - unitsPerEm, - xWidthAvg, - }; -} - -export async function getMetricsForFamily(family: string) { - family = withoutQuotes(family); - - if (family in metricCache) return metricCache[family]; - - try { - const name = fontFamilyToCamelCase(family); - const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection'); - const metrics = entireMetricsCollection[name as keyof typeof entireMetricsCollection]; - - if (!('descent' in metrics)) { - metricCache[family] = null; - return null; - } - - const filteredMetrics = filterRequiredMetrics(metrics); - metricCache[family] = filteredMetrics; - return filteredMetrics; - } catch { - metricCache[family] = null; - return null; - } -} - -export async function readMetrics(family: string, buffer: Buffer) { - const metrics = await fromBuffer(buffer); - - metricCache[family] = filterRequiredMetrics(metrics); - - return metricCache[family]; -} - -// See: https://github.com/seek-oss/capsize/blob/master/packages/core/src/round.ts -function toPercentage(value: number, fractionDigits = 4) { - const percentage = value * 100; - return `${+percentage.toFixed(fractionDigits)}%`; -} - -function toCSS(properties: Record, indent = 2) { - return Object.entries(properties) - .map(([key, value]) => `${' '.repeat(indent)}${key}: ${value};`) - .join('\n'); -} - -export function generateFallbackFontFace( - metrics: FontFaceMetrics, - fallback: { - name: string; - font: string; - metrics?: FontFaceMetrics; - [key: string]: any; - }, -) { - const { - name: fallbackName, - font: fallbackFontName, - metrics: fallbackMetrics, - ...properties - } = fallback; - - // Credits to: https://github.com/seek-oss/capsize/blob/master/packages/core/src/createFontStack.ts - - // Calculate size adjust - const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm; - const fallbackFontXAvgRatio = fallbackMetrics - ? fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm - : 1; - - const sizeAdjust = - fallbackMetrics && preferredFontXAvgRatio && fallbackFontXAvgRatio - ? preferredFontXAvgRatio / fallbackFontXAvgRatio - : 1; - - const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust; - - // Calculate metric overrides for preferred font - const ascentOverride = metrics.ascent / adjustedEmSquare; - const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare; - const lineGapOverride = metrics.lineGap / adjustedEmSquare; - - const declaration = { - 'font-family': JSON.stringify(fallbackName), - src: `local(${JSON.stringify(fallbackFontName)})`, - 'size-adjust': toPercentage(sizeAdjust), - 'ascent-override': toPercentage(ascentOverride), - 'descent-override': toPercentage(descentOverride), - 'line-gap-override': toPercentage(lineGapOverride), - ...properties, - }; - - return `@font-face {\n${toCSS(declaration)}\n}\n`; -} diff --git a/packages/astro/src/assets/fonts/orchestrate.ts b/packages/astro/src/assets/fonts/orchestrate.ts new file mode 100644 index 000000000000..5f799dc4ef03 --- /dev/null +++ b/packages/astro/src/assets/fonts/orchestrate.ts @@ -0,0 +1,227 @@ +import { bold } from 'kleur/colors'; +import * as unifont from 'unifont'; +import type { Storage } from 'unstorage'; +import type { Logger } from '../../core/logger/core.js'; +import { LOCAL_PROVIDER_NAME } from './constants.js'; +import type { + CssRenderer, + FontFileReader, + FontMetricsResolver, + FontTypeExtractor, + Hasher, + LocalProviderUrlResolver, + RemoteFontProviderResolver, + SystemFallbacksProvider, + UrlProxy, +} from './definitions.js'; +import { extractUnifontProviders } from './logic/extract-unifont-providers.js'; +import { normalizeRemoteFontFaces } from './logic/normalize-remote-font-faces.js'; +import { type CollectedFontForMetrics, optimizeFallbacks } from './logic/optimize-fallbacks.js'; +import { resolveFamilies } from './logic/resolve-families.js'; +import { resolveLocalFont } from './providers/local.js'; +import type { + ConsumableMap, + CreateUrlProxyParams, + Defaults, + FontFamily, + FontFileDataMap, + PreloadData, +} from './types.js'; +import { pickFontFaceProperty, unifontFontFaceDataToProperties } from './utils.js'; + +/** + * Manages how fonts are resolved: + * + * - families are resolved + * - unifont providers are extracted from families + * - unifont is initialized + * + * For each family: + * - We create a URL proxy + * - We resolve the font and normalize the result + * + * For each resolved font: + * - We generate the CSS font face + * - We generate optimized fallbacks if applicable + * - We generate CSS variables + * + * Once that's done, the collected data is returned + */ +export async function orchestrate({ + families, + hasher, + remoteFontProviderResolver, + localProviderUrlResolver, + storage, + cssRenderer, + systemFallbacksProvider, + fontMetricsResolver, + fontTypeExtractor, + fontFileReader, + logger, + createUrlProxy, + defaults, +}: { + families: Array; + hasher: Hasher; + remoteFontProviderResolver: RemoteFontProviderResolver; + localProviderUrlResolver: LocalProviderUrlResolver; + storage: Storage; + cssRenderer: CssRenderer; + systemFallbacksProvider: SystemFallbacksProvider; + fontMetricsResolver: FontMetricsResolver; + fontTypeExtractor: FontTypeExtractor; + fontFileReader: FontFileReader; + // TODO: follow this implementation: https://github.com/withastro/astro/pull/13756/commits/e30ac2b7082a3eed36225da6e88449890cbcbe6b + logger: Logger; + createUrlProxy: (params: CreateUrlProxyParams) => UrlProxy; + defaults: Defaults; +}): Promise<{ + fontFileDataMap: FontFileDataMap; + consumableMap: ConsumableMap; +}> { + let resolvedFamilies = await resolveFamilies({ + families, + hasher, + remoteFontProviderResolver, + localProviderUrlResolver, + }); + + const extractedUnifontProvidersResult = extractUnifontProviders({ + families: resolvedFamilies, + hasher, + }); + resolvedFamilies = extractedUnifontProvidersResult.families; + const unifontProviders = extractedUnifontProvidersResult.providers; + + const { resolveFont } = await unifont.createUnifont(unifontProviders, { + storage, + }); + + /** + * Holds associations of hash and original font file URLs, so they can be + * downloaded whenever the hash is requested. + */ + const fontFileDataMap: FontFileDataMap = new Map(); + /** + * Holds associations of CSS variables and preloadData/css to be passed to the virtual module. + */ + const consumableMap: ConsumableMap = new Map(); + + for (const family of resolvedFamilies) { + const preloadData: Array = []; + let css = ''; + + /** + * Holds a list of font files to be used for optimized fallbacks generation + */ + const collectedFonts: Array = []; + const fallbacks = family.fallbacks ?? defaults.fallbacks ?? []; + + /** + * Allows collecting and transforming original URLs from providers, so the Vite + * plugin has control over URLs. + */ + const urlProxy = createUrlProxy({ + local: family.provider === LOCAL_PROVIDER_NAME, + hasUrl: (hash) => fontFileDataMap.has(hash), + saveUrl: ({ hash, url, init }) => { + fontFileDataMap.set(hash, { url, init }); + }, + savePreload: (preload) => { + preloadData.push(preload); + }, + saveFontData: (collected) => { + if ( + fallbacks && + fallbacks.length > 0 && + // If the same data has already been sent for this family, we don't want to have + // duplicated fallbacks. Such scenario can occur with unicode ranges. + !collectedFonts.some((f) => JSON.stringify(f.data) === JSON.stringify(collected.data)) + ) { + // If a family has fallbacks, we store the first url we get that may + // be used for the fallback generation. + collectedFonts.push(collected); + } + }, + }); + + let fonts: Array; + + if (family.provider === LOCAL_PROVIDER_NAME) { + const result = resolveLocalFont({ + family, + urlProxy, + fontTypeExtractor, + fontFileReader, + }); + // URLs are already proxied at this point so no further processing is required + fonts = result.fonts; + } else { + const result = await resolveFont( + family.name, + // We do not merge the defaults, we only provide defaults as a fallback + { + weights: family.weights ?? defaults.weights, + styles: family.styles ?? defaults.styles, + subsets: family.subsets ?? defaults.subsets, + fallbacks: family.fallbacks ?? defaults.fallbacks, + }, + // By default, unifont goes through all providers. We use a different approach where + // we specify a provider per font. Name has been set while extracting unifont providers + // from families (inside extractUnifontProviders). + [family.provider.name!], + ); + if (result.fonts.length === 0) { + logger.warn( + 'assets', + `No data found for font family ${bold(family.name)}. Review your configuration`, + ); + } + // The data returned by the remote provider contains original URLs. We proxy them. + fonts = normalizeRemoteFontFaces({ fonts: result.fonts, urlProxy, fontTypeExtractor }); + } + + for (const data of fonts) { + css += cssRenderer.generateFontFace( + family.nameWithHash, + unifontFontFaceDataToProperties({ + src: data.src, + weight: data.weight, + style: data.style, + // User settings override the generated font settings. We use a helper function + // because local and remote providers store this data in different places. + display: pickFontFaceProperty('display', { data, family }), + unicodeRange: pickFontFaceProperty('unicodeRange', { data, family }), + stretch: pickFontFaceProperty('stretch', { data, family }), + featureSettings: pickFontFaceProperty('featureSettings', { data, family }), + variationSettings: pickFontFaceProperty('variationSettings', { data, family }), + }), + ); + } + + const cssVarValues = [family.nameWithHash]; + const optimizeFallbacksResult = await optimizeFallbacks({ + family, + fallbacks, + collectedFonts, + enabled: family.optimizedFallbacks ?? defaults.optimizedFallbacks ?? false, + systemFallbacksProvider, + fontMetricsResolver, + }); + + if (optimizeFallbacksResult) { + css += optimizeFallbacksResult.css; + cssVarValues.push(...optimizeFallbacksResult.fallbacks); + } else { + // If there are no optimized fallbacks, we pass the provided fallbacks as is. + cssVarValues.push(...fallbacks); + } + + css += cssRenderer.generateCssVariable(family.cssVariable, cssVarValues); + + consumableMap.set(family.cssVariable, { preloadData, css }); + } + + return { fontFileDataMap, consumableMap }; +} diff --git a/packages/astro/src/assets/fonts/providers/index.ts b/packages/astro/src/assets/fonts/providers/index.ts index 3d7dcfdc13df..7c41a4b6d010 100644 --- a/packages/astro/src/assets/fonts/providers/index.ts +++ b/packages/astro/src/assets/fonts/providers/index.ts @@ -30,13 +30,11 @@ function fontsource() { }); } -// TODO: https://github.com/unjs/unifont/issues/108. Once resolved, remove the unifont patch -// This provider downloads too many files when there's a variable font -// available. This is bad because it doesn't align with our default font settings /** [Google](https://fonts.google.com/) */ -function google() { +function google(config?: Parameters[0]) { return defineAstroFontProvider({ entrypoint: 'astro/assets/fonts/providers/google', + config, }); } diff --git a/packages/astro/src/assets/fonts/providers/local.ts b/packages/astro/src/assets/fonts/providers/local.ts index 80f500e75805..63fdf3d65c7c 100644 --- a/packages/astro/src/assets/fonts/providers/local.ts +++ b/packages/astro/src/assets/fonts/providers/local.ts @@ -1,46 +1,70 @@ import type * as unifont from 'unifont'; +import { FONT_FORMATS } from '../constants.js'; +import type { FontFileReader, FontTypeExtractor, UrlProxy } from '../definitions.js'; import type { ResolvedLocalFontFamily } from '../types.js'; -import { extractFontType } from '../utils.js'; - -// https://fonts.nuxt.com/get-started/providers#local -// https://github.com/nuxt/fonts/blob/main/src/providers/local.ts -// https://github.com/unjs/unifont/blob/main/src/providers/google.ts - -type InitializedProvider = NonNullable>>; - -type ResolveFontResult = NonNullable>>; interface Options { family: ResolvedLocalFontFamily; - proxyURL: (value: string) => string; + urlProxy: UrlProxy; + fontTypeExtractor: FontTypeExtractor; + fontFileReader: FontFileReader; } -export function resolveLocalFont({ family, proxyURL }: Options): ResolveFontResult { - const fonts: ResolveFontResult['fonts'] = []; +export function resolveLocalFont({ + family, + urlProxy, + fontTypeExtractor, + fontFileReader, +}: Options): { + fonts: Array; +} { + return { + fonts: family.variants.map((variant) => { + const shouldInfer = variant.weight === undefined || variant.style === undefined; - for (const variant of family.variants) { - const data: ResolveFontResult['fonts'][number] = { - weight: variant.weight, - style: variant.style, - src: variant.src.map(({ url: originalURL, tech }) => { - return { - originalURL, - url: proxyURL(originalURL), - format: extractFontType(originalURL), - tech, - }; - }), - }; - if (variant.display) data.display = variant.display; - if (variant.unicodeRange) data.unicodeRange = variant.unicodeRange; - if (variant.stretch) data.stretch = variant.stretch; - if (variant.featureSettings) data.featureSettings = variant.featureSettings; - if (variant.variationSettings) data.variationSettings = variant.variationSettings; + // We prepare the data + const data: unifont.FontFaceData = { + // If it should be inferred, we don't want to set the value + weight: variant.weight, + style: variant.style, + src: [], + unicodeRange: variant.unicodeRange, + display: variant.display, + stretch: variant.stretch, + featureSettings: variant.featureSettings, + variationSettings: variant.variationSettings, + }; + // We proxy each source + data.src = variant.src.map((source, index) => { + // We only try to infer for the first source. Indeed if it doesn't work, the function + // call will throw an error so that will be interruped anyways + if (shouldInfer && index === 0) { + const result = fontFileReader.extract({ family: family.name, url: source.url }); + if (variant.weight === undefined) data.weight = result.weight; + if (variant.style === undefined) data.style = result.style; + } - fonts.push(data); - } + const type = fontTypeExtractor.extract(source.url); - return { - fonts, + return { + originalURL: source.url, + url: urlProxy.proxy({ + url: source.url, + type, + // We only use the first source for preloading. For example if woff2 and woff + // are available, we only keep woff2. + collectPreload: index === 0, + data: { + weight: data.weight, + style: data.style, + }, + init: null, + }), + format: FONT_FORMATS.find((e) => e.type === type)?.format, + tech: source.tech, + }; + }); + return data; + }), }; } diff --git a/packages/astro/src/assets/fonts/providers/utils.ts b/packages/astro/src/assets/fonts/providers/utils.ts deleted file mode 100644 index 7c0bf958362c..000000000000 --- a/packages/astro/src/assets/fonts/providers/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AstroError, AstroErrorData } from '../../../core/errors/index.js'; -import type { AstroFontProvider, ResolvedFontProvider } from '../types.js'; -import { resolveEntrypoint } from '../utils.js'; - -export function validateMod(mod: any, entrypoint: string): Pick { - // We do not throw astro errors directly to avoid duplication. Instead, we throw an error to be used as cause - try { - if (typeof mod !== 'object' || mod === null) { - throw new Error(`Expected an object for the module, but received ${typeof mod}.`); - } - - if (typeof mod.provider !== 'function') { - throw new Error(`Invalid provider export in module, expected a function.`); - } - - return { - provider: mod.provider, - }; - } catch (cause) { - throw new AstroError( - { - ...AstroErrorData.CannotLoadFontProvider, - message: AstroErrorData.CannotLoadFontProvider.message(entrypoint), - }, - { cause }, - ); - } -} - -export type ResolveMod = (id: string) => Promise; - -export interface ResolveProviderOptions { - root: URL; - provider: AstroFontProvider; - resolveMod: ResolveMod; -} - -export async function resolveProvider({ - root, - provider: { entrypoint, config }, - resolveMod, -}: ResolveProviderOptions): Promise { - const id = resolveEntrypoint(root, entrypoint.toString()).href; - const mod = await resolveMod(id); - const { provider } = validateMod(mod, id); - return { config, provider }; -} diff --git a/packages/astro/src/assets/fonts/sync.ts b/packages/astro/src/assets/fonts/sync.ts index 57ed03377124..a1af4dd701e6 100644 --- a/packages/astro/src/assets/fonts/sync.ts +++ b/packages/astro/src/assets/fonts/sync.ts @@ -1,6 +1,7 @@ import type { AstroSettings } from '../../types/astro.js'; import { FONTS_TYPES_FILE } from './constants.js'; +// TODO: investigate moving to orchestrate export function syncFonts(settings: AstroSettings): void { if (!settings.config.experimental.fonts) { return; diff --git a/packages/astro/src/assets/fonts/types.ts b/packages/astro/src/assets/fonts/types.ts index 1cac9bf753e5..fda2971ce5d8 100644 --- a/packages/astro/src/assets/fonts/types.ts +++ b/packages/astro/src/assets/fonts/types.ts @@ -1,11 +1,14 @@ +import type { Font } from '@capsizecss/unpack'; import type * as unifont from 'unifont'; import type { z } from 'zod'; import type { fontProviderSchema, localFontFamilySchema, remoteFontFamilySchema, + styleSchema, } from './config.js'; -import type { FONT_TYPES } from './constants.js'; +import type { FONT_TYPES, GENERIC_FALLBACK_NAMES } from './constants.js'; +import type { CollectedFontForMetrics } from './logic/optimize-fallbacks.js'; export type AstroFontProvider = z.infer; @@ -26,7 +29,7 @@ export interface ResolvedLocalFontFamily Omit { variants: Array< Omit & { - weight: string; + weight?: string; src: Array<{ url: string; tech?: string }>; } >; @@ -34,6 +37,7 @@ export interface ResolvedLocalFontFamily type RemoteFontFamily = z.infer; +/** @lintignore somehow required by pickFontFaceProperty in utils */ export interface ResolvedRemoteFontFamily extends ResolvedFontFamilyAttributes, Omit, 'provider' | 'weights'> { @@ -49,7 +53,7 @@ export type FontType = (typeof FONT_TYPES)[number]; /** * Preload data is used for links generation inside the component */ -export type PreloadData = Array<{ +export interface PreloadData { /** * Absolute link to a font file, eg. /_astro/fonts/abc.woff */ @@ -58,4 +62,45 @@ export type PreloadData = Array<{ * A font type, eg. woff2, woff, ttf... */ type: FontType; -}>; +} + +export type FontFaceMetrics = Pick< + Font, + 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg' +>; + +export type GenericFallbackName = (typeof GENERIC_FALLBACK_NAMES)[number]; + +export type Defaults = Partial< + Pick< + ResolvedRemoteFontFamily, + 'weights' | 'styles' | 'subsets' | 'fallbacks' | 'optimizedFallbacks' + > +>; + +export interface FontFileData { + hash: string; + url: string; + init: RequestInit | null; +} + +export interface CreateUrlProxyParams { + local: boolean; + hasUrl: (hash: string) => boolean; + saveUrl: (input: FontFileData) => void; + savePreload: (preload: PreloadData) => void; + saveFontData: (collected: CollectedFontForMetrics) => void; +} + +/** + * Holds associations of hash and original font file URLs, so they can be + * downloaded whenever the hash is requested. + */ +export type FontFileDataMap = Map>; + +/** + * Holds associations of CSS variables and preloadData/css to be passed to the virtual module. + */ +export type ConsumableMap = Map; css: string }>; + +export type Style = z.output; diff --git a/packages/astro/src/assets/fonts/utils.ts b/packages/astro/src/assets/fonts/utils.ts index 1803ecc8b2a0..d56feed6dd7a 100644 --- a/packages/astro/src/assets/fonts/utils.ts +++ b/packages/astro/src/assets/fonts/utils.ts @@ -1,73 +1,60 @@ import { createRequire } from 'node:module'; -import { extname } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pathToFileURL } from 'node:url'; import type * as unifont from 'unifont'; import type { Storage } from 'unstorage'; -import { AstroError, AstroErrorData } from '../../core/errors/index.js'; -import { DEFAULT_FALLBACKS, FONT_TYPES, LOCAL_PROVIDER_NAME } from './constants.js'; -import type { FontFaceMetrics, generateFallbackFontFace } from './metrics.js'; -import { type ResolveProviderOptions, resolveProvider } from './providers/utils.js'; -import type { - FontFamily, - FontType, - LocalFontFamily, - ResolvedFontFamily, - ResolvedLocalFontFamily, -} from './types.js'; +import { FONT_TYPES, GENERIC_FALLBACK_NAMES, LOCAL_PROVIDER_NAME } from './constants.js'; +import type { CssProperties } from './definitions.js'; +import type { FontType, GenericFallbackName, ResolvedFontFamily } from './types.js'; -// Source: https://github.com/nuxt/fonts/blob/main/src/css/render.ts#L7-L21 -export function generateFontFace(family: string, font: unifont.FontFaceData) { - return [ - '@font-face {', - ` font-family: ${family};`, - ` src: ${renderFontSrc(font.src)};`, - ` font-display: ${font.display ?? 'swap'};`, - font.unicodeRange && ` unicode-range: ${font.unicodeRange};`, - font.weight && - ` font-weight: ${Array.isArray(font.weight) ? font.weight.join(' ') : font.weight};`, - font.style && ` font-style: ${font.style};`, - font.stretch && ` font-stretch: ${font.stretch};`, - font.featureSettings && ` font-feature-settings: ${font.featureSettings};`, - font.variationSettings && ` font-variation-settings: ${font.variationSettings};`, - `}`, - ] - .filter(Boolean) - .join('\n'); +/** + * Turns unifont font face data into generic CSS properties, to be consumed by the CSS renderer. + */ +export function unifontFontFaceDataToProperties( + font: Partial, +): CssProperties { + return { + src: font.src ? renderFontSrc(font.src) : undefined, + 'font-display': font.display ?? 'swap', + 'unicode-range': font.unicodeRange?.length ? font.unicodeRange.join(',') : undefined, + 'font-weight': Array.isArray(font.weight) ? font.weight.join(' ') : font.weight?.toString(), + 'font-style': font.style, + 'font-stretch': font.stretch, + 'font-feature-settings': font.featureSettings, + 'font-variation-settings': font.variationSettings, + }; } -// Source: https://github.com/nuxt/fonts/blob/main/src/css/render.ts#L68-L81 -function renderFontSrc(sources: Exclude[]) { +/** + * Turns unifont font face data src into a valid CSS property. + * Adapted from https://github.com/nuxt/fonts/blob/main/src/css/render.ts#L68-L81 + */ +export function renderFontSrc( + sources: Exclude[], +): string { return sources .map((src) => { - if ('url' in src) { - let rendered = `url("${src.url}")`; - for (const key of ['format', 'tech'] as const) { - if (key in src) { - rendered += ` ${key}(${src[key]})`; - } - } - return rendered; + if ('name' in src) { + return `local("${src.name}")`; + } + let rendered = `url("${src.url}")`; + if (src.format) { + rendered += ` format("${src.format}")`; + } + if (src.tech) { + rendered += ` tech(${src.tech})`; } - return `local("${src.name}")`; + return rendered; }) .join(', '); } -export function extractFontType(str: string): FontType { - // Extname includes a leading dot - const extension = extname(str).slice(1); - if (!isFontType(extension)) { - throw new AstroError( - { - ...AstroErrorData.CannotExtractFontType, - message: AstroErrorData.CannotExtractFontType.message(str), - }, - { - cause: `Unexpected extension, got "${extension}"`, - }, - ); - } - return extension; +const QUOTES_RE = /^["']|["']$/g; + +/** + * Removes the quotes from a string. Used for family names + */ +export function withoutQuotes(str: string): string { + return str.trim().replace(QUOTES_RE, ''); } export function isFontType(str: string): str is FontType { @@ -78,261 +65,40 @@ export async function cache( storage: Storage, key: string, cb: () => Promise, -): Promise<{ cached: boolean; data: Buffer }> { +): Promise { const existing = await storage.getItemRaw(key); if (existing) { - return { cached: true, data: existing }; + return existing; } const data = await cb(); await storage.setItemRaw(key, data); - return { cached: false, data }; -} - -export interface ProxyURLOptions { - /** - * The original URL - */ - value: string; - /** - * Specifies how the hash is computed. Can be based on the value, - * a specific string for testing etc - */ - hashString: (value: string) => string; - /** - * Use the hook to save the associated value and hash, and possibly - * transform it (eg. apply a base) - */ - collect: (data: { - hash: string; - type: FontType; - value: string; - }) => string; -} - -/** - * The fonts data we receive contains urls or file paths we do no control. - * However, we will emit font files ourselves so we store the original value - * and replace it with a url we control. For example with the value "https://foo.bar/file.woff2": - * - font type is woff2 - * - hash will be ".woff2" - * - `collect` will save the association of the original url and the new hash for later use - * - the returned url will be `/_astro/fonts/.woff2` - */ -export function proxyURL({ value, hashString, collect }: ProxyURLOptions): string { - const type = extractFontType(value); - const hash = `${hashString(value)}.${type}`; - const url = collect({ hash, type, value }); - // Now that we collected the original url, we return our proxy so the consumer can override it - return url; -} - -export function isGenericFontFamily(str: string): str is keyof typeof DEFAULT_FALLBACKS { - return Object.keys(DEFAULT_FALLBACKS).includes(str); + return data; } -export type GetMetricsForFamilyFont = { - hash: string; - url: string; -} | null; - -export type GetMetricsForFamily = ( - name: string, - /** A remote url or local filepath to a font file. Used if metrics can't be resolved purely from the family name */ - font: GetMetricsForFamilyFont, -) => Promise; - -/** - * Generates CSS for a given family fallbacks if possible. - * - * It works by trying to get metrics (using capsize) of the provided font family. - * If some can be computed, they will be applied to the eligible fallbacks to match - * the original font shape as close as possible. - */ -export async function generateFallbacksCSS({ - family, - fallbacks: _fallbacks, - font: fontData, - metrics, -}: { - family: Pick; - /** The family fallbacks */ - fallbacks: Array; - font: GetMetricsForFamilyFont; - metrics: { - getMetricsForFamily: GetMetricsForFamily; - generateFontFace: typeof generateFallbackFontFace; - } | null; -}): Promise }> { - // We avoid mutating the original array - let fallbacks = [..._fallbacks]; - if (fallbacks.length === 0) { - return null; - } - - let css = ''; - - if (!metrics) { - return { css, fallbacks }; - } - - // The last element of the fallbacks is usually a generic family name (eg. serif) - const lastFallback = fallbacks[fallbacks.length - 1]; - // If it's not a generic family name, we can't infer local fonts to be used as fallbacks - if (!isGenericFontFamily(lastFallback)) { - return { css, fallbacks }; - } - - // If it's a generic family name, we get the associated local fonts (eg. Arial) - const localFonts = DEFAULT_FALLBACKS[lastFallback]; - // Some generic families do not have associated local fonts so we abort early - if (localFonts.length === 0) { - return { css, fallbacks }; - } - - const foundMetrics = await metrics.getMetricsForFamily(family.name, fontData); - if (!foundMetrics) { - // If there are no metrics, we can't generate useful fallbacks - return { css, fallbacks }; - } - - const localFontsMappings = localFonts.map((font) => ({ - font, - name: `"${family.nameWithHash} fallback: ${font}"`, - })); - - // We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided - fallbacks = [...new Set([...localFontsMappings.map((m) => m.name), ...fallbacks])]; - - for (const { font, name } of localFontsMappings) { - css += metrics.generateFontFace(foundMetrics, { font, name }); - } - - return { css, fallbacks }; +export function isGenericFontFamily(str: string): str is GenericFallbackName { + return (GENERIC_FALLBACK_NAMES as unknown as Array).includes(str); } -function dedupe>(arr: T): T { +export function dedupe>(arr: T): T { return [...new Set(arr)] as T; } -function resolveVariants({ - variants, - root, -}: { variants: LocalFontFamily['variants']; root: URL }): ResolvedLocalFontFamily['variants'] { - return variants.map((variant) => ({ - ...variant, - weight: variant.weight.toString(), - src: variant.src.map((value) => { - const isValue = typeof value === 'string' || value instanceof URL; - const url = (isValue ? value : value.url).toString(); - const tech = isValue ? undefined : value.tech; - return { - url: fileURLToPath(resolveEntrypoint(root, url)), - tech, - }; - }), - })); -} - -/** - * Resolves the font family provider. If none is provided, it will infer the provider as - * one of the built-in providers and resolve it. The most important part is that if a - * provider is not provided but `src` is, then it's inferred as the local provider. - */ -export async function resolveFontFamily({ - family, - generateNameWithHash, - root, - resolveMod, -}: Omit & { - family: FontFamily; - generateNameWithHash: (family: FontFamily) => string; -}): Promise { - const nameWithHash = generateNameWithHash(family); - - if (family.provider === LOCAL_PROVIDER_NAME) { - return { - ...family, - nameWithHash, - variants: resolveVariants({ variants: family.variants, root }), - fallbacks: family.fallbacks ? dedupe(family.fallbacks) : undefined, - }; - } - - return { - ...family, - nameWithHash, - provider: await resolveProvider({ - root, - resolveMod, - provider: family.provider, - }), - weights: family.weights ? dedupe(family.weights.map((weight) => weight.toString())) : undefined, - styles: family.styles ? dedupe(family.styles) : undefined, - subsets: family.subsets ? dedupe(family.subsets) : undefined, - fallbacks: family.fallbacks ? dedupe(family.fallbacks) : undefined, - unicodeRange: family.unicodeRange ? dedupe(family.unicodeRange) : undefined, - }; -} - export function sortObjectByKey>(unordered: T): T { const ordered = Object.keys(unordered) .sort() .reduce((obj, key) => { + const value = unordered[key]; // @ts-expect-error Type 'T' is generic and can only be indexed for reading. That's fine here - obj[key] = unordered[key]; + obj[key] = Array.isArray(value) + ? value.map((v) => (typeof v === 'object' && v !== null ? sortObjectByKey(v) : v)) + : typeof value === 'object' && value !== null + ? sortObjectByKey(value) + : value; return obj; }, {} as T); return ordered; } -/** - * Extracts providers from families so they can be consumed by unifont. - * It deduplicates them based on their config and provider name: - * - If several families use the same provider (by value, not by reference), we only use one provider - * - If one provider is used with different settings for 2 families, we make sure there are kept as 2 providers - */ -export function familiesToUnifontProviders({ - families, - hashString, -}: { - families: Array; - hashString: (value: string) => string; -}): { families: Array; providers: Array } { - const hashes = new Set(); - const providers: Array = []; - - for (const { provider } of families) { - if (provider === LOCAL_PROVIDER_NAME) { - continue; - } - - const unifontProvider = provider.provider(provider.config); - const hash = hashString( - JSON.stringify( - sortObjectByKey({ - name: unifontProvider._name, - ...provider.config, - }), - ), - ); - if (hashes.has(hash)) { - continue; - } - // Makes sure every font uses the right instance of a given provider - // if this provider is provided several times with different options - // We have to mutate the unifont provider name because unifont deduplicates - // based on the name. - unifontProvider._name += `-${hash}`; - // We set the provider name so we can tell unifont what provider to use when - // resolving font faces - provider.name = unifontProvider._name; - hashes.add(hash); - providers.push(unifontProvider); - } - - return { families, providers }; -} - export function resolveEntrypoint(root: URL, entrypoint: string): URL { const require = createRequire(root); @@ -342,3 +108,12 @@ export function resolveEntrypoint(root: URL, entrypoint: string): URL { return new URL(entrypoint, root); } } + +export function pickFontFaceProperty< + T extends keyof Pick< + unifont.FontFaceData, + 'display' | 'unicodeRange' | 'stretch' | 'featureSettings' | 'variationSettings' + >, +>(property: T, { data, family }: { data: unifont.FontFaceData; family: ResolvedFontFamily }) { + return data[property] ?? (family.provider === LOCAL_PROVIDER_NAME ? undefined : family[property]); +} diff --git a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts index 7258255b13f9..cdfe4a05585b 100644 --- a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts +++ b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts @@ -2,28 +2,52 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { removeTrailingForwardSlash } from '@astrojs/internal-helpers/path'; -import { type Storage, createStorage } from 'unstorage'; -import fsLiteDriver from 'unstorage/drivers/fs-lite'; import type { Plugin } from 'vite'; -import xxhash from 'xxhash-wasm'; import { collectErrorMetadata } from '../../core/errors/dev/utils.js'; import { AstroError, AstroErrorData, isAstroError } from '../../core/errors/index.js'; import type { Logger } from '../../core/logger/core.js'; import { formatErrorMessage } from '../../core/messages.js'; +import { appendForwardSlash, joinPaths, prependForwardSlash } from '../../core/path.js'; import { getClientOutputDirectory } from '../../prerender/utils.js'; import type { AstroSettings } from '../../types/astro.js'; import { + ASSETS_DIR, CACHE_DIR, + DEFAULTS, RESOLVED_VIRTUAL_MODULE_ID, - URL_PREFIX, VIRTUAL_MODULE_ID, } from './constants.js'; -import { loadFonts } from './load.js'; -import { generateFallbackFontFace, getMetricsForFamily, readMetrics } from './metrics.js'; -import type { ResolveMod } from './providers/utils.js'; -import type { PreloadData, ResolvedFontFamily } from './types.js'; -import { cache, extractFontType, resolveFontFamily, sortObjectByKey } from './utils.js'; +import type { + CssRenderer, + FontFetcher, + FontTypeExtractor, + RemoteFontProviderModResolver, + UrlResolver, +} from './definitions.js'; +import { createMinifiableCssRenderer } from './implementations/css-renderer.js'; +import { createDataCollector } from './implementations/data-collector.js'; +import { createAstroErrorHandler } from './implementations/error-handler.js'; +import { createCachedFontFetcher } from './implementations/font-fetcher.js'; +import { createFontaceFontFileReader } from './implementations/font-file-reader.js'; +import { createCapsizeFontMetricsResolver } from './implementations/font-metrics-resolver.js'; +import { createFontTypeExtractor } from './implementations/font-type-extractor.js'; +import { createXxHasher } from './implementations/hasher.js'; +import { createRequireLocalProviderUrlResolver } from './implementations/local-provider-url-resolver.js'; +import { + createBuildRemoteFontProviderModResolver, + createDevServerRemoteFontProviderModResolver, +} from './implementations/remote-font-provider-mod-resolver.js'; +import { createRemoteFontProviderResolver } from './implementations/remote-font-provider-resolver.js'; +import { createFsStorage } from './implementations/storage.js'; +import { createSystemFallbacksProvider } from './implementations/system-fallbacks-provider.js'; +import { + createLocalUrlProxyContentResolver, + createRemoteUrlProxyContentResolver, +} from './implementations/url-proxy-content-resolver.js'; +import { createUrlProxy } from './implementations/url-proxy.js'; +import { createBuildUrlResolver, createDevUrlResolver } from './implementations/url-resolver.js'; +import { orchestrate } from './orchestrate.js'; +import type { ConsumableMap, FontFileDataMap } from './types.js'; interface Options { settings: AstroSettings; @@ -31,114 +55,122 @@ interface Options { logger: Logger; } -async function fetchFont(url: string): Promise { - try { - if (isAbsolute(url)) { - return await readFile(url); - } - // TODO: find a way to pass headers - // https://github.com/unjs/unifont/issues/143 - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Response was not successful, received status code ${response.status}`); - } - return Buffer.from(await response.arrayBuffer()); - } catch (cause) { - throw new AstroError( - { - ...AstroErrorData.CannotFetchFontFile, - message: AstroErrorData.CannotFetchFontFile.message(url), - }, - { cause }, - ); - } -} - export function fontsPlugin({ settings, sync, logger }: Options): Plugin { if (!settings.config.experimental.fonts) { - // this is required because the virtual module does not exist - // when fonts are not enabled, and that prevents rollup from building + // This is required because the virtual module may be imported as + // a side effect // TODO: remove once fonts are stabilized return { name: 'astro:fonts:fallback', - config() { - return { - build: { - rollupOptions: { - external: [VIRTUAL_MODULE_ID], - }, - }, - }; + resolveId(id) { + if (id === VIRTUAL_MODULE_ID) { + return RESOLVED_VIRTUAL_MODULE_ID; + } + }, + load(id) { + if (id === RESOLVED_VIRTUAL_MODULE_ID) { + return { + code: '', + }; + } }, }; } - // We don't need to take the trailing slash and build output configuration options - // into account because we only serve (dev) or write (build) static assets (equivalent - // to trailingSlash: never) - const baseUrl = removeTrailingForwardSlash(settings.config.base) + URL_PREFIX; + // We don't need to worry about config.trailingSlash because we are dealing with + // static assets only, ie. trailingSlash: 'never' + const assetsDir = prependForwardSlash( + appendForwardSlash(joinPaths(settings.config.build.assets, ASSETS_DIR)), + ); + const baseUrl = joinPaths(settings.config.base, assetsDir); - let resolvedMap: Map | null = null; - // Key is `${hash}.${ext}`, value is a URL. - // When a font file is requested (eg. /_astro/fonts/abc.woff), we use the hash - // to download the original file, or retrieve it from cache - let hashToUrlMap: Map | null = null; + let fontFileDataMap: FontFileDataMap | null = null; + let consumableMap: ConsumableMap | null = null; let isBuild: boolean; - let storage: Storage | null = null; + let fontFetcher: FontFetcher | null = null; + let fontTypeExtractor: FontTypeExtractor | null = null; const cleanup = () => { - resolvedMap = null; - hashToUrlMap = null; - storage = null; + consumableMap = null; + fontFileDataMap = null; + fontFetcher = null; }; - async function initialize({ resolveMod, base }: { resolveMod: ResolveMod; base: URL }) { - const { h64ToString } = await xxhash(); - - storage = createStorage({ - // Types are weirly exported - driver: (fsLiteDriver as unknown as typeof fsLiteDriver.default)({ - base: fileURLToPath(base), - }), + async function initialize({ + cacheDir, + modResolver, + cssRenderer, + urlResolver, + }: { + cacheDir: URL; + modResolver: RemoteFontProviderModResolver; + cssRenderer: CssRenderer; + urlResolver: UrlResolver; + }) { + const { root } = settings.config; + // Dependencies. Once extracted to a dedicated vite plugin, those may be passed as + // a Vite plugin option. + const hasher = await createXxHasher(); + const errorHandler = createAstroErrorHandler(); + const remoteFontProviderResolver = createRemoteFontProviderResolver({ + root, + modResolver, + errorHandler, }); + // TODO: remove when stabilizing + const pathsToWarn = new Set(); + const localProviderUrlResolver = createRequireLocalProviderUrlResolver({ + root, + intercept: (path) => { + if (path.startsWith(fileURLToPath(settings.config.publicDir))) { + if (pathsToWarn.has(path)) { + return; + } + pathsToWarn.add(path); + logger.warn( + 'assets', + `Found a local font file ${JSON.stringify(path)} in the \`public/\` folder. To avoid duplicated files in the build output, move this file into \`src/\``, + ); + } + }, + }); + const storage = createFsStorage({ base: cacheDir }); + const systemFallbacksProvider = createSystemFallbacksProvider(); + fontFetcher = createCachedFontFetcher({ storage, errorHandler, fetch, readFile }); + const fontMetricsResolver = createCapsizeFontMetricsResolver({ fontFetcher, cssRenderer }); + fontTypeExtractor = createFontTypeExtractor({ errorHandler }); + const fontFileReader = createFontaceFontFileReader({ errorHandler }); - // We initialize shared variables here and reset them in buildEnd - // to avoid locking memory - hashToUrlMap = new Map(); - resolvedMap = new Map(); - - const families: Array = []; - - for (const family of settings.config.experimental.fonts!) { - families.push( - await resolveFontFamily({ - family, - root: settings.config.root, - resolveMod, - generateNameWithHash: (_family) => - `${_family.name}-${h64ToString(JSON.stringify(sortObjectByKey(_family)))}`, - }), - ); - } - - await loadFonts({ - base: baseUrl, - families, + const res = await orchestrate({ + families: settings.config.experimental.fonts!, + hasher, + remoteFontProviderResolver, + localProviderUrlResolver, storage, - hashToUrlMap, - resolvedMap, - hashString: h64ToString, - generateFallbackFontFace, - getMetricsForFamily: async (name, font) => { - let metrics = await getMetricsForFamily(name); - if (font && !metrics) { - const { data } = await cache(storage!, font.hash, () => fetchFont(font.url)); - metrics = await readMetrics(name, data); - } - return metrics; + cssRenderer, + systemFallbacksProvider, + fontMetricsResolver, + fontTypeExtractor, + fontFileReader, + logger, + createUrlProxy: ({ local, ...params }) => { + const dataCollector = createDataCollector(params); + const contentResolver = local + ? createLocalUrlProxyContentResolver({ errorHandler }) + : createRemoteUrlProxyContentResolver(); + return createUrlProxy({ + urlResolver, + contentResolver, + hasher, + dataCollector, + }); }, - log: (message) => logger.info('assets', message), + defaults: DEFAULTS, }); + // We initialize shared variables here and reset them in buildEnd + // to avoid locking memory + fontFileDataMap = res.fontFileDataMap; + consumableMap = res.consumableMap; } return { @@ -149,20 +181,29 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { async buildStart() { if (isBuild) { await initialize({ - resolveMod: (id) => import(id), - base: new URL(CACHE_DIR, settings.config.cacheDir), + cacheDir: new URL(CACHE_DIR, settings.config.cacheDir), + modResolver: createBuildRemoteFontProviderModResolver(), + cssRenderer: createMinifiableCssRenderer({ minify: true }), + urlResolver: createBuildUrlResolver({ + base: baseUrl, + assetsPrefix: settings.config.build.assetsPrefix, + }), }); } }, async configureServer(server) { await initialize({ - resolveMod: (id) => server.ssrLoadModule(id), // In dev, we cache fonts data in .astro so it can be easily inspected and cleared - base: new URL(CACHE_DIR, settings.dotAstroDir), + cacheDir: new URL(CACHE_DIR, settings.dotAstroDir), + modResolver: createDevServerRemoteFontProviderModResolver({ server }), + cssRenderer: createMinifiableCssRenderer({ minify: false }), + urlResolver: createDevUrlResolver({ base: baseUrl }), }); // The map is always defined at this point. Its values contains urls from remote providers // as well as local paths for the local provider. We filter them to only keep the filepaths - const localPaths = [...hashToUrlMap!.values()].filter((url) => isAbsolute(url)); + const localPaths = [...fontFileDataMap!.values()] + .filter(({ url }) => isAbsolute(url)) + .map((v) => v.url); server.watcher.on('change', (path) => { if (localPaths.includes(path)) { logger.info('assets', 'Font file updated'); @@ -179,15 +220,13 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { } }); - // Base is taken into account by default. The prefix contains a traling slash, - // so it matches correctly any hash, eg. /_astro/fonts/abc.woff => abc.woff - server.middlewares.use(URL_PREFIX, async (req, res, next) => { + server.middlewares.use(assetsDir, async (req, res, next) => { if (!req.url) { return next(); } const hash = req.url.slice(1); - const url = hashToUrlMap?.get(hash); - if (!url) { + const associatedData = fontFileDataMap?.get(hash); + if (!associatedData) { return next(); } // We don't want the request to be cached in dev because we cache it already internally, @@ -200,10 +239,10 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { // Storage should be defined at this point since initialize it called before registering // the middleware. hashToUrlMap is defined at the same time so if it's not set by now, // no url will be matched and this line will not be reached. - const { data } = await cache(storage!, hash, () => fetchFont(url)); + const data = await fontFetcher!.fetch({ hash, ...associatedData }); res.setHeader('Content-Length', data.length); - res.setHeader('Content-Type', `font/${extractFontType(hash)}`); + res.setHeader('Content-Type', `font/${fontTypeExtractor!.extract(hash)}`); res.end(data); } catch (err) { @@ -224,10 +263,10 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { return RESOLVED_VIRTUAL_MODULE_ID; } }, - load(id, opts) { - if (id === RESOLVED_VIRTUAL_MODULE_ID && opts?.ssr) { + load(id) { + if (id === RESOLVED_VIRTUAL_MODULE_ID) { return { - code: `export const fontsData = new Map(${JSON.stringify(Array.from(resolvedMap?.entries() ?? []))})`, + code: `export const fontsData = new Map(${JSON.stringify(Array.from(consumableMap?.entries() ?? []))})`, }; } }, @@ -239,17 +278,17 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { try { const dir = getClientOutputDirectory(settings); - const fontsDir = new URL('.' + baseUrl, dir); + const fontsDir = new URL(`.${assetsDir}`, dir); try { mkdirSync(fontsDir, { recursive: true }); } catch (cause) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause }); } - if (hashToUrlMap) { + if (fontFileDataMap) { logger.info('assets', 'Copying fonts...'); await Promise.all( - Array.from(hashToUrlMap.entries()).map(async ([hash, url]) => { - const { data } = await cache(storage!, hash, () => fetchFont(url)); + Array.from(fontFileDataMap.entries()).map(async ([hash, associatedData]) => { + const data = await fontFetcher!.fetch({ hash, ...associatedData }); try { writeFileSync(new URL(hash, fontsDir), data); } catch (cause) { diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index 334fb38daf8a..a88276e2e419 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -156,8 +156,6 @@ export async function getImage( if (layout !== 'none') { resolvedOptions.style = addCSSVarsToStyle( { - w: String(resolvedOptions.width), - h: String(resolvedOptions.height), fit: cssFitValues.includes(resolvedOptions.fit ?? '') && resolvedOptions.fit, pos: resolvedOptions.position, }, diff --git a/packages/astro/src/assets/layout.ts b/packages/astro/src/assets/layout.ts index adc117f39967..ea0be6f74d53 100644 --- a/packages/astro/src/assets/layout.ts +++ b/packages/astro/src/assets/layout.ts @@ -68,8 +68,8 @@ export const getWidths = ({ return originalWidth && width > originalWidth ? [originalWidth] : [width, maxSize]; } - // For responsive layout we want to return all breakpoints smaller than 2x requested width. - if (layout === 'responsive') { + // For constrained layout we want to return all breakpoints smaller than 2x requested width. + if (layout === 'constrained') { return ( [ // Always include the image at 1x and 2x the specified width @@ -100,15 +100,15 @@ export const getSizesAttribute = ({ switch (layout) { // If screen is wider than the max size then image width is the max size, // otherwise it's the width of the screen - case `responsive`: + case 'constrained': return `(min-width: ${width}px) ${width}px, 100vw`; // Image is always the same width, whatever the size of the screen - case `fixed`: + case 'fixed': return `${width}px`; // Image is always the width of the screen - case `full-width`: + case 'full-width': return `100vw`; case 'none': diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index 4bb643a9cdfe..7fa062188db1 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -81,7 +81,7 @@ interface SharedServiceProps = Record export type ExternalImageService = Record> = SharedServiceProps; -export type LocalImageTransform = { +type LocalImageTransform = { src: string; [key: string]: any; }; diff --git a/packages/astro/src/assets/services/sharp.ts b/packages/astro/src/assets/services/sharp.ts index bbae39eb093a..c6088ad9959c 100644 --- a/packages/astro/src/assets/services/sharp.ts +++ b/packages/astro/src/assets/services/sharp.ts @@ -108,7 +108,20 @@ const sharpService: LocalImageService = { } } - result.toFormat(transform.format as keyof FormatEnum, { quality: quality }); + const isGifInput = + inputBuffer[0] === 0x47 && // 'G' + inputBuffer[1] === 0x49 && // 'I' + inputBuffer[2] === 0x46 && // 'F' + inputBuffer[3] === 0x38 && // '8' + (inputBuffer[4] === 0x39 || inputBuffer[4] === 0x37) && // '9' or '7' + inputBuffer[5] === 0x61; // 'a' + + if (transform.format === 'webp' && isGifInput) { + // Convert animated GIF to animated WebP with loop=0 (infinite) + result.webp({ quality: typeof quality === 'number' ? quality : undefined, loop: 0 }); + } else { + result.toFormat(transform.format as keyof FormatEnum, { quality }); + } } const { data, info } = await result.toBuffer({ resolveWithObject: true }); diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts index ac6df6799158..ca1216e71f5c 100644 --- a/packages/astro/src/assets/types.ts +++ b/packages/astro/src/assets/types.ts @@ -6,7 +6,7 @@ export type ImageQualityPreset = 'low' | 'mid' | 'high' | 'max' | (string & {}); export type ImageQuality = ImageQualityPreset | number; export type ImageInputFormat = (typeof VALID_INPUT_FORMATS)[number]; export type ImageOutputFormat = (typeof VALID_OUTPUT_FORMATS)[number] | (string & {}); -export type ImageLayout = 'responsive' | 'fixed' | 'full-width' | 'none'; +export type ImageLayout = 'constrained' | 'fixed' | 'full-width' | 'none'; export type ImageFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | (string & {}); export type AssetsGlobalStaticImagesList = Map< @@ -162,15 +162,15 @@ type ImageSharedProps = T & { /** * The layout type for responsive images. Requires the `experimental.responsiveImages` flag to be enabled in the Astro config. * - * Allowed values are `responsive`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`. + * Allowed values are `constrained`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`. * - * - `responsive` - The image will scale to fit the container, maintaining its aspect ratio, but will not exceed the specified dimensions. + * - `constrained` - The image will scale to fit the container, maintaining its aspect ratio, but will not exceed the specified dimensions. * - `fixed` - The image will maintain its original dimensions. * - `full-width` - The image will scale to fit the container, maintaining its aspect ratio, even if that means the image will exceed its original dimensions. * * **Example**: * ```astro - * ... + * ... * ``` */ diff --git a/packages/astro/src/assets/utils/etag.ts b/packages/astro/src/assets/utils/etag.ts index 78d208b908f7..d1650b8c011e 100644 --- a/packages/astro/src/assets/utils/etag.ts +++ b/packages/astro/src/assets/utils/etag.ts @@ -8,7 +8,7 @@ * Simplified, optimized and add modified for 52 bit, which provides a larger hash space * and still making use of Javascript's 53-bit integer space. */ -export const fnv1a52 = (str: string) => { +const fnv1a52 = (str: string) => { const len = str.length; let i = 0, t0 = 0, diff --git a/packages/astro/src/assets/utils/vendor/image-size/lookup.ts b/packages/astro/src/assets/utils/vendor/image-size/lookup.ts index d3283e72e0b0..d3fd3fe19d5a 100644 --- a/packages/astro/src/assets/utils/vendor/image-size/lookup.ts +++ b/packages/astro/src/assets/utils/vendor/image-size/lookup.ts @@ -1,16 +1,7 @@ -import type { imageType } from './types/index.js' import { typeHandlers } from './types/index.js' import { detector } from './detector.js' import type { ISizeCalculationResult } from './types/interface.ts' -type Options = { - disabledTypes: imageType[] -} - -const globalOptions: Options = { - disabledTypes: [], -} - /** * Return size information based on an Uint8Array * @@ -22,10 +13,6 @@ export function lookup(input: Uint8Array): ISizeCalculationResult { const type = detector(input) if (typeof type !== 'undefined') { - if (globalOptions.disabledTypes.includes(type)) { - throw new TypeError('disabled file type: ' + type) - } - // find an appropriate handler for this file type const size = typeHandlers.get(type)!.calculate(input) if (size !== undefined) { @@ -37,7 +24,3 @@ export function lookup(input: Uint8Array): ISizeCalculationResult { // throw up, if we don't understand the file throw new TypeError('unsupported file type: ' + type) } - -export const disableTypes = (types: imageType[]): void => { - globalOptions.disabledTypes = types -} diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index 693db9cb7198..11f5985543bb 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -503,11 +503,7 @@ function addVitePlugin(mod: ProxifiedModule, pluginId: string, packageName: } } -export function setAdapter( - mod: ProxifiedModule, - adapter: IntegrationInfo, - exportName: string, -) { +function setAdapter(mod: ProxifiedModule, adapter: IntegrationInfo, exportName: string) { const config = getDefaultExportOptions(mod); const adapterId = toIdent(adapter.id); diff --git a/packages/astro/src/cli/info/index.ts b/packages/astro/src/cli/info/index.ts index aca66ad719e0..554c89608af0 100644 --- a/packages/astro/src/cli/info/index.ts +++ b/packages/astro/src/cli/info/index.ts @@ -52,7 +52,7 @@ export async function printInfo({ flags }: InfoOptions) { await copyToClipboard(output, flags.copy); } -export async function copyToClipboard(text: string, force?: boolean) { +async function copyToClipboard(text: string, force?: boolean) { text = text.trim(); const system = platform(); let command = ''; diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts index c31841d24b49..7824f0688389 100644 --- a/packages/astro/src/cli/preferences/index.ts +++ b/packages/astro/src/cli/preferences/index.ts @@ -26,7 +26,7 @@ const PREFERENCES_SUBCOMMANDS = [ 'reset', 'list', ] as const; -export type Subcommand = (typeof PREFERENCES_SUBCOMMANDS)[number]; +type Subcommand = (typeof PREFERENCES_SUBCOMMANDS)[number]; type AnnotatedValue = { annotation: string; value: string | number | boolean }; type AnnotatedValues = Record; diff --git a/packages/astro/src/config/entrypoint.ts b/packages/astro/src/config/entrypoint.ts index a1301662eb00..dc2f47242d8b 100644 --- a/packages/astro/src/config/entrypoint.ts +++ b/packages/astro/src/config/entrypoint.ts @@ -8,7 +8,7 @@ export { envField } from '../env/config.js'; export { mergeConfig } from '../core/config/merge.js'; export { validateConfig } from '../core/config/validate.js'; export { fontProviders, defineAstroFontProvider } from '../assets/fonts/providers/index.js'; -export type { AstroFontProvider as FontProvider } from '../assets/fonts/types.js'; +export type { AstroFontProvider } from '../assets/fonts/types.js'; /** * Return the configuration needed to use the Sharp-based image service diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index 6559d459462f..5c92c47a05db 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -124,10 +124,6 @@ export type AddClientRenderer = { entrypoint: string; }; -type ContainerImportRendererFn = ( - containerRenderer: ContainerRenderer, -) => Promise; - function createManifest( manifest?: AstroContainerManifest, renderers?: SSRLoadedRenderer[], @@ -269,12 +265,6 @@ export class experimental_AstroContainer { */ #withManifest = false; - /** - * Internal function responsible for importing a renderer - * @private - */ - #getRenderer: ContainerImportRendererFn | undefined; - private constructor({ streaming = false, manifest, @@ -414,7 +404,7 @@ export class experimental_AstroContainer { } // NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it. - // @ematipico: I plan to use it for a possible integration that could help people + // @ts-expect-error @ematipico: I plan to use it for a possible integration that could help people private static async createFromManifest( manifest: SSRManifest, ): Promise { diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts index 10879f2b51dd..1992e7706414 100644 --- a/packages/astro/src/content/content-layer.ts +++ b/packages/astro/src/content/content-layer.ts @@ -26,7 +26,7 @@ import { } from './utils.js'; import { type WrappedWatcher, createWatcherWrapper } from './watcher.js'; -export interface ContentLayerOptions { +interface ContentLayerOptions { store: MutableDataStore; settings: AstroSettings; logger: Logger; @@ -39,7 +39,7 @@ type CollectionLoader = () => | Record> | Promise>>; -export class ContentLayer { +class ContentLayer { #logger: Logger; #store: MutableDataStore; #settings: AstroSettings; @@ -342,7 +342,7 @@ export class ContentLayer { } } -export async function simpleLoader( +async function simpleLoader( handler: CollectionLoader, context: LoaderContext, ) { diff --git a/packages/astro/src/content/index.ts b/packages/astro/src/content/index.ts index 2aef23e85472..9161a5fe91ee 100644 --- a/packages/astro/src/content/index.ts +++ b/packages/astro/src/content/index.ts @@ -1,7 +1,6 @@ -export { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from './consts.js'; export { attachContentServerListeners } from './server-listeners.js'; export { createContentTypesGenerator } from './types-generator.js'; -export { contentObservable, getContentPaths, hasAssetPropagationFlag } from './utils.js'; +export { getContentPaths, hasAssetPropagationFlag } from './utils.js'; export { astroContentAssetPropagationPlugin } from './vite-plugin-content-assets.js'; export { astroContentImportPlugin } from './vite-plugin-content-imports.js'; export { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js'; diff --git a/packages/astro/src/content/loaders/file.ts b/packages/astro/src/content/loaders/file.ts index c37998a762ec..d877381c23fe 100644 --- a/packages/astro/src/content/loaders/file.ts +++ b/packages/astro/src/content/loaders/file.ts @@ -1,10 +1,12 @@ import { promises as fs, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import yaml from 'js-yaml'; +import { FileGlobNotSupported, FileParserNotFound } from '../../core/errors/errors-data.js'; +import { AstroError } from '../../core/errors/index.js'; import { posixRelative } from '../utils.js'; import type { Loader, LoaderContext } from './types.js'; -export interface FileOptions { +interface FileOptions { /** * the parsing function to use for this data * @default JSON.parse or yaml.load, depending on the extension of the file @@ -21,8 +23,7 @@ export interface FileOptions { */ export function file(fileName: string, options?: FileOptions): Loader { if (fileName.includes('*')) { - // TODO: AstroError - throw new Error('Glob patterns are not supported in `file` loader. Use `glob` loader instead.'); + throw new AstroError(FileGlobNotSupported); } let parse: ((text: string) => any) | null = null; @@ -39,10 +40,10 @@ export function file(fileName: string, options?: FileOptions): Loader { if (options?.parser) parse = options.parser; if (parse === null) { - // TODO: AstroError - throw new Error( - `No parser found for file '${fileName}'. Try passing a parser to the \`file\` loader.`, - ); + throw new AstroError({ + ...FileParserNotFound, + message: FileParserNotFound.message(fileName), + }); } async function syncData(filePath: string, { logger, parseData, store, config }: LoaderContext) { diff --git a/packages/astro/src/content/loaders/glob.ts b/packages/astro/src/content/loaders/glob.ts index 9727ccc08e05..cb6faa059690 100644 --- a/packages/astro/src/content/loaders/glob.ts +++ b/packages/astro/src/content/loaders/glob.ts @@ -10,7 +10,7 @@ import type { RenderedContent } from '../data-store.js'; import { getContentEntryIdAndSlug, posixRelative } from '../utils.js'; import type { Loader } from './types.js'; -export interface GenerateIdOptions { +interface GenerateIdOptions { /** The path to the entry file, relative to the base directory. */ entry: string; @@ -20,7 +20,7 @@ export interface GenerateIdOptions { data: Record; } -export interface GlobOptions { +interface GlobOptions { /** The glob pattern to match files, relative to the base directory */ pattern: string | Array; /** The base directory to resolve the glob pattern from. Relative to the root directory, or an absolute file URL. Defaults to `.` */ diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index b08ea252e3ee..a319dfd297cd 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -17,7 +17,7 @@ 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, + type CONTENT_FLAGS, CONTENT_LAYER_TYPE, CONTENT_MODULE_FLAG, DEFERRED_MODULE, @@ -495,15 +495,6 @@ export function safeParseFrontmatter(source: string, id?: string) { */ export const globalContentConfigObserver = contentObservable({ status: 'init' }); -export function hasAnyContentFlag(viteId: string): boolean { - const flags = new URLSearchParams(viteId.split('?')[1] ?? ''); - const flag = Array.from(flags.keys()).at(0); - if (typeof flag !== 'string') { - return false; - } - return CONTENT_FLAGS.includes(flag as any); -} - export function hasContentFlag(viteId: string, flag: (typeof CONTENT_FLAGS)[number]): boolean { const flags = new URLSearchParams(viteId.split('?')[1] ?? ''); return flags.has(flag); @@ -542,7 +533,7 @@ async function loadContentConfig({ } } -export async function autogenerateCollections({ +async function autogenerateCollections({ config, settings, fs, @@ -679,7 +670,7 @@ type Observable = { export type ContentObservable = Observable; -export function contentObservable(initialCtx: ContentCtx): ContentObservable { +function contentObservable(initialCtx: ContentCtx): ContentObservable { type Subscriber = (ctx: ContentCtx) => void; const subscribers = new Set(); let ctx = initialCtx; @@ -809,7 +800,7 @@ export function globWithUnderscoresIgnored(relContentDir: string, exts: string[] /** * Convert a platform path to a posix path. */ -export function posixifyPath(filePath: string) { +function posixifyPath(filePath: string) { return filePath.split(path.sep).join('/'); } 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 df84eed5c031..ff0435844b5c 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -198,7 +198,7 @@ export function astroContentVirtualModPlugin({ }; } -export async function generateContentEntryFile({ +async function generateContentEntryFile({ settings, lookupMap, isClient, @@ -258,7 +258,7 @@ export async function generateContentEntryFile({ * This is used internally to resolve entry imports when using `getEntry()`. * @see `templates/content/module.mjs` */ -export async function generateLookupMap({ +async function generateLookupMap({ settings, fs, }: { diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index ce7990b64299..6bb166e6b177 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -122,7 +122,7 @@ export class App { // to return the host 404 if the user doesn't provide a custom 404 ensure404Route(this.#manifestData); this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base); - this.#pipeline = this.#createPipeline(this.#manifestData, streaming); + this.#pipeline = this.#createPipeline(streaming); this.#adapterLogger = new AstroIntegrationLogger( this.#logger.options, this.#manifest.adapterName, @@ -136,12 +136,11 @@ export class App { /** * Creates a pipeline by reading the stored manifest * - * @param manifestData * @param streaming * @private */ - #createPipeline(manifestData: RoutesList, streaming = false) { - return AppPipeline.create(manifestData, { + #createPipeline(streaming = false) { + return AppPipeline.create({ logger: this.#logger, manifest: this.#manifest, runtimeMode: 'production', diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index fe9b6257d90a..632c2671081d 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,4 +1,4 @@ -import type { ComponentInstance, RoutesList } from '../../types/astro.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'; import { Pipeline, type TryRewriteResult } from '../base-pipeline.js'; @@ -8,31 +8,26 @@ import { createModuleScriptElement, createStylesheetElementSet } from '../render import { findRouteToRewrite } from '../routing/rewrite.js'; export class AppPipeline extends Pipeline { - #manifestData: RoutesList | undefined; - - static create( - manifestData: RoutesList, - { - logger, - manifest, - runtimeMode, - renderers, - resolve, - serverLike, - streaming, - defaultRoutes, - }: Pick< - AppPipeline, - | 'logger' - | 'manifest' - | 'runtimeMode' - | 'renderers' - | 'resolve' - | 'serverLike' - | 'streaming' - | 'defaultRoutes' - >, - ) { + static create({ + logger, + manifest, + runtimeMode, + renderers, + resolve, + serverLike, + streaming, + defaultRoutes, + }: Pick< + AppPipeline, + | 'logger' + | 'manifest' + | 'runtimeMode' + | 'renderers' + | 'resolve' + | 'serverLike' + | 'streaming' + | 'defaultRoutes' + >) { const pipeline = new AppPipeline( logger, manifest, @@ -51,7 +46,6 @@ export class AppPipeline extends Pipeline { undefined, defaultRoutes, ); - pipeline.#manifestData = manifestData; return pipeline; } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 9339f23848b9..90e4be871f6d 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -12,7 +12,7 @@ import type { } from '../../types/public/internal.js'; import type { SinglePageBuiltModule } from '../build/types.js'; -export type ComponentPath = string; +type ComponentPath = string; export type StylesheetAsset = | { type: 'inline'; content: string } @@ -35,7 +35,7 @@ export type SerializedRouteInfo = Omit & { routeData: SerializedRouteData; }; -export type ImportComponentInstance = () => Promise; +type ImportComponentInstance = () => Promise; export type AssetsPrefix = | string diff --git a/packages/astro/src/core/build/consts.ts b/packages/astro/src/core/build/consts.ts index 2926d1e8686c..bf3162fc4b92 100644 --- a/packages/astro/src/core/build/consts.ts +++ b/packages/astro/src/core/build/consts.ts @@ -1,2 +1 @@ export const CHUNKS_PATH = 'chunks/'; -export const CONTENT_PATH = 'content/'; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 2313ca01a89b..35c96a7a99d1 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -297,7 +297,7 @@ async function generatePage( const path = paths[i]; promises.push(limit(() => generatePathWithLogs(path, route, i, paths, true))); } - await Promise.allSettled(promises); + await Promise.all(promises); } else { for (let i = 0; i < paths.length; i++) { const path = paths[i]; diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 9a90daf9d428..76e56d3d05c9 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -33,7 +33,7 @@ import { staticBuild, viteBuild } from './static-build.js'; import type { StaticBuildOptions } from './types.js'; import { getTimeStat } from './util.js'; -export interface BuildOptions { +interface BuildOptions { /** * Output a development-based build similar to code transformed in `astro dev`. This * can be useful to test build-only issues with additional debugging information included. diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index 78271d4e9cef..7d69b72353ad 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -7,13 +7,13 @@ import { debug } from '../logger/core.js'; import { DEFAULT_COMPONENTS } from '../routing/default.js'; import { makePageDataKey } from './plugins/util.js'; -export interface CollectPagesDataOptions { +interface CollectPagesDataOptions { settings: AstroSettings; logger: Logger; manifest: RoutesList; } -export interface CollectPagesDataResult { +interface CollectPagesDataResult { assets: Record; allPages: AllPagesData; } diff --git a/packages/astro/src/core/build/plugin.ts b/packages/astro/src/core/build/plugin.ts index f16b5a1d9423..33b1b722f57c 100644 --- a/packages/astro/src/core/build/plugin.ts +++ b/packages/astro/src/core/build/plugin.ts @@ -9,7 +9,7 @@ export type BuildTarget = 'server' | 'client'; type MutateChunk = (chunk: OutputChunk, targets: BuildTarget[], newCode: string) => void; -export interface BuildBeforeHookResult { +interface BuildBeforeHookResult { enforce?: 'after-user-plugins'; vitePlugin: VitePlugin | VitePlugin[] | undefined; } diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index 2b64d338395b..5b949c841f11 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -9,7 +9,7 @@ import { } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; -export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { +function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { return { name: '@astro/rollup-plugin-astro-analyzer', async generateBundle() { diff --git a/packages/astro/src/core/build/plugins/plugin-chunks.ts b/packages/astro/src/core/build/plugins/plugin-chunks.ts index 3348e126c01c..f281386e58d4 100644 --- a/packages/astro/src/core/build/plugins/plugin-chunks.ts +++ b/packages/astro/src/core/build/plugins/plugin-chunks.ts @@ -2,7 +2,7 @@ import type { Plugin as VitePlugin } from 'vite'; import type { AstroBuildPlugin } from '../plugin.js'; import { extendManualChunks } from './util.js'; -export function vitePluginChunks(): VitePlugin { +function vitePluginChunks(): VitePlugin { return { name: 'astro:chunks', outputOptions(outputOptions) { diff --git a/packages/astro/src/core/build/plugins/plugin-component-entry.ts b/packages/astro/src/core/build/plugins/plugin-component-entry.ts index d7fc8aa5ef5f..ec614f508ee6 100644 --- a/packages/astro/src/core/build/plugins/plugin-component-entry.ts +++ b/packages/astro/src/core/build/plugins/plugin-component-entry.ts @@ -2,14 +2,14 @@ import type { Plugin as VitePlugin } from 'vite'; import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; -export const astroEntryPrefix = '\0astro-entry:'; +const astroEntryPrefix = '\0astro-entry:'; /** * When adding hydrated or client:only components as Rollup inputs, sometimes we're not using all * of the export names, e.g. `import { Counter } from './ManyComponents.jsx'`. This plugin proxies * entries to re-export only the names the user is using. */ -export function vitePluginComponentEntry(internals: BuildInternals): VitePlugin { +function vitePluginComponentEntry(internals: BuildInternals): VitePlugin { const componentToExportNames = new Map(); mergeComponentExportNames(internals.discoveredHydratedComponents); diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index c39d4da6f08f..d985a6a5311a 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -1,12 +1,11 @@ import type { GetModuleInfo } from 'rollup'; -import type { BuildOptions, ResolvedConfig, Rollup, Plugin as VitePlugin } from 'vite'; +import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin } from 'vite'; import { isBuildableCSSRequest } from '../../../vite-plugin-astro-server/util.js'; import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin, BuildTarget } from '../plugin.js'; import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js'; import { hasAssetPropagationFlag } from '../../../content/index.js'; -import type { AstroPluginCssMetadata } from '../../../vite-plugin-astro/index.js'; import * as assetName from '../css-asset-name.js'; import { getParentExtendedModuleInfos, @@ -156,32 +155,6 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { }, }; - /** - * This plugin is a port of https://github.com/vitejs/vite/pull/16058. It enables removing unused - * scoped CSS from the bundle if the scoped target (e.g. Astro files) were not bundled. - * Once/If that PR is merged, we can refactor this away, renaming `meta.astroCss` to `meta.vite`. - */ - const cssScopeToPlugin: VitePlugin = { - name: 'astro:rollup-plugin-css-scope-to', - renderChunk(_, chunk, __, meta) { - for (const id in chunk.modules) { - // If this CSS is scoped to its importers exports, check if those importers exports - // are rendered in the chunks. If they are not, we can skip bundling this CSS. - const modMeta = this.getModuleInfo(id)?.meta as AstroPluginCssMetadata | undefined; - const cssScopeTo = modMeta?.astroCss?.cssScopeTo; - if (cssScopeTo && !isCssScopeToRendered(cssScopeTo, Object.values(meta.chunks))) { - // If this CSS is not used, delete it from the chunk modules so that Vite is unable - // to trace that it's used - delete chunk.modules[id]; - const moduleIdsIndex = chunk.moduleIds.indexOf(id); - if (moduleIdsIndex > -1) { - chunk.moduleIds.splice(moduleIdsIndex, 1); - } - } - } - }, - }; - const singleCssPlugin: VitePlugin = { name: 'astro:rollup-plugin-single-css', enforce: 'post', @@ -273,7 +246,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { }, }; - return [cssBuildPlugin, cssScopeToPlugin, singleCssPlugin, inlineStylesheetsPlugin]; + return [cssBuildPlugin, singleCssPlugin, inlineStylesheetsPlugin]; } /***** UTILITY FUNCTIONS *****/ @@ -321,25 +294,3 @@ function appendCSSToPage( } } } - -/** - * `cssScopeTo` is a map of `importer`s to its `export`s. This function iterate each `cssScopeTo` entries - * and check if the `importer` and its `export`s exists in the final chunks. If at least one matches, - * `cssScopeTo` is considered "rendered" by Rollup and we return true. - */ -function isCssScopeToRendered( - cssScopeTo: Record, - chunks: Rollup.RenderedChunk[], -) { - for (const moduleId in cssScopeTo) { - const exports = cssScopeTo[moduleId]; - // Find the chunk that renders this `moduleId` and get the rendered module - const renderedModule = chunks.find((c) => c.moduleIds.includes(moduleId))?.modules[moduleId]; - // Return true if `renderedModule` exists and one of its exports is rendered - if (renderedModule?.renderedExports.some((e) => exports.includes(e))) { - return true; - } - } - - return false; -} diff --git a/packages/astro/src/core/build/plugins/plugin-internals.ts b/packages/astro/src/core/build/plugins/plugin-internals.ts index 01d5245151d3..2d4dfc3603f3 100644 --- a/packages/astro/src/core/build/plugins/plugin-internals.ts +++ b/packages/astro/src/core/build/plugins/plugin-internals.ts @@ -3,7 +3,7 @@ import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import { normalizeEntryId } from './plugin-component-entry.js'; -export function vitePluginInternals(input: Set, internals: BuildInternals): VitePlugin { +function vitePluginInternals(input: Set, internals: BuildInternals): VitePlugin { return { name: '@astro/plugin-build-internals', diff --git a/packages/astro/src/core/build/plugins/plugin-renderers.ts b/packages/astro/src/core/build/plugins/plugin-renderers.ts index 1b4b8105ac31..4ef4342ddb88 100644 --- a/packages/astro/src/core/build/plugins/plugin-renderers.ts +++ b/packages/astro/src/core/build/plugins/plugin-renderers.ts @@ -6,7 +6,7 @@ import type { StaticBuildOptions } from '../types.js'; export const RENDERERS_MODULE_ID = '@astro-renderers'; export const RESOLVED_RENDERERS_MODULE_ID = `\0${RENDERERS_MODULE_ID}`; -export function vitePluginRenderers(opts: StaticBuildOptions): VitePlugin { +function vitePluginRenderers(opts: StaticBuildOptions): VitePlugin { return { name: '@astro/plugin-renderers', diff --git a/packages/astro/src/core/build/plugins/plugin-scripts.ts b/packages/astro/src/core/build/plugins/plugin-scripts.ts index e41d1fe44e61..022f3716d5a8 100644 --- a/packages/astro/src/core/build/plugins/plugin-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-scripts.ts @@ -6,7 +6,7 @@ import { shouldInlineAsset } from './util.js'; /** * Inline scripts from Astro files directly into the HTML. */ -export function vitePluginScripts(internals: BuildInternals): VitePlugin { +function vitePluginScripts(internals: BuildInternals): VitePlugin { let assetInlineLimit: NonNullable; return { diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index fdc05026fc10..649bdc39ebec 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -13,7 +13,7 @@ import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; import { getVirtualModulePageName } from './util.js'; -export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; +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'; diff --git a/packages/astro/src/core/build/plugins/util.ts b/packages/astro/src/core/build/plugins/util.ts index c636928d0bc2..63200d9e90f8 100644 --- a/packages/astro/src/core/build/plugins/util.ts +++ b/packages/astro/src/core/build/plugins/util.ts @@ -46,7 +46,7 @@ export function extendManualChunks(outputOptions: OutputOptions, hooks: ExtendMa export const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; // This is an arbitrary string that we use to make a pageData key // Has to be a invalid character for a route, to avoid conflicts. -export const ASTRO_PAGE_KEY_SEPARATOR = '&'; +const ASTRO_PAGE_KEY_SEPARATOR = '&'; /** * Generate a unique key to identify each page in the build process. @@ -103,10 +103,7 @@ export function getPagesFromVirtualModulePageName( * @param virtualModulePrefix The prefix at the beginning of the virtual module * @param id Virtual module name */ -export function getComponentFromVirtualModulePageName( - virtualModulePrefix: string, - id: string, -): string { +function getComponentFromVirtualModulePageName(virtualModulePrefix: string, id: string): string { return id.slice(virtualModulePrefix.length).replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); } diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index f647e053a642..4c3e0136fc5f 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -6,7 +6,7 @@ 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; +type ComponentPath = string; export type ViteID = string; export type StylesheetAsset = diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index ae15805ffa3d..9a371e994db2 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -26,7 +26,7 @@ export function resolveRoot(cwd?: string | URL): string { // Config paths to search for. In order of likely appearance // to speed up the check. -export const configPaths = Object.freeze([ +const configPaths = Object.freeze([ 'astro.config.mjs', 'astro.config.js', 'astro.config.ts', diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts index f5449d4ffea6..00832e84733d 100644 --- a/packages/astro/src/core/config/index.ts +++ b/packages/astro/src/core/config/index.ts @@ -1,11 +1,9 @@ export { - configPaths, resolveConfig, resolveConfigPath, resolveRoot, } from './config.js'; export { createNodeLogger } from './logging.js'; export { mergeConfig } from './merge.js'; -export type { AstroConfigType } from './schemas/index.js'; export { createSettings } from './settings.js'; export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js'; diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index 5d2c0196a58b..7618ff521a5e 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -31,6 +31,7 @@ import type { AstroUserConfig, ViteUserConfig } from '../../../types/public/conf // Also, make sure to not index the complexified type, as it would return a simplified value type, which goes // back to the issue again. The complexified type should be the base representation that we want to expose. +/** @lintignore */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface ComplexifyUnionObj {} @@ -42,6 +43,7 @@ type ShikiTheme = ComplexifyWithUnion>; type ShikiTransformer = ComplexifyWithUnion[number]>; type RehypePlugin = ComplexifyWithUnion<_RehypePlugin>; type RemarkPlugin = ComplexifyWithUnion<_RemarkPlugin>; +/** @lintignore */ export type RemarkRehype = ComplexifyWithOmit<_RemarkRehype>; export const ASTRO_CONFIG_DEFAULTS = { @@ -268,7 +270,7 @@ export const AstroConfigSchema = z.object({ }), ) .default([]), - experimentalLayout: z.enum(['responsive', 'fixed', 'full-width', 'none']).optional(), + experimentalLayout: z.enum(['constrained', 'fixed', 'full-width', 'none']).optional(), experimentalObjectFit: z.string().optional(), experimentalObjectPosition: z.string().optional(), experimentalBreakpoints: z.array(z.number()).optional(), diff --git a/packages/astro/src/core/config/timer.ts b/packages/astro/src/core/config/timer.ts index d41009b11499..d9e47ab211ab 100644 --- a/packages/astro/src/core/config/timer.ts +++ b/packages/astro/src/core/config/timer.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; // Type used by `bench-memory.js` -export interface Stat { +interface Stat { elapsedTime: number; heapUsedChange: number; heapUsedTotal: number; diff --git a/packages/astro/src/core/config/tsconfig.ts b/packages/astro/src/core/config/tsconfig.ts index 03d89e30cc36..6a165a4902f1 100644 --- a/packages/astro/src/core/config/tsconfig.ts +++ b/packages/astro/src/core/config/tsconfig.ts @@ -169,7 +169,7 @@ function deepMergeObjects>(a: T, b: T): T { // https://github.com/unjs/pkg-types/blob/78328837d369d0145a8ddb35d7fe1fadda4bfadf/src/types/tsconfig.ts // See https://github.com/unjs/pkg-types/blob/78328837d369d0145a8ddb35d7fe1fadda4bfadf/LICENSE for license information -export type StripEnums> = { +type StripEnums> = { [K in keyof T]: T[K] extends boolean ? T[K] : T[K] extends string diff --git a/packages/astro/src/core/constants.ts b/packages/astro/src/core/constants.ts index dc09a1f69474..8b304aa12f46 100644 --- a/packages/astro/src/core/constants.ts +++ b/packages/astro/src/core/constants.ts @@ -46,11 +46,6 @@ export const ROUTE_TYPE_HEADER = 'X-Astro-Route-Type'; */ export const DEFAULT_404_COMPONENT = 'astro-default-404.astro'; -/** - * The value of the `component` field of the default 500 page, which is used when there is no user-provided 404.astro page. - */ -export const DEFAULT_500_COMPONENT = 'astro-default-500.astro'; - /** * A response with one of these status codes will create a redirect response. */ diff --git a/packages/astro/src/core/cookies/index.ts b/packages/astro/src/core/cookies/index.ts index 1ac732f1b1fb..f837acf4895c 100644 --- a/packages/astro/src/core/cookies/index.ts +++ b/packages/astro/src/core/cookies/index.ts @@ -2,6 +2,5 @@ export { AstroCookies } from './cookies.js'; export { attachCookiesToResponse, getSetCookiesFromResponse, - responseHasCookies, } from './response.js'; export type { AstroCookieSetOptions, AstroCookieGetOptions } from './cookies.js'; diff --git a/packages/astro/src/core/cookies/response.ts b/packages/astro/src/core/cookies/response.ts index 288bb3e930fb..8b94ed5f2538 100644 --- a/packages/astro/src/core/cookies/response.ts +++ b/packages/astro/src/core/cookies/response.ts @@ -6,10 +6,6 @@ export function attachCookiesToResponse(response: Response, cookies: AstroCookie Reflect.set(response, astroCookiesSymbol, cookies); } -export function responseHasCookies(response: Response): boolean { - return Reflect.has(response, astroCookiesSymbol); -} - export function getCookiesFromResponse(response: Response): AstroCookies | undefined { let cookies = Reflect.get(response, astroCookiesSymbol); if (cookies != null) { diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts index c984fae7d254..5da3f0c399e7 100644 --- a/packages/astro/src/core/dev/container.ts +++ b/packages/astro/src/core/dev/container.ts @@ -30,7 +30,7 @@ export interface Container { close: () => Promise; } -export interface CreateContainerParams { +interface CreateContainerParams { logger: Logger; settings: AstroSettings; inlineConfig?: AstroInlineConfig; diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts index 47de19bde627..bf68dd926dda 100644 --- a/packages/astro/src/core/dev/index.ts +++ b/packages/astro/src/core/dev/index.ts @@ -1,3 +1,3 @@ -export { createContainer, startContainer } from './container.js'; +export { startContainer } from './container.js'; export { default } from './dev.js'; export { createContainerWithAutomaticRestart } from './restart.js'; diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts index 4aa1e2b74130..7138055ec88d 100644 --- a/packages/astro/src/core/dev/restart.ts +++ b/packages/astro/src/core/dev/restart.ts @@ -103,7 +103,7 @@ async function restartContainer(container: Container): Promise { return decodeKey(encodedKey); } -/** - * 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 */ diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index a6a4c174a1bb..d19e376dd935 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -976,7 +976,9 @@ export const RedirectWithNoLocation = { export const UnsupportedExternalRedirect = { name: 'UnsupportedExternalRedirect', title: 'Unsupported or malformed URL.', - message: 'An external redirect must start with http or https, and must be a valid URL.', + message: (from: string, to: string) => + `The destination URL in the external redirect from "${from}" to "${to}" is unsupported.`, + hint: 'An external redirect must start with http or https, and must be a valid URL.', } satisfies ErrorData; /** @@ -1307,6 +1309,21 @@ export const CannotExtractFontType = { hint: 'Open an issue at https://github.com/withastro/astro/issues.', } satisfies ErrorData; +/** + * @docs + * @description + * Cannot determine weight and style from font file, update your family config and set `weight` and `style` manually instead. + * @message + * An error occured while determining the weight and style from the local font file. + */ +export const CannotDetermineWeightAndStyleFromFontFile = { + name: 'CannotDetermineWeightAndStyleFromFontFile', + title: 'Cannot determine weight and style from font file.', + message: (family: string, url: string) => + `An error occurred while determining the \`weight\` and \`style\` from local family "${family}" font file: ${url}`, + hint: 'Update your family config and set `weight` and `style` manually instead.', +} satisfies ErrorData; + /** * @docs * @description @@ -1524,6 +1541,7 @@ export const GenerateContentTypesError = { hint: (fileName?: string) => `This error is often caused by a syntax error inside your content, or your content configuration file. Check your ${fileName ?? 'content config'} file for typos.`, } satisfies ErrorData; + /** * @docs * @kind heading @@ -1798,6 +1816,34 @@ export const UnsupportedConfigTransformError = { hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue', } satisfies ErrorData; +/** + * @docs + * @see + * - [Passing a `parser` to the `file` loader](https://docs.astro.build/en/guides/content-collections/#parser-function) + * @description + * The `file` loader can’t determine which parser to use. Please provide a custom parser (e.g. `toml.parse` or `csv-parse`) to create a collection from your file type. + */ +export const FileParserNotFound = { + name: 'FileParserNotFound', + title: 'File parser not found', + message: (fileName: string) => + `No parser was found for '${fileName}'. Pass a parser function (e.g. \`parser: csv\`) to the \`file\` loader.`, +} satisfies ErrorData; + +/** + * @docs + * @see + * - [Astro's built-in loaders](https://docs.astro.build/en/guides/content-collections/#built-in-loaders) + * @description + * The `file` loader must be passed a single local file. Glob patterns are not supported. Use the built-in `glob` loader to create entries from patterns of multiple local files. + */ +export const FileGlobNotSupported = { + name: 'FileGlobNotSupported', + title: 'Glob patterns are not supported in the file loader', + message: 'Glob patterns are not supported in the `file` loader. Use the `glob` loader instead.', + hint: `See Astro's built-in file and glob loaders https://docs.astro.build/en/guides/content-collections/#built-in-loaders for supported usage.`, +} satisfies ErrorData; + /** * @docs * @kind heading diff --git a/packages/astro/src/core/errors/index.ts b/packages/astro/src/core/errors/index.ts index 1889a72c67bd..df948d664617 100644 --- a/packages/astro/src/core/errors/index.ts +++ b/packages/astro/src/core/errors/index.ts @@ -8,7 +8,6 @@ export { MarkdownError, isAstroError, } from './errors.js'; -export type { ErrorLocation, ErrorWithMetadata } from './errors.js'; -export { codeFrame } from './printer.js'; +export type { ErrorWithMetadata } from './errors.js'; export { createSafeError, positionAt } from './utils.js'; export { errorMap } from './zod-error-map.js'; diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts index 9d8c7e35744e..76a0f29d7011 100644 --- a/packages/astro/src/core/logger/core.ts +++ b/packages/astro/src/core/logger/core.ts @@ -11,7 +11,7 @@ export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // sam * rather than specific to a single command, function, use, etc. The label will be * shown in the log message to the user, so it should be relevant. */ -export type LoggerLabel = +type LoggerLabel = | 'add' | 'build' | 'check' @@ -53,6 +53,7 @@ export interface LogOptions { // Here be the dragons we've slain: // https://github.com/withastro/astro/issues/2625 // https://github.com/withastro/astro/issues/3309 +/** @lintignore */ export const dateTimeFormat = new Intl.DateTimeFormat([], { hour: '2-digit', minute: '2-digit', @@ -76,7 +77,7 @@ export const levels: Record = { }; /** Full logging API */ -export function log( +function log( opts: LogOptions, level: LoggerLevel, label: string | null, @@ -105,17 +106,17 @@ export function isLogLevelEnabled(configuredLogLevel: LoggerLevel, level: Logger } /** Emit a user-facing message. Useful for UI and other console messages. */ -export function info(opts: LogOptions, label: string | null, message: string, newLine = true) { +function info(opts: LogOptions, label: string | null, message: string, newLine = true) { return log(opts, 'info', label, message, newLine); } /** Emit a warning message. Useful for high-priority messages that aren't necessarily errors. */ -export function warn(opts: LogOptions, label: string | null, message: string, newLine = true) { +function warn(opts: LogOptions, label: string | null, message: string, newLine = true) { return log(opts, 'warn', label, message, newLine); } /** Emit a error message, Useful when Astro can't recover from some error. */ -export function error(opts: LogOptions, label: string | null, message: string, newLine = true) { +function error(opts: LogOptions, label: string | null, message: string, newLine = true) { return log(opts, 'error', label, message, newLine); } diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index ca2dcfae46a4..5a2b510aedd8 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -1,7 +1,6 @@ import { bgCyan, bgGreen, - bgRed, bgWhite, bgYellow, black, @@ -203,16 +202,6 @@ export function success(message: string, tip?: string) { .join('\n'); } -export function failure(message: string, tip?: string) { - const badge = bgRed(black(` error `)); - const headline = red(message); - const footer = tip ? `\n ▶ ${tip}` : undefined; - return ['', `${badge} ${headline}`, footer] - .filter((v) => v !== undefined) - .map((msg) => ` ${msg}`) - .join('\n'); -} - export function actionRequired(message: string) { const badge = bgYellow(black(` action required `)); const headline = yellow(message); diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 082ac8153987..341b8c1e70a7 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -5,6 +5,7 @@ import { ForbiddenRewrite } from '../errors/errors-data.js'; import { AstroError } from '../errors/index.js'; import { apiContextRoutesSymbol } from '../render-context.js'; import { type Pipeline, getParams } from '../render/index.js'; +import { setOriginPathname } from '../routing/rewrite.js'; import { defineMiddleware } from './index.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js @@ -46,6 +47,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { handleContext.request, ); } + const oldPathname = handleContext.url.pathname; const pipeline: Pipeline = Reflect.get(handleContext, apiContextRoutesSymbol); const { routeData, pathname } = await pipeline.tryRewrite( payload, @@ -76,6 +78,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { handleContext.url = new URL(newRequest.url); handleContext.cookies = new AstroCookies(newRequest); handleContext.params = getParams(routeData, pathname); + setOriginPathname(handleContext.request, oldPathname); } return applyHandle(i + 1, handleContext); } else { diff --git a/packages/astro/src/core/preview/static-preview-server.ts b/packages/astro/src/core/preview/static-preview-server.ts index 814566161ec8..50c8405f2434 100644 --- a/packages/astro/src/core/preview/static-preview-server.ts +++ b/packages/astro/src/core/preview/static-preview-server.ts @@ -8,7 +8,7 @@ import * as msg from '../messages.js'; import { getResolvedHostForHttpServer } from './util.js'; import { vitePluginAstroPreview } from './vite-plugin-astro-preview.js'; -export interface PreviewServer { +interface PreviewServer { host?: string; port: number; server: http.Server; diff --git a/packages/astro/src/core/redirects/index.ts b/packages/astro/src/core/redirects/index.ts index 321195cbd54d..f979b30dd6a5 100644 --- a/packages/astro/src/core/redirects/index.ts +++ b/packages/astro/src/core/redirects/index.ts @@ -1,4 +1,3 @@ export { RedirectComponentInstance, RedirectSinglePageBuiltModule } from './component.js'; export { routeIsRedirect } from './helpers.js'; export { getRedirectLocationOrThrow } from './validate.js'; -export { renderRedirect } from './render.js'; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 07462db56662..a162b5e5a5ac 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -153,6 +153,7 @@ export class RenderContext { } const lastNext = async (ctx: APIContext, payload?: RewritePayload) => { if (payload) { + const oldPathname = this.pathname; pipeline.logger.debug('router', 'Called rewriting to:', payload); // we intentionally let the error bubble up const { @@ -193,10 +194,10 @@ export class RenderContext { } this.isRewriting = true; this.url = new URL(this.request.url); - this.cookies = new AstroCookies(this.request); this.params = getParams(routeData, pathname); this.pathname = pathname; this.status = 200; + setOriginPathname(this.request, oldPathname); } let response: Response; @@ -296,6 +297,7 @@ export class RenderContext { async #executeRewrite(reroutePayload: RewritePayload) { this.pipeline.logger.debug('router', 'Calling rewrite: ', reroutePayload); + const oldPathname = this.pathname; const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite( reroutePayload, this.request, @@ -331,6 +333,7 @@ export class RenderContext { this.isRewriting = true; // we found a route and a component, we can change the status code to 200 this.status = 200; + setOriginPathname(this.request, oldPathname); return await this.render(componentInstance); } diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts index b56a2eaf2cc0..bc6d2c6f4a88 100644 --- a/packages/astro/src/core/render/index.ts +++ b/packages/astro/src/core/render/index.ts @@ -1,22 +1,4 @@ -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'; export { loadRenderer } from './renderer.js'; export { Slots } from './slots.js'; - -export interface SSROptions { - /** The pipeline instance */ - pipeline: Pipeline; - /** location of file on disk */ - filePath: URL; - /** the web request (needed for dynamic routes) */ - pathname: string; - /** The runtime component instance */ - preload: ComponentInstance; - /** Request */ - request: Request; - /** optional, in case we need to render something outside a dev server */ - route: RouteData; -} diff --git a/packages/astro/src/core/render/ssr-element.ts b/packages/astro/src/core/render/ssr-element.ts index 7b5cac844b6b..074e01851e66 100644 --- a/packages/astro/src/core/render/ssr-element.ts +++ b/packages/astro/src/core/render/ssr-element.ts @@ -14,7 +14,7 @@ export function createAssetLink(href: string, base?: string, assetsPrefix?: Asse } } -export function createStylesheetElement( +function createStylesheetElement( stylesheet: StylesheetAsset, base?: string, assetsPrefix?: AssetsPrefix, @@ -60,7 +60,7 @@ export function createModuleScriptElement( } } -export function createModuleScriptElementWithSrc( +function createModuleScriptElementWithSrc( src: string, base?: string, assetsPrefix?: AssetsPrefix, diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts index 5e646bb8959d..e2c79211e99a 100644 --- a/packages/astro/src/core/request.ts +++ b/packages/astro/src/core/request.ts @@ -3,7 +3,7 @@ import type { Logger } from './logger/core.js'; type HeaderType = Headers | Record | IncomingHttpHeaders; -export interface CreateRequestOptions { +interface CreateRequestOptions { url: URL | string; clientAddress?: string | undefined; headers: HeaderType; diff --git a/packages/astro/src/core/routing/3xx.ts b/packages/astro/src/core/routing/3xx.ts index 4b800b55f6d5..e2a48ae56530 100644 --- a/packages/astro/src/core/routing/3xx.ts +++ b/packages/astro/src/core/routing/3xx.ts @@ -1,4 +1,4 @@ -export type RedirectTemplate = { +type RedirectTemplate = { from?: string; absoluteLocation: string | URL; status: number; 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 6cca57a59caa..0adade6e977a 100644 --- a/packages/astro/src/core/routing/astro-designed-error-pages.ts +++ b/packages/astro/src/core/routing/astro-designed-error-pages.ts @@ -1,7 +1,7 @@ import notFoundTemplate from '../../template/4xx.js'; import type { ComponentInstance, RoutesList } from '../../types/astro.js'; import type { RouteData } from '../../types/public/internal.js'; -import { DEFAULT_404_COMPONENT, DEFAULT_500_COMPONENT } from '../constants.js'; +import { DEFAULT_404_COMPONENT } from '../constants.js'; export const DEFAULT_404_ROUTE: RouteData = { component: DEFAULT_404_COMPONENT, @@ -18,21 +18,6 @@ export const DEFAULT_404_ROUTE: RouteData = { origin: 'internal', }; -export const DEFAULT_500_ROUTE: RouteData = { - component: DEFAULT_500_COMPONENT, - generate: () => '', - params: [], - pattern: /\/500/, - prerender: false, - pathname: '/500', - segments: [[{ content: '500', dynamic: false, spread: false }]], - type: 'page', - route: '/500', - fallbackRoutes: [], - isIndex: false, - origin: 'internal', -}; - export function ensure404Route(manifest: RoutesList) { if (!manifest.routes.some((route) => route.route === '/404')) { manifest.routes.push(DEFAULT_404_ROUTE); diff --git a/packages/astro/src/core/routing/index.ts b/packages/astro/src/core/routing/index.ts index 1267d67e7553..663d184c2c8a 100644 --- a/packages/astro/src/core/routing/index.ts +++ b/packages/astro/src/core/routing/index.ts @@ -1,4 +1,3 @@ export { createRoutesList } from './manifest/create.js'; -export { deserializeRouteData, serializeRouteData } from './manifest/serialization.js'; -export { matchAllRoutes, matchRoute } from './match.js'; -export { validateDynamicRouteModule, validateGetStaticPathsResult } from './validation.js'; +export { serializeRouteData } from './manifest/serialization.js'; +export { matchAllRoutes } from './match.js'; diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 692d47bebee7..fb9998e5f96f 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -47,7 +47,7 @@ interface Item { const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/; const ROUTE_SPREAD = /^\.{3}.+$/; -export function getParts(part: string, file: string) { +function getParts(part: string, file: string) { const result: RoutePart[] = []; part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => { if (!str) return; @@ -102,7 +102,7 @@ function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[] return true; } -export interface CreateRouteManifestParams { +interface CreateRouteManifestParams { /** Astro Settings object */ settings: AstroSettings; /** Current working directory */ @@ -357,12 +357,12 @@ function createRedirectRoutes( destination = to.destination; } - // URLs that don't start with leading slash should be considered external - if (!destination.startsWith('/')) { - // check if the link starts with http or https; if not, log a warning - if (!/^https?:\/\//.test(destination) && !URL.canParse(destination)) { - throw new AstroError(UnsupportedExternalRedirect); - } + // check if the link starts with http or https; if not, throw an error + if (URL.canParse(destination) && !/^https?:\/\//.test(destination)) { + throw new AstroError({ + ...UnsupportedExternalRedirect, + message: UnsupportedExternalRedirect.message(from, destination), + }); } routes.push({ diff --git a/packages/astro/src/core/routing/request.ts b/packages/astro/src/core/routing/request.ts index f7e917a53cde..0a801b233731 100644 --- a/packages/astro/src/core/routing/request.ts +++ b/packages/astro/src/core/routing/request.ts @@ -3,7 +3,7 @@ */ // Parses multiple header and returns first value if available. -export function getFirstForwardedValue(multiValueHeader?: string | string[] | null) { +function getFirstForwardedValue(multiValueHeader?: string | string[] | null) { return multiValueHeader ?.toString() ?.split(',') diff --git a/packages/astro/src/core/routing/rewrite.ts b/packages/astro/src/core/routing/rewrite.ts index a8d7862f6f0e..5a8bf4b9a3be 100644 --- a/packages/astro/src/core/routing/rewrite.ts +++ b/packages/astro/src/core/routing/rewrite.ts @@ -14,7 +14,7 @@ import { import { createRequest } from '../request.js'; import { DEFAULT_404_ROUTE } from './astro-designed-error-pages.js'; -export type FindRouteToRewrite = { +type FindRouteToRewrite = { payload: RewritePayload; routes: RouteData[]; request: Request; @@ -23,7 +23,7 @@ export type FindRouteToRewrite = { base: AstroConfig['base']; }; -export interface FindRouteToRewriteResult { +interface FindRouteToRewriteResult { routeData: RouteData; newUrl: URL; pathname: string; @@ -80,7 +80,7 @@ export function findRouteToRewrite({ } // Convert '/' to '' for trailingSlash: 'never' - if (pathname === '/' && !shouldAppendSlash) { + if (pathname === '/' && base !== '/' && !shouldAppendSlash) { pathname = ''; } 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 8cfbd69965c9..7c719c2caf82 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 @@ -4,7 +4,7 @@ import type { AstroPluginOptions } from '../../types/astro.js'; import type { AstroPluginMetadata } from '../../vite-plugin-astro/index.js'; export const VIRTUAL_ISLAND_MAP_ID = '@astro-server-islands'; -export const RESOLVED_VIRTUAL_ISLAND_MAP_ID = '\0' + VIRTUAL_ISLAND_MAP_ID; +const RESOLVED_VIRTUAL_ISLAND_MAP_ID = '\0' + VIRTUAL_ISLAND_MAP_ID; const serverIslandPlaceholder = "'$$server-islands$$'"; export function vitePluginServerIslands({ settings, logger }: AstroPluginOptions): VitePlugin { diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index b6e0c959141b..77e7c0b6f464 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -36,7 +36,7 @@ import { createRoutesList } from '../routing/index.js'; import { ensureProcessNodeEnv } from '../util.js'; import { normalizePath } from '../viteUtils.js'; -export type SyncOptions = { +type SyncOptions = { mode: string; /** * @internal only used for testing diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 22be8d0ba240..9269bf17da79 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -100,7 +100,7 @@ export function viteID(filePath: URL): string { } export const VALID_ID_PREFIX = `/@id/`; -export const NULL_BYTE_PLACEHOLDER = `__x00__`; +const NULL_BYTE_PLACEHOLDER = `__x00__`; // Strip valid id prefix and replace null byte placeholder. Both are prepended to resolved ids // as they are not valid browser import specifiers (by the Vite's importAnalysis plugin) diff --git a/packages/astro/src/events/session.ts b/packages/astro/src/events/session.ts index 513afaf63a0b..8bc400d7401b 100644 --- a/packages/astro/src/events/session.ts +++ b/packages/astro/src/events/session.ts @@ -17,7 +17,7 @@ type ConfigInfoRecord = Record; type ConfigInfoBase = { [alias in keyof AstroUserConfig]: ConfigInfoValue | ConfigInfoRecord; }; -export interface ConfigInfo extends ConfigInfoBase { +interface ConfigInfo extends ConfigInfoBase { build: ConfigInfoRecord; image: ConfigInfoRecord; markdown: ConfigInfoRecord; diff --git a/packages/astro/src/integrations/features-validation.ts b/packages/astro/src/integrations/features-validation.ts index 9383c76b2e7c..0edcb7bb9139 100644 --- a/packages/astro/src/integrations/features-validation.ts +++ b/packages/astro/src/integrations/features-validation.ts @@ -96,7 +96,7 @@ export function validateSupportedFeatures( return validationResult; } -export function unwrapSupportKind(supportKind?: AdapterSupport): AdapterSupportsKind | undefined { +function unwrapSupportKind(supportKind?: AdapterSupport): AdapterSupportsKind | undefined { if (!supportKind) { return undefined; } @@ -104,7 +104,7 @@ export function unwrapSupportKind(supportKind?: AdapterSupport): AdapterSupports return typeof supportKind === 'object' ? supportKind.support : supportKind; } -export function getSupportMessage(supportKind: AdapterSupport): string | undefined { +function getSupportMessage(supportKind: AdapterSupport): string | undefined { return typeof supportKind === 'object' ? supportKind.message : undefined; } diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts index e344867392b7..eaedb1ed91e4 100644 --- a/packages/astro/src/preferences/index.ts +++ b/packages/astro/src/preferences/index.ts @@ -16,13 +16,13 @@ type DotKeys = T extends object }[keyof T] : never; -export type GetDotKey< +type GetDotKey< T extends Record, K extends string, > = K extends `${infer U}.${infer Rest}` ? GetDotKey : T[K]; -export type PreferenceLocation = 'global' | 'project'; -export interface PreferenceOptions { +type PreferenceLocation = 'global' | 'project'; +interface PreferenceOptions { location?: PreferenceLocation; /** * If `true`, the server will be reloaded after setting the preference. @@ -40,7 +40,7 @@ type DeepPartial = T extends object : T; export type PreferenceKey = DotKeys; -export interface PreferenceList extends Record> { +interface PreferenceList extends Record> { fromAstroConfig: DeepPartial; defaults: PublicPreferences; } diff --git a/packages/astro/src/type-utils.ts b/packages/astro/src/type-utils.ts index 1aa816aadb81..206d38dfd08d 100644 --- a/packages/astro/src/type-utils.ts +++ b/packages/astro/src/type-utils.ts @@ -21,14 +21,6 @@ export type OmitPreservingIndexSignature = { [P in keyof T as Exclude]: T[P]; }; -// Transform a string into its kebab case equivalent (camelCase -> kebab-case). Useful for CSS-in-JS to CSS. -export type Kebab = T extends `${infer F}${infer R}` - ? Kebab ? '' : '-'}${Lowercase}`> - : A; - -// Transform every key of an object to its kebab case equivalent using the above utility -export type KebabKeys = { [K in keyof T as K extends string ? Kebab : K]: T[K] }; - // Similar to `keyof`, gets the type of all the values of an object export type ValueOf = T[keyof T]; diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index a2f9d3253ea9..4e7acda3bd5f 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -363,9 +363,9 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * @see output * @description * - * Deploy to your favorite server, serverless, or edge host with build adapters. Import one of our first-party adapters for [Netlify](https://docs.astro.build/en/guides/deploy/netlify/#adapter-for-ssr), [Vercel](https://docs.astro.build/en/guides/deploy/vercel/#adapter-for-ssr), and more to engage Astro SSR. + * Deploy to your favorite server, serverless, or edge host with build adapters. Import one of our first-party adapters ([Cloudflare](/en/guides/integrations-guide/cloudflare/), [Netlify](/en/guides/integrations-guide/netlify/), [Node.js](/en/guides/integrations-guide/node/), [Vercel](/en/guides/integrations-guide/vercel/)) or explore [community adapters](https://astro.build/integrations/2/?search=&categories%5B%5D=adapters) to enable on-demand rendering in your Astro project. * - * [See our On-demand Rendering guide](https://docs.astro.build/en/guides/on-demand-rendering/) for more on SSR, and [our deployment guides](https://docs.astro.build/en/guides/deploy/) for a complete list of hosts. + * See our [on-demand rendering guide](/en/guides/on-demand-rendering/) for more on Astro's server rendering options. * * ```js * import netlify from '@astrojs/netlify'; @@ -906,6 +906,26 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * ``` */ + /** + * @docs + * @name server.allowedHosts + * @type {string[] | true} + * @default `[]` + * @version 5.4.0 + * @description + * + * A list of hostnames that Astro is allowed to respond to. When the value is set to `true`, any + * hostname is allowed. + * + * ```js + * { + * server: { + * allowedHosts: ['staging.example.com', 'qa.example.com'] + * } + * } + * ``` + */ + /** * @docs * @name server.open @@ -1311,7 +1331,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * @description * The default layout type for responsive images. Can be overridden by the `layout` prop on the image component. * Requires the `experimental.responsiveImages` flag to be enabled. - * - `responsive` - The image will scale to fit the container, maintaining its aspect ratio, but will not exceed the specified dimensions. + * - `constrained` - The image will scale to fit the container, maintaining its aspect ratio, but will not exceed the specified dimensions. * - `fixed` - The image will maintain its original dimensions. * - `full-width` - The image will scale to fit the container, maintaining its aspect ratio. */ @@ -2076,14 +2096,14 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * } * ``` * - * When enabled, you can pass a `layout` props to any `` or `` component to create a responsive image. When a layout is set, images have automatically generated `srcset` and `sizes` attributes based on the image's dimensions and the layout type. Images with `responsive` and `full-width` layouts will have styles applied to ensure they resize according to their container. + * When enabled, you can pass a `layout` props to any `` or `` component to create a responsive image. When a layout is set, images have automatically generated `srcset` and `sizes` attributes based on the image's dimensions and the layout type. Images with `constrained` and `full-width` layouts will have styles applied to ensure they resize according to their container. * * ```astro title=MyComponent.astro * --- * import { Image, Picture } from 'astro:assets'; * import myImage from '../assets/my_image.png'; * --- - * A description of my image. + * A description of my image. * * ``` * This `` component will generate the following HTML output: @@ -2105,31 +2125,28 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * fetchpriority="auto" * width="800" * height="600" - * style="--w: 800; --h: 600; --fit: cover; --pos: center;" - * data-astro-image="responsive" + * style="--fit: cover; --pos: center;" + * data-astro-image="constrained" * > * ``` * * The following styles are applied to ensure the images resize correctly: * * ```css title="Responsive Image Styles" - * [data-astro-image] { - * width: 100%; - * height: auto; - * object-fit: var(--fit); - * object-position: var(--pos); - * aspect-ratio: var(--w) / var(--h) + * + * :where([data-astro-image]) { + * object-fit: var(--fit); + * object-position: var(--pos); * } * - * [data-astro-image=responsive] { - * max-width: calc(var(--w) * 1px); - * max-height: calc(var(--h) * 1px) + * :where([data-astro-image='full-width']) { + * width: 100%; * } * - * [data-astro-image=fixed] { - * width: calc(var(--w) * 1px); - * height: calc(var(--h) * 1px) + * :where([data-astro-image='constrained']) { + * max-width: 100%; * } + * * ``` * You can enable responsive images for all `` and `` components by setting `image.experimentalLayout` with a default value. This can be overridden by the `layout` prop on each component. * @@ -2138,7 +2155,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * { * image: { * // Used for all `` and `` components unless overridden - * experimentalLayout: 'responsive', + * experimentalLayout: 'constrained', * }, * experimental: { * responsiveImages: true, @@ -2163,12 +2180,12 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * * These are additional properties available to the `` and `` components when responsive images are enabled: * - * - `layout`: The layout type for the image. Can be `responsive`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`. + * - `layout`: The layout type for the image. Can be `constrained`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`. * - `fit`: Defines how the image should be cropped if the aspect ratio is changed. Values match those of CSS `object-fit`. Defaults to `cover`, or the value of `image.experimentalObjectFit` if set. * - `position`: Defines the position of the image crop if the aspect ratio is changed. Values match those of CSS `object-position`. Defaults to `center`, or the value of `image.experimentalObjectPosition` if set. * - `priority`: If set, eagerly loads the image. Otherwise images will be lazy-loaded. Use this for your largest above-the-fold image. Defaults to `false`. * - * The `widths` and `sizes` attributes are automatically generated based on the image's dimensions and the layout type, and in most cases should not be set manually. The generated `sizes` attribute for `responsive` and `full-width` images + * The `widths` and `sizes` attributes are automatically generated based on the image's dimensions and the layout type, and in most cases should not be set manually. The generated `sizes` attribute for `constrained` and `full-width` images * is based on the assumption that the image is displayed at close to the full width of the screen when the viewport is smaller than the image's width. If it is significantly different (e.g. if it's in a multi-column layout on small screens) you may need to adjust the `sizes` attribute manually for best results. * * The `densities` attribute is not compatible with responsive images and will be ignored if set. diff --git a/packages/astro/src/types/typed-emitter.ts b/packages/astro/src/types/typed-emitter.ts index 43139bd4e687..771907bf0863 100644 --- a/packages/astro/src/types/typed-emitter.ts +++ b/packages/astro/src/types/typed-emitter.ts @@ -4,7 +4,7 @@ * https://github.com/andywer/typed-emitter/blob/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4/LICENSE */ -export type EventMap = { +type EventMap = { [key: string]: (...args: any[]) => void; }; diff --git a/packages/astro/src/vite-plugin-astro-server/controller.ts b/packages/astro/src/vite-plugin-astro-server/controller.ts index 9ba345d69a20..06c8796e7e39 100644 --- a/packages/astro/src/vite-plugin-astro-server/controller.ts +++ b/packages/astro/src/vite-plugin-astro-server/controller.ts @@ -16,7 +16,7 @@ export interface DevServerController { onHMRError: LoaderEvents['hmr-error']; } -export type CreateControllerParams = +type CreateControllerParams = | { loader: ModuleLoader; } @@ -84,7 +84,7 @@ function createLoaderController(loader: ModuleLoader): DevServerController { return controller; } -export interface RunWithErrorHandlingParams { +interface RunWithErrorHandlingParams { controller: DevServerController; pathname: string; run: () => Promise; diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 88b8b0a81b1a..3e64b560a87f 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -26,7 +26,7 @@ import { handleRequest } from './request.js'; import { setRouteError } from './server-state.js'; import { trailingSlashMiddleware } from './trailing-slash.js'; -export interface AstroPluginOptions { +interface AstroPluginOptions { settings: AstroSettings; logger: Logger; fs: typeof fs; diff --git a/packages/astro/src/vite-plugin-astro-server/response.ts b/packages/astro/src/vite-plugin-astro-server/response.ts index ac7883c6028f..a1747c2dd54c 100644 --- a/packages/astro/src/vite-plugin-astro-server/response.ts +++ b/packages/astro/src/vite-plugin-astro-server/response.ts @@ -1,29 +1,11 @@ import type http from 'node:http'; import { Http2ServerResponse } from 'node:http2'; -import type { ErrorWithMetadata } from '../core/errors/index.js'; -import type { ModuleLoader } from '../core/module-loader/index.js'; - import { Readable } from 'node:stream'; import { getSetCookiesFromResponse } from '../core/cookies/index.js'; import { getViteErrorPayload } from '../core/errors/dev/index.js'; +import type { ErrorWithMetadata } from '../core/errors/index.js'; +import type { ModuleLoader } from '../core/module-loader/index.js'; import { redirectTemplate } from '../core/routing/3xx.js'; -import notFoundTemplate from '../template/4xx.js'; - -export async function handle404Response( - origin: string, - req: http.IncomingMessage, - res: http.ServerResponse, -) { - const pathname = decodeURI(new URL(origin + req.url).pathname); - - const html = notFoundTemplate({ - statusCode: 404, - title: 'Not found', - tabTitle: '404: Not Found', - pathname, - }); - writeHtmlResponse(res, 404, html); -} export async function handle500Response( loader: ModuleLoader, diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index cbaa768c414f..8c52ed4dc218 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -30,7 +30,7 @@ type AsyncReturnType Promise> = T extends ( ? R : any; -export interface MatchedRoute { +interface MatchedRoute { route: RouteData; filePath: URL; resolvedPathname: string; diff --git a/packages/astro/src/vite-plugin-astro-server/server-state.ts b/packages/astro/src/vite-plugin-astro-server/server-state.ts index 94f1fe8a5027..5e6c13111dc1 100644 --- a/packages/astro/src/vite-plugin-astro-server/server-state.ts +++ b/packages/astro/src/vite-plugin-astro-server/server-state.ts @@ -1,6 +1,6 @@ -export type ErrorState = 'fresh' | 'error'; +type ErrorState = 'fresh' | 'error'; -export interface RouteState { +interface RouteState { state: ErrorState; error?: Error; } @@ -18,10 +18,6 @@ export function createServerState(): ServerState { }; } -export function hasAnyFailureState(serverState: ServerState) { - return serverState.state !== 'fresh'; -} - export function setRouteError(serverState: ServerState, pathname: string, error: Error) { if (serverState.routes.has(pathname)) { const routeState = serverState.routes.get(pathname)!; diff --git a/packages/astro/src/vite-plugin-astro/hmr.ts b/packages/astro/src/vite-plugin-astro/hmr.ts index 93fecc7aa1f0..1dd09d143cca 100644 --- a/packages/astro/src/vite-plugin-astro/hmr.ts +++ b/packages/astro/src/vite-plugin-astro/hmr.ts @@ -3,7 +3,7 @@ import type { Logger } from '../core/logger/core.js'; import type { CompileMetadata } from './types.js'; import { frontmatterRE } from './utils.js'; -export interface HandleHotUpdateOptions { +interface HandleHotUpdateOptions { logger: Logger; astroFileToCompileMetadata: Map; } diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index 21d9dcfb1486..8edbc98ae31c 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -2,11 +2,7 @@ import type { SourceDescription } from 'rollup'; import type * as vite from 'vite'; import type { Logger } from '../core/logger/core.js'; import type { AstroSettings } from '../types/astro.js'; -import type { - PluginCssMetadata as AstroPluginCssMetadata, - PluginMetadata as AstroPluginMetadata, - CompileMetadata, -} from './types.js'; +import type { PluginMetadata as AstroPluginMetadata, CompileMetadata } from './types.js'; import { defaultClientConditions, defaultServerConditions, normalizePath } from 'vite'; import type { AstroConfig } from '../types/public/config.js'; @@ -16,7 +12,7 @@ import { handleHotUpdate } from './hmr.js'; import { parseAstroRequest } from './query.js'; import { loadId } from './utils.js'; export { getAstroMetadata } from './metadata.js'; -export type { AstroPluginMetadata, AstroPluginCssMetadata }; +export type { AstroPluginMetadata }; interface AstroPluginOptions { settings: AstroSettings; @@ -138,17 +134,15 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl return { code: result.code, - // This metadata is used by `cssScopeToPlugin` to remove this module from the bundle - // if the `filename` default export (the Astro component) is unused. + // `vite.cssScopeTo` is a Vite feature that allows this CSS to be treeshaken + // if the Astro component's default export is not used meta: result.isGlobal ? undefined - : ({ - astroCss: { - cssScopeTo: { - [filename]: ['default'], - }, + : { + vite: { + cssScopeTo: [filename, 'default'], }, - } satisfies AstroPluginCssMetadata), + }, }; } case 'script': { diff --git a/packages/astro/src/vite-plugin-astro/query.ts b/packages/astro/src/vite-plugin-astro/query.ts index c9829de3f6a8..266657d69790 100644 --- a/packages/astro/src/vite-plugin-astro/query.ts +++ b/packages/astro/src/vite-plugin-astro/query.ts @@ -1,4 +1,4 @@ -export interface AstroQuery { +interface AstroQuery { astro?: boolean; src?: boolean; type?: 'script' | 'template' | 'style' | 'custom'; @@ -7,7 +7,7 @@ export interface AstroQuery { raw?: boolean; } -export interface ParsedRequestResult { +interface ParsedRequestResult { filename: string; query: AstroQuery; } diff --git a/packages/astro/src/vite-plugin-astro/types.ts b/packages/astro/src/vite-plugin-astro/types.ts index d85fd6483064..0cb0c70c0799 100644 --- a/packages/astro/src/vite-plugin-astro/types.ts +++ b/packages/astro/src/vite-plugin-astro/types.ts @@ -2,7 +2,7 @@ import type { HoistedScript, TransformResult } from '@astrojs/compiler'; import type { CompileCssResult } from '../core/compile/types.js'; import type { PropagationHint } from '../types/public/internal.js'; -export interface PageOptions { +interface PageOptions { prerender?: boolean; } @@ -18,27 +18,6 @@ export interface PluginMetadata { }; } -export interface PluginCssMetadata { - astroCss: { - /** - * For Astro CSS virtual modules, it can scope to the main Astro module's default export - * so that if those exports are treeshaken away, the CSS module will also be treeshaken. - * - * Example config if the CSS id is `/src/Foo.astro?astro&type=style&lang.css`: - * ```js - * cssScopeTo: { - * '/src/Foo.astro': ['default'] - * } - * ``` - * - * The above is the only config we use today, but we're exposing as a `Record` to follow the - * upstream Vite implementation: https://github.com/vitejs/vite/pull/16058. When/If that lands, - * we can also remove our custom implementation. - */ - cssScopeTo: Record; - }; -} - export interface CompileMetadata { /** Used for HMR to compare code changes */ originalCode: string; diff --git a/packages/astro/src/vite-plugin-load-fallback/index.ts b/packages/astro/src/vite-plugin-load-fallback/index.ts index f7cc9cd1c907..02385207da59 100644 --- a/packages/astro/src/vite-plugin-load-fallback/index.ts +++ b/packages/astro/src/vite-plugin-load-fallback/index.ts @@ -6,7 +6,7 @@ import { cleanUrl } from '../vite-plugin-utils/index.js'; type NodeFileSystemModule = typeof nodeFs; -export interface LoadFallbackPluginParams { +interface LoadFallbackPluginParams { fs?: NodeFileSystemModule; root: URL; } diff --git a/packages/astro/src/vite-plugin-markdown/images.ts b/packages/astro/src/vite-plugin-markdown/images.ts index b99d1af233b2..c98d3c2ca04f 100644 --- a/packages/astro/src/vite-plugin-markdown/images.ts +++ b/packages/astro/src/vite-plugin-markdown/images.ts @@ -15,7 +15,7 @@ export function getMarkdownCodeForImages( const imageSources = {}; ${localImagePaths .map((entry) => { - const rawUrl = JSON.stringify(entry.raw); + const rawUrl = JSON.stringify(entry.raw).replace(/'/g, '''); return `{ const regex = new RegExp('__ASTRO_IMAGE_="([^"]*' + ${rawUrl.replace( /[.*+?^${}()|[\]\\]/g, @@ -25,7 +25,7 @@ export function getMarkdownCodeForImages( let occurrenceCounter = 0; while ((match = regex.exec(html)) !== null) { const matchKey = ${rawUrl} + '_' + occurrenceCounter; - const imageProps = JSON.parse(match[1].replace(/"/g, '"')); + const imageProps = JSON.parse(match[1].replace(/"/g, '"').replace(/'/g, "'")); const { src, ...props } = imageProps; imageSources[matchKey] = await getImage({src: Astro__${entry.safeName}, ...props}); occurrenceCounter++; @@ -35,7 +35,7 @@ export function getMarkdownCodeForImages( .join('\n')} ${remoteImagePaths .map((raw) => { - const rawUrl = JSON.stringify(raw); + const rawUrl = JSON.stringify(raw).replace(/'/g, '''); return `{ const regex = new RegExp('__ASTRO_IMAGE_="([^"]*' + ${rawUrl.replace( /[.*+?^${}()|[\]\\]/g, @@ -45,7 +45,7 @@ export function getMarkdownCodeForImages( let occurrenceCounter = 0; while ((match = regex.exec(html)) !== null) { const matchKey = ${rawUrl} + '_' + occurrenceCounter; - const props = JSON.parse(match[1].replace(/"/g, '"')); + const props = JSON.parse(match[1].replace(/"/g, '"').replace(/'/g, "'")); imageSources[matchKey] = await getImage(props); occurrenceCounter++; } diff --git a/packages/astro/src/vite-plugin-scanner/index.ts b/packages/astro/src/vite-plugin-scanner/index.ts index a86205484960..4f1700dfe79f 100644 --- a/packages/astro/src/vite-plugin-scanner/index.ts +++ b/packages/astro/src/vite-plugin-scanner/index.ts @@ -9,7 +9,7 @@ import { isEndpoint, isPage } from '../core/util.js'; import { normalizePath, rootRelativePath } from '../core/viteUtils.js'; import type { AstroSettings, RoutesList } from '../types/astro.js'; -export interface AstroPluginScannerOptions { +interface AstroPluginScannerOptions { settings: AstroSettings; logger: Logger; routesList: RoutesList; diff --git a/packages/astro/templates/env.mjs b/packages/astro/templates/env.mjs index 6526033514de..c314b4392aca 100644 --- a/packages/astro/templates/env.mjs +++ b/packages/astro/templates/env.mjs @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ // @ts-check import { schema } from 'virtual:astro:env/internal'; import { diff --git a/packages/astro/test/astro-cookies.test.js b/packages/astro/test/astro-cookies.test.js index 482474b54be2..e375cf1e8a74 100644 --- a/packages/astro/test/astro-cookies.test.js +++ b/packages/astro/test/astro-cookies.test.js @@ -194,5 +194,12 @@ describe('Astro.cookies', () => { assert.equal(response.status, 200); assert.match(response.headers.get('Set-Cookie'), /test=value/); }); + + it('can set cookies in a rewritten endpoint request from middleware', async () => { + const request = new Request('http://example.com/rewrite-me'); + const response = await app.render(request, { addCookieHeader: true }); + assert.equal(response.status, 200); + assert.match(response.headers.get('Set-Cookie'), /my_cookie=value/); + }); }); }); diff --git a/packages/astro/test/build-concurrency.test.js b/packages/astro/test/build-concurrency.test.js new file mode 100644 index 000000000000..87a0b4912856 --- /dev/null +++ b/packages/astro/test/build-concurrency.test.js @@ -0,0 +1,29 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture } from './test-utils.js'; + +describe('Building with concurrency > 1', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/build-concurrency/', + build: { + concurrency: 2, + }, + }); + }); + + it('Errors and exits', async () => { + let built = false; + try { + await fixture.build(); + built = true; + } catch (err) { + assert.match(err.message, /This is a test/); + } + + assert.equal(built, false, 'Build should not complete'); + }); +}); diff --git a/packages/astro/test/core-image-layout.test.js b/packages/astro/test/core-image-layout.test.js index bb0e03c481a6..16b62b95f9c3 100644 --- a/packages/astro/test/core-image-layout.test.js +++ b/packages/astro/test/core-image-layout.test.js @@ -86,22 +86,16 @@ describe('astro:image:layout', () => { it('sets the style', () => { let $img = $('#local-both img'); - assert.match($img.attr('style'), /--w: 300/); - assert.match($img.attr('style'), /--h: 400/); - assert.equal($img.data('astro-image'), 'responsive'); + assert.equal($img.data('astro-image'), 'constrained'); }); it('sets the style when no dimensions set', () => { let $img = $('#local img'); - assert.match($img.attr('style'), /--w: 2316/); - assert.match($img.attr('style'), /--h: 1544/); - assert.equal($img.data('astro-image'), 'responsive'); + assert.equal($img.data('astro-image'), 'constrained'); }); it('sets style for fixed image', () => { let $img = $('#local-fixed img'); - assert.match($img.attr('style'), /--w: 800/); - assert.match($img.attr('style'), /--h: 600/); assert.equal($img.data('astro-image'), 'fixed'); }); @@ -396,8 +390,8 @@ describe('astro:image:layout', () => { it('adds inline style attributes', () => { let $img = $('#picture-attributes img'); const style = $img.attr('style'); - assert.match(style, /--w:/); - assert.match(style, /--h:/); + assert.match(style, /--fit:/); + assert.match(style, /--pos:/); }); it('passing in style as an object', () => { @@ -645,22 +639,16 @@ describe('astro:image:layout', () => { it('sets the style', () => { let $img = $('#local-both img'); - assert.match($img.attr('style'), /--w: 300/); - assert.match($img.attr('style'), /--h: 400/); - assert.equal($img.data('astro-image'), 'responsive'); + assert.equal($img.data('astro-image'), 'constrained'); }); it('sets the style when no dimensions set', () => { let $img = $('#local img'); - assert.match($img.attr('style'), /--w: 2316/); - assert.match($img.attr('style'), /--h: 1544/); - assert.equal($img.data('astro-image'), 'responsive'); + assert.equal($img.data('astro-image'), 'constrained'); }); it('sets style for fixed image', () => { let $img = $('#local-fixed img'); - assert.match($img.attr('style'), /--w: 800/); - assert.match($img.attr('style'), /--h: 600/); assert.equal($img.data('astro-image'), 'fixed'); }); diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index 10ca9b686412..353f544a75cb 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -493,7 +493,7 @@ describe('astro:image', () => { $ = cheerio.load(html); let $img = $('img'); - assert.equal($img.length, 3); + assert.equal($img.length, 4); $img.each((_, el) => { assert.equal(el.attribs.src?.startsWith('/_image'), true); }); diff --git a/packages/astro/test/fixtures/astro-cookies/src/middleware.ts b/packages/astro/test/fixtures/astro-cookies/src/middleware.ts new file mode 100644 index 000000000000..c5df6f851f7c --- /dev/null +++ b/packages/astro/test/fixtures/astro-cookies/src/middleware.ts @@ -0,0 +1,9 @@ +import { defineMiddleware } from "astro:middleware"; + +export const onRequest = defineMiddleware((ctx, next) => { + if (ctx.url.pathname === "/rewrite-me") { + return next("/rewrite-target"); + } else { + return next(); + } +}) diff --git a/packages/astro/test/fixtures/build-concurrency/package.json b/packages/astro/test/fixtures/build-concurrency/package.json new file mode 100644 index 000000000000..d5dfaf4598e9 --- /dev/null +++ b/packages/astro/test/fixtures/build-concurrency/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/concurrency", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/build-concurrency/src/pages/index.astro b/packages/astro/test/fixtures/build-concurrency/src/pages/index.astro new file mode 100644 index 000000000000..3ce79d51ca0d --- /dev/null +++ b/packages/astro/test/fixtures/build-concurrency/src/pages/index.astro @@ -0,0 +1,5 @@ +--- +throw Error('This is a test') +--- + +

Hello world!

diff --git a/packages/astro/test/fixtures/build-concurrency/src/pages/page2.astro b/packages/astro/test/fixtures/build-concurrency/src/pages/page2.astro new file mode 100644 index 000000000000..ecfc83400ea5 --- /dev/null +++ b/packages/astro/test/fixtures/build-concurrency/src/pages/page2.astro @@ -0,0 +1,4 @@ +--- +--- + +

Page 2

diff --git a/packages/astro/test/fixtures/core-image-layout/astro.config.mjs b/packages/astro/test/fixtures/core-image-layout/astro.config.mjs index b32208e5f67a..b819fd8778a4 100644 --- a/packages/astro/test/fixtures/core-image-layout/astro.config.mjs +++ b/packages/astro/test/fixtures/core-image-layout/astro.config.mjs @@ -3,7 +3,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ image: { - experimentalLayout: 'responsive', + experimentalLayout: 'constrained', }, experimental: { diff --git a/packages/astro/test/fixtures/core-image/src/assets/penguin with apostrophe'.jpg b/packages/astro/test/fixtures/core-image/src/assets/penguin with apostrophe'.jpg new file mode 100644 index 000000000000..1a8986ac5092 Binary files /dev/null and b/packages/astro/test/fixtures/core-image/src/assets/penguin with apostrophe'.jpg differ diff --git a/packages/astro/test/fixtures/core-image/src/pages/specialChars.md b/packages/astro/test/fixtures/core-image/src/pages/specialChars.md index a5f22cb59e45..ab52da3c5d06 100644 --- a/packages/astro/test/fixtures/core-image/src/pages/specialChars.md +++ b/packages/astro/test/fixtures/core-image/src/pages/specialChars.md @@ -1,5 +1,6 @@ ![C++](../assets/c++.png) ![Penguin with space](../assets/penguin%20with%20space.jpg) ![Penguin with percent](../assets/penguin%20with%20percent%25.jpg) +![Penguin with apostrophe](../assets/penguin%20with%20apostrophe%27.jpg) Image with special characters in file name worked. diff --git a/packages/astro/test/fixtures/reroute/src/pages/index.astro b/packages/astro/test/fixtures/reroute/src/pages/index.astro index 91a6fd0fb0fc..64932710db3c 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/index.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/index.astro @@ -1,5 +1,6 @@ --- const auth = Astro.locals.auth; +const origin = Astro.originPathname; --- @@ -8,5 +9,6 @@ const auth = Astro.locals.auth;

Index

{auth ?

Called auth

: ""} +

Origin: {origin}

diff --git a/packages/astro/test/fixtures/rewrite-issue-13633/astro.config.mjs b/packages/astro/test/fixtures/rewrite-issue-13633/astro.config.mjs new file mode 100644 index 000000000000..cb4250b192d4 --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-issue-13633/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + base: '', + trailingSlash: 'never', + output: 'server' +}); diff --git a/packages/astro/test/fixtures/rewrite-issue-13633/package.json b/packages/astro/test/fixtures/rewrite-issue-13633/package.json new file mode 100644 index 000000000000..58b8a21e221f --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-issue-13633/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/rewrite-issue13633", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/rewrite-issue-13633/src/middleware.js b/packages/astro/test/fixtures/rewrite-issue-13633/src/middleware.js new file mode 100644 index 000000000000..4b3de7f2847b --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-issue-13633/src/middleware.js @@ -0,0 +1,4 @@ +export const onRequest = async (ctx, next) => { + const response = await next('/'); + return response; +}; diff --git a/packages/astro/test/fixtures/rewrite-issue-13633/src/pages/about.astro b/packages/astro/test/fixtures/rewrite-issue-13633/src/pages/about.astro new file mode 100644 index 000000000000..17712c0f72fc --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-issue-13633/src/pages/about.astro @@ -0,0 +1,12 @@ +--- + +--- + + + + About page + + +

About page

+ + diff --git a/packages/astro/test/fixtures/rewrite-issue-13633/src/pages/index.astro b/packages/astro/test/fixtures/rewrite-issue-13633/src/pages/index.astro new file mode 100644 index 000000000000..336e7fcfaeda --- /dev/null +++ b/packages/astro/test/fixtures/rewrite-issue-13633/src/pages/index.astro @@ -0,0 +1,12 @@ +--- + +--- + + + + Index page + + +

Index page

+ + diff --git a/packages/astro/test/fonts.test.js b/packages/astro/test/fonts.test.js index a4364aa62a78..4f8aabdb146c 100644 --- a/packages/astro/test/fonts.test.js +++ b/packages/astro/test/fonts.test.js @@ -1,76 +1,101 @@ -import assert from 'node:assert/strict'; // @ts-check -import { after, before, describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { readdir } from 'node:fs/promises'; +import { describe, it } from 'node:test'; import { fontProviders } from 'astro/config'; import * as cheerio from 'cheerio'; import { loadFixture } from './test-utils.js'; -describe('astro:fonts', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {import('./test-utils.js').DevServer} */ - let devServer; +/** + * @param {Omit} inlineConfig + */ +async function createDevFixture(inlineConfig) { + const fixture = await loadFixture({ root: './fixtures/fonts/', ...inlineConfig }); + const devServer = await fixture.startDevServer(); - describe(' component', () => { - // TODO: remove once fonts are stabilized - describe('Fonts are not enabled', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/fonts/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { + return { + fixture, + devServer, + run: async (/** @type {() => any} */ cb) => { + try { + return await cb(); + } finally { await devServer.stop(); - }); + } + }, + }; +} +/** + * @param {Omit} inlineConfig + */ +async function createBuildFixture(inlineConfig) { + const fixture = await loadFixture({ root: './fixtures/fonts/', ...inlineConfig }); + await fixture.build({}); - it('Throws an error if fonts are not enabled', async () => { - const res = await fixture.fetch('/'); - const body = await res.text(); - assert.equal( - body.includes('