diff --git a/.changeset/chilly-berries-press.md b/.changeset/chilly-berries-press.md new file mode 100644 index 000000000000..9932af210b54 --- /dev/null +++ b/.changeset/chilly-berries-press.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: upgrade to cookie v1. Cookie names must now contain only ASCII characters diff --git a/.changeset/clean-papers-give.md b/.changeset/clean-papers-give.md new file mode 100644 index 000000000000..44d488dfee8f --- /dev/null +++ b/.changeset/clean-papers-give.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': major +'@sveltejs/adapter-vercel': major +--- + +chore: use `rolldown` for edge function bundling diff --git a/.changeset/cold-carrots-raise.md b/.changeset/cold-carrots-raise.md new file mode 100644 index 000000000000..5b77b8c63f25 --- /dev/null +++ b/.changeset/cold-carrots-raise.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/package': major +'@sveltejs/kit': major +'@sveltejs/enhanced-img': major +--- + +breaking: require Node 22 or newer diff --git a/.changeset/config.json b/.changeset/config.json index b5a4060d1a52..d81c3e9bb053 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,7 +4,7 @@ "commit": false, "linked": [], "access": "public", - "baseBranch": "main", + "baseBranch": "version-3", "bumpVersionsWithWorkspaceProtocolOnly": true, "ignore": ["!(@sveltejs/*)"] } diff --git a/.changeset/cuddly-radios-brush.md b/.changeset/cuddly-radios-brush.md new file mode 100644 index 000000000000..96091fec4f61 --- /dev/null +++ b/.changeset/cuddly-radios-brush.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove the `preloadStrategy` option. `modulepreload` will always be used diff --git a/.changeset/cuddly-tigers-attend.md b/.changeset/cuddly-tigers-attend.md new file mode 100644 index 000000000000..2d1d9c4b5929 --- /dev/null +++ b/.changeset/cuddly-tigers-attend.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: default the cookie `path` option to `'/'` diff --git a/.changeset/cyan-zoos-love.md b/.changeset/cyan-zoos-love.md new file mode 100644 index 000000000000..391ee6403465 --- /dev/null +++ b/.changeset/cyan-zoos-love.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `@sveltejs/kit/node/polyfills` diff --git a/.changeset/early-worlds-slide.md b/.changeset/early-worlds-slide.md new file mode 100644 index 000000000000..f9280d7ee391 --- /dev/null +++ b/.changeset/early-worlds-slide.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require `@sveltejs/vite-plugin-svelte` v7 diff --git a/.changeset/easy-shrimps-like.md b/.changeset/easy-shrimps-like.md new file mode 100644 index 000000000000..07f74e1d4a1c --- /dev/null +++ b/.changeset/easy-shrimps-like.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/enhanced-img': minor +--- + +breaking: require Vite 8 and `vite-plugin-svelte` 7 diff --git a/.changeset/eighty-paws-love.md b/.changeset/eighty-paws-love.md new file mode 100644 index 000000000000..b301fa8f1b2c --- /dev/null +++ b/.changeset/eighty-paws-love.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `createEntries` from the `Builder` object passed to adapter functions diff --git a/.changeset/fine-ghosts-camp.md b/.changeset/fine-ghosts-camp.md new file mode 100644 index 000000000000..6eacab83c095 --- /dev/null +++ b/.changeset/fine-ghosts-camp.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-netlify': major +--- + +breaking: write output that conforms to the stable [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/). + +Deploying and previewing with Netlify CLI now requires [v17.31.0](https://github.com/netlify/cli/releases/tag/v17.31.0) or later. Run `npm i -g netlify-cli@latest` to upgrade. diff --git a/.changeset/five-readers-march.md b/.changeset/five-readers-march.md new file mode 100644 index 000000000000..e9fb4707d606 --- /dev/null +++ b/.changeset/five-readers-march.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: upgrade `@cloudflare/workers-types` to 4.20260219.0 diff --git a/.changeset/fluffy-tires-judge.md b/.changeset/fluffy-tires-judge.md new file mode 100644 index 000000000000..f5a5e090b2f9 --- /dev/null +++ b/.changeset/fluffy-tires-judge.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: upgrade minimum `wrangler` version to ^4.67.0 diff --git a/.changeset/forty-ducks-fly.md b/.changeset/forty-ducks-fly.md new file mode 100644 index 000000000000..98c98783ee29 --- /dev/null +++ b/.changeset/forty-ducks-fly.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: remove `platform.context` in favour of `platform.ctx` diff --git a/.changeset/gentle-radios-go.md b/.changeset/gentle-radios-go.md new file mode 100644 index 000000000000..33d159e8acb5 --- /dev/null +++ b/.changeset/gentle-radios-go.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove the deprecated CSRF `checkOrigin` option in favor of `trustedOrigins` diff --git a/.changeset/gentle-signs-cross.md b/.changeset/gentle-signs-cross.md new file mode 100644 index 000000000000..b819bf018351 --- /dev/null +++ b/.changeset/gentle-signs-cross.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: the `delta` property now only exists for `popstate` navigation events diff --git a/.changeset/giant-numbers-care.md b/.changeset/giant-numbers-care.md new file mode 100644 index 000000000000..e033cd7e2d28 --- /dev/null +++ b/.changeset/giant-numbers-care.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove deprecated `pragma` header in version polling for improved CORS support diff --git a/.changeset/heavy-showers-leave.md b/.changeset/heavy-showers-leave.md new file mode 100644 index 000000000000..4e44e7aee7a2 --- /dev/null +++ b/.changeset/heavy-showers-leave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require Svelte 5.48.0 or newer diff --git a/.changeset/large-onions-attack.md b/.changeset/large-onions-attack.md new file mode 100644 index 000000000000..aef0217173b9 --- /dev/null +++ b/.changeset/large-onions-attack.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': major +--- + +chore: migrate from rollup to rolldown diff --git a/.changeset/late-lions-crash.md b/.changeset/late-lions-crash.md new file mode 100644 index 000000000000..f120c80d8caa --- /dev/null +++ b/.changeset/late-lions-crash.md @@ -0,0 +1,9 @@ +--- +"@sveltejs/adapter-auto": major +"@sveltejs/adapter-cloudflare": major +"@sveltejs/adapter-netlify": major +"@sveltejs/adapter-node": major +"@sveltejs/adapter-static": major +--- + +breaking: require SvelteKit 3 diff --git a/.changeset/light-singers-lie.md b/.changeset/light-singers-lie.md new file mode 100644 index 000000000000..745edec0deb3 --- /dev/null +++ b/.changeset/light-singers-lie.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +chore: change `error`, `isHttpError`, `redirect`, and `isRedirect` to refer to public type instead of internal class diff --git a/.changeset/nasty-impalas-wear.md b/.changeset/nasty-impalas-wear.md new file mode 100644 index 000000000000..14fef49885f0 --- /dev/null +++ b/.changeset/nasty-impalas-wear.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: resolve paths using the Vite config `root` option instead of `process.cwd()` to better support monorepo configurations such as Vitest workspaces diff --git a/.changeset/nine-coins-cheer.md b/.changeset/nine-coins-cheer.md new file mode 100644 index 000000000000..6880deb22899 --- /dev/null +++ b/.changeset/nine-coins-cheer.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require Vite 8. Provides new functionality even for existing Vite 8 users such as faster builds with Vite hook filters and more powerful SvelteKit adapters with the Vite environment API diff --git a/.changeset/plenty-cougars-wave.md b/.changeset/plenty-cougars-wave.md new file mode 100644 index 000000000000..03d6b75dd276 --- /dev/null +++ b/.changeset/plenty-cougars-wave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': patch +--- + +chore: check the `WORKERS_CI` environment variable to determine if we're building for Cloudflare Workers diff --git a/.changeset/polite-lemons-refuse.md b/.changeset/polite-lemons-refuse.md new file mode 100644 index 000000000000..053810bb3e1d --- /dev/null +++ b/.changeset/polite-lemons-refuse.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': major +'@sveltejs/adapter-vercel': major +--- + +breaking: edge function build target is now `es2022` diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..095075f16771 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,54 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@sveltejs/adapter-auto": "7.0.1", + "@sveltejs/adapter-cloudflare": "7.2.7", + "test-cloudflare-pages": "0.0.1", + "test-cloudflare-workers": "0.0.1", + "@sveltejs/adapter-netlify": "6.0.0", + "test-netlify-basic": "0.0.1", + "test-netlify-edge": "0.0.1", + "@sveltejs/adapter-node": "5.5.3", + "@sveltejs/adapter-static": "3.0.10", + "~TODO~": "0.0.1", + "@sveltejs/adapter-vercel": "6.3.1", + "@sveltejs/amp": "1.1.5", + "@sveltejs/enhanced-img": "0.10.2", + "enhanced-img-basics": "0.0.1", + "@sveltejs/kit": "2.51.0", + "test-amp": "0.0.1", + "test-async": "0.0.1", + "test-basics": "0.0.2-next.0", + "test-dev-only": "0.0.2-next.0", + "test-embed": "0.0.1", + "test-hash-based-routing": "0.0.1", + "test-no-ssr": "0.0.1", + "test-options": "0.0.1", + "test-options-2": "0.0.1", + "test-options-3": "0.0.1", + "test-prerendered-app-error-pages": "0.0.1", + "test-writes": "0.0.2-next.0", + "prerenderable-incorrect-fragment": "0.0.1", + "prerenderable-remote-function-error": "0.0.1", + "prerenderable-not-prerendered": "0.0.1", + "private-dynamic-env": "0.0.1", + "private-dynamic-env-dynamic-import": "0.0.1", + "private-static-env": "0.0.1", + "private-static-env-dynamic-import": "0.0.1", + "server-only-folder": "0.0.1", + "server-only-folder-dynamic-import": "0.0.1", + "server-only-module": "0.0.1", + "server-only-module-dynamic-import": "0.0.1", + "service-worker-dynamic-public-env": "0.0.1", + "service-worker-private-env": "0.0.1", + "syntax-error": "0.0.1", + "prerendering-test-basics": "0.0.2-next.0", + "prerendering-test-options": "0.0.1", + "prerendering-test-paths-base": "0.0.1", + "@sveltejs/package": "2.5.7", + "test-redirect-importer": "0.0.1", + "playground-basic": "0.0.0" + }, + "changesets": [] +} diff --git a/.changeset/salty-chefs-sleep.md b/.changeset/salty-chefs-sleep.md new file mode 100644 index 000000000000..1f2cb46beafb --- /dev/null +++ b/.changeset/salty-chefs-sleep.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/package': patch +'@sveltejs/kit': patch +--- + +chore: remove dependency on kleur diff --git a/.changeset/shaggy-walls-wave.md b/.changeset/shaggy-walls-wave.md new file mode 100644 index 000000000000..cb7cffae3a95 --- /dev/null +++ b/.changeset/shaggy-walls-wave.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": major +--- + +breaking: `svelte.config.js` will now be included in type checking \ No newline at end of file diff --git a/.changeset/shiny-feet-crash.md b/.changeset/shiny-feet-crash.md new file mode 100644 index 000000000000..0d1485671007 --- /dev/null +++ b/.changeset/shiny-feet-crash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +chore: deprecate `Response` helpers in favor of platform-provided alternatives diff --git a/.changeset/spicy-drinks-build.md b/.changeset/spicy-drinks-build.md new file mode 100644 index 000000000000..453c6dc1b288 --- /dev/null +++ b/.changeset/spicy-drinks-build.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +chore: remove dependency on `set-cookie-parser` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51b2a3c61073..354f52d73fba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - main + - version-3 paths-ignore: &paths_ignore - '.changeset/**' - '.githooks/**' @@ -68,14 +68,6 @@ jobs: fail-fast: false matrix: include: - - node-version: 18 - os: ubuntu-latest - e2e-browser: 'chromium' - vite: 'baseline' - - node-version: 20 - os: ubuntu-latest - e2e-browser: 'chromium' - vite: 'current' - node-version: 22 os: ubuntu-latest e2e-browser: 'chromium' @@ -84,10 +76,10 @@ jobs: os: ubuntu-latest e2e-browser: 'chromium' vite: 'current' - - node-version: 24 - os: ubuntu-latest - e2e-browser: 'chromium' - vite: 'beta' +# - node-version: 24 +# os: ubuntu-latest +# e2e-browser: 'chromium' +# vite: 'beta' env: KIT_E2E_BROWSER: ${{matrix.e2e-browser}} MATRIX_VITE: ${{matrix.vite}} @@ -256,7 +248,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18, 20, 22, 24] + node-version: [22, 24] steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v5.0.0 @@ -269,16 +261,6 @@ jobs: with: deno-version: ^2.2.4 - run: pnpm install --frozen-lockfile - - name: setup overrides for matrix.node - if: matrix.node-version == 18 - run: - | # copies catalogs.node-xx to overrides in pnpm-workspace.yaml so the subsequent `pnpm install` enforces them - yq -i '.overrides =.catalogs."node-18"' pnpm-workspace.yaml - pnpm install --no-frozen-lockfile - git checkout pnpm-lock.yaml pnpm-workspace.yaml # revert changes to pnpm files to avoid cache key changes - pnpm --dir packages/kit ls vite @sveltejs/vite-plugin-svelte - run: pnpm playwright install chromium --no-shell - run: cd packages/kit && pnpm prepublishOnly - run: pnpm run test:others - env: - NODE_OPTIONS: ${{matrix.node-version == 22 && '--experimental-strip-types' || ''}} # allows loading svelte.config.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92544147ec3c..a14e411a00a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: branches: - - main + - version-3 concurrency: # prevent two release workflows from running at once diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 4731d73282aa..3956f47aedf6 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -296,7 +296,7 @@ export async function load({ fetch, params }) { ## Cookies -A server `load` function can get and set [`cookies`](@sveltejs-kit#Cookies). +A server `load` function can get [`cookies`](@sveltejs-kit#Cookies) as shown below. When setting cookies, SvelteKit provides default values for `httpOnly`, `secure`, and `path` — as described in [the API documentation](@sveltejs-kit#Cookies) — in order to improve security and developer experience. ```js /// file: src/routes/+layout.server.js diff --git a/documentation/docs/25-build-and-deploy/40-adapter-node.md b/documentation/docs/25-build-and-deploy/40-adapter-node.md index 347396904790..70f3ce553af5 100644 --- a/documentation/docs/25-build-and-deploy/40-adapter-node.md +++ b/documentation/docs/25-build-and-deploy/40-adapter-node.md @@ -33,7 +33,7 @@ You will need the output directory, the project's `package.json`, and the produc node build ``` -Development dependencies will be bundled into your app using [Rollup](https://rollupjs.org). To control whether a given package is bundled or externalised, place it in `devDependencies` or `dependencies` respectively in your `package.json`. +Development dependencies will be bundled into your app using [Rolldown](https://rolldown.rs/). To control whether a given package is bundled or externalised, place it in `devDependencies` or `dependencies` respectively in your `package.json`. ### Compressing responses diff --git a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md index 50f25f4a9299..f9b83326de39 100644 --- a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md +++ b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md @@ -48,7 +48,7 @@ The following options apply to all functions: - `split`: if `true`, causes a route to be deployed as an individual function. If `split` is set to `true` at the adapter level, all routes will be deployed as individual functions Additionally, the following option applies to edge functions: -- `external`: an array of dependencies that esbuild should treat as external when bundling functions. This should only be used to exclude optional dependencies that will not run outside Node +- `external`: an array of dependencies that Rolldown should treat as external when bundling functions. This should only be used to exclude optional dependencies that will not run outside Node And the following option apply to serverless functions: - `memory`: the amount of memory available to the function. Defaults to `1024` Mb, and can be decreased to `128` Mb or [increased](https://vercel.com/docs/concepts/limits/overview#serverless-function-memory) in 64Mb increments up to `3008` Mb on Pro or Enterprise accounts diff --git a/documentation/docs/25-build-and-deploy/99-writing-adapters.md b/documentation/docs/25-build-and-deploy/99-writing-adapters.md index 931420b8fb8f..823e2ffea350 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -57,7 +57,6 @@ Within the `adapt` method, there are a number of things that an adapter should d - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` - Listens for requests from the platform, converts them to a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `server.respond(request, { getClientAddress })` function to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it - expose any platform-specific information to SvelteKit via the `platform` option passed to `server.respond` - - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/node/polyfills` helper for platforms that can use `undici` - Bundle the output to avoid needing to install dependencies on the target platform, if necessary - Put the user's static files and the generated JS/CSS in the correct location for the target platform diff --git a/documentation/docs/60-appendix/10-faq.md b/documentation/docs/60-appendix/10-faq.md index abf22e0a4f9b..ea93645ce326 100644 --- a/documentation/docs/60-appendix/10-faq.md +++ b/documentation/docs/60-appendix/10-faq.md @@ -31,7 +31,7 @@ Here are a few things to keep in mind when checking if a library is packaged cor - `main` should be defined if `exports` is not. It should be either a CommonJS or ESM file and adhere to the previous bullet. If a `module` field is defined, it should refer to an ESM file. - Svelte components should be distributed as uncompiled `.svelte` files with any JS in the package written as ESM only. Custom script and style languages, like TypeScript and SCSS, should be preprocessed as vanilla JS and CSS respectively. We recommend using [`svelte-package`](./packaging) for packaging Svelte libraries, which will do this for you. -Libraries work best in the browser with Vite when they distribute an ESM version, especially if they are dependencies of a Svelte component library. You may wish to suggest to library authors that they provide an ESM version. However, CommonJS (CJS) dependencies should work as well since, by default, [`vite-plugin-svelte` will ask Vite to pre-bundle them](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) using `esbuild` to convert them to ESM. +Libraries work best in the browser with Vite when they distribute an ESM version, especially if they are dependencies of a Svelte component library. You may wish to suggest to library authors that they provide an ESM version. However, CommonJS (CJS) dependencies should work as well since, by default, [`vite-plugin-svelte` will ask Vite to pre-bundle them](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) using `rolldown` to convert them to ESM. If you are still encountering issues we recommend searching both [the Vite issue tracker](https://github.com/vitejs/vite/issues) and the issue tracker of the library in question. Sometimes issues can be worked around by fiddling with the [`optimizeDeps`](https://vitejs.dev/config/#dep-optimization-options) or [`ssr`](https://vitejs.dev/config/#ssr-options) config values though we recommend this as only a short-term workaround in favor of fixing the library in question. diff --git a/documentation/docs/98-reference/15-@sveltejs-kit-node-polyfills.md b/documentation/docs/98-reference/15-@sveltejs-kit-node-polyfills.md deleted file mode 100644 index a80c80ab3139..000000000000 --- a/documentation/docs/98-reference/15-@sveltejs-kit-node-polyfills.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: @sveltejs/kit/node/polyfills ---- - -> MODULE: @sveltejs/kit/node/polyfills diff --git a/eslint.config.js b/eslint.config.js index 224b3ca761d8..a0bf66f7f644 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -57,7 +57,6 @@ export default [ ignores: [ 'packages/adapter-cloudflare/test/apps/**/*', 'packages/adapter-netlify/test/apps/**/*', - 'packages/adapter-node/rollup.config.js', 'packages/adapter-node/tests/smoke.spec_disabled.js', 'packages/adapter-static/test/apps/**/*', 'packages/adapter-vercel/test/apps/**/*', diff --git a/package.json b/package.json index 879aa8cdc449..f7f7cda56cd4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test:vite-ecosystem-ci": "pnpm --dir packages/kit test", "test:others": "pnpm -r --filter='./packages/*' --filter=!./packages/kit/ --workspace-concurrency=1 test", "check": "pnpm -r prepublishOnly && pnpm -r check", - "lint": "pnpm -r lint && eslint --cache --cache-location node_modules/.eslintcache 'packages/**/*.js'", + "lint": "pnpm -r lint && echo '\nRunning eslint...' && eslint --cache --cache-location node_modules/.eslintcache 'packages/**/*.js'", "format": "pnpm -r format", "precommit": "pnpm format && pnpm lint", "changeset:version": "changeset version && pnpm -r generate:version && git add --all", @@ -30,7 +30,8 @@ "@svitejs/changesets-changelog-github-compact": "catalog:", "eslint": "catalog:", "prettier": "catalog:", - "prettier-plugin-svelte": "catalog:" + "prettier-plugin-svelte": "catalog:", + "vitest": "catalog:" }, "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", "engines": { diff --git a/packages/adapter-auto/index.js b/packages/adapter-auto/index.js index 64bc04312afc..6813807e0086 100644 --- a/packages/adapter-auto/index.js +++ b/packages/adapter-auto/index.js @@ -4,17 +4,6 @@ import path from 'node:path'; import fs from 'node:fs'; import process from 'node:process'; -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_0_0 - */ - /** @type {Record string>} */ const commands = { npm: (name, version) => `npm install -D ${name}@${version}`, @@ -149,11 +138,10 @@ async function get_adapter() { /** @type {() => Adapter} */ export default () => ({ name: '@sveltejs/adapter-auto', - /** @param {Builder2_0_0} builder */ adapt: async (builder) => { const adapter = await get_adapter(); - if (adapter) return adapter.adapt(/** @type {import('@sveltejs/kit').Builder} */ (builder)); + if (adapter) return adapter.adapt(builder); builder.log.warn( 'Could not detect a supported production environment. See https://svelte.dev/docs/kit/adapters to learn how to configure your app to run on the platform of your choosing' diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json index ae2e87f09b59..370978a6acaa 100644 --- a/packages/adapter-auto/package.json +++ b/packages/adapter-auto/package.json @@ -46,6 +46,6 @@ "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "@sveltejs/kit": "^3.0.0" } } diff --git a/packages/adapter-auto/vitest.config.js b/packages/adapter-auto/vitest.config.js new file mode 100644 index 000000000000..a15b3a470a6d --- /dev/null +++ b/packages/adapter-auto/vitest.config.js @@ -0,0 +1,5 @@ +// we need this file to prevent Vitest from resolving a Vitest config from another directory + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/packages/adapter-cloudflare/ambient.d.ts b/packages/adapter-cloudflare/ambient.d.ts index d99dd15ab6cc..c485bb610945 100644 --- a/packages/adapter-cloudflare/ambient.d.ts +++ b/packages/adapter-cloudflare/ambient.d.ts @@ -9,8 +9,6 @@ declare global { export interface Platform { env: unknown; ctx: ExecutionContext; - /** @deprecated Use `ctx` instead */ - context: ExecutionContext; caches: CacheStorage; cf?: IncomingRequestCfProperties; } diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 7d51fd0967ea..66c8a2da246e 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -1,4 +1,3 @@ -import { VERSION } from '@sveltejs/kit'; import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import process from 'node:process'; @@ -12,13 +11,11 @@ import { } from './utils.js'; const name = '@sveltejs/adapter-cloudflare'; -const [kit_major, kit_minor] = VERSION.split('.'); /** @type {import('./index.js').default} */ export default function (options = {}) { return { name, - /** @param {Builder2_0_0} builder */ async adapt(builder) { if ( existsSync('_routes.json') || @@ -126,8 +123,8 @@ export default function (options = {}) { ASSETS: assets_binding } }); - if (builder.hasServerInstrumentationFile?.()) { - builder.instrument?.({ + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ entrypoint: worker_dest, instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js` }); @@ -214,16 +211,7 @@ export default function (options = {}) { }; }, supports: { - read: ({ route }) => { - // TODO bump peer dep in next adapter major to simplify this - if (kit_major === '2' && kit_minor < '25') { - throw new Error( - `${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` when using SvelteKit < 2.25.0` - ); - } - - return true; - }, + read: () => true, instrumentation: () => true } }; diff --git a/packages/adapter-cloudflare/internal.d.ts b/packages/adapter-cloudflare/internal.d.ts index 848182720e5d..6c79569f7f7f 100644 --- a/packages/adapter-cloudflare/internal.d.ts +++ b/packages/adapter-cloudflare/internal.d.ts @@ -10,32 +10,3 @@ declare module 'MANIFEST' { export const app_path: string; export const base_path: string; } - -type PartialExcept = Partial> & Required>; - -/** - * We use a custom `Builder` type here to ensure compatibility with the minimum version of SvelteKit. - */ -type Builder2_0_0 = PartialExcept< - import('@sveltejs/kit').Builder, - | 'log' - | 'rimraf' - | 'mkdirp' - | 'config' - | 'prerendered' - | 'routes' - | 'createEntries' - | 'generateFallback' - | 'generateEnvModule' - | 'generateManifest' - | 'getBuildDirectory' - | 'getClientDirectory' - | 'getServerDirectory' - | 'getAppPath' - | 'writeClient' - | 'writePrerendered' - | 'writePrerendered' - | 'writeServer' - | 'copy' - | 'compress' ->; diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index 8d04ca549f16..1b77c76bb160 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -34,7 +34,7 @@ "ambient.d.ts" ], "scripts": { - "build": "esbuild src/worker.js --bundle --outfile=files/worker.js --external:SERVER --external:MANIFEST --external:cloudflare:workers --format=esm", + "build": "rolldown -c", "lint": "prettier --check .", "format": "pnpm lint --write", "check": "tsc --skipLibCheck", @@ -44,19 +44,19 @@ "prepublishOnly": "pnpm build" }, "dependencies": { - "@cloudflare/workers-types": "^4.20250507.0", + "@cloudflare/workers-types": "^4.20260219.0", "worktop": "0.8.0-next.18" }, "devDependencies": { "@playwright/test": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "esbuild": "catalog:", + "rolldown": "catalog:", "typescript": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0", - "wrangler": "^4.0.0" + "@sveltejs/kit": "^3.0.0", + "wrangler": "^4.67.0" } } diff --git a/packages/adapter-cloudflare/rolldown.config.js b/packages/adapter-cloudflare/rolldown.config.js new file mode 100644 index 000000000000..89e1458358b4 --- /dev/null +++ b/packages/adapter-cloudflare/rolldown.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'rolldown'; + +export default defineConfig({ + input: 'src/worker.js', + output: { + file: 'files/worker.js' + }, + external: ['SERVER', 'MANIFEST', 'cloudflare:workers'], + platform: 'browser' +}); diff --git a/packages/adapter-cloudflare/src/worker.js b/packages/adapter-cloudflare/src/worker.js index bcee34b882e2..f34f099bb759 100644 --- a/packages/adapter-cloudflare/src/worker.js +++ b/packages/adapter-cloudflare/src/worker.js @@ -100,7 +100,6 @@ export default { platform: { env, ctx, - context: ctx, // deprecated in favor of ctx // @ts-expect-error webworker types from worktop are not compatible with Cloudflare Workers types caches, // @ts-expect-error the type is correct but ts is confused because platform.cf uses the type from index.ts while req.cf uses the type from index.d.ts diff --git a/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts b/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts deleted file mode 100644 index 101a9db0b8bf..000000000000 --- a/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: remove this in 3.0 once svelte.config.js is included by the generated tsconfig.json -// this ensures we get the ambient types from the adapter -import '../../../../index.js'; diff --git a/packages/adapter-cloudflare/test/apps/workers/test/test.js b/packages/adapter-cloudflare/test/apps/workers/test/test.js index 40072953a16f..44832c70890f 100644 --- a/packages/adapter-cloudflare/test/apps/workers/test/test.js +++ b/packages/adapter-cloudflare/test/apps/workers/test/test.js @@ -1,10 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - test('worker', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1')).toContainText('Sum: 3'); @@ -16,7 +13,10 @@ test('ctx', async ({ request }) => { }); test('read from $app/server works', async ({ request }) => { - const content = fs.readFileSync(path.resolve(__dirname, '../src/routes/read/file.txt'), 'utf-8'); + const content = fs.readFileSync( + path.resolve(import.meta.dirname, '../src/routes/read/file.txt'), + 'utf-8' + ); const response = await request.get('/read'); expect(await response.text()).toBe(content); }); diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json index d1fdac44e7b3..5a06e032a358 100644 --- a/packages/adapter-cloudflare/tsconfig.json +++ b/packages/adapter-cloudflare/tsconfig.json @@ -3,20 +3,22 @@ "allowJs": true, "checkJs": true, "noEmit": true, - "target": "es2022", - "module": "node16", - "moduleResolution": "node16", + "module": "nodenext", + "moduleResolution": "nodenext", "paths": { "@sveltejs/kit": ["../kit/types/index"] }, // taken from the Cloudflare Workers TypeScript template https://github.com/cloudflare/workers-sdk/blob/main/packages/create-cloudflare/templates/hello-world/ts/tsconfig.json - "lib": ["es2021"], + "target": "es2024", + "lib": ["es2024"], "types": ["node", "@cloudflare/workers-types"] }, "include": [ "index.js", "utils.js", "utils.spec.js", + "rolldown.config.js", + "vitest.config.js", "test/utils.js", "internal.d.ts", "src/worker.js" diff --git a/packages/adapter-cloudflare/utils.js b/packages/adapter-cloudflare/utils.js index 28d83691b12a..2b1c7c74b567 100644 --- a/packages/adapter-cloudflare/utils.js +++ b/packages/adapter-cloudflare/utils.js @@ -9,7 +9,7 @@ export function is_building_for_cloudflare_pages(wrangler_config) { return true; } - if (wrangler_config.main || wrangler_config.assets) { + if (!!process.env.WORKERS_CI || wrangler_config.main || wrangler_config.assets) { return false; } @@ -81,7 +81,7 @@ export function parse_redirects(file_contents) { /** * Generates the [_routes.json](https://developers.cloudflare.com/pages/functions/routing/#create-a-_routesjson-file) * file that dictates which routes invoke the Cloudflare Worker. - * @param {Builder2_0_0} builder + * @param {import('@sveltejs/kit').Builder} builder * @param {string[]} client_assets * @param {string[]} redirects * @param {import('./index.js').AdapterOptions['routes']} routes diff --git a/packages/adapter-cloudflare/utils.spec.js b/packages/adapter-cloudflare/utils.spec.js index f6059a96609f..7d3755559376 100644 --- a/packages/adapter-cloudflare/utils.spec.js +++ b/packages/adapter-cloudflare/utils.spec.js @@ -68,6 +68,15 @@ describe('detects Cloudflare Workers project', () => { ) ).toBe(false); }); + + test('WORKERS_CI environment variable', () => { + vi.stubEnv('WORKERS_CI', '1'); + const result = is_building_for_cloudflare_pages( + /** @type {import('wrangler').Unstable_Config} */ ({}) + ); + vi.unstubAllEnvs(); + expect(result).toBe(false); + }); }); describe('validates Wrangler config', () => { diff --git a/packages/adapter-cloudflare/vitest.config.js b/packages/adapter-cloudflare/vitest.config.js new file mode 100644 index 000000000000..a15b3a470a6d --- /dev/null +++ b/packages/adapter-cloudflare/vitest.config.js @@ -0,0 +1,5 @@ +// we need this file to prevent Vitest from resolving a Vitest config from another directory + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index cd8ff0521324..2c1355b42425 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -1,11 +1,11 @@ -/** @import { BuildOptions } from 'esbuild' */ -import { appendFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join, resolve, posix } from 'node:path'; +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, posix } from 'node:path'; import { fileURLToPath } from 'node:url'; import { builtinModules } from 'node:module'; import process from 'node:process'; -import esbuild from 'esbuild'; import toml from '@iarna/toml'; +import { build } from 'rolldown'; +import { matches, get_publish_directory, s } from './utils.js'; /** * @typedef {{ @@ -14,6 +14,9 @@ import toml from '@iarna/toml'; * } & toml.JsonMap} NetlifyConfig */ +const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8')); +const adapter_version = pkg.version; + const name = '@sveltejs/adapter-netlify'; const files = fileURLToPath(new URL('./files', import.meta.url).href); @@ -21,13 +24,16 @@ const edge_set_in_env_var = process.env.NETLIFY_SVELTEKIT_USE_EDGE === 'true' || process.env.NETLIFY_SVELTEKIT_USE_EDGE === '1'; +const netlify_framework_config_path = '.netlify/v1/config.json'; +const netlify_framework_serverless_path = '.netlify/v1/functions'; +const netlify_framework_edge_path = '.netlify/v1/edge-functions'; + const FUNCTION_PREFIX = 'sveltekit-'; /** @type {import('./index.js').default} */ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { return { name, - /** @param {import('@sveltejs/kit').Builder} builder */ async adapt(builder) { if (!builder.routes) { throw new Error( @@ -55,11 +61,14 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { // empty out existing build directories builder.rimraf(publish); + builder.rimraf('.netlify/v1'); + + // clean up legacy directories from older adapter versions to avoid + // gnarly edge cases when an existing project is upgraded to this version builder.rimraf('.netlify/edge-functions'); builder.rimraf('.netlify/server'); builder.rimraf('.netlify/package.json'); builder.rimraf('.netlify/serverless.js'); - if (existsSync('.netlify/functions-internal')) { for (const file of readdirSync('.netlify/functions-internal')) { if (file.startsWith(FUNCTION_PREFIX)) { @@ -75,13 +84,13 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { builder.writeClient(publish_dir); builder.writePrerendered(publish_dir); - builder.log.minor('Writing custom headers...'); - const headers_file = join(publish, '_headers'); - builder.copy('_headers', headers_file); - appendFileSync( - headers_file, - `\n\n/${builder.getAppPath()}/immutable/*\n cache-control: public\n cache-control: immutable\n cache-control: max-age=31536000\n` - ); + // Copy user's custom _headers file if it exists + if (existsSync('_headers')) { + builder.copy('_headers', join(publish, '_headers')); + } + + builder.log.minor('Writing Netlify config...'); + write_frameworks_config({ builder }); if (edge) { if (split) { @@ -90,7 +99,7 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { await generate_edge_functions({ builder }); } else { - generate_lambda_functions({ builder, split, publish }); + generate_serverless_functions({ builder, split, publish }); } }, @@ -100,131 +109,24 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { } }; } -/** - * @param { object } params - * @param {import('@sveltejs/kit').Builder} params.builder - */ -async function generate_edge_functions({ builder }) { - const tmp = builder.getBuildDirectory('netlify-tmp'); - builder.rimraf(tmp); - builder.mkdirp(tmp); - - builder.mkdirp('.netlify/edge-functions'); - - builder.log.minor('Generating Edge Function...'); - const relativePath = posix.relative(tmp, builder.getServerDirectory()); - builder.copy(`${files}/edge.js`, `${tmp}/entry.js`, { - replace: { - '0SERVER': `${relativePath}/index.js`, - MANIFEST: './manifest.js' - } - }); - - const manifest = builder.generateManifest({ - relativePath - }); - - writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`); - - /** @type {{ assets: Set }} */ - // we have to prepend the file:// protocol because Windows doesn't support absolute path imports - const { assets } = (await import(`file://${tmp}/manifest.js`)).manifest; - - const path = '/*'; - // We only need to specify paths without the trailing slash because - // Netlify will handle the optional trailing slash for us - const excluded = [ - // Contains static files - `/${builder.getAppPath()}/immutable/*`, - `/${builder.getAppPath()}/version.json`, - ...builder.prerendered.paths, - ...Array.from(assets).flatMap((asset) => { - if (asset.endsWith('/index.html')) { - const dir = asset.replace(/\/index\.html$/, ''); - return [ - `${builder.config.kit.paths.base}/${asset}`, - `${builder.config.kit.paths.base}/${dir}` - ]; - } - return `${builder.config.kit.paths.base}/${asset}`; - }), - // Should not be served by SvelteKit at all - '/.netlify/*' - ]; - - /** @type {import('@netlify/edge-functions').Manifest} */ - const edge_manifest = { - functions: [ - { - function: 'render', - path, - excludedPath: /** @type {`/${string}`[]} */ (excluded) - } - ], - version: 1 - }; - - /** @type {BuildOptions} */ - const esbuild_config = { - bundle: true, - format: 'esm', - platform: 'browser', - sourcemap: 'linked', - target: 'es2020', - loader: { - '.wasm': 'copy', - '.woff': 'copy', - '.woff2': 'copy', - '.ttf': 'copy', - '.eot': 'copy', - '.otf': 'copy' - }, - // Node built-ins are allowed, but must be prefixed with `node:` - // https://docs.netlify.com/edge-functions/api/#runtime-environment - external: builtinModules.map((id) => `node:${id}`), - alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])) - }; - await Promise.all([ - esbuild.build({ - entryPoints: [`${tmp}/entry.js`], - outfile: '.netlify/edge-functions/render.js', - ...esbuild_config - }), - builder.hasServerInstrumentationFile() && - esbuild.build({ - entryPoints: [`${builder.getServerDirectory()}/instrumentation.server.js`], - outfile: '.netlify/edge/instrumentation.server.js', - ...esbuild_config - }) - ]); - - if (builder.hasServerInstrumentationFile()) { - builder.instrument({ - entrypoint: '.netlify/edge-functions/render.js', - instrumentation: '.netlify/edge/instrumentation.server.js', - start: '.netlify/edge/start.js' - }); - } - - writeFileSync('.netlify/edge-functions/manifest.json', JSON.stringify(edge_manifest)); -} /** * @param { object } params * @param {import('@sveltejs/kit').Builder} params.builder * @param { string } params.publish * @param { boolean } params.split */ -function generate_lambda_functions({ builder, publish, split }) { - builder.mkdirp('.netlify/functions-internal/.svelte-kit'); +function generate_serverless_functions({ builder, publish, split }) { + // https://docs.netlify.com/build/frameworks/frameworks-api/#netlifyv1functions + builder.mkdirp(netlify_framework_serverless_path); - builder.writeServer('.netlify/server'); + builder.writeServer('.netlify/v1/server'); const replace = { '0SERVER': './server/index.js' // digit prefix prevents CJS build from using this as a variable name, which would also get replaced }; - builder.copy(files, '.netlify', { replace, filter: (name) => !name.endsWith('edge.js') }); + builder.copy(files, '.netlify/v1', { replace, filter: (file) => !file.endsWith('edge.js') }); builder.log.minor('Generating serverless functions...'); @@ -306,77 +208,43 @@ function generate_lambda_functions({ builder, publish, split }) { } } +/** + * @returns {NetlifyConfig | null} + */ function get_netlify_config() { if (!existsSync('netlify.toml')) return null; try { - return /** @type {NetlifyConfig} */ (toml.parse(readFileSync('netlify.toml', 'utf-8'))); + return toml.parse(readFileSync('netlify.toml', 'utf-8')); } catch (err) { if (err instanceof Error) { - err.message = `Error parsing netlify.toml: ${err.message}`; + throw new Error(`Failed to parse netlify.toml: ${err.message}`, { cause: err }); } throw err; } } /** - * @param {NetlifyConfig | null} netlify_config - * @param {import('@sveltejs/kit').Builder} builder - **/ -function get_publish_directory(netlify_config, builder) { - if (netlify_config) { - if (!netlify_config.build?.publish) { - builder.log.minor('No publish directory specified in netlify.toml, using default'); - return; - } - - if (resolve(netlify_config.build.publish) === process.cwd()) { - throw new Error( - 'The publish directory cannot be set to the site root. Please change it to another value such as "build" in netlify.toml.' - ); - } - return netlify_config.build.publish; - } - - builder.log.warn( - 'No netlify.toml found. Using default publish directory. Consult https://svelte.dev/docs/kit/adapter-netlify#usage for more details' - ); -} - -/** - * @typedef {{ rest: boolean, dynamic: boolean, content: string }} RouteSegment + * Writes the Netlify Frameworks API config file + * https://docs.netlify.com/build/frameworks/frameworks-api/ + * @param {{ builder: import('@sveltejs/kit').Builder }} params */ - -/** - * @param {RouteSegment[]} a - * @param {RouteSegment[]} b - * @returns {boolean} - */ -function matches(a, b) { - if (a[0] && b[0]) { - if (b[0].rest) { - if (b.length === 1) return true; - - const next_b = b.slice(1); - - for (let i = 0; i < a.length; i += 1) { - if (matches(a.slice(i), next_b)) return true; +function write_frameworks_config({ builder }) { + // https://docs.netlify.com/build/frameworks/frameworks-api/#headers + /** @type {{ headers: Array<{ for: string, values: Record }> }} */ + const config = { + headers: [ + { + for: `/${builder.getAppPath()}/immutable/*`, + values: { + 'cache-control': 'public, immutable, max-age=31536000' + } } + ] + }; - return false; - } - - if (!b[0].dynamic) { - if (!a[0].dynamic && a[0].content !== b[0].content) return false; - } - - if (a.length === 1 && b.length === 1) return true; - return matches(a.slice(1), b.slice(1)); - } else if (a[0]) { - return a.length === 1 && a[0].rest; - } else { - return b.length === 1 && b[0].rest; - } + builder.mkdirp('.netlify/v1'); + writeFileSync(netlify_framework_config_path, s(config)); } /** @@ -399,17 +267,17 @@ function generate_serverless_function({ builder, routes, patterns, name, exclude const config = generate_config_export(patterns, exclude); if (builder.hasServerInstrumentationFile()) { - writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn); + writeFileSync(`${netlify_framework_serverless_path}/${name}.mjs`, fn); builder.instrument({ - entrypoint: `.netlify/functions-internal/${name}.mjs`, - instrumentation: '.netlify/server/instrumentation.server.js', - start: `.netlify/functions-start/${name}.start.mjs`, + entrypoint: `${netlify_framework_serverless_path}/${name}.mjs`, + instrumentation: '.netlify/v1/server/instrumentation.server.js', + start: `.netlify/v1/server/${name}.start.mjs`, module: { generateText: generate_traced_module(config) } }); } else { - writeFileSync(`.netlify/functions-internal/${name}.mjs`, `${fn}\n${config}`); + writeFileSync(`${netlify_framework_serverless_path}/${name}.mjs`, `${fn}\n${config}`); } } @@ -425,6 +293,8 @@ export default init(${manifest}); `; } +const generator_string = `@sveltejs/adapter-netlify@${adapter_version}`; + /** * @param {string[]} patterns * @param {string[]} [exclude] @@ -432,10 +302,14 @@ export default init(${manifest}); */ function generate_config_export(patterns, exclude = []) { // TODO: add a human friendly name for the function https://docs.netlify.com/build/frameworks/frameworks-api/#configuration-options-2 + + // https://docs.netlify.com/build/frameworks/frameworks-api/#configuration-options-2 return `\ export const config = { - path: [${patterns.map((s) => JSON.stringify(s)).join(', ')}], - excludedPath: [${['/.netlify/*', ...exclude].map((s) => JSON.stringify(s)).join(', ')}], + name: 'SvelteKit server', + generator: '${generator_string}', + path: [${patterns.map(s).join(', ')}], + excludedPath: [${['/.netlify/*', ...exclude].map(s).join(', ')}], preferStatic: true }; `; @@ -448,10 +322,135 @@ export const config = { function generate_traced_module(config) { return ({ instrumentation, start }) => { return `\ -import './${instrumentation}'; -const { default: _0 } = await import('./${start}'); +import '../server/${instrumentation}'; +const { default: _0 } = await import('../server/${start}'); export { _0 as default }; ${config}`; }; } + +/** @satisfies {import('rolldown').BuildOptions} */ +const rolldown_config = { + platform: 'browser', + output: { + sourcemap: true, + codeSplitting: false + }, + transform: { + target: 'es2022' + }, + // Node built-ins are allowed, but must be prefixed with `node:` + // https://docs.netlify.com/edge-functions/api/#runtime-environment + external: builtinModules.map((id) => `node:${id}`), + resolve: { + alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])) + } +}; + +/** + * @param { object } params + * @param {import('@sveltejs/kit').Builder} params.builder + */ +async function generate_edge_functions({ builder }) { + const tmp = builder.getBuildDirectory('netlify-tmp'); + builder.rimraf(tmp); + builder.mkdirp(tmp); + + // https://docs.netlify.com/build/frameworks/frameworks-api/#edge-functions + builder.mkdirp('.netlify/v1/edge-functions'); + + builder.log.minor('Generating Edge Function...'); + const relativePath = posix.relative(tmp, builder.getServerDirectory()); + + builder.copy(`${files}/edge.js`, `${tmp}/entry.js`, { + replace: { + '0SERVER': `${relativePath}/index.js`, + MANIFEST: './manifest.js' + } + }); + + const manifest = builder.generateManifest({ + relativePath + }); + + writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`); + + /** @type {{ assets: Set }} */ + // we have to prepend the file:// protocol because Windows doesn't support absolute path imports + const { assets } = (await import(`file://${tmp}/manifest.js`)).manifest; + + const path = '/*'; + // We only need to specify paths without the trailing slash because + // Netlify will handle the optional trailing slash for us + const excluded_paths = [ + // Contains static files + `/${builder.getAppPath()}/immutable/*`, + `/${builder.getAppPath()}/version.json`, + ...builder.prerendered.paths, + ...Array.from(assets).flatMap((asset) => { + if (asset.endsWith('/index.html')) { + const dir = asset.replace(/\/index\.html$/, ''); + return [ + `${builder.config.kit.paths.base}/${asset}`, + `${builder.config.kit.paths.base}/${dir}` + ]; + } + return `${builder.config.kit.paths.base}/${asset}`; + }), + // Should not be served by SvelteKit at all + '/.netlify/*' + ]; + + await Promise.all([ + build({ + ...rolldown_config, + input: `${tmp}/entry.js`, + output: { + ...rolldown_config.output, + file: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}render.js` + } + }), + builder.hasServerInstrumentationFile() && + build({ + ...rolldown_config, + input: `${builder.getServerDirectory()}/instrumentation.server.js`, + output: { + ...rolldown_config.output, + file: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}instrumentation.server.js` + } + }) + ]); + + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ + entrypoint: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}render.js`, + instrumentation: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}instrumentation.server.js`, + start: `${netlify_framework_edge_path}/${FUNCTION_PREFIX}start.js` + }); + } + + add_edge_function_config({ builder, path, excluded_paths }); +} + +/** + * Adds edge function configuration to the Frameworks API config file `config.json` + * https://docs.netlify.com/build/frameworks/frameworks-api/#netlifyv1edge-functions + * @param {{ builder: import('@sveltejs/kit').Builder, path: string, excluded_paths: string[] }} params + */ +function add_edge_function_config({ path, excluded_paths }) { + const config = JSON.parse(readFileSync(netlify_framework_config_path, 'utf-8')); + + // https://docs.netlify.com/build/frameworks/frameworks-api/#configuration-options-1 + config.edge_functions = [ + { + function: `${FUNCTION_PREFIX}render`, + name: 'SvelteKit server', + generator: generator_string, + path, + excludedPath: excluded_paths + } + ]; + + writeFileSync(netlify_framework_config_path, s(config)); +} diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index 0ff4c4aa12d8..166f71fb56d9 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -33,19 +33,19 @@ "ambient.d.ts" ], "scripts": { - "dev": "rollup -cw", - "build": "rollup -c", + "dev": "rolldown -cw", + "build": "rolldown -c", "check": "tsc", "lint": "prettier --check .", "format": "pnpm lint --write", - "test": "pnpm test:integration", + "test": "pnpm test:unit && pnpm test:integration", "test:unit": "vitest run", "test:integration": "pnpm build && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test", "prepublishOnly": "pnpm build" }, "dependencies": { "@iarna/toml": "^2.2.5", - "esbuild": "^0.25.4" + "rolldown": "^1.0.0-rc.6" }, "devDependencies": { "@netlify/dev": "catalog:", @@ -53,12 +53,8 @@ "@netlify/functions": "catalog:", "@netlify/node-cookies": "^0.1.0", "@netlify/types": "^2.1.0", - "@rollup/plugin-commonjs": "catalog:", - "@rollup/plugin-json": "catalog:", - "@rollup/plugin-node-resolve": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "rollup": "^4.59.0", "typescript": "catalog:", "vitest": "catalog:" }, diff --git a/packages/adapter-netlify/rollup.config.js b/packages/adapter-netlify/rolldown.config.js similarity index 53% rename from packages/adapter-netlify/rollup.config.js rename to packages/adapter-netlify/rolldown.config.js index 82d61b85d29d..9230e1b5fb01 100644 --- a/packages/adapter-netlify/rollup.config.js +++ b/packages/adapter-netlify/rolldown.config.js @@ -1,11 +1,8 @@ -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; import { rmSync } from 'node:fs'; /** * @param {string} filepath - * @returns {import('rollup').Plugin} + * @returns {import('rolldown').Plugin} */ function clearOutput(filepath) { return { @@ -20,21 +17,20 @@ function clearOutput(filepath) { }; } -/** @type {import('rollup').RollupOptions} */ +/** @type {import('rolldown').RolldownOptions} */ const config = { input: { serverless: 'src/serverless.js', - shims: 'src/shims.js', edge: 'src/edge.js' }, output: { dir: 'files', format: 'esm' }, - // @ts-ignore https://github.com/rollup/plugins/issues/1329 - plugins: [clearOutput('files'), nodeResolve({ preferBuiltins: true }), commonjs(), json()], + plugins: [clearOutput('files')], external: (id) => id === '0SERVER' || id === 'MANIFEST' || id.startsWith('node:'), - preserveEntrySignatures: 'exports-only' + preserveEntrySignatures: 'exports-only', + platform: 'node' }; export default config; diff --git a/packages/adapter-netlify/src/serverless.js b/packages/adapter-netlify/src/serverless.js index 592eb081b7b9..d783fe09ec2b 100644 --- a/packages/adapter-netlify/src/serverless.js +++ b/packages/adapter-netlify/src/serverless.js @@ -1,4 +1,3 @@ -import './shims.js'; import { Server } from '0SERVER'; import { createReadableStream } from '@sveltejs/kit/node'; import process from 'node:process'; diff --git a/packages/adapter-netlify/src/shims.js b/packages/adapter-netlify/src/shims.js deleted file mode 100644 index 2490311daa1e..000000000000 --- a/packages/adapter-netlify/src/shims.js +++ /dev/null @@ -1,2 +0,0 @@ -import { installPolyfills } from '@sveltejs/kit/node/polyfills'; -installPolyfills(); diff --git a/packages/adapter-netlify/test/apps/basic/.gitignore b/packages/adapter-netlify/test/apps/basic/.gitignore index 88f661c765c6..4150a3674e3a 100644 --- a/packages/adapter-netlify/test/apps/basic/.gitignore +++ b/packages/adapter-netlify/test/apps/basic/.gitignore @@ -3,4 +3,4 @@ node_modules /.svelte-kit /.netlify /build -deno.lock \ No newline at end of file +deno.lock diff --git a/packages/adapter-netlify/test/apps/basic/_headers b/packages/adapter-netlify/test/apps/basic/_headers new file mode 100644 index 000000000000..01c0aab8e046 --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/_headers @@ -0,0 +1,2 @@ +/custom-header-path + X-Custom-Header: test-value diff --git a/packages/adapter-netlify/test/apps/basic/_redirects b/packages/adapter-netlify/test/apps/basic/_redirects new file mode 100644 index 000000000000..fd5703a26e6a --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/_redirects @@ -0,0 +1 @@ +/redirect-me /greeting/redirected 301 diff --git a/packages/adapter-netlify/test/apps/basic/netlify.toml b/packages/adapter-netlify/test/apps/basic/netlify.toml index 5bd2fc95156c..a19ca0df6798 100644 --- a/packages/adapter-netlify/test/apps/basic/netlify.toml +++ b/packages/adapter-netlify/test/apps/basic/netlify.toml @@ -1,9 +1,8 @@ [build] publish = "build" -# TODO: remove this after we refactor to the Netlify frameworks API -# we are purposely misusing the user functions config to discover our framework -# build output because our adapter still outputs using an older API but the new -# Netlify dev server adheres to the new API +# TODO(serhalp): remove this when @netlify/dev supports serve/preview. +# @netlify/dev does not yet have explicit support for a "serve" mode, but this funky workaround is +# sufficient for our purposes for now. NOTE: do not do this in real apps; this is only for testing. [functions] -directory = ".netlify/functions-internal" +directory = ".netlify/v1/functions" diff --git a/packages/adapter-netlify/test/apps/basic/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/basic/src/instrumentation.server.js deleted file mode 100644 index acc9022e1d64..000000000000 --- a/packages/adapter-netlify/test/apps/basic/src/instrumentation.server.js +++ /dev/null @@ -1 +0,0 @@ -// this is just here to make sure the changes resulting from it work diff --git a/packages/adapter-netlify/test/apps/basic/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/basic/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.server.js b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.server.js new file mode 100644 index 000000000000..31e005a89bad --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { name: params.name }; +} diff --git a/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.svelte b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.svelte new file mode 100644 index 000000000000..4dcbba568369 --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.svelte @@ -0,0 +1,5 @@ + + +

Hello {data.name}

diff --git a/packages/adapter-netlify/test/apps/basic/svelte.config.js b/packages/adapter-netlify/test/apps/basic/svelte.config.js index 050579db13ba..20cd2b3ff5b8 100644 --- a/packages/adapter-netlify/test/apps/basic/svelte.config.js +++ b/packages/adapter-netlify/test/apps/basic/svelte.config.js @@ -3,12 +3,7 @@ import adapter from '../../../index.js'; /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter(), - experimental: { - instrumentation: { - server: true - } - } + adapter: adapter() } }; diff --git a/packages/adapter-netlify/test/apps/basic/test/test.js b/packages/adapter-netlify/test/apps/basic/test/test.js index 50329932eb04..b208dd54a8b7 100644 --- a/packages/adapter-netlify/test/apps/basic/test/test.js +++ b/packages/adapter-netlify/test/apps/basic/test/test.js @@ -1,12 +1,37 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +test('page renders', async ({ request }) => { + const response = await request.get('/'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello from SvelteKit'); +}); + +test('dynamic route works', async ({ request }) => { + const response = await request.get('/greeting/world'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello world'); +}); test('read from $app/server works', async ({ request }) => { - const content = fs.readFileSync(path.resolve(__dirname, '../src/routes/read/file.txt'), 'utf-8'); + const content = fs.readFileSync( + path.resolve(import.meta.dirname, '../src/routes/read/file.txt'), + 'utf-8' + ); const response = await request.get('/read'); expect(await response.text()).toBe(content); }); + +test('_redirects are copied to publish directory', () => { + const redirects = fs.readFileSync( + path.resolve(import.meta.dirname, '../build/_redirects'), + 'utf-8' + ); + expect(redirects).toContain('/redirect-me /greeting/redirected 301'); +}); + +test('_headers are copied to publish directory', () => { + const headers = fs.readFileSync(path.resolve(import.meta.dirname, '../build/_headers'), 'utf-8'); + expect(headers).toContain('X-Custom-Header: test-value'); +}); diff --git a/packages/adapter-netlify/test/apps/edge/_headers b/packages/adapter-netlify/test/apps/edge/_headers new file mode 100644 index 000000000000..01c0aab8e046 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/_headers @@ -0,0 +1,2 @@ +/custom-header-path + X-Custom-Header: test-value diff --git a/packages/adapter-netlify/test/apps/edge/_redirects b/packages/adapter-netlify/test/apps/edge/_redirects new file mode 100644 index 000000000000..fd5703a26e6a --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/_redirects @@ -0,0 +1 @@ +/redirect-me /greeting/redirected 301 diff --git a/packages/adapter-netlify/test/apps/edge/netlify.toml b/packages/adapter-netlify/test/apps/edge/netlify.toml index fba5510330d6..cc05e8c07a1d 100644 --- a/packages/adapter-netlify/test/apps/edge/netlify.toml +++ b/packages/adapter-netlify/test/apps/edge/netlify.toml @@ -1,14 +1,11 @@ [build] publish = "build" -# TODO: remove these once we overhaul the Netlify adapter to use the new edge declarations https://docs.netlify.com/build/edge-functions/declarations/#declare-edge-functions-inline +# TODO(serhalp): remove this when @netlify/dev supports serve/preview. +# @netlify/dev does not yet have explicit support for a "serve" mode, but this funky workaround is +# sufficient for our purposes for now. NOTE: do not do this in real apps; this is only for testing. +edge_functions = ".netlify/v1/edge-functions" -# defaults to "netlify/edge-functions" (without the . prefix) -edge_functions = ".netlify/edge-functions" - -# the dev server doesn't read the manifest.json in edge-functions so we need -# to explicitly declare this here [[edge_functions]] +function = "sveltekit-render" path = "/*" -function = "render" -excludedPath = ["/_app/immutable/*", "/_app/version.json", "/.netlify/*"] diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/edge/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.server.js b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.server.js new file mode 100644 index 000000000000..31e005a89bad --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { name: params.name }; +} diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.svelte b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.svelte new file mode 100644 index 000000000000..4dcbba568369 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.svelte @@ -0,0 +1,5 @@ + + +

