From ecb423b54d459be8c0693c723799be4171369afd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 29 Dec 2021 04:21:25 -0500 Subject: [PATCH] Overhaul adapter API (#2931) * start fiddling about with code-split server code * generate build data * working, minus adapters * prerendering * WIP * tidy some stuff up * fix prefixes * adapter-node working * tidy up * tidy * make copy opts optional * tidy * netlify working albeit with single function * expose route data to adapters * some progress * lint * remove sandbox example from PR * implement createEntries API * fix some tests * AMP fix * tests all passing * update gitignore * disable adapter-node tests for now * typechecking * always create prerendered output directory * manifest -> generateManifest, add utils-level method * spread -> rest * finesse return value of copy helper * no need to pass config through to adapter * Update packages/kit/src/core/build/index.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * fix * refactor out method extraction * add comment to resolve separate routes/components entry point detection * remove out of date comment * nicer rollup output types * actually that caused chaos, doing this instead * fix tests * update adapter-cloudflare * refactor build functions into client/server/service worker modules * make build_server more readable * oops * gah * windows fix * split manifest into public and private parts, so we can use manifest.assets in adapters * lint * prevent duplication of asset list in functions * exclude prerendered pages from functions * lint * get vercel adapter working (with v1 filesystem API) * target node14 * rename utils -> builder * add methods for getting directories instead of hardcoding .svelte-kit etc * fix lockfile * update cloudflare adapters * update readmes * tidy up * tweak docs * remove sandbox from workspace * try pinning version * Update packages/kit/src/core/build/build_server.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * remove redundant strict fs stuff * remove sandbox from gitignore * lint * fix cloudflare pages handling of prerendered pages * support esm and cjs manifests * force createEntries to follow prerender * fix types * fix adapter-netlify * separate App from InternalApp * Update packages/kit/types/internal.d.ts Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * expose SSRManifest * fall back to HTTP fetch if endpoint is missing * app.render should not be passed a host string * default hostHeader to host in config * fix types * replace page.host with page.origin * fix host stuff * make SSR route splitting optional * simplify createEntries * these are no longer generic * implement .json heuristic * lint * changesets * changeset * Update packages/adapter-netlify/index.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * Update packages/adapter-node/README.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * this.server -> this.vite * give a bare-bones description of createEntries * lint * add missing origin * remove TODO * replace ts-ignore with ts-expect-error * Update packages/kit/src/core/adapt/builder.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * document AdapterEntry * Update packages/kit/types/config.d.ts Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * oops * manifest -> vite_manifest Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/clean-camels-pump.md | 12 + .changeset/early-snakes-peel.md | 7 + .changeset/fresh-years-do.md | 5 + .changeset/fuzzy-forks-worry.md | 5 + .changeset/smart-deers-approve.md | 5 + .changeset/tiny-singers-allow.md | 6 + .github/workflows/ci.yml | 6 +- .github/workflows/release.yml | 2 +- documentation/docs/01-routing.md | 4 +- documentation/docs/03-loading.md | 6 +- documentation/docs/04-hooks.md | 2 +- documentation/docs/05-modules.md | 10 +- documentation/docs/10-adapters.md | 12 +- documentation/docs/14-configuration.md | 27 +- packages/adapter-auto/index.js | 10 +- packages/adapter-cloudflare-workers/README.md | 33 - .../adapter-cloudflare-workers/files/entry.js | 9 +- .../adapter-cloudflare-workers/index.d.ts | 7 +- packages/adapter-cloudflare-workers/index.js | 83 +-- packages/adapter-cloudflare/README.md | 30 +- packages/adapter-cloudflare/files/worker.js | 43 +- packages/adapter-cloudflare/index.d.ts | 4 +- packages/adapter-cloudflare/index.js | 53 +- packages/adapter-netlify/.gitignore | 3 +- packages/adapter-netlify/README.md | 44 +- packages/adapter-netlify/files/entry.js | 71 --- packages/adapter-netlify/files/shims.js | 1 - packages/adapter-netlify/index.d.ts | 7 +- packages/adapter-netlify/index.js | 219 +++++-- packages/adapter-netlify/package.json | 15 +- packages/adapter-netlify/rollup.config.js | 23 + packages/adapter-netlify/src/handler.js | 79 +++ packages/adapter-node/README.md | 77 +-- packages/adapter-node/index.js | 117 +--- packages/adapter-node/package.json | 8 +- packages/adapter-node/rollup.config.js | 28 +- packages/adapter-node/src/handler.js | 95 +++ packages/adapter-node/src/index.js | 18 +- packages/adapter-node/src/kit-middleware.js | 46 -- packages/adapter-node/src/middlewares.js | 48 -- packages/adapter-node/src/shims.js | 9 - packages/adapter-node/src/types.d.ts | 7 + packages/adapter-node/tests/smoke.js | 2 +- packages/adapter-static/index.js | 18 +- packages/adapter-vercel/README.md | 31 - packages/adapter-vercel/files/entry.js | 10 +- packages/adapter-vercel/files/routes.json | 9 - packages/adapter-vercel/files/shims.js | 1 - packages/adapter-vercel/index.d.ts | 7 +- packages/adapter-vercel/index.js | 98 +-- packages/adapter-vercel/tsconfig.json | 2 +- .../templates/default/package.json | 1 + packages/kit/.gitignore | 2 +- packages/kit/src/core/adapt/builder.js | 171 +++++ packages/kit/src/core/adapt/index.js | 6 +- packages/kit/src/core/adapt/prerender.js | 43 +- .../.svelte-kit/output/server/app.js | 21 +- .../.svelte-kit/output/server/manifest.js | 1 + packages/kit/src/core/adapt/test/index.js | 34 +- packages/kit/src/core/adapt/utils.js | 45 -- packages/kit/src/core/build/build_client.js | 120 ++++ packages/kit/src/core/build/build_server.js | 287 +++++++++ .../src/core/build/build_service_worker.js | 88 +++ packages/kit/src/core/build/index.js | 592 ++---------------- packages/kit/src/core/build/utils.js | 38 ++ packages/kit/src/core/config/index.js | 2 - packages/kit/src/core/config/index.spec.js | 12 +- packages/kit/src/core/config/options.js | 10 +- packages/kit/src/core/config/test/index.js | 6 +- packages/kit/src/core/config/types.d.ts | 11 - packages/kit/src/core/create_app/index.js | 3 +- .../src/core/create_manifest_data/index.js | 31 +- .../core/create_manifest_data/index.spec.js | 39 ++ packages/kit/src/core/dev/index.js | 209 +++---- .../kit/src/core/generate_manifest/index.js | 107 ++++ packages/kit/src/core/preview/index.js | 15 +- packages/kit/src/core/utils.js | 15 + packages/kit/src/runtime/client/renderer.js | 12 +- packages/kit/src/runtime/client/start.js | 15 +- packages/kit/src/runtime/server/endpoint.js | 4 +- packages/kit/src/runtime/server/index.js | 24 +- packages/kit/src/runtime/server/page/index.js | 5 +- .../kit/src/runtime/server/page/load_node.js | 48 +- .../kit/src/runtime/server/page/render.js | 19 +- .../kit/src/runtime/server/page/respond.js | 6 +- .../runtime/server/page/respond_with_error.js | 6 +- packages/kit/src/runtime/server/utils.js | 22 + packages/kit/src/utils/filesystem.js | 66 +- packages/kit/src/utils/filesystem.spec.js | 82 ++- packages/kit/src/utils/misc.js | 1 + .../amp/src/routes/{host => origin}/_tests.js | 12 +- .../src/routes/{host => origin}/index.json.js | 4 +- .../src/routes/origin}/index.svelte | 12 +- .../apps/basics/src/routes/host/_tests.js | 16 - .../apps/basics/src/routes/origin/_tests.js | 19 + .../src/routes/{host => origin}/index.svelte | 2 +- .../kit/test/apps/basics/svelte.config.js | 5 +- .../apps/options/source/pages/host/_tests.js | 10 - .../options/source/pages/origin/_tests.js | 12 + .../pages/{host => origin}/index.json.js | 4 +- .../source/pages/origin}/index.svelte | 12 +- .../kit/test/apps/options/svelte.config.js | 3 +- packages/kit/types/ambient-modules.d.ts | 4 +- packages/kit/types/app.d.ts | 31 +- packages/kit/types/config.d.ts | 87 ++- packages/kit/types/hooks.d.ts | 10 + packages/kit/types/index.d.ts | 4 +- packages/kit/types/internal.d.ts | 68 +- packages/kit/types/page.d.ts | 2 +- pnpm-lock.yaml | 96 ++- 110 files changed, 2345 insertions(+), 1761 deletions(-) create mode 100644 .changeset/clean-camels-pump.md create mode 100644 .changeset/early-snakes-peel.md create mode 100644 .changeset/fresh-years-do.md create mode 100644 .changeset/fuzzy-forks-worry.md create mode 100644 .changeset/smart-deers-approve.md create mode 100644 .changeset/tiny-singers-allow.md delete mode 100644 packages/adapter-netlify/files/entry.js delete mode 100644 packages/adapter-netlify/files/shims.js create mode 100644 packages/adapter-netlify/rollup.config.js create mode 100644 packages/adapter-netlify/src/handler.js create mode 100644 packages/adapter-node/src/handler.js delete mode 100644 packages/adapter-node/src/kit-middleware.js delete mode 100644 packages/adapter-node/src/middlewares.js delete mode 100644 packages/adapter-node/src/shims.js create mode 100644 packages/adapter-node/src/types.d.ts delete mode 100644 packages/adapter-vercel/files/routes.json delete mode 100644 packages/adapter-vercel/files/shims.js create mode 100644 packages/kit/src/core/adapt/builder.js create mode 100644 packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/manifest.js delete mode 100644 packages/kit/src/core/adapt/utils.js create mode 100644 packages/kit/src/core/build/build_client.js create mode 100644 packages/kit/src/core/build/build_server.js create mode 100644 packages/kit/src/core/build/build_service_worker.js create mode 100644 packages/kit/src/core/build/utils.js create mode 100644 packages/kit/src/core/generate_manifest/index.js create mode 100644 packages/kit/src/utils/misc.js rename packages/kit/test/apps/amp/src/routes/{host => origin}/_tests.js (52%) rename packages/kit/test/apps/amp/src/routes/{host => origin}/index.json.js (56%) rename packages/kit/test/apps/{options/source/pages/host => amp/src/routes/origin}/index.svelte (61%) delete mode 100644 packages/kit/test/apps/basics/src/routes/host/_tests.js create mode 100644 packages/kit/test/apps/basics/src/routes/origin/_tests.js rename packages/kit/test/apps/basics/src/routes/{host => origin}/index.svelte (70%) delete mode 100644 packages/kit/test/apps/options/source/pages/host/_tests.js create mode 100644 packages/kit/test/apps/options/source/pages/origin/_tests.js rename packages/kit/test/apps/options/source/pages/{host => origin}/index.json.js (56%) rename packages/kit/test/apps/{amp/src/routes/host => options/source/pages/origin}/index.svelte (61%) diff --git a/.changeset/clean-camels-pump.md b/.changeset/clean-camels-pump.md new file mode 100644 index 000000000000..f8f2b74a24d3 --- /dev/null +++ b/.changeset/clean-camels-pump.md @@ -0,0 +1,12 @@ +--- +'@sveltejs/adapter-auto': patch +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-static': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +Overhaul adapter API diff --git a/.changeset/early-snakes-peel.md b/.changeset/early-snakes-peel.md new file mode 100644 index 000000000000..1296ef6d688b --- /dev/null +++ b/.changeset/early-snakes-peel.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-vercel': patch +--- + +Remove esbuild options diff --git a/.changeset/fresh-years-do.md b/.changeset/fresh-years-do.md new file mode 100644 index 000000000000..d3d8f5dd9e0b --- /dev/null +++ b/.changeset/fresh-years-do.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Replace config.kit.hostHeader with config.kit.headers.host, add config.kit.headers.protocol diff --git a/.changeset/fuzzy-forks-worry.md b/.changeset/fuzzy-forks-worry.md new file mode 100644 index 000000000000..6418da2a94b4 --- /dev/null +++ b/.changeset/fuzzy-forks-worry.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Replace page.host with page.origin diff --git a/.changeset/smart-deers-approve.md b/.changeset/smart-deers-approve.md new file mode 100644 index 000000000000..1fa96d9ad4b0 --- /dev/null +++ b/.changeset/smart-deers-approve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +Add experimental function splitting diff --git a/.changeset/tiny-singers-allow.md b/.changeset/tiny-singers-allow.md new file mode 100644 index 000000000000..8284fe0291b8 --- /dev/null +++ b/.changeset/tiny-singers-allow.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +--- + +Don't bundle final output diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29b7e53a00b1..f24c23a0fc63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2.0.1 with: - version: 6 + version: 6.23.2 - uses: actions/setup-node@v2 with: node-version: '14.x' @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2.0.1 with: - version: 6 + version: 6.23.2 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} @@ -75,7 +75,7 @@ jobs: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2.0.1 with: - version: 6 + version: 6.23.2 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54ab3b856e29..03bcc02557fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - uses: pnpm/action-setup@v2.0.1 with: - version: 6 + version: 6.23.2 - name: Setup Node.js 12.x uses: actions/setup-node@v2 with: diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md index 45967672be89..eadcdac009b3 100644 --- a/documentation/docs/01-routing.md +++ b/documentation/docs/01-routing.md @@ -59,7 +59,6 @@ type RequestHeaders = Record; export type RawBody = null | Uint8Array; export interface IncomingRequest { method: string; - host: string; path: string; query: URLSearchParams; headers: RequestHeaders; @@ -72,6 +71,7 @@ type ParameterizedBody = Body extends FormData // ServerRequest is exported as Request export interface ServerRequest, Body = unknown> extends IncomingRequest { + origin: string; params: Record; body: ParameterizedBody; locals: Locals; // populated by hooks handle @@ -96,7 +96,7 @@ export interface RequestHandler< } ``` - For example, our hypothetical blog page, `/blog/cool-article`, might request data from `/blog/cool-article.json`, which could be represented by a `src/routes/blog/[slug].json.js` endpoint: +For example, our hypothetical blog page, `/blog/cool-article`, might request data from `/blog/cool-article.json`, which could be represented by a `src/routes/blog/[slug].json.js` endpoint: ```js import db from '$lib/database'; diff --git a/documentation/docs/03-loading.md b/documentation/docs/03-loading.md index 148f90338183..348b50a5f48e 100644 --- a/documentation/docs/03-loading.md +++ b/documentation/docs/03-loading.md @@ -14,7 +14,7 @@ export interface LoadInput< Session = any > { page: { - host: string; + origin: string; path: string; params: PageParams; query: URLSearchParams; @@ -92,11 +92,11 @@ The `load` function receives an object containing four fields — `page`, `fetch #### page -`page` is a `{ host, path, params, query }` object where `host` is the URL's host, `path` is its pathname, `params` is derived from `path` and the route filename, and `query` is an instance of [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). Mutating `page` does not update the current URL; you should instead navigate using [`goto`](#modules-$app-navigation). +`page` is an `{ origin, path, params, query }` object where `origin` is the URL's origin, `path` is its pathname, `params` is derived from `path` and the route filename, and `query` is an instance of [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). Mutating `page` does not update the current URL; you should instead navigate using [`goto`](#modules-$app-navigation). So if the example above was `src/routes/blog/[slug].svelte` and the URL was `https://example.com/blog/some-post?foo=bar&baz&bizz=a&bizz=b`, the following would be true: -- `page.host === 'example.com'` +- `page.origin === 'https://example.com'` - `page.path === '/blog/some-post'` - `page.params.slug === 'some-post'` - `page.query.get('foo') === 'bar'` diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index dafd7093d80b..dcb776969cc7 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -26,7 +26,6 @@ type RequestHeaders = Record; export type RawBody = null | Uint8Array; export interface IncomingRequest { method: string; - host: string; path: string; query: URLSearchParams; headers: RequestHeaders; @@ -39,6 +38,7 @@ type ParameterizedBody = Body extends FormData // ServerRequest is exported as Request export interface ServerRequest, Body = unknown> extends IncomingRequest { + origin: string; params: Record; body: ParameterizedBody; locals: Locals; // populated by hooks handle diff --git a/documentation/docs/05-modules.md b/documentation/docs/05-modules.md index b8a1784ae1a3..9a085d046507 100644 --- a/documentation/docs/05-modules.md +++ b/documentation/docs/05-modules.md @@ -57,7 +57,7 @@ Because of that, the stores are not free-floating objects: they must be accessed The stores themselves attach to the correct context at the point of subscription, which means you can import and use them directly in components without boilerplate. However, it still needs to be called synchronously on component or page initialisation when `$`-prefix isn't used. Use `getStores` to safely `.subscribe` asynchronously instead. - `navigating` is a [readable store](https://svelte.dev/tutorial/readable-stores). When navigating starts, its value is `{ from, to }`, where `from` and `to` both mirror the `page` store value. When navigating finishes, its value reverts to `null`. -- `page` is a readable store whose value reflects the object passed to `load` functions — it contains `host`, `path`, `params` and `query`. See the [`page` section](#loading-input-page) above for more details. +- `page` is a readable store whose value reflects the object passed to `load` functions — it contains `origin`, `path`, `params` and `query`. See the [`page` section](#loading-input-page) above for more details. - `session` is a [writable store](https://svelte.dev/tutorial/writable-stores) whose initial value is whatever was returned from [`getSession`](#hooks-getsession). It can be written to, but this will _not_ cause changes to persist on the server — this is something you must implement yourself. ### $lib @@ -84,12 +84,12 @@ This module provides a helper function to sequence multiple `handle` calls. import { sequence } from '@sveltejs/kit/hooks'; async function first({ request, resolve }) { - console.log('first'); - return await resolve(request); + console.log('first'); + return await resolve(request); } async function second({ request, resolve }) { - console.log('second'); - return await resolve(request); + console.log('second'); + return await resolve(request); } export const handle = sequence(first, second); diff --git a/documentation/docs/10-adapters.md b/documentation/docs/10-adapters.md index 27212513f0e5..7a4fd2c91b21 100644 --- a/documentation/docs/10-adapters.md +++ b/documentation/docs/10-adapters.md @@ -76,7 +76,7 @@ export default function (options) { /** @type {import('@sveltejs/kit').Adapter} */ return { name: 'adapter-package-name', - async adapt({ utils, config }) { + async adapt(builder) { // adapter implementation } }; @@ -88,15 +88,15 @@ The types for `Adapter` and its parameters are available in [types/config.d.ts]( Within the `adapt` method, there are a number of things that an adapter should do: - Clear out the build directory +- Call `builder.prerender({ dest })` to prerender pages - Output code that: - - Imports `init` and `render` from `.svelte-kit/output/server/app.js` - - Calls `init`, which configures the app + - Imports `App` from `${builder.getServerDirectory()}/app.js` + - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` - Listens for requests from the platform, converts them to a a [SvelteKit request](#hooks-handle), calls the `render` function to generate a [SvelteKit response](#hooks-handle) and responds with it - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/install-fetch` helper for platforms that can use `node-fetch` -- Bundle the output to avoid needing to install dependencies on the target platform, if desired -- Call `utils.prerender` +- 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 -If possible, we recommend putting the adapter output under the `build/` directory with any intermediate output placed under `.svelte-kit/[adapter-name]`. +Where possible, we recommend putting the adapter output under the `build/` directory with any intermediate output placed under `.svelte-kit/[adapter-name]`. > The adapter API may change before 1.0. diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md index aaa07b6a5694..c9707584f4b4 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/14-configuration.md @@ -26,8 +26,11 @@ const config = { template: 'src/app.html' }, floc: false, + headers: { + host: null, + protocol: null + }, host: null, - hostHeader: null, hydrate: true, package: { dir: 'package', @@ -47,6 +50,7 @@ const config = { entries: ['*'], onError: 'fail' }, + protocol: null, router: true, serviceWorker: { register: true, @@ -103,25 +107,30 @@ Permissions-Policy: interest-cohort=() > This only applies to server-rendered responses — headers for prerendered pages (e.g. created with [adapter-static](/~https://github.com/sveltejs/kit/tree/master/packages/adapter-static)) are determined by the hosting platform. -### host - -A value that overrides the `Host` header when populating `page.host` +### headers -### hostHeader +The [`page.origin`] property is derived from the request protocol (normally `https`) and the host, which is taken from the `Host` header by default. -If your app is behind a reverse proxy (think load balancers and CDNs) then the `Host` header will be incorrect. In most cases, the underlying host is exposed via the [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) header and you should specify this in your config if you need to access `page.host`: +If your app is behind a reverse proxy (think load balancers and CDNs) then the `Host` header will be incorrect. In most cases, the underlying protocol and host are exposed via the [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) and [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) headers, which can be specified in your config: ```js // svelte.config.js export default { kit: { - hostHeader: 'X-Forwarded-Host' + headers: { + host: 'X-Forwarded-Host', + protocol: 'X-Forwarded-Proto' + } } }; ``` **You should only do this if you trust the reverse proxy**, which is why it isn't the default. +### host + +A value that overrides the one derived from [`config.kit.headers.host`](#configuration-headers-host). + ### hydrate Whether to [hydrate](#ssr-and-javascript-hydrate) the server-rendered HTML with a client-side app. (It's rare that you would set this to `false` on an app-wide basis.) @@ -194,6 +203,10 @@ See [Prerendering](#ssr-and-javascript-prerender). An object containing zero or }; ``` +### protocol + +The protocol is assumed to be `'https'` (unless you're developing locally without the `--https` flag) unless [`config.kit.headers.protocol`](#configuration-headers-protocol) is set. If necessary, you can override it here. + ### router Enables or disables the client-side [router](#ssr-and-javascript-router) app-wide. diff --git a/packages/adapter-auto/index.js b/packages/adapter-auto/index.js index 62b4072b6b8e..8b07df0d9169 100644 --- a/packages/adapter-auto/index.js +++ b/packages/adapter-auto/index.js @@ -5,12 +5,10 @@ export default function () { return { name: '@sveltejs/adapter-auto', - async adapt(options) { + async adapt(builder) { for (const candidate of adapters) { if (candidate.test()) { - options.utils.log.info( - `Detected environment: ${candidate.name}. Using ${candidate.module}` - ); + builder.log.info(`Detected environment: ${candidate.name}. Using ${candidate.module}`); let module; @@ -30,11 +28,11 @@ export default function () { } const adapter = module.default(); - return adapter.adapt(options); + return adapter.adapt(builder); } } - options.utils.log.warn( + builder.log.warn( 'Could not detect a supported production environment. See https://kit.svelte.dev/docs#adapters to learn how to configure your app to run on the platform of your choosing' ); } diff --git a/packages/adapter-cloudflare-workers/README.md b/packages/adapter-cloudflare-workers/README.md index 33de7d4e54b2..50511e89a745 100644 --- a/packages/adapter-cloudflare-workers/README.md +++ b/packages/adapter-cloudflare-workers/README.md @@ -2,8 +2,6 @@ SvelteKit adapter that creates a Cloudflare Workers site using a function for dynamic server rendering. -This is very experimental; the adapter API isn't at all fleshed out, and things will definitely change. - _**Comparisons**_ - `adapter-cloudflare` – supports all SvelteKit features; builds for @@ -86,37 +84,6 @@ npm run build && wrangler publish More info on configuring a cloudflare worker site can be found [here](https://developers.cloudflare.com/workers/platform/sites/start-from-existing) -## Advanced Configuration - -### esbuild - -As an escape hatch, you may optionally specify a function which will receive the final esbuild options generated by this adapter and returns a modified esbuild configuration. The result of this function will be passed as-is to esbuild. The function can be async. - -For example, you may wish to add a plugin: - -```js -adapterCfw({ - esbuild(options) { - return { - ...options, - plugins: [] - }; - } -}); -``` - -The default options for this version are as follows: - -```js -const options = { - entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], - outfile: `${entrypoint}/index.js`, - bundle: true, - target: 'es2020', - platform: 'browser' -}; -``` - ## Changelog [The Changelog for this package is available on GitHub](/~https://github.com/sveltejs/kit/blob/master/packages/adapter-cloudflare-workers/CHANGELOG.md). diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index f7a7e397a620..0422ab397828 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,8 +1,8 @@ -// TODO hardcoding the relative location makes this brittle -import { init, render } from '../output/server/app.js'; +import { App } from 'APP'; +import { manifest } from './manifest.js'; import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler'; -init(); +const app = new App(manifest); addEventListener('fetch', (event) => { event.respondWith(handle(event)); @@ -29,8 +29,7 @@ async function handle(event) { const request_url = new URL(request.url); try { - const rendered = await render({ - host: request_url.host, + const rendered = await app.render({ path: request_url.pathname, query: request_url.searchParams, rawBody: await read(request), diff --git a/packages/adapter-cloudflare-workers/index.d.ts b/packages/adapter-cloudflare-workers/index.d.ts index 7f3175d6e358..62aee0f1eaa9 100644 --- a/packages/adapter-cloudflare-workers/index.d.ts +++ b/packages/adapter-cloudflare-workers/index.d.ts @@ -1,9 +1,4 @@ import { Adapter } from '@sveltejs/kit'; -import { BuildOptions } from 'esbuild'; -interface AdapterOptions { - esbuild?: (options: BuildOptions) => Promise | BuildOptions; -} - -declare function plugin(options?: AdapterOptions): Adapter; +declare function plugin(): Adapter; export = plugin; diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 91909812030d..b58907fc979c 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -1,73 +1,78 @@ -import fs from 'fs'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { relative } from 'path'; import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; import { fileURLToPath } from 'url'; -/** - * @typedef {import('esbuild').BuildOptions} BuildOptions - */ - /** @type {import('.')} */ -export default function (options) { +export default function () { return { name: '@sveltejs/adapter-cloudflare-workers', - async adapt({ utils }) { - const { site } = validate_config(utils); + async adapt(builder) { + const { site } = validate_config(builder); const bucket = site.bucket; const entrypoint = site['entry-point'] || 'workers-site'; const files = fileURLToPath(new URL('./files', import.meta.url)); + const tmp = builder.getBuildDirectory('cloudflare-workers-tmp'); + + builder.rimraf(bucket); + builder.rimraf(entrypoint); - utils.rimraf(bucket); - utils.rimraf(entrypoint); + builder.log.info('Prerendering static pages...'); + await builder.prerender({ + dest: bucket + }); - utils.log.info('Installing worker dependencies...'); - utils.copy(`${files}/_package.json`, '.svelte-kit/cloudflare-workers/package.json'); + builder.log.info('Installing worker dependencies...'); + builder.copy(`${files}/_package.json`, `${tmp}/package.json`); // TODO would be cool if we could make this step unnecessary somehow - const stdout = execSync('npm install', { cwd: '.svelte-kit/cloudflare-workers' }); - utils.log.info(stdout.toString()); + const stdout = execSync('npm install', { cwd: tmp }); + builder.log.info(stdout.toString()); + + builder.log.minor('Generating worker...'); + const relativePath = relative(tmp, builder.getServerDirectory()); + + builder.copy(`${files}/entry.js`, `${tmp}/entry.js`, { + replace: { + APP: `${relativePath}/app.js` + } + }); - utils.log.minor('Generating worker...'); - utils.copy(`${files}/entry.js`, '.svelte-kit/cloudflare-workers/entry.js'); + writeFileSync( + `${tmp}/manifest.js`, + `export const manifest = ${builder.generateManifest({ + relativePath + })};\n` + ); - /** @type {BuildOptions} */ - const default_options = { - entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], + await esbuild.build({ + entryPoints: [`${tmp}/entry.js`], outfile: `${entrypoint}/index.js`, bundle: true, target: 'es2020', platform: 'browser' - }; - - const build_options = - options && options.esbuild ? await options.esbuild(default_options) : default_options; - - await esbuild.build(build_options); - - fs.writeFileSync(`${entrypoint}/package.json`, JSON.stringify({ main: 'index.js' })); - - utils.log.info('Prerendering static pages...'); - await utils.prerender({ - dest: bucket }); - utils.log.minor('Copying assets...'); - utils.copy_static_files(bucket); - utils.copy_client_files(bucket); + writeFileSync(`${entrypoint}/package.json`, JSON.stringify({ main: 'index.js' })); + + builder.log.minor('Copying assets...'); + builder.writeClient(bucket); + builder.writeStatic(bucket); } }; } -function validate_config(utils) { - if (fs.existsSync('wrangler.toml')) { +function validate_config(builder) { + if (existsSync('wrangler.toml')) { let wrangler_config; try { - wrangler_config = toml.parse(fs.readFileSync('wrangler.toml', 'utf-8')); + wrangler_config = toml.parse(readFileSync('wrangler.toml', 'utf-8')); } catch (err) { err.message = `Error parsing wrangler.toml: ${err.message}`; throw err; @@ -82,11 +87,11 @@ function validate_config(utils) { return wrangler_config; } - utils.log.error( + builder.log.error( 'Consult https://developers.cloudflare.com/workers/platform/sites/configuration on how to setup your site' ); - utils.log( + builder.log( ` Sample wrangler.toml: diff --git a/packages/adapter-cloudflare/README.md b/packages/adapter-cloudflare/README.md index 19e627ed7b6f..1e7fe8112a9c 100644 --- a/packages/adapter-cloudflare/README.md +++ b/packages/adapter-cloudflare/README.md @@ -1,7 +1,6 @@ # adapter-cloudflare -[Adapter](https://kit.svelte.dev/docs#adapters) for building SvelteKit -applications on Cloudflare Pages with Workers integration. +[Adapter](https://kit.svelte.dev/docs#adapters) for building SvelteKit applications on Cloudflare Pages with Workers integration. _**Comparisons**_ @@ -27,36 +26,16 @@ $ npm i --save-dev @sveltejs/adapter-cloudflare@next You can include these changes in your `svelte.config.js` configuration file: ```js -import cloudflare from '@sveltejs/adapter-cloudflare'; +import adapter from '@sveltejs/adapter-cloudflare'; export default { kit: { target: '#svelte', - adapter: cloudflare({ - // any esbuild options - }) + adapter: adapter() } }; ``` -### Options - -The adapter optionally accepts all -[`esbuild.build`](https://esbuild.github.io/api/#build-api) configuration. - -These are the default options, of which, all but `target` and `platform` are -enforced: - -```js -target: 'es2020', -platform: 'browser', -entryPoints: '< input >', -outfile: '/_worker.js', -allowOverwrite: true, -format: 'esm', -bundle: true, -``` - ## Deployment Please follow the [Get Started Guide](https://developers.cloudflare.com/pages/get-started) for Cloudflare Pages to begin. @@ -73,5 +52,4 @@ When configuring your project settings, you must use the following settings: ## Changelog -[The Changelog for this package is available on -GitHub](/~https://github.com/sveltejs/kit/blob/master/packages/adapter-cloudflare/CHANGELOG.md). +[The Changelog for this package is available on GitHub](/~https://github.com/sveltejs/kit/blob/master/packages/adapter-cloudflare/CHANGELOG.md). diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index df9fea234039..4d5296022925 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -1,30 +1,41 @@ -/* global ASSETS */ -import { init, render } from '../output/server/app.js'; +import { App } from '../output/server/app.js'; +import { manifest, prerendered } from './manifest.js'; -init(); +const app = new App(manifest); + +const prefix = `/${manifest.appDir}/`; export default { async fetch(req, env) { const url = new URL(req.url); - // check generated asset_set for static files - let pathname = url.pathname.substring(1); + // static assets + if (url.pathname.startsWith(prefix)) return env.ASSETS.fetch(req); + + // prerendered pages and index.html files + const pathname = url.pathname.replace(/\/$/, ''); + let file = pathname.substring(1); + try { - pathname = decodeURIComponent(pathname); + file = decodeURIComponent(file); } catch (err) { // ignore } - if (ASSETS.has(pathname)) { + if ( + manifest.assets.has(file) || + manifest.assets.has(file + '/index.html') || + prerendered.has(pathname || '/') + ) { return env.ASSETS.fetch(req); } + // dynamically-generated pages try { - const rendered = await render({ - host: url.host || '', - path: url.pathname || '', - query: url.searchParams || '', - rawBody: await read(req), + const rendered = await app.render({ + path: url.pathname, + query: url.searchParams, + rawBody: new Uint8Array(await req.arrayBuffer()), headers: Object.fromEntries(req.headers), method: req.method }); @@ -32,7 +43,7 @@ export default { if (rendered) { return new Response(rendered.body, { status: rendered.status, - headers: makeHeaders(rendered.headers) + headers: make_headers(rendered.headers) }); } } catch (e) { @@ -46,11 +57,7 @@ export default { } }; -async function read(request) { - return new Uint8Array(await request.arrayBuffer()); -} - -function makeHeaders(headers) { +function make_headers(headers) { const result = new Headers(); for (const header in headers) { const value = headers[header]; diff --git a/packages/adapter-cloudflare/index.d.ts b/packages/adapter-cloudflare/index.d.ts index 512610df3813..62aee0f1eaa9 100644 --- a/packages/adapter-cloudflare/index.d.ts +++ b/packages/adapter-cloudflare/index.d.ts @@ -1,4 +1,4 @@ import { Adapter } from '@sveltejs/kit'; -import { BuildOptions } from 'esbuild'; -export default function (options?: BuildOptions): Adapter; +declare function plugin(): Adapter; +export = plugin; diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index c0fa58184f96..c1be491f5972 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -1,48 +1,47 @@ -import { join } from 'path'; +import { writeFileSync } from 'fs'; +import { relative } from 'path'; import { fileURLToPath } from 'url'; -import { readFileSync, writeFileSync } from 'fs'; import * as esbuild from 'esbuild'; -/** - * @param {esbuild.BuildOptions} [options] - */ +/** @type {import('.')} */ export default function (options = {}) { return { name: '@sveltejs/adapter-cloudflare', - async adapt({ utils, config }) { + async adapt(builder) { const files = fileURLToPath(new URL('./files', import.meta.url)); - const target_dir = join(process.cwd(), '.svelte-kit', 'cloudflare'); - utils.rimraf(target_dir); + const dest = builder.getBuildDirectory('cloudflare'); + const tmp = builder.getBuildDirectory('cloudflare-tmp'); - const static_files = utils - .copy(config.kit.files.assets, target_dir) - .map((f) => f.replace(`${target_dir}/`, '')); + builder.rimraf(dest); + builder.rimraf(tmp); + builder.mkdirp(tmp); - const client_files = utils - .copy(`${process.cwd()}/.svelte-kit/output/client`, target_dir) - .map((f) => f.replace(`${target_dir}/`, '')); + builder.writeStatic(dest); + builder.writeClient(dest); - // returns nothing, very sad - // TODO(future) get/save output - await utils.prerender({ - dest: `${target_dir}/` - }); - - const static_assets = [...static_files, ...client_files]; - const assets = `const ASSETS = new Set(${JSON.stringify(static_assets)});\n`; + const { paths } = await builder.prerender({ dest }); - const worker = readFileSync(join(files, 'worker.js'), { encoding: 'utf-8' }); + const relativePath = relative(tmp, builder.getServerDirectory()); - const target_worker = join(target_dir, '_worker.js'); + writeFileSync( + `${tmp}/manifest.js`, + `export const manifest = ${builder.generateManifest({ + relativePath + })};\n\nexport const prerendered = new Set(${JSON.stringify(paths)});\n` + ); - writeFileSync(target_worker, assets + worker); + builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, { + replace: { + APP: `${relativePath}/app.js` + } + }); await esbuild.build({ target: 'es2020', platform: 'browser', ...options, - entryPoints: [target_worker], - outfile: target_worker, + entryPoints: [`${tmp}/_worker.js`], + outfile: `${dest}/_worker.js`, allowOverwrite: true, format: 'esm', bundle: true diff --git a/packages/adapter-netlify/.gitignore b/packages/adapter-netlify/.gitignore index 91dfed8d4a8b..1f664acc2b82 100644 --- a/packages/adapter-netlify/.gitignore +++ b/packages/adapter-netlify/.gitignore @@ -1,2 +1,3 @@ .DS_Store -node_modules \ No newline at end of file +node_modules +/files \ No newline at end of file diff --git a/packages/adapter-netlify/README.md b/packages/adapter-netlify/README.md index ba5276100a32..0f19f88803a2 100644 --- a/packages/adapter-netlify/README.md +++ b/packages/adapter-netlify/README.md @@ -1,8 +1,6 @@ # adapter-netlify -Adapter for Svelte apps that creates a Netlify app, using a function for dynamic server rendering. A future version might use a function per route, though it's unclear if that has any real advantages. - -This is very experimental; the adapter API isn't at all fleshed out, and things will definitely change. +Adapter for Svelte apps that creates a Netlify app, using serverless functions for dynamically generating pages. ## Installation @@ -19,7 +17,11 @@ import adapter from '@sveltejs/adapter-netlify'; export default { kit: { - adapter: adapter(), // currently the adapter does not take any options + adapter: adapter({ + // if true, will split your app into multiple functions + // instead of creating a single one for the entire app + split: false + }), target: '#svelte' } }; @@ -41,7 +43,7 @@ You may build your app using functionality provided directly by SvelteKit withou ### Using Netlify Redirect Rules -During compilation a required "catch all" redirect rule is automatically appended to your `_redirects` file. (If it doesn't exist yet, it will be created.) That means: +During compilation, redirect rules are automatically appended to your `_redirects` file. (If it doesn't exist yet, it will be created.) That means: - `[[redirects]]` in `netlify.toml` will never match as `_redirects` has a [higher priority](https://docs.netlify.com/routing/redirects/#rule-processing-order). So always put your rules in the [`_redirects` file](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file). - `_redirects` shouldn't have any custom "catch all" rules such as `/* /foobar/:splat`. Otherwise the automatically appended rule will never be applied as Netlify is only processing [the first matching rule](https://docs.netlify.com/routing/redirects/#rule-processing-order). @@ -66,38 +68,6 @@ During compilation a required "catch all" redirect rule is automatically appende node_bundler = "esbuild" ``` -## Advanced Configuration - -### esbuild - -As an escape hatch, you may optionally specify a function which will receive the final esbuild options generated by this adapter and returns a modified esbuild configuration. The result of this function will be passed as-is to esbuild. The function can be async. - -For example, you may wish to add a plugin: - -```js -adapterNetlify({ - esbuild(defaultOptions) { - return { - ...defaultOptions, - plugins: [] - }; - } -}); -``` - -The default options for this version are as follows: - -```js -{ - entryPoints: ['.svelte-kit/netlify/entry.js'], - // This is Netlify's internal functions directory, not the one for user functions. - outfile: '.netlify/functions-internal/__render.js', - bundle: true, - inject: ['pathTo/shims.js'], - platform: 'node' -} -``` - ## Changelog [The Changelog for this package is available on GitHub](/~https://github.com/sveltejs/kit/blob/master/packages/adapter-netlify/CHANGELOG.md). diff --git a/packages/adapter-netlify/files/entry.js b/packages/adapter-netlify/files/entry.js deleted file mode 100644 index ebf337948a90..000000000000 --- a/packages/adapter-netlify/files/entry.js +++ /dev/null @@ -1,71 +0,0 @@ -// TODO hardcoding the relative location makes this brittle -import { init, render } from '../output/server/app.js'; - -init(); - -export async function handler(event) { - const { path, httpMethod, headers, rawQuery, body, isBase64Encoded } = event; - - const query = new URLSearchParams(rawQuery); - - const encoding = isBase64Encoded ? 'base64' : headers['content-encoding'] || 'utf-8'; - const rawBody = typeof body === 'string' ? Buffer.from(body, encoding) : body; - - const rendered = await render({ - method: httpMethod, - headers, - path, - query, - rawBody - }); - - if (!rendered) { - return { - statusCode: 404, - body: 'Not found' - }; - } - - const partial_response = { - statusCode: rendered.status, - ...split_headers(rendered.headers) - }; - - if (rendered.body instanceof Uint8Array) { - // Function responses should be strings (or undefined), and responses with binary - // content should be base64 encoded and set isBase64Encoded to true. - // /~https://github.com/netlify/functions/blob/main/src/function/response.ts - return { - ...partial_response, - isBase64Encoded: true, - body: Buffer.from(rendered.body).toString('base64') - }; - } - - return { - ...partial_response, - body: rendered.body - }; -} - -/** - * Splits headers into two categories: single value and multi value - * @param {Record} headers - * @returns {{ - * headers: Record, - * multiValueHeaders: Record - * }} - */ -function split_headers(headers) { - const h = {}; - const m = {}; - for (const key in headers) { - const value = headers[key]; - const target = Array.isArray(value) ? m : h; - target[key] = value; - } - return { - headers: h, - multiValueHeaders: m - }; -} diff --git a/packages/adapter-netlify/files/shims.js b/packages/adapter-netlify/files/shims.js deleted file mode 100644 index cd9f71d6863c..000000000000 --- a/packages/adapter-netlify/files/shims.js +++ /dev/null @@ -1 +0,0 @@ -export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; diff --git a/packages/adapter-netlify/index.d.ts b/packages/adapter-netlify/index.d.ts index 7f3175d6e358..241175bc0667 100644 --- a/packages/adapter-netlify/index.d.ts +++ b/packages/adapter-netlify/index.d.ts @@ -1,9 +1,4 @@ import { Adapter } from '@sveltejs/kit'; -import { BuildOptions } from 'esbuild'; -interface AdapterOptions { - esbuild?: (options: BuildOptions) => Promise | BuildOptions; -} - -declare function plugin(options?: AdapterOptions): Adapter; +declare function plugin(opts?: { split?: boolean }): Adapter; export = plugin; diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 2368d0f2d1cd..7af85cc601c1 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -1,83 +1,158 @@ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs'; import { join, resolve } from 'path'; import { fileURLToPath } from 'url'; +import glob from 'tiny-glob/sync.js'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; /** - * @typedef {import('esbuild').BuildOptions} BuildOptions + * @typedef {{ + * build?: { publish?: string } + * functions?: { node_bundler?: 'zisi' | 'esbuild' } + * } & toml.JsonMap} NetlifyConfig */ +const files = fileURLToPath(new URL('./files', import.meta.url)); + /** @type {import('.')} */ -export default function (options) { +export default function ({ split = false } = {}) { return { name: '@sveltejs/adapter-netlify', - async adapt({ utils }) { - // "build" is the default publish directory when Netlify detects SvelteKit - const publish = get_publish_directory(utils) || 'build'; - - utils.log.minor(`Publishing to "${publish}"`); - - utils.rimraf(publish); + async adapt(builder) { + const netlify_config = get_netlify_config(); - const files = fileURLToPath(new URL('./files', import.meta.url)); - - utils.log.minor('Generating serverless function...'); - utils.copy(join(files, 'entry.js'), '.svelte-kit/netlify/entry.js'); - - /** @type {BuildOptions} */ - const default_options = { - entryPoints: ['.svelte-kit/netlify/entry.js'], - // Any functions in ".netlify/functions-internal" are bundled in addition to user-defined Netlify functions. - // See /~https://github.com/netlify/build/pull/3213 for more details - outfile: '.netlify/functions-internal/__render.js', - bundle: true, - inject: [join(files, 'shims.js')], - platform: 'node' - }; + // "build" is the default publish directory when Netlify detects SvelteKit + const publish = get_publish_directory(netlify_config, builder) || 'build'; - const build_options = - options && options.esbuild ? await options.esbuild(default_options) : default_options; + // empty out existing build directories + builder.rimraf(publish); + builder.rimraf('.netlify/functions-internal'); + builder.rimraf('.netlify/server'); + builder.rimraf('.netlify/package.json'); + builder.rimraf('.netlify/handler.js'); - await esbuild.build(build_options); + builder.mkdirp('.netlify/functions-internal'); - writeFileSync(join('.netlify', 'package.json'), JSON.stringify({ type: 'commonjs' })); + builder.log.minor(`Publishing to "${publish}"`); - utils.log.minor('Prerendering static pages...'); - await utils.prerender({ + builder.log.minor('Prerendering static pages...'); + await builder.prerender({ dest: publish }); - utils.log.minor('Copying assets...'); - utils.copy_static_files(publish); - utils.copy_client_files(publish); - - utils.log.minor('Writing redirects...'); - - const redirectPath = join(publish, '_redirects'); - utils.copy('_redirects', redirectPath); - appendFileSync(redirectPath, '\n\n/* /.netlify/functions/__render 200'); + builder.writeServer('.netlify/server'); + + // for esbuild, use ESM + // for zip-it-and-ship-it, use CJS until /~https://github.com/netlify/zip-it-and-ship-it/issues/750 + const esm = netlify_config?.functions?.node_bundler === 'esbuild'; + + /** @type {string[]} */ + const redirects = []; + + if (esm) { + builder.copy(`${files}/esm`, '.netlify'); + } else { + glob('**/*.js', { cwd: '.netlify/server' }).forEach((file) => { + const filepath = `.netlify/server/${file}`; + const input = readFileSync(filepath, 'utf8'); + const output = esbuild.transformSync(input, { format: 'cjs', target: 'node12' }).code; + writeFileSync(filepath, output); + }); + + builder.copy(`${files}/cjs`, '.netlify'); + writeFileSync(join('.netlify', 'package.json'), JSON.stringify({ type: 'commonjs' })); + } + + if (split) { + builder.log.minor('Generating serverless functions...'); + + builder.createEntries((route) => { + const parts = []; + + for (const segment of route.segments) { + if (segment.rest) { + parts.push('*'); + break; // Netlify redirects don't allow anything after a * + } else if (segment.dynamic) { + parts.push(`:${parts.length}`); + } else { + parts.push(segment.content); + } + } + + const pattern = `/${parts.join('/')}`; + const name = parts.join('-').replace(/[:.]/g, '_').replace('*', '__rest') || 'index'; + + return { + id: pattern, + filter: (other) => matches(route.segments, other.segments), + complete: (entry) => { + const manifest = entry.generateManifest({ + relativePath: '../server', + format: esm ? 'esm' : 'cjs' + }); + + const fn = esm + ? `import { init } from '../handler.js';\n\nexport const handler = init(${manifest});\n` + : `const { init } = require('../handler.js');\n\nexports.handler = init(${manifest});\n`; + + writeFileSync(`.netlify/functions-internal/${name}.js`, fn); + + redirects.push(`${pattern} /.netlify/functions/${name} 200`); + } + }; + }); + } else { + builder.log.minor('Generating serverless functions...'); + + const manifest = builder.generateManifest({ + relativePath: '../server', + format: esm ? 'esm' : 'cjs' + }); + + const fn = esm + ? `import { init } from '../handler.js';\n\nexport const handler = init(${manifest});\n` + : `const { init } = require('../handler.js');\n\nexports.handler = init(${manifest});\n`; + + writeFileSync('.netlify/functions-internal/render.js', fn); + + redirects.push('* /.netlify/functions/render 200'); + } + + builder.log.minor('Copying assets...'); + builder.writeStatic(publish); + builder.writeClient(publish); + + builder.log.minor('Writing redirects...'); + const redirect_file = join(publish, '_redirects'); + builder.copy('_redirects', redirect_file); + appendFileSync(redirect_file, `\n\n${redirects.join('\n')}`); + + // TODO write a _headers file that makes client-side assets immutable } }; } + +function get_netlify_config() { + if (!existsSync('netlify.toml')) return null; + + try { + return /** @type {NetlifyConfig} */ (toml.parse(readFileSync('netlify.toml', 'utf-8'))); + } catch (err) { + err.message = `Error parsing netlify.toml: ${err.message}`; + throw err; + } +} + /** - * @param {import('@sveltejs/kit').AdapterUtils} utils + * @param {NetlifyConfig} netlify_config + * @param {import('@sveltejs/kit').Builder} builder **/ -function get_publish_directory(utils) { - if (existsSync('netlify.toml')) { - /** @type {{ build?: { publish?: string }} & toml.JsonMap } */ - let netlify_config; - - try { - netlify_config = toml.parse(readFileSync('netlify.toml', 'utf-8')); - } catch (err) { - err.message = `Error parsing netlify.toml: ${err.message}`; - throw err; - } - +function get_publish_directory(netlify_config, builder) { + if (netlify_config) { if (!netlify_config.build || !netlify_config.build.publish) { - utils.log.warn('No publish directory specified in netlify.toml, using default'); + builder.log.warn('No publish directory specified in netlify.toml, using default'); return; } @@ -94,7 +169,43 @@ function get_publish_directory(utils) { return netlify_config.build.publish; } - utils.log.warn( + builder.log.warn( 'No netlify.toml found. Using default publish directory. Consult /~https://github.com/sveltejs/kit/tree/master/packages/adapter-netlify#configuration for more details ' ); } + +/** + * @typedef {{ rest: boolean, dynamic: boolean, content: string }} RouteSegment + */ + +/** + * @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; + } + + 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; + } +} diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index f0a347d9d431..96ba0acf4027 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -22,15 +22,24 @@ "index.d.ts" ], "scripts": { + "dev": "rimraf files && rollup -cw", + "build": "rimraf files && rollup -c", "lint": "eslint --ignore-path .gitignore \"**/*.{ts,js,svelte}\" && npm run check-format", "format": "npm run check-format -- --write", - "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore" + "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", + "prepublishOnly": "npm run build" }, "dependencies": { "@iarna/toml": "^2.2.5", - "esbuild": "^0.13.15" + "esbuild": "^0.13.15", + "tiny-glob": "^0.2.9" }, "devDependencies": { - "@sveltejs/kit": "workspace:*" + "@rollup/plugin-commonjs": "^21.0.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.0.5", + "@sveltejs/kit": "workspace:*", + "rimraf": "^3.0.2", + "rollup": "^2.58.0" } } diff --git a/packages/adapter-netlify/rollup.config.js b/packages/adapter-netlify/rollup.config.js new file mode 100644 index 000000000000..6d96c6e299b9 --- /dev/null +++ b/packages/adapter-netlify/rollup.config.js @@ -0,0 +1,23 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; + +export default [ + { + input: { + handler: 'src/handler.js' + }, + output: [ + { + dir: 'files/cjs', + format: 'cjs' + }, + { + dir: 'files/esm', + format: 'esm' + } + ], + plugins: [nodeResolve(), commonjs(), json()], + external: ['./server/app.js', ...require('module').builtinModules] + } +]; diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js new file mode 100644 index 000000000000..4c8c18547810 --- /dev/null +++ b/packages/adapter-netlify/src/handler.js @@ -0,0 +1,79 @@ +import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; +import { App } from './server/app.js'; + +__fetch_polyfill(); + +export function init(manifest) { + const app = new App(manifest); + + return async (event) => { + const { path, httpMethod, headers, rawQuery, body, isBase64Encoded } = event; + + const query = new URLSearchParams(rawQuery); + + const encoding = isBase64Encoded ? 'base64' : headers['content-encoding'] || 'utf-8'; + const rawBody = typeof body === 'string' ? Buffer.from(body, encoding) : body; + + const rendered = await app.render({ + method: httpMethod, + headers, + path, + query, + rawBody + }); + + if (!rendered) { + return { + statusCode: 404, + body: 'Not found' + }; + } + + const partial_response = { + statusCode: rendered.status, + ...split_headers(rendered.headers) + }; + + if (rendered.body instanceof Uint8Array) { + // Function responses should be strings (or undefined), and responses with binary + // content should be base64 encoded and set isBase64Encoded to true. + // /~https://github.com/netlify/functions/blob/main/src/function/response.ts + return { + ...partial_response, + isBase64Encoded: true, + body: Buffer.from(rendered.body).toString('base64') + }; + } + + return { + ...partial_response, + body: rendered.body + }; + }; +} + +/** + * Splits headers into two categories: single value and multi value + * @param {Record} headers + * @returns {{ + * headers: Record, + * multiValueHeaders: Record + * }} + */ +function split_headers(headers) { + /** @type {Record} */ + const h = {}; + + /** @type {Record} */ + const m = {}; + + for (const key in headers) { + const value = headers[key]; + const target = Array.isArray(value) ? m : h; + target[key] = value; + } + return { + headers: h, + multiValueHeaders: m + }; +} diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index 608f9aed0919..71cf7d353f2d 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -27,10 +27,6 @@ export default { ## Options -### entryPoint - -The server entry point. Allows you to provide a [custom server implementation](#middleware). Defaults to the provided reference server. - ### out The directory to build the server to. It defaults to `build` — i.e. `node build` would start the server locally after it has been created. @@ -60,77 +56,32 @@ env: { MY_HOST_VARIABLE=127.0.0.1 MY_PORT_VARIABLE=4000 node build ``` -## Middleware +## Custom server -The adapter exports a middleware `(req, res, next) => {}` that's compatible with [Express](/~https://github.com/expressjs/expressjs.com) / [Connect](/~https://github.com/senchalabs/connect) / [Polka](/~https://github.com/lukeed/polka). Additionally, it also exports a reference server implementation using this middleware with a plain Node HTTP server. +The adapter creates two files in your build directory — `index.js` and `handler.js`. Running `index.js` — e.g. `node build`, if you use the default build directory — will start a server on the configured port. -But you can use your favorite server framework to combine it with other middleware and server logic. You can import `kitMiddleware`, your ready-to-use SvelteKit middleware from the `build` directory. You can use [the `entryPoint` option](#entryPoint) to bundle your custom server entry point. +Alternatively, you can import the `handler.js` file, which exports a handler suitable for use with [Express](/~https://github.com/expressjs/expressjs.com), [Connect](/~https://github.com/senchalabs/connect) or [Polka](/~https://github.com/lukeed/polka) (or even just the built-in [`http.createServer`](https://nodejs.org/dist/latest/docs/api/http.html#httpcreateserveroptions-requestlistener)) and set up your own server: ```js -// src/server.js -import { assetsMiddleware, prerenderedMiddleware, kitMiddleware } from '../build/middlewares.js'; -import polka from 'polka'; - -const app = polka(); +// my-server.js +import { handler } from './build/handler.js'; +import express from 'express'; -const myMiddleware = function (req, res, next) { - console.log('Hello world!'); - next(); -}; - -app.use(myMiddleware); +const app = express(); -app.get('/no-svelte', (req, res) => { - res.end('This is not Svelte!'); +// add a route that lives separately from the SvelteKit app +app.get('/healthcheck', (req, res) => { + res.end('ok'); }); -app.all('*', assetsMiddleware, prerenderedMiddleware, kitMiddleware); - -// Express users can also write in a second way: -// app.use(assetsMiddleware, prerenderedMiddleware, kitMiddleware); - -app.listen(3000); -``` - -For using middleware in dev mode, [see the FAQ](https://kit.svelte.dev/faq#how-do-i-use-x-with-sveltekit-how-do-i-use-middleware). - -## Advanced Configuration - -### esbuild +// let SvelteKit handle everything else, including serving prerendered pages and static assets +app.use(handler); -As an escape hatch, you may optionally specify a function which will receive the final esbuild options generated by this adapter and returns a modified esbuild configuration. The result of this function will be passed as-is to esbuild. The function can be async. - -For example, you may wish to add a plugin: - -```js -adapterNode({ - esbuild(defaultOptions) { - return { - ...defaultOptions, - plugins: [] - }; - } +app.listen(3000, () => { + console.log('listening on port 3000'); }); ``` -The default options for this version are as follows: - -```js -{ - entryPoints: ['.svelte-kit/node/index.js'], - outfile: 'pathTo/index.js', - bundle: true, - external: allProductionDependencies, // from package.json - format: 'esm', - platform: 'node', - target: 'node14', - inject: ['pathTo/shims.js'], - define: { - esbuild_app_dir: `"${config.kit.appDir}"` - } -} -``` - ## Deploying You will need the output directory (`build` by default), the project's `package.json`, and the production dependencies in `node_modules` to run the application. Production dependencies can be generated with `npm ci --prod`, you can also skip this step if your app doesn't have any dependencies. You can then start your app with diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 3d2b258fb6a7..0ab2c6becc19 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,13 +1,4 @@ -import esbuild from 'esbuild'; -import { - createReadStream, - createWriteStream, - existsSync, - readFileSync, - statSync, - writeFileSync -} from 'fs'; -import { join, resolve } from 'path'; +import { createReadStream, createWriteStream, statSync, writeFileSync } from 'fs'; import { pipeline } from 'stream'; import glob from 'tiny-glob'; import { fileURLToPath } from 'url'; @@ -16,103 +7,55 @@ import zlib from 'zlib'; const pipe = promisify(pipeline); +const files = fileURLToPath(new URL('./files', import.meta.url)); + /** * @typedef {import('esbuild').BuildOptions} BuildOptions */ /** @type {import('.')} */ export default function ({ - entryPoint = '.svelte-kit/node/index.js', out = 'build', precompress, - env: { path: path_env = 'SOCKET_PATH', host: host_env = 'HOST', port: port_env = 'PORT' } = {}, - esbuild: esbuild_config + env: { path: path_env = 'SOCKET_PATH', host: host_env = 'HOST', port: port_env = 'PORT' } = {} } = {}) { return { name: '@sveltejs/adapter-node', - async adapt({ utils, config }) { - utils.rimraf(out); + async adapt(builder) { + builder.rimraf(out); - utils.log.minor('Copying assets'); - const static_directory = join(out, 'assets'); - utils.copy_client_files(static_directory); - utils.copy_static_files(static_directory); + builder.log.minor('Copying assets'); + builder.writeClient(`${out}/client`); + builder.writeServer(`${out}/server`); + builder.writeStatic(`${out}/static`); - if (precompress) { - utils.log.minor('Compressing assets'); - await compress(static_directory); - } + builder.log.minor('Prerendering static pages'); + await builder.prerender({ + dest: `${out}/prerendered` + }); - utils.log.minor('Building SvelteKit middleware'); - const files = fileURLToPath(new URL('./files', import.meta.url)); - utils.copy(files, '.svelte-kit/node'); writeFileSync( - '.svelte-kit/node/env.js', - `export const path = process.env[${JSON.stringify( - path_env - )}] || false;\nexport const host = process.env[${JSON.stringify( - host_env - )}] || '0.0.0.0';\nexport const port = process.env[${JSON.stringify( - port_env - )}] || (!path && 3000);` + `${out}/manifest.js`, + `export const manifest = ${builder.generateManifest({ + relativePath: './server' + })};\n` ); - /** @type {BuildOptions} */ - const defaultOptions = { - entryPoints: ['.svelte-kit/node/middlewares.js'], - outfile: join(out, 'middlewares.js'), - bundle: true, - external: Object.keys(JSON.parse(readFileSync('package.json', 'utf8')).dependencies || {}), - format: 'esm', - platform: 'node', - target: 'node14', - inject: [join(files, 'shims.js')], - define: { - APP_DIR: `"/${config.kit.appDir}/"` + builder.copy(files, out, { + replace: { + APP: './server/app.js', + MANIFEST: './manifest.js', + PATH_ENV: JSON.stringify(path_env), + HOST_ENV: JSON.stringify(host_env), + PORT_ENV: JSON.stringify(port_env) } - }; - const build_options = esbuild_config ? await esbuild_config(defaultOptions) : defaultOptions; - await esbuild.build(build_options); - - utils.log.minor('Building SvelteKit server'); - /** @type {BuildOptions} */ - const default_options_ref_server = { - entryPoints: [entryPoint], - outfile: join(out, 'index.js'), - bundle: true, - format: 'esm', - platform: 'node', - target: 'node14', - // external exclude workaround, see /~https://github.com/evanw/esbuild/issues/514 - plugins: [ - { - name: 'fix-middlewares-exclude', - setup(build) { - // Match an import of "middlewares.js" and mark it as external - const internal_middlewares_path = resolve('.svelte-kit/node/middlewares.js'); - const build_middlewares_path = resolve(out, 'middlewares.js'); - build.onResolve({ filter: /\/middlewares\.js$/ }, ({ path, resolveDir }) => { - const resolved = resolve(resolveDir, path); - if (resolved === internal_middlewares_path || resolved === build_middlewares_path) { - return { path: './middlewares.js', external: true }; - } - }); - } - } - ] - }; - const build_options_ref_server = esbuild_config - ? await esbuild_config(default_options_ref_server) - : default_options_ref_server; - await esbuild.build(build_options_ref_server); - - utils.log.minor('Prerendering static pages'); - await utils.prerender({ - dest: `${out}/prerendered` }); - if (precompress && existsSync(`${out}/prerendered`)) { - utils.log.minor('Compressing prerendered pages'); + + if (precompress) { + builder.log.minor('Compressing assets'); + await compress(`${out}/client`); + await compress(`${out}/static`); await compress(`${out}/prerendered`); } } diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json index dd89a2c34fb4..62d7ba35ec01 100644 --- a/packages/adapter-node/package.json +++ b/packages/adapter-node/package.json @@ -22,16 +22,15 @@ "index.d.ts" ], "scripts": { - "dev": "rollup -cw", - "build": "rollup -c", - "test": "c8 uvu tests", + "dev": "rimraf files && rollup -cw", + "build": "rimraf files && rollup -c", + "test": "echo \"tests temporarily disabled\" # c8 uvu tests", "lint": "eslint --ignore-path .gitignore \"**/*.{ts,js,svelte}\" && npm run check-format", "format": "npm run check-format -- --write", "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", "prepublishOnly": "npm run build" }, "dependencies": { - "esbuild": "^0.13.15", "tiny-glob": "^0.2.9" }, "devDependencies": { @@ -42,6 +41,7 @@ "compression": "^1.7.4", "node-fetch": "^3.1.0", "polka": "^1.0.0-next.22", + "rimraf": "^3.0.2", "rollup": "^2.60.2", "sirv": "^1.0.19", "uvu": "^0.5.2" diff --git a/packages/adapter-node/rollup.config.js b/packages/adapter-node/rollup.config.js index 619756ee6c1e..1f2decb4ead7 100644 --- a/packages/adapter-node/rollup.config.js +++ b/packages/adapter-node/rollup.config.js @@ -4,31 +4,15 @@ import json from '@rollup/plugin-json'; export default [ { - input: 'src/middlewares.js', - output: { - file: 'files/middlewares.js', - format: 'esm', - sourcemap: true - }, - plugins: [nodeResolve(), commonjs(), json()], - external: ['../output/server/app.js', ...require('module').builtinModules] - }, - { - input: 'src/index.js', - output: { - file: 'files/index.js', - format: 'esm', - sourcemap: true + input: { + index: 'src/index.js', + handler: 'src/handler.js' }, - plugins: [nodeResolve(), commonjs(), json()], - external: ['./middlewares.js', './env.js', ...require('module').builtinModules] - }, - { - input: 'src/shims.js', output: { - file: 'files/shims.js', + dir: 'files', format: 'esm' }, - external: ['module'] + plugins: [nodeResolve(), commonjs(), json()], + external: ['APP', 'MANIFEST', ...require('module').builtinModules] } ]; diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js new file mode 100644 index 000000000000..35bbcbfcca7f --- /dev/null +++ b/packages/adapter-node/src/handler.js @@ -0,0 +1,95 @@ +import path from 'path'; +import sirv from 'sirv'; +import { fileURLToPath } from 'url'; +import { getRawBody } from '@sveltejs/kit/node'; +import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; + +// @ts-ignore +import { App } from 'APP'; +import { manifest } from 'MANIFEST'; + +__fetch_polyfill(); + +const app = new App(manifest); + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const serve_client = sirv(path.join(__dirname, '/client'), { + etag: true, + maxAge: 31536000, + immutable: true, + gzip: true, + brotli: true +}); + +const serve_static = sirv(path.join(__dirname, '/static'), { + etag: true, + maxAge: 31536000, + immutable: true, + gzip: true, + brotli: true +}); + +const serve_prerendered = sirv(path.join(__dirname, '/prerendered'), { + etag: true, + maxAge: 0, + gzip: true, + brotli: true +}); + +/** @type {import('polka').Middleware} */ +const ssr = async (req, res) => { + let parsed; + try { + parsed = new URL(req.url || '', 'http://localhost'); + } catch (e) { + res.statusCode = 400; + return res.end('Invalid URL'); + } + + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } + + const rendered = await app.render({ + method: req.method, + headers: req.headers, // TODO: what about repeated headers, i.e. string[] + path: parsed.pathname, + query: parsed.searchParams, + rawBody: body + }); + + if (rendered) { + res.writeHead(rendered.status, rendered.headers); + if (rendered.body) { + res.write(rendered.body); + } + res.end(); + } else { + res.statusCode = 404; + res.end('Not found'); + } +}; + +/** @param {import('polka').Middleware[]} handlers */ +function sequence(handlers) { + /** @type {import('polka').Middleware} */ + return (req, res, next) => { + /** @param {number} i */ + function handle(i) { + handlers[i](req, res, () => { + if (i < handlers.length) handle(i + 1); + else next(); + }); + } + + handle(0); + }; +} + +export const handler = sequence([serve_client, serve_static, serve_prerendered, ssr]); diff --git a/packages/adapter-node/src/index.js b/packages/adapter-node/src/index.js index b8e807ea8450..65024cf97d36 100644 --- a/packages/adapter-node/src/index.js +++ b/packages/adapter-node/src/index.js @@ -1,21 +1,21 @@ -// @ts-ignore -import { path, host, port } from './env.js'; -import { assetsMiddleware, kitMiddleware, prerenderedMiddleware } from './middlewares.js'; +import { handler } from './handler.js'; import compression from 'compression'; import polka from 'polka'; +/* global PATH_ENV, HOST_ENV, PORT_ENV */ + +export const path = process.env[PATH_ENV] || false; +export const host = process.env[HOST_ENV] || '0.0.0.0'; +export const port = process.env[PORT_ENV] || (!path && '3000'); + const server = polka().use( // /~https://github.com/lukeed/polka/issues/173 // @ts-ignore - nothing we can do about so just ignore it compression({ threshold: 0 }), - assetsMiddleware, - kitMiddleware, - prerenderedMiddleware + handler ); -const listenOpts = { path, host, port }; - -server.listen(listenOpts, () => { +server.listen({ path, host, port }, () => { console.log(`Listening on ${path ? path : host + ':' + port}`); }); diff --git a/packages/adapter-node/src/kit-middleware.js b/packages/adapter-node/src/kit-middleware.js deleted file mode 100644 index bfa4a3429e48..000000000000 --- a/packages/adapter-node/src/kit-middleware.js +++ /dev/null @@ -1,46 +0,0 @@ -import { getRawBody } from '@sveltejs/kit/node'; - -/** - * @return {import('polka').Middleware} - */ -// TODO: type render function from @sveltejs/kit/adapter -// @ts-ignore -export function create_kit_middleware({ render }) { - return async (req, res) => { - let parsed; - try { - parsed = new URL(req.url || '', 'http://localhost'); - } catch (e) { - res.statusCode = 400; - return res.end('Invalid URL'); - } - - let body; - - try { - body = await getRawBody(req); - } catch (err) { - res.statusCode = err.status || 400; - return res.end(err.reason || 'Invalid request body'); - } - - const rendered = await render({ - method: req.method, - headers: req.headers, // TODO: what about repeated headers, i.e. string[] - path: parsed.pathname, - query: parsed.searchParams, - rawBody: body - }); - - if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) { - res.write(rendered.body); - } - res.end(); - } else { - res.statusCode = 404; - res.end('Not found'); - } - }; -} diff --git a/packages/adapter-node/src/middlewares.js b/packages/adapter-node/src/middlewares.js deleted file mode 100644 index ba4ee030244d..000000000000 --- a/packages/adapter-node/src/middlewares.js +++ /dev/null @@ -1,48 +0,0 @@ -// TODO hardcoding the relative location makes this brittle -// Also, we need most of the logic in another file for testing because -// ../output/server/app.js doesn't exist when we run the tests -// @ts-ignore -import { init, render } from '../output/server/app.js'; -import { create_kit_middleware } from './kit-middleware.js'; - -import fs from 'fs'; -import { dirname, join } from 'path'; -import sirv from 'sirv'; -import { fileURLToPath } from 'url'; - -// App is a dynamic file built from the application layer. - -const __dirname = dirname(fileURLToPath(import.meta.url)); -/** @type {import('polka').Middleware} */ -const noop_handler = (_req, _res, next) => next(); -const paths = { - assets: join(__dirname, '/assets'), - prerendered: join(__dirname, '/prerendered') -}; - -export const prerenderedMiddleware = fs.existsSync(paths.prerendered) - ? sirv(paths.prerendered, { - etag: true, - maxAge: 0, - gzip: true, - brotli: true - }) - : noop_handler; - -export const assetsMiddleware = fs.existsSync(paths.assets) - ? sirv(paths.assets, { - setHeaders: (res, pathname) => { - // @ts-expect-error - dynamically replaced with define - if (pathname.startsWith(/* eslint-disable-line no-undef */ APP_DIR)) { - res.setHeader('cache-control', 'public, max-age=31536000, immutable'); - } - }, - gzip: true, - brotli: true - }) - : noop_handler; - -export const kitMiddleware = (function () { - init(); - return create_kit_middleware({ render }); -})(); diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js deleted file mode 100644 index 839b2dd5b108..000000000000 --- a/packages/adapter-node/src/shims.js +++ /dev/null @@ -1,9 +0,0 @@ -import { createRequire } from 'module'; -export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; - -// esbuild automatically renames "require" -// So we still have to use Object.defineProperty here -Object.defineProperty(globalThis, 'require', { - enumerable: true, - value: createRequire(import.meta.url) -}); diff --git a/packages/adapter-node/src/types.d.ts b/packages/adapter-node/src/types.d.ts new file mode 100644 index 000000000000..4fc8a2cd7a87 --- /dev/null +++ b/packages/adapter-node/src/types.d.ts @@ -0,0 +1,7 @@ +declare global { + const PATH_ENV: string; + const HOST_ENV: string; + const PORT_ENV: string; +} + +export {}; diff --git a/packages/adapter-node/tests/smoke.js b/packages/adapter-node/tests/smoke.js index ed7698959b2b..55a80de34081 100644 --- a/packages/adapter-node/tests/smoke.js +++ b/packages/adapter-node/tests/smoke.js @@ -1,5 +1,5 @@ import { test } from 'uvu'; -import { create_kit_middleware } from '../src/kit-middleware.js'; +import { create_kit_middleware } from '../src/handler.js'; import * as assert from 'uvu/assert'; import fetch from 'node-fetch'; import polka from 'polka'; diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index 3e089a62a6b9..611f8acc9116 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -11,14 +11,14 @@ export default function ({ pages = 'build', assets = pages, fallback, precompres return { name: '@sveltejs/adapter-static', - async adapt({ utils }) { - utils.rimraf(assets); - utils.rimraf(pages); + async adapt(builder) { + builder.rimraf(assets); + builder.rimraf(pages); - utils.copy_static_files(assets); - utils.copy_client_files(assets); + builder.writeStatic(assets); + builder.writeClient(assets); - await utils.prerender({ + await builder.prerender({ fallback, all: !fallback, dest: pages @@ -26,13 +26,13 @@ export default function ({ pages = 'build', assets = pages, fallback, precompres if (precompress) { if (pages === assets) { - utils.log.minor('Compressing assets and pages'); + builder.log.minor('Compressing assets and pages'); await compress(assets); } else { - utils.log.minor('Compressing assets'); + builder.log.minor('Compressing assets'); await compress(assets); - utils.log.minor('Compressing pages'); + builder.log.minor('Compressing pages'); await compress(pages); } } diff --git a/packages/adapter-vercel/README.md b/packages/adapter-vercel/README.md index fd980fb0d390..9fba0c62666f 100644 --- a/packages/adapter-vercel/README.md +++ b/packages/adapter-vercel/README.md @@ -19,37 +19,6 @@ export default { }; ``` -## Advanced Configuration - -### esbuild - -As an escape hatch, you may optionally specify a function which will receive the final esbuild options generated by this adapter and returns a modified esbuild configuration. The result of this function will be passed as-is to esbuild. The function can be async. - -For example, you may wish to add a plugin: - -```js -adapterVercel({ - esbuild(defaultOptions) { - return { - ...defaultOptions, - plugins: [] - }; - } -}); -``` - -The default options for this version are as follows: - -```js -{ - entryPoints: ['.svelte-kit/vercel/entry.js'], - outfile: `pathToLambdaFolder/index.js`, - bundle: true, - inject: ['pathTo/shims.js'], - platform: 'node' -} -``` - ## Changelog [The Changelog for this package is available on GitHub](/~https://github.com/sveltejs/kit/blob/master/packages/adapter-vercel/CHANGELOG.md). diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index 35d6e3170a7c..48994586526b 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -1,9 +1,11 @@ +import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; import { getRawBody } from '@sveltejs/kit/node'; +import { App } from 'APP'; +import { manifest } from 'MANIFEST'; -// TODO hardcoding the relative location makes this brittle -import { init, render } from '../output/server/app.js'; +__fetch_polyfill(); -init(); +const app = new App(manifest); export default async (req, res) => { const { pathname, searchParams } = new URL(req.url || '', 'http://localhost'); @@ -17,7 +19,7 @@ export default async (req, res) => { return res.end(err.reason || 'Invalid request body'); } - const rendered = await render({ + const rendered = await app.render({ method: req.method, headers: req.headers, path: pathname, diff --git a/packages/adapter-vercel/files/routes.json b/packages/adapter-vercel/files/routes.json deleted file mode 100644 index ffaed1bca535..000000000000 --- a/packages/adapter-vercel/files/routes.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "handle": "filesystem" - }, - { - "src": "/.*", - "dest": ".vercel/functions/render" - } -] diff --git a/packages/adapter-vercel/files/shims.js b/packages/adapter-vercel/files/shims.js deleted file mode 100644 index cd9f71d6863c..000000000000 --- a/packages/adapter-vercel/files/shims.js +++ /dev/null @@ -1 +0,0 @@ -export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index 7f3175d6e358..62aee0f1eaa9 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -1,9 +1,4 @@ import { Adapter } from '@sveltejs/kit'; -import { BuildOptions } from 'esbuild'; -interface AdapterOptions { - esbuild?: (options: BuildOptions) => Promise | BuildOptions; -} - -declare function plugin(options?: AdapterOptions): Adapter; +declare function plugin(): Adapter; export = plugin; diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index d5c53689f143..b2ee92c82070 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -1,63 +1,77 @@ import { writeFileSync } from 'fs'; -import { join } from 'path'; +import { relative } from 'path'; import { fileURLToPath } from 'url'; import esbuild from 'esbuild'; -/** - * @typedef {import('esbuild').BuildOptions} BuildOptions - */ +// By writing to .output, we opt in to the Vercel filesystem API: +// https://vercel.com/docs/file-system-api +const VERCEL_OUTPUT = '.output'; /** @type {import('.')} **/ -export default function (options) { +export default function () { return { name: '@sveltejs/adapter-vercel', - async adapt({ utils }) { - const dir = '.vercel_build_output'; - utils.rimraf(dir); + async adapt(builder) { + const tmp = builder.getBuildDirectory('vercel-tmp'); - const files = fileURLToPath(new URL('./files', import.meta.url)); + builder.rimraf(VERCEL_OUTPUT); + builder.rimraf(tmp); - const dirs = { - static: join(dir, 'static'), - lambda: join(dir, 'functions/node/render') - }; - - // TODO ideally we'd have something like utils.tmpdir('vercel') - // rather than hardcoding '.svelte-kit/vercel/entry.js', and the - // relative import from that file to output/server/app.js - // would be controlled. at the moment we're exposing - // implementation details that could change - utils.log.minor('Generating serverless function...'); - utils.copy(join(files, 'entry.js'), '.svelte-kit/vercel/entry.js'); - - /** @type {BuildOptions} */ - const default_options = { - entryPoints: ['.svelte-kit/vercel/entry.js'], - outfile: join(dirs.lambda, 'index.js'), - bundle: true, - inject: [join(files, 'shims.js')], - platform: 'node' - }; + builder.log.minor('Prerendering static pages...'); + await builder.prerender({ + dest: `${VERCEL_OUTPUT}/static` + }); + + builder.log.minor('Generating serverless function...'); - const build_options = - options && options.esbuild ? await options.esbuild(default_options) : default_options; + const files = fileURLToPath(new URL('./files', import.meta.url)); + const relativePath = relative(tmp, builder.getServerDirectory()); - await esbuild.build(build_options); + builder.copy(files, tmp, { + replace: { + APP: `${relativePath}/app.js`, + MANIFEST: './manifest.js' + } + }); - writeFileSync(join(dirs.lambda, 'package.json'), JSON.stringify({ type: 'commonjs' })); + writeFileSync( + `${tmp}/manifest.js`, + `export const manifest = ${builder.generateManifest({ + relativePath + })};\n` + ); - utils.log.minor('Prerendering static pages...'); - await utils.prerender({ - dest: dirs.static + await esbuild.build({ + entryPoints: [`${tmp}/entry.js`], + outfile: `${VERCEL_OUTPUT}/server/pages/__render.js`, + target: 'node14', + bundle: true, + platform: 'node' }); - utils.log.minor('Copying assets...'); - utils.copy_static_files(dirs.static); - utils.copy_client_files(dirs.static); + writeFileSync( + `${VERCEL_OUTPUT}/server/pages/package.json`, + JSON.stringify({ type: 'commonjs' }) + ); + + builder.log.minor('Copying assets...'); + builder.writeClient(`${VERCEL_OUTPUT}/static`); + builder.writeStatic(`${VERCEL_OUTPUT}/static`); - utils.log.minor('Writing routes...'); - utils.copy(join(files, 'routes.json'), join(dir, 'config/routes.json')); + builder.log.minor('Writing manifests...'); + writeFileSync( + `${VERCEL_OUTPUT}/routes-manifest.json`, + JSON.stringify({ + version: 3, + dynamicRoutes: [ + { + page: '/__render', + regex: '^/.*' + } + ] + }) + ); } }; } diff --git a/packages/adapter-vercel/tsconfig.json b/packages/adapter-vercel/tsconfig.json index 3b817fb6b17a..d856a1dae902 100644 --- a/packages/adapter-vercel/tsconfig.json +++ b/packages/adapter-vercel/tsconfig.json @@ -9,5 +9,5 @@ "moduleResolution": "node", "allowSyntheticDefaultImports": true }, - "include": ["./index.js", "src"] + "include": ["./index.js", "files"] } diff --git a/packages/create-svelte/templates/default/package.json b/packages/create-svelte/templates/default/package.json index 4ea063e9529a..e9fc3ebc3d92 100644 --- a/packages/create-svelte/templates/default/package.json +++ b/packages/create-svelte/templates/default/package.json @@ -4,6 +4,7 @@ "scripts": { "dev": "svelte-kit dev", "build": "svelte-kit build --verbose", + "preview": "svelte-kit preview", "start": "svelte-kit start" }, "devDependencies": { diff --git a/packages/kit/.gitignore b/packages/kit/.gitignore index 9c7349f74e5f..2337b45cd70d 100644 --- a/packages/kit/.gitignore +++ b/packages/kit/.gitignore @@ -5,4 +5,4 @@ /client/**/*.d.ts /test/**/.svelte-kit /test/**/build -!/src/api/adapt/test/fixtures/*/.svelte-kit +!/src/core/adapt/test/fixtures/*/.svelte-kit diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js new file mode 100644 index 000000000000..9ba6bcb7e8b0 --- /dev/null +++ b/packages/kit/src/core/adapt/builder.js @@ -0,0 +1,171 @@ +import { SVELTE_KIT } from '../constants.js'; +import { copy, rimraf, mkdirp } from '../../utils/filesystem.js'; +import { prerender } from './prerender.js'; +import { generate_manifest } from '../generate_manifest/index.js'; + +/** + * @param {{ + * cwd: string; + * config: import('types/config').ValidatedConfig; + * build_data: import('types/internal').BuildData; + * log: import('types/internal').Logger; + * }} opts + * @returns {import('types/config').Builder} + */ +export function create_builder({ cwd, config, build_data, log }) { + /** @type {Set} */ + const prerendered_paths = new Set(); + let generated_manifest = false; + + /** @param {import('types/internal').RouteData} route */ + function not_prerendered(route) { + if (route.type === 'page' && route.path) { + return !prerendered_paths.has(route.path); + } + + return true; + } + + return { + log, + rimraf, + mkdirp, + copy, + + createEntries(fn) { + generated_manifest = true; + + const { routes } = build_data.manifest_data; + + /** @type {import('types/config').RouteDefinition[]} */ + const facades = routes.map((route) => ({ + type: route.type, + segments: route.segments, + pattern: route.pattern, + methods: route.type === 'page' ? ['get'] : build_data.server.methods[route.file] + })); + + const seen = new Set(); + + for (let i = 0; i < routes.length; i += 1) { + const route = routes[i]; + const { id, filter, complete } = fn(facades[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 < routes.length; j += 1) { + if (filter(facades[j])) { + group.push(routes[j]); + } + } + + const filtered = new Set(group.filter(not_prerendered)); + + // heuristic: if /foo/[bar] is included, /foo/[bar].json should + // also be included, since the page likely needs the endpoint + filtered.forEach((route) => { + if (route.type === 'page') { + const length = route.segments.length; + + const endpoint = routes.find((candidate) => { + if (candidate.segments.length !== length) return false; + + for (let i = 0; i < length; i += 1) { + const a = route.segments[i]; + const b = candidate.segments[i]; + + if (i === length - 1) { + return b.content === `${a.content}.json`; + } + + if (a.content !== b.content) return false; + } + }); + + if (endpoint) { + filtered.add(endpoint); + } + } + }); + + if (filtered.size > 0) { + complete({ + generateManifest: ({ relativePath, format }) => + generate_manifest(build_data, relativePath, Array.from(filtered), format) + }); + } + } + }, + + generateManifest: ({ relativePath, format }) => { + generated_manifest = true; + return generate_manifest( + build_data, + relativePath, + build_data.manifest_data.routes.filter(not_prerendered), + format + ); + }, + + getBuildDirectory(name) { + return `${cwd}/${SVELTE_KIT}/${name}`; + }, + + getClientDirectory() { + return `${cwd}/${SVELTE_KIT}/output/client`; + }, + + getServerDirectory() { + return `${cwd}/${SVELTE_KIT}/output/server`; + }, + + getStaticDirectory() { + return config.kit.files.assets; + }, + + writeClient(dest) { + return copy(`${cwd}/${SVELTE_KIT}/output/client`, dest, { + filter: (file) => file[0] !== '.' + }); + }, + + writeServer(dest) { + return copy(`${cwd}/${SVELTE_KIT}/output/server`, dest, { + filter: (file) => file[0] !== '.' + }); + }, + + writeStatic(dest) { + return copy(config.kit.files.assets, dest); + }, + + async prerender({ all = false, dest, fallback }) { + if (generated_manifest) { + throw new Error( + 'Adapters must call prerender(...) before createEntries(...) or generateManifest(...)' + ); + } + + const prerendered = await prerender({ + out: dest, + all, + cwd, + config, + build_data, + fallback, + log + }); + + prerendered.paths.forEach((path) => { + prerendered_paths.add(path); + prerendered_paths.add(path + '/'); + }); + + return prerendered; + } + }; +} diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 475a9578a9da..75711e181afe 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -1,6 +1,6 @@ import colors from 'kleur'; import { logger } from '../utils.js'; -import { get_utils } from './utils.js'; +import { create_builder } from './builder.js'; /** * @param {import('types/config').ValidatedConfig} config @@ -13,8 +13,8 @@ export async function adapt(config, build_data, { cwd = process.cwd(), verbose } console.log(colors.bold().cyan(`\n> Using ${name}`)); const log = logger({ verbose }); - const utils = get_utils({ cwd, config, build_data, log }); - await adapt({ utils, config }); + const builder = create_builder({ cwd, config, build_data, log }); + await adapt(builder); log.success('done'); } diff --git a/packages/kit/src/core/adapt/prerender.js b/packages/kit/src/core/adapt/prerender.js index 6b1ff98b0c63..6474f3823036 100644 --- a/packages/kit/src/core/adapt/prerender.js +++ b/packages/kit/src/core/adapt/prerender.js @@ -94,34 +94,46 @@ const REDIRECT = 3; * fallback?: string; * all: boolean; // disregard `export const prerender = true` * }} opts - * @returns {Promise>} returns a promise that resolves to an array of paths corresponding to the files that have been prerendered. + * @returns {Promise<{ paths: string[] }>} returns a promise that resolves to an array of paths corresponding to the files that have been prerendered. */ export async function prerender({ cwd, out, log, config, build_data, fallback, all }) { if (!config.kit.prerender.enabled && !fallback) { - return []; + return { paths: [] }; } __fetch_polyfill(); + mkdirp(out); + const dir = resolve_path(cwd, `${SVELTE_KIT}/output`); const seen = new Set(); const server_root = resolve_path(dir); - /** @type {import('types/internal').App} */ - const app = await import(pathToFileURL(`${server_root}/server/app.js`).href); + /** @type {import('types/internal').AppModule} */ + const { App, override } = await import(pathToFileURL(`${server_root}/server/app.js`).href); - app.init({ + override({ paths: config.kit.paths, prerendering: true, read: (file) => readFileSync(join(config.kit.files.assets, file)) }); + const { manifest } = await import(pathToFileURL(`${server_root}/server/manifest.js`).href); + + const app = new App(manifest); + const error = chooseErrorHandler(log, config.kit.prerender.onError); - const files = new Set([...build_data.static, ...build_data.client]); - const written_files = []; + const files = new Set([ + ...build_data.static, + ...build_data.client.chunks.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`), + ...build_data.client.assets.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`) + ]); + + /** @type {string[]} */ + const paths = []; build_data.static.forEach((file) => { if (file.endsWith('/index.html')) { @@ -168,7 +180,6 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a const rendered = await app.render( { - host: config.kit.host, method: 'GET', headers: {}, path, @@ -195,15 +206,15 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } const file = `${out}${parts.join('/')}`; - mkdirp(dirname(file)); if (response_type === REDIRECT) { const location = get_single_valued_header(headers, 'location'); if (location) { + mkdirp(dirname(file)); + log.warn(`${rendered.status} ${decoded_path} -> ${location}`); writeFileSync(file, ``); - written_files.push(file); const resolved = resolve(path, location); if (is_root_relative(resolved)) { @@ -217,9 +228,11 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } if (rendered.status === 200) { + mkdirp(dirname(file)); + log.info(`${rendered.status} ${decoded_path}`); writeFileSync(file, rendered.body || ''); - written_files.push(file); + paths.push(normalize(decoded_path)); } else if (response_type !== OK) { error({ status: rendered.status, path, referrer, referenceType: 'linked' }); } @@ -239,7 +252,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a if (result.body) { writeFileSync(file, result.body); - written_files.push(file); + paths.push(dependency_path); } if (response_type === OK) { @@ -317,7 +330,6 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a if (fallback) { const rendered = await app.render( { - host: config.kit.host, method: 'GET', headers: {}, path: '[fallback]', // this doesn't matter, but it's easiest if it's a string @@ -336,8 +348,9 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a const file = join(out, fallback); mkdirp(dirname(file)); writeFileSync(file, rendered.body || ''); - written_files.push(file); } - return written_files; + return { + paths + }; } diff --git a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js index b14e54f7bb12..a59d5d30f0a5 100644 --- a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js +++ b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js @@ -1,8 +1,13 @@ -export const init = () => {}; -export const render = () => ({ - status: 200, - headers: { - 'content-type': 'text/html' - }, - body: '' -}); +export class App { + render() { + return { + status: 200, + headers: { + 'content-type': 'text/html' + }, + body: '' + }; + } +} + +export function override() {} diff --git a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/manifest.js b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/manifest.js new file mode 100644 index 000000000000..570899a6def8 --- /dev/null +++ b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/manifest.js @@ -0,0 +1 @@ +export const manifest = {}; diff --git a/packages/kit/src/core/adapt/test/index.js b/packages/kit/src/core/adapt/test/index.js index 52ac99f98267..2c212dee5055 100644 --- a/packages/kit/src/core/adapt/test/index.js +++ b/packages/kit/src/core/adapt/test/index.js @@ -3,7 +3,7 @@ import { join } from 'path'; import * as uvu from 'uvu'; import * as assert from 'uvu/assert'; import glob from 'tiny-glob/sync.js'; -import { get_utils } from '../utils.js'; +import { create_builder } from '../builder.js'; import { fileURLToPath } from 'url'; import { SVELTE_KIT } from '../../constants.js'; @@ -22,7 +22,7 @@ const log = Object.assign(logger, { success: logger }); -const suite = uvu.suite('adapter utils'); +const suite = uvu.suite('adapter'); suite('copy files', () => { const cwd = join(__dirname, 'fixtures/basic'); @@ -39,9 +39,16 @@ suite('copy files', () => { }; /** @type {import('types/internal').BuildData} */ - const build_data = { client: [], server: [], static: [], entries: [] }; + const build_data = { + // @ts-expect-error + client: {}, + // @ts-expect-error + server: {}, + static: [], + entries: [] + }; - const utils = get_utils({ + const builder = create_builder({ cwd, config: /** @type {import('types/config').ValidatedConfig} */ (mocked), build_data, @@ -51,7 +58,7 @@ suite('copy files', () => { const dest = join(__dirname, 'output'); rmSync(dest, { recursive: true, force: true }); - utils.copy_static_files(dest); + builder.writeStatic(dest); assert.equal( glob('**', { @@ -61,7 +68,7 @@ suite('copy files', () => { ); rmSync(dest, { recursive: true, force: true }); - utils.copy_client_files(dest); + builder.writeClient(dest); assert.equal( glob('**', { cwd: `${cwd}/${SVELTE_KIT}/output/client` }), @@ -69,7 +76,7 @@ suite('copy files', () => { ); rmSync(dest, { recursive: true, force: true }); - utils.copy_server_files(dest); + builder.writeServer(dest); assert.equal( glob('**', { cwd: `${cwd}/${SVELTE_KIT}/output/server` }), @@ -99,9 +106,16 @@ suite('prerender', async () => { }; /** @type {import('types/internal').BuildData} */ - const build_data = { client: [], server: [], static: [], entries: ['/nested'] }; + const build_data = { + // @ts-expect-error + client: { assets: [], chunks: [] }, + // @ts-expect-error + server: { chunks: [] }, + static: [], + entries: ['/nested'] + }; - const utils = get_utils({ + const builder = create_builder({ cwd, config: /** @type {import('types/config').ValidatedConfig} */ (mocked), build_data, @@ -111,7 +125,7 @@ suite('prerender', async () => { const dest = join(__dirname, 'output'); rmSync(dest, { recursive: true, force: true }); - await utils.prerender({ + await builder.prerender({ all: true, dest }); diff --git a/packages/kit/src/core/adapt/utils.js b/packages/kit/src/core/adapt/utils.js deleted file mode 100644 index 764f84309ac0..000000000000 --- a/packages/kit/src/core/adapt/utils.js +++ /dev/null @@ -1,45 +0,0 @@ -import { SVELTE_KIT } from '../constants.js'; -import { copy, rimraf, mkdirp } from '../../utils/filesystem.js'; -import { prerender } from './prerender.js'; - -/** - * @param {{ - * cwd: string; - * config: import('types/config').ValidatedConfig; - * build_data: import('types/internal').BuildData; - * log: import('types/internal').Logger; - * }} opts - * @returns {import('types/config').AdapterUtils} - */ -export function get_utils({ cwd, config, build_data, log }) { - return { - log, - rimraf, - mkdirp, - copy, - - copy_client_files(dest) { - return copy(`${cwd}/${SVELTE_KIT}/output/client`, dest, (file) => file[0] !== '.'); - }, - - copy_server_files(dest) { - return copy(`${cwd}/${SVELTE_KIT}/output/server`, dest, (file) => file[0] !== '.'); - }, - - copy_static_files(dest) { - return copy(config.kit.files.assets, dest); - }, - - async prerender({ all = false, dest, fallback }) { - await prerender({ - out: dest, - all, - cwd, - config, - build_data, - fallback, - log - }); - } - }; -} diff --git a/packages/kit/src/core/build/build_client.js b/packages/kit/src/core/build/build_client.js new file mode 100644 index 000000000000..41e974eaca47 --- /dev/null +++ b/packages/kit/src/core/build/build_client.js @@ -0,0 +1,120 @@ +import fs from 'fs'; +import path from 'path'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { deep_merge } from '../../utils/object.js'; +import { print_config_conflicts } from '../config/index.js'; +import { create_app } from '../create_app/index.js'; +import { copy_assets, posixify } from '../utils.js'; +import { create_build, find_deps } from './utils.js'; + +/** + * @param {{ + * cwd: string; + * assets_base: string; + * config: import('types/config').ValidatedConfig + * manifest_data: import('types/internal').ManifestData + * build_dir: string; + * output_dir: string; + * client_entry_file: string; + * service_worker_entry_file: string | null; + * service_worker_register: boolean; + * }} options + */ +export async function build_client({ + cwd, + assets_base, + config, + manifest_data, + build_dir, + output_dir, + client_entry_file +}) { + create_app({ + manifest_data, + output: build_dir, + cwd + }); + + copy_assets(build_dir); + + process.env.VITE_SVELTEKIT_AMP = config.kit.amp ? 'true' : ''; + + const client_out_dir = `${output_dir}/client/${config.kit.appDir}`; + + /** @type {Record} */ + const input = { + start: path.resolve(cwd, client_entry_file) + }; + + // This step is optional — Vite/Rollup will create the necessary chunks + // for everything regardless — but it means that entry chunks reflect + // their location in the source code, which is helpful for debugging + manifest_data.components.forEach((file) => { + const resolved = path.resolve(cwd, file); + const relative = path.relative(config.kit.files.routes, resolved); + + const name = relative.startsWith('..') + ? path.basename(file) + : posixify(path.join('pages', relative)); + input[name] = resolved; + }); + + /** @type {[any, string[]]} */ + const [merged_config, conflicts] = deep_merge(config.kit.vite(), { + configFile: false, + root: cwd, + base: assets_base, + build: { + cssCodeSplit: true, + manifest: true, + outDir: client_out_dir, + polyfillDynamicImport: false, + rollupOptions: { + input, + output: { + entryFileNames: '[name]-[hash].js', + chunkFileNames: 'chunks/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]' + }, + preserveEntrySignatures: 'strict' + } + }, + resolve: { + alias: { + $app: path.resolve(`${build_dir}/runtime/app`), + $lib: config.kit.files.lib + } + }, + plugins: [ + svelte({ + extensions: config.extensions, + emitCss: !config.kit.amp, + compilerOptions: { + hydratable: !!config.kit.hydrate + } + }) + ] + }); + + print_config_conflicts(conflicts, 'kit.vite.', 'build_client'); + + const { chunks, assets } = await create_build(merged_config); + + /** @type {import('vite').Manifest} */ + const vite_manifest = JSON.parse(fs.readFileSync(`${client_out_dir}/manifest.json`, 'utf-8')); + + const entry_js = new Set(); + const entry_css = new Set(); + find_deps(client_entry_file, vite_manifest, entry_js, entry_css); + + return { + assets, + chunks, + entry: { + file: vite_manifest[client_entry_file].file, + js: Array.from(entry_js), + css: Array.from(entry_css) + }, + vite_manifest + }; +} diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js new file mode 100644 index 000000000000..e38d90851691 --- /dev/null +++ b/packages/kit/src/core/build/build_server.js @@ -0,0 +1,287 @@ +import fs from 'fs'; +import path from 'path'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { deep_merge } from '../../utils/object.js'; +import { print_config_conflicts } from '../config/index.js'; +import { posixify, resolve_entry } from '../utils.js'; +import { create_build } from './utils.js'; +import { SVELTE_KIT } from '../constants.js'; +import { s } from '../../utils/misc.js'; + +/** + * @param {{ + * runtime: string, + * hooks: string, + * config: import('types/config').ValidatedConfig + * }} opts + * @returns + */ +const template = ({ config, hooks, runtime }) => ` +import { respond } from '${runtime}'; +import root from './generated/root.svelte'; +import { set_paths, assets, base } from './runtime/paths.js'; +import { set_prerendering } from './runtime/env.js'; +import * as user_hooks from ${s(hooks)}; + +const template = ({ head, body }) => ${s(fs.readFileSync(config.kit.files.template, 'utf-8')) + .replace('%svelte.head%', '" + head + "') + .replace('%svelte.body%', '" + body + "')}; + +let read = null; + +set_paths(${s(config.kit.paths)}); + +// this looks redundant, but the indirection allows us to access +// named imports without triggering Rollup's missing import detection +const get_hooks = hooks => ({ + getSession: hooks.getSession || (() => ({})), + handle: hooks.handle || (({ request, resolve }) => resolve(request)), + handleError: hooks.handleError || (({ error }) => console.error(error.stack)), + externalFetch: hooks.externalFetch || fetch +}); + +let default_protocol = 'https'; + +// allow paths to be globally overridden +// in svelte-kit preview and in prerendering +export function override(settings) { + default_protocol = settings.protocol || default_protocol; + set_paths(settings.paths); + set_prerendering(settings.prerendering); + read = settings.read; +} + +export class App { + constructor(manifest) { + const hooks = get_hooks(user_hooks); + + this.options = { + amp: ${config.kit.amp}, + dev: false, + floc: ${config.kit.floc}, + get_stack: error => String(error), // for security + handle_error: (error, request) => { + hooks.handleError({ error, request }); + error.stack = this.options.get_stack(error); + }, + hooks, + hydrate: ${s(config.kit.hydrate)}, + manifest, + paths: { base, assets }, + prefix: assets + '/${config.kit.appDir}/', + prerender: ${config.kit.prerender.enabled}, + read, + root, + service_worker: ${ + config.kit.files.serviceWorker && config.kit.serviceWorker.register + ? "'/service-worker.js'" + : 'null' + }, + router: ${s(config.kit.router)}, + ssr: ${s(config.kit.ssr)}, + target: ${s(config.kit.target)}, + template, + trailing_slash: ${s(config.kit.trailingSlash)} + }; + } + + render(request, { + prerender + } = {}) { + const host = ${ + config.kit.host + ? s(config.kit.host) + : `request.headers[${s(config.kit.headers.host || 'host')}]` + }; + const protocol = ${ + config.kit.protocol + ? s(config.kit.protocol) + : config.kit.headers.protocol + ? `request.headers[${s(config.kit.headers.protocol)}] || default_protocol` + : 'default_protocol' + }; + + return respond({ ...request, origin: protocol + '://' + host }, this.options, { prerender }); + } +} +`; + +/** + * @param {{ + * cwd: string; + * assets_base: string; + * config: import('types/config').ValidatedConfig + * manifest_data: import('types/internal').ManifestData + * build_dir: string; + * output_dir: string; + * }} options + * @param {string} runtime + */ +export async function build_server( + { cwd, assets_base, config, manifest_data, build_dir, output_dir }, + runtime +) { + let hooks_file = resolve_entry(config.kit.files.hooks); + if (!hooks_file || !fs.existsSync(hooks_file)) { + hooks_file = path.resolve(cwd, `${SVELTE_KIT}/build/hooks.js`); + fs.writeFileSync(hooks_file, ''); + } + + /** @type {Record} */ + const input = { + app: `${build_dir}/app.js` + }; + + // add entry points for every endpoint... + manifest_data.routes.forEach((route) => { + if (route.type === 'endpoint') { + const resolved = path.resolve(cwd, route.file); + const relative = path.relative(config.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.components.forEach((file) => { + const resolved = path.resolve(cwd, file); + const relative = path.relative(config.kit.files.routes, resolved); + + const name = relative.startsWith('..') + ? posixify(path.join('entries/pages', path.basename(file))) + : posixify(path.join('entries/pages', relative)); + input[name] = resolved; + }); + + /** @type {(file: string) => string} */ + const app_relative = (file) => { + const relative_file = path.relative(build_dir, path.resolve(cwd, file)); + return relative_file[0] === '.' ? relative_file : `./${relative_file}`; + }; + + // prettier-ignore + fs.writeFileSync( + input.app, + template({ + config, + hooks: app_relative(hooks_file), + runtime + }) + ); + + /** @type {import('vite').UserConfig} */ + const vite_config = config.kit.vite(); + + const default_config = { + build: { + target: 'es2020' + } + }; + + // don't warn on overriding defaults + const [modified_vite_config] = deep_merge(default_config, vite_config); + + /** @type {[any, string[]]} */ + const [merged_config, conflicts] = deep_merge(modified_vite_config, { + configFile: false, + root: cwd, + base: assets_base, + build: { + ssr: true, + outDir: `${output_dir}/server`, + manifest: true, + polyfillDynamicImport: false, + rollupOptions: { + input, + output: { + format: 'esm', + entryFileNames: '[name].js', + chunkFileNames: 'chunks/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]' + }, + preserveEntrySignatures: 'strict' + } + }, + plugins: [ + svelte({ + extensions: config.extensions, + compilerOptions: { + hydratable: !!config.kit.hydrate + } + }) + ], + resolve: { + alias: { + $app: path.resolve(`${build_dir}/runtime/app`), + $lib: config.kit.files.lib + } + } + }); + + print_config_conflicts(conflicts, 'kit.vite.', 'build_server'); + + const { chunks } = await create_build(merged_config); + + /** @type {Record} */ + const lookup = {}; + chunks.forEach((chunk) => { + if (!chunk.facadeModuleId) return; + const id = chunk.facadeModuleId.slice(cwd.length + 1); + lookup[id] = chunk.exports; + }); + + /** @type {Record} */ + const methods = {}; + manifest_data.routes.forEach((route) => { + if (route.type === 'endpoint' && lookup[route.file]) { + methods[route.file] = lookup[route.file] + .map((x) => /** @type {import('types/internal').HttpMethod} */ (method_names[x])) + .filter(Boolean); + } + }); + + return { + chunks, + /** @type {import('vite').Manifest} */ + vite_manifest: JSON.parse(fs.readFileSync(`${output_dir}/server/manifest.json`, 'utf-8')), + methods: get_methods(cwd, chunks, manifest_data) + }; +} + +/** @type {Record} */ +const method_names = { + get: 'get', + head: 'head', + post: 'post', + put: 'put', + del: 'delete', + patch: 'patch' +}; + +/** + * + * @param {string} cwd + * @param {import('rollup').OutputChunk[]} output + * @param {import('types/internal').ManifestData} manifest_data + */ +function get_methods(cwd, output, manifest_data) { + /** @type {Record} */ + const lookup = {}; + output.forEach((chunk) => { + if (!chunk.facadeModuleId) return; + const id = chunk.facadeModuleId.slice(cwd.length + 1); + lookup[id] = chunk.exports; + }); + + /** @type {Record} */ + const methods = {}; + manifest_data.routes.forEach((route) => { + if (route.type === 'endpoint' && lookup[route.file]) { + methods[route.file] = lookup[route.file] + .map((x) => /** @type {import('types/internal').HttpMethod} */ (method_names[x])) + .filter(Boolean); + } + }); + + return methods; +} diff --git a/packages/kit/src/core/build/build_service_worker.js b/packages/kit/src/core/build/build_service_worker.js new file mode 100644 index 000000000000..d19744b1d076 --- /dev/null +++ b/packages/kit/src/core/build/build_service_worker.js @@ -0,0 +1,88 @@ +import fs from 'fs'; +import path from 'path'; +import vite from 'vite'; +import { s } from '../../utils/misc.js'; +import { deep_merge } from '../../utils/object.js'; +import { print_config_conflicts } from '../config/index.js'; + +/** + * @param {{ + * cwd: string; + * assets_base: string; + * config: import('types/config').ValidatedConfig + * manifest_data: import('types/internal').ManifestData + * build_dir: string; + * output_dir: string; + * client_entry_file: string; + * service_worker_entry_file: string | null; + * }} options + * @param {import('vite').Manifest} client_manifest + */ +export async function build_service_worker( + { cwd, assets_base, config, manifest_data, build_dir, output_dir, service_worker_entry_file }, + client_manifest +) { + // TODO add any assets referenced in template .html file, e.g. favicon? + const app_files = new Set(); + for (const key in client_manifest) { + const { file, css } = client_manifest[key]; + app_files.add(file); + if (css) { + css.forEach((file) => { + app_files.add(file); + }); + } + } + + fs.writeFileSync( + `${build_dir}/runtime/service-worker.js`, + ` + export const timestamp = ${Date.now()}; + + export const build = [ + ${Array.from(app_files) + .map((file) => `${s(`${config.kit.paths.base}/${config.kit.appDir}/${file}`)}`) + .join(',\n\t\t\t\t')} + ]; + + export const files = [ + ${manifest_data.assets + .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`) + .join(',\n\t\t\t\t')} + ]; + ` + .replace(/^\t{3}/gm, '') + .trim() + ); + + /** @type {[any, string[]]} */ + const [merged_config, conflicts] = deep_merge(config.kit.vite(), { + configFile: false, + root: cwd, + base: assets_base, + build: { + lib: { + entry: service_worker_entry_file, + name: 'app', + formats: ['es'] + }, + rollupOptions: { + output: { + entryFileNames: 'service-worker.js' + } + }, + outDir: `${output_dir}/client`, + emptyOutDir: false + }, + resolve: { + alias: { + '$service-worker': path.resolve(`${build_dir}/runtime/service-worker`), + $lib: config.kit.files.lib + } + } + }); + + print_config_conflicts(conflicts, 'kit.vite.', 'build_service_worker'); + + await vite.build(merged_config); +} diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index 0b8c67b22d03..ec1bd357770d 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -1,21 +1,15 @@ -import fs from 'fs'; +import fs, { writeFileSync } from 'fs'; import path from 'path'; - -import { svelte } from '@sveltejs/vite-plugin-svelte'; -import glob from 'tiny-glob/sync.js'; -import vite from 'vite'; - -import { rimraf } from '../../utils/filesystem.js'; -import { deep_merge } from '../../utils/object.js'; - -import { print_config_conflicts } from '../config/index.js'; -import { create_app } from '../create_app/index.js'; +import { mkdirp, rimraf } from '../../utils/filesystem.js'; import create_manifest_data from '../create_manifest_data/index.js'; import { SVELTE_KIT } from '../constants.js'; -import { copy_assets, posixify, resolve_entry } from '../utils.js'; - -/** @param {any} value */ -const s = (value) => JSON.stringify(value); +import { posixify, resolve_entry } from '../utils.js'; +import { generate_manifest } from '../generate_manifest/index.js'; +import { s } from '../../utils/misc.js'; +import { build_service_worker } from './build_service_worker.js'; +import { build_client } from './build_client.js'; +import { build_server } from './build_server.js'; +import { find_deps } from './utils.js'; /** * @param {import('types/config').ValidatedConfig} config @@ -41,7 +35,7 @@ export async function build(config, { cwd = process.cwd(), runtime = '@sveltejs/ // used relative paths, I _think_ this could get fixed. Issue here: // /~https://github.com/vitejs/vite/issues/2009 assets_base: `${config.kit.paths.assets || config.kit.paths.base}/${config.kit.appDir}/`, - manifest: create_manifest_data({ + manifest_data: create_manifest_data({ config, output: build_dir, cwd @@ -52,546 +46,60 @@ export async function build(config, { cwd = process.cwd(), runtime = '@sveltejs/ service_worker_register: config.kit.serviceWorker.register }; - const client_manifest = await build_client(options); - await build_server(options, client_manifest, runtime); - - if (options.service_worker_entry_file) { - if (config.kit.paths.assets) { - throw new Error('Cannot use service worker alongside config.kit.paths.assets'); - } - - await build_service_worker(options, client_manifest); - } - - const client = glob('**', { cwd: `${output_dir}/client`, filesOnly: true }).map(posixify); - const server = glob('**', { cwd: `${output_dir}/server`, filesOnly: true }).map(posixify); - - return { - client, - server, - static: options.manifest.assets.map((asset) => posixify(asset.file)), - entries: options.manifest.routes - .map((route) => (route.type === 'page' ? route.path : '')) - .filter(Boolean) - }; -} - -/** - * @param {{ - * cwd: string; - * assets_base: string; - * config: import('types/config').ValidatedConfig - * manifest: import('types/internal').ManifestData - * build_dir: string; - * output_dir: string; - * client_entry_file: string; - * service_worker_entry_file: string | null; - * service_worker_register: boolean; - * }} options - */ -async function build_client({ - cwd, - assets_base, - config, - manifest, - build_dir, - output_dir, - client_entry_file -}) { - create_app({ - manifest_data: manifest, - output: build_dir, - cwd - }); - - copy_assets(build_dir); - - process.env.VITE_SVELTEKIT_AMP = config.kit.amp ? 'true' : ''; - - const client_out_dir = `${output_dir}/client/${config.kit.appDir}`; - - /** @type {Record} */ - const input = { - start: path.resolve(cwd, client_entry_file) - }; - - // This step is optional — Vite/Rollup will create the necessary chunks - // for everything regardless — but it means that entry chunks reflect - // their location in the source code, which is helpful for debugging - manifest.components.forEach((file) => { - const resolved = path.resolve(cwd, file); - const relative = path.relative(config.kit.files.routes, resolved); + const client = await build_client(options); + const server = await build_server(options, runtime); - const name = relative.startsWith('..') - ? path.basename(file) - : posixify(path.join('pages', relative)); - input[name] = resolved; - }); - - /** @type {import('vite').UserConfig} */ - const vite_config = config.kit.vite(); - - const default_config = {}; - - // don't warn on overriding defaults - const [modified_vite_config] = deep_merge(default_config, vite_config); - - /** @type {[any, string[]]} */ - const [merged_config, conflicts] = deep_merge(modified_vite_config, { - configFile: false, - root: cwd, - base: assets_base, - build: { - cssCodeSplit: true, - manifest: true, - outDir: client_out_dir, - polyfillDynamicImport: false, - rollupOptions: { - input, - output: { - entryFileNames: '[name]-[hash].js', - chunkFileNames: 'chunks/[name]-[hash].js', - assetFileNames: 'assets/[name]-[hash][extname]' - }, - preserveEntrySignatures: 'strict' - } - }, - resolve: { - alias: { - $app: path.resolve(`${build_dir}/runtime/app`), - $lib: config.kit.files.lib + const styles_lookup = new Map(); + if (options.config.kit.amp) { + client.assets.forEach((asset) => { + if (asset.fileName.endsWith('.css')) { + styles_lookup.set(asset.fileName, asset.source); } - }, - plugins: [ - svelte({ - extensions: config.extensions, - emitCss: !config.kit.amp, - compilerOptions: { - hydratable: !!config.kit.hydrate - } - }) - ] - }); - - print_config_conflicts(conflicts, 'kit.vite.', 'build_client'); - - await vite.build(merged_config); - - const client_manifest_file = `${client_out_dir}/manifest.json`; - /** @type {import('vite').Manifest} */ - const client_manifest = JSON.parse(fs.readFileSync(client_manifest_file, 'utf-8')); - fs.renameSync(client_manifest_file, `${output_dir}/manifest.json`); // inspectable but not shipped - - return client_manifest; -} - -/** - * @param {{ - * cwd: string; - * assets_base: string; - * config: import('types/config').ValidatedConfig - * manifest: import('types/internal').ManifestData - * build_dir: string; - * output_dir: string; - * client_entry_file: string; - * service_worker_entry_file: string | null; - * service_worker_register: boolean; - * }} options - * @param {import('vite').Manifest} client_manifest - * @param {string} runtime - */ -async function build_server( - { - cwd, - assets_base, - config, - manifest, - build_dir, - output_dir, - client_entry_file, - service_worker_entry_file, - service_worker_register - }, - client_manifest, - runtime -) { - let hooks_file = resolve_entry(config.kit.files.hooks); - if (!hooks_file || !fs.existsSync(hooks_file)) { - hooks_file = path.resolve(cwd, `${SVELTE_KIT}/build/hooks.js`); - fs.writeFileSync(hooks_file, ''); - } - - const app_file = `${build_dir}/app.js`; - - /** @type {(file: string) => string} */ - const app_relative = (file) => { - const relative_file = path.relative(build_dir, path.resolve(cwd, file)); - return relative_file[0] === '.' ? relative_file : `./${relative_file}`; - }; - - const prefix = `/${config.kit.appDir}/`; - - /** - * @param {string} file - * @param {Set} js_deps - * @param {Set} css_deps - */ - function find_deps(file, js_deps, css_deps) { - const chunk = client_manifest[file]; - - if (js_deps.has(chunk.file)) return; - js_deps.add(chunk.file); - - if (chunk.css) { - chunk.css.forEach((file) => css_deps.add(file)); - } - - if (chunk.imports) { - chunk.imports.forEach((file) => find_deps(file, js_deps, css_deps)); - } + }); } - /** @type {Record} */ - const metadata_lookup = {}; + mkdirp(`${output_dir}/server/nodes`); + options.manifest_data.components.forEach((component, i) => { + const file = `${output_dir}/server/nodes/${i}.js`; - manifest.components.forEach((file) => { - const js_deps = new Set(); - const css_deps = new Set(); + const js = new Set(); + const css = new Set(); + find_deps(component, client.vite_manifest, js, css); - find_deps(file, js_deps, css_deps); + const styles = config.kit.amp && Array.from(css).map((file) => styles_lookup.get(file)); - const js = Array.from(js_deps); - const css = Array.from(css_deps); + const node = `import * as module from '../${server.vite_manifest[component].file}'; + export { module }; + export const entry = '${client.vite_manifest[component].file}'; + export const js = ${JSON.stringify(Array.from(js))}; + export const css = ${JSON.stringify(Array.from(css))}; + ${styles ? `export const styles = ${s(styles)}` : ''} + `.replace(/^\t\t\t/gm, ''); - const styles = config.kit.amp - ? Array.from(css_deps).map((url) => { - const resolved = `${output_dir}/client/${config.kit.appDir}/${url}`; - return fs.readFileSync(resolved, 'utf-8'); - }) - : []; - - metadata_lookup[file] = { - entry: client_manifest[file].file, - css, - js, - styles - }; + writeFileSync(file, node); }); - /** @type {Set} */ - const entry_js = new Set(); - /** @type {Set} */ - const entry_css = new Set(); - - find_deps(client_entry_file, entry_js, entry_css); - - // prettier-ignore - fs.writeFileSync( - app_file, - ` - import { respond } from '${runtime}'; - import root from './generated/root.svelte'; - import { set_paths, assets } from './runtime/paths.js'; - import { set_prerendering } from './runtime/env.js'; - import * as user_hooks from ${s(app_relative(hooks_file))}; - - const template = ({ head, body }) => ${s(fs.readFileSync(config.kit.files.template, 'utf-8')) - .replace('%svelte.head%', '" + head + "') - .replace('%svelte.body%', '" + body + "')}; - - let options = null; - - const default_settings = { paths: ${s(config.kit.paths)} }; - - // allow paths to be overridden in svelte-kit preview - // and in prerendering - export function init(settings = default_settings) { - set_paths(settings.paths); - set_prerendering(settings.prerendering || false); - - const hooks = get_hooks(user_hooks); - - options = { - amp: ${config.kit.amp}, - dev: false, - entry: { - file: assets + ${s(prefix + client_manifest[client_entry_file].file)}, - css: [${Array.from(entry_css).map(dep => 'assets + ' + s(prefix + dep))}], - js: [${Array.from(entry_js).map(dep => 'assets + ' + s(prefix + dep))}] - }, - fetched: undefined, - floc: ${config.kit.floc}, - get_component_path: id => assets + ${s(prefix)} + entry_lookup[id], - get_stack: error => String(error), // for security - handle_error: (error, request) => { - hooks.handleError({ error, request }); - error.stack = options.get_stack(error); - }, - hooks, - hydrate: ${s(config.kit.hydrate)}, - initiator: undefined, - load_component, - manifest, - paths: settings.paths, - prerender: ${config.kit.prerender.enabled}, - read: settings.read, - root, - service_worker: ${service_worker_entry_file && service_worker_register ? "'/service-worker.js'" : 'null'}, - router: ${s(config.kit.router)}, - ssr: ${s(config.kit.ssr)}, - target: ${s(config.kit.target)}, - template, - trailing_slash: ${s(config.kit.trailingSlash)} - }; - } - - // input has already been decoded by decodeURI - // now handle the rest that decodeURIComponent would do - const d = s => s - .replace(/%23/g, '#') - .replace(/%3[Bb]/g, ';') - .replace(/%2[Cc]/g, ',') - .replace(/%2[Ff]/g, '/') - .replace(/%3[Ff]/g, '?') - .replace(/%3[Aa]/g, ':') - .replace(/%40/g, '@') - .replace(/%26/g, '&') - .replace(/%3[Dd]/g, '=') - .replace(/%2[Bb]/g, '+') - .replace(/%24/g, '$'); - - const empty = () => ({}); - - const manifest = { - assets: ${s(manifest.assets)}, - layout: ${s(manifest.layout)}, - error: ${s(manifest.error)}, - routes: [ - ${manifest.routes - .map((route) => { - if (route.type === 'page') { - const params = get_params(route.params); - - return `{ - type: 'page', - pattern: ${route.pattern}, - params: ${params}, - a: [${route.a.map(file => file && s(file)).join(', ')}], - b: [${route.b.map(file => file && s(file)).join(', ')}] - }`; - } else { - const params = get_params(route.params); - const load = `() => import(${s(app_relative(route.file))})`; - - return `{ - type: 'endpoint', - pattern: ${route.pattern}, - params: ${params}, - load: ${load} - }`; - } - }) - .join(',\n\t\t\t\t\t')} - ] - }; - - // this looks redundant, but the indirection allows us to access - // named imports without triggering Rollup's missing import detection - const get_hooks = hooks => ({ - getSession: hooks.getSession || (() => ({})), - handle: hooks.handle || (({ request, resolve }) => resolve(request)), - handleError: hooks.handleError || (({ error }) => console.error(error.stack)), - externalFetch: hooks.externalFetch || fetch - }); - - const module_lookup = { - ${manifest.components.map(file => `${s(file)}: () => import(${s(app_relative(file))})`)} - }; - - const metadata_lookup = ${s(metadata_lookup)}; - - async function load_component(file) { - const { entry, css, js, styles } = metadata_lookup[file]; - return { - module: await module_lookup[file](), - entry: assets + ${s(prefix)} + entry, - css: css.map(dep => assets + ${s(prefix)} + dep), - js: js.map(dep => assets + ${s(prefix)} + dep), - styles - }; - } - - export function render(request, { - prerender - } = {}) { - const host = ${config.kit.host ? s(config.kit.host) : `request.headers[${s(config.kit.hostHeader || 'host')}]`}; - return respond({ ...request, host }, options, { prerender }); - } - ` - .replace(/^\t{3}/gm, '') - .trim() - ); - - /** @type {import('vite').UserConfig} */ - const vite_config = config.kit.vite(); - - const default_config = { - build: { - target: 'es2020' - } - }; - - // don't warn on overriding defaults - const [modified_vite_config] = deep_merge(default_config, vite_config); - - /** @type {[any, string[]]} */ - const [merged_config, conflicts] = deep_merge(modified_vite_config, { - configFile: false, - root: cwd, - base: assets_base, - build: { - ssr: true, - outDir: `${output_dir}/server`, - polyfillDynamicImport: false, - rollupOptions: { - input: { - app: app_file - }, - output: { - format: 'esm', - entryFileNames: '[name].js', - chunkFileNames: 'chunks/[name]-[hash].js', - assetFileNames: 'assets/[name]-[hash][extname]' - }, - preserveEntrySignatures: 'strict' - } - }, - plugins: [ - svelte({ - extensions: config.extensions, - compilerOptions: { - hydratable: !!config.kit.hydrate - } - }) - ], - resolve: { - alias: { - $app: path.resolve(`${build_dir}/runtime/app`), - $lib: config.kit.files.lib - } + if (options.service_worker_entry_file) { + if (config.kit.paths.assets) { + throw new Error('Cannot use service worker alongside config.kit.paths.assets'); } - }); - print_config_conflicts(conflicts, 'kit.vite.', 'build_server'); - - await vite.build(merged_config); -} - -/** - * @param {{ - * cwd: string; - * assets_base: string; - * config: import('types/config').ValidatedConfig - * manifest: import('types/internal').ManifestData - * build_dir: string; - * output_dir: string; - * client_entry_file: string; - * service_worker_entry_file: string | null; - * service_worker_register: boolean; - * }} options - * @param {import('vite').Manifest} client_manifest - */ -async function build_service_worker( - { cwd, assets_base, config, manifest, build_dir, output_dir, service_worker_entry_file }, - client_manifest -) { - // TODO add any assets referenced in template .html file, e.g. favicon? - const app_files = new Set(); - for (const key in client_manifest) { - const { file, css } = client_manifest[key]; - app_files.add(file); - if (css) { - css.forEach((file) => { - app_files.add(file); - }); - } + await build_service_worker(options, client.vite_manifest); } - fs.writeFileSync( - `${build_dir}/runtime/service-worker.js`, - ` - export const timestamp = ${Date.now()}; - - export const build = [ - ${Array.from(app_files) - .map((file) => `${s(`${config.kit.paths.base}/${config.kit.appDir}/${file}`)}`) - .join(',\n\t\t\t\t')} - ]; - - export const files = [ - ${manifest.assets - .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`) - .join(',\n\t\t\t\t')} - ]; - ` - .replace(/^\t{3}/gm, '') - .trim() - ); - - /** @type {import('vite').UserConfig} */ - const vite_config = config.kit.vite(); - - const default_config = {}; - - // don't warn on overriding defaults - const [modified_vite_config] = deep_merge(default_config, vite_config); - - /** @type {[any, string[]]} */ - const [merged_config, conflicts] = deep_merge(modified_vite_config, { - configFile: false, - root: cwd, - base: assets_base, - build: { - lib: { - entry: service_worker_entry_file, - name: 'app', - formats: ['es'] - }, - rollupOptions: { - output: { - entryFileNames: 'service-worker.js' - } - }, - outDir: `${output_dir}/client`, - emptyOutDir: false - }, - resolve: { - alias: { - '$service-worker': path.resolve(`${build_dir}/runtime/service-worker`), - $lib: config.kit.files.lib - } - } - }); - - print_config_conflicts(conflicts, 'kit.vite.', 'build_service_worker'); + const build_data = { + app_dir: config.kit.appDir, + manifest_data: options.manifest_data, + client, + server, + static: options.manifest_data.assets.map((asset) => posixify(asset.file)), + entries: options.manifest_data.routes + .map((route) => (route.type === 'page' ? route.path : '')) + .filter(Boolean) + }; - await vite.build(merged_config); -} + const manifest = `export const manifest = ${generate_manifest(build_data, '.')};\n`; + fs.writeFileSync(`${output_dir}/server/manifest.js`, manifest); -/** @param {string[]} array */ -function get_params(array) { - // given an array of params like `['x', 'y', 'z']` for - // src/routes/[x]/[y]/[z]/svelte, create a function - // that turns a RexExpMatchArray into ({ x, y, z }) - return array.length - ? '(m) => ({ ' + - array - .map((param, i) => { - return param.startsWith('...') - ? `${param.slice(3)}: d(m[${i + 1}] || '')` - : `${param}: d(m[${i + 1}])`; - }) - .join(', ') + - '})' - : 'empty'; + return build_data; } diff --git a/packages/kit/src/core/build/utils.js b/packages/kit/src/core/build/utils.js new file mode 100644 index 000000000000..704a159c9c8b --- /dev/null +++ b/packages/kit/src/core/build/utils.js @@ -0,0 +1,38 @@ +import vite from 'vite'; + +/** @param {import('vite').UserConfig} config */ +export async function create_build(config) { + const { output } = /** @type {import('rollup').RollupOutput} */ (await vite.build(config)); + + const chunks = /** @type {import('rollup').OutputChunk[]} */ ( + output.filter((output) => output.type === 'chunk') + ); + + const assets = /** @type {import('rollup').OutputAsset[]} */ ( + output.filter((output) => output.type === 'asset') + ); + + return { chunks, assets }; +} + +/** + * @param {string} file + * @param {import('vite').Manifest} manifest + * @param {Set} css + * @param {Set} js + * @returns + */ +export function find_deps(file, manifest, js, css) { + const chunk = manifest[file]; + + if (js.has(chunk.file)) return; + js.add(chunk.file); + + if (chunk.css) { + chunk.css.forEach((file) => css.add(file)); + } + + if (chunk.imports) { + chunk.imports.forEach((file) => find_deps(file, manifest, js, css)); + } +} diff --git a/packages/kit/src/core/config/index.js b/packages/kit/src/core/config/index.js index 6af9d9b348af..4f23fec7ca66 100644 --- a/packages/kit/src/core/config/index.js +++ b/packages/kit/src/core/config/index.js @@ -4,8 +4,6 @@ import * as url from 'url'; import { logger } from '../utils.js'; import options from './options.js'; -/** @typedef {import('./types').ConfigDefinition} ConfigDefinition */ - /** * @param {string} cwd * @param {import('types/config').ValidatedConfig} validated diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 8a81ecc22739..a5d25714fa4f 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -29,8 +29,11 @@ test('fills in defaults', () => { template: 'src/app.html' }, floc: false, + headers: { + host: null, + protocol: null + }, host: null, - hostHeader: null, hydrate: true, package: { dir: 'package', @@ -52,6 +55,7 @@ test('fills in defaults', () => { onError: 'fail', pages: undefined }, + protocol: null, router: true, ssr: true, target: null, @@ -132,8 +136,11 @@ test('fills in partial blanks', () => { template: 'src/app.html' }, floc: false, + headers: { + host: null, + protocol: null + }, host: null, - hostHeader: null, hydrate: true, package: { dir: 'package', @@ -155,6 +162,7 @@ test('fills in partial blanks', () => { onError: 'fail', pages: undefined }, + protocol: null, router: true, ssr: true, target: null, diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 30dce7eed9e3..ac6c82d2731d 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -1,4 +1,3 @@ -/** @typedef {import('./types').ConfigDefinition} ConfigDefinition */ /** @typedef {import('./types').Validator} Validator */ /** @type {Validator} */ @@ -67,9 +66,12 @@ const options = object( floc: boolean(false), - host: string(null), + headers: object({ + host: string(null), + protocol: string(null) + }), - hostHeader: string(null), + host: string(null), hydrate: boolean(true), @@ -160,6 +162,8 @@ const options = object( }) }), + protocol: string(null), + router: boolean(true), serviceWorker: object({ diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index c01305ccb8be..79ecf067dd38 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -34,8 +34,11 @@ async function testLoadDefaultConfig(path) { template: join(cwd, 'src/app.html') }, floc: false, + headers: { + host: null, + protocol: null + }, host: null, - hostHeader: null, hydrate: true, package: { dir: 'package', @@ -54,6 +57,7 @@ async function testLoadDefaultConfig(path) { onError: 'fail', pages: undefined }, + protocol: null, router: true, ssr: true, target: null, diff --git a/packages/kit/src/core/config/types.d.ts b/packages/kit/src/core/config/types.d.ts index c0c3f2178902..00d005c32886 100644 --- a/packages/kit/src/core/config/types.d.ts +++ b/packages/kit/src/core/config/types.d.ts @@ -1,12 +1 @@ -export type ConfigDefinition = - | { - type: 'leaf'; - fallback: any; - validate(value: any, keypath: string): any; - } - | { - type: 'branch'; - children: Record; - }; - export type Validator = (input: T, keypath: string) => T; diff --git a/packages/kit/src/core/create_app/index.js b/packages/kit/src/core/create_app/index.js index 62d43217b29d..ec4ec77dc918 100644 --- a/packages/kit/src/core/create_app/index.js +++ b/packages/kit/src/core/create_app/index.js @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import { s } from '../../utils/misc.js'; import { mkdirp } from '../../utils/filesystem.js'; /** @type {Map} */ @@ -17,8 +18,6 @@ export function write_if_changed(file, code) { } } -const s = JSON.stringify; - /** @typedef {import('types/internal').ManifestData} ManifestData */ /** diff --git a/packages/kit/src/core/create_manifest_data/index.js b/packages/kit/src/core/create_manifest_data/index.js index fe7422bc1bae..04912a23109b 100644 --- a/packages/kit/src/core/create_manifest_data/index.js +++ b/packages/kit/src/core/create_manifest_data/index.js @@ -9,7 +9,7 @@ import { posixify } from '../utils.js'; * @typedef {{ * content: string; * dynamic: boolean; - * spread: boolean; + * rest: boolean; * }} Part * @typedef {{ * basename: string; @@ -137,13 +137,13 @@ export default function create_manifest_data({ config, output, cwd = process.cwd if (last_part.dynamic) { last_segment.push({ dynamic: false, - spread: false, + rest: false, content: item.route_suffix }); } else { last_segment[last_segment.length - 1] = { dynamic: false, - spread: false, + rest: false, content: `${last_part.content}${item.route_suffix}` }; } @@ -160,6 +160,18 @@ export default function create_manifest_data({ config, output, cwd = process.cwd const params = parent_params.slice(); params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content)); + // TODO seems slightly backwards to derive the simple segment representation + // from the more complex form, rather than vice versa — maybe swap it round + const simple_segments = segments.map((segment) => { + return { + dynamic: segment.some((part) => part.dynamic), + rest: segment.some((part) => part.rest), + content: segment + .map((part) => (part.dynamic ? `[${part.content}]` : part.content)) + .join('') + }; + }); + if (item.is_dir) { const layout_reset = find_layout('__layout.reset', item.file); const layout = find_layout('__layout', item.file); @@ -209,6 +221,7 @@ export default function create_manifest_data({ config, output, cwd = process.cwd routes.push({ type: 'page', + segments: simple_segments, pattern, params, path, @@ -220,6 +233,7 @@ export default function create_manifest_data({ config, output, cwd = process.cwd routes.push({ type: 'endpoint', + segments: simple_segments, pattern, file: item.file, params @@ -288,14 +302,13 @@ function comparator(a, b) { if (!a_sub_part) return 1; // b is more specific, so goes first if (!b_sub_part) return -1; - // if spread, order later - if (a_sub_part.spread && b_sub_part.spread) { + if (a_sub_part.rest && b_sub_part.rest) { // sort alphabetically return a_sub_part.content < b_sub_part.content ? -1 : 1; } - // If one is ...spread order it later - if (a_sub_part.spread !== b_sub_part.spread) return a_sub_part.spread ? 1 : -1; + // If one is ...rest order it later + if (a_sub_part.rest !== b_sub_part.rest) return a_sub_part.rest ? 1 : -1; if (a_sub_part.dynamic !== b_sub_part.dynamic) { return a_sub_part.dynamic ? 1 : -1; @@ -337,7 +350,7 @@ function get_parts(part, file) { result.push({ content, dynamic, - spread: dynamic && /^\.{3}.+$/.test(content) + rest: dynamic && /^\.{3}.+$/.test(content) }); }); @@ -351,7 +364,7 @@ function get_parts(part, file) { function get_pattern(segments, add_trailing_slash) { const path = segments .map((segment) => { - return segment[0].spread + return segment[0].rest ? '(?:\\/(.*))?' : '\\/' + segment diff --git a/packages/kit/src/core/create_manifest_data/index.spec.js b/packages/kit/src/core/create_manifest_data/index.spec.js index df956becc14e..02415088d089 100644 --- a/packages/kit/src/core/create_manifest_data/index.spec.js +++ b/packages/kit/src/core/create_manifest_data/index.spec.js @@ -50,6 +50,7 @@ test('creates routes', () => { assert.equal(routes, [ { type: 'page', + segments: [], pattern: /^\/$/, params: [], path: '/', @@ -59,6 +60,7 @@ test('creates routes', () => { { type: 'page', + segments: [{ rest: false, dynamic: false, content: 'about' }], pattern: /^\/about\/?$/, params: [], path: '/about', @@ -68,6 +70,7 @@ test('creates routes', () => { { type: 'endpoint', + segments: [{ rest: false, dynamic: false, content: 'blog.json' }], pattern: /^\/blog\.json$/, file: 'samples/basic/blog/index.json.js', params: [] @@ -75,6 +78,7 @@ test('creates routes', () => { { type: 'page', + segments: [{ rest: false, dynamic: false, content: 'blog' }], pattern: /^\/blog\/?$/, params: [], path: '/blog', @@ -84,6 +88,10 @@ test('creates routes', () => { { type: 'endpoint', + segments: [ + { rest: false, dynamic: false, content: 'blog' }, + { rest: false, dynamic: true, content: '[slug].json' } + ], pattern: /^\/blog\/([^/]+?)\.json$/, file: 'samples/basic/blog/[slug].json.ts', params: ['slug'] @@ -91,6 +99,10 @@ test('creates routes', () => { { type: 'page', + segments: [ + { rest: false, dynamic: false, content: 'blog' }, + { rest: false, dynamic: true, content: '[slug]' } + ], pattern: /^\/blog\/([^/]+?)\/?$/, params: ['slug'], path: '', @@ -114,6 +126,7 @@ test('creates routes with layout', () => { assert.equal(routes, [ { type: 'page', + segments: [], pattern: /^\/$/, params: [], path: '/', @@ -123,6 +136,7 @@ test('creates routes with layout', () => { { type: 'page', + segments: [{ rest: false, dynamic: false, content: 'foo' }], pattern: /^\/foo\/?$/, params: [], path: '/foo', @@ -225,6 +239,7 @@ test('allows multiple slugs', () => { [ { type: 'endpoint', + segments: [{ dynamic: true, rest: false, content: '[file].[ext]' }], pattern: /^\/([^/]+?)\.([^/]+?)$/, file: 'samples/multiple-slugs/[file].[ext].js', params: ['file', 'ext'] @@ -245,6 +260,7 @@ test('ignores things that look like lockfiles', () => { assert.equal(routes, [ { type: 'endpoint', + segments: [{ rest: false, dynamic: false, content: 'foo' }], file: 'samples/lockfiles/foo.js', params: [], pattern: /^\/foo\/?$/ @@ -270,6 +286,7 @@ test('works with custom extensions', () => { assert.equal(routes, [ { type: 'page', + segments: [], pattern: /^\/$/, params: [], path: '/', @@ -279,6 +296,7 @@ test('works with custom extensions', () => { { type: 'page', + segments: [{ rest: false, dynamic: false, content: 'about' }], pattern: /^\/about\/?$/, params: [], path: '/about', @@ -288,6 +306,7 @@ test('works with custom extensions', () => { { type: 'endpoint', + segments: [{ rest: false, dynamic: false, content: 'blog.json' }], pattern: /^\/blog\.json$/, file: 'samples/custom-extension/blog/index.json.js', params: [] @@ -295,6 +314,7 @@ test('works with custom extensions', () => { { type: 'page', + segments: [{ rest: false, dynamic: false, content: 'blog' }], pattern: /^\/blog\/?$/, params: [], path: '/blog', @@ -304,6 +324,10 @@ test('works with custom extensions', () => { { type: 'endpoint', + segments: [ + { rest: false, dynamic: false, content: 'blog' }, + { rest: false, dynamic: true, content: '[slug].json' } + ], pattern: /^\/blog\/([^/]+?)\.json$/, file: 'samples/custom-extension/blog/[slug].json.js', params: ['slug'] @@ -311,6 +335,10 @@ test('works with custom extensions', () => { { type: 'page', + segments: [ + { rest: false, dynamic: false, content: 'blog' }, + { rest: false, dynamic: true, content: '[slug]' } + ], pattern: /^\/blog\/([^/]+?)\/?$/, params: ['slug'], path: '', @@ -343,6 +371,11 @@ test('includes nested error components', () => { assert.equal(routes, [ { type: 'page', + segments: [ + { rest: false, dynamic: false, content: 'foo' }, + { rest: false, dynamic: false, content: 'bar' }, + { rest: false, dynamic: false, content: 'baz' } + ], pattern: /^\/foo\/bar\/baz\/?$/, params: [], path: '/foo/bar/baz', @@ -369,6 +402,7 @@ test('resets layout', () => { assert.equal(routes, [ { type: 'page', + segments: [], pattern: /^\/$/, params: [], path: '/', @@ -377,6 +411,7 @@ test('resets layout', () => { }, { type: 'page', + segments: [{ rest: false, dynamic: false, content: 'foo' }], pattern: /^\/foo\/?$/, params: [], path: '/foo', @@ -389,6 +424,10 @@ test('resets layout', () => { }, { type: 'page', + segments: [ + { rest: false, dynamic: false, content: 'foo' }, + { rest: false, dynamic: false, content: 'bar' } + ], pattern: /^\/foo\/bar\/?$/, params: [], path: '/foo/bar', diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index f1846e03d395..049288e5eb84 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -19,7 +19,7 @@ import { create_app } from '../create_app/index.js'; import create_manifest_data from '../create_manifest_data/index.js'; import { getRawBody } from '../node/index.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; -import { copy_assets, resolve_entry } from '../utils.js'; +import { copy_assets, get_mime_lookup, resolve_entry } from '../utils.js'; import { coalesce_to_error } from '../../utils/error.js'; /** @typedef {{ cwd?: string, port: number, host?: string, https: boolean, config: import('types/config').ValidatedConfig }} Options */ @@ -99,7 +99,7 @@ class Watcher extends EventEmitter { this.config.kit.files.lib, this.config.kit.files.routes, path.resolve(this.cwd, 'src'), - path.resolve(this.cwd, '.svelte-kit'), + path.resolve(this.cwd, SVELTE_KIT), path.resolve(this.cwd, 'node_modules'), path.resolve(vite.searchForWorkspaceRoot(this.cwd), 'node_modules') ]) @@ -124,7 +124,7 @@ class Watcher extends EventEmitter { // don't warn on overriding defaults const [modified_vite_config] = deep_merge(default_config, vite_config); - const kit_plugin = await create_plugin(this.config, this.dir, this.cwd, () => { + const kit_plugin = await create_plugin(this.config, this.dir, this.https, () => { if (!this.manifest) { throw new Error('Manifest is not available'); } @@ -196,33 +196,85 @@ class Watcher extends EventEmitter { cwd: this.cwd }); - /** @type {import('types/internal').SSRManifest} */ + /** @type {import('types/app').SSRManifest} */ this.manifest = { - assets: manifest_data.assets, - layout: manifest_data.layout, - error: manifest_data.error, - routes: manifest_data.routes.map((route) => { - if (route.type === 'page') { + appDir: this.config.kit.appDir, + assets: new Set(manifest_data.assets.map((asset) => asset.file)), + _: { + mime: get_mime_lookup(manifest_data), + entry: { + file: `/${SVELTE_KIT}/dev/runtime/internal/start.js`, + css: [], + js: [] + }, + nodes: manifest_data.components.map((id) => { + return async () => { + const url = `/${id}`; + + if (!this.vite) throw new Error('Vite server has not been initialized'); + + const module = /** @type {SSRComponent} */ (await this.vite.ssrLoadModule(url)); + const node = await this.vite.moduleGraph.getModuleByUrl(url); + + if (!node) throw new Error(`Could not find node for ${url}`); + + const deps = new Set(); + find_deps(node, deps); + + const styles = new Set(); + + for (const dep of deps) { + const parsed = new URL(dep.url, 'http://localhost/'); + const query = parsed.searchParams; + + // TODO what about .scss files, etc? + if ( + dep.file.endsWith('.css') || + (query.has('svelte') && query.get('type') === 'style') + ) { + try { + const mod = await this.vite.ssrLoadModule(dep.url); + styles.add(mod.default); + } catch { + // this can happen with dynamically imported modules, I think + // because the Vite module graph doesn't distinguish between + // static and dynamic imports? TODO investigate, submit fix + } + } + } + + return { + module, + entry: url.endsWith('.svelte') ? url : url + '?import', + css: [], + js: [], + styles: Array.from(styles) + }; + }; + }), + routes: manifest_data.routes.map((route) => { + if (route.type === 'page') { + return { + type: 'page', + pattern: route.pattern, + params: get_params(route.params), + a: route.a.map((id) => manifest_data.components.indexOf(id)), + b: route.b.map((id) => manifest_data.components.indexOf(id)) + }; + } + return { - type: 'page', + type: 'endpoint', pattern: route.pattern, params: get_params(route.params), - a: route.a, - b: route.b + load: async () => { + if (!this.vite) throw new Error('Vite server has not been initialized'); + const url = path.resolve(this.cwd, route.file); + return await this.vite.ssrLoadModule(url); + } }; - } - - return { - type: 'endpoint', - pattern: route.pattern, - params: get_params(route.params), - load: async () => { - if (!this.vite) throw new Error('Vite server has not been initialized'); - const url = path.resolve(this.cwd, route.file); - return await this.vite.ssrLoadModule(url); - } - }; - }) + }) + } }; } @@ -245,31 +297,15 @@ function get_params(array) { // src/routes/[x]/[y]/[z]/svelte, create a function // that turns a RegExpExecArray into ({ x, y, z }) - // input has already been decoded by decodeURI - // now handle the rest that decodeURIComponent would do - const d = /** @param {string} s */ (s) => - s - .replace(/%23/g, '#') - .replace(/%3[Bb]/g, ';') - .replace(/%2[Cc]/g, ',') - .replace(/%2[Ff]/g, '/') - .replace(/%3[Ff]/g, '?') - .replace(/%3[Aa]/g, ':') - .replace(/%40/g, '@') - .replace(/%26/g, '&') - .replace(/%3[Dd]/g, '=') - .replace(/%2[Bb]/g, '+') - .replace(/%24/g, '$'); - /** @param {RegExpExecArray} match */ const fn = (match) => { /** @type {Record} */ const params = {}; array.forEach((key, i) => { if (key.startsWith('...')) { - params[key.slice(3)] = d(match[i + 1] || ''); + params[key.slice(3)] = match[i + 1] || ''; } else { - params[key] = d(match[i + 1]); + params[key] = match[i + 1]; } }); return params; @@ -281,28 +317,15 @@ function get_params(array) { /** * @param {import('types/config').ValidatedConfig} config * @param {string} dir - * @param {string} cwd - * @param {() => import('types/internal').SSRManifest} get_manifest + * @param {boolean} https + * @param {() => import('types/app').SSRManifest} get_manifest */ -async function create_plugin(config, dir, cwd, get_manifest) { +async function create_plugin(config, dir, https, get_manifest) { /** * @type {amp_validator.Validator?} */ const validator = config.kit.amp ? await amp_validator.getInstance() : null; - /** - * @param {import('vite').ModuleNode} node - * @param {Set} deps - */ - const find_deps = (node, deps) => { - for (const dep of node.importedModules) { - if (!deps.has(dep)) { - deps.add(dep); - find_deps(dep, deps); - } - } - }; - /** * @param {vite.ViteDevServer} vite */ @@ -372,15 +395,13 @@ async function create_plugin(config, dir, cwd, get_manifest) { return res.end(err.reason || 'Invalid request body'); } - const host = /** @type {string} */ ( - config.kit.host || req.headers[config.kit.hostHeader || 'host'] - ); + const origin = `${https ? 'https' : 'http'}://${req.headers.host}`; const rendered = await respond( { headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), method: req.method, - host, + origin, path: parsed.pathname.replace(config.kit.paths.base, ''), query: parsed.searchParams, rawBody: body @@ -388,11 +409,6 @@ async function create_plugin(config, dir, cwd, get_manifest) { { amp: config.kit.amp, dev: true, - entry: { - file: `/${SVELTE_KIT}/dev/runtime/internal/start.js`, - css: [], - js: [] - }, floc: config.kit.floc, get_stack: (error) => { vite.ssrFixStacktrace(error); @@ -404,52 +420,12 @@ async function create_plugin(config, dir, cwd, get_manifest) { }, hooks, hydrate: config.kit.hydrate, + manifest: get_manifest(), paths: { base: config.kit.paths.base, assets: config.kit.paths.assets ? SVELTE_KIT_ASSETS : config.kit.paths.base }, - load_component: async (id) => { - const url = `/${id}`; - - const module = /** @type {SSRComponent} */ (await vite.ssrLoadModule(url)); - const node = await vite.moduleGraph.getModuleByUrl(url); - - if (!node) throw new Error(`Could not find node for ${url}`); - - const deps = new Set(); - find_deps(node, deps); - - const styles = new Set(); - - for (const dep of deps) { - const parsed = new URL(dep.url, 'http://localhost/'); - const query = parsed.searchParams; - - // TODO what about .scss files, etc? - if ( - dep.file.endsWith('.css') || - (query.has('svelte') && query.get('type') === 'style') - ) { - try { - const mod = await vite.ssrLoadModule(dep.url); - styles.add(mod.default); - } catch { - // this can happen with dynamically imported modules, I think - // because the Vite module graph doesn't distinguish between - // static and dynamic imports? TODO investigate, submit fix - } - } - } - - return { - module, - entry: url.endsWith('.svelte') ? url : url + '?import', - css: [], - js: [], - styles: Array.from(styles) - }; - }, - manifest: get_manifest(), + prefix: '', prerender: config.kit.prerender.enabled, read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)), root, @@ -564,3 +540,16 @@ function remove_html_middlewares(server) { } } } + +/** + * @param {import('vite').ModuleNode} node + * @param {Set} deps + */ +function find_deps(node, deps) { + for (const dep of node.importedModules) { + if (!deps.has(dep)) { + deps.add(dep); + find_deps(dep, deps); + } + } +} diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js new file mode 100644 index 000000000000..a2cb05d6e9d6 --- /dev/null +++ b/packages/kit/src/core/generate_manifest/index.js @@ -0,0 +1,107 @@ +import { s } from '../../utils/misc.js'; +import { get_mime_lookup } from '../utils.js'; + +/** + * @param {import('../../../types/internal').BuildData} build_data; + * @param {string} relative_path; + * @param {import('../../../types/internal').RouteData[]} routes; + * @param {'esm' | 'cjs'} format + */ +export function generate_manifest( + build_data, + relative_path, + routes = build_data.manifest_data.routes, + format = 'esm' +) { + const bundled_nodes = new Map(); + + // 0 and 1 are special, they correspond to the root layout and root error nodes + bundled_nodes.set(build_data.manifest_data.components[0], { + path: `${relative_path}/nodes/0.js`, + index: 0 + }); + + bundled_nodes.set(build_data.manifest_data.components[1], { + path: `${relative_path}/nodes/1.js`, + index: 1 + }); + + routes.forEach((route) => { + if (route.type === 'page') { + [...route.a, ...route.b].forEach((component) => { + if (component && !bundled_nodes.has(component)) { + const i = build_data.manifest_data.components.indexOf(component); + + bundled_nodes.set(component, { + path: `${relative_path}/nodes/${i}.js`, + index: bundled_nodes.size + }); + } + }); + } + }); + + /** @type {(path: string) => string} */ + const importer = + format === 'esm' + ? (path) => `() => import('${path}')` + : (path) => `() => Promise.resolve().then(() => require('${path}'))`; + + // prettier-ignore + return `{ + appDir: ${s(build_data.app_dir)}, + assets: new Set(${s(build_data.manifest_data.assets.map(asset => asset.file))}), + _: { + mime: ${s(get_mime_lookup(build_data.manifest_data))}, + entry: ${s(build_data.client.entry)}, + nodes: [ + ${Array.from(bundled_nodes.values()).map(node => importer(node.path)).join(',\n\t\t\t\t')} + ], + routes: [ + ${routes.map(route => { + if (route.type === 'page') { + return `{ + type: 'page', + pattern: ${route.pattern}, + params: ${get_params(route.params)}, + path: ${route.path ? s(route.path) : null}, + a: ${s(route.a.map(component => component && bundled_nodes.get(component).index))}, + b: ${s(route.b.map(component => component && bundled_nodes.get(component).index))} + }`.replace(/^\t\t/gm, ''); + } else { + if (!build_data.server.vite_manifest[route.file]) { + // this is necessary in cases where a .css file snuck in — + // perhaps it would be better to disallow these (and others?) + return null; + } + + return `{ + type: 'endpoint', + pattern: ${route.pattern}, + params: ${get_params(route.params)}, + load: ${importer(`${relative_path}/${build_data.server.vite_manifest[route.file].file}`)} + }`.replace(/^\t\t/gm, ''); + } + }).filter(Boolean).join(',\n\t\t\t\t')} + ] + } + }`.replace(/^\t/gm, ''); +} + +/** @param {string[]} array */ +function get_params(array) { + // given an array of params like `['x', 'y', 'z']` for + // src/routes/[x]/[y]/[z]/svelte, create a function + // that turns a RexExpMatchArray into ({ x, y, z }) + return array.length + ? '(m) => ({ ' + + array + .map((param, i) => { + return param.startsWith('...') + ? `${param.slice(3)}: m[${i + 1}] || ''` + : `${param}: m[${i + 1}]`; + }) + .join(', ') + + '})' + : 'null'; +} diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 5b1568ecc78b..fba7ee83a813 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -34,9 +34,12 @@ export async function preview({ __fetch_polyfill(); const app_file = resolve(cwd, `${SVELTE_KIT}/output/server/app.js`); + const manifest_file = resolve(cwd, `${SVELTE_KIT}/output/server/manifest.js`); - /** @type {import('types/internal').App} */ - const app = await import(pathToFileURL(app_file).href); + /** @type {import('types/internal').AppModule} */ + const { App, override } = await import(pathToFileURL(app_file).href); + + const { manifest } = await import(pathToFileURL(manifest_file).href); /** @type {import('sirv').RequestHandler} */ const static_handler = fs.existsSync(config.kit.files.assets) @@ -53,15 +56,18 @@ export async function preview({ const has_asset_path = !!config.kit.paths.assets; - app.init({ + override({ paths: { base: config.kit.paths.base, assets: has_asset_path ? SVELTE_KIT_ASSETS : config.kit.paths.base }, prerendering: false, + protocol: use_https ? 'https' : 'http', read: (file) => fs.readFileSync(join(config.kit.files.assets, file)) }); + const app = new App(manifest); + /** @type {import('vite').UserConfig} */ const vite_config = (config.kit.vite && config.kit.vite()) || {}; @@ -89,9 +95,6 @@ export async function preview({ const rendered = parsed.pathname.startsWith(config.kit.paths.base) && (await app.render({ - host: /** @type {string} */ ( - config.kit.host || req.headers[config.kit.hostHeader || 'host'] - ), method: req.method, headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), path: parsed.pathname.replace(config.kit.paths.base, ''), diff --git a/packages/kit/src/core/utils.js b/packages/kit/src/core/utils.js index 81547e30d1bf..ba1545354fe0 100644 --- a/packages/kit/src/core/utils.js +++ b/packages/kit/src/core/utils.js @@ -74,3 +74,18 @@ export function resolve_entry(entry) { export function posixify(str) { return str.replace(/\\/g, '/'); } + +/** @param {import('./create_app/index.js').ManifestData} manifest_data */ +export function get_mime_lookup(manifest_data) { + /** @type {Record} */ + const mime = {}; + + manifest_data.assets.forEach((asset) => { + if (asset.type) { + const ext = path.extname(asset.file); + mime[ext] = asset.type; + } + }); + + return mime; +} diff --git a/packages/kit/src/runtime/client/renderer.js b/packages/kit/src/runtime/client/renderer.js index 63906bcdb40b..666ed2b20eae 100644 --- a/packages/kit/src/runtime/client/renderer.js +++ b/packages/kit/src/runtime/client/renderer.js @@ -69,13 +69,13 @@ export class Renderer { * fallback: [CSRComponent, CSRComponent]; * target: Node; * session: any; - * host: string; + * origin: string; * }} opts */ - constructor({ Root, fallback, target, session, host }) { + constructor({ Root, fallback, target, session, origin }) { this.Root = Root; this.fallback = fallback; - this.host = host; + this.origin = origin; /** @type {import('./router').Router | undefined} */ this.router; @@ -535,7 +535,7 @@ export class Renderer { /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */ const load_input = { page: { - host: page.host, + origin: page.origin, params, get path() { node.uses.path = true; @@ -607,7 +607,7 @@ export class Renderer { }; /** @type {import('types/page').Page} */ - const page = { host: this.host, path, query, params }; + const page = { origin: this.origin, path, query, params }; /** @type {Array} */ let branch = []; @@ -748,7 +748,7 @@ export class Renderer { */ async _load_error({ status, error, path, query }) { const page = { - host: this.host, + origin: this.origin, path, query, params: {} diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index 5d7868f6a7e1..b06d0fca9039 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -15,7 +15,7 @@ import { set_paths } from '../paths.js'; * }, * target: Node; * session: any; - * host: string; + * origin: string; * route: boolean; * spa: boolean; * trailing_slash: import('types/internal').TrailingSlash; @@ -27,7 +27,16 @@ import { set_paths } from '../paths.js'; * }; * }} opts */ -export async function start({ paths, target, session, host, route, spa, trailing_slash, hydrate }) { +export async function start({ + paths, + target, + session, + origin, + route, + spa, + trailing_slash, + hydrate +}) { if (import.meta.env.DEV && !target) { throw new Error('Missing target element. See https://kit.svelte.dev/docs#configuration-target'); } @@ -37,7 +46,7 @@ export async function start({ paths, target, session, host, route, spa, trailing fallback, target, session, - host + origin }); const router = route diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 9e4664eec30d..c513da3fa76c 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,5 +1,5 @@ import { get_single_valued_header } from '../../utils/http.js'; -import { lowercase_keys } from './utils.js'; +import { decode_params, lowercase_keys } from './utils.js'; /** @param {string} body */ function error(body) { @@ -48,7 +48,7 @@ export async function render_endpoint(request, route, match) { return; } - const params = route.params(match); + const params = route.params ? decode_params(route.params(match)) : {}; const response = await handler({ ...request, params }); const preface = `Invalid response from route ${request.path}`; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 52d4f05d5ee3..e63482a77150 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -55,7 +55,7 @@ export async function respond(incoming, options, state = {}) { } const decoded = decodeURI(request.path); - for (const route of options.manifest.routes) { + for (const route of options.manifest._.routes) { const match = route.pattern.exec(decoded); if (!match) continue; @@ -92,15 +92,19 @@ export async function respond(incoming, options, state = {}) { } } - const $session = await options.hooks.getSession(request); - return await respond_with_error({ - request, - options, - state, - $session, - status: 404, - error: new Error(`Not found: ${request.path}`) - }); + // if this request came direct from the user, rather than + // via a `fetch` in a `load`, render a 404 page + if (!state.initiator) { + const $session = await options.hooks.getSession(request); + return await respond_with_error({ + request, + options, + state, + $session, + status: 404, + error: new Error(`Not found: ${request.path}`) + }); + } } }); } catch (/** @type {unknown} */ err) { diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 0dac81e68a5d..d32ef3740435 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,3 +1,4 @@ +import { decode_params } from '../utils.js'; import { respond } from './respond.js'; /** @@ -18,10 +19,10 @@ export async function render_page(request, route, match, options, state) { }; } - const params = route.params(match); + const params = route.params ? decode_params(route.params(match)) : {}; const page = { - host: request.host, + origin: request.origin, path: request.path, query: request.query, params diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index 41d16eebe61e..a5018bdf2025 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -1,10 +1,9 @@ import { normalize } from '../../load.js'; import { respond } from '../index.js'; +import { s } from '../../../utils/misc.js'; import { escape_json_string_in_html } from '../../../utils/escape.js'; import { is_root_relative, resolve } from '../../../utils/url.js'; -const s = JSON.stringify; - /** * @param {{ * request: import('types/hooks').ServerRequest; @@ -115,20 +114,24 @@ export async function load_node({ resolved.startsWith(prefix) ? resolved.slice(prefix.length) : resolved ).slice(1); const filename_html = `${filename}/index.html`; // path may also match path/index.html - const asset = options.manifest.assets.find( - (d) => d.file === filename || d.file === filename_html - ); - if (asset) { - response = options.read - ? new Response(options.read(asset.file), { - headers: asset.type ? { 'content-type': asset.type } : {} - }) - : await fetch( - // TODO we need to know what protocol to use - `http://${page.host}/${asset.file}`, - /** @type {RequestInit} */ (opts) - ); + const is_asset = options.manifest.assets.has(filename); + const is_asset_html = options.manifest.assets.has(filename_html); + + if (is_asset || is_asset_html) { + const file = is_asset ? filename : filename_html; + + if (options.read) { + const type = is_asset + ? options.manifest._.mime[filename.slice(filename.lastIndexOf('.'))] + : 'text/html'; + + response = new Response(options.read(file), { + headers: type ? { 'content-type': type } : {} + }); + } else { + response = await fetch(`${page.origin}/${file}`, /** @type {RequestInit} */ (opts)); + } } else if (is_root_relative(resolved)) { const relative = resolved; @@ -157,7 +160,7 @@ export async function load_node({ const rendered = await respond( { - host: request.host, + origin: request.origin, method: opts.method || 'GET', headers: Object.fromEntries(opts.headers), path: relative, @@ -183,6 +186,13 @@ export async function load_node({ status: rendered.status, headers: /** @type {Record} */ (rendered.headers) }); + } else { + // we can't load the endpoint from our own manifest, + // so we need to make an actual HTTP request + return fetch(request.origin + relative + search, { + method: opts.method || 'GET', + headers: opts.headers + }); } } else { // external @@ -191,9 +201,9 @@ export async function load_node({ } // external fetch - if (typeof request.host !== 'undefined') { - const { hostname: fetch_hostname } = new URL(url); - const [server_hostname] = request.host.split(':'); + if (typeof request.origin !== 'undefined') { + const fetch_hostname = new URL(url).hostname; + const server_hostname = new URL(request.origin).hostname; // allow cookie passthrough for "same-origin" // if SvelteKit is serving my.domain.com: diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 685436051f56..d0bdaa5ecc56 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -3,8 +3,7 @@ import { writable } from 'svelte/store'; import { coalesce_to_error } from '../../../utils/error.js'; import { hash } from '../../hash.js'; import { escape_html_attr } from '../../../utils/escape.js'; - -const s = JSON.stringify; +import { s } from '../../../utils/misc.js'; // TODO rename this function/module @@ -28,8 +27,8 @@ export async function render_response({ error, page }) { - const css = new Set(options.entry.css); - const js = new Set(options.entry.js); + const css = new Set(options.manifest._.entry.css); + const js = new Set(options.manifest._.entry.js); const styles = new Set(); /** @type {Array<{ url: string, body: string, json: string }>} */ @@ -101,8 +100,8 @@ export async function render_response({ ? `` : '' : [ - ...Array.from(js).map((dep) => ``), - ...Array.from(css).map((dep) => ``) + ...Array.from(js).map((dep) => ``), + ...Array.from(css).map((dep) => ``) ].join('\n\t\t'); /** @type {string} */ @@ -119,14 +118,14 @@ export async function render_response({ } else if (include_js) { // prettier-ignore init = ` -

{host}

-

{$page.host}

-

{data.host}

+

{origin}

+

{$page.origin}

+

{data.origin}

diff --git a/packages/kit/test/apps/basics/src/routes/host/_tests.js b/packages/kit/test/apps/basics/src/routes/host/_tests.js deleted file mode 100644 index 4194d3620500..000000000000 --- a/packages/kit/test/apps/basics/src/routes/host/_tests.js +++ /dev/null @@ -1,16 +0,0 @@ -import * as assert from 'uvu/assert'; - -/** @type {import('test').TestMaker} */ -export default function (test) { - test('can access host through page store', null, async ({ base, page }) => { - page.setExtraHTTPHeaders({ - 'x-forwarded-host': 'forwarded.com' - }); - - await page.goto(`${base}/host`); - assert.equal(await page.textContent('h1'), 'forwarded.com'); - - // reset - page.setExtraHTTPHeaders({}); - }); -} diff --git a/packages/kit/test/apps/basics/src/routes/origin/_tests.js b/packages/kit/test/apps/basics/src/routes/origin/_tests.js new file mode 100644 index 000000000000..7b9f88b54769 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/origin/_tests.js @@ -0,0 +1,19 @@ +import * as assert from 'uvu/assert'; + +/** @type {import('test').TestMaker} */ +export default function (test, is_dev) { + test('can access origin through page store', null, async ({ base, page }) => { + if (!is_dev) { + page.setExtraHTTPHeaders({ + 'x-forwarded-host': 'forwarded.com', + 'x-forwarded-proto': 'https' + }); + } + + await page.goto(`${base}/origin`); + assert.equal(await page.textContent('h1'), is_dev ? base : 'https://forwarded.com'); + + // reset + page.setExtraHTTPHeaders({}); + }); +} diff --git a/packages/kit/test/apps/basics/src/routes/host/index.svelte b/packages/kit/test/apps/basics/src/routes/origin/index.svelte similarity index 70% rename from packages/kit/test/apps/basics/src/routes/host/index.svelte rename to packages/kit/test/apps/basics/src/routes/origin/index.svelte index e2e5325415c3..3cf0adceb58d 100644 --- a/packages/kit/test/apps/basics/src/routes/host/index.svelte +++ b/packages/kit/test/apps/basics/src/routes/origin/index.svelte @@ -2,4 +2,4 @@ import { page } from '$app/stores'; -

{$page.host}

\ No newline at end of file +

{$page.origin}

diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index 32c11b217047..5619c319a01d 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -1,7 +1,10 @@ /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - hostHeader: 'x-forwarded-host', + headers: { + host: 'x-forwarded-host', + protocol: 'x-forwarded-proto' + }, vite: { build: { minify: false diff --git a/packages/kit/test/apps/options/source/pages/host/_tests.js b/packages/kit/test/apps/options/source/pages/host/_tests.js deleted file mode 100644 index 295fe31e204a..000000000000 --- a/packages/kit/test/apps/options/source/pages/host/_tests.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as assert from 'uvu/assert'; - -/** @type {import('test').TestMaker} */ -export default function (test) { - test('sets host', '/path-base/host/', async ({ page }) => { - assert.equal(await page.textContent('[data-source="load"]'), 'example.com'); - assert.equal(await page.textContent('[data-source="store"]'), 'example.com'); - assert.equal(await page.textContent('[data-source="endpoint"]'), 'example.com'); - }); -} diff --git a/packages/kit/test/apps/options/source/pages/origin/_tests.js b/packages/kit/test/apps/options/source/pages/origin/_tests.js new file mode 100644 index 000000000000..676ffe5ea42a --- /dev/null +++ b/packages/kit/test/apps/options/source/pages/origin/_tests.js @@ -0,0 +1,12 @@ +import * as assert from 'uvu/assert'; + +/** @type {import('test').TestMaker} */ +export default function (test, is_dev) { + test('sets origin', '/path-base/origin/', async ({ base, page }) => { + const origin = is_dev ? base : 'https://example.com'; + + assert.equal(await page.textContent('[data-source="load"]'), origin); + assert.equal(await page.textContent('[data-source="store"]'), origin); + assert.equal(await page.textContent('[data-source="endpoint"]'), origin); + }); +} diff --git a/packages/kit/test/apps/options/source/pages/host/index.json.js b/packages/kit/test/apps/options/source/pages/origin/index.json.js similarity index 56% rename from packages/kit/test/apps/options/source/pages/host/index.json.js rename to packages/kit/test/apps/options/source/pages/origin/index.json.js index 9a6349a5892c..743f7cc6cee8 100644 --- a/packages/kit/test/apps/options/source/pages/host/index.json.js +++ b/packages/kit/test/apps/options/source/pages/origin/index.json.js @@ -1,6 +1,6 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ host }) { +export function get({ origin }) { return { - body: { host } + body: { origin } }; } diff --git a/packages/kit/test/apps/amp/src/routes/host/index.svelte b/packages/kit/test/apps/options/source/pages/origin/index.svelte similarity index 61% rename from packages/kit/test/apps/amp/src/routes/host/index.svelte rename to packages/kit/test/apps/options/source/pages/origin/index.svelte index b1078fb1e0c5..24f066165751 100644 --- a/packages/kit/test/apps/amp/src/routes/host/index.svelte +++ b/packages/kit/test/apps/options/source/pages/origin/index.svelte @@ -1,12 +1,12 @@ -

{host}

-

{$page.host}

-

{data.host}

+

{origin}

+

{$page.origin}

+

{data.origin}

diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js index 1efeaa1728f5..23c884d7a9ae 100644 --- a/packages/kit/test/apps/options/svelte.config.js +++ b/packages/kit/test/apps/options/svelte.config.js @@ -22,7 +22,8 @@ const config = { paths: { base: '/path-base', assets: 'https://cdn.example.com/stuff' - } + }, + protocol: 'https' } }; diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index 86f58a5cc1d0..ec38f363153a 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -161,7 +161,9 @@ declare module '@sveltejs/kit/ssr' { type State = import('@sveltejs/kit/types/internal').SSRRenderState; export interface Respond { - (incoming: IncomingRequest, options: Options, state?: State): Promise; + (incoming: IncomingRequest & { origin: string }, options: Options, state?: State): Promise< + Response | undefined + >; } export const respond: Respond; } diff --git a/packages/kit/types/app.d.ts b/packages/kit/types/app.d.ts index 0fbe02bd9d7f..bc659d869407 100644 --- a/packages/kit/types/app.d.ts +++ b/packages/kit/types/app.d.ts @@ -1,11 +1,21 @@ import { ReadOnlyFormData, RequestHeaders } from './helper'; import { ServerResponse } from './hooks'; +import { PrerenderOptions, SSRNodeLoader, SSRRoute } from './internal'; -export interface App { - init(): void; +export class App { + constructor(manifest: SSRManifest); render(incoming: IncomingRequest): Promise; } +export class InternalApp extends App { + render( + incoming: IncomingRequest, + options?: { + prerender: PrerenderOptions; + } + ): Promise; +} + export type RawBody = null | Uint8Array; export type ParameterizedBody = Body extends FormData ? ReadOnlyFormData @@ -13,9 +23,24 @@ export type ParameterizedBody = Body extends FormData export interface IncomingRequest { method: string; - host: string; path: string; query: URLSearchParams; headers: RequestHeaders; rawBody: RawBody; } + +export interface SSRManifest { + appDir: string; + assets: Set; + /** private fields */ + _: { + mime: Record; + entry: { + file: string; + js: string[]; + css: string[]; + }; + nodes: SSRNodeLoader[]; + routes: SSRRoute[]; + }; +} diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index d2cd01674f24..541c27223f3e 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -1,38 +1,99 @@ import { UserConfig as ViteConfig } from 'vite'; import { RecursiveRequired } from './helper'; -import { Logger, TrailingSlash } from './internal'; +import { HttpMethod, Logger, RouteSegment, TrailingSlash } from './internal'; -export interface AdapterUtils { +export interface RouteDefinition { + type: 'page' | 'endpoint'; + pattern: RegExp; + segments: RouteSegment[]; + methods: HttpMethod[]; +} + +export interface AdapterEntry { + /** + * A string that uniquely identifies an HTTP service (e.g. serverless function) and is used for deduplication. + * For example, `/foo/a-[b]` and `/foo/[c]` are different routes, but would both + * be represented in a Netlify _redirects file as `/foo/:param`, so they share an ID + */ + id: string; + + /** + * A function that compares the candidate route with the current route to determine + * if it should be treated as a fallback for the current route. For example, `/foo/[c]` + * is a fallback for `/foo/a-[b]`, and `/[...catchall]` is a fallback for all routes + */ + filter: (route: RouteDefinition) => boolean; + + /** + * A function that is invoked once the entry has been created. This is where you + * should write the function to the filesystem and generate redirect manifests. + */ + complete: (entry: { + generateManifest: (opts: { relativePath: string; format?: 'esm' | 'cjs' }) => string; + }) => void; +} + +export interface Builder { log: Logger; rimraf(dir: string): void; mkdirp(dir: string): void; + + /** + * Create entry points that map to individual functions + * @param fn A function that groups a set of routes into an entry point + */ + createEntries(fn: (route: RouteDefinition) => AdapterEntry): void; + + generateManifest: (opts: { relativePath: string; format?: 'esm' | 'cjs' }) => string; + + getBuildDirectory(name: string): string; + getClientDirectory(): string; + getServerDirectory(): string; + getStaticDirectory(): string; + /** * @param dest the destination folder to which files should be copied * @returns an array of paths corresponding to the files that have been created by the copy */ - copy_client_files(dest: string): string[]; + writeClient(dest: string): string[]; /** * @param dest the destination folder to which files should be copied * @returns an array of paths corresponding to the files that have been created by the copy */ - copy_server_files(dest: string): string[]; + writeServer(dest: string): string[]; /** * @param dest the destination folder to which files should be copied * @returns an array of paths corresponding to the files that have been created by the copy */ - copy_static_files(dest: string): string[]; + writeStatic(dest: string): string[]; /** - * @param from the source folder from which files should be copied - * @param to the destination folder to which files should be copied + * @param from the source file or folder + * @param to the destination file or folder + * @param opts.filter a function to determine whether a file or folder should be copied + * @param opts.replace a map of strings to replace * @returns an array of paths corresponding to the files that have been created by the copy */ - copy(from: string, to: string, filter?: (basename: string) => boolean): string[]; - prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise; + copy( + from: string, + to: string, + opts?: { + filter?: (basename: string) => boolean; + replace?: Record; + } + ): string[]; + + prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise<{ + paths: string[]; + }>; } export interface Adapter { name: string; - adapt(context: { utils: AdapterUtils; config: ValidatedConfig }): Promise; + headers?: { + host?: string; + protocol?: string; + }; + adapt(builder: Builder): Promise; } export interface PrerenderErrorHandler { @@ -62,8 +123,11 @@ export interface Config { template?: string; }; floc?: boolean; + headers?: { + host?: string; + protocol?: string; + }; host?: string; - hostHeader?: string; hydrate?: boolean; package?: { dir?: string; @@ -82,6 +146,7 @@ export interface Config { entries?: string[]; onError?: PrerenderOnErrorValue; }; + protocol?: string; router?: boolean; serviceWorker?: { register?: boolean; diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index 4bbc81267b05..43afa3d040bd 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -5,6 +5,7 @@ export type StrictBody = string | Uint8Array; export interface ServerRequest, Body = unknown> extends IncomingRequest { + origin: string; params: Record; body: ParameterizedBody; locals: Locals; @@ -27,6 +28,15 @@ export interface Handle, Body = unknown> { }): MaybePromise; } +// internally, `resolve` could return `undefined`, so we differentiate InternalHandle +// from the public Handle type +export interface InternalHandle, Body = unknown> { + (input: { + request: ServerRequest; + resolve(request: ServerRequest): MaybePromise; + }): MaybePromise; +} + export interface HandleError, Body = unknown> { (input: { error: Error & { frame?: string }; request: ServerRequest }): void; } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index e9b4d91d90d3..c56ce65c4d45 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3,8 +3,8 @@ import './ambient-modules'; -export { App, IncomingRequest, RawBody } from './app'; -export { Adapter, AdapterUtils, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; +export { App, IncomingRequest, RawBody, SSRManifest } from './app'; +export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; export { EndpointOutput, RequestHandler } from './endpoint'; export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput, Page } from './page'; export { diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index bfc5b5ab0a05..33f380486eec 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,10 +1,11 @@ +import { OutputAsset, OutputChunk } from 'rollup'; import { RequestHandler } from './endpoint'; -import { App as PublicApp, IncomingRequest } from './app'; +import { InternalApp, SSRManifest } from './app'; import { ExternalFetch, GetSession, - Handle, HandleError, + InternalHandle, ServerRequest, ServerResponse } from './hooks'; @@ -18,22 +19,18 @@ export interface PrerenderOptions { dependencies: Map; } -export interface App extends PublicApp { - init(options?: { +export interface AppModule { + App: typeof InternalApp; + + override(options: { paths: { base: string; assets: string; }; prerendering: boolean; + protocol?: 'http' | 'https'; read(file: string): Buffer; }): void; - - render( - incoming: IncomingRequest, - options?: { - prerender: PrerenderOptions; - } - ): Promise; } export interface Logger { @@ -84,12 +81,12 @@ export interface SSRPage { /** * plan a is to render 1 or more layout components followed by a leaf component. */ - a: PageId[]; + a: number[]; /** * plan b — if one of them components fails in `load` we backtrack until we find * the nearest error component. */ - b: PageId[]; + b: number[]; } export interface SSREndpoint { @@ -105,17 +102,12 @@ export type SSRRoute = SSREndpoint | SSRPage; export type CSRRoute = [RegExp, CSRComponentLoader[], CSRComponentLoader[], GetParams?]; -export interface SSRManifest { - assets: Asset[]; - layout: string; - error: string; - routes: SSRRoute[]; -} +export type SSRNodeLoader = () => Promise; export interface Hooks { externalFetch: ExternalFetch; getSession: GetSession; - handle: Handle; + handle: InternalHandle; handleError: HandleError; } @@ -134,22 +126,17 @@ export interface SSRNode { export interface SSRRenderOptions { amp: boolean; dev: boolean; - entry: { - file: string; - css: string[]; - js: string[]; - }; floc: boolean; get_stack: (error: Error) => string | undefined; handle_error(error: Error & { frame?: string }, request: ServerRequest): void; hooks: Hooks; hydrate: boolean; - load_component(id: PageId): Promise; manifest: SSRManifest; paths: { base: string; assets: string; }; + prefix: string; prerender: boolean; read(file: string): Buffer; root: SSRComponent['default']; @@ -174,8 +161,17 @@ export interface Asset { type: string | null; } +export interface RouteSegment { + content: string; + dynamic: boolean; + rest: boolean; +} + +export type HttpMethod = 'get' | 'head' | 'post' | 'put' | 'delete' | 'patch'; + export interface PageData { type: 'page'; + segments: RouteSegment[]; pattern: RegExp; params: string[]; path: string; @@ -185,6 +181,7 @@ export interface PageData { export interface EndpointData { type: 'endpoint'; + segments: RouteSegment[]; pattern: RegExp; params: string[]; file: string; @@ -201,8 +198,23 @@ export interface ManifestData { } export interface BuildData { - client: string[]; - server: string[]; + app_dir: string; + manifest_data: ManifestData; + client: { + assets: OutputAsset[]; + chunks: OutputChunk[]; + entry: { + file: string; + js: string[]; + css: string[]; + }; + vite_manifest: import('vite').Manifest; + }; + server: { + chunks: OutputChunk[]; + methods: Record; + vite_manifest: import('vite').Manifest; + }; static: string[]; entries: string[]; } diff --git a/packages/kit/types/page.d.ts b/packages/kit/types/page.d.ts index dd0e89ea17ed..4372e83d5cb9 100644 --- a/packages/kit/types/page.d.ts +++ b/packages/kit/types/page.d.ts @@ -1,7 +1,7 @@ import { InferValue, MaybePromise, Rec } from './helper'; export interface Page = Record> { - host: string; + origin: string; path: string; params: Params; query: URLSearchParams; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03f87433b56c..304861fe29f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,13 +99,25 @@ importers: packages/adapter-netlify: specifiers: '@iarna/toml': ^2.2.5 + '@rollup/plugin-commonjs': ^21.0.0 + '@rollup/plugin-json': ^4.1.0 + '@rollup/plugin-node-resolve': ^13.0.5 '@sveltejs/kit': workspace:* esbuild: ^0.13.15 + rimraf: ^3.0.2 + rollup: ^2.58.0 + tiny-glob: ^0.2.9 dependencies: '@iarna/toml': 2.2.5 esbuild: 0.13.15 + tiny-glob: 0.2.9 devDependencies: + '@rollup/plugin-commonjs': 21.0.1_rollup@2.60.2 + '@rollup/plugin-json': 4.1.0_rollup@2.60.2 + '@rollup/plugin-node-resolve': 13.0.6_rollup@2.60.2 '@sveltejs/kit': link:../kit + rimraf: 3.0.2 + rollup: 2.60.2 packages/adapter-node: specifiers: @@ -114,15 +126,14 @@ importers: '@types/compression': ^1.7.2 c8: ^7.10.0 compression: ^1.7.4 - esbuild: ^0.13.15 node-fetch: ^3.1.0 polka: ^1.0.0-next.22 + rimraf: ^3.0.2 rollup: ^2.60.2 sirv: ^1.0.19 tiny-glob: ^0.2.9 uvu: ^0.5.2 dependencies: - esbuild: 0.13.15 tiny-glob: 0.2.9 devDependencies: '@rollup/plugin-json': 4.1.0_rollup@2.60.2 @@ -132,6 +143,7 @@ importers: compression: 1.7.4 node-fetch: 3.1.0 polka: 1.0.0-next.22 + rimraf: 3.0.2 rollup: 2.60.2 sirv: 1.0.19 uvu: 0.5.2 @@ -210,8 +222,8 @@ importers: '@lukeed/uuid': 2.0.0 cookie: 0.4.1 devDependencies: - '@sveltejs/adapter-auto': link:../../../adapter-auto - '@sveltejs/kit': link:../../../kit + '@sveltejs/adapter-auto': 1.0.0-next.5 + '@sveltejs/kit': 1.0.0-next.203_svelte@3.44.2 svelte: 3.44.2 svelte-preprocess: 4.9.8_svelte@3.44.2+typescript@4.5.2 typescript: 4.5.2 @@ -549,7 +561,6 @@ packages: /@iarna/toml/2.2.5: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} - dev: false /@istanbuljs/schema/0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} @@ -768,7 +779,53 @@ packages: dependencies: estree-walker: 2.0.2 picomatch: 2.3.0 - dev: false + + /@sveltejs/adapter-auto/1.0.0-next.5: + resolution: {integrity: sha512-pD0iULDFuOQ0I9fLvdjL9BrA90hZwYtcaI25pNCdxGzdn9sgpXhxmON807fJgn1lG9HQ1LBTABz4gUMXviVAdg==} + dependencies: + '@sveltejs/adapter-cloudflare': 1.0.0-next.4 + '@sveltejs/adapter-netlify': 1.0.0-next.36 + '@sveltejs/adapter-vercel': 1.0.0-next.32 + dev: true + + /@sveltejs/adapter-cloudflare/1.0.0-next.4: + resolution: {integrity: sha512-c792TBbVhWtqAjxeorkHUArz4bTFpvAXfgMZALakNV4lakUH/1bgjisrSg5xMNQlUqu1kkUlKQic61avsKH8gw==} + dependencies: + esbuild: 0.13.15 + dev: true + + /@sveltejs/adapter-netlify/1.0.0-next.36: + resolution: {integrity: sha512-LdrIXCTBnIubtt/lthcnyt5VljuHpZlVzUqpWXk9Eu6bpNKblqQLMHkTBQfIbPfanmNSDZXJQVsdcFLqF2/+Cw==} + dependencies: + '@iarna/toml': 2.2.5 + esbuild: 0.13.15 + dev: true + + /@sveltejs/adapter-vercel/1.0.0-next.32: + resolution: {integrity: sha512-ZcltaS5bAobGD5P0z7xJIjPHSlGpF7padMIkqTzJxwMEb/acGgdO5yzDS8XUEaSNgj+prpD2oG8+gm33ds8x0A==} + dependencies: + esbuild: 0.13.15 + dev: true + + /@sveltejs/kit/1.0.0-next.203_svelte@3.44.2: + resolution: {integrity: sha512-+bhfmkzquWMSCCFIJoh36U9D6IKC0p/NrIr4CBHMAdRpqzL9lMP0AYQm1fUDH20F/6b2tWvGxmEg+gAmHikRfA==} + engines: {node: '>=14.13'} + hasBin: true + peerDependencies: + svelte: ^3.44.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 1.0.0-next.32_svelte@3.44.2+vite@2.7.2 + cheap-watch: 1.0.4 + sade: 1.7.4 + svelte: 3.44.2 + vite: 2.7.2 + transitivePeerDependencies: + - diff-match-patch + - less + - sass + - stylus + - supports-color + dev: true /@sveltejs/vite-plugin-svelte/1.0.0-next.32_svelte@3.44.2+vite@2.7.2: resolution: {integrity: sha512-Lhf5BxVylosHIW6U2s6WDQA39ycd+bXivC8gHsXCJeLzxoHj7Pv7XAOk25xRSXT4wHg9DWFMBQh2DFU0DxHZ2g==} @@ -791,7 +848,6 @@ packages: vite: 2.7.2 transitivePeerDependencies: - supports-color - dev: false /@types/amphtml-validator/1.0.1: resolution: {integrity: sha512-DWE7fy6KtC+Uw0KV/HAmjuH2GB/o8yskXlvmVWR7mOVsLDybp+XrwkzEeRFU9wGjWKeRMBNGsx+5DRq7sUsAwA==} @@ -1369,7 +1425,6 @@ packages: /cheap-watch/1.0.4: resolution: {integrity: sha512-QR/9FrtRL5fjfUJBhAKCdi0lSRQ3rVRRum3GF9wDKp2TJbEIMGhUEr2yU8lORzm9Isdjx7/k9S0DFDx+z5VGtw==} engines: {node: '>=8'} - dev: false /chokidar/3.5.2: resolution: {integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==} @@ -1721,7 +1776,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false optional: true /esbuild-darwin-64/0.13.15: @@ -1729,7 +1783,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false optional: true /esbuild-darwin-arm64/0.13.15: @@ -1737,7 +1790,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false optional: true /esbuild-freebsd-64/0.13.15: @@ -1745,7 +1797,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: false optional: true /esbuild-freebsd-arm64/0.13.15: @@ -1753,7 +1804,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: false optional: true /esbuild-linux-32/0.13.15: @@ -1761,7 +1811,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-64/0.13.15: @@ -1769,7 +1818,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-arm/0.13.15: @@ -1777,7 +1825,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-arm64/0.13.15: @@ -1785,7 +1832,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-mips64le/0.13.15: @@ -1793,7 +1839,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-ppc64le/0.13.15: @@ -1801,7 +1846,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-netbsd-64/0.13.15: @@ -1809,7 +1853,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: false optional: true /esbuild-openbsd-64/0.13.15: @@ -1817,7 +1860,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: false optional: true /esbuild-sunos-64/0.13.15: @@ -1825,7 +1867,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: false optional: true /esbuild-windows-32/0.13.15: @@ -1833,7 +1874,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false optional: true /esbuild-windows-64/0.13.15: @@ -1841,7 +1881,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false optional: true /esbuild-windows-arm64/0.13.15: @@ -1849,7 +1888,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false optional: true /esbuild/0.13.15: @@ -1874,7 +1912,6 @@ packages: esbuild-windows-32: 0.13.15 esbuild-windows-64: 0.13.15 esbuild-windows-arm64: 0.13.15 - dev: false /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -3119,7 +3156,6 @@ packages: resolution: {integrity: sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: false /natural-compare/1.4.0: resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} @@ -3392,7 +3428,6 @@ packages: /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false /picomatch/2.3.0: resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} @@ -3488,7 +3523,6 @@ packages: nanoid: 3.1.30 picocolors: 1.0.0 source-map-js: 1.0.1 - dev: false /preferred-pm/3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} @@ -3666,7 +3700,6 @@ packages: /require-relative/0.8.7: resolution: {integrity: sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=} - dev: false /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -3875,7 +3908,6 @@ packages: /source-map-js/1.0.1: resolution: {integrity: sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==} engines: {node: '>=0.10.0'} - dev: false /source-map/0.7.3: resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} @@ -4060,7 +4092,6 @@ packages: svelte: '>=3.19.0' dependencies: svelte: 3.44.2 - dev: false /svelte-preprocess/4.9.8_svelte@3.44.2+typescript@4.4.4: resolution: {integrity: sha512-EQS/oRZzMtYdAprppZxY3HcysKh11w54MgA63ybtL+TAZ4hVqYOnhw41JVJjWN9dhPnNjjLzvbZ2tMhTsla1Og==} @@ -4447,7 +4478,6 @@ packages: rollup: 2.60.2 optionalDependencies: fsevents: 2.3.2 - dev: false /wcwidth/1.0.1: resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=}