Hello {data.name}

diff --git a/packages/adapter-netlify/test/apps/edge/test/test.js b/packages/adapter-netlify/test/apps/edge/test/test.js index 50329932eb04..11818baa35c5 100644 --- a/packages/adapter-netlify/test/apps/edge/test/test.js +++ b/packages/adapter-netlify/test/apps/edge/test/test.js @@ -1,12 +1,29 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +test('page renders', async ({ request }) => { + const response = await request.get('/'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello from SvelteKit'); +}); + +test('dynamic route works', async ({ request }) => { + const response = await request.get('/greeting/world'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello world'); +}); test('read from $app/server works', async ({ request }) => { - const content = fs.readFileSync(path.resolve(__dirname, '../src/routes/read/file.txt'), 'utf-8'); + const content = fs.readFileSync( + path.resolve(import.meta.dirname, '../src/routes/read/file.txt'), + 'utf-8' + ); const response = await request.get('/read'); expect(await response.text()).toBe(content); }); + +test('_headers are copied to publish directory', () => { + const headers = fs.readFileSync(path.resolve(import.meta.dirname, '../build/_headers'), 'utf-8'); + expect(headers).toContain('X-Custom-Header: test-value'); +}); diff --git a/packages/adapter-netlify/test/apps/instrumentation/.gitignore b/packages/adapter-netlify/test/apps/instrumentation/.gitignore new file mode 100644 index 000000000000..88f661c765c6 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +/.svelte-kit +/.netlify +/build +deno.lock \ No newline at end of file diff --git a/packages/adapter-netlify/test/apps/instrumentation/netlify.toml b/packages/adapter-netlify/test/apps/instrumentation/netlify.toml new file mode 100644 index 000000000000..d2d45458b84f --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/netlify.toml @@ -0,0 +1,8 @@ +[build] +publish = "build" + +# TODO(serhalp): remove this when @netlify/dev supports serve/preview. +# @netlify/dev does not yet have explicit support for a "serve" mode, but this funky workaround is +# sufficient for our purposes for now. NOTE: do not do this in real apps; this is only for testing. +[functions] +directory = "./.netlify/v1/functions" diff --git a/packages/adapter-netlify/test/apps/instrumentation/package.json b/packages/adapter-netlify/test/apps/instrumentation/package.json new file mode 100644 index 000000000000..e2f181b4503a --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/package.json @@ -0,0 +1,19 @@ +{ + "name": "test-netlify-instrumentation", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "node ../../preview.js", + "prepare": "svelte-kit sync || echo ''", + "test": "playwright test" + }, + "devDependencies": { + "@sveltejs/kit": "workspace:^", + "@sveltejs/vite-plugin-svelte": "catalog:", + "svelte": "catalog:", + "vite": "catalog:" + }, + "type": "module" +} diff --git a/packages/adapter-netlify/test/apps/instrumentation/playwright.config.js b/packages/adapter-netlify/test/apps/instrumentation/playwright.config.js new file mode 100644 index 000000000000..33d36b651014 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/playwright.config.js @@ -0,0 +1 @@ +export { config as default } from '../../utils.js'; diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/app.html b/packages/adapter-netlify/test/apps/instrumentation/src/app.html new file mode 100644 index 000000000000..26bc25de4e52 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/app.html @@ -0,0 +1,12 @@ + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/instrumentation/src/instrumentation.server.js new file mode 100644 index 000000000000..00368839194c --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/instrumentation.server.js @@ -0,0 +1 @@ +globalThis.__INSTRUMENTATION_RAN__ = true; diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/instrumentation/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/routes/instrumented/+server.js b/packages/adapter-netlify/test/apps/instrumentation/src/routes/instrumented/+server.js new file mode 100644 index 000000000000..def88b135b5f --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/routes/instrumented/+server.js @@ -0,0 +1,3 @@ +export function GET() { + return new Response(String(globalThis.__INSTRUMENTATION_RAN__ === true)); +} diff --git a/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js b/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js new file mode 100644 index 000000000000..050579db13ba --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from '../../../index.js'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + experimental: { + instrumentation: { + server: true + } + } + } +}; + +export default config; diff --git a/packages/adapter-netlify/test/apps/instrumentation/test/test.js b/packages/adapter-netlify/test/apps/instrumentation/test/test.js new file mode 100644 index 000000000000..452b099714a3 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/test/test.js @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; + +test('page renders', async ({ request }) => { + const response = await request.get('/'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello from SvelteKit'); +}); + +test('instrumentation.server.js runs at startup', async ({ request }) => { + const response = await request.get('/instrumented'); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('true'); +}); diff --git a/packages/adapter-netlify/test/apps/instrumentation/tsconfig.json b/packages/adapter-netlify/test/apps/instrumentation/tsconfig.json new file mode 100644 index 000000000000..34380ebc986e --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + "extends": "./.svelte-kit/tsconfig.json" +} diff --git a/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts b/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts new file mode 100644 index 000000000000..72a307cc0e56 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import type { UserConfig } from 'vite'; + +const config: UserConfig = { + build: { + minify: false + }, + plugins: [sveltekit()] +}; + +export default config; diff --git a/packages/adapter-netlify/test/apps/split/.gitignore b/packages/adapter-netlify/test/apps/split/.gitignore index 88f661c765c6..4150a3674e3a 100644 --- a/packages/adapter-netlify/test/apps/split/.gitignore +++ b/packages/adapter-netlify/test/apps/split/.gitignore @@ -3,4 +3,4 @@ node_modules /.svelte-kit /.netlify /build -deno.lock \ No newline at end of file +deno.lock diff --git a/packages/adapter-netlify/test/apps/split/_redirects b/packages/adapter-netlify/test/apps/split/_redirects new file mode 100644 index 000000000000..fd5703a26e6a --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/_redirects @@ -0,0 +1 @@ +/redirect-me /greeting/redirected 301 diff --git a/packages/adapter-netlify/test/apps/split/netlify.toml b/packages/adapter-netlify/test/apps/split/netlify.toml index 5bd2fc95156c..a19ca0df6798 100644 --- a/packages/adapter-netlify/test/apps/split/netlify.toml +++ b/packages/adapter-netlify/test/apps/split/netlify.toml @@ -1,9 +1,8 @@ [build] publish = "build" -# TODO: remove this after we refactor to the Netlify frameworks API -# we are purposely misusing the user functions config to discover our framework -# build output because our adapter still outputs using an older API but the new -# Netlify dev server adheres to the new API +# TODO(serhalp): remove this when @netlify/dev supports serve/preview. +# @netlify/dev does not yet have explicit support for a "serve" mode, but this funky workaround is +# sufficient for our purposes for now. NOTE: do not do this in real apps; this is only for testing. [functions] -directory = ".netlify/functions-internal" +directory = ".netlify/v1/functions" diff --git a/packages/adapter-netlify/test/apps/split/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/split/src/instrumentation.server.js deleted file mode 100644 index acc9022e1d64..000000000000 --- a/packages/adapter-netlify/test/apps/split/src/instrumentation.server.js +++ /dev/null @@ -1 +0,0 @@ -// this is just here to make sure the changes resulting from it work diff --git a/packages/adapter-netlify/test/apps/split/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/split/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.server.js b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.server.js new file mode 100644 index 000000000000..31e005a89bad --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { name: params.name }; +} diff --git a/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.svelte b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.svelte new file mode 100644 index 000000000000..4dcbba568369 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.svelte @@ -0,0 +1,5 @@ + + +

Hello {data.name}

diff --git a/packages/adapter-netlify/test/apps/split/svelte.config.js b/packages/adapter-netlify/test/apps/split/svelte.config.js index 82a386bcfc2e..4bbe888fd1cf 100644 --- a/packages/adapter-netlify/test/apps/split/svelte.config.js +++ b/packages/adapter-netlify/test/apps/split/svelte.config.js @@ -6,9 +6,6 @@ const config = { kit: { adapter: adapter({ split: true }), experimental: { - instrumentation: { - server: true - }, remoteFunctions: true } } diff --git a/packages/adapter-netlify/test/apps/split/test/test.js b/packages/adapter-netlify/test/apps/split/test/test.js index 1e502c5c559a..4093bf3fb413 100644 --- a/packages/adapter-netlify/test/apps/split/test/test.js +++ b/packages/adapter-netlify/test/apps/split/test/test.js @@ -1,3 +1,5 @@ +import fs from 'node:fs'; +import path from 'node:path'; import { expect, test } from '@playwright/test'; test('routes to routes with dynamic params', async ({ page }) => { @@ -20,3 +22,17 @@ test('client-side fetch for query remote function data', async ({ page }) => { await page.goto('/remote/query'); await expect(page.locator('p')).toHaveText('a: 1'); }); + +test('split generates multiple function files', () => { + const functions_dir = path.resolve(import.meta.dirname, '../.netlify/v1/functions'); + const files = fs.readdirSync(functions_dir).filter((f) => f.startsWith('sveltekit-')); + expect(files.length).toBeGreaterThan(1); +}); + +test('_redirects are copied to publish directory', () => { + const redirects = fs.readFileSync( + path.resolve(import.meta.dirname, '../build/_redirects'), + 'utf-8' + ); + expect(redirects).toContain('/redirect-me /greeting/redirected 301'); +}); diff --git a/packages/adapter-netlify/test/preview.js b/packages/adapter-netlify/test/preview.js index 603d47741d82..c142137e9663 100644 --- a/packages/adapter-netlify/test/preview.js +++ b/packages/adapter-netlify/test/preview.js @@ -6,19 +6,20 @@ import process from 'node:process'; import { getRequest, setResponse } from '@sveltejs/kit/node'; const netlifyDev = new NetlifyDev({}); - -const serverReady = netlifyDev.start(); +await netlifyDev.start(); const port = process.env.PORT ? +process.env.PORT : 8888; const base = `http://localhost:${port}`; http .createServer(async (req, res) => { - await serverReady; const request = await getRequest({ request: req, base }); - const response = - (await netlifyDev.handle(request)) ?? new Response('Not Found', { status: 404 }); - await setResponse(res, response); + const response = await netlifyDev.handle(request); + if (response) { + await setResponse(res, response); + return; + } + await setResponse(res, new Response('Not Found', { status: 404 })); }) .listen(port); console.log(`Netlify Dev listening on http://localhost:${port}`); diff --git a/packages/adapter-netlify/utils.js b/packages/adapter-netlify/utils.js new file mode 100644 index 000000000000..7c90fea67e87 --- /dev/null +++ b/packages/adapter-netlify/utils.js @@ -0,0 +1,78 @@ +import { resolve } from 'node:path'; +import process from 'node:process'; + +/** + * @typedef {{ rest: boolean, dynamic: boolean, content: string }} RouteSegment + */ + +/** + * @typedef {{ + * build?: { publish?: string } + * functions?: { node_bundler?: 'zisi' | 'esbuild' } + * }} NetlifyConfig + */ + +/** + * @param {RouteSegment[]} a + * @param {RouteSegment[]} b + * @returns {boolean} + */ +export function matches(a, b) { + if (a[0] && b[0]) { + if (b[0].rest) { + if (b.length === 1) return true; + + const next_b = b.slice(1); + + for (let i = 0; i < a.length; i += 1) { + if (matches(a.slice(i), next_b)) return true; + } + + return false; + } + + if (!b[0].dynamic) { + if (!a[0].dynamic && a[0].content !== b[0].content) return false; + } + + if (a.length === 1 && b.length === 1) return true; + return matches(a.slice(1), b.slice(1)); + } else if (a[0]) { + return a.length === 1 && a[0].rest; + } else { + return b.length === 1 && b[0].rest; + } +} + +/** + * @param {NetlifyConfig | null} netlify_config + * @param {import('@sveltejs/kit').Builder} builder + * @returns {string | undefined} + */ +export function get_publish_directory(netlify_config, builder) { + if (netlify_config) { + if (!netlify_config.build?.publish) { + builder.log.minor('No publish directory specified in netlify.toml, using default'); + return; + } + + if (resolve(netlify_config.build.publish) === process.cwd()) { + throw new Error( + 'The publish directory cannot be set to the site root. Please change it to another value such as "build" in netlify.toml.' + ); + } + return netlify_config.build.publish; + } + + builder.log.warn( + 'No netlify.toml found. Using default publish directory. Consult https://svelte.dev/docs/kit/adapter-netlify#usage for more details' + ); +} + +/** + * @param {*} value + * @returns {string} + */ +export function s(value) { + return JSON.stringify(value, null, '\t'); +} diff --git a/packages/adapter-netlify/utils.spec.js b/packages/adapter-netlify/utils.spec.js new file mode 100644 index 000000000000..60da6beda5cd --- /dev/null +++ b/packages/adapter-netlify/utils.spec.js @@ -0,0 +1,161 @@ +import process from 'node:process'; +import { describe, test, expect, vi } from 'vitest'; +import { matches, get_publish_directory } from './utils.js'; + +/** + * Helper to create a static route segment + * @param {string} content + * @returns {{ rest: boolean, dynamic: boolean, content: string }} + */ +function static_segment(content) { + return { rest: false, dynamic: false, content }; +} + +/** + * Helper to create a dynamic route segment + * @param {string} content + * @returns {{ rest: boolean, dynamic: boolean, content: string }} + */ +function dynamic_segment(content) { + return { rest: false, dynamic: true, content }; +} + +/** + * Helper to create a rest route segment + * @param {string} content + * @returns {{ rest: boolean, dynamic: boolean, content: string }} + */ +function rest_segment(content) { + return { rest: true, dynamic: true, content }; +} + +describe('matches', () => { + test('two identical static routes match', () => { + expect( + matches( + [static_segment('blog'), static_segment('post')], + [static_segment('blog'), static_segment('post')] + ) + ).toBe(true); + }); + + test('static segment mismatch returns false', () => { + expect(matches([static_segment('blog')], [static_segment('about')])).toBe(false); + }); + + test('dynamic segment matches any static segment', () => { + expect(matches([static_segment('blog')], [dynamic_segment('[slug]')])).toBe(true); + }); + + test('rest segment at end matches everything', () => { + expect( + matches([static_segment('blog'), static_segment('post')], [rest_segment('[...rest]')]) + ).toBe(true); + }); + + test('rest-only route matches any route', () => { + expect( + matches( + [static_segment('a'), static_segment('b'), static_segment('c')], + [rest_segment('[...rest]')] + ) + ).toBe(true); + }); + + test('rest segment matches remaining segments', () => { + expect( + matches( + [static_segment('blog'), static_segment('2024'), static_segment('post')], + [static_segment('blog'), rest_segment('[...rest]')] + ) + ).toBe(true); + }); + + test('empty segments (index routes)', () => { + expect(matches([], [])).toBe(false); + }); + + test('mismatched lengths without rest return false', () => { + expect( + matches([static_segment('blog'), static_segment('post')], [static_segment('blog')]) + ).toBe(false); + }); + + test('rest with following segments', () => { + expect( + matches( + [static_segment('a'), static_segment('b'), static_segment('page')], + [rest_segment('[...rest]'), static_segment('page')] + ) + ).toBe(true); + }); + + test('rest with following segments that do not match', () => { + expect( + matches( + [static_segment('a'), static_segment('b'), static_segment('other')], + [rest_segment('[...rest]'), static_segment('page')] + ) + ).toBe(false); + }); + + test('a has trailing rest and b is shorter', () => { + expect(matches([rest_segment('[...rest]')], [static_segment('blog')])).toBe(true); + }); + + test('a is longer without rest in b', () => { + expect(matches([static_segment('a'), static_segment('b')], [static_segment('a')])).toBe(false); + }); + + test('b is longer without rest in a', () => { + expect(matches([static_segment('a')], [static_segment('a'), static_segment('b')])).toBe(false); + }); + + test('dynamic in a matches static in b', () => { + expect(matches([dynamic_segment('[id]')], [static_segment('blog')])).toBe(true); + }); + + test('both dynamic segments match', () => { + expect(matches([dynamic_segment('[id]')], [dynamic_segment('[slug]')])).toBe(true); + }); +}); + +describe('get_publish_directory', () => { + test('returns undefined when no netlify.toml, with warning logged', () => { + const warn = vi.fn(); + const builder = /** @type {any} */ ({ log: { warn, minor: vi.fn() } }); + + const result = get_publish_directory(null, builder); + + expect(result).toBeUndefined(); + expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('No netlify.toml found')); + }); + + test('returns undefined when config has no build.publish, with minor log', () => { + const minor = vi.fn(); + const builder = /** @type {any} */ ({ log: { warn: vi.fn(), minor } }); + + const result = get_publish_directory({ build: {} }, builder); + + expect(result).toBeUndefined(); + expect(minor).toHaveBeenCalledOnce(); + expect(minor).toHaveBeenCalledWith(expect.stringContaining('No publish directory specified')); + }); + + test('returns the publish value when specified', () => { + const builder = /** @type {any} */ ({ log: { warn: vi.fn(), minor: vi.fn() } }); + + const result = get_publish_directory({ build: { publish: 'dist' } }, builder); + + expect(result).toBe('dist'); + }); + + test('throws when publish is site root', () => { + const builder = /** @type {any} */ ({ log: { warn: vi.fn(), minor: vi.fn() } }); + + expect(() => get_publish_directory({ build: { publish: process.cwd() } }, builder)).toThrow( + 'The publish directory cannot be set to the site root' + ); + }); +}); diff --git a/packages/adapter-netlify/vitest.config.js b/packages/adapter-netlify/vitest.config.js new file mode 100644 index 000000000000..a15b3a470a6d --- /dev/null +++ b/packages/adapter-netlify/vitest.config.js @@ -0,0 +1,5 @@ +// we need this file to prevent Vitest from resolving a Vitest config from another directory + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index f6efaf07b991..3949e366ea8a 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,20 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { rollup } from 'rollup'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; - -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_4_0 - */ +import { rolldown } from 'rolldown'; const files = fileURLToPath(new URL('./files', import.meta.url).href); @@ -24,7 +10,6 @@ export default function (opts = {}) { return { name: '@sveltejs/adapter-node', - /** @param {Builder2_4_0} builder */ async adapt(builder) { const tmp = builder.getBuildDirectory('adapter-node'); @@ -65,29 +50,23 @@ export default function (opts = {}) { manifest: `${tmp}/manifest.js` }; - if (builder.hasServerInstrumentationFile?.()) { + if (builder.hasServerInstrumentationFile()) { input['instrumentation.server'] = `${tmp}/instrumentation.server.js`; } // we bundle the Vite output so that deployments only need // their production dependencies. Anything in devDependencies // will get included in the bundled code - const bundle = await rollup({ + const bundle = await rolldown({ input, external: [ // dependencies could have deep exports, so we need a regex ...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\\/.*)?$`)) ], - plugins: [ - nodeResolve({ - preferBuiltins: true, - exportConditions: ['node'] - }), - // @ts-ignore https://github.com/rollup/plugins/issues/1329 - commonjs({ strictRequires: true }), - // @ts-ignore https://github.com/rollup/plugins/issues/1329 - json() - ] + platform: 'node', + resolve: { + conditionNames: ['node'] + } }); await bundle.write({ @@ -103,14 +82,13 @@ export default function (opts = {}) { HANDLER: './handler.js', MANIFEST: './server/manifest.js', SERVER: './server/index.js', - SHIMS: './shims.js', ENV_PREFIX: JSON.stringify(envPrefix), PRECOMPRESS: JSON.stringify(precompress) } }); - if (builder.hasServerInstrumentationFile?.()) { - builder.instrument?.({ + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ entrypoint: `${out}/index.js`, instrumentation: `${out}/server/instrumentation.server.js`, module: { diff --git a/packages/adapter-node/internal.d.ts b/packages/adapter-node/internal.d.ts index 147c5af6b49f..8387855f7893 100644 --- a/packages/adapter-node/internal.d.ts +++ b/packages/adapter-node/internal.d.ts @@ -18,5 +18,3 @@ declare module 'MANIFEST' { declare module 'SERVER' { export { Server } from '@sveltejs/kit'; } - -declare module 'SHIMS' {} diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json index 55b8e6d91fa7..3121921749bc 100644 --- a/packages/adapter-node/package.json +++ b/packages/adapter-node/package.json @@ -33,8 +33,8 @@ "ambient.d.ts" ], "scripts": { - "dev": "rollup -cw", - "build": "rollup -c", + "dev": "rolldown -cw", + "build": "rolldown -c", "test": "vitest run", "check": "tsc", "lint": "prettier --check .", @@ -51,12 +51,9 @@ "vitest": "catalog:" }, "dependencies": { - "@rollup/plugin-commonjs": "^29.0.0", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "rollup": "^4.59.0" + "rolldown": "^1.0.0-rc.6" }, "peerDependencies": { - "@sveltejs/kit": "^2.4.0" + "@sveltejs/kit": "^3.0.0" } } diff --git a/packages/adapter-node/rolldown.config.js b/packages/adapter-node/rolldown.config.js new file mode 100644 index 000000000000..d84faba8ae5a --- /dev/null +++ b/packages/adapter-node/rolldown.config.js @@ -0,0 +1,67 @@ +import { builtinModules } from 'node:module'; +import { rmSync } from 'node:fs'; + +/** + * @param {string} filepath + * @returns {import('rolldown').Plugin} + */ +function clearOutput(filepath) { + return { + name: 'clear-output', + buildStart: { + order: 'pre', + sequential: true, + handler() { + rmSync(filepath, { recursive: true, force: true }); + } + } + }; +} + +/** + * @returns {import('rolldown').Plugin} + */ +function prefixBuiltinModules() { + return { + name: 'prefix-built-in-modules', + resolveId(source) { + if (builtinModules.includes(source)) { + return { id: 'node:' + source, external: true }; + } + } + }; +} + +export default [ + { + input: 'src/index.js', + output: { + file: 'files/index.js', + format: 'esm' + }, + plugins: [clearOutput('files/index.js'), prefixBuiltinModules()], + external: ['ENV', 'HANDLER'], + platform: 'node' + }, + { + input: 'src/env.js', + output: { + file: 'files/env.js', + format: 'esm' + }, + plugins: [clearOutput('files/env.js'), prefixBuiltinModules()], + external: ['HANDLER'], + platform: 'node' + }, + { + input: 'src/handler.js', + output: { + file: 'files/handler.js', + format: 'esm', + codeSplitting: false + }, + plugins: [clearOutput('files/handler.js'), prefixBuiltinModules()], + external: ['ENV', 'MANIFEST', 'SERVER'], + platform: 'node' + } +]; diff --git a/packages/adapter-node/rollup.config.js b/packages/adapter-node/rollup.config.js deleted file mode 100644 index 40f8e07558aa..000000000000 --- a/packages/adapter-node/rollup.config.js +++ /dev/null @@ -1,93 +0,0 @@ -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import { builtinModules } from 'node:module'; -import { rmSync } from 'node:fs'; - -/** - * @param {string} filepath - * @returns {import('rollup').Plugin} - */ -function clearOutput(filepath) { - return { - name: 'clear-output', - buildStart: { - order: 'pre', - sequential: true, - handler() { - rmSync(filepath, { recursive: true, force: true }); - } - } - }; -} - -/** - * @returns {import('rollup').Plugin} - */ -function prefixBuiltinModules() { - return { - name: 'prefix-built-in-modules', - resolveId(source) { - if (builtinModules.includes(source)) { - return { id: 'node:' + source, external: true }; - } - } - }; -} - -export default [ - { - input: 'src/index.js', - output: { - file: 'files/index.js', - format: 'esm' - }, - plugins: [ - clearOutput('files/index.js'), - nodeResolve({ preferBuiltins: true }), - commonjs(), - json(), - prefixBuiltinModules() - ], - external: ['ENV', 'HANDLER'] - }, - { - input: 'src/env.js', - output: { - file: 'files/env.js', - format: 'esm' - }, - plugins: [ - clearOutput('files/env.js'), - nodeResolve(), - commonjs(), - json(), - prefixBuiltinModules() - ], - external: ['HANDLER'] - }, - { - input: 'src/handler.js', - output: { - file: 'files/handler.js', - format: 'esm', - inlineDynamicImports: true - }, - plugins: [ - clearOutput('files/handler.js'), - nodeResolve(), - commonjs(), - json(), - prefixBuiltinModules() - ], - external: ['ENV', 'MANIFEST', 'SERVER', 'SHIMS'] - }, - { - input: 'src/shims.js', - output: { - file: 'files/shims.js', - format: 'esm' - }, - plugins: [clearOutput('files/shims.js'), nodeResolve(), commonjs(), prefixBuiltinModules()] - } -]; diff --git a/packages/adapter-node/src/env.js b/packages/adapter-node/src/env.js index 05251f8658b2..f7e5c2edf0d9 100644 --- a/packages/adapter-node/src/env.js +++ b/packages/adapter-node/src/env.js @@ -1,4 +1,3 @@ -/* global ENV_PREFIX */ import process from 'node:process'; const expected = new Set([ diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index b8fd2f38cbe5..a6c7d9eca50a 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -1,4 +1,3 @@ -import 'SHIMS'; import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; @@ -11,9 +10,6 @@ import { manifest, prerendered, base } from 'MANIFEST'; import { env } from 'ENV'; import { parse_as_bytes, parse_origin } from '../utils.js'; -/* global ENV_PREFIX */ -/* global PRECOMPRESS */ - const server = new Server(manifest); // parse_origin validates ORIGIN and throws descriptive errors for invalid values diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js deleted file mode 100644 index 2490311daa1e..000000000000 --- a/packages/adapter-node/src/shims.js +++ /dev/null @@ -1,2 +0,0 @@ -import { installPolyfills } from '@sveltejs/kit/node/polyfills'; -installPolyfills(); diff --git a/packages/adapter-node/tsconfig.json b/packages/adapter-node/tsconfig.json index 6bcfb5cb7c2d..d5fcb3c28f4a 100644 --- a/packages/adapter-node/tsconfig.json +++ b/packages/adapter-node/tsconfig.json @@ -13,6 +13,8 @@ }, "include": [ "index.js", + "rolldown.config.js", + "vitest.config.js", "src/**/*.js", "tests/**/*.js", "tests/**/*.ts", diff --git a/packages/adapter-node/vitest.config.js b/packages/adapter-node/vitest.config.js new file mode 100644 index 000000000000..a15b3a470a6d --- /dev/null +++ b/packages/adapter-node/vitest.config.js @@ -0,0 +1,5 @@ +// we need this file to prevent Vitest from resolving a Vitest config from another directory + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index c2b87301c8a8..6ba84906c6c5 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -5,7 +5,6 @@ import { platforms } from './platforms.js'; export default function (options) { return { name: '@sveltejs/adapter-static', - /** @param {import('./internal.js').Builder2_0_0} builder */ async adapt(builder) { if (!options?.fallback && builder.config.kit.router?.type !== 'hash') { const dynamic_routes = builder.routes.filter((route) => route.prerender !== true); @@ -52,7 +51,6 @@ See https://svelte.dev/docs/kit/page-options#prerender for more details` } const { - // @ts-ignore pages = 'build', assets = pages, fallback, diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json index 488ef1d88c09..019628f67f5a 100644 --- a/packages/adapter-static/package.json +++ b/packages/adapter-static/package.json @@ -48,6 +48,6 @@ "vite": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "@sveltejs/kit": "^3.0.0" } } diff --git a/packages/adapter-static/platforms.js b/packages/adapter-static/platforms.js index 6ed421625668..51a6bf0a3562 100644 --- a/packages/adapter-static/platforms.js +++ b/packages/adapter-static/platforms.js @@ -6,12 +6,12 @@ import process from 'node:process'; * name: string; * test: () => boolean; * defaults: import('./index.js').AdapterOptions; - * done: (builder: import('./internal.js').Builder2_0_0) => void; + * done: (builder: import('@sveltejs/kit').Builder) => void; * }} * Platform */ // This function is duplicated in adapter-vercel -/** @param {import('./internal.js').Builder2_0_0} builder */ +/** @param {import('@sveltejs/kit').Builder} builder */ function static_vercel_config(builder) { /** @type {any[]} */ const prerendered_redirects = []; diff --git a/packages/adapter-static/tsconfig.json b/packages/adapter-static/tsconfig.json index 7395ffa36b2e..22ea3b1d6c08 100644 --- a/packages/adapter-static/tsconfig.json +++ b/packages/adapter-static/tsconfig.json @@ -11,5 +11,5 @@ }, "types": ["node"] }, - "include": ["index.js", "internal.d.ts", "test/utils.js"] + "include": ["index.js", "test/utils.js"] } diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index d6213e554d4b..a17c6e3d6194 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -1,32 +1,45 @@ -/** @import { BuildOptions } from 'esbuild' */ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; +import { VERSION } from '@sveltejs/kit'; import { nodeFileTrace } from '@vercel/nft'; -import esbuild from 'esbuild'; +import { build } from 'rolldown'; import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; -import { VERSION } from '@sveltejs/kit'; - -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_4_0 - */ const name = '@sveltejs/adapter-vercel'; const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user routes -const [kit_major, kit_minor] = VERSION.split('.'); - // https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules const compatible_node_modules = ['async_hooks', 'events', 'buffer', 'assert', 'util']; +/** @satisfies {import('rolldown').BuildOptions} */ +const rolldown_config = { + platform: 'browser', + resolve: { + conditionNames: [ + // Vercel's Edge runtime key https://runtime-keys.proposal.wintercg.org/#edge-light + 'edge-light', + // re-include these since they are included by default when no conditions are specified + 'import', + 'browser', + 'default' + ] + }, + external: [...compatible_node_modules, ...compatible_node_modules.map((id) => `node:${id}`)], + transform: { + // minimum Node.js version supported is v14.6.0 that is mapped to ES2019 + // https://edge-runtime.vercel.app/features/polyfills + // TODO verify the latest ES version the edge runtime supports + target: 'es2022' + }, + output: { + sourcemap: true, + banner: () => 'globalThis.global = globalThis;', + codeSplitting: false + } +}; + /** @type {import('./index.js').default} **/ const plugin = function (defaults = {}) { if ('edge' in defaults) { @@ -35,7 +48,7 @@ const plugin = function (defaults = {}) { return { name, - /** @param {Builder2_4_0} builder */ + /** @param {import('@sveltejs/kit').Builder} builder */ async adapt(builder) { if (!builder.routes) { throw new Error( @@ -87,8 +100,8 @@ const plugin = function (defaults = {}) { MANIFEST: './manifest.js' } }); - if (builder.hasServerInstrumentationFile?.()) { - builder.instrument?.({ + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ entrypoint: `${tmp}/index.js`, instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js` }); @@ -139,53 +152,34 @@ const plugin = function (defaults = {}) { try { const outdir = `${dirs.functions}/${name}.func`; - /** @type {BuildOptions} */ - const esbuild_config = { - // minimum Node.js version supported is v14.6.0 that is mapped to ES2019 - // https://edge-runtime.vercel.app/features/polyfills - // TODO verify the latest ES version the edge runtime supports - target: 'es2020', - bundle: true, - platform: 'browser', - conditions: [ - // Vercel's Edge runtime key https://runtime-keys.proposal.wintercg.org/#edge-light - 'edge-light', - // re-include these since they are included by default when no conditions are specified - // https://esbuild.github.io/api/#conditions - 'module' - ], - format: 'esm', - external: [ - ...compatible_node_modules, - ...compatible_node_modules.map((id) => `node:${id}`), - ...(config.external || []) - ], - sourcemap: 'linked', - banner: { js: 'globalThis.global = globalThis;' }, - loader: { - '.wasm': 'copy', - '.woff': 'copy', - '.woff2': 'copy', - '.ttf': 'copy', - '.eot': 'copy', - '.otf': 'copy' - } - }; - const result = await esbuild.build({ - entryPoints: [`${tmp}/edge.js`], - outfile: `${outdir}/index.js`, - ...esbuild_config - }); - let instrumentation_result; - if (builder.hasServerInstrumentationFile?.()) { - instrumentation_result = await esbuild.build({ - entryPoints: [`${builder.getServerDirectory()}/instrumentation.server.js`], - outfile: `${outdir}/instrumentation.server.js`, - ...esbuild_config - }); + const build_config = { + ...rolldown_config, + external: [...rolldown_config.external, ...(config.external || [])] + }; - builder.instrument?.({ + await Promise.all([ + build({ + ...build_config, + input: `${tmp}/edge.js`, + output: { + ...build_config.output, + file: `${outdir}/index.js` + } + }), + builder.hasServerInstrumentationFile() && + build({ + ...build_config, + input: `${builder.getServerDirectory()}/instrumentation.server.js`, + output: { + ...build_config.output, + file: `${outdir}/instrumentation.server.js` + } + }) + ]); + + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ entrypoint: `${outdir}/index.js`, instrumentation: `${outdir}/instrumentation.server.js`, module: { @@ -193,45 +187,10 @@ const plugin = function (defaults = {}) { } }); } - - const warnings = instrumentation_result - ? [...result.warnings, ...instrumentation_result.warnings] - : result.warnings; - - if (warnings.length > 0) { - const formatted = await esbuild.formatMessages(warnings, { - kind: 'warning', - color: true - }); - - console.error(formatted.join('\n')); - } } catch (err) { - const error = /** @type {import('esbuild').BuildFailure} */ (err); - for (const e of error.errors) { - for (const node of e.notes) { - const match = - /The package "(.+)" wasn't found on the file system but is built into node/.exec( - node.text - ); - - if (match) { - node.text = `Cannot use "${match[1]}" when deploying to Vercel Edge Functions.`; - } - } - } - - const formatted = await esbuild.formatMessages(error.errors, { - kind: 'error', - color: true - }); - - console.error(formatted.join('\n')); - throw new Error( - `Bundling with esbuild failed with ${error.errors.length} ${ - error.errors.length === 1 ? 'error' : 'errors' - }`, + 'Bundling edge function with Rolldown failed' + + (err instanceof Error ? `: ${err.message}` : ''), { cause: err } ); } @@ -486,8 +445,7 @@ const plugin = function (defaults = {}) { } } - // optional chaining to support older versions that don't have this setting yet - if (builder.config.kit.router?.resolution === 'server') { + if (builder.config.kit.router.resolution === 'server') { // Create a separate edge function just for server-side route resolution. // By omitting all routes we're ensuring it's small (the routes will still be available // to the route resolution, because it does not rely on the server routing manifest) @@ -516,18 +474,7 @@ const plugin = function (defaults = {}) { }, supports: { - read: ({ config, route }) => { - const runtime = config.runtime ?? defaults.runtime; - - // TODO bump peer dep in next adapter major to simplify this - if (runtime === 'edge' && kit_major === '2' && kit_minor < '25') { - throw new Error( - `${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` configured with \`runtime: 'edge'\` and SvelteKit < 2.25.0` - ); - } - - return true; - }, + read: () => true, instrumentation: () => true } }; @@ -561,7 +508,7 @@ function write(file, data) { // This function is duplicated in adapter-static /** - * @param {Builder2_4_0} builder + * @param {import('@sveltejs/kit').Builder} builder * @param {import('./index.js').Config} config * @param {string} dir */ @@ -699,7 +646,7 @@ function static_vercel_config(builder, config, dir) { } /** - * @param {Builder2_4_0} builder + * @param {import('@sveltejs/kit').Builder} builder * @param {string} entry * @param {string} dir * @param {import('./index.js').ServerlessConfig} config @@ -820,18 +767,10 @@ async function create_function_bundle(builder, entry, dir, config) { } /** - * - * @param {Builder2_4_0} builder - * @param {any} vercel_config + * @param {import('@sveltejs/kit').Builder} builder + * @param {any} vercel_config see https://vercel.com/docs/project-configuration/vercel-json */ function validate_vercel_json(builder, vercel_config) { - if (builder.routes.length > 0 && !builder.routes[0].api) { - // bail — we're on an older SvelteKit version that doesn't - // populate `route.api.methods`, so we can't check - // to see if cron paths are valid - return; - } - const crons = /** @type {Array} */ ( Array.isArray(vercel_config?.crons) ? vercel_config.crons : [] ); diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json index dc650e8a5492..00e7836fc39e 100644 --- a/packages/adapter-vercel/package.json +++ b/packages/adapter-vercel/package.json @@ -37,11 +37,13 @@ "lint": "prettier --check .", "format": "pnpm lint --write", "check": "tsc", - "test": "vitest run" + "test:unit": "vitest run", + "test:build": "cd test/apps/basic && pnpm build", + "test": "pnpm test:unit && pnpm test:build" }, "dependencies": { "@vercel/nft": "^1.3.2", - "esbuild": "^0.25.4" + "rolldown": "^1.0.0-rc.6" }, "devDependencies": { "@sveltejs/kit": "workspace:^", @@ -50,9 +52,6 @@ "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.4.0" - }, - "engines": { - "node": ">=20.0" + "@sveltejs/kit": "^3.0.0" } } diff --git a/packages/adapter-vercel/vitest.config.js b/packages/adapter-vercel/vitest.config.js new file mode 100644 index 000000000000..a15b3a470a6d --- /dev/null +++ b/packages/adapter-vercel/vitest.config.js @@ -0,0 +1,5 @@ +// we need this file to prevent Vitest from resolving a Vitest config from another directory + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/packages/enhanced-img/package.json b/packages/enhanced-img/package.json index 84c29225d4c3..90d9da7dac70 100644 --- a/packages/enhanced-img/package.json +++ b/packages/enhanced-img/package.json @@ -41,21 +41,24 @@ "magic-string": "^0.30.5", "sharp": "^0.34.1", "svelte-parse-markup": "^0.1.5", - "vite-imagetools": "^9.0.3", + "vite-imagetools": "^10.0.0", "zimmerframe": "^1.1.2" }, "devDependencies": { "@types/estree": "catalog:", "@types/node": "catalog:", - "rollup": "^4.59.0", + "rolldown": "catalog:", "svelte": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^6.0.0 || ^7.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.0.0", - "vite": "^6.3.0 || >=7.0.0" + "vite": ">=8.0.0" + }, + "engines": { + "node": ">=22" } } diff --git a/packages/enhanced-img/src/vite-plugin.js b/packages/enhanced-img/src/vite-plugin.js index c8ccb26b3f17..105a1651acbf 100644 --- a/packages/enhanced-img/src/vite-plugin.js +++ b/packages/enhanced-img/src/vite-plugin.js @@ -33,7 +33,7 @@ export function image_plugin(imagetools_plugin) { } const api = svelteConfigPlugin.api; // @ts-expect-error plugin.transform is defined below before configResolved is called - plugin.transform.filter.id = (api.filter ?? api.idFilter).id; // TODO: idFilter was used by earlier versions of vite-plugin-svelte@6, remove when @7 is required + plugin.transform.filter.id = api.filter.id; }, transform: { order: 'pre', // puts it before vite-plugin-svelte:compile diff --git a/packages/enhanced-img/test/apps/basics/test/test.js b/packages/enhanced-img/test/apps/basics/test/test.js index 4644464605ff..d8bb982bbea6 100644 --- a/packages/enhanced-img/test/apps/basics/test/test.js +++ b/packages/enhanced-img/test/apps/basics/test/test.js @@ -1,8 +1,5 @@ import { expect, test } from '@playwright/test'; -import process from 'node:process'; -const is_node18 = process.versions.node.startsWith('18.'); -// TODO: remove with SvelteKit 3 -test.skip(is_node18, 'enhanced-img requires vite-plugin-svelte@6 which requires node20'); + test('images are properly rendered', async ({ page }) => { await page.goto('/'); diff --git a/packages/enhanced-img/test/markup-plugin.spec.js b/packages/enhanced-img/test/markup-plugin.spec.js index 1da359e91cff..33b9288a6c0c 100644 --- a/packages/enhanced-img/test/markup-plugin.spec.js +++ b/packages/enhanced-img/test/markup-plugin.spec.js @@ -3,7 +3,7 @@ import path from 'node:path'; import { expect, it } from 'vitest'; import { image_plugin, parse_object } from '../src/vite-plugin.js'; -const resolve = /** @param {string} file */ (file) => path.resolve(__dirname, file); +const resolve = /** @param {string} file */ (file) => path.resolve(import.meta.dirname, file); it('Image preprocess snapshot test', async () => { const filename = 'Input.svelte'; @@ -41,7 +41,7 @@ it('Image preprocess snapshot test', async () => { if (!transformed.code) throw new Error('transform did not return any code'); // Make imports readable - const ouput = transformed.code.replace(/import/g, '\n\timport'); + const ouput = transformed.code.toString().replace(/import/g, '\n\timport'); await expect(ouput).toMatchFileSnapshot('./Output.svelte'); }); diff --git a/packages/enhanced-img/test/utils.js b/packages/enhanced-img/test/utils.js index 3417a89c55f4..66d9762807b2 100644 --- a/packages/enhanced-img/test/utils.js +++ b/packages/enhanced-img/test/utils.js @@ -2,20 +2,15 @@ import { devices } from '@playwright/test'; import process from 'node:process'; import { number_from_env } from '../../../test-utils/index.js'; -// TODO: remove with SvelteKit 3 -const is_node18 = process.versions.node.startsWith('18.'); /** @type {import('@playwright/test').PlaywrightTestConfig} */ export const config = { forbidOnly: !!process.env.CI, // generous timeouts on CI timeout: process.env.CI ? 45000 : 15000, - webServer: is_node18 - ? undefined - : { - // do not try to build on node18 - command: 'pnpm build && pnpm preview', - port: 4173 - }, + webServer: { + command: 'pnpm build && pnpm preview', + port: 4173 + }, retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0), projects: [ { diff --git a/packages/enhanced-img/tsconfig.json b/packages/enhanced-img/tsconfig.json index aa8cebdd02c1..488271d87ab3 100644 --- a/packages/enhanced-img/tsconfig.json +++ b/packages/enhanced-img/tsconfig.json @@ -14,5 +14,5 @@ "noUnusedParameters": true, "types": ["node"] }, - "include": ["src/**/*", "types/**/*", "test/**/*"] + "include": ["src/**/*", "types/**/*", "test/**/*", "vitest.config.js"] } diff --git a/packages/enhanced-img/vitest.config.js b/packages/enhanced-img/vitest.config.js new file mode 100644 index 000000000000..a15b3a470a6d --- /dev/null +++ b/packages/enhanced-img/vitest.config.js @@ -0,0 +1,5 @@ +// we need this file to prevent Vitest from resolving a Vitest config from another directory + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/packages/kit/package.json b/packages/kit/package.json index 622537590943..c26d1a8d4369 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -20,15 +20,12 @@ "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", - "@types/cookie": "^0.6.0", "acorn": "^8.14.1", - "cookie": "^0.6.0", + "cookie": "^1.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.2", - "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "devDependencies": { @@ -37,20 +34,20 @@ "@sveltejs/vite-plugin-svelte": "catalog:", "@types/connect": "catalog:", "@types/node": "catalog:", - "@types/set-cookie-parser": "catalog:", - "dts-buddy": "^0.7.0", - "rollup": "^4.59.0", + "dts-buddy": "catalog:", + "rolldown": "catalog:", "svelte": "catalog:", + "svelte-preprocess": "catalog:", "typescript": "^5.3.3", "vite": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", "@opentelemetry/api": "^1.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", + "svelte": "^5.48.0", "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + "vite": "^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -115,10 +112,6 @@ "types": "./types/index.d.ts", "import": "./src/exports/node/index.js" }, - "./node/polyfills": { - "types": "./types/index.d.ts", - "import": "./src/exports/node/polyfills.js" - }, "./hooks": { "types": "./types/index.d.ts", "import": "./src/exports/hooks/index.js" @@ -130,6 +123,6 @@ }, "types": "types/index.d.ts", "engines": { - "node": ">=18.13" + "node": ">=22" } } diff --git a/packages/kit/scripts/generate-dts.js b/packages/kit/scripts/generate-dts.js index b470d6d63494..60c7fe7d103f 100644 --- a/packages/kit/scripts/generate-dts.js +++ b/packages/kit/scripts/generate-dts.js @@ -7,7 +7,6 @@ await createBundle({ '@sveltejs/kit': 'src/exports/public.d.ts', '@sveltejs/kit/hooks': 'src/exports/hooks/index.js', '@sveltejs/kit/node': 'src/exports/node/index.js', - '@sveltejs/kit/node/polyfills': 'src/exports/node/polyfills.js', '@sveltejs/kit/vite': 'src/exports/vite/index.js', '$app/environment': 'src/runtime/app/environment/types.d.ts', '$app/forms': 'src/runtime/app/forms.js', diff --git a/packages/kit/scripts/generate-version.js b/packages/kit/scripts/generate-version.js index ecf715c9a15c..1ab7960f55ab 100644 --- a/packages/kit/scripts/generate-version.js +++ b/packages/kit/scripts/generate-version.js @@ -1,8 +1,11 @@ import fs from 'node:fs'; +import path from 'node:path'; -const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); +const pkg = JSON.parse( + fs.readFileSync(path.join(import.meta.dirname, '..', 'package.json'), 'utf-8') +); fs.writeFileSync( - './src/version.js', + path.join(import.meta.dirname, '..', 'src', 'version.js'), `// generated during release, do not modify\n\n/** @type {string} */\nexport const VERSION = '${pkg.version}';\n` ); diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js index 99024d192d23..4b810ae9f697 100755 --- a/packages/kit/src/cli.js +++ b/packages/kit/src/cli.js @@ -1,7 +1,6 @@ import fs from 'node:fs'; import process from 'node:process'; -import { parseArgs } from 'node:util'; -import colors from 'kleur'; +import { parseArgs, styleText } from 'node:util'; import { load_config } from './core/config/index.js'; import { coalesce_to_error } from './utils/error.js'; @@ -11,9 +10,9 @@ function handle_error(e) { if (error.name === 'SyntaxError') throw error; - console.error(colors.bold().red(`> ${error.message}`)); + console.error(styleText(['bold', 'red'], `> ${error.message}`)); if (error.stack) { - console.error(colors.gray(error.stack.split('\n').slice(1).join('\n'))); + console.error(styleText('grey', error.stack.split('\n').slice(1).join('\n'))); } process.exit(1); @@ -48,7 +47,7 @@ try { }); } catch (err) { const error = /** @type {Error} */ (err); - console.error(colors.bold().red(`> ${error.message}`)); + console.error(styleText(['bold', 'red'], `> ${error.message}`)); console.log(help); process.exit(1); } @@ -82,14 +81,14 @@ if (command === 'sync') { } try { - const config = await load_config(); + const config = await load_config({ cwd: process.cwd() }); const sync = await import('./core/sync/sync.js'); sync.all_types(config, values.mode); } catch (error) { handle_error(error); } } else { - console.error(colors.bold().red(`> Unknown command: ${command}`)); + console.error(styleText(['bold', 'red'], `> Unknown command: ${command}`)); console.log(help); process.exit(1); } diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 42ff2145db31..bb5ef66edea7 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -2,11 +2,10 @@ /** @import { ResolvedConfig } from 'vite' */ /** @import { RouteDefinition } from '@sveltejs/kit' */ /** @import { RouteData, ValidatedConfig, BuildData, ServerMetadata, ServerMetadataRoute, Prerendered, PrerenderMap, Logger, RemoteChunk } from 'types' */ -import colors from 'kleur'; import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs'; import { extname, resolve, join, dirname, relative } from 'node:path'; import { pipeline } from 'node:stream'; -import { promisify } from 'node:util'; +import { promisify, styleText } from 'node:util'; import zlib from 'node:zlib'; import { copy, rimraf, mkdirp, posixify } from '../../utils/filesystem.js'; import { generate_manifest } from '../generate_manifest/index.js'; @@ -104,61 +103,11 @@ export function create_builder({ ); }, - async createEntries(fn) { - const seen = new Set(); - - for (let i = 0; i < route_data.length; i += 1) { - const route = route_data[i]; - if (prerender_map.get(route.id) === true) continue; - const { id, filter, complete } = fn(routes[i]); - - if (seen.has(id)) continue; - seen.add(id); - - const group = [route]; - - // figure out which lower priority routes should be considered fallbacks - for (let j = i + 1; j < route_data.length; j += 1) { - if (prerender_map.get(routes[j].id) === true) continue; - if (filter(routes[j])) { - group.push(route_data[j]); - } - } - - const filtered = new Set(group); - - // heuristic: if /foo/[bar] is included, /foo/[bar].json should - // also be included, since the page likely needs the endpoint - // TODO is this still necessary, given the new way of doing things? - filtered.forEach((route) => { - if (route.page) { - const endpoint = route_data.find((candidate) => candidate.id === route.id + '.json'); - - if (endpoint) { - filtered.add(endpoint); - } - } - }); - - if (filtered.size > 0) { - await complete({ - generateManifest: ({ relativePath }) => - generate_manifest({ - build_data, - prerendered: [], - relative_path: relativePath, - routes: Array.from(filtered), - remotes - }) - }); - } - } - }, - findServerAssets(route_data) { return find_server_assets( build_data, - route_data.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))) + route_data.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))), + vite_config.root ); }, @@ -168,16 +117,16 @@ export function create_builder({ const fallback = await generate_fallback({ manifest_path, - env: { ...env.private, ...env.public } + env: { ...env.private, ...env.public }, + root: vite_config.root }); if (existsSync(dest)) { console.log( - colors - .bold() - .yellow( - `Overwriting ${dest} with fallback page. Consider using a different name for the fallback.` - ) + styleText( + ['bold', 'yellow'], + `Overwriting ${dest} with fallback page. Consider using a different name for the fallback.` + ) ); } @@ -199,7 +148,8 @@ export function create_builder({ routes: subset ? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))) : route_data.filter((route) => prerender_map.get(route.id) !== true), - remotes + remotes, + root: vite_config.root }); }, diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index b69c7b8296bb..304a1ba5987d 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -6,11 +6,8 @@ import { create_builder } from './builder.js'; import { posixify } from '../../utils/filesystem.js'; import { list_files } from '../utils.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = join(__filename, '..'); - test('copy files', () => { - const cwd = join(__dirname, 'fixtures/basic'); + const cwd = join(import.meta.dirname, 'fixtures/basic'); const outDir = join(cwd, '.svelte-kit'); /** @type {import('@sveltejs/kit').Config} */ @@ -19,7 +16,7 @@ test('copy files', () => { kit: { appDir: '_app', files: { - assets: join(__dirname, 'fixtures/basic/static') + assets: join(import.meta.dirname, 'fixtures/basic/static') }, outDir } @@ -42,7 +39,7 @@ test('copy files', () => { log: {} }); - const dest = join(__dirname, 'output'); + const dest = join(import.meta.dirname, 'output'); rmSync(dest, { recursive: true, force: true }); @@ -74,8 +71,8 @@ test('compress files', async () => { }); test('instrument generates facade with posix paths', () => { - const fixtureDir = join(__dirname, 'fixtures/instrument'); - const dest = join(__dirname, 'output'); + const fixtureDir = join(import.meta.dirname, 'fixtures/instrument'); + const dest = join(import.meta.dirname, 'output'); rmSync(dest, { recursive: true, force: true }); mkdirSync(join(dest, 'server'), { recursive: true }); diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 48d14116369e..d50a57e36060 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -1,4 +1,4 @@ -import colors from 'kleur'; +import { styleText } from 'node:util'; import { create_builder } from './builder.js'; /** @@ -24,7 +24,7 @@ export async function adapt( // This is only called when adapter is truthy, so the cast is safe const { name, adapt } = /** @type {import('@sveltejs/kit').Adapter} */ (config.kit.adapter); - console.log(colors.bold().cyan(`\n> Using ${name}`)); + console.log(styleText(['bold', 'cyan'], `\n> Using ${name}`)); const builder = create_builder({ config, diff --git a/packages/kit/src/core/config/index.js b/packages/kit/src/core/config/index.js index 42146a89d4c3..6ed2f27d8ef3 100644 --- a/packages/kit/src/core/config/index.js +++ b/packages/kit/src/core/config/index.js @@ -59,10 +59,10 @@ export function load_error_page(config) { /** * Loads and validates Svelte config file - * @param {{ cwd?: string }} options + * @param {{ cwd: string }} options * @returns {Promise} */ -export async function load_config({ cwd = process.cwd() } = {}) { +export async function load_config({ cwd }) { const config_files = ['js', 'ts'] .map((ext) => path.join(cwd, `svelte.config.${ext}`)) .filter((f) => fs.existsSync(f)); @@ -94,11 +94,13 @@ export async function load_config({ cwd = process.cwd() } = {}) { /** * @param {import('@sveltejs/kit').Config} config + * @param {{ cwd: string }} options * @returns {import('types').ValidatedConfig} */ -function process_config(config, { cwd = process.cwd() } = {}) { +export function process_config(config, { cwd }) { const validated = validate_config(config, cwd); + validated.kit.env.dir = path.resolve(cwd, validated.kit.env.dir); validated.kit.outDir = path.resolve(cwd, validated.kit.outDir); for (const key in validated.kit.files) { diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 6b0e9808affc..9b76f9acd23a 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -1,12 +1,8 @@ import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { assert, expect, test } from 'vitest'; import { validate_config, load_config } from './index.js'; import process from 'node:process'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = join(__filename, '..'); - /** * mutates and remove keys from an object when check callback returns true * @param {Record} o any object @@ -68,7 +64,7 @@ const get_defaults = (prefix = '') => ({ reportOnly: directive_defaults }, csrf: { - checkOrigin: true, + checkOrigin: undefined, trustedOrigins: [] }, embedded: false, @@ -101,13 +97,14 @@ const get_defaults = (prefix = '') => ({ }, inlineStyleThreshold: 0, moduleExtensions: ['.js', '.ts'], - output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' }, + output: { bundleStrategy: 'split', preloadStrategy: undefined }, outDir: join(prefix, '.svelte-kit'), router: { type: 'pathname', resolution: 'client' }, serviceWorker: { + options: undefined, register: true }, typescript: {}, @@ -363,7 +360,7 @@ validate_paths( ); test('load default config (esm)', async () => { - const cwd = join(__dirname, 'fixtures/default'); + const cwd = join(import.meta.dirname, 'fixtures/default'); const config = await load_config({ cwd }); remove_keys(config, ([, v]) => typeof v === 'function'); @@ -375,7 +372,7 @@ test('load default config (esm)', async () => { }); test('load default config (esm) with .ts extensions', async () => { - const cwd = join(__dirname, 'fixtures/typescript'); + const cwd = join(import.meta.dirname, 'fixtures/typescript'); const config = await load_config({ cwd }); remove_keys(config, ([, v]) => typeof v === 'function'); @@ -387,7 +384,7 @@ test('load default config (esm) with .ts extensions', async () => { }); test('load .js config when both .js and .ts configs are present', async () => { - const cwd = join(__dirname, 'fixtures/multiple'); + const cwd = join(import.meta.dirname, 'fixtures/multiple'); const config = await load_config({ cwd }); remove_keys(config, ([, v]) => typeof v === 'function'); @@ -402,7 +399,7 @@ test('errors on loading config with incorrect default export', async () => { let message = null; try { - const cwd = join(__dirname, 'fixtures', 'export-string'); + const cwd = join(import.meta.dirname, 'fixtures', 'export-string'); await load_config({ cwd }); } catch (/** @type {any} */ e) { message = e.message; @@ -506,7 +503,7 @@ test('errors on invalid forkPreloads values', () => { }); test('uses src prefix for other kit.files options', async () => { - const cwd = join(__dirname, 'fixtures/custom-src'); + const cwd = join(import.meta.dirname, 'fixtures/custom-src'); const config = await load_config({ cwd }); remove_keys(config, ([, v]) => typeof v === 'function'); diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index ac30ce4fbd94..498f8d58457b 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -1,8 +1,6 @@ /** @import { Validator } from './types.js' */ import process from 'node:process'; -import colors from 'kleur'; -import { supportsTrustedTypes } from '../sync/utils.js'; const directives = object({ 'child-src': string_array(), @@ -29,14 +27,8 @@ const directives = object({ 'navigate-to': string_array(), 'report-uri': string_array(), 'report-to': string_array(), - 'require-trusted-types-for': validate(undefined, (input, keypath) => { - assert_trusted_types_supported(keypath); - return string_array()(input, keypath); - }), - 'trusted-types': validate(undefined, (input, keypath) => { - assert_trusted_types_supported(keypath); - return string_array()(input, keypath); - }), + 'require-trusted-types-for': string_array(), + 'trusted-types': string_array(), 'upgrade-insecure-requests': boolean(false), 'require-sri-for': string_array(), 'block-all-mixed-content': boolean(false), @@ -116,10 +108,8 @@ const options = object( }), csrf: object({ - checkOrigin: deprecate( - boolean(true), - (keypath) => - `\`${keypath}\` has been deprecated in favour of \`csrf.trustedOrigins\`. It will be removed in a future version` + checkOrigin: removed( + (keypath) => `\`${keypath}\` has been removed in favour of \`csrf.trustedOrigins\`` ), trustedOrigins: string_array([]) }), @@ -167,7 +157,9 @@ const options = object( outDir: string('.svelte-kit'), output: object({ - preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs']), + preloadStrategy: removed( + (keypath) => `\`${keypath}\` has been removed. modulepreload will always be used` + ), bundleStrategy: list(['split', 'single', 'inline']) }), @@ -325,22 +317,37 @@ const options = object( true ); +// /** +// * @param {Validator} fn +// * @param {(keypath: string) => string} get_message +// * @returns {Validator} +// */ +// function deprecate( +// fn, +// get_message = (keypath) => +// `The \`${keypath}\` option is deprecated, and will be removed in a future version` +// ) { +// return (input, keypath) => { +// if (input !== undefined) { +// console.warn(styleText(['bold', 'yellow'], get_message(keypath))); +// } + +// return fn(input, keypath); +// }; +// } + /** - * @param {Validator} fn * @param {(keypath: string) => string} get_message * @returns {Validator} */ -function deprecate( - fn, +function removed( get_message = (keypath) => - `The \`${keypath}\` option is deprecated, and will be removed in a future version` + `The \`${keypath}\` option has been removed. Please see the list of breaking changes for your major release` ) { return (input, keypath) => { - if (input !== undefined) { - console.warn(colors.bold().yellow(get_message(keypath))); + if (typeof input !== 'undefined') { + throw new Error(get_message(keypath)); } - - return fn(input, keypath); }; } @@ -493,13 +500,4 @@ function assert_string(input, keypath) { } } -/** @param {string} keypath */ -function assert_trusted_types_supported(keypath) { - if (!supportsTrustedTypes()) { - throw new Error( - `${keypath} is not supported by your version of Svelte. Please upgrade to Svelte 5.51.0 or later to use this directive.` - ); - } -} - export default options; diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 455eb0f44b68..fcc8ec66779f 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -1,6 +1,6 @@ import { GENERATED_COMMENT } from '../constants.js'; import { dedent } from './sync/utils.js'; -import { runtime_base } from './utils.js'; +import { get_runtime_base } from './utils.js'; /** * @typedef {'public' | 'private'} EnvType @@ -32,15 +32,16 @@ export function create_static_module(id, env) { /** * @param {EnvType} type * @param {Record | undefined} dev_values If in a development mode, values to pre-populate the module with. + * @param {string} root */ -export function create_dynamic_module(type, dev_values) { +export function create_dynamic_module(type, dev_values, root) { if (dev_values) { const keys = Object.entries(dev_values).map( ([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}` ); return `export const env = {\n${keys.join(',\n')}\n}`; } - return `export { ${type}_env as env } from '${runtime_base}/shared-server.js';`; + return `export { ${type}_env as env } from '${get_runtime_base(root)}/shared-server.js';`; } /** diff --git a/packages/kit/src/core/generate_manifest/find_server_assets.js b/packages/kit/src/core/generate_manifest/find_server_assets.js index 044db8419107..393941919b54 100644 --- a/packages/kit/src/core/generate_manifest/find_server_assets.js +++ b/packages/kit/src/core/generate_manifest/find_server_assets.js @@ -4,8 +4,9 @@ import { find_deps } from '../../exports/vite/build/utils.js'; * Finds all the assets that are imported by server files associated with `routes` * @param {import('types').BuildData} build_data * @param {import('types').RouteData[]} routes + * @param {string} root */ -export function find_server_assets(build_data, routes) { +export function find_server_assets(build_data, routes, root) { /** * All nodes actually used in the routes definition (prerendered routes are omitted). * Root layout/error is always included as they are needed for 404 and root errors. @@ -19,7 +20,7 @@ export function find_server_assets(build_data, routes) { /** @param {string} id */ function add_assets(id) { if (id in build_data.server_manifest) { - const deps = find_deps(build_data.server_manifest, id, false); + const deps = find_deps(build_data.server_manifest, id, false, root); for (const asset of deps.assets) { server_assets.add(asset); } diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index 6b2445aa2107..4bbc78509bdf 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -20,9 +20,17 @@ import { uneval } from 'devalue'; * relative_path: string; * routes: import('types').RouteData[]; * remotes: RemoteChunk[]; + * root: string; * }} opts */ -export function generate_manifest({ build_data, prerendered, relative_path, routes, remotes }) { +export function generate_manifest({ + build_data, + prerendered, + relative_path, + routes, + remotes, + root +}) { /** * @type {Map} The new index of each node in the filtered nodes array */ @@ -34,7 +42,7 @@ export function generate_manifest({ build_data, prerendered, relative_path, rout */ const used_nodes = new Set([0, 1]); - const server_assets = find_server_assets(build_data, routes); + const server_assets = find_server_assets(build_data, routes, root); for (const route of routes) { if (route.page) { @@ -119,7 +127,7 @@ export function generate_manifest({ build_data, prerendered, relative_path, rout pattern: ${route.pattern}, params: ${s(route.params)}, page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${reindexed.get(route.page.leaf)} }` : 'null'}, - endpoint: ${route.endpoint ? loader(join_relative(relative_path, resolve_symlinks(build_data.server_manifest, route.endpoint.file).chunk.file)) : 'null'} + endpoint: ${route.endpoint ? loader(join_relative(relative_path, resolve_symlinks(build_data.server_manifest, route.endpoint.file, root).chunk.file)) : 'null'} } `; }).filter(Boolean).join(',\n')} diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 2b967e555f2b..98ddcca5276d 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -4,7 +4,6 @@ import { pathToFileURL } from 'node:url'; import { validate_server_exports } from '../../utils/exports.js'; import { load_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; -import { installPolyfills } from '../../exports/node/polyfills.js'; import { ENDPOINT_METHODS } from '../../constants.js'; import { filter_env } from '../../utils/env.js'; import { has_server_load, resolve_route } from '../../utils/routing.js'; @@ -26,6 +25,7 @@ export default forked(import.meta.url, analyse); * out: string; * output_config: import('types').RecursiveRequired; * remotes: RemoteChunk[]; + * root: string; * }} opts */ async function analyse({ @@ -37,21 +37,20 @@ async function analyse({ env, out, output_config, - remotes + remotes, + root }) { /** @type {import('@sveltejs/kit').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; /** @type {import('types').ValidatedKitConfig} */ - const config = (await load_config()).kit; + const config = (await load_config({ cwd: root })).kit; const server_root = join(config.outDir, 'output'); /** @type {import('types').ServerInternalModule} */ const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href); - installPolyfills(); - // configure `import { building } from '$app/environment'` — // essential we do this before analysing the code internal.set_building(); @@ -66,7 +65,17 @@ async function analyse({ internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`)); // first, build server nodes without the client manifest so we can analyse it - build_server_nodes(out, config, manifest_data, server_manifest, null, null, null, output_config); + build_server_nodes( + out, + config, + manifest_data, + server_manifest, + null, + null, + null, + output_config, + root + ); /** @type {import('types').ServerMetadata} */ const metadata = { diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js index 39356887a289..66fa0c6379e7 100644 --- a/packages/kit/src/core/postbuild/fallback.js +++ b/packages/kit/src/core/postbuild/fallback.js @@ -1,7 +1,6 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { installPolyfills } from '../../exports/node/polyfills.js'; import { load_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; @@ -10,14 +9,13 @@ export default forked(import.meta.url, generate_fallback); /** * @param {{ * manifest_path: string; - * env: Record + * env: Record; + * root: string; * }} opts */ -async function generate_fallback({ manifest_path, env }) { +async function generate_fallback({ manifest_path, env, root }) { /** @type {import('types').ValidatedKitConfig} */ - const config = (await load_config()).kit; - - installPolyfills(); + const config = (await load_config({ cwd: root })).kit; const server_root = join(config.outDir, 'output'); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 806952e39268..f5e7db0cfde9 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -1,7 +1,6 @@ import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { installPolyfills } from '../../exports/node/polyfills.js'; import { mkdirp, posixify, walk } from '../../utils/filesystem.js'; import { decode_uri, is_root_relative, resolve } from '../../utils/url.js'; import { escape_html } from '../../utils/escape.js'; @@ -32,10 +31,11 @@ const SPECIAL_HASHLINKS = new Set(['', 'top']); * manifest_path: string; * metadata: import('types').ServerMetadata; * verbose: boolean; - * env: Record + * env: Record; + * root: string; * }} opts */ -async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { +async function prerender({ hash, out, manifest_path, metadata, verbose, env, root }) { /** @type {import('@sveltejs/kit').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; @@ -100,12 +100,13 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { const prerendered_routes = new Set(); /** @type {import('types').ValidatedKitConfig} */ - const config = (await load_config()).kit; + const config = (await load_config({ cwd: root })).kit; if (hash) { const fallback = await generate_fallback({ manifest_path, - env + env, + root }); const file = output_filename('/', true); @@ -124,8 +125,6 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { /** @type {import('types').Logger} */ const log = logger({ verbose }); - installPolyfills(); - /** @type {Map} */ const saved = new Map(); diff --git a/packages/kit/src/core/postbuild/queue.js b/packages/kit/src/core/postbuild/queue.js index a816e6eb2350..3edcfe94b7e5 100644 --- a/packages/kit/src/core/postbuild/queue.js +++ b/packages/kit/src/core/postbuild/queue.js @@ -1,6 +1,3 @@ -/** @import { PromiseWithResolvers } from '../../utils/promise.js' */ -import { with_resolvers } from '../../utils/promise.js'; - /** * @typedef {{ * fn: () => Promise, @@ -13,7 +10,9 @@ import { with_resolvers } from '../../utils/promise.js'; export function queue(concurrency) { /** @type {Task[]} */ const tasks = []; - const { promise, resolve, reject } = /** @type {PromiseWithResolvers} */ (with_resolvers()); + const { promise, resolve, reject } = /** @type {PromiseWithResolvers} */ ( + Promise.withResolvers() + ); let current = 0; let closed = false; diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 5facdc78dd5c..d7ffe012d6bd 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -1,13 +1,11 @@ +import { lookup } from 'mrmime'; import fs from 'node:fs'; import path from 'node:path'; -import process from 'node:process'; -import colors from 'kleur'; -import { lookup } from 'mrmime'; -import { list_files, runtime_directory } from '../../utils.js'; +import { styleText } from 'node:util'; import { posixify, resolve_entry } from '../../../utils/filesystem.js'; import { parse_route_id } from '../../../utils/routing.js'; +import { list_files, runtime_directory } from '../../utils.js'; import { sort_routes } from './sort.js'; -import { isSvelte5Plus } from '../utils.js'; import { create_node_analyser, get_page_options @@ -18,14 +16,14 @@ import { * @param {{ * config: import('types').ValidatedConfig; * fallback?: string; - * cwd?: string; + * cwd: string; * }} opts * @returns {import('types').ManifestData} */ export default function create_manifest_data({ config, - fallback = `${runtime_directory}/components/${isSvelte5Plus() ? 'svelte-5' : 'svelte-4'}`, - cwd = process.cwd() + fallback = `${runtime_directory}/components`, + cwd }) { const assets = create_assets(config); const hooks = create_hooks(config, cwd); @@ -115,8 +113,8 @@ function create_matchers(config, cwd) { } /** - * @param {import('types').ValidatedConfig} config * @param {string} cwd + * @param {import('types').ValidatedConfig} config * @param {string} fallback */ function create_routes_and_nodes(cwd, config, fallback) { @@ -240,12 +238,11 @@ function create_routes_and_nodes(cwd, config, fallback) { ); if (typo) { console.log( - colors - .bold() - .yellow( - `Missing route file prefix. Did you mean +${file.name}?` + - ` at ${path.join(dir, file.name)}` - ) + styleText( + ['bold', 'yellow'], + `Missing route file prefix. Did you mean +${file.name}?` + + ` at ${path.join(dir, file.name)}` + ) ); } @@ -420,7 +417,7 @@ function create_routes_and_nodes(cwd, config, fallback) { const indexes = new Map(nodes.map((node, i) => [node, i])); - const node_analyser = create_node_analyser(); + const node_analyser = create_node_analyser(cwd); for (const route of routes) { if (!route.leaf) continue; @@ -472,7 +469,7 @@ function create_routes_and_nodes(cwd, config, fallback) { for (const route of routes) { if (route.endpoint) { - route.endpoint.page_options = get_page_options(route.endpoint.file); + route.endpoint.page_options = get_page_options(route.endpoint.file, cwd); } } diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js index 09f732bed8f9..6a7a1b1b1c4c 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js @@ -1,12 +1,11 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { assert, expect, test } from 'vitest'; import create_manifest_data from './index.js'; import { sort_routes } from './sort.js'; import { validate_config } from '../../config/index.js'; -const cwd = fileURLToPath(new URL('./test', import.meta.url)); +const cwd = path.join(import.meta.dirname, 'test'); /** * @param {string} dir @@ -87,7 +86,7 @@ test('creates routes', () => { { id: '/blog.json', pattern: '/^/blog.json/?$/', - endpoint: { file: 'samples/basic/blog.json/+server.js', page_options: null } + endpoint: { file: 'samples/basic/blog.json/+server.js', page_options: {} } }, { id: '/blog', @@ -99,7 +98,7 @@ test('creates routes', () => { pattern: '/^/blog/([^/]+?).json/?$/', endpoint: { file: 'samples/basic/blog/[slug].json/+server.ts', - page_options: null + page_options: {} } }, { @@ -310,7 +309,7 @@ test('allows rest parameters inside segments', () => { pattern: '/^/([^]*?).json/?$/', endpoint: { file: 'samples/rest-prefix-suffix/[...rest].json/+server.js', - page_options: null + page_options: {} } } ]); @@ -348,7 +347,7 @@ test('optional parameters', () => { { id: '/[[foo]]bar', pattern: '/^/([^/]*)?bar/?$/', - endpoint: { file: 'samples/optional/[[foo]]bar/+server.js', page_options: null } + endpoint: { file: 'samples/optional/[[foo]]bar/+server.js', page_options: {} } }, { id: '/nested', pattern: '/^/nested/?$/' }, { @@ -481,7 +480,7 @@ test('allows multiple slugs', () => { pattern: '/^/([^/]+?).([^/]+?)/?$/', endpoint: { file: 'samples/multiple-slugs/[file].[ext]/+server.js', - page_options: null + page_options: {} } } ]); @@ -506,7 +505,7 @@ test('ignores things that look like lockfiles', () => { pattern: '/^/foo/?$/', endpoint: { file: 'samples/lockfiles/foo/+server.js', - page_options: null + page_options: {} } } ]); @@ -542,7 +541,7 @@ test('works with custom extensions', () => { pattern: '/^/blog.json/?$/', endpoint: { file: 'samples/custom-extension/blog.json/+server.js', - page_options: null + page_options: {} } }, { @@ -555,7 +554,7 @@ test('works with custom extensions', () => { pattern: '/^/blog/([^/]+?).json/?$/', endpoint: { file: 'samples/custom-extension/blog/[slug].json/+server.js', - page_options: null + page_options: {} } }, { diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index d9680e8ab65f..839e877716d0 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -1,4 +1,5 @@ import path from 'node:path'; +import process from 'node:process'; import create_manifest_data from './create_manifest_data/index.js'; import { write_client_manifest } from './write_client_manifest.js'; import { write_root } from './write_root.js'; @@ -16,25 +17,27 @@ import { * Initialize SvelteKit's generated files that only depend on the config and mode. * @param {import('types').ValidatedConfig} config * @param {string} mode + * @param {string} root The project root directory */ -export function init(config, mode) { - write_tsconfig(config.kit); +export function init(config, mode, root) { + write_tsconfig(config.kit, root); write_ambient(config.kit, mode); } /** * Update SvelteKit's generated files * @param {import('types').ValidatedConfig} config + * @param {string} root The project root directory */ -export function create(config) { - const manifest_data = create_manifest_data({ config }); +export function create(config, root) { + const manifest_data = create_manifest_data({ config, cwd: root }); const output = path.join(config.kit.outDir, 'generated'); write_client_manifest(config.kit, manifest_data, `${output}/client`); - write_server(config, output); + write_server(config, output, root); write_root(manifest_data, config, output); - write_all_types(config, manifest_data); + write_all_types(config, manifest_data, root); write_non_ambient(config.kit, manifest_data); return { manifest_data }; @@ -47,9 +50,10 @@ export function create(config) { * @param {import('types').ValidatedConfig} config * @param {import('types').ManifestData} manifest_data * @param {string} file + * @param {string} root The project root directory */ -export function update(config, manifest_data, file) { - const node_analyser = create_node_analyser(); +export function update(config, manifest_data, file, root) { + const node_analyser = create_node_analyser(root); for (const node of manifest_data.nodes) { node.page_options = node_analyser.get_page_options(node); @@ -57,11 +61,11 @@ export function update(config, manifest_data, file) { for (const route of manifest_data.routes) { if (route.endpoint) { - route.endpoint.page_options = get_page_options(route.endpoint.file); + route.endpoint.page_options = get_page_options(route.endpoint.file, root); } } - write_types(config, manifest_data, file); + write_types(config, manifest_data, file, root); write_non_ambient(config.kit, manifest_data); } @@ -69,10 +73,11 @@ export function update(config, manifest_data, file) { * Run sync.init and sync.create in series, returning the result from sync.create. * @param {import('types').ValidatedConfig} config * @param {string} mode The Vite mode + * @param {string} root The project root directory */ -export function all(config, mode) { - init(config, mode); - return create(config); +export function all(config, mode, root) { + init(config, mode, root); + return create(config, root); } /** @@ -81,16 +86,18 @@ export function all(config, mode) { * @param {string} mode The Vite mode */ export function all_types(config, mode) { - init(config, mode); - const manifest_data = create_manifest_data({ config }); - write_all_types(config, manifest_data); + const cwd = process.cwd(); + init(config, mode, cwd); + const manifest_data = create_manifest_data({ config, cwd }); + write_all_types(config, manifest_data, cwd); write_non_ambient(config.kit, manifest_data); } /** * Regenerate __SERVER__/internal.js in response to src/{app.html,error.html,service-worker.js} changing * @param {import('types').ValidatedConfig} config + * @param {string} root The project root directory */ -export function server(config) { - write_server(config, path.join(config.kit.outDir, 'generated')); +export function server(config, root) { + write_server(config, path.join(config.kit.outDir, 'generated'), root); } diff --git a/packages/kit/src/core/sync/utils.js b/packages/kit/src/core/sync/utils.js index 106360654b1f..0e0181366f9d 100644 --- a/packages/kit/src/core/sync/utils.js +++ b/packages/kit/src/core/sync/utils.js @@ -1,12 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { mkdirp } from '../../utils/filesystem.js'; -import { import_peer } from '../../utils/import.js'; - -/** @type {{ VERSION: string }} */ -const { VERSION } = await import_peer('svelte/compiler'); - -const [MAJOR, MINOR] = VERSION.split('.').map(Number); /** @type {Map} */ const previous_contents = new Map(); @@ -74,12 +68,3 @@ export function dedent(strings, ...values) { return str; } - -export function isSvelte5Plus() { - return MAJOR >= 5; -} - -// TODO 3.0 remove this once we can bump the peerDep range -export function supportsTrustedTypes() { - return (MAJOR === 5 && MINOR >= 51) || MAJOR > 5; -} diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 7495e3134854..f47f21991ee4 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -1,8 +1,8 @@ import path from 'node:path'; +import { styleText } from 'node:util'; import { relative_path, resolve_entry } from '../../utils/filesystem.js'; import { s } from '../../utils/misc.js'; -import { dedent, isSvelte5Plus, write_if_changed } from './utils.js'; -import colors from 'kleur'; +import { dedent, write_if_changed } from './utils.js'; /** * Writes the client manifest to disk. The manifest is used to power the router. It contains the @@ -126,12 +126,11 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { const typo = resolve_entry('src/+hooks.client'); if (typo) { console.log( - colors - .bold() - .yellow( - `Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` + - ` at ${path.resolve(typo)}` - ) + styleText( + ['bold', 'yellow'], + `Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` + + ` at ${path.resolve(typo)}` + ) ); } @@ -177,7 +176,7 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { export const decode = (type, value) => decoders[type](value); - export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; + export { default as root } from '../root.js'; ` ); diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js index 445c17e45dbc..839cba4b4c00 100644 --- a/packages/kit/src/core/sync/write_root.js +++ b/packages/kit/src/core/sync/write_root.js @@ -1,4 +1,4 @@ -import { dedent, isSvelte5Plus, write_if_changed } from './utils.js'; +import { dedent, write_if_changed } from './utils.js'; /** * @param {import('types').ManifestData} manifest_data @@ -8,7 +8,7 @@ import { dedent, isSvelte5Plus, write_if_changed } from './utils.js'; export function write_root(manifest_data, config, output) { // TODO remove default layout altogether - const use_boundaries = config.kit.experimental.handleRenderingErrors && isSvelte5Plus(); + const use_boundaries = config.kit.experimental.handleRenderingErrors; const max_depth = Math.max( ...manifest_data.routes.map((route) => @@ -26,7 +26,7 @@ export function write_root(manifest_data, config, output) { /** @type {string} */ let pyramid; - if (isSvelte5Plus() && use_boundaries) { + if (use_boundaries) { // with the @const we force the data[depth] access to be derived, which is important to not fire updates needlessly // TODO in Svelte 5 we should rethink the client.js side, we can likely make data a $state and only update indexes that changed there, simplifying this a lot pyramid = dedent` @@ -55,39 +55,21 @@ export function write_root(manifest_data, config, output) { `; } else { pyramid = dedent` - ${ - isSvelte5Plus() - ? ` - ` - : `` - }`; + + `; while (l--) { pyramid = dedent` {#if constructors[${l + 1}]} - ${ - isSvelte5Plus() - ? dedent`{@const Pyramid_${l} = constructors[${l}]} - - - ${pyramid} - ` - : dedent` + {@const Pyramid_${l} = constructors[${l}]} + + ${pyramid} - ` - } - + {:else} - ${ - isSvelte5Plus() - ? dedent` - {@const Pyramid_${l} = constructors[${l}]} - - - ` - : dedent`` - } - + {@const Pyramid_${l} = constructors[${l}]} + + {/if} `; } @@ -97,62 +79,34 @@ export function write_root(manifest_data, config, output) { `${output}/root.svelte`, dedent` - ${isSvelte5Plus() ? '' : ''} + ${pyramid} @@ -183,14 +137,12 @@ export function write_root(manifest_data, config, output) { ` ); - if (isSvelte5Plus()) { - write_if_changed( - `${output}/root.js`, - dedent` - import { asClassComponent } from 'svelte/legacy'; - import Root from './root.svelte'; - export default asClassComponent(Root); - ` - ); - } + write_if_changed( + `${output}/root.js`, + dedent` + import { asClassComponent } from 'svelte/legacy'; + import Root from './root.svelte'; + export default asClassComponent(Root); + ` + ); } diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 711a007345e0..ebc1db336184 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -1,12 +1,11 @@ import path from 'node:path'; -import process from 'node:process'; +import { styleText } from 'node:util'; import { hash } from '../../utils/hash.js'; import { posixify, resolve_entry } from '../../utils/filesystem.js'; import { s } from '../../utils/misc.js'; import { load_error_page, load_template } from '../config/index.js'; import { runtime_directory } from '../utils.js'; -import { isSvelte5Plus, write_if_changed } from './utils.js'; -import colors from 'kleur'; +import { write_if_changed } from './utils.js'; import { escape_html } from '../../utils/escape.js'; /** @@ -29,7 +28,7 @@ const server_template = ({ template, error_page }) => ` -import root from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; +import root from '../root.js'; import { set_building, set_prerendering } from '__sveltekit/environment'; import { set_assets } from '$app/paths/internal/server'; import { set_manifest, set_read_implementation } from '__sveltekit/server'; @@ -39,14 +38,13 @@ export const options = { app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, async: ${s(!!config.compilerOptions?.experimental?.async)}, csp: ${s(config.kit.csp)}, - csrf_check_origin: ${s(config.kit.csrf.checkOrigin && !config.kit.csrf.trustedOrigins.includes('*'))}, + csrf_check_origin: ${s(!config.kit.csrf.trustedOrigins.includes('*'))}, csrf_trusted_origins: ${s(config.kit.csrf.trustedOrigins)}, embedded: ${config.kit.embedded}, env_public_prefix: '${config.kit.env.publicPrefix}', env_private_prefix: '${config.kit.env.privatePrefix}', hash_routing: ${s(config.kit.router.type === 'hash')}, hooks: null, // added lazily, via \`get_hooks\` - preload_strategy: ${s(config.kit.output.preloadStrategy)}, root, service_worker: ${has_service_worker}, service_worker_options: ${config.kit.serviceWorker.register ? s(config.kit.serviceWorker.options) : 'null'}, @@ -103,20 +101,20 @@ export { set_assets, set_building, set_manifest, set_prerendering, set_private_e * Write server configuration to disk * @param {import('types').ValidatedConfig} config * @param {string} output + * @param {string} root The project root directory */ -export function write_server(config, output) { +export function write_server(config, output, root) { const server_hooks_file = resolve_entry(config.kit.files.hooks.server); const universal_hooks_file = resolve_entry(config.kit.files.hooks.universal); const typo = resolve_entry('src/+hooks.server'); if (typo) { console.log( - colors - .bold() - .yellow( - `Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` + - ` at ${path.resolve(typo)}` - ) + styleText( + ['bold', 'yellow'], + `Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` + + ` at ${path.resolve(typo)}` + ) ); } @@ -136,7 +134,7 @@ export function write_server(config, output) { has_service_worker: config.kit.serviceWorker.register && !!resolve_entry(config.kit.files.serviceWorker), runtime_directory: relative(runtime_directory), - template: load_template(process.cwd(), config), + template: load_template(root, config), error_page: load_error_page(config) }) ); diff --git a/packages/kit/src/core/sync/write_tsconfig.js b/packages/kit/src/core/sync/write_tsconfig.js index d0f6330e1b1f..1845a962e2cd 100644 --- a/packages/kit/src/core/sync/write_tsconfig.js +++ b/packages/kit/src/core/sync/write_tsconfig.js @@ -1,7 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import process from 'node:process'; -import colors from 'kleur'; +import { styleText } from 'node:util'; import { posixify } from '../../utils/filesystem.js'; import { write_if_changed } from './utils.js'; @@ -17,10 +16,11 @@ function maybe_file(cwd, file) { } /** + * @param {string} cwd * @param {string} file */ -function project_relative(file) { - return posixify(path.relative('.', file)); +function project_relative(cwd, file) { + return posixify(path.relative(cwd, file)); } /** @@ -37,21 +37,23 @@ function remove_trailing_slashstar(file) { /** * Generates the tsconfig that the user's tsconfig inherits from. * @param {import('types').ValidatedKitConfig} kit + * @param {string} cwd */ -export function write_tsconfig(kit, cwd = process.cwd()) { +export function write_tsconfig(kit, cwd) { const out = path.join(kit.outDir, 'tsconfig.json'); const user_config = load_user_tsconfig(cwd); if (user_config) validate_user_config(cwd, out, user_config); - write_if_changed(out, JSON.stringify(get_tsconfig(kit), null, '\t')); + write_if_changed(out, JSON.stringify(get_tsconfig(kit, cwd), null, '\t')); } /** * Generates the tsconfig that the user's tsconfig inherits from. * @param {import('types').ValidatedKitConfig} kit + * @param {string} cwd */ -export function get_tsconfig(kit) { +export function get_tsconfig(kit, cwd) { /** @param {string} file */ const config_relative = (file) => posixify(path.relative(kit.outDir, file)); @@ -59,6 +61,7 @@ export function get_tsconfig(kit) { 'ambient.d.ts', // careful: changing this name would be a breaking change, because it's referenced in the service-workers documentation 'non-ambient.d.ts', './types/**/$types.d.ts', + config_relative('svelte.config.js'), config_relative('vite.config.js'), config_relative('vite.config.ts') ]); @@ -74,11 +77,11 @@ export function get_tsconfig(kit) { // Test folder is a special case - we advocate putting tests in a top-level test folder // and it's not configurable (should we make it?) - const test_folder = project_relative('test'); + const test_folder = project_relative(cwd, 'test'); include.add(config_relative(`${test_folder}/**/*.js`)); include.add(config_relative(`${test_folder}/**/*.ts`)); include.add(config_relative(`${test_folder}/**/*.svelte`)); - const tests_folder = project_relative('tests'); + const tests_folder = project_relative(cwd, 'tests'); include.add(config_relative(`${tests_folder}/**/*.js`)); include.add(config_relative(`${tests_folder}/**/*.ts`)); include.add(config_relative(`${tests_folder}/**/*.svelte`)); @@ -101,7 +104,7 @@ export function get_tsconfig(kit) { compilerOptions: { // generated options paths: { - ...get_tsconfig_paths(kit), + ...get_tsconfig_paths(kit, cwd), '$app/types': ['./types/index.d.ts'] }, rootDirs: [config_relative('.'), './types'], @@ -169,22 +172,22 @@ function validate_user_config(cwd, out, config) { // TODO: baseUrl will be removed in TypeScript 7.0 if (baseUrl || paths) { console.warn( - colors - .bold() - .yellow( - `You have specified a baseUrl and/or paths in your ${config.kind} which interferes with SvelteKit's auto-generated tsconfig.json. ` + - 'Remove it to avoid problems with intellisense. For path aliases, use `kit.alias` instead: https://svelte.dev/docs/kit/configuration#alias' - ) + styleText( + ['bold', 'yellow'], + `You have specified a baseUrl and/or paths in your ${config.kind} which interferes with SvelteKit's auto-generated tsconfig.json. ` + + 'Remove it to avoid problems with intellisense. For path aliases, use `kit.alias` instead: https://svelte.dev/docs/kit/configuration#alias' + ) ); } } else { - let relative = posixify(path.relative('.', out)); + let relative = posixify(path.relative(cwd, out)); if (!relative.startsWith('./')) relative = './' + relative; console.warn( - colors - .bold() - .yellow(`Your ${config.kind} should extend the configuration generated by SvelteKit:`) + styleText( + ['bold', 'yellow'], + `Your ${config.kind} should extend the configuration generated by SvelteKit:` + ) ); console.warn(`{\n "extends": "${relative}"\n}`); } @@ -200,8 +203,9 @@ const value_regex = /^(.*?)((\/\*)|(\.\w+))?$/; * Related to vite alias creation. * * @param {import('types').ValidatedKitConfig} config + * @param {string} cwd */ -function get_tsconfig_paths(config) { +function get_tsconfig_paths(config, cwd) { /** @param {string} file */ const config_relative = (file) => { let relative_path = path.relative(config.outDir, file); @@ -212,8 +216,8 @@ function get_tsconfig_paths(config) { }; const alias = { ...config.alias }; - if (fs.existsSync(project_relative(config.files.lib))) { - alias['$lib'] = project_relative(config.files.lib); + if (fs.existsSync(project_relative(cwd, config.files.lib))) { + alias['$lib'] = project_relative(cwd, config.files.lib); } /** @type {Record} */ diff --git a/packages/kit/src/core/sync/write_tsconfig.spec.js b/packages/kit/src/core/sync/write_tsconfig.spec.js index b8fe6eadbca8..47e03dac5273 100644 --- a/packages/kit/src/core/sync/write_tsconfig.spec.js +++ b/packages/kit/src/core/sync/write_tsconfig.spec.js @@ -15,7 +15,7 @@ test('Creates tsconfig path aliases from kit.alias', () => { } }); - const { compilerOptions } = get_tsconfig(kit); + const { compilerOptions } = get_tsconfig(kit, '.'); // $lib isn't part of the outcome because there's a "path exists" // check in the implementation @@ -42,7 +42,7 @@ test('Allows generated tsconfig to be mutated', () => { } }); - const config = get_tsconfig(kit); + const config = get_tsconfig(kit, '.'); // @ts-expect-error assert.equal(config.extends, 'some/other/tsconfig.json'); @@ -60,7 +60,7 @@ test('Allows generated tsconfig to be replaced', () => { } }); - const config = get_tsconfig(kit); + const config = get_tsconfig(kit, '.'); // @ts-expect-error assert.equal(config.extends, 'some/other/tsconfig.json'); @@ -75,12 +75,13 @@ test('Creates tsconfig include from kit.files', () => { } }); - const { include } = get_tsconfig(kit); + const { include } = get_tsconfig(kit, '.'); expect(include).toEqual([ 'ambient.d.ts', 'non-ambient.d.ts', './types/**/$types.d.ts', + '../svelte.config.js', '../vite.config.js', '../vite.config.ts', '../app/**/*.js', diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index bbf63ec45600..3ecd36c23c7c 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -1,6 +1,5 @@ import fs from 'node:fs'; import path from 'node:path'; -import process from 'node:process'; import MagicString from 'magic-string'; import { posixify, rimraf, walk } from '../../../utils/filesystem.js'; import { compact } from '../../../utils/array.js'; @@ -25,21 +24,20 @@ const is_whitespace = (/** @type {string} */ char) => /\s/.test(char); * @typedef {Map} RoutesMap */ -const cwd = process.cwd(); - /** * Creates types for the whole manifest * @param {import('types').ValidatedConfig} config * @param {import('types').ManifestData} manifest_data + * @param {string} root The project root directory */ -export function write_all_types(config, manifest_data) { +export function write_all_types(config, manifest_data, root) { if (!ts) return; const types_dir = `${config.kit.outDir}/types`; // empty out files that no longer need to exist const routes_dir = remove_relative_parent_traversals( - posixify(path.relative('.', config.kit.files.routes)) + posixify(path.relative(root, config.kit.files.routes)) ); const expected_directories = new Set( manifest_data.routes.map((route) => path.join(routes_dir, route.id)) @@ -109,7 +107,7 @@ export function write_all_types(config, manifest_data) { const source_last_updated = Math.max( // ctimeMs includes move operations whereas mtimeMs does not - ...input_files.map((file) => fs.statSync(file).ctimeMs) + ...input_files.map((file) => fs.statSync(path.resolve(root, file)).ctimeMs) ); const types_last_updated = Math.max(...output_files.map((file) => file.updated)); @@ -124,7 +122,7 @@ export function write_all_types(config, manifest_data) { if (should_generate) { // track which old files end up being surplus to requirements const to_delete = new Set(output_files.map((file) => file.name)); - update_types(config, routes_map, route, to_delete); + update_types(config, routes_map, route, root, to_delete); meta_data[route.id] = input_files; } } @@ -138,8 +136,9 @@ export function write_all_types(config, manifest_data) { * @param {import('types').ValidatedConfig} config * @param {import('types').ManifestData} manifest_data * @param {string} file + * @param {string} root The project root directory */ -export function write_types(config, manifest_data, file) { +export function write_types(config, manifest_data, file, root) { if (!ts) return; if (!path.basename(file).startsWith('+')) { @@ -153,7 +152,7 @@ export function write_types(config, manifest_data, file) { if (!route) return; if (!route.leaf && !route.layout && !route.endpoint) return; // nothing to do - update_types(config, create_routes_map(manifest_data), route); + update_types(config, create_routes_map(manifest_data), route, root); } /** @@ -176,11 +175,12 @@ function create_routes_map(manifest_data) { * @param {import('types').ValidatedConfig} config * @param {RoutesMap} routes * @param {import('types').RouteData} route + * @param {string} root The project root directory * @param {Set} [to_delete] */ -function update_types(config, routes, route, to_delete = new Set()) { +function update_types(config, routes, route, root, to_delete = new Set()) { const routes_dir = remove_relative_parent_traversals( - posixify(path.relative('.', config.kit.files.routes)) + posixify(path.relative(root, config.kit.files.routes)) ); const outdir = path.join(config.kit.outDir, 'types', routes_dir, route.id); @@ -251,7 +251,7 @@ function update_types(config, routes, route, to_delete = new Set()) { declarations: d, exports: e, proxies - } = process_node(route.leaf, outdir, true, route_info.proxies); + } = process_node(route.leaf, outdir, true, route_info.proxies, root); exports.push(...e); declarations.push(...d); @@ -300,7 +300,7 @@ function update_types(config, routes, route, to_delete = new Set()) { layout_params.push({ ...param, optional: true }); } - ensureProxies(page, leaf.proxies); + ensureProxies(page, leaf.proxies, root); if ( // Be defensive - if a proxy doesn't exist (because it couldn't be created), assume a load function exists. @@ -336,6 +336,7 @@ function update_types(config, routes, route, to_delete = new Set()) { outdir, false, { server: null, universal: null }, + root, all_pages_have_load ); @@ -375,9 +376,10 @@ function update_types(config, routes, route, to_delete = new Set()) { * @param {string} outdir * @param {boolean} is_page * @param {Proxies} proxies + * @param {string} root The project root directory * @param {boolean} [all_pages_have_load] */ -function process_node(node, outdir, is_page, proxies, all_pages_have_load = true) { +function process_node(node, outdir, is_page, proxies, root, all_pages_have_load = true) { const params = `${is_page ? 'Route' : 'Layout'}Params`; const prefix = is_page ? 'Page' : 'Layout'; @@ -393,7 +395,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true /** @type {string} */ let data; - ensureProxies(node, proxies); + ensureProxies(node, proxies, root); if (node.server) { const basename = path.basename(node.server); @@ -426,7 +428,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true // The advantage is that type updates are reflected without saving. const from = proxy.modified ? `./proxy${replace_ext_with_js(basename)}` - : path_to_original(outdir, node.server); + : path_to_original(outdir, node.server, root); exports.push( 'type ExcludeActionFailure = T extends Kit.ActionFailure ? never : T extends void ? never : T;', @@ -495,7 +497,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true // The advantage is that type updates are reflected without saving. const from = proxy.modified ? `./proxy${replace_ext_with_js(path.basename(file_path))}` - : path_to_original(outdir, file_path); + : path_to_original(outdir, file_path, root); const type = `Kit.LoadProperties>>`; return expand ? `Expand>>` : type; } else { @@ -515,24 +517,26 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true * * @param {import('types').PageNode} node * @param {Proxies} proxies + * @param {string} root The project root directory */ -function ensureProxies(node, proxies) { +function ensureProxies(node, proxies, root) { if (node.server && !proxies.server) { - proxies.server = createProxy(node.server, true); + proxies.server = createProxy(node.server, true, root); } if (node.universal && !proxies.universal) { - proxies.universal = createProxy(node.universal, false); + proxies.universal = createProxy(node.universal, false, root); } } /** * @param {string} file_path * @param {boolean} is_server + * @param {string} root The project root directory * @returns {Proxy} */ -function createProxy(file_path, is_server) { - const proxy = tweak_types(fs.readFileSync(file_path, 'utf8'), is_server); +function createProxy(file_path, is_server, root) { + const proxy = tweak_types(fs.readFileSync(path.resolve(root, file_path), 'utf8'), is_server); if (proxy) { return { ...proxy, @@ -577,9 +581,10 @@ function get_parent_type(node, type) { /** * @param {string} outdir * @param {string} file_path + * @param {string} root The project root directory */ -function path_to_original(outdir, file_path) { - return posixify(path.relative(outdir, path.join(cwd, replace_ext_with_js(file_path)))); +function path_to_original(outdir, file_path, root) { + return posixify(path.relative(outdir, path.join(root, replace_ext_with_js(file_path)))); } /** diff --git a/packages/kit/src/core/sync/write_types/index.spec.js b/packages/kit/src/core/sync/write_types/index.spec.js index 647ceff6611e..15f79e4c15bb 100644 --- a/packages/kit/src/core/sync/write_types/index.spec.js +++ b/packages/kit/src/core/sync/write_types/index.spec.js @@ -2,7 +2,6 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; -import { fileURLToPath } from 'node:url'; import { assert, expect, test } from 'vitest'; import { rimraf } from '../../../utils/filesystem.js'; import create_manifest_data from '../create_manifest_data/index.js'; @@ -10,7 +9,7 @@ import { tweak_types, write_all_types } from './index.js'; import { write_non_ambient } from '../write_non_ambient.js'; import { validate_config } from '../../config/index.js'; -const cwd = fileURLToPath(new URL('./test', import.meta.url)); +const cwd = path.join(import.meta.dirname, 'test'); /** * @param {string} dir @@ -23,13 +22,16 @@ function run_test(dir) { initial.kit.files.assets = path.resolve(cwd, 'static'); initial.kit.files.params = path.resolve(cwd, dir, 'params'); initial.kit.files.routes = path.resolve(cwd, dir); - initial.kit.outDir = path.resolve(cwd, path.join(dir, '.svelte-kit')); + initial.kit.outDir = path.resolve(cwd, dir, '.svelte-kit'); + + const root = path.join(cwd, dir); const manifest = create_manifest_data({ - config: /** @type {import('types').ValidatedConfig} */ (initial) + config: /** @type {import('types').ValidatedConfig} */ (initial), + cwd: root }); - write_all_types(initial, manifest); + write_all_types(initial, manifest, root); write_non_ambient(initial.kit, manifest); } diff --git a/packages/kit/src/core/sync/write_types/test/actions/+page.server.js b/packages/kit/src/core/sync/write_types/test/actions/+page.server.js index 3347ea120fac..453d08708447 100644 --- a/packages/kit/src/core/sync/write_types/test/actions/+page.server.js +++ b/packages/kit/src/core/sync/write_types/test/actions/+page.server.js @@ -39,7 +39,7 @@ export const actions = { /** * Ordinarily this would live in a +page.svelte, but to make it easy to run the tests, we put it here. * The `export` is so that eslint doesn't throw a hissy fit about the unused variable - * @type {import('./.svelte-kit/types/src/core/sync/write_types/test/actions/$types').SubmitFunction} + * @type {import('./.svelte-kit/types/$types').SubmitFunction} */ export const submit = () => { return ({ result }) => { diff --git a/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/+page.js b/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/+page.js index 2368bd47160f..37854020e94f 100644 --- a/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/+page.js +++ b/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/+page.js @@ -1,6 +1,6 @@ // test to see if layout adjusts correctly if +page.js exists, but no load function -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/layout-advanced/(main)/$types').PageData} */ +/** @type {import('../.svelte-kit/types/(main)/$types').PageData} */ const data = { root: '' }; diff --git a/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/sub/+page.js b/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/sub/+page.js index 908adae6e5df..05cf07858240 100644 --- a/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/sub/+page.js +++ b/packages/kit/src/core/sync/write_types/test/layout-advanced/(main)/sub/+page.js @@ -1,4 +1,4 @@ -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/layout-advanced/(main)/sub/$types').PageLoad} */ +/** @type {import('../../.svelte-kit/types/(main)/sub/$types').PageLoad} */ export async function load({ parent }) { const p = await parent(); p.main; @@ -10,7 +10,7 @@ export async function load({ parent }) { }; } -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/layout-advanced/(main)/sub/$types').PageData} */ +/** @type {import('../../.svelte-kit/types/(main)/sub/$types').PageData} */ const data = { main: '', root: '', diff --git a/packages/kit/src/core/sync/write_types/test/layout/+layout.js b/packages/kit/src/core/sync/write_types/test/layout/+layout.js index c8bc95024f98..8643783872c6 100644 --- a/packages/kit/src/core/sync/write_types/test/layout/+layout.js +++ b/packages/kit/src/core/sync/write_types/test/layout/+layout.js @@ -1,4 +1,4 @@ -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').LayoutLoad} */ +/** @type {import('./.svelte-kit/types/$types').LayoutLoad} */ export function load({ data }) { data.server; // @ts-expect-error diff --git a/packages/kit/src/core/sync/write_types/test/layout/+layout.server.js b/packages/kit/src/core/sync/write_types/test/layout/+layout.server.js index 77a2a9780b5b..f3b024c96c5b 100644 --- a/packages/kit/src/core/sync/write_types/test/layout/+layout.server.js +++ b/packages/kit/src/core/sync/write_types/test/layout/+layout.server.js @@ -1,4 +1,4 @@ -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').LayoutServerLoad} */ +/** @type {import('./.svelte-kit/types/$types').LayoutServerLoad} */ export function load() { return { server: 'server' diff --git a/packages/kit/src/core/sync/write_types/test/layout/+page.js b/packages/kit/src/core/sync/write_types/test/layout/+page.js index 4abd01995480..63b0672410d7 100644 --- a/packages/kit/src/core/sync/write_types/test/layout/+page.js +++ b/packages/kit/src/core/sync/write_types/test/layout/+page.js @@ -1,4 +1,4 @@ -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').PageLoad} */ +/** @type {import('./.svelte-kit/types/$types').PageLoad} */ export function load({ data }) { data.pageServer; // @ts-expect-error @@ -8,7 +8,7 @@ export function load({ data }) { }; } -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').PageData} */ +/** @type {import('./.svelte-kit/types/$types').PageData} */ const data = { shared: 'asd', pageShared: 'asd' diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js index 024289bef97e..cbb0d64378c9 100644 --- a/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js @@ -1,6 +1,6 @@ /* eslint-disable */ -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/$types').PageLoad} */ +/** @type {import('../../.svelte-kit/types/optional/[[optionalNarrowedParam=narrowed]]/$types').PageLoad} */ export function load({ params }) { if (params.optionalNarrowedParam) { /** @type {"a" | "b"} */ diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js index 5a67f890cd8c..3c27fddc8492 100644 --- a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js @@ -1,6 +1,6 @@ /* eslint-disable */ -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/$types').LayoutLoad} */ +/** @type {import('../.svelte-kit/types/required/$types').LayoutLoad} */ export function load({ params }) { if (params.narrowedParam) { /** @type {"a" | "b"} */ diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js index 395ae659aa1d..76d9483badff 100644 --- a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js @@ -1,6 +1,6 @@ /* eslint-disable */ -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/$types').PageLoad} */ +/** @type {import('../../.svelte-kit/types/required/[narrowedParam=narrowed]/$types').PageLoad} */ export function load({ params }) { /** @type {"a" | "b"} */ let a; diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js index 6fd50653a023..bd803fe94597 100644 --- a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js @@ -1,6 +1,6 @@ /* eslint-disable */ -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/$types').PageLoad} */ +/** @type {import('../../.svelte-kit/types/required/[regularParam=not_narrowed]/$types').PageLoad} */ export function load({ params }) { /** @type {string} a*/ const a = params.regularParam; diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js index f26d10e3aa74..393446edfb18 100644 --- a/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js @@ -1,6 +1,6 @@ /* eslint-disable */ -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/$types').PageLoad} */ +/** @type {import('../../.svelte-kit/types/spread/[...spread=narrowed]/$types').PageLoad} */ export function load({ params }) { /** @type {"a" | "b"} */ let a; diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/+page.js b/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/+page.js index 9d11e44f0552..e9b7de888b7f 100644 --- a/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/+page.js +++ b/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/+page.js @@ -1,4 +1,4 @@ -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/simple-page-server-and-shared/$types').PageLoad} */ +/** @type {import('./.svelte-kit/types/$types').PageLoad} */ export function load({ data }) { data.server; // @ts-expect-error @@ -8,7 +8,7 @@ export function load({ data }) { }; } -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/simple-page-server-and-shared/$types').PageData} */ +/** @type {import('./.svelte-kit/types/$types').PageData} */ const data = { shared: 'asd' }; diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-server-only/+page.server.js b/packages/kit/src/core/sync/write_types/test/simple-page-server-only/+page.server.js index cfdb779689e4..b33e149baf62 100644 --- a/packages/kit/src/core/sync/write_types/test/simple-page-server-only/+page.server.js +++ b/packages/kit/src/core/sync/write_types/test/simple-page-server-only/+page.server.js @@ -8,7 +8,7 @@ export const actions = { default: () => ({ action: 'bar' }) }; -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/simple-page-server-only/$types').PageData} */ +/** @type {import('./.svelte-kit/types/$types').PageData} */ const data = { foo: 'asd' }; @@ -16,7 +16,7 @@ data.foo; // @ts-expect-error data.bar; -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/simple-page-server-only/$types').ActionData} */ +/** @type {import('./.svelte-kit/types/$types').ActionData} */ const actionData = { action: 'bar' }; actionData.action; // @ts-expect-error diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-server-only/sub/+page.server.js b/packages/kit/src/core/sync/write_types/test/simple-page-server-only/sub/+page.server.js index 572c955d4daa..20b96ccb5441 100644 --- a/packages/kit/src/core/sync/write_types/test/simple-page-server-only/sub/+page.server.js +++ b/packages/kit/src/core/sync/write_types/test/simple-page-server-only/sub/+page.server.js @@ -1,4 +1,4 @@ -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/simple-page-server-only/sub/$types').PageServerLoad} */ +/** @type {import('../.svelte-kit/types/sub/$types').PageServerLoad} */ export function load() { if (Math.random() > 0.5) { return { @@ -7,7 +7,7 @@ export function load() { } } -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/simple-page-server-only/sub/$types').PageData} */ +/** @type {import('../.svelte-kit/types/sub/$types').PageData} */ const data = /** @type {any} */ ({ foo: 'bar' }); // the any cast prevents TypeScript from narrowing this to foo being defined diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/+page.js b/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/+page.js index 900e9369bd13..03eee6efca6b 100644 --- a/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/+page.js +++ b/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/+page.js @@ -4,7 +4,7 @@ export function load() { }; } -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/simple-page-shared-only/$types').PageData} */ +/** @type {import('./.svelte-kit/types/$types').PageData} */ const data = { shared: 'asd' }; diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/sub/+page.js b/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/sub/+page.js index 5b31531c2364..bcf2036bd312 100644 --- a/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/sub/+page.js +++ b/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/sub/+page.js @@ -1,4 +1,4 @@ -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/simple-page-shared-only/sub/$types').PageLoad} */ +/** @type {import('../.svelte-kit/types/sub/$types').PageLoad} */ export function load() { if (Math.random() > 0.5) { return { @@ -7,7 +7,7 @@ export function load() { } } -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/simple-page-shared-only/sub/$types').PageData} */ +/** @type {import('../.svelte-kit/types/sub/$types').PageData} */ const data = /** @type {any} */ ({ foo: 'bar' }); // the any cast prevents TypeScript from narrowing this to foo being defined diff --git a/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/+layout.js b/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/+layout.js index f0ae674a8f07..44f1d712d137 100644 --- a/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/+layout.js +++ b/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/+layout.js @@ -1,4 +1,4 @@ -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/$types').LayoutLoad} */ +/** @type {import('./.svelte-kit/types/$types').LayoutLoad} */ export function load({ params }) { params.rest; params.slug; diff --git a/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/nested/+layout.js b/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/nested/+layout.js index d41974fea45b..563f8f2c81f3 100644 --- a/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/nested/+layout.js +++ b/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/nested/+layout.js @@ -1,4 +1,4 @@ -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/nested/$types').LayoutLoad} */ +/** @type {import('../.svelte-kit/types/nested/$types').LayoutLoad} */ export function load({ params }) { params.rest; // @ts-expect-error diff --git a/packages/kit/src/core/sync/write_types/test/slugs/+layout.js b/packages/kit/src/core/sync/write_types/test/slugs/+layout.js index 96531291b144..913bfa77e30c 100644 --- a/packages/kit/src/core/sync/write_types/test/slugs/+layout.js +++ b/packages/kit/src/core/sync/write_types/test/slugs/+layout.js @@ -1,4 +1,4 @@ -/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/slugs/$types').LayoutLoad} */ +/** @type {import('./.svelte-kit/types/$types').LayoutLoad} */ export function load({ params }) { params.optional; params.rest; diff --git a/packages/kit/src/core/sync/write_types/test/slugs/[...rest]/+page.js b/packages/kit/src/core/sync/write_types/test/slugs/[...rest]/+page.js index cdebe4d37303..067a12c5c1d0 100644 --- a/packages/kit/src/core/sync/write_types/test/slugs/[...rest]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/slugs/[...rest]/+page.js @@ -1,4 +1,4 @@ -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/slugs/[...rest]/$types').PageLoad} */ +/** @type {import('../.svelte-kit/types/[...rest]/$types').PageLoad} */ export function load({ params }) { params.rest.charAt(1); // @ts-expect-error diff --git a/packages/kit/src/core/sync/write_types/test/slugs/[slug]/+page.js b/packages/kit/src/core/sync/write_types/test/slugs/[slug]/+page.js index 9a82f5ab5782..0f0777d66937 100644 --- a/packages/kit/src/core/sync/write_types/test/slugs/[slug]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/slugs/[slug]/+page.js @@ -1,4 +1,4 @@ -/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/slugs/[slug]/$types').PageLoad} */ +/** @type {import('../.svelte-kit/types/[slug]/$types').PageLoad} */ export function load({ params }) { params.slug.charAt(1); // @ts-expect-error diff --git a/packages/kit/src/core/sync/write_types/test/slugs/x/[[optional]]/+page.js b/packages/kit/src/core/sync/write_types/test/slugs/x/[[optional]]/+page.js index 3f3f9702b49a..c5ba3708879c 100644 --- a/packages/kit/src/core/sync/write_types/test/slugs/x/[[optional]]/+page.js +++ b/packages/kit/src/core/sync/write_types/test/slugs/x/[[optional]]/+page.js @@ -1,4 +1,4 @@ -/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/slugs/x/[[optional]]/$types').PageLoad} */ +/** @type {import('../../.svelte-kit/types/x/[[optional]]/$types').PageLoad} */ export async function load({ parent, params }) { const p = await parent(); /** @type {NonNullable} */ diff --git a/packages/kit/src/core/utils.js b/packages/kit/src/core/utils.js index 668306e9e70f..73ded00b82fb 100644 --- a/packages/kit/src/core/utils.js +++ b/packages/kit/src/core/utils.js @@ -1,8 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import process from 'node:process'; import { fileURLToPath } from 'node:url'; -import colors from 'kleur'; +import { styleText } from 'node:util'; import { posixify, to_fs } from '../utils/filesystem.js'; /** @@ -19,10 +18,14 @@ export const runtime_directory = posixify(fileURLToPath(new URL('../runtime', im * This allows us to import SvelteKit internals that aren't exposed via `pkg.exports` in a * way that works whether `@sveltejs/kit` is installed inside the project's `node_modules` * or in a workspace root + * @param {string} root + * @returns {string} */ -export const runtime_base = runtime_directory.startsWith(process.cwd()) - ? `/${path.relative('.', runtime_directory)}` - : to_fs(runtime_directory); +export function get_runtime_base(root) { + return runtime_directory.startsWith(root) + ? `/${path.relative(root, runtime_directory)}` + : to_fs(runtime_directory); +} function noop() {} @@ -34,11 +37,10 @@ export function logger({ verbose }) { /** @param {string} msg */ const err = (msg) => console.error(msg.replace(/^/gm, ' ')); - log.success = (msg) => log(colors.green(`✔ ${msg}`)); - log.error = (msg) => err(colors.bold().red(msg)); - log.warn = (msg) => log(colors.bold().yellow(msg)); - - log.minor = verbose ? (msg) => log(colors.grey(msg)) : noop; + log.success = (msg) => log(styleText('green', `✔ ${msg}`)); + log.error = (msg) => err(styleText(['bold', 'red'], msg)); + log.warn = (msg) => log(styleText(['bold', 'yellow'], msg)); + log.minor = verbose ? (msg) => log(styleText('grey', msg)) : noop; log.info = verbose ? log : noop; return log; diff --git a/packages/kit/src/exports/hooks/sequence.spec.js b/packages/kit/src/exports/hooks/sequence.spec.js index e3ccc1cf930e..a6516f445f4a 100644 --- a/packages/kit/src/exports/hooks/sequence.spec.js +++ b/packages/kit/src/exports/hooks/sequence.spec.js @@ -2,7 +2,6 @@ /** @import { RequestState } from 'types' */ import { assert, expect, test, vi } from 'vitest'; import { sequence } from './sequence.js'; -import { installPolyfills } from '../node/polyfills.js'; import { noop_span } from '../../runtime/telemetry/noop.js'; const dummy_event = vi.hoisted( @@ -29,8 +28,6 @@ vi.mock(import('@sveltejs/kit/internal/server'), async (actualPromise) => { }; }); -installPolyfills(); - test('applies handlers in sequence', async () => { /** @type {string[]} */ const order = []; diff --git a/packages/kit/src/exports/index.js b/packages/kit/src/exports/index.js index 5cc542929408..501f8a605d95 100644 --- a/packages/kit/src/exports/index.js +++ b/packages/kit/src/exports/index.js @@ -44,7 +44,7 @@ export { VERSION } from '../version.js'; * @param {number} status * @param {App.Error} body * @return {never} - * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. + * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling. * @throws {Error} If the provided status is invalid (not between 400 and 599). */ /** @@ -58,7 +58,7 @@ export { VERSION } from '../version.js'; * @param {number} status * @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body] * @return {never} - * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. + * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling. * @throws {Error} If the provided status is invalid (not between 400 and 599). */ /** @@ -69,7 +69,7 @@ export { VERSION } from '../version.js'; * @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. * @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property. * @return {never} - * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. + * @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling. * @throws {Error} If the provided status is invalid (not between 400 and 599). */ export function error(status, body) { @@ -85,7 +85,7 @@ export function error(status, body) { * @template {number} T * @param {unknown} e * @param {T} [status] The status to filter for. - * @return {e is (HttpError & { status: T extends undefined ? never : T })} + * @return {e is (import('./public.js').HttpError & { status: T extends undefined ? never : T })} */ export function isHttpError(e, status) { if (!(e instanceof HttpError)) return false; @@ -105,7 +105,7 @@ export function isHttpError(e, status) { * * @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number)} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308. * @param {string | URL} location The location to redirect to. - * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. + * @throws {import('./public.js').Redirect} This error instructs SvelteKit to redirect to the specified location. * @throws {Error} If the provided status is invalid. * @return {never} */ @@ -124,7 +124,7 @@ export function redirect(status, location) { /** * Checks whether this is a redirect thrown by {@link redirect}. * @param {unknown} e The object to check. - * @return {e is Redirect} + * @return {e is import('./public.js').Redirect} */ export function isRedirect(e) { return e instanceof Redirect; @@ -134,6 +134,7 @@ export function isRedirect(e) { * Create a JSON `Response` object from the supplied data. * @param {any} data The value that will be serialized as JSON. * @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. `Content-Type: application/json` and `Content-Length` headers will be added automatically. + * @deprecated use `Response.json` */ export function json(data, init) { // TODO deprecate this in favour of `Response.json` when it's @@ -162,6 +163,7 @@ export function json(data, init) { * Create a `Response` object from the supplied body. * @param {string} body The value that will be used as-is. * @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. A `Content-Length` header will be added automatically. + * @deprecated use `new Response` */ export function text(body, init) { const headers = new Headers(init?.headers); diff --git a/packages/kit/src/exports/node/index.js b/packages/kit/src/exports/node/index.js index ae2cae9ab3f8..a03da29be4c1 100644 --- a/packages/kit/src/exports/node/index.js +++ b/packages/kit/src/exports/node/index.js @@ -1,6 +1,5 @@ import { createReadStream } from 'node:fs'; import { Readable } from 'node:stream'; -import * as set_cookie_parser from 'set-cookie-parser'; import { SvelteKitError } from '../internal/index.js'; /** @@ -36,16 +35,11 @@ function get_raw_body(req, body_size_limit) { return new ReadableStream({ start(controller) { if (body_size_limit !== undefined && content_length > body_size_limit) { - let message = `Content-length of ${content_length} exceeds limit of ${body_size_limit} bytes.`; - - if (body_size_limit === 0) { - // https://github.com/sveltejs/kit/pull/11589 - // TODO this exists to aid migration — remove in a future version - message += ' To disable body size limits, specify Infinity rather than 0.'; - } - - const error = new SvelteKitError(413, 'Payload Too Large', message); - + const error = new SvelteKitError( + 413, + 'Payload Too Large', + `Content-length of ${content_length} exceeds limit of ${body_size_limit} bytes.` + ); controller.error(error); return; } @@ -120,9 +114,9 @@ export async function getRequest({ request, base, bodySizeLimit }) { delete headers[':scheme']; } - // TODO: Whenever Node >=22 is minimum supported version, we can use `request.readableAborted` - // @see https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/lib/internal/streams/readable.js#L1443-L1453 const controller = new AbortController(); + // TODO: Whenever Node >=22.17 is the minimum supported version, we can do `if (request.readableAborted) controller.abort()` instead + // see https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/lib/internal/streams/readable.js#L1443-L1453 let errored = false; let end_emitted = false; request.once('error', () => (errored = true)); @@ -156,15 +150,7 @@ export async function getRequest({ request, base, bodySizeLimit }) { export async function setResponse(res, response) { for (const [key, value] of response.headers) { try { - res.setHeader( - key, - key === 'set-cookie' - ? set_cookie_parser.splitCookiesString( - // This is absurd but necessary, TODO: investigate why - /** @type {string}*/ (response.headers.get(key)) - ) - : value - ); + res.setHeader(key, key === 'set-cookie' ? response.headers.getSetCookie() : value); } catch (error) { res.getHeaderNames().forEach((name) => res.removeHeader(name)); res.writeHead(500).end(String(error)); diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 60a50033add7..3c7677a53e22 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -122,13 +122,12 @@ export interface Builder { /** An array of all routes (including prerendered) */ routes: RouteDefinition[]; - // TODO 3.0 remove this method /** * Create separate functions that map to one or more routes of your app. * @param fn A function that groups a set of routes into an entry point - * @deprecated Use `builder.routes` instead + * @deprecated removed in 3.0. Use `builder.routes` instead */ - createEntries: (fn: (route: RouteDefinition) => AdapterEntry) => Promise; + createEntries?: (fn: (route: RouteDefinition) => AdapterEntry) => Promise; /** * Find all the assets imported by server files belonging to `routes` @@ -264,57 +263,50 @@ export interface Cookies { /** * Gets a cookie that was previously set with `cookies.set`, or from the request headers. * @param name the name of the cookie - * @param opts the options, passed directly to `cookie.parse`. See documentation [here](https://github.com/jshttp/cookie#cookieparsestr-options) + * @param opts the options, passed directly to `cookie.parse`. See documentation [here](https://github.com/jshttp/cookie?tab=readme-ov-file#cookieparsecookiestr-options) */ - get: (name: string, opts?: import('cookie').CookieParseOptions) => string | undefined; + get: (name: string, opts?: import('cookie').ParseOptions) => string | undefined; /** * Gets all cookies that were previously set with `cookies.set`, or from the request headers. - * @param opts the options, passed directly to `cookie.parse`. See documentation [here](https://github.com/jshttp/cookie#cookieparsestr-options) + * @param opts the options, passed directly to `cookie.parse`. See documentation [here](https://github.com/jshttp/cookie?tab=readme-ov-file#cookieparsecookiestr-options) */ - getAll: (opts?: import('cookie').CookieParseOptions) => Array<{ name: string; value: string }>; + getAll: (opts?: import('cookie').ParseOptions) => Array<{ name: string; value: string }>; /** * Sets a cookie. This will add a `set-cookie` header to the response, but also make the cookie available via `cookies.get` or `cookies.getAll` during the current request. * - * The `httpOnly` and `secure` options are `true` by default (except on http://localhost, where `secure` is `false`), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The `sameSite` option defaults to `lax`. + * The `httpOnly` and `secure` options are `true` by default (except on http://localhost, where `secure` is `false`), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. * - * You must specify a `path` for the cookie. In most cases you should explicitly set `path: '/'` to make the cookie available throughout your app. You can use relative paths, or set `path: ''` to make the cookie only available on the current path and its children + * The `path` option is `'/'` by default. You can use relative paths, or set `path: ''` to make the cookie only available on the current path and its children. * @param name the name of the cookie * @param value the cookie value - * @param opts the options, passed directly to `cookie.serialize`. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) + * @param opts the options passed to `cookie.serialize` with the SvelteKit defaults described above. See documentation [here](https://github.com/jshttp/cookie?tab=readme-ov-file#cookiestringifysetcookiesetcookieobj-options) */ - set: ( - name: string, - value: string, - opts: import('cookie').CookieSerializeOptions & { path: string } - ) => void; + set: (name: string, value: string, opts: import('cookie').SerializeOptions) => void; /** * Deletes a cookie by setting its value to an empty string and setting the expiry date in the past. * - * You must specify a `path` for the cookie. In most cases you should explicitly set `path: '/'` to make the cookie available throughout your app. You can use relative paths, or set `path: ''` to make the cookie only available on the current path and its children + * The `httpOnly` and `secure` options are `true` by default (except on http://localhost, where `secure` is `false`), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. + * + * The `path` option is `'/'` by default. You can use relative paths, or set `path: ''` to make the cookie only available on the current path and its children. * @param name the name of the cookie - * @param opts the options, passed directly to `cookie.serialize`. The `path` must match the path of the cookie you want to delete. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) + * @param opts the options passed to `cookie.serialize` with the SvelteKit defaults described above. See documentation [here](https://github.com/jshttp/cookie?tab=readme-ov-file#cookiestringifysetcookiesetcookieobj-options) */ - delete: (name: string, opts: import('cookie').CookieSerializeOptions & { path: string }) => void; + delete: (name: string, opts: import('cookie').SerializeOptions) => void; /** * Serialize a cookie name-value pair into a `Set-Cookie` header string, but don't apply it to the response. * - * The `httpOnly` and `secure` options are `true` by default (except on http://localhost, where `secure` is `false`), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The `sameSite` option defaults to `lax`. - * - * You must specify a `path` for the cookie. In most cases you should explicitly set `path: '/'` to make the cookie available throughout your app. You can use relative paths, or set `path: ''` to make the cookie only available on the current path and its children + * The `httpOnly` and `secure` options are `true` by default (except on http://localhost, where `secure` is `false`), and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. * + * The `path` option is `'/'` by default. You can use relative paths, or set `path: ''` to make the cookie only available on the current path and its children. * @param name the name of the cookie * @param value the cookie value - * @param opts the options, passed directly to `cookie.serialize`. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) + * @param opts the options passed to `cookie.serialize` with the SvelteKit defaults described above. See documentation [here](https://github.com/jshttp/cookie?tab=readme-ov-file#cookiestringifysetcookiesetcookieobj-options) */ - serialize: ( - name: string, - value: string, - opts: import('cookie').CookieSerializeOptions & { path: string } - ) => string; + serialize: (name: string, value: string, opts: import('cookie').SerializeOptions) => string; } /** @@ -428,7 +420,7 @@ export interface KitConfig { * * To allow people to make `POST`, `PUT`, `PATCH`, or `DELETE` requests with a `Content-Type` of `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain` to your app from other origins, you will need to disable this option. Be careful! * @default true - * @deprecated Use `trustedOrigins: ['*']` instead + * @deprecated removed in 3.0. Use `trustedOrigins: ['*']` instead */ checkOrigin?: boolean; /** @@ -628,6 +620,7 @@ export interface KitConfig { * - `preload-mjs` - uses `` but with the `.mjs` extension which prevents double-parsing in Chromium. Some static webservers will fail to serve .mjs files with a `Content-Type: application/javascript` header, which will cause your application to break. If that doesn't apply to you, this is the option that will deliver the best performance for the largest number of users, until `modulepreload` is more widely supported. * @default "modulepreload" * @since 1.8.4 + * @deprecated removed in 3.0 */ preloadStrategy?: 'modulepreload' | 'preload-js' | 'preload-mjs'; /** @@ -636,7 +629,7 @@ export interface KitConfig { * - If `'single'`, creates just one .js bundle and one .css file containing code for the entire app. * - If `'inline'`, inlines all JavaScript and CSS of the entire app into the HTML. The result is usable without a server (i.e. you can just open the file in your browser). * - * When using `'split'`, you can also adjust the bundling behaviour by setting [`output.experimentalMinChunkSize`](https://rollupjs.org/configuration-options/#output-experimentalminchunksize) and [`output.manualChunks`](https://rollupjs.org/configuration-options/#output-manualchunks) inside your Vite config's [`build.rollupOptions`](https://vite.dev/config/build-options.html#build-rollupoptions). + * When using `'split'`, you can also adjust the bundling behaviour by setting [`output.codeSplitting`](https://rolldown.rs/reference/OutputOptions.codeSplitting) inside your Vite config's [`build.rolldownOptions`](https://vite.dev/config/build-options#build-rolldownoptions). * * If you want to inline your assets, you'll need to set Vite's [`build.assetsInlineLimit`](https://vite.dev/config/build-options.html#build-assetsinlinelimit) option to an appropriate size then import your assets through Vite. * @@ -1281,13 +1274,6 @@ export interface NavigationGoto extends NavigationBase { * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect */ type: 'goto'; - - // TODO 3.0 remove this property, so that it only exists when type is 'popstate' - // (would possibly be a breaking change to do it prior to that) - /** - * In case of a history back/forward navigation, the number of steps to go back/forward - */ - delta?: undefined; } export interface NavigationLeave extends NavigationBase { @@ -1296,13 +1282,6 @@ export interface NavigationLeave extends NavigationBase { * - `leave`: The app is being left either because the tab is being closed or a navigation to a different document is occurring */ type: 'leave'; - - // TODO 3.0 remove this property, so that it only exists when type is 'popstate' - // (would possibly be a breaking change to do it prior to that) - /** - * In case of a history back/forward navigation, the number of steps to go back/forward - */ - delta?: undefined; } export interface NavigationFormSubmit extends NavigationBase { @@ -1316,13 +1295,6 @@ export interface NavigationFormSubmit extends NavigationBase { * The `SubmitEvent` that caused the navigation */ event: SubmitEvent; - - // TODO 3.0 remove this property, so that it only exists when type is 'popstate' - // (would possibly be a breaking change to do it prior to that) - /** - * In case of a history back/forward navigation, the number of steps to go back/forward - */ - delta?: undefined; } export interface NavigationPopState extends NavigationBase { @@ -1354,13 +1326,6 @@ export interface NavigationLink extends NavigationBase { * The `PointerEvent` that caused the navigation */ event: PointerEvent; - - // TODO 3.0 remove this property, so that it only exists when type is 'popstate' - // (would possibly be a breaking change to do it prior to that) - /** - * In case of a history back/forward navigation, the number of steps to go back/forward - */ - delta?: undefined; } export type Navigation = diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index a960a20b828d..65ae7914a291 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -20,8 +20,9 @@ import { escape_for_interpolation } from '../../../utils/escape.js'; * @param {import('vite').Manifest} server_manifest * @param {import('vite').Manifest | null} client_manifest * @param {string | null} assets_path - * @param {import('vite').Rollup.RollupOutput['output'] | null} client_chunks + * @param {import('vite').Rolldown.RolldownOutput['output'] | null} client_chunks * @param {import('types').RecursiveRequired} output_config + * @param {string} root */ export function build_server_nodes( out, @@ -31,7 +32,8 @@ export function build_server_nodes( client_manifest, assets_path, client_chunks, - output_config + output_config, + root ) { mkdirp(`${out}/server/nodes`); mkdirp(`${out}/server/stylesheets`); @@ -133,7 +135,7 @@ export function build_server_nodes( exports.push( 'let component_cache;', `export const component = async () => component_cache ??= (await import('../${ - resolve_symlinks(server_manifest, node.component).chunk.file + resolve_symlinks(server_manifest, node.component, root).chunk.file }')).default;` ); } @@ -143,7 +145,7 @@ export function build_server_nodes( exports.push(`export const universal = ${s(node.page_options, null, 2)};`); } else { imports.push( - `import * as universal from '../${resolve_symlinks(server_manifest, node.universal).chunk.file}';` + `import * as universal from '../${resolve_symlinks(server_manifest, node.universal, root).chunk.file}';` ); // TODO: when building for analysis, explain why the file was loaded on the server if we fail to load it exports.push('export { universal };'); @@ -153,7 +155,7 @@ export function build_server_nodes( if (node.server) { imports.push( - `import * as server from '../${resolve_symlinks(server_manifest, node.server).chunk.file}';` + `import * as server from '../${resolve_symlinks(server_manifest, node.server, root).chunk.file}';` ); exports.push('export { server };'); exports.push(`export const server_id = ${s(node.server)};`); @@ -165,7 +167,7 @@ export function build_server_nodes( output_config.bundleStrategy === 'split' ) { const entry_path = `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`; - const entry = find_deps(client_manifest, entry_path, true); + const entry = find_deps(client_manifest, entry_path, true, root); // Eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC. // However, if it is not used during SSR (not present in the server manifest), @@ -174,13 +176,13 @@ export function build_server_nodes( /** @type {import('types').AssetDependencies | undefined} */ let component; if (node.component) { - component = find_deps(server_manifest, node.component, true); + component = find_deps(server_manifest, node.component, true, root); } /** @type {import('types').AssetDependencies | undefined} */ let universal; if (node.universal) { - universal = find_deps(server_manifest, node.universal, true); + universal = find_deps(server_manifest, node.universal, true, root); } /** @type {Set} */ diff --git a/packages/kit/src/exports/vite/build/build_service_worker.js b/packages/kit/src/exports/vite/build/build_service_worker.js deleted file mode 100644 index 2ea6f5e7a9e1..000000000000 --- a/packages/kit/src/exports/vite/build/build_service_worker.js +++ /dev/null @@ -1,149 +0,0 @@ -import fs from 'node:fs'; -import process from 'node:process'; -import * as vite from 'vite'; -import { dedent } from '../../../core/sync/utils.js'; -import { s } from '../../../utils/misc.js'; -import { get_config_aliases, strip_virtual_prefix, get_env, normalize_id } from '../utils.js'; -import { create_static_module } from '../../../core/env.js'; -import { env_static_public, service_worker } from '../module_ids.js'; - -// @ts-ignore `vite.rolldownVersion` only exists in `rolldown-vite` -const is_rolldown = !!vite.rolldownVersion; - -/** - * @param {string} out - * @param {import('types').ValidatedKitConfig} kit - * @param {import('vite').ResolvedConfig} vite_config - * @param {import('types').ManifestData} manifest_data - * @param {string} service_worker_entry_file - * @param {import('types').Prerendered} prerendered - * @param {import('vite').Manifest} client_manifest - */ -export async function build_service_worker( - out, - kit, - vite_config, - manifest_data, - service_worker_entry_file, - prerendered, - client_manifest -) { - const build = new Set(); - for (const key in client_manifest) { - const { file, css = [], assets = [] } = client_manifest[key]; - build.add(file); - css.forEach((file) => build.add(file)); - assets.forEach((file) => build.add(file)); - } - - // in a service worker, `location` is the location of the service worker itself, - // which is guaranteed to be `/service-worker.js` - const base = "location.pathname.split('/').slice(0, -1).join('/')"; - - const service_worker_code = dedent` - export const base = /*@__PURE__*/ ${base}; - - export const build = [ - ${Array.from(build) - .map((file) => `base + ${s(`/${file}`)}`) - .join(',\n')} - ]; - - export const files = [ - ${manifest_data.assets - .filter((asset) => kit.serviceWorker.files(asset.file)) - .map((asset) => `base + ${s(`/${asset.file}`)}`) - .join(',\n')} - ]; - - export const prerendered = [ - ${prerendered.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')} - ]; - - export const version = ${s(kit.version.name)}; - `; - - const env = get_env(kit.env, vite_config.mode); - - /** - * @type {import('vite').Plugin} - */ - const sw_virtual_modules = { - name: 'service-worker-build-virtual-modules', - resolveId(id) { - if (id.startsWith('$env/') || id.startsWith('$app/') || id === '$service-worker') { - // ids with :$ don't work with reverse proxies like nginx - return `\0virtual:${id.substring(1)}`; - } - }, - - load(id) { - if (!id.startsWith('\0virtual:')) return; - - if (id === service_worker) { - return service_worker_code; - } - - if (id === env_static_public) { - return create_static_module('$env/static/public', env.public); - } - - const normalized_cwd = vite.normalizePath(process.cwd()); - const normalized_lib = vite.normalizePath(kit.files.lib); - const relative = normalize_id(id, normalized_lib, normalized_cwd); - const stripped = strip_virtual_prefix(relative); - throw new Error( - `Cannot import ${stripped} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.` - ); - } - }; - - /** @type {import('vite').InlineConfig} */ - const config = { - build: { - modulePreload: false, - rollupOptions: { - input: { - 'service-worker': service_worker_entry_file - }, - output: { - // .mjs so that esbuild doesn't incorrectly inject `export` https://github.com/vitejs/vite/issues/15379 - entryFileNames: `service-worker.${is_rolldown ? 'js' : 'mjs'}`, - assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`, - inlineDynamicImports: !is_rolldown - } - }, - outDir: `${out}/client`, - emptyOutDir: false, - minify: vite_config.build.minify - }, - configFile: false, - define: vite_config.define, - publicDir: false, - plugins: [sw_virtual_modules], - resolve: { - alias: [...get_config_aliases(kit)] - }, - experimental: { - renderBuiltUrl(filename) { - return { - runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` - }; - } - } - }; - - // we must reference Vite 8 options conditionally. Otherwise, older Vite - // versions throw an error about unknown config options - if (is_rolldown && config?.build?.rollupOptions?.output) { - // @ts-ignore only available in Vite 8 - config.build.rollupOptions.output.codeSplitting = true; - } - - await vite.build(config); - - // rename .mjs to .js to avoid incorrect MIME types with ancient webservers - if (!is_rolldown) { - fs.renameSync(`${out}/client/service-worker.mjs`, `${out}/client/service-worker.js`); - } -} diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index 689882593e92..ddd91c6a8927 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -1,22 +1,23 @@ /** @import { ServerMetadata } from 'types' */ -/** @import { OutputBundle } from 'rollup' */ +/** @import { Rolldown } from 'vite' */ import fs from 'node:fs'; import path from 'node:path'; import { Parser } from 'acorn'; import MagicString from 'magic-string'; import { posixify } from '../../../utils/filesystem.js'; -import { import_peer } from '../../../utils/import.js'; /** + * @param {typeof import('vite')} vite * @param {string} out * @param {Array<{ hash: string, file: string }>} remotes * @param {ServerMetadata} metadata * @param {string} cwd - * @param {OutputBundle} server_bundle + * @param {Rolldown.RolldownOutput} server_bundle * @param {NonNullable['sourcemap']} sourcemap */ export async function treeshake_prerendered_remotes( + vite, out, remotes, metadata, @@ -26,8 +27,6 @@ export async function treeshake_prerendered_remotes( ) { if (remotes.length === 0) return; - const vite = /** @type {typeof import('vite')} */ (await import_peer('vite')); - for (const remote of remotes) { const exports_map = metadata.remotes.get(remote.hash); if (!exports_map) continue; @@ -90,7 +89,7 @@ export async function treeshake_prerendered_remotes( const stubbed = modified_code.toString(); fs.writeFileSync(chunk_path, stubbed); - const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( + const bundle = /** @type {import('vite').Rolldown.RolldownOutput} */ ( await vite.build({ configFile: false, build: { diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index 303841266c4d..e53e788bfbcf 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -7,9 +7,10 @@ import { normalizePath } from 'vite'; * @param {import('vite').Manifest} manifest * @param {string} entry * @param {boolean} add_dynamic_css + * @param {string} root * @returns {import('types').AssetDependencies} */ -export function find_deps(manifest, entry, add_dynamic_css) { +export function find_deps(manifest, entry, add_dynamic_css, root) { /** @type {Set} */ const seen = new Set(); @@ -35,7 +36,7 @@ export function find_deps(manifest, entry, add_dynamic_css) { if (seen.has(current)) return; seen.add(current); - const { chunk } = resolve_symlinks(manifest, current); + const { chunk } = resolve_symlinks(manifest, current, root); if (add_js) imports.add(chunk.file); @@ -81,7 +82,7 @@ export function find_deps(manifest, entry, add_dynamic_css) { } } - const { chunk, file } = resolve_symlinks(manifest, entry); + const { chunk, file } = resolve_symlinks(manifest, entry, root); traverse(file, true, entry, 0); @@ -101,10 +102,11 @@ export function find_deps(manifest, entry, add_dynamic_css) { /** * @param {import('vite').Manifest} manifest * @param {string} file + * @param {string} root */ -export function resolve_symlinks(manifest, file) { +export function resolve_symlinks(manifest, file, root) { while (!manifest[file]) { - const next = normalizePath(path.relative('.', fs.realpathSync(file))); + const next = normalizePath(path.relative(root, fs.realpathSync(file))); if (next === file) throw new Error(`Could not find file "${file}" in Vite manifest`); file = next; } diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 8371a81ba5b1..442e85c9ebe0 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -1,26 +1,23 @@ import fs from 'node:fs'; import path from 'node:path'; -import process from 'node:process'; import { URL } from 'node:url'; import { AsyncLocalStorage } from 'node:async_hooks'; -import colors from 'kleur'; +import { styleText } from 'node:util'; import sirv from 'sirv'; import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite'; import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js'; -import { installPolyfills } from '../../../exports/node/polyfills.js'; import { coalesce_to_error } from '../../../utils/error.js'; import { from_fs, posixify, resolve_entry, to_fs } from '../../../utils/filesystem.js'; import { load_error_page } from '../../../core/config/index.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import * as sync from '../../../core/sync/sync.js'; -import { get_mime_lookup, runtime_base } from '../../../core/utils.js'; +import { get_mime_lookup, get_runtime_base } from '../../../core/utils.js'; import { compact } from '../../../utils/array.js'; import { is_chrome_devtools_request, not_found } from '../utils.js'; import { SCHEME } from '../../../utils/url.js'; import { check_feature } from '../../../utils/features.js'; import { escape_html } from '../../../utils/escape.js'; -const cwd = process.cwd(); // vite-specifc queries that we should skip handling for css urls const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; @@ -29,11 +26,10 @@ const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; * @param {import('vite').ResolvedConfig} vite_config * @param {import('types').ValidatedConfig} svelte_config * @param {() => Array<{ hash: string, file: string }>} get_remotes + * @param {string} root The project root directory * @return {Promise void>>} */ -export async function dev(vite, vite_config, svelte_config, get_remotes) { - installPolyfills(); - +export async function dev(vite, vite_config, svelte_config, get_remotes, root) { const async_local_storage = new AsyncLocalStorage(); globalThis.__SVELTEKIT_TRACK__ = (label) => { @@ -54,7 +50,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { return fetch(info, init); }; - sync.init(svelte_config, vite_config.mode); + sync.init(svelte_config, vite_config.mode, root); /** @type {import('types').ManifestData} */ let manifest_data; @@ -69,7 +65,9 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { try { return await vite.ssrLoadModule(url, { fixStacktrace: true }); } catch (/** @type {any} */ err) { - const msg = buildErrorMessage(err, [colors.red(`Internal server error: ${err.message}`)]); + const msg = buildErrorMessage(err, [ + styleText('red', `Internal server error: ${err.message}`) + ]); if (!vite.config.logger.hasErrorLogged(err)) { vite.config.logger.error(msg, { error: err }); @@ -104,7 +102,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { function update_manifest() { try { - ({ manifest_data } = sync.create(svelte_config)); + ({ manifest_data } = sync.create(svelte_config, root)); if (manifest_error) { manifest_error = null; @@ -113,7 +111,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { } catch (error) { manifest_error = /** @type {Error} */ (error); - console.error(colors.bold().red(manifest_error.message)); + console.error(styleText(['bold', 'red'], manifest_error.message)); vite.ws.send({ type: 'error', err: { @@ -132,7 +130,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { mimeTypes: get_mime_lookup(manifest_data), _: { client: { - start: `${runtime_base}/client/entry.js`, + start: `${get_runtime_base(root)}/client/entry.js`, app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`, imports: [], stylesheets: [], @@ -275,7 +273,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { page: route.page, endpoint: endpoint ? async () => { - const url = path.resolve(cwd, endpoint.file); + const url = path.resolve(root, endpoint.file); return await loud_ssr_load_module(url); } : null, @@ -289,7 +287,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { for (const key in manifest_data.matchers) { const file = manifest_data.matchers[key]; - const url = path.resolve(cwd, file); + const url = path.resolve(root, file); const module = await vite.ssrLoadModule(url, { fixStacktrace: true }); if (module.match) { @@ -359,7 +357,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { // Don't run for a single file if the whole manifest is about to get updated // Unless it's a file where the trailing slash page option might have changed if (timeout || restarting || !/\+(page|layout|server).*$/.test(file)) return; - sync.update(svelte_config, manifest_data, file); + sync.update(svelte_config, manifest_data, file, root); }); const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files; @@ -382,7 +380,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { file.startsWith(serviceWorker) || file.startsWith(hooks.server) ) { - sync.server(svelte_config); + sync.server(svelte_config, root); } }); @@ -454,7 +452,9 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { }`; const decoded = decodeURI(new URL(base + req.url).pathname); - const file = posixify(path.resolve(decoded.slice(svelte_config.kit.paths.base.length + 1))); + const file = posixify( + path.resolve(root, decoded.slice(svelte_config.kit.paths.base.length + 1)) + ); const is_file = fs.existsSync(file) && !fs.statSync(file).isDirectory(); const allowed = !vite_config.server.fs.strict || @@ -501,11 +501,13 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { // we have to import `Server` before calling `set_assets` const { Server } = /** @type {import('types').ServerModule} */ ( - await vite.ssrLoadModule(`${runtime_base}/server/index.js`, { fixStacktrace: true }) + await vite.ssrLoadModule(`${get_runtime_base(root)}/server/index.js`, { + fixStacktrace: true + }) ); const { set_fix_stack_trace } = await vite.ssrLoadModule( - `${runtime_base}/shared-server.js` + `${get_runtime_base(root)}/shared-server.js` ); set_fix_stack_trace(fix_stack_trace); @@ -525,7 +527,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { }); if (manifest_error) { - console.error(colors.bold().red(manifest_error.message)); + console.error(styleText(['bold', 'red'], manifest_error.message)); const error_page = load_error_page(svelte_config); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index b448fc7bb3d1..1a6f3c43636b 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1,18 +1,17 @@ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; +import { styleText } from 'node:util'; -import colors from 'kleur'; +import { exactRegex, prefixRegex } from 'rolldown/filter'; import { copy, mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js'; import { create_static_module, create_dynamic_module } from '../../core/env.js'; import * as sync from '../../core/sync/sync.js'; import { create_assets } from '../../core/sync/create_manifest_data/index.js'; import { runtime_directory, logger } from '../../core/utils.js'; -import { load_config } from '../../core/config/index.js'; import { generate_manifest } from '../../core/generate_manifest/index.js'; import { build_server_nodes } from './build/build_server.js'; -import { build_service_worker } from './build/build_service_worker.js'; import { assets_base, find_deps, resolve_symlinks } from './build/utils.js'; import { dev } from './dev/index.js'; import { preview } from './preview/index.js'; @@ -21,15 +20,17 @@ import { get_config_aliases, get_env, normalize_id, - stackless + stackless, + strip_virtual_prefix } from './utils.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; import { hash } from '../../utils/hash.js'; -import { dedent, isSvelte5Plus } from '../../core/sync/utils.js'; +import { dedent } from '../../core/sync/utils.js'; import { + app_server, env_dynamic_private, env_dynamic_public, env_static_private, @@ -41,9 +42,13 @@ import { import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; import { should_ignore, has_children } from './static_analysis/utils.js'; +import { load_config } from '../../core/config/index.js'; import { treeshake_prerendered_remotes } from './build/remote.js'; -const cwd = posixify(process.cwd()); +const cwd = process.cwd(); + +/** @type {string} */ +let root; /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { @@ -59,7 +64,7 @@ const enforced_config = { }, manifest: true, outDir: true, - rollupOptions: { + rolldownOptions: { input: true, output: { format: true, @@ -78,8 +83,7 @@ const enforced_config = { $lib: true, '$service-worker': true } - }, - root: true + } }; const options_regex = /(export\s+const\s+(prerender|csr|ssr|trailingSlash))\s*=/s; @@ -99,7 +103,7 @@ const warning_preprocessor = { const fixed = basename.replace('.svelte', '(.server).js/ts'); const message = - `\n${colors.bold().red(path.relative('.', filename))}\n` + + `\n${styleText(['bold', 'red'], path.relative(root, filename))}\n` + `\`${match[1]}\` will be ignored — move it to ${fixed} instead. See https://svelte.dev/docs/kit/page-options for more information.`; if (!warned.has(message)) { @@ -114,10 +118,10 @@ const warning_preprocessor = { const basename = path.basename(filename); - if (basename.startsWith('+layout.') && !has_children(content, isSvelte5Plus())) { + if (basename.startsWith('+layout.') && !has_children(content, true)) { const message = - `\n${colors.bold().red(path.relative('.', filename))}\n` + - `\`\`${isSvelte5Plus() ? ' or `{@render ...}` tag' : ''}` + + `\n${styleText(['bold', 'red'], path.relative(root, filename))}\n` + + '`` or `{@render ...}` tag' + ' missing — inner content will not be rendered'; if (!warned.has(message)) { @@ -128,76 +132,113 @@ const warning_preprocessor = { } }; +/** @type {typeof import('@sveltejs/vite-plugin-svelte')} */ +let vite_plugin_svelte; + /** * Returns the SvelteKit Vite plugins. * @returns {Promise} */ export async function sveltekit() { - const svelte_config = await load_config(); - - /** @type {import('@sveltejs/vite-plugin-svelte').Options['preprocess']} */ - let preprocess = svelte_config.preprocess; - if (Array.isArray(preprocess)) { - preprocess = [...preprocess, warning_preprocessor]; - } else if (preprocess) { - preprocess = [preprocess, warning_preprocessor]; - } else { - preprocess = warning_preprocessor; - } + // the config options will be set only after the Vite `config` hook runs + // because we need to find `svelte.config.js` relative to `vite.config.root` + const svelte_config = /** @type {import('types').ValidatedConfig} */ ({}); /** @type {import('@sveltejs/vite-plugin-svelte').Options} */ const vite_plugin_svelte_options = { - configFile: false, - extensions: svelte_config.extensions, - preprocess, - onwarn: svelte_config.onwarn, - compilerOptions: { - // @ts-ignore - ignore this property when running `pnpm check` against Svelte 5 in the ecosystem CI - hydratable: isSvelte5Plus() ? undefined : true, - ...svelte_config.compilerOptions - }, - ...svelte_config.vitePlugin + // we don't want vite-plugin-svelte to load the config file itself because + // it will try to validate it without knowing that kit options are valid + configFile: false }; - const { svelte } = await import_peer('@sveltejs/vite-plugin-svelte'); + vite_plugin_svelte = await import_peer('@sveltejs/vite-plugin-svelte', cwd); - return [...svelte(vite_plugin_svelte_options), ...(await kit({ svelte_config }))]; + return [ + plugin_svelte_config({ vite_plugin_svelte_options, svelte_config }), + ...vite_plugin_svelte.svelte(vite_plugin_svelte_options), + ...kit({ svelte_config }) + ]; +} + +/** @param {import('vite').UserConfig | import('vite').ResolvedConfig} vite_config */ +function resolve_root(vite_config) { + return posixify(vite_config.root ? path.resolve(vite_config.root) : cwd); } -// These variables live outside the `kit()` function because it is re-invoked by each Vite build +/** + * Resolves the Svelte config using the `vite.config.root` setting before any + * of our other plugins try to access the config objects + * @param {{ + * vite_plugin_svelte_options: import('@sveltejs/vite-plugin-svelte').Options; + * svelte_config: import('types').ValidatedConfig; + * }} options + * @return {import('vite').Plugin} + */ +function plugin_svelte_config({ vite_plugin_svelte_options, svelte_config }) { + return { + name: 'vite-plugin-sveltekit-resolve-svelte-config', + // make sure it runs first + enforce: 'pre', + config: { + order: 'pre', + async handler(config) { + root = resolve_root(config); + + const user_svelte_config = await load_config({ cwd: root }); -let secondary_build_started = false; + /** @type {import('@sveltejs/vite-plugin-svelte').Options['preprocess']} */ + let preprocess = user_svelte_config.preprocess; + if (Array.isArray(preprocess)) { + preprocess = [...preprocess, warning_preprocessor]; + } else if (preprocess) { + preprocess = [preprocess, warning_preprocessor]; + } else { + preprocess = warning_preprocessor; + } -/** @type {import('types').ManifestData} */ -let manifest_data; + vite_plugin_svelte_options.extensions = user_svelte_config.extensions; + vite_plugin_svelte_options.preprocess = preprocess; + vite_plugin_svelte_options.onwarn = user_svelte_config.onwarn; + vite_plugin_svelte_options.compilerOptions = { ...user_svelte_config.compilerOptions }; + Object.assign(vite_plugin_svelte_options, user_svelte_config.vitePlugin); -/** @type {import('types').ServerMetadata | undefined} only set at build time once analysis is finished */ -let build_metadata = undefined; + Object.assign(svelte_config, user_svelte_config); + } + }, + // TODO: do we even need to set `root` based on the final Vite config? + configResolved: { + order: 'pre', + handler(config) { + root = resolve_root(config); + } + } + }; +} /** - * Returns the SvelteKit Vite plugin. Vite executes Rollup hooks as well as some of its own. + * Returns the SvelteKit Vite plugin. Vite executes Rolldown hooks as well as some of its own. * Background reading is available at: - * - https://vitejs.dev/guide/api-plugin.html - * - https://rollupjs.org/guide/en/#plugin-development + * - https://vite.dev/guide/api-plugin.html + * - https://rolldown.rs/apis/plugin-api * * You can get an idea of the lifecycle by looking at the flow charts here: - * - https://rollupjs.org/guide/en/#build-hooks - * - https://rollupjs.org/guide/en/#output-generation-hooks + * - https://rolldown.rs/apis/plugin-api#build-hooks + * - https://rolldown.rs/apis/plugin-api#output-generation-hooks * * @param {{ svelte_config: import('types').ValidatedConfig }} options - * @return {Promise} + * @return {import('vite').Plugin[]} */ -async function kit({ svelte_config }) { - /** @type {import('vite')} */ - const vite = await import_peer('vite'); +function kit({ svelte_config }) { + /** @type {typeof import('vite')} */ + let vite; - // @ts-ignore `vite.rolldownVersion` only exists in `vite 8` - const is_rolldown = !!vite.rolldownVersion; + /** @type {import('types').ValidatedKitConfig} */ + let kit; + /** @type {string} */ + let out; - const { kit } = svelte_config; - const out = `${kit.outDir}/output`; - - const version_hash = hash(kit.version.name); + /** @type {string} */ + let version_hash; /** @type {import('vite').ResolvedConfig} */ let vite_config; @@ -211,19 +252,26 @@ async function kit({ svelte_config }) { /** @type {{ public: Record; private: Record }} */ let env; - /** @type {() => Promise} */ - let finalise; + /** @type {import('types').ManifestData} */ + let manifest_data; + + /** @type {import('types').ServerMetadata | undefined} only set at build time once analysis is finished */ + let build_metadata = undefined; /** @type {import('vite').UserConfig} */ let initial_config; - const service_worker_entry_file = resolve_entry(kit.files.serviceWorker); - const parsed_service_worker = path.parse(kit.files.serviceWorker); - - const normalized_cwd = vite.normalizePath(cwd); - const normalized_lib = vite.normalizePath(kit.files.lib); - const normalized_node_modules = vite.normalizePath(path.resolve('node_modules')); - + /** @type {string | null} */ + let service_worker_entry_file; + /** @type {import('node:path').ParsedPath} */ + let parsed_service_worker; + + /** @type {string} */ + let normalized_cwd; + /** @type {string} */ + let normalized_lib; + /** @type {string} */ + let normalized_node_modules; /** * A map showing which features (such as `$app/server:read`) are defined * in which chunks, so that we can later determine which routes use which features @@ -238,26 +286,44 @@ async function kit({ svelte_config }) { const plugin_setup = { name: 'vite-plugin-sveltekit-setup', + applyToEnvironment(environment) { + return environment.name !== 'serviceWorker'; + }, + /** * Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file. * @see https://vitejs.dev/guide/api-plugin.html#config */ config: { order: 'pre', - handler(config, config_env) { + async handler(config, config_env) { initial_config = config; vite_config_env = config_env; is_build = config_env.command === 'build'; + ({ kit } = svelte_config); + out = `${kit.outDir}/output`; + + version_hash = hash(kit.version.name); + env = get_env(kit.env, vite_config_env.mode); + service_worker_entry_file = resolve_entry(kit.files.serviceWorker); + parsed_service_worker = path.parse(kit.files.serviceWorker); + + vite = await import_peer('vite', root); + + normalized_cwd = vite.normalizePath(root); + normalized_lib = vite.normalizePath(kit.files.lib); + normalized_node_modules = vite.normalizePath(path.resolve(root, 'node_modules')); + const allow = new Set([ kit.files.lib, kit.files.routes, kit.outDir, - path.resolve('src'), // TODO this isn't correct if user changed all his files to sth else than src (like in test/options) - path.resolve('node_modules'), - path.resolve(vite.searchForWorkspaceRoot(cwd), 'node_modules') + path.resolve(root, kit.files.src), + path.resolve(root, 'node_modules'), + path.resolve(cwd, 'node_modules') ]); // We can only add directories to the allow list, so we find out @@ -274,10 +340,9 @@ async function kit({ svelte_config }) { alias: [ { find: '__SERVER__', replacement: `${generated}/server` }, { find: '$app', replacement: `${runtime_directory}/app` }, - ...get_config_aliases(kit) + ...get_config_aliases(kit, root) ] }, - root: cwd, server: { cors: { preflightContinue: true }, fs: { @@ -315,7 +380,7 @@ async function kit({ svelte_config }) { // export conditions resolved correctly through Vite. This prevents adapters // that bundle later on from resolving the export conditions incorrectly // and for example include browser-only code in the server output - // because they for example use esbuild.build with `platform: 'browser'` + // because they for example use rolldown.build with `platform: 'browser'` 'esm-env', // This forces `$app/*` modules to be bundled, since they depend on // virtual modules like `__sveltekit/environment` (this isn't a valid bare @@ -328,36 +393,23 @@ async function kit({ svelte_config }) { if (kit.experimental.remoteFunctions) { // treat .remote.js files as empty for the purposes of prebundling - // detects rolldown to avoid a warning message in vite 8 beta const remote_id_filter = new RegExp( `.remote(${kit.moduleExtensions.join('|')})$`.replaceAll('.', '\\.') ); - new_config.optimizeDeps ??= {}; // for some reason ts says this could be undefined even though it was set above - if (is_rolldown) { - // @ts-ignore - new_config.optimizeDeps.rolldownOptions ??= {}; - // @ts-ignore - new_config.optimizeDeps.rolldownOptions.plugins ??= []; - // @ts-ignore - new_config.optimizeDeps.rolldownOptions.plugins.push({ - name: 'vite-plugin-sveltekit-setup:optimize-remote-functions', - load: { - filter: { id: remote_id_filter }, - handler() { - return ''; - } - } - }); - } else { - new_config.optimizeDeps.esbuildOptions ??= {}; - new_config.optimizeDeps.esbuildOptions.plugins ??= []; - new_config.optimizeDeps.esbuildOptions.plugins.push({ - name: 'vite-plugin-sveltekit-setup:optimize-remote-functions', - setup(build) { - build.onLoad({ filter: remote_id_filter }, () => ({ contents: '' })); + // @ts-expect-error optimizeDeps is already set above + new_config.optimizeDeps.rolldownOptions ??= {}; + // @ts-expect-error + new_config.optimizeDeps.rolldownOptions.plugins ??= []; + // @ts-expect-error + new_config.optimizeDeps.rolldownOptions.plugins.push({ + name: 'vite-plugin-sveltekit-setup:optimize-remote-functions', + load: { + filter: { id: remote_id_filter }, + handler() { + return ''; } - }); - } + } + }); } const define = { @@ -374,36 +426,14 @@ async function kit({ svelte_config }) { }; if (is_build) { - if (!new_config.build) new_config.build = {}; - new_config.build.ssr = !secondary_build_started; - new_config.define = { ...define, __SVELTEKIT_ADAPTER_NAME__: s(kit.adapter?.name), __SVELTEKIT_APP_VERSION_FILE__: s(`${kit.appDir}/version.json`), - __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval), - __SVELTEKIT_PAYLOAD__: new_config.build.ssr - ? '{}' - : `globalThis.__sveltekit_${version_hash}` + __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval) }; - if (!secondary_build_started) { - manifest_data = sync.all(svelte_config, config_env.mode).manifest_data; - // During the initial server build we don't know yet - new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = 'true'; - new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = 'true'; - } else { - const nodes = Object.values( - /** @type {import('types').ServerMetadata} */ (build_metadata).nodes - ); - - // Through the finished analysis we can now check if any node has server or universal load functions - const has_server_load = nodes.some((node) => node.has_server_load); - const has_universal_load = nodes.some((node) => node.has_universal_load); - - new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = s(has_server_load); - new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = s(has_universal_load); - } + manifest_data = sync.all(svelte_config, config_env.mode, root).manifest_data; } else { new_config.define = { ...define, @@ -413,14 +443,10 @@ async function kit({ svelte_config }) { __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true' }; - // @ts-ignore this prevents a reference error if `client.js` is imported on the server - globalThis.__sveltekit_dev = {}; - // These Kit dependencies are packaged as CommonJS, which means they must always be externalized. // Without this, the tests will still pass but `pnpm dev` will fail in projects that link `@sveltejs/kit`. /** @type {NonNullable} */ (new_config.ssr).external = [ - 'cookie', - 'set-cookie-parser' + 'cookie' ]; } @@ -442,6 +468,10 @@ async function kit({ svelte_config }) { const plugin_virtual_modules = { name: 'vite-plugin-sveltekit-virtual-modules', + applyToEnvironment(environment) { + return environment.name !== 'serviceWorker'; + }, + resolveId(id, importer) { if (id === '__sveltekit/manifest') { return `${kit.outDir}/generated/client-optimized/app.js`; @@ -450,7 +480,7 @@ async function kit({ svelte_config }) { // If importing from a service-worker, only allow $service-worker & $env/static/public, but none of the other virtual modules. // This check won't catch transitive imports, but it will warn when the import comes from a service-worker directly. // Transitive imports will be caught during the build. - // TODO move this logic to plugin_guard + // TODO move this logic to plugin_guard. add a filter to this resolveId when doing so if (importer) { const parsed_importer = path.parse(importer); @@ -483,45 +513,56 @@ async function kit({ svelte_config }) { return `\0virtual:${id}`; } }, + load: { + filter: { + id: [ + exactRegex(env_static_private), + exactRegex(env_static_public), + exactRegex(env_dynamic_private), + exactRegex(env_dynamic_public), + exactRegex(service_worker), + exactRegex(sveltekit_environment), + exactRegex(sveltekit_server) + ] + }, + handler(id) { + switch (id) { + case env_static_private: + return create_static_module('$env/static/private', env.private); + + case env_static_public: + return create_static_module('$env/static/public', env.public); + + case env_dynamic_private: + return create_dynamic_module( + 'private', + vite_config_env.command === 'serve' ? env.private : undefined, + root + ); - load(id, options) { - const browser = !options?.ssr; - - const global = is_build - ? `globalThis.__sveltekit_${version_hash}` - : 'globalThis.__sveltekit_dev'; - - switch (id) { - case env_static_private: - return create_static_module('$env/static/private', env.private); - - case env_static_public: - return create_static_module('$env/static/public', env.public); - - case env_dynamic_private: - return create_dynamic_module( - 'private', - vite_config_env.command === 'serve' ? env.private : undefined - ); + case env_dynamic_public: { + // populate `$env/dynamic/public` from `window` + if (this.environment.config.consumer === 'client') { + const global = is_build + ? `globalThis.__sveltekit_${version_hash}` + : 'globalThis.__sveltekit_dev'; + return `export const env = ${global}.env;`; + } - case env_dynamic_public: - // populate `$env/dynamic/public` from `window` - if (browser) { - return `export const env = ${global}.env;`; + return create_dynamic_module( + 'public', + vite_config_env.command === 'serve' ? env.public : undefined, + root + ); } - return create_dynamic_module( - 'public', - vite_config_env.command === 'serve' ? env.public : undefined - ); - - case service_worker: - return create_service_worker_module(svelte_config); + case service_worker: + return create_service_worker_module(svelte_config); - case sveltekit_environment: { - const { version } = svelte_config.kit; + case sveltekit_environment: { + const { version } = svelte_config.kit; - return dedent` + return dedent` export const version = ${s(version.name)}; export let building = false; export let prerendering = false; @@ -534,10 +575,10 @@ async function kit({ svelte_config }) { prerendering = true; } `; - } + } - case sveltekit_server: { - return dedent` + case sveltekit_server: { + return dedent` export let read_implementation = null; export let manifest = null; @@ -550,6 +591,7 @@ async function kit({ svelte_config }) { manifest = _; } `; + } } } } @@ -571,45 +613,71 @@ async function kit({ svelte_config }) { // are added to the module graph enforce: 'pre', - async resolveId(id, importer, options) { - if (importer && !importer.endsWith('index.html')) { - const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }); - - if (resolved) { - const normalized = normalize_id(resolved.id, normalized_lib, normalized_cwd); + applyToEnvironment(environment) { + return environment.name !== 'serviceWorker'; + }, - let importers = import_map.get(normalized); + resolveId: { + // TODO: use composable filter API here when supported: + // https://github.com/vitejs/rolldown-vite/issues/605 + // filter: ([ + // exclude(importerId(/index\.html$/)), + // include(importerId(/.+/)) + // ]), + async handler(id, importer, options) { + if (importer && !importer.endsWith('index.html')) { + const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }); + + if (resolved) { + const normalized = normalize_id(resolved.id, normalized_lib, normalized_cwd); + + let importers = import_map.get(normalized); + + if (!importers) { + importers = new Set(); + import_map.set(normalized, importers); + } - if (!importers) { - importers = new Set(); - import_map.set(normalized, importers); + importers.add(normalize_id(importer, normalized_lib, normalized_cwd)); } - - importers.add(normalize_id(importer, normalized_lib, normalized_cwd)); } } }, - load(id, options) { - if (options?.ssr === true || process.env.TEST === 'true') { - return; - } + load: { + filter: { + id: [ + exactRegex(env_static_private), + exactRegex(env_dynamic_private), + exactRegex(app_server), + /\/server\//, + new RegExp(`${server_only_pattern.source}$`) + ] + }, + handler(id) { + if (this.environment.config.consumer !== 'client') return; - // skip .server.js files outside the cwd or in node_modules, as the filename might not mean 'server-only module' in this context - const is_internal = id.startsWith(normalized_cwd) && !id.startsWith(normalized_node_modules); + // skip .server.js files outside the cwd or in node_modules, as the filename might not mean 'server-only module' in this context + const is_internal = + id.startsWith(normalized_cwd) && !id.startsWith(normalized_node_modules); - const normalized = normalize_id(id, normalized_lib, normalized_cwd); + const normalized = normalize_id(id, normalized_lib, normalized_cwd); - const is_server_only = - normalized === '$env/static/private' || - normalized === '$env/dynamic/private' || - normalized === '$app/server' || - normalized.startsWith('$lib/server/') || - (is_internal && server_only_pattern.test(path.basename(id))); + const is_server_only = + normalized === '$env/static/private' || + normalized === '$env/dynamic/private' || + normalized === '$app/server' || + normalized.startsWith('$lib/server/') || + (is_internal && server_only_pattern.test(path.basename(id))); + + // skip .server.js files outside the cwd or in node_modules, as the filename might not mean 'server-only module' in this context + // TODO: address https://github.com/sveltejs/kit/issues/12529 + if (!is_server_only) { + return; + } - if (is_server_only) { // in dev, this doesn't exist, so we need to create it - manifest_data ??= sync.all(svelte_config, vite_config_env.mode).manifest_data; + manifest_data ??= sync.all(svelte_config, vite_config_env.mode, root).manifest_data; /** @type {Set} */ const entrypoints = new Set(); @@ -621,7 +689,6 @@ async function kit({ svelte_config }) { if (manifest_data.hooks.client) entrypoints.add(manifest_data.hooks.client); if (manifest_data.hooks.universal) entrypoints.add(manifest_data.hooks.universal); - const normalized = normalize_id(id, normalized_lib, normalized_cwd); const chain = [normalized]; let current = normalized; @@ -685,16 +752,33 @@ async function kit({ svelte_config }) { const plugin_remote = { name: 'vite-plugin-sveltekit-remote', - resolveId(id) { - if (id.startsWith('\0sveltekit-remote:')) return id; + applyToEnvironment(environment) { + return environment.name !== 'serviceWorker'; }, - load(id) { - // On-the-fly generated entry point for remote file just forwards the original module - // We're not using manualChunks because it can cause problems with circular dependencies - // (e.g. https://github.com/sveltejs/kit/issues/14679) and module ordering in general - // (e.g. https://github.com/sveltejs/kit/issues/14590). - if (id.startsWith('\0sveltekit-remote:')) { + // prevent other plugins from resolving our remote virtual module + resolveId: { + filter: { + id: prefixRegex('\0sveltekit-remote:') + }, + handler(id) { + return id; + } + }, + + load: { + filter: { + id: prefixRegex('\0sveltekit-remote:') + }, + handler(id) { + if (!kit.experimental.remoteFunctions) { + return null; + } + + // On-the-fly generated entry point for remote file just forwards the original module + // We're not using manualChunks because it can cause problems with circular dependencies + // (e.g. https://github.com/sveltejs/kit/issues/14679) and module ordering in general + // (e.g. https://github.com/sveltejs/kit/issues/14590). const hash_id = id.slice('\0sveltekit-remote:'.length); const original = remote_original_by_hash.get(hash_id); if (!original) throw new Error(`Expected to find metadata for remote file ${id}`); @@ -703,16 +787,24 @@ async function kit({ svelte_config }) { }, configureServer(_dev_server) { + if (!kit.experimental.remoteFunctions) { + return; + } + dev_server = _dev_server; }, - async transform(code, id, opts) { + async transform(code, id) { + if (!kit.experimental.remoteFunctions) { + return; + } + const normalized = normalize_id(id, normalized_lib, normalized_cwd); if (!svelte_config.kit.moduleExtensions.some((ext) => normalized.endsWith(`.remote${ext}`))) { return; } - const file = posixify(path.relative(cwd, id)); + const file = posixify(path.relative(root, id)); const remote = { hash: hash(file), file @@ -720,7 +812,7 @@ async function kit({ svelte_config }) { remotes.push(remote); - if (opts?.ssr) { + if (this.environment.config.consumer !== 'client') { // we need to add an `await Promise.resolve()` because if the user imports this function // on the client AND in a load function when loading the client module we will trigger // an ssrLoadModule during dev. During a link preload, the module can be mistakenly @@ -780,7 +872,7 @@ async function kit({ svelte_config }) { } // in prod, we already built and analysed the server code before - // building the client code, so `remote_exports` is populated + // building the client code, so `remotes` is populated else if (build_metadata?.remotes) { const exports = build_metadata?.remotes.get(remote.hash); if (!exports) throw new Error('Expected to find metadata for remote file ' + id); @@ -810,10 +902,101 @@ async function kit({ svelte_config }) { } }; + /** @type {import('vite').Manifest} */ + let client_manifest; + /** @type {import('types').Prerendered} */ + let prerendered; + + /** @type {Set} */ + let build; + /** @type {string} */ + let service_worker_code; + + /** + * Creates the service worker virtual modules + * @type {import('vite').Plugin} + */ + const plugin_service_worker = { + name: 'vite-plugin-sveltekit-service-worker', + + applyToEnvironment(environment) { + return environment.name === 'serviceWorker'; + }, + + resolveId(id) { + if (id.startsWith('$env/') || id.startsWith('$app/') || id === '$service-worker') { + // ids with :$ don't work with reverse proxies like nginx + return `\0virtual:${id.substring(1)}`; + } + }, + + load(id) { + if (!build) { + build = new Set(); + for (const key in client_manifest) { + const { file, css = [], assets = [] } = client_manifest[key]; + build.add(file); + css.forEach((file) => build.add(file)); + assets.forEach((file) => build.add(file)); + } + + // in a service worker, `location` is the location of the service worker itself, + // which is guaranteed to be `/service-worker.js` + const base = "location.pathname.split('/').slice(0, -1).join('/')"; + + service_worker_code = dedent` + export const base = /*@__PURE__*/ ${base}; + + export const build = [ + ${Array.from(build) + .map((file) => `base + ${s(`/${file}`)}`) + .join(',\n')} + ]; + + export const files = [ + ${manifest_data.assets + .filter((asset) => kit.serviceWorker.files(asset.file)) + .map((asset) => `base + ${s(`/${asset.file}`)}`) + .join(',\n')} + ]; + + export const prerendered = [ + ${prerendered.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')} + ]; + + export const version = ${s(kit.version.name)}; + `; + } + + if (!id.startsWith('\0virtual:')) return; + + if (id === service_worker) { + return service_worker_code; + } + + if (id === env_static_public) { + return create_static_module('$env/static/public', env.public); + } + + const normalized_cwd = vite.normalizePath(vite_config.root); + const normalized_lib = vite.normalizePath(kit.files.lib); + const relative = normalize_id(id, normalized_lib, normalized_cwd); + const stripped = strip_virtual_prefix(relative); + throw new Error( + `Cannot import ${stripped} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.` + ); + } + }; + /** @type {import('vite').Plugin} */ const plugin_compile = { name: 'vite-plugin-sveltekit-compile', + applyToEnvironment(environment) { + return environment.name !== 'serviceWorker'; + }, + + // TODO: add `order: pre` to avoid false-positive warnings of overridden config options set by Vitest /** * Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file. * @see https://vitejs.dev/guide/api-plugin.html#config @@ -825,131 +1008,125 @@ async function kit({ svelte_config }) { /** @type {import('vite').UserConfig} */ let new_config; - const kit_paths_base = kit.paths.base || '/'; - if (is_build) { - const ssr = /** @type {boolean} */ (config.build?.ssr); const prefix = `${kit.appDir}/immutable`; /** @type {Record} */ - const input = {}; + const server_input = { + index: `${runtime_directory}/server/index.js`, + internal: `${kit.outDir}/generated/server/internal.js`, + ['remote-entry']: `${runtime_directory}/app/server/remote/index.js` + }; - if (ssr) { - input.index = `${runtime_directory}/server/index.js`; - input.internal = `${kit.outDir}/generated/server/internal.js`; - input['remote-entry'] = `${runtime_directory}/app/server/remote/index.js`; + // add entry points for every endpoint... + manifest_data.routes.forEach((route) => { + if (route.endpoint) { + const resolved = path.resolve(root, route.endpoint.file); + const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); + const name = posixify(path.join('entries/endpoints', relative.replace(/\.js$/, ''))); + server_input[name] = resolved; + } + }); - // add entry points for every endpoint... - manifest_data.routes.forEach((route) => { - if (route.endpoint) { - const resolved = path.resolve(route.endpoint.file); + // ...and every component used by pages... + manifest_data.nodes.forEach((node) => { + for (const file of [node.component, node.universal, node.server]) { + if (file) { + const resolved = path.resolve(root, file); const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); - const name = posixify( - path.join('entries/endpoints', relative.replace(/\.js$/, '')) - ); - input[name] = resolved; - } - }); - // ...and every component used by pages... - manifest_data.nodes.forEach((node) => { - for (const file of [node.component, node.universal, node.server]) { - if (file) { - const resolved = path.resolve(file); - const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); - - const name = relative.startsWith('..') - ? posixify(path.join('entries/fallbacks', path.basename(file))) - : posixify(path.join('entries/pages', relative.replace(/\.js$/, ''))); - input[name] = resolved; - } + const name = relative.startsWith('..') + ? posixify(path.join('entries/fallbacks', path.basename(file))) + : posixify(path.join('entries/pages', relative.replace(/\.js$/, ''))); + server_input[name] = resolved; } - }); + } + }); - // ...and every matcher - Object.entries(manifest_data.matchers).forEach(([key, file]) => { - const name = posixify(path.join('entries/matchers', key)); - input[name] = path.resolve(file); - }); + // ...and every matcher + Object.entries(manifest_data.matchers).forEach(([key, file]) => { + const name = posixify(path.join('entries/matchers', key)); + server_input[name] = path.resolve(root, file); + }); + + // ...and the hooks files + if (manifest_data.hooks.server) { + server_input['entries/hooks.server'] = path.resolve(root, manifest_data.hooks.server); + } + if (manifest_data.hooks.universal) { + server_input['entries/hooks.universal'] = path.resolve( + root, + manifest_data.hooks.universal + ); + } - // ...and the hooks files - if (manifest_data.hooks.server) { - input['entries/hooks.server'] = path.resolve(manifest_data.hooks.server); + // ...and the server instrumentation file + const server_instrumentation = resolve_entry( + path.join(kit.files.src, 'instrumentation.server') + ); + if (server_instrumentation) { + const { adapter } = kit; + if (adapter && !adapter.supports?.instrumentation?.()) { + throw new Error(`${server_instrumentation} is unsupported in ${adapter.name}.`); } - if (manifest_data.hooks.universal) { - input['entries/hooks.universal'] = path.resolve(manifest_data.hooks.universal); + if (!kit.experimental.instrumentation.server) { + error_for_missing_config( + '`instrumentation.server.js`', + 'kit.experimental.instrumentation.server', + 'true' + ); } + server_input['instrumentation.server'] = server_instrumentation; + } - // ...and the server instrumentation file - const server_instrumentation = resolve_entry( - path.join(kit.files.src, 'instrumentation.server') - ); - if (server_instrumentation) { - const { adapter } = kit; - if (adapter && !adapter.supports?.instrumentation?.()) { - throw new Error(`${server_instrumentation} is unsupported in ${adapter.name}.`); - } - if (!kit.experimental.instrumentation.server) { - error_for_missing_config( - '`instrumentation.server.js`', - 'kit.experimental.instrumentation.server', - 'true' - ); - } - input['instrumentation.server'] = server_instrumentation; - } - } else if (svelte_config.kit.output.bundleStrategy !== 'split') { - input['bundle'] = `${runtime_directory}/client/bundle.js`; + /** @type {Record} */ + const client_input = {}; + + if (svelte_config.kit.output.bundleStrategy !== 'split') { + client_input['bundle'] = `${runtime_directory}/client/bundle.js`; } else { - input['entry/start'] = `${runtime_directory}/client/entry.js`; - input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`; + client_input['entry/start'] = `${runtime_directory}/client/entry.js`; + client_input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`; manifest_data.nodes.forEach((node, i) => { if (node.component || node.universal) { - input[`nodes/${i}`] = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; + client_input[`nodes/${i}`] = + `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; } }); } - // see the kit.output.preloadStrategy option for details on why we have multiple options here - const ext = kit.output.preloadStrategy === 'preload-mjs' ? 'mjs' : 'js'; + const inline = svelte_config.kit.output.bundleStrategy === 'inline'; - // We could always use a relative asset base path here, but it's better for performance not to. - // E.g. Vite generates `new URL('/asset.png', import.meta).href` for a relative path vs just '/asset.png'. - // That's larger and takes longer to run and also causes an HTML diff between SSR and client - // causing us to do a more expensive hydration check. - const client_base = - kit.paths.relative !== false || kit.paths.assets ? './' : kit_paths_base; + const config_base = assets_base(kit); - const inline = !ssr && svelte_config.kit.output.bundleStrategy === 'inline'; - const split = ssr || svelte_config.kit.output.bundleStrategy === 'split'; + /** @type {string} */ + const base = kit.paths.assets || kit.paths.base || '/'; + const root_to_assets = prefix + '/assets/'; + const assets_to_root = + prefix + .split('/') + .map(() => '..') + .join('/') + '/../'; new_config = { - base: ssr ? assets_base(kit) : client_base, + appType: 'custom', + base: config_base, build: { - copyPublicDir: !ssr, - cssCodeSplit: svelte_config.kit.output.bundleStrategy !== 'inline', + cssCodeSplit: !inline, cssMinify: initial_config.build?.minify == null ? true : !!initial_config.build.minify, manifest: true, - outDir: `${out}/${ssr ? 'server' : 'client'}`, - rollupOptions: { - input: inline ? input['bundle'] : input, + rolldownOptions: { output: { - format: inline ? 'iife' : 'esm', name: `__sveltekit_${version_hash}.app`, - entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`, - chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`, assetFileNames: `${prefix}/assets/[name].[hash][extname]`, hoistTransitiveImports: false, - sourcemapIgnoreList, - inlineDynamicImports: is_rolldown ? undefined : !split + sourcemapIgnoreList }, preserveEntrySignatures: 'strict', onwarn(warning, handler) { if ( - (is_rolldown - ? warning.code === 'IMPORT_IS_UNDEFINED' - : warning.code === 'MISSING_EXPORT') && + warning.code === 'IMPORT_IS_UNDEFINED' && warning.id === `${kit.outDir}/generated/client-optimized/app.js` ) { // ignore e.g. undefined `handleError` hook when @@ -960,34 +1137,128 @@ async function kit({ svelte_config }) { handler(warning); } }, - ssrEmitAssets: true, - target: ssr ? 'node18.13' : undefined + emptyOutDir: false, + ssrEmitAssets: true }, - publicDir: kit.files.assets, - worker: { - rollupOptions: { - output: { - entryFileNames: `${prefix}/workers/[name]-[hash].js`, - chunkFileNames: `${prefix}/workers/chunks/[hash].js`, - assetFileNames: `${prefix}/workers/assets/[name]-[hash][extname]`, - hoistTransitiveImports: false + builder: { + sharedConfigBuild: true, + sharedPlugins: true + }, + environments: { + ssr: { + build: { + copyPublicDir: false, + outDir: `${out}/server`, + target: 'node22', + rolldownOptions: { + input: server_input, + output: { + entryFileNames: '[name].js', + chunkFileNames: 'chunks/[name].js' + } + } + }, + // during the initial server build we don't know yet + define: { + __SVELTEKIT_HAS_SERVER_LOAD__: 'true', + __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true', + __SVELTEKIT_PAYLOAD__: '{}' + } + }, + client: { + build: { + outDir: `${out}/client`, + rolldownOptions: { + input: inline ? client_input['bundle'] : client_input, + output: { + format: inline ? 'iife' : 'esm', + entryFileNames: `${prefix}/[name].[hash].js`, + chunkFileNames: `${prefix}/chunks/[hash].js`, + codeSplitting: svelte_config.kit.output.bundleStrategy === 'split' + }, + // This silences Rolldown warnings about not supporting `import.meta` + // for the `iife` output format. We don't care because it's + // only used in development and will be treeshaken away + transform: inline + ? { + define: { + 'import.meta': '{}' + } + } + : undefined + } + }, + define: { + __SVELTEKIT_PAYLOAD__: `globalThis.__sveltekit_${version_hash}` } } - } + }, + experimental: { + // we can't change the base path per environment so we're setting the + // base prefix for files here ourselves + renderBuiltUrl: + // if the Vite base is relative, we need to ensure paths used during SSR are absolute + config_base === './' + ? (filename, { ssr }) => { + if (ssr) return base + filename; + } + : // but if the Vite base is absolute, we just need to ensure + // client paths are relative rather than absolute + (filename, { ssr, hostType }) => { + if (ssr) return; + + if (hostType === 'js') { + // We could always use a relative asset base path here, but it's better for performance not to. + // E.g. Vite generates `new URL('/asset.png', import.meta).href` for a relative path vs just '/asset.png'. + // That's larger and takes longer to run and also causes an HTML diff between SSR and client + // causing us to do a more expensive hydration check. + return { + relative: kit.paths.relative !== false || !!kit.paths.assets + }; + } + + // _app/immutable/assets files + if (filename.startsWith(root_to_assets)) { + return `./${filename.slice(root_to_assets.length)}`; + } + + // static dir files + return assets_to_root + filename; + } + }, + publicDir: kit.files.assets }; - // we must reference Vite 8 options conditionally. Otherwise, older Vite - // versions throw an error about unknown config options - if (is_rolldown && new_config?.build?.rollupOptions?.output) { - // @ts-ignore only available in Vite 8 - new_config.build.rollupOptions.output.codeSplitting = split; + if (service_worker_entry_file) { + /** @type {Record} */ ( + new_config.environments + ).serviceWorker = { + build: { + modulePreload: false, + rolldownOptions: { + input: { + 'service-worker': service_worker_entry_file + }, + output: { + entryFileNames: 'service-worker.js', + assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`, + codeSplitting: false + } + }, + outDir: `${out}/client`, + minify: initial_config.build?.minify + }, + consumer: 'client' + }; } } else { new_config = { appType: 'custom', - base: kit_paths_base, + // we avoid setting base to paths.assets in dev so that we get the + // trailing slash redirect to paths.base if it is set + base: kit.paths.base || '/', build: { - rollupOptions: { + rolldownOptions: { // Vite dependency crawler needs an explicit JS entry point // even though server otherwise works without it input: `${runtime_directory}/client/entry.js` @@ -1008,7 +1279,7 @@ async function kit({ svelte_config }) { * @see https://vitejs.dev/guide/api-plugin.html#configureserver */ async configureServer(vite) { - return await dev(vite, vite_config, svelte_config, () => remotes); + return await dev(vite, vite_config, svelte_config, () => remotes, root); }, /** @@ -1019,20 +1290,6 @@ async function kit({ svelte_config }) { return preview(vite, vite_config, svelte_config); }, - /** - * Clears the output directories. - */ - buildStart() { - if (secondary_build_started) return; - - if (is_build) { - if (!vite_config.build.watch) { - rimraf(out); - } - mkdirp(out); - } - }, - renderChunk(code, chunk) { if (code.includes('__SVELTEKIT_TRACK__')) { return { @@ -1048,7 +1305,7 @@ async function kit({ svelte_config }) { }, generateBundle() { - if (vite_config.build.ssr) return; + if (this.environment.config.consumer !== 'client') return; this.emitFile({ type: 'asset', @@ -1057,365 +1314,339 @@ async function kit({ svelte_config }) { }); }, - /** - * Vite builds a single bundle. We need three bundles: client, server, and service worker. - * The user's package.json scripts will invoke the Vite CLI to execute the server build. We - * then use this hook to kick off builds for the client and service worker. - */ - writeBundle: { - sequential: true, - async handler(_options, server_bundle) { - if (secondary_build_started) return; // only run this once - - const verbose = vite_config.logLevel === 'info'; - const log = logger({ verbose }); - - /** @type {import('vite').Manifest} */ - const server_manifest = JSON.parse(read(`${out}/server/.vite/manifest.json`)); - - /** @type {import('types').BuildData} */ - const build_data = { - app_dir: kit.appDir, - app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, - manifest_data, - out_dir: out, - service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? - client: null, - server_manifest - }; + async buildApp(builder) { + // clears the output directories + if (!builder.config.build.watch) { + rimraf(out); + } + mkdirp(out); + + const server_bundle = /** @type {import('vite').Rolldown.RolldownOutput} */ ( + await builder.build(builder.environments.ssr) + ); + + const verbose = vite_config.logLevel === 'info'; + const log = logger({ verbose }); + + /** @type {import('vite').Manifest} */ + const server_manifest = JSON.parse(read(`${out}/server/.vite/manifest.json`)); + + /** @type {import('types').BuildData} */ + const build_data = { + app_dir: kit.appDir, + app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, + manifest_data, + out_dir: out, + service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? + client: null, + server_manifest + }; - const manifest_path = `${out}/server/manifest-full.js`; - fs.writeFileSync( - manifest_path, - `export const manifest = ${generate_manifest({ - build_data, - prerendered: [], - relative_path: '.', - routes: manifest_data.routes, - remotes - })};\n` - ); + const manifest_path = `${out}/server/manifest-full.js`; + fs.writeFileSync( + manifest_path, + `export const manifest = ${generate_manifest({ + build_data, + prerendered: [], + relative_path: '.', + routes: manifest_data.routes, + remotes, + root + })};\n` + ); + + log.info('Analysing routes'); + + const { metadata } = await analyse({ + hash: kit.router.type === 'hash', + manifest_path, + manifest_data, + server_manifest, + tracked_features, + env: { ...env.private, ...env.public }, + out, + output_config: svelte_config.output, + remotes, + root + }); - log.info('Analysing routes'); - - const { metadata } = await analyse({ - hash: kit.router.type === 'hash', - manifest_path, - manifest_data, - server_manifest, - tracked_features, - env: { ...env.private, ...env.public }, - out, - output_config: svelte_config.output, - remotes - }); - - build_metadata = metadata; - - log.info('Building app'); - - // create client build - write_client_manifest( - kit, - manifest_data, - `${kit.outDir}/generated/client-optimized`, - metadata.nodes - ); + build_metadata = metadata; - secondary_build_started = true; + log.info('Building app'); - let client_chunks; + // create client build + write_client_manifest( + kit, + manifest_data, + `${kit.outDir}/generated/client-optimized`, + metadata.nodes + ); - try { - const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( - await vite.build({ - configFile: vite_config.configFile, - // CLI args - mode: vite_config_env.mode, - logLevel: vite_config.logLevel, - clearScreen: vite_config.clearScreen, - build: { - minify: initial_config.build?.minify, - assetsInlineLimit: vite_config.build.assetsInlineLimit, - sourcemap: vite_config.build.sourcemap - }, - optimizeDeps: { - force: vite_config.optimizeDeps.force - } - }) - ); + const nodes = Object.values( + /** @type {import('types').ServerMetadata} */ (build_metadata).nodes + ); - client_chunks = bundle.output; - } catch (e) { - const error = - e instanceof Error ? e : new Error(/** @type {any} */ (e).message ?? e ?? ''); + // Through the finished analysis we can now check if any node has server or universal load functions + const has_server_load = nodes.some((node) => node.has_server_load); + const has_universal_load = nodes.some((node) => node.has_universal_load); - // without this, errors that occur during the secondary build - // will be logged twice - throw stackless(error.stack ?? error.message); - } + if (builder.environments.client.config.define) { + builder.environments.client.config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = + s(has_server_load); + builder.environments.client.config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = + s(has_universal_load); + } - // We use `build.ssrEmitAssets` so that asset URLs created from - // imports in server-only modules correspond to files in the build, - // but we don't want to copy over CSS imports as these are already - // accounted for in the client bundle. In most cases it would be - // a no-op, but for SSR builds `url(...)` paths are handled - // differently (relative for client, absolute for server) - // resulting in different hashes, and thus duplication - const ssr_stylesheets = new Set( - Object.values(server_manifest) - .map((chunk) => chunk.css ?? []) - .flat() - ); + const { output: client_chunks } = /** @type {import('vite').Rolldown.RolldownOutput} */ ( + await builder.build(builder.environments.client) + ); + + // We use `build.ssrEmitAssets` so that asset URLs created from + // imports in server-only modules correspond to files in the build, + // but we don't want to copy over CSS imports as these are already + // accounted for in the client bundle. In most cases it would be + // a no-op, but for SSR builds `url(...)` paths are handled + // differently (relative for client, absolute for server) + // resulting in different hashes, and thus duplication + const ssr_stylesheets = new Set( + Object.values(server_manifest) + .map((chunk) => chunk.css ?? []) + .flat() + ); + + const assets_path = `${kit.appDir}/immutable/assets`; + const server_assets = `${out}/server/${assets_path}`; + const client_assets = `${out}/client/${assets_path}`; + + if (fs.existsSync(server_assets)) { + for (const file of fs.readdirSync(server_assets)) { + const src = `${server_assets}/${file}`; + const dest = `${client_assets}/${file}`; + + if (fs.existsSync(dest) || ssr_stylesheets.has(`${assets_path}/${file}`)) { + continue; + } - const assets_path = `${kit.appDir}/immutable/assets`; - const server_assets = `${out}/server/${assets_path}`; - const client_assets = `${out}/client/${assets_path}`; + if (file.endsWith('.css')) { + // make absolute paths in CSS relative, for portability + const content = fs + .readFileSync(src, 'utf-8') + .replaceAll(`${kit.paths.base}/${assets_path}`, '.'); - if (fs.existsSync(server_assets)) { - for (const file of fs.readdirSync(server_assets)) { - const src = `${server_assets}/${file}`; - const dest = `${client_assets}/${file}`; + fs.writeFileSync(src, content); + } - if (fs.existsSync(dest) || ssr_stylesheets.has(`${assets_path}/${file}`)) { - continue; - } + copy(src, dest); + } + } - if (file.endsWith('.css')) { - // make absolute paths in CSS relative, for portability - const content = fs - .readFileSync(src, 'utf-8') - .replaceAll(`${kit.paths.base}/${assets_path}`, '.'); + /** @type {import('vite').Manifest} */ + client_manifest = JSON.parse(read(`${out}/client/.vite/manifest.json`)); + + /** + * @param {string} entry + * @param {boolean} [add_dynamic_css] + */ + const deps_of = (entry, add_dynamic_css = false) => + find_deps(client_manifest, posixify(path.relative(root, entry)), add_dynamic_css, root); + + if (svelte_config.kit.output.bundleStrategy === 'split') { + const start = deps_of(`${runtime_directory}/client/entry.js`); + const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`); + + build_data.client = { + start: start.file, + app: app.file, + imports: [...start.imports, ...app.imports], + stylesheets: [...start.stylesheets, ...app.stylesheets], + fonts: [...start.fonts, ...app.fonts], + uses_env_dynamic_public: client_chunks.some( + (chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] + ) + }; - fs.writeFileSync(src, content); + // In case of server-side route resolution, we create a purpose-built route manifest that is + // similar to that on the client, with as much information computed upfront so that we + // don't need to include any code of the actual routes in the server bundle. + if (svelte_config.kit.router.resolution === 'server') { + const nodes = manifest_data.nodes.map((node, i) => { + if (node.component || node.universal) { + const entry = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; + const deps = deps_of(entry, true); + const file = resolve_symlinks( + client_manifest, + `${kit.outDir}/generated/client-optimized/nodes/${i}.js`, + root + ).chunk.file; + + return { file, css: deps.stylesheets }; } - - copy(src, dest); - } + }); + build_data.client.nodes = nodes.map((node) => node?.file); + build_data.client.css = nodes.map((node) => node?.css); + + build_data.client.routes = compact( + manifest_data.routes.map((route) => { + if (!route.page) return; + + return { + id: route.id, + pattern: route.pattern, + params: route.params, + layouts: route.page.layouts.map((l) => + l !== undefined ? [metadata.nodes[l].has_server_load, l] : undefined + ), + errors: route.page.errors, + leaf: [metadata.nodes[route.page.leaf].has_server_load, route.page.leaf] + }; + }) + ); } + } else { + const start = deps_of(`${runtime_directory}/client/bundle.js`); + + build_data.client = { + start: start.file, + imports: start.imports, + stylesheets: start.stylesheets, + fonts: start.fonts, + uses_env_dynamic_public: client_chunks.some( + (chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] + ) + }; - /** @type {import('vite').Manifest} */ - const client_manifest = JSON.parse(read(`${out}/client/.vite/manifest.json`)); - - /** - * @param {string} entry - * @param {boolean} [add_dynamic_css] - */ - const deps_of = (entry, add_dynamic_css = false) => - find_deps(client_manifest, posixify(path.relative('.', entry)), add_dynamic_css); - - if (svelte_config.kit.output.bundleStrategy === 'split') { - const start = deps_of(`${runtime_directory}/client/entry.js`); - const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`); - - build_data.client = { - start: start.file, - app: app.file, - imports: [...start.imports, ...app.imports], - stylesheets: [...start.stylesheets, ...app.stylesheets], - fonts: [...start.fonts, ...app.fonts], - uses_env_dynamic_public: client_chunks.some( - (chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] + if (svelte_config.kit.output.bundleStrategy === 'inline') { + const style = /** @type {import('vite').Rolldown.OutputAsset} */ ( + client_chunks.find( + (chunk) => + chunk.type === 'asset' && chunk.names.length === 1 && chunk.names[0] === 'style.css' ) - }; + ); - // In case of server-side route resolution, we create a purpose-built route manifest that is - // similar to that on the client, with as much information computed upfront so that we - // don't need to include any code of the actual routes in the server bundle. - if (svelte_config.kit.router.resolution === 'server') { - const nodes = manifest_data.nodes.map((node, i) => { - if (node.component || node.universal) { - const entry = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; - const deps = deps_of(entry, true); - const file = resolve_symlinks( - client_manifest, - `${kit.outDir}/generated/client-optimized/nodes/${i}.js` - ).chunk.file; - - return { file, css: deps.stylesheets }; - } - }); - build_data.client.nodes = nodes.map((node) => node?.file); - build_data.client.css = nodes.map((node) => node?.css); - - build_data.client.routes = compact( - manifest_data.routes.map((route) => { - if (!route.page) return; - - return { - id: route.id, - pattern: route.pattern, - params: route.params, - layouts: route.page.layouts.map((l) => - l !== undefined ? [metadata.nodes[l].has_server_load, l] : undefined - ), - errors: route.page.errors, - leaf: [metadata.nodes[route.page.leaf].has_server_load, route.page.leaf] - }; - }) - ); - } - } else { - const start = deps_of(`${runtime_directory}/client/bundle.js`); - - build_data.client = { - start: start.file, - imports: start.imports, - stylesheets: start.stylesheets, - fonts: start.fonts, - uses_env_dynamic_public: client_chunks.some( - (chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] - ) + build_data.client.inline = { + script: read(`${out}/client/${start.file}`), + style: /** @type {string | undefined} */ (style?.source) }; + } + } - if (svelte_config.kit.output.bundleStrategy === 'inline') { - const style = /** @type {import('vite').Rollup.OutputAsset} */ ( - client_chunks.find( - (chunk) => - chunk.type === 'asset' && - chunk.names.length === 1 && - chunk.names[0] === 'style.css' - ) - ); + // regenerate manifest now that we have client entry... + fs.writeFileSync( + manifest_path, + `export const manifest = ${generate_manifest({ + build_data, + prerendered: [], + relative_path: '.', + routes: manifest_data.routes, + remotes, + root + })};\n` + ); + + // regenerate nodes with the client manifest... + build_server_nodes( + out, + kit, + manifest_data, + server_manifest, + client_manifest, + assets_path, + client_chunks, + svelte_config.kit.output, + root + ); + + // ...and prerender + const prerender_results = await prerender({ + hash: kit.router.type === 'hash', + out, + manifest_path, + metadata, + verbose, + env: { ...env.private, ...env.public }, + root + }); + prerendered = prerender_results.prerendered; + + await treeshake_prerendered_remotes( + vite, + out, + remotes, + metadata, + cwd, + server_bundle, + vite_config.build.sourcemap + ); + + // generate a new manifest that doesn't include prerendered pages + fs.writeFileSync( + `${out}/server/manifest.js`, + `export const manifest = ${generate_manifest({ + build_data, + prerendered: prerendered.paths, + relative_path: '.', + routes: manifest_data.routes.filter( + (route) => prerender_results.prerender_map.get(route.id) !== true + ), + remotes, + root + })};\n` + ); - build_data.client.inline = { - script: read(`${out}/client/${start.file}`), - style: /** @type {string | undefined} */ (style?.source) - }; - } + if (service_worker_entry_file) { + if (kit.paths.assets) { + throw new Error('Cannot use service worker alongside config.kit.paths.assets'); } - // regenerate manifest now that we have client entry... - fs.writeFileSync( - manifest_path, - `export const manifest = ${generate_manifest({ - build_data, - prerendered: [], - relative_path: '.', - routes: manifest_data.routes, - remotes - })};\n` - ); + log.info('Building service worker'); - // regenerate nodes with the client manifest... - build_server_nodes( - out, - kit, - manifest_data, - server_manifest, - client_manifest, - assets_path, - client_chunks, - svelte_config.kit.output - ); + builder.environments.serviceWorker.config.define = + builder.environments.client.config.define; + builder.environments.serviceWorker.config.resolve.alias = [ + ...get_config_aliases(kit, vite_config.root) + ]; + builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { + return { + runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` + }; + }; - // ...and prerender - const { prerendered, prerender_map } = await prerender({ - hash: kit.router.type === 'hash', - out, - manifest_path, - metadata, - verbose, - env: { ...env.private, ...env.public } - }); + await builder.build(builder.environments.serviceWorker); + } - await treeshake_prerendered_remotes( - out, - remotes, + console.log( + `\nRun ${styleText(['bold', 'cyan'], 'npm run preview')} to preview your production build locally.` + ); + + if (kit.adapter) { + const { adapt } = await import('../../core/adapt/index.js'); + await adapt( + svelte_config, + build_data, metadata, - cwd, - server_bundle, - vite_config.build.sourcemap + prerendered, + prerender_results.prerender_map, + log, + remotes, + vite_config ); + } else { + console.log(styleText(['bold', 'yellow'], '\nNo adapter specified')); - // generate a new manifest that doesn't include prerendered pages - fs.writeFileSync( - `${out}/server/manifest.js`, - `export const manifest = ${generate_manifest({ - build_data, - prerendered: prerendered.paths, - relative_path: '.', - routes: manifest_data.routes.filter((route) => prerender_map.get(route.id) !== true), - remotes - })};\n` + const link = styleText(['bold', 'cyan'], 'https://svelte.dev/docs/kit/adapters'); + console.log( + `See ${link} to learn how to configure your app to run on the platform of your choosing` ); - - if (service_worker_entry_file) { - if (kit.paths.assets) { - throw new Error('Cannot use service worker alongside config.kit.paths.assets'); - } - - log.info('Building service worker'); - - await build_service_worker( - out, - kit, - { - ...vite_config, - build: { - ...vite_config.build, - minify: initial_config.build?.minify ?? true - } - }, - manifest_data, - service_worker_entry_file, - prerendered, - client_manifest - ); - } - - // we need to defer this to closeBundle, so that adapters copy files - // created by other Vite plugins - finalise = async () => { - console.log( - `\nRun ${colors - .bold() - .cyan('npm run preview')} to preview your production build locally.` - ); - - if (kit.adapter) { - const { adapt } = await import('../../core/adapt/index.js'); - await adapt( - svelte_config, - build_data, - metadata, - prerendered, - prerender_map, - log, - remotes, - vite_config - ); - } else { - console.log(colors.bold().yellow('\nNo adapter specified')); - - const link = colors.bold().cyan('https://svelte.dev/docs/kit/adapters'); - console.log( - `See ${link} to learn how to configure your app to run on the platform of your choosing` - ); - } - - secondary_build_started = false; - }; - } - }, - - /** - * Runs the adapter. - */ - closeBundle: { - sequential: true, - async handler() { - if (!vite_config.build.ssr) return; - await finalise?.(); } } }; return [ plugin_setup, - kit.experimental.remoteFunctions && plugin_remote, + plugin_remote, plugin_virtual_modules, - plugin_guard, + process.env.TEST !== 'true' ? plugin_guard : undefined, + plugin_service_worker, plugin_compile ].filter((p) => !!p); } @@ -1429,8 +1660,10 @@ function warn_overridden_config(config, resolved_config) { if (overridden.length > 0) { console.error( - colors.bold().red('The following Vite config options will be overridden by SvelteKit:') + - overridden.map((key) => `\n - ${key}`).join('') + styleText( + ['bold', 'red'], + 'The following Vite config options will be overridden by SvelteKit:' + ) + overridden.map((key) => `\n - ${key}`).join('') ); } } diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 3b900845f9af..71b017c046b8 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -5,7 +5,6 @@ import { lookup } from 'mrmime'; import sirv from 'sirv'; import { loadEnv, normalizePath } from 'vite'; import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js'; -import { installPolyfills } from '../../../exports/node/polyfills.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { is_chrome_devtools_request, not_found } from '../utils.js'; @@ -19,8 +18,6 @@ import { is_chrome_devtools_request, not_found } from '../utils.js'; * @param {import('types').ValidatedConfig} svelte_config */ export async function preview(vite, vite_config, svelte_config) { - installPolyfills(); - const { paths } = svelte_config.kit; const base = paths.base; const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base; diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index 6d4248032da5..015a58c45b84 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -1,3 +1,4 @@ +import path from 'node:path'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import { Parser } from 'acorn'; import { read } from '../../../utils/filesystem.js'; @@ -213,11 +214,13 @@ function get_name(node) { /** * Reads and statically analyses a file for page options * @param {string} filepath + * @param {string} root The project root directory * @returns {PageOptions | null} Returns the page options for the file or `null` if unanalysable */ -export function get_page_options(filepath) { +export function get_page_options(filepath, root) { + const input = read(path.resolve(root, filepath)); + try { - const input = read(filepath); const page_options = statically_analyse_page_options(filepath, input); if (page_options === null) { return null; @@ -229,7 +232,10 @@ export function get_page_options(filepath) { } } -export function create_node_analyser() { +/** + * @param {string} root + */ +export function create_node_analyser(root) { const static_exports = new Map(); /** @@ -273,7 +279,7 @@ export function create_node_analyser() { } if (node.server) { - const server_page_options = get_page_options(node.server); + const server_page_options = get_page_options(node.server, root); if (server_page_options === null) { cache(key, null); return null; @@ -282,7 +288,7 @@ export function create_node_analyser() { } if (node.universal) { - const universal_page_options = get_page_options(node.universal); + const universal_page_options = get_page_options(node.universal, root); if (universal_page_options === null) { cache(key, null); return null; diff --git a/packages/kit/src/exports/vite/static_analysis/utils.spec.js b/packages/kit/src/exports/vite/static_analysis/utils.spec.js index 1b65daba559c..67ee5d2f8900 100644 --- a/packages/kit/src/exports/vite/static_analysis/utils.spec.js +++ b/packages/kit/src/exports/vite/static_analysis/utils.spec.js @@ -1,14 +1,7 @@ -import { expect, test, vi } from 'vitest'; +import { expect, test } from 'vitest'; import path from 'node:path'; import { should_ignore, has_children } from './utils.js'; -// Mock the colors module to avoid issues in tests -vi.mock('kleur', () => ({ - default: { - bold: () => ({ red: (/** @type {string} */ str) => str }) - } -})); - // We need to test the warning_preprocessor functionality // Since it's not exported, we'll recreate the relevant parts for testing const options_regex = /(export\s+const\s+(prerender|csr|ssr|trailingSlash))\s*=/s; diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index 8c6b86428463..46e5b48576f2 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -20,8 +20,9 @@ import { * Related to tsconfig path alias creation. * * @param {import('types').ValidatedKitConfig} config + * @param {string} root * */ -export function get_config_aliases(config) { +export function get_config_aliases(config, root) { /** @type {import('vite').Alias[]} */ const alias = [ // For now, we handle `$lib` specially here rather than make it a default value for @@ -38,16 +39,16 @@ export function get_config_aliases(config) { // Doing just `{ find: key.slice(0, -2) ,..}` would mean `import .. from "key"` would also be matched, which we don't want alias.push({ find: new RegExp(`^${escape_for_regexp(key.slice(0, -2))}\\/(.+)$`), - replacement: `${path.resolve(value)}/$1` + replacement: `${path.resolve(root, value)}/$1` }); } else if (key + '/*' in config.alias) { // key and key/* both exist -> the replacement for key needs to happen _only_ on import .. from "key" alias.push({ find: new RegExp(`^${escape_for_regexp(key)}$`), - replacement: path.resolve(value) + replacement: path.resolve(root, value) }); } else { - alias.push({ find: key, replacement: path.resolve(value) }); + alias.push({ find: key, replacement: path.resolve(root, value) }); } } diff --git a/packages/kit/src/exports/vite/utils.spec.js b/packages/kit/src/exports/vite/utils.spec.js index 90c9ee0c91e1..7878487573fe 100644 --- a/packages/kit/src/exports/vite/utils.spec.js +++ b/packages/kit/src/exports/vite/utils.spec.js @@ -18,7 +18,7 @@ test('transform kit.alias to resolve.alias', () => { } }); - const aliases = get_config_aliases(config.kit); + const aliases = get_config_aliases(config.kit, '.'); const transformed = aliases.map((entry) => { const replacement = posixify(path.relative('.', entry.replacement)); diff --git a/packages/kit/src/runtime/app/environment/index.js b/packages/kit/src/runtime/app/environment/index.js index 1729d5b4f72b..10fa912a6c56 100644 --- a/packages/kit/src/runtime/app/environment/index.js +++ b/packages/kit/src/runtime/app/environment/index.js @@ -1,2 +1,3 @@ export { BROWSER as browser, DEV as dev } from 'esm-env'; +// TODO: write these to disk export { building, version } from '__sveltekit/environment'; diff --git a/packages/kit/src/runtime/app/server/index.js b/packages/kit/src/runtime/app/server/index.js index 43b58e09815f..2cef0b763dc1 100644 --- a/packages/kit/src/runtime/app/server/index.js +++ b/packages/kit/src/runtime/app/server/index.js @@ -1,5 +1,5 @@ import { read_implementation, manifest } from '__sveltekit/server'; -import { base } from '$app/paths'; +import { assets } from '$app/paths/internal/server'; import { DEV } from 'esm-env'; import { base64_decode } from '../../utils.js'; @@ -55,7 +55,9 @@ export function read(asset) { } const file = decodeURIComponent( - DEV && asset.startsWith('/@fs') ? asset : asset.slice(base.length + 1) + DEV && asset.startsWith(assets + '/@fs') + ? asset.slice(assets.length) + : asset.slice(assets.length + 1) ); if (file in manifest._.server_assets) { diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 1e9c8f012872..b785bcb7a587 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -2,11 +2,9 @@ /** @import { InternalRemoteFormIssue, MaybePromise, RemoteFormInternals } from 'types' */ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { get_request_store } from '@sveltejs/kit/internal/server'; -import { DEV } from 'esm-env'; import { create_field_proxy, set_nested_value, - throw_on_old_property_access, deep_set, normalize_issue, flatten_issues @@ -90,32 +88,6 @@ export function form(validate_or_fn, maybe_fn) { name: '', id: '', fn: async (data, meta, form_data) => { - // TODO 3.0 remove this warning - if (DEV && !data) { - const error = () => { - throw new Error( - 'Remote form functions no longer get passed a FormData object. ' + - "`form` now has the same signature as `query` or `command`, i.e. it expects to be invoked like `form(schema, callback)` or `form('unchecked', callback)`. " + - 'The payload of the callback function is now a POJO instead of a FormData object. See https://kit.svelte.dev/docs/remote-functions#form for details.' - ); - }; - data = {}; - for (const key of [ - 'append', - 'delete', - 'entries', - 'forEach', - 'get', - 'getAll', - 'has', - 'keys', - 'set', - 'values' - ]) { - Object.defineProperty(data, key, { get: error }); - } - } - /** @type {{ submission: true, input?: Record, issues?: InternalRemoteFormIssue[], result: Output }} */ const output = {}; @@ -202,20 +174,6 @@ export function form(validate_or_fn, maybe_fn) { } }); - // TODO 3.0 remove - if (DEV) { - throw_on_old_property_access(instance); - - Object.defineProperty(instance, 'buttonProps', { - get() { - throw new Error( - '`form.buttonProps` has been removed: Instead of `