chore: fix build dependencies and frontend config
- Bump Docker SDK, downgrade otel deps for Go 1.24 compatibility - Fix duplicate i18n import in InstanceCard - Fix SvelteKit prerender/strict config for SPA build - Update Dockerfile with GOTOOLCHAIN=auto - Generate package-lock.json and go.sum WIP: still resolving Go 1.24 vs otel transitive dep versions
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Tyreal Hu
|
||||
Copyright (c) 2025 The Svelte Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# @sveltejs/acorn-typescript
|
||||
|
||||
[](LICENSE.md) [](https://svelte.dev/chat)
|
||||
|
||||
This is a plugin for [Acorn](http://marijnhaverbeke.nl/acorn/) - a tiny, fast JavaScript parser, written completely in JavaScript.
|
||||
|
||||
It was created as an experimental alternative, faster [TypeScript](https://www.typescriptlang.org/) parser. It will help you to parse
|
||||
TypeScript using Acorn.
|
||||
|
||||
## Usage
|
||||
|
||||
To get started, import the plugin and use Acorn's extension mechanism to register it. You have to enable `options.locations` while using `@sveltejs/acorn-typescript`.
|
||||
|
||||
```typescript
|
||||
import { Parser } from 'acorn';
|
||||
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
||||
|
||||
const node = Parser.extend(tsPlugin()).parse(
|
||||
`
|
||||
const a = 1
|
||||
type A = number
|
||||
export {
|
||||
a,
|
||||
type A as B
|
||||
}
|
||||
`,
|
||||
{
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
If you want to enable parsing within a TypeScript ambient context, where certain syntax have different rules (like `.d.ts` files and inside [declare module blocks](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)):
|
||||
|
||||
```typescript
|
||||
import { Parser } from 'acorn';
|
||||
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
||||
|
||||
const node = Parser.extend(tsPlugin({ dts: true })).parse(
|
||||
`
|
||||
const a = 1
|
||||
type A = number
|
||||
export {
|
||||
a,
|
||||
type A as B
|
||||
}
|
||||
`,
|
||||
{
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## SUPPORTED
|
||||
|
||||
- Typescript normal syntax
|
||||
- Support to parse TypeScript [Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html)
|
||||
- Support to parse JSX & TSX
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
[click](./CHANGELOG.md)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We want to thank [TyrealHu](https://github.com/TyrealHu) for his original work on this project. He maintained [`acorn-typescript`](https://github.com/TyrealHu/acorn-typescript) until early 2024.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Parser } from 'acorn';
|
||||
|
||||
export function tsPlugin(options?: {
|
||||
dts?: boolean;
|
||||
/** Whether to use JSX. Defaults to false */
|
||||
jsx?:
|
||||
| boolean
|
||||
| {
|
||||
allowNamespaces?: boolean;
|
||||
allowNamespacedObjects?: boolean;
|
||||
};
|
||||
}): (BaseParser: typeof Parser) => typeof Parser;
|
||||
+5016
File diff suppressed because it is too large
Load Diff
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@sveltejs/acorn-typescript",
|
||||
"version": "1.0.9",
|
||||
"description": "Acorn plugin that parses TypeScript",
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sveltejs/acorn-typescript.git"
|
||||
},
|
||||
"author": "tyrealhu and the Svelte team",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/acorn-typescript/issues"
|
||||
},
|
||||
"homepage": "https://github.com/sveltejs/acorn-typescript#readme",
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.11",
|
||||
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "~5.3.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"prettier": "~3.5.2",
|
||||
"test262": "git+https://github.com/tc39/test262.git#88ebb1e3755198cd08757bca1698effbbf360345",
|
||||
"test262-parser-runner": "^0.5.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "esbuild src/index.ts --bundle --format=esm --outfile=index.js --platform=node --external:acorn",
|
||||
"format": "prettier --write .",
|
||||
"check": "tsc --noEmit",
|
||||
"lint": "prettier --check .",
|
||||
"test": "vitest run",
|
||||
"test:update": "cross-env UPDATE_SNAPSHOT=true vitest run && pnpm run format",
|
||||
"test:test262": "pnpm run build && node ./test/run_test262.js",
|
||||
"changeset:version": "changeset version && git add --all",
|
||||
"changeset:release": "changeset publish",
|
||||
"playground": "pnpm build && node ./playground/index.js"
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# @sveltejs/adapter-static
|
||||
|
||||
[Adapter](https://svelte.dev/docs/kit/adapters) for SvelteKit apps that prerenders your entire site as a collection of static files. It's also possible to create an SPA with it by specifying a fallback page which renders an empty shell. If you'd like to prerender only some pages and not create an SPA for those left out, you will need to use a different adapter together with [the `prerender` option](https://svelte.dev/docs/kit/page-options#prerender).
|
||||
|
||||
## Docs
|
||||
|
||||
[Docs](https://svelte.dev/docs/kit/adapter-static)
|
||||
|
||||
## Changelog
|
||||
|
||||
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/kit/blob/main/packages/adapter-static/CHANGELOG.md).
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { Adapter } from '@sveltejs/kit';
|
||||
|
||||
export interface AdapterOptions {
|
||||
pages?: string;
|
||||
assets?: string;
|
||||
fallback?: string;
|
||||
precompress?: boolean;
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
export default function plugin(options?: AdapterOptions): Adapter;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import path from 'node:path';
|
||||
import { platforms } from './platforms.js';
|
||||
|
||||
/** @type {import('./index.js').default} */
|
||||
export default function (options) {
|
||||
return {
|
||||
name: '@sveltejs/adapter-static',
|
||||
/** @param {import('./internal.js').Builder2_0_0} builder */
|
||||
async adapt(builder) {
|
||||
if (!options?.fallback && builder.config.kit.router?.type !== 'hash') {
|
||||
const dynamic_routes = builder.routes.filter((route) => route.prerender !== true);
|
||||
if (dynamic_routes.length > 0 && options?.strict !== false) {
|
||||
const prefix = path.relative('.', builder.config.kit.files.routes);
|
||||
const has_param_routes = builder.routes.some((route) => route.id.includes('['));
|
||||
const config_option =
|
||||
has_param_routes || JSON.stringify(builder.config.kit.prerender.entries) !== '["*"]'
|
||||
? ` - adjust the \`prerender.entries\` config option ${
|
||||
has_param_routes
|
||||
? '(routes with parameters are not part of entry points by default)'
|
||||
: ''
|
||||
} — see https://svelte.dev/docs/kit/configuration#prerender for more info.`
|
||||
: '';
|
||||
|
||||
builder.log.error(
|
||||
`@sveltejs/adapter-static: all routes must be fully prerenderable, but found the following routes that are dynamic:
|
||||
${dynamic_routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join('\n')}
|
||||
|
||||
You have the following options:
|
||||
- set the \`fallback\` option — see https://svelte.dev/docs/kit/single-page-apps#usage for more info.
|
||||
- add \`export const prerender = true\` to your root \`+layout.js/.ts\` or \`+layout.server.js/.ts\` file. This will try to prerender all pages.
|
||||
- add \`export const prerender = true\` to any \`+server.js/ts\` files that are not fetched by page \`load\` functions.
|
||||
${config_option}
|
||||
- pass \`strict: false\` to \`adapter-static\` to ignore this error. Only do this if you are sure you don't need the routes in question in your final app, as they will be unavailable. See https://github.com/sveltejs/kit/tree/main/packages/adapter-static#strict for more info.
|
||||
|
||||
If this doesn't help, you may need to use a different adapter. @sveltejs/adapter-static can only be used for sites that don't need a server for dynamic rendering, and can run on just a static file server.
|
||||
See https://svelte.dev/docs/kit/page-options#prerender for more details`
|
||||
);
|
||||
throw new Error('Encountered dynamic routes');
|
||||
}
|
||||
}
|
||||
|
||||
const platform = platforms.find((platform) => platform.test());
|
||||
|
||||
if (platform) {
|
||||
if (options) {
|
||||
builder.log.warn(
|
||||
`Detected ${platform.name}. Please remove adapter-static options to enable zero-config mode`
|
||||
);
|
||||
} else {
|
||||
builder.log.info(`Detected ${platform.name}, using zero-config mode`);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
// @ts-ignore
|
||||
pages = 'build',
|
||||
assets = pages,
|
||||
fallback,
|
||||
precompress
|
||||
} = options ?? platform?.defaults ?? /** @type {import('./index.js').AdapterOptions} */ ({});
|
||||
|
||||
builder.rimraf(assets);
|
||||
builder.rimraf(pages);
|
||||
|
||||
builder.generateEnvModule();
|
||||
builder.writeClient(assets);
|
||||
builder.writePrerendered(pages);
|
||||
|
||||
if (fallback) {
|
||||
await builder.generateFallback(path.join(pages, fallback));
|
||||
}
|
||||
|
||||
if (precompress) {
|
||||
builder.log.minor('Compressing assets and pages');
|
||||
if (pages === assets) {
|
||||
await builder.compress(assets);
|
||||
} else {
|
||||
await Promise.all([builder.compress(assets), builder.compress(pages)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (pages === assets) {
|
||||
builder.log(`Wrote site to "${pages}"`);
|
||||
} else {
|
||||
builder.log(`Wrote pages to "${pages}" and assets to "${assets}"`);
|
||||
}
|
||||
|
||||
if (!options) platform?.done(builder);
|
||||
}
|
||||
};
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@sveltejs/adapter-static",
|
||||
"version": "3.0.10",
|
||||
"description": "Adapter for SvelteKit apps that prerenders your entire site as a collection of static files",
|
||||
"keywords": [
|
||||
"adapter",
|
||||
"deploy",
|
||||
"hosting",
|
||||
"ssg",
|
||||
"static site generation",
|
||||
"svelte",
|
||||
"sveltekit"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sveltejs/kit.git",
|
||||
"directory": "packages/adapter-static"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://svelte.dev/docs/kit/adapter-static",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"platforms.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0-next.3",
|
||||
"@types/node": "^18.19.119",
|
||||
"sirv": "^3.0.0",
|
||||
"svelte": "^5.39.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.3.5",
|
||||
"@sveltejs/kit": "^2.43.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "prettier --check .",
|
||||
"check": "tsc",
|
||||
"format": "pnpm lint --write",
|
||||
"test": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test"
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import fs from 'node:fs';
|
||||
import process from 'node:process';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string;
|
||||
* test: () => boolean;
|
||||
* defaults: import('./index.js').AdapterOptions;
|
||||
* done: (builder: import('./internal.js').Builder2_0_0) => void;
|
||||
* }}
|
||||
* Platform */
|
||||
|
||||
// This function is duplicated in adapter-vercel
|
||||
/** @param {import('./internal.js').Builder2_0_0} builder */
|
||||
function static_vercel_config(builder) {
|
||||
/** @type {any[]} */
|
||||
const prerendered_redirects = [];
|
||||
|
||||
/** @type {Record<string, { path: string }>} */
|
||||
const overrides = {};
|
||||
|
||||
for (const [src, redirect] of builder.prerendered.redirects) {
|
||||
prerendered_redirects.push({
|
||||
src,
|
||||
headers: {
|
||||
Location: redirect.location
|
||||
},
|
||||
status: redirect.status
|
||||
});
|
||||
}
|
||||
|
||||
for (const [path, page] of builder.prerendered.pages) {
|
||||
if (path.endsWith('/') && path !== '/') {
|
||||
prerendered_redirects.push(
|
||||
{ src: path, dest: path.slice(0, -1) },
|
||||
{ src: path.slice(0, -1), status: 308, headers: { Location: path } }
|
||||
);
|
||||
|
||||
overrides[page.file] = { path: path.slice(1, -1) };
|
||||
} else {
|
||||
overrides[page.file] = { path: path.slice(1) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 3,
|
||||
routes: [
|
||||
...prerendered_redirects,
|
||||
{
|
||||
src: `/${builder.getAppPath()}/immutable/.+`,
|
||||
headers: {
|
||||
'cache-control': 'public, immutable, max-age=31536000'
|
||||
}
|
||||
},
|
||||
{
|
||||
handle: 'filesystem'
|
||||
}
|
||||
],
|
||||
overrides
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {Platform[]} */
|
||||
export const platforms = [
|
||||
{
|
||||
name: 'Vercel',
|
||||
test: () => !!process.env.VERCEL,
|
||||
defaults: {
|
||||
pages: '.vercel/output/static'
|
||||
},
|
||||
done: (builder) => {
|
||||
const config = static_vercel_config(builder);
|
||||
fs.writeFileSync('.vercel/output/config.json', JSON.stringify(config, null, ' '));
|
||||
}
|
||||
}
|
||||
];
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# The fastest way to build Svelte apps
|
||||
|
||||
This is the [SvelteKit](https://svelte.dev/docs/kit) framework and CLI.
|
||||
|
||||
The quickest way to get started is via the [sv](https://npmjs.com/package/sv) package:
|
||||
|
||||
```sh
|
||||
npx sv create my-app
|
||||
cd my-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
See the [documentation](https://svelte.dev/docs/kit) to learn more.
|
||||
|
||||
## Changelog
|
||||
|
||||
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md).
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"name": "@sveltejs/kit",
|
||||
"version": "2.55.0",
|
||||
"description": "SvelteKit is the fastest way to build Svelte apps",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"official",
|
||||
"svelte",
|
||||
"sveltekit",
|
||||
"vite"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sveltejs/kit.git",
|
||||
"directory": "packages/kit"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://svelte.dev",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.6.4",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0-next.3",
|
||||
"@types/connect": "^3.4.38",
|
||||
"@types/node": "^18.19.119",
|
||||
"@types/set-cookie-parser": "^2.4.7",
|
||||
"dts-buddy": "^0.7.0",
|
||||
"rollup": "^4.59.0",
|
||||
"svelte": "^5.53.5",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"svelte-kit": "svelte-kit.js"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"!src/**/*.spec.js",
|
||||
"!src/core/**/fixtures",
|
||||
"!src/core/**/test",
|
||||
"types",
|
||||
"svelte-kit.js"
|
||||
],
|
||||
"imports": {
|
||||
"#app/paths": {
|
||||
"browser": "./src/runtime/app/paths/client.js",
|
||||
"default": "./src/runtime/app/paths/server.js"
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/index.js"
|
||||
},
|
||||
"./internal": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/internal/index.js"
|
||||
},
|
||||
"./internal/server": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/internal/server.js"
|
||||
},
|
||||
"./node": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/node/index.js"
|
||||
},
|
||||
"./node/polyfills": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/node/polyfills.js"
|
||||
},
|
||||
"./hooks": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/hooks/index.js"
|
||||
},
|
||||
"./vite": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/exports/vite/index.js"
|
||||
}
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.13"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "prettier --config ../../.prettierrc --check .",
|
||||
"check": "tsc && cd ./test/types && tsc",
|
||||
"check:all": "tsc && pnpm -r --filter=\"./**\" check",
|
||||
"format": "prettier --config ../../.prettierrc --write .",
|
||||
"test": "pnpm test:unit && pnpm test:integration",
|
||||
"test:integration": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test",
|
||||
"test:cross-platform:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:dev",
|
||||
"test:cross-platform:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:build",
|
||||
"test:server-side-route-resolution:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:dev",
|
||||
"test:server-side-route-resolution:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:server-side-route-resolution:build",
|
||||
"test:svelte-async:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:svelte-async:dev",
|
||||
"test:svelte-async:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:svelte-async:build",
|
||||
"test:unit:dev": "vitest --config kit.vitest.config.js run",
|
||||
"test:unit:prod": "NODE_ENV=production vitest --config kit.vitest.config.js run csp.spec.js cookie.spec.js",
|
||||
"test:unit": "pnpm test:unit:dev && pnpm test:unit:prod",
|
||||
"generate:version": "node scripts/generate-version.js",
|
||||
"generate:types": "node scripts/generate-dts.js"
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import fs from 'node:fs';
|
||||
import process from 'node:process';
|
||||
import { parseArgs } from 'node:util';
|
||||
import colors from 'kleur';
|
||||
import { load_config } from './core/config/index.js';
|
||||
import { coalesce_to_error } from './utils/error.js';
|
||||
|
||||
/** @param {unknown} e */
|
||||
function handle_error(e) {
|
||||
const error = coalesce_to_error(e);
|
||||
|
||||
if (error.name === 'SyntaxError') throw error;
|
||||
|
||||
console.error(colors.bold().red(`> ${error.message}`));
|
||||
if (error.stack) {
|
||||
console.error(colors.gray(error.stack.split('\n').slice(1).join('\n')));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
||||
|
||||
const help = `
|
||||
Usage: svelte-kit <command> [options]
|
||||
|
||||
Commands:
|
||||
sync Synchronise generated type definitions
|
||||
|
||||
Options:
|
||||
--version, -v Show version number
|
||||
--help, -h Show this help message
|
||||
|
||||
Sync Options:
|
||||
--mode <mode> Specify a mode for loading environment variables (default: development)
|
||||
`;
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseArgs({
|
||||
options: {
|
||||
version: { type: 'boolean', short: 'v' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
mode: { type: 'string', default: 'development' }
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: true
|
||||
});
|
||||
} catch (err) {
|
||||
const error = /** @type {Error} */ (err);
|
||||
console.error(colors.bold().red(`> ${error.message}`));
|
||||
console.log(help);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { values, positionals } = parsed;
|
||||
|
||||
if (values.version) {
|
||||
console.log(pkg.version);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (values.help) {
|
||||
console.log(help);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const command = positionals[0];
|
||||
|
||||
if (!command) {
|
||||
console.log(help);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (command === 'sync') {
|
||||
const config_files = ['js', 'ts']
|
||||
.map((ext) => `svelte.config.${ext}`)
|
||||
.filter((f) => fs.existsSync(f));
|
||||
if (config_files.length === 0) {
|
||||
console.warn(`Missing Svelte config file in ${process.cwd()} — skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await load_config();
|
||||
const sync = await import('./core/sync/sync.js');
|
||||
sync.all_types(config, values.mode);
|
||||
} catch (error) {
|
||||
handle_error(error);
|
||||
}
|
||||
} else {
|
||||
console.error(colors.bold().red(`> Unknown command: ${command}`));
|
||||
console.log(help);
|
||||
process.exit(1);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A fake asset path used in `vite dev` and `vite preview`, so that we can
|
||||
* serve local assets while verifying that requests are correctly prefixed
|
||||
*/
|
||||
export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets';
|
||||
|
||||
export const GENERATED_COMMENT = '// this file is generated — do not edit it\n';
|
||||
|
||||
export const ENDPOINT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
||||
|
||||
export const MUTATIVE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
|
||||
export const PAGE_METHODS = ['GET', 'POST', 'HEAD'];
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
/** @import { Builder } from '@sveltejs/kit' */
|
||||
/** @import { ResolvedConfig } from 'vite' */
|
||||
/** @import { RouteDefinition } from '@sveltejs/kit' */
|
||||
/** @import { RouteData, ValidatedConfig, BuildData, ServerMetadata, ServerMetadataRoute, Prerendered, PrerenderMap, Logger, RemoteChunk } from 'types' */
|
||||
import colors from 'kleur';
|
||||
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs';
|
||||
import { extname, resolve, join, dirname, relative } from 'node:path';
|
||||
import { pipeline } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import zlib from 'node:zlib';
|
||||
import { copy, rimraf, mkdirp, posixify } from '../../utils/filesystem.js';
|
||||
import { generate_manifest } from '../generate_manifest/index.js';
|
||||
import { get_route_segments } from '../../utils/routing.js';
|
||||
import { get_env } from '../../exports/vite/utils.js';
|
||||
import generate_fallback from '../postbuild/fallback.js';
|
||||
import { write } from '../sync/utils.js';
|
||||
import { list_files } from '../utils.js';
|
||||
import { find_server_assets } from '../generate_manifest/find_server_assets.js';
|
||||
import { reserved } from '../env.js';
|
||||
|
||||
const pipe = promisify(pipeline);
|
||||
const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.wasm', '.txt'];
|
||||
|
||||
/**
|
||||
* Creates the Builder which is passed to adapters for building the application.
|
||||
* @param {{
|
||||
* config: ValidatedConfig;
|
||||
* build_data: BuildData;
|
||||
* server_metadata: ServerMetadata;
|
||||
* route_data: RouteData[];
|
||||
* prerendered: Prerendered;
|
||||
* prerender_map: PrerenderMap;
|
||||
* log: Logger;
|
||||
* vite_config: ResolvedConfig;
|
||||
* remotes: RemoteChunk[]
|
||||
* }} opts
|
||||
* @returns {Builder}
|
||||
*/
|
||||
export function create_builder({
|
||||
config,
|
||||
build_data,
|
||||
server_metadata,
|
||||
route_data,
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
vite_config,
|
||||
remotes
|
||||
}) {
|
||||
/** @type {Map<RouteDefinition, RouteData>} */
|
||||
const lookup = new Map();
|
||||
|
||||
/**
|
||||
* Rather than exposing the internal `RouteData` type, which is subject to change,
|
||||
* we expose a stable type that adapters can use to group/filter routes
|
||||
*/
|
||||
const routes = route_data.map((route) => {
|
||||
const { config, methods, page, api } = /** @type {ServerMetadataRoute} */ (
|
||||
server_metadata.routes.get(route.id)
|
||||
);
|
||||
|
||||
/** @type {RouteDefinition} */
|
||||
const facade = {
|
||||
id: route.id,
|
||||
api,
|
||||
page,
|
||||
segments: get_route_segments(route.id).map((segment) => ({
|
||||
dynamic: segment.includes('['),
|
||||
rest: segment.includes('[...'),
|
||||
content: segment
|
||||
})),
|
||||
pattern: route.pattern,
|
||||
prerender: prerender_map.get(route.id) ?? false,
|
||||
methods,
|
||||
config
|
||||
};
|
||||
|
||||
lookup.set(facade, route);
|
||||
|
||||
return facade;
|
||||
});
|
||||
|
||||
return {
|
||||
log,
|
||||
rimraf,
|
||||
mkdirp,
|
||||
copy,
|
||||
|
||||
config,
|
||||
prerendered,
|
||||
routes,
|
||||
|
||||
async compress(directory) {
|
||||
if (!existsSync(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = list_files(directory, (file) => extensions.includes(extname(file))).map(
|
||||
(file) => resolve(directory, file)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
files.flatMap((file) => [compress_file(file, 'gz'), compress_file(file, 'br')])
|
||||
);
|
||||
},
|
||||
|
||||
async createEntries(fn) {
|
||||
const seen = new Set();
|
||||
|
||||
for (let i = 0; i < route_data.length; i += 1) {
|
||||
const route = route_data[i];
|
||||
if (prerender_map.get(route.id) === true) continue;
|
||||
const { id, filter, complete } = fn(routes[i]);
|
||||
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
|
||||
const group = [route];
|
||||
|
||||
// figure out which lower priority routes should be considered fallbacks
|
||||
for (let j = i + 1; j < route_data.length; j += 1) {
|
||||
if (prerender_map.get(routes[j].id) === true) continue;
|
||||
if (filter(routes[j])) {
|
||||
group.push(route_data[j]);
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = new Set(group);
|
||||
|
||||
// heuristic: if /foo/[bar] is included, /foo/[bar].json should
|
||||
// also be included, since the page likely needs the endpoint
|
||||
// TODO is this still necessary, given the new way of doing things?
|
||||
filtered.forEach((route) => {
|
||||
if (route.page) {
|
||||
const endpoint = route_data.find((candidate) => candidate.id === route.id + '.json');
|
||||
|
||||
if (endpoint) {
|
||||
filtered.add(endpoint);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.size > 0) {
|
||||
await complete({
|
||||
generateManifest: ({ relativePath }) =>
|
||||
generate_manifest({
|
||||
build_data,
|
||||
prerendered: [],
|
||||
relative_path: relativePath,
|
||||
routes: Array.from(filtered),
|
||||
remotes
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findServerAssets(route_data) {
|
||||
return find_server_assets(
|
||||
build_data,
|
||||
route_data.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route)))
|
||||
);
|
||||
},
|
||||
|
||||
async generateFallback(dest) {
|
||||
const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`;
|
||||
const env = get_env(config.kit.env, vite_config.mode);
|
||||
|
||||
const fallback = await generate_fallback({
|
||||
manifest_path,
|
||||
env: { ...env.private, ...env.public }
|
||||
});
|
||||
|
||||
if (existsSync(dest)) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Overwriting ${dest} with fallback page. Consider using a different name for the fallback.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
write(dest, fallback);
|
||||
},
|
||||
|
||||
generateEnvModule() {
|
||||
const dest = `${config.kit.outDir}/output/prerendered/dependencies/${config.kit.appDir}/env.js`;
|
||||
const env = get_env(config.kit.env, vite_config.mode);
|
||||
|
||||
write(dest, `export const env=${JSON.stringify(env.public)}`);
|
||||
},
|
||||
|
||||
generateManifest({ relativePath, routes: subset }) {
|
||||
return generate_manifest({
|
||||
build_data,
|
||||
prerendered: prerendered.paths,
|
||||
relative_path: relativePath,
|
||||
routes: subset
|
||||
? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route)))
|
||||
: route_data.filter((route) => prerender_map.get(route.id) !== true),
|
||||
remotes
|
||||
});
|
||||
},
|
||||
|
||||
getBuildDirectory(name) {
|
||||
return `${config.kit.outDir}/${name}`;
|
||||
},
|
||||
|
||||
getClientDirectory() {
|
||||
return `${config.kit.outDir}/output/client`;
|
||||
},
|
||||
|
||||
getServerDirectory() {
|
||||
return `${config.kit.outDir}/output/server`;
|
||||
},
|
||||
|
||||
getAppPath() {
|
||||
return build_data.app_path;
|
||||
},
|
||||
|
||||
writeClient(dest) {
|
||||
return copy(`${config.kit.outDir}/output/client`, dest, {
|
||||
// avoid making vite build artefacts public
|
||||
filter: (basename) => basename !== '.vite'
|
||||
});
|
||||
},
|
||||
|
||||
writePrerendered(dest) {
|
||||
const source = `${config.kit.outDir}/output/prerendered`;
|
||||
|
||||
return [
|
||||
...copy(`${source}/pages`, dest),
|
||||
...copy(`${source}/dependencies`, dest),
|
||||
...copy(`${source}/data`, dest)
|
||||
];
|
||||
},
|
||||
|
||||
writeServer(dest) {
|
||||
return copy(`${config.kit.outDir}/output/server`, dest);
|
||||
},
|
||||
|
||||
hasServerInstrumentationFile() {
|
||||
return existsSync(`${config.kit.outDir}/output/server/instrumentation.server.js`);
|
||||
},
|
||||
|
||||
instrument({
|
||||
entrypoint,
|
||||
instrumentation,
|
||||
start = join(dirname(entrypoint), 'start.js'),
|
||||
module = {
|
||||
exports: ['default']
|
||||
}
|
||||
}) {
|
||||
if (!existsSync(instrumentation)) {
|
||||
throw new Error(
|
||||
`Instrumentation file ${instrumentation} not found. This is probably a bug in your adapter.`
|
||||
);
|
||||
}
|
||||
if (!existsSync(entrypoint)) {
|
||||
throw new Error(
|
||||
`Entrypoint file ${entrypoint} not found. This is probably a bug in your adapter.`
|
||||
);
|
||||
}
|
||||
|
||||
copy(entrypoint, start);
|
||||
if (existsSync(`${entrypoint}.map`)) {
|
||||
copy(`${entrypoint}.map`, `${start}.map`);
|
||||
}
|
||||
|
||||
const relative_instrumentation = posixify(relative(dirname(entrypoint), instrumentation));
|
||||
const relative_start = posixify(relative(dirname(entrypoint), start));
|
||||
|
||||
const facade =
|
||||
'generateText' in module
|
||||
? module.generateText({
|
||||
instrumentation: relative_instrumentation,
|
||||
start: relative_start
|
||||
})
|
||||
: create_instrumentation_facade({
|
||||
instrumentation: relative_instrumentation,
|
||||
start: relative_start,
|
||||
exports: module.exports
|
||||
});
|
||||
|
||||
rimraf(entrypoint);
|
||||
write(entrypoint, facade);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {'gz' | 'br'} format
|
||||
*/
|
||||
async function compress_file(file, format = 'gz') {
|
||||
const compress =
|
||||
format == 'br'
|
||||
? zlib.createBrotliCompress({
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
||||
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size
|
||||
}
|
||||
})
|
||||
: zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION });
|
||||
|
||||
const source = createReadStream(file);
|
||||
const destination = createWriteStream(`${file}.${format}`);
|
||||
|
||||
await pipe(source, compress, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of exports, generate a facade that:
|
||||
* - Imports the instrumentation file
|
||||
* - Imports `exports` from the entrypoint (dynamically, if `tla` is true)
|
||||
* - Re-exports `exports` from the entrypoint
|
||||
*
|
||||
* `default` receives special treatment: It will be imported as `default` and exported with `export default`.
|
||||
*
|
||||
* @param {{ instrumentation: string; start: string; exports: string[] }} opts
|
||||
* @returns {string}
|
||||
*/
|
||||
function create_instrumentation_facade({ instrumentation, start, exports }) {
|
||||
const import_instrumentation = `import './${instrumentation}';`;
|
||||
|
||||
let alias_index = 0;
|
||||
const aliases = new Map();
|
||||
|
||||
for (const name of exports.filter((name) => reserved.has(name))) {
|
||||
/*
|
||||
* you can do evil things like `export { c as class }`.
|
||||
* in order to import these, you need to alias them, and then un-alias them when re-exporting
|
||||
* this map will allow us to generate the following:
|
||||
* import { class as _1 } from 'entrypoint';
|
||||
* export { _1 as class };
|
||||
*/
|
||||
let alias = `_${alias_index++}`;
|
||||
while (exports.includes(alias)) {
|
||||
alias = `_${alias_index++}`;
|
||||
}
|
||||
|
||||
aliases.set(name, alias);
|
||||
}
|
||||
|
||||
const import_statements = [];
|
||||
const export_statements = [];
|
||||
|
||||
for (const name of exports) {
|
||||
const alias = aliases.get(name);
|
||||
if (alias) {
|
||||
import_statements.push(`${name}: ${alias}`);
|
||||
export_statements.push(`${alias} as ${name}`);
|
||||
} else {
|
||||
import_statements.push(`${name}`);
|
||||
export_statements.push(`${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const entrypoint_facade = [
|
||||
`const { ${import_statements.join(', ')} } = await import('./${start}');`,
|
||||
export_statements.length > 0 ? `export { ${export_statements.join(', ')} };` : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
return `${import_instrumentation}\n${entrypoint_facade}`;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import colors from 'kleur';
|
||||
import { create_builder } from './builder.js';
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').BuildData} build_data
|
||||
* @param {import('types').ServerMetadata} server_metadata
|
||||
* @param {import('types').Prerendered} prerendered
|
||||
* @param {import('types').PrerenderMap} prerender_map
|
||||
* @param {import('types').Logger} log
|
||||
* @param {import('types').RemoteChunk[]} remotes
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
*/
|
||||
export async function adapt(
|
||||
config,
|
||||
build_data,
|
||||
server_metadata,
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
remotes,
|
||||
vite_config
|
||||
) {
|
||||
// This is only called when adapter is truthy, so the cast is safe
|
||||
const { name, adapt } = /** @type {import('@sveltejs/kit').Adapter} */ (config.kit.adapter);
|
||||
|
||||
console.log(colors.bold().cyan(`\n> Using ${name}`));
|
||||
|
||||
const builder = create_builder({
|
||||
config,
|
||||
build_data,
|
||||
server_metadata,
|
||||
route_data: build_data.manifest_data.routes.filter((route) => route.page || route.endpoint),
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
remotes,
|
||||
vite_config
|
||||
});
|
||||
|
||||
await adapt(builder);
|
||||
|
||||
log.success('done');
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>%sveltekit.error.message%</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
--bg: white;
|
||||
--fg: #222;
|
||||
--divider: #ccc;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 32rem;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: 200;
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
top: -0.05rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
border-left: 1px solid var(--divider);
|
||||
padding: 0 0 0 1rem;
|
||||
margin: 0 0 0 1rem;
|
||||
min-height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message h1 {
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
--bg: #222;
|
||||
--fg: #ddd;
|
||||
--divider: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error">
|
||||
<span class="status">%sveltekit.status%</span>
|
||||
<div class="message">
|
||||
<h1>%sveltekit.error.message%</h1>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import * as url from 'node:url';
|
||||
import options from './options.js';
|
||||
|
||||
/**
|
||||
* Loads the template (src/app.html by default) and validates that it has the
|
||||
* required content.
|
||||
* @param {string} cwd
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function load_template(cwd, { kit }) {
|
||||
const { env, files } = kit;
|
||||
|
||||
const relative = path.relative(cwd, files.appTemplate);
|
||||
|
||||
if (!fs.existsSync(files.appTemplate)) {
|
||||
throw new Error(`${relative} does not exist`);
|
||||
}
|
||||
|
||||
const contents = fs.readFileSync(files.appTemplate, 'utf8');
|
||||
|
||||
const expected_tags = ['%sveltekit.head%', '%sveltekit.body%'];
|
||||
expected_tags.forEach((tag) => {
|
||||
if (contents.indexOf(tag) === -1) {
|
||||
throw new Error(`${relative} is missing ${tag}`);
|
||||
}
|
||||
});
|
||||
|
||||
for (const match of contents.matchAll(/%sveltekit\.env\.([^%]+)%/g)) {
|
||||
if (!match[1].startsWith(env.publicPrefix)) {
|
||||
throw new Error(
|
||||
`Environment variables in ${relative} must start with ${env.publicPrefix} (saw %sveltekit.env.${match[1]}%)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the error page (src/error.html by default) if it exists.
|
||||
* Falls back to a generic error page content.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function load_error_page(config) {
|
||||
let { errorTemplate } = config.kit.files;
|
||||
|
||||
// Don't do this inside resolving the config, because that would mean
|
||||
// adding/removing error.html isn't detected and would require a restart.
|
||||
if (!fs.existsSync(config.kit.files.errorTemplate)) {
|
||||
errorTemplate = url.fileURLToPath(new URL('./default-error.html', import.meta.url));
|
||||
}
|
||||
|
||||
return fs.readFileSync(errorTemplate, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and validates Svelte config file
|
||||
* @param {{ cwd?: string }} options
|
||||
* @returns {Promise<import('types').ValidatedConfig>}
|
||||
*/
|
||||
export async function load_config({ cwd = process.cwd() } = {}) {
|
||||
const config_files = ['js', 'ts']
|
||||
.map((ext) => path.join(cwd, `svelte.config.${ext}`))
|
||||
.filter((f) => fs.existsSync(f));
|
||||
|
||||
if (config_files.length === 0) {
|
||||
console.log(
|
||||
`No Svelte config file found in ${cwd} - using SvelteKit's default configuration without an adapter.`
|
||||
);
|
||||
return process_config({}, { cwd });
|
||||
}
|
||||
const config_file = config_files[0];
|
||||
if (config_files.length > 1) {
|
||||
console.log(
|
||||
`Found multiple Svelte config files in ${cwd}: ${config_files.map((f) => path.basename(f)).join(', ')}. Using ${path.basename(config_file)}`
|
||||
);
|
||||
}
|
||||
const config = await import(`${url.pathToFileURL(config_file).href}?ts=${Date.now()}`);
|
||||
|
||||
try {
|
||||
return process_config(config.default, { cwd });
|
||||
} catch (e) {
|
||||
const error = /** @type {Error} */ (e);
|
||||
|
||||
// redact the stack trace — it's not helpful to users
|
||||
error.stack = `Could not load ${config_file}: ${error.message}\n`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').Config} config
|
||||
* @returns {import('types').ValidatedConfig}
|
||||
*/
|
||||
function process_config(config, { cwd = process.cwd() } = {}) {
|
||||
const validated = validate_config(config);
|
||||
|
||||
validated.kit.outDir = path.resolve(cwd, validated.kit.outDir);
|
||||
|
||||
for (const key in validated.kit.files) {
|
||||
if (key === 'hooks') {
|
||||
validated.kit.files.hooks.client = path.resolve(cwd, validated.kit.files.hooks.client);
|
||||
validated.kit.files.hooks.server = path.resolve(cwd, validated.kit.files.hooks.server);
|
||||
validated.kit.files.hooks.universal = path.resolve(cwd, validated.kit.files.hooks.universal);
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
validated.kit.files[key] = path.resolve(cwd, validated.kit.files[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').Config} config
|
||||
* @returns {import('types').ValidatedConfig}
|
||||
*/
|
||||
export function validate_config(config) {
|
||||
if (typeof config !== 'object') {
|
||||
throw new Error(
|
||||
'The Svelte config file must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
|
||||
);
|
||||
}
|
||||
|
||||
const validated = options(config, 'config');
|
||||
const files = validated.kit.files;
|
||||
|
||||
files.hooks.client ??= path.join(files.src, 'hooks.client');
|
||||
files.hooks.server ??= path.join(files.src, 'hooks.server');
|
||||
files.hooks.universal ??= path.join(files.src, 'hooks');
|
||||
files.lib ??= path.join(files.src, 'lib');
|
||||
files.params ??= path.join(files.src, 'params');
|
||||
files.routes ??= path.join(files.src, 'routes');
|
||||
files.serviceWorker ??= path.join(files.src, 'service-worker');
|
||||
files.appTemplate ??= path.join(files.src, 'app.html');
|
||||
files.errorTemplate ??= path.join(files.src, 'error.html');
|
||||
|
||||
if (validated.kit.router.resolution === 'server') {
|
||||
if (validated.kit.router.type === 'hash') {
|
||||
throw new Error(
|
||||
"The `router.resolution` option cannot be 'server' if `router.type` is 'hash'"
|
||||
);
|
||||
}
|
||||
if (validated.kit.output.bundleStrategy !== 'split') {
|
||||
throw new Error(
|
||||
"The `router.resolution` option cannot be 'server' if `output.bundleStrategy` is 'inline' or 'single'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
import process from 'node:process';
|
||||
import colors from 'kleur';
|
||||
|
||||
/** @typedef {import('./types.js').Validator} Validator */
|
||||
|
||||
const directives = object({
|
||||
'child-src': string_array(),
|
||||
'default-src': string_array(),
|
||||
'frame-src': string_array(),
|
||||
'worker-src': string_array(),
|
||||
'connect-src': string_array(),
|
||||
'font-src': string_array(),
|
||||
'img-src': string_array(),
|
||||
'manifest-src': string_array(),
|
||||
'media-src': string_array(),
|
||||
'object-src': string_array(),
|
||||
'prefetch-src': string_array(),
|
||||
'script-src': string_array(),
|
||||
'script-src-elem': string_array(),
|
||||
'script-src-attr': string_array(),
|
||||
'style-src': string_array(),
|
||||
'style-src-elem': string_array(),
|
||||
'style-src-attr': string_array(),
|
||||
'base-uri': string_array(),
|
||||
sandbox: string_array(),
|
||||
'form-action': string_array(),
|
||||
'frame-ancestors': string_array(),
|
||||
'navigate-to': string_array(),
|
||||
'report-uri': string_array(),
|
||||
'report-to': string_array(),
|
||||
'require-trusted-types-for': string_array(),
|
||||
'trusted-types': string_array(),
|
||||
'upgrade-insecure-requests': boolean(false),
|
||||
'require-sri-for': string_array(),
|
||||
'block-all-mixed-content': boolean(false),
|
||||
'plugin-types': string_array(),
|
||||
referrer: string_array()
|
||||
});
|
||||
|
||||
/** @type {Validator} */
|
||||
const options = object(
|
||||
{
|
||||
extensions: validate(['.svelte'], (input, keypath) => {
|
||||
if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {
|
||||
throw new Error(`${keypath} must be an array of strings`);
|
||||
}
|
||||
|
||||
input.forEach((extension) => {
|
||||
if (extension[0] !== '.') {
|
||||
throw new Error(`Each member of ${keypath} must start with '.' — saw '${extension}'`);
|
||||
}
|
||||
|
||||
if (!/^(\.[a-z0-9]+)+$/i.test(extension)) {
|
||||
throw new Error(`File extensions must be alphanumeric — saw '${extension}'`);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
kit: object({
|
||||
adapter: validate(null, (input, keypath) => {
|
||||
if (typeof input !== 'object' || !input.adapt) {
|
||||
let message = `${keypath} should be an object with an "adapt" method`;
|
||||
|
||||
if (Array.isArray(input) || typeof input === 'string') {
|
||||
// for the early adapter adopters
|
||||
message += ', rather than the name of an adapter';
|
||||
}
|
||||
|
||||
throw new Error(`${message}. See https://svelte.dev/docs/kit/adapters`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
alias: validate({}, (input, keypath) => {
|
||||
if (typeof input !== 'object') {
|
||||
throw new Error(`${keypath} should be an object`);
|
||||
}
|
||||
|
||||
for (const key in input) {
|
||||
assert_string(input[key], `${keypath}.${key}`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
appDir: validate('_app', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (input) {
|
||||
if (input.startsWith('/') || input.endsWith('/')) {
|
||||
throw new Error(
|
||||
"config.kit.appDir cannot start or end with '/'. See https://svelte.dev/docs/kit/configuration"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${keypath} cannot be empty`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
csp: object({
|
||||
mode: list(['auto', 'hash', 'nonce']),
|
||||
directives,
|
||||
reportOnly: directives
|
||||
}),
|
||||
|
||||
csrf: object({
|
||||
checkOrigin: deprecate(
|
||||
boolean(true),
|
||||
(keypath) =>
|
||||
`\`${keypath}\` has been deprecated in favour of \`csrf.trustedOrigins\`. It will be removed in a future version`
|
||||
),
|
||||
trustedOrigins: string_array([])
|
||||
}),
|
||||
|
||||
embedded: boolean(false),
|
||||
|
||||
env: object({
|
||||
dir: string(process.cwd()),
|
||||
publicPrefix: string('PUBLIC_'),
|
||||
privatePrefix: string('')
|
||||
}),
|
||||
|
||||
experimental: object({
|
||||
tracing: object({
|
||||
server: boolean(false)
|
||||
}),
|
||||
instrumentation: object({
|
||||
server: boolean(false)
|
||||
}),
|
||||
remoteFunctions: boolean(false),
|
||||
forkPreloads: boolean(false),
|
||||
handleRenderingErrors: boolean(false)
|
||||
}),
|
||||
|
||||
files: object({
|
||||
src: string('src'),
|
||||
assets: string('static'),
|
||||
hooks: object({
|
||||
client: string(null),
|
||||
server: string(null),
|
||||
universal: string(null)
|
||||
}),
|
||||
lib: string(null),
|
||||
params: string(null),
|
||||
routes: string(null),
|
||||
serviceWorker: string(null),
|
||||
appTemplate: string(null),
|
||||
errorTemplate: string(null)
|
||||
}),
|
||||
|
||||
inlineStyleThreshold: number(0),
|
||||
|
||||
moduleExtensions: string_array(['.js', '.ts']),
|
||||
|
||||
outDir: string('.svelte-kit'),
|
||||
|
||||
output: object({
|
||||
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs']),
|
||||
bundleStrategy: list(['split', 'single', 'inline'])
|
||||
}),
|
||||
|
||||
paths: object({
|
||||
base: validate('', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (input !== '' && (input.endsWith('/') || !input.startsWith('/'))) {
|
||||
throw new Error(
|
||||
`${keypath} option must either be the empty string or a root-relative path that starts but doesn't end with '/'. See https://svelte.dev/docs/kit/configuration#paths`
|
||||
);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
assets: validate('', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (input) {
|
||||
if (!/^[a-z]+:\/\//.test(input)) {
|
||||
throw new Error(
|
||||
`${keypath} option must be an absolute path, if specified. See https://svelte.dev/docs/kit/configuration#paths`
|
||||
);
|
||||
}
|
||||
|
||||
if (input.endsWith('/')) {
|
||||
throw new Error(
|
||||
`${keypath} option must not end with '/'. See https://svelte.dev/docs/kit/configuration#paths`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
relative: boolean(true)
|
||||
}),
|
||||
|
||||
prerender: object({
|
||||
concurrency: number(1),
|
||||
crawl: boolean(true),
|
||||
entries: validate(['*'], (input, keypath) => {
|
||||
if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {
|
||||
throw new Error(`${keypath} must be an array of strings`);
|
||||
}
|
||||
|
||||
input.forEach((page) => {
|
||||
if (page !== '*' && page[0] !== '/') {
|
||||
throw new Error(
|
||||
`Each member of ${keypath} must be either '*' or an absolute path beginning with '/' — saw '${page}'`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
handleHttpError: validate(
|
||||
(/** @type {any} */ { message }) => {
|
||||
throw new Error(
|
||||
message +
|
||||
'\nTo suppress or handle this error, implement `handleHttpError` in https://svelte.dev/docs/kit/configuration#prerender'
|
||||
);
|
||||
},
|
||||
(input, keypath) => {
|
||||
if (typeof input === 'function') return input;
|
||||
if (['fail', 'warn', 'ignore'].includes(input)) return input;
|
||||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`);
|
||||
}
|
||||
),
|
||||
|
||||
handleMissingId: validate(
|
||||
(/** @type {any} */ { message }) => {
|
||||
throw new Error(
|
||||
message +
|
||||
'\nTo suppress or handle this error, implement `handleMissingId` in https://svelte.dev/docs/kit/configuration#prerender'
|
||||
);
|
||||
},
|
||||
(input, keypath) => {
|
||||
if (typeof input === 'function') return input;
|
||||
if (['fail', 'warn', 'ignore'].includes(input)) return input;
|
||||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`);
|
||||
}
|
||||
),
|
||||
|
||||
handleEntryGeneratorMismatch: validate(
|
||||
(/** @type {any} */ { message }) => {
|
||||
throw new Error(
|
||||
message +
|
||||
'\nTo suppress or handle this error, implement `handleEntryGeneratorMismatch` in https://svelte.dev/docs/kit/configuration#prerender'
|
||||
);
|
||||
},
|
||||
(input, keypath) => {
|
||||
if (typeof input === 'function') return input;
|
||||
if (['fail', 'warn', 'ignore'].includes(input)) return input;
|
||||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`);
|
||||
}
|
||||
),
|
||||
|
||||
handleUnseenRoutes: validate(
|
||||
(/** @type {any} */ { message }) => {
|
||||
throw new Error(
|
||||
message +
|
||||
'\nTo suppress or handle this error, implement `handleUnseenRoutes` in https://svelte.dev/docs/kit/configuration#prerender'
|
||||
);
|
||||
},
|
||||
(input, keypath) => {
|
||||
if (typeof input === 'function') return input;
|
||||
if (['fail', 'warn', 'ignore'].includes(input)) return input;
|
||||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`);
|
||||
}
|
||||
),
|
||||
|
||||
origin: validate('http://sveltekit-prerender', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
let origin;
|
||||
|
||||
try {
|
||||
origin = new URL(input).origin;
|
||||
} catch {
|
||||
throw new Error(`${keypath} must be a valid origin`);
|
||||
}
|
||||
|
||||
if (input !== origin) {
|
||||
throw new Error(`${keypath} must be a valid origin (${origin} rather than ${input})`);
|
||||
}
|
||||
|
||||
return origin;
|
||||
})
|
||||
}),
|
||||
|
||||
router: object({
|
||||
type: list(['pathname', 'hash']),
|
||||
resolution: list(['client', 'server'])
|
||||
}),
|
||||
|
||||
serviceWorker: object({
|
||||
register: boolean(true),
|
||||
// options could be undefined but if it is defined we only validate that
|
||||
// it's an object since the type comes from the browser itself
|
||||
options: validate(undefined, object({}, true)),
|
||||
files: fun((filename) => !/\.DS_Store/.test(filename))
|
||||
}),
|
||||
|
||||
typescript: object({
|
||||
config: fun((config) => config)
|
||||
}),
|
||||
|
||||
version: object({
|
||||
name: string(Date.now().toString()),
|
||||
pollInterval: number(0)
|
||||
})
|
||||
})
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {Validator} fn
|
||||
* @param {(keypath: string) => string} get_message
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function deprecate(
|
||||
fn,
|
||||
get_message = (keypath) =>
|
||||
`The \`${keypath}\` option is deprecated, and will be removed in a future version`
|
||||
) {
|
||||
return (input, keypath) => {
|
||||
if (input !== undefined) {
|
||||
console.warn(colors.bold().yellow(get_message(keypath)));
|
||||
}
|
||||
|
||||
return fn(input, keypath);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, Validator>} children
|
||||
* @param {boolean} [allow_unknown]
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function object(children, allow_unknown = false) {
|
||||
return (input, keypath) => {
|
||||
/** @type {Record<string, any>} */
|
||||
const output = {};
|
||||
|
||||
if ((input && typeof input !== 'object') || Array.isArray(input)) {
|
||||
throw new Error(`${keypath} should be an object`);
|
||||
}
|
||||
|
||||
for (const key in input) {
|
||||
if (!(key in children)) {
|
||||
if (allow_unknown) {
|
||||
output[key] = input[key];
|
||||
} else {
|
||||
let message = `Unexpected option ${keypath}.${key}`;
|
||||
|
||||
// special case
|
||||
if (keypath === 'config.kit' && key in options) {
|
||||
message += ` (did you mean config.${key}?)`;
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in children) {
|
||||
const validator = children[key];
|
||||
output[key] = validator(input && input[key], `${keypath}.${key}`);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} fallback
|
||||
* @param {(value: any, keypath: string) => any} fn
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function validate(fallback, fn) {
|
||||
return (input, keypath) => {
|
||||
return input === undefined ? fallback : fn(input, keypath);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | null} fallback
|
||||
* @param {boolean} allow_empty
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function string(fallback, allow_empty = true) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (!allow_empty && input === '') {
|
||||
throw new Error(`${keypath} cannot be empty`);
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[] | undefined} [fallback]
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function string_array(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (!Array.isArray(input) || input.some((value) => typeof value !== 'string')) {
|
||||
throw new Error(`${keypath} must be an array of strings, if specified`);
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fallback
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function number(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (typeof input !== 'number') {
|
||||
throw new Error(`${keypath} should be a number, if specified`);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} fallback
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function boolean(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (typeof input !== 'boolean') {
|
||||
throw new Error(`${keypath} should be true or false, if specified`);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} options
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function list(options, fallback = options[0]) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (!options.includes(input)) {
|
||||
// prettier-ignore
|
||||
const msg = options.length > 2
|
||||
? `${keypath} should be one of ${options.slice(0, -1).map(input => `"${input}"`).join(', ')} or "${options[options.length - 1]}"`
|
||||
: `${keypath} should be either "${options[0]}" or "${options[1]}"`;
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(...args: any) => any} fallback
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function fun(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (typeof input !== 'function') {
|
||||
throw new Error(`${keypath} should be a function, if specified`);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {string} keypath
|
||||
*/
|
||||
function assert_string(input, keypath) {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error(`${keypath} should be a string, if specified`);
|
||||
}
|
||||
}
|
||||
|
||||
export default options;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type Validator<T = any> = (input: T, keypath: string) => T;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { GENERATED_COMMENT } from '../constants.js';
|
||||
import { dedent } from './sync/utils.js';
|
||||
import { runtime_base } from './utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {'public' | 'private'} EnvType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {string}
|
||||
*/
|
||||
export function create_static_module(id, env) {
|
||||
/** @type {string[]} */
|
||||
const declarations = [];
|
||||
|
||||
for (const key in env) {
|
||||
if (!valid_identifier.test(key) || reserved.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const comment = `/** @type {import('${id}').${key}} */`;
|
||||
const declaration = `export const ${key} = ${JSON.stringify(env[key])};`;
|
||||
|
||||
declarations.push(`${comment}\n${declaration}`);
|
||||
}
|
||||
|
||||
return GENERATED_COMMENT + declarations.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EnvType} type
|
||||
* @param {Record<string, string> | undefined} dev_values If in a development mode, values to pre-populate the module with.
|
||||
*/
|
||||
export function create_dynamic_module(type, dev_values) {
|
||||
if (dev_values) {
|
||||
const keys = Object.entries(dev_values).map(
|
||||
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
||||
);
|
||||
return `export const env = {\n${keys.join(',\n')}\n}`;
|
||||
}
|
||||
return `export { ${type}_env as env } from '${runtime_base}/shared-server.js';`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EnvType} id
|
||||
* @param {import('types').Env} env
|
||||
* @returns {string}
|
||||
*/
|
||||
export function create_static_types(id, env) {
|
||||
const declarations = Object.keys(env[id])
|
||||
.filter((k) => valid_identifier.test(k))
|
||||
.map((k) => `export const ${k}: string;`);
|
||||
|
||||
return dedent`
|
||||
declare module '$env/static/${id}' {
|
||||
${declarations.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EnvType} id
|
||||
* @param {import('types').Env} env
|
||||
* @param {{
|
||||
* public_prefix: string;
|
||||
* private_prefix: string;
|
||||
* }} prefixes
|
||||
* @returns {string}
|
||||
*/
|
||||
export function create_dynamic_types(id, env, { public_prefix, private_prefix }) {
|
||||
const properties = Object.keys(env[id])
|
||||
.filter((k) => valid_identifier.test(k))
|
||||
.map((k) => `${k}: string;`);
|
||||
|
||||
const public_prefixed = `[key: \`${public_prefix}\${string}\`]`;
|
||||
const private_prefixed = `[key: \`${private_prefix}\${string}\`]`;
|
||||
|
||||
if (id === 'private') {
|
||||
if (public_prefix) {
|
||||
properties.push(`${public_prefixed}: undefined;`);
|
||||
}
|
||||
properties.push(`${private_prefixed}: string | undefined;`);
|
||||
} else {
|
||||
if (private_prefix) {
|
||||
properties.push(`${private_prefixed}: undefined;`);
|
||||
}
|
||||
properties.push(`${public_prefixed}: string | undefined;`);
|
||||
}
|
||||
|
||||
return dedent`
|
||||
declare module '$env/dynamic/${id}' {
|
||||
export const env: {
|
||||
${properties.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export const reserved = new Set([
|
||||
'do',
|
||||
'if',
|
||||
'in',
|
||||
'for',
|
||||
'let',
|
||||
'new',
|
||||
'try',
|
||||
'var',
|
||||
'case',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'null',
|
||||
'this',
|
||||
'true',
|
||||
'void',
|
||||
'with',
|
||||
'await',
|
||||
'break',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'false',
|
||||
'super',
|
||||
'throw',
|
||||
'while',
|
||||
'yield',
|
||||
'delete',
|
||||
'export',
|
||||
'import',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'switch',
|
||||
'typeof',
|
||||
'default',
|
||||
'extends',
|
||||
'finally',
|
||||
'package',
|
||||
'private',
|
||||
'continue',
|
||||
'debugger',
|
||||
'function',
|
||||
'arguments',
|
||||
'interface',
|
||||
'protected',
|
||||
'implements',
|
||||
'instanceof'
|
||||
]);
|
||||
|
||||
export const valid_identifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { find_deps } from '../../exports/vite/build/utils.js';
|
||||
|
||||
/**
|
||||
* Finds all the assets that are imported by server files associated with `routes`
|
||||
* @param {import('types').BuildData} build_data
|
||||
* @param {import('types').RouteData[]} routes
|
||||
*/
|
||||
export function find_server_assets(build_data, routes) {
|
||||
/**
|
||||
* All nodes actually used in the routes definition (prerendered routes are omitted).
|
||||
* Root layout/error is always included as they are needed for 404 and root errors.
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const used_nodes = new Set([0, 1]);
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const server_assets = new Set();
|
||||
|
||||
/** @param {string} id */
|
||||
function add_assets(id) {
|
||||
if (id in build_data.server_manifest) {
|
||||
const deps = find_deps(build_data.server_manifest, id, false);
|
||||
for (const asset of deps.assets) {
|
||||
server_assets.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.page) {
|
||||
for (const i of route.page.layouts) used_nodes.add(i);
|
||||
for (const i of route.page.errors) used_nodes.add(i);
|
||||
used_nodes.add(route.page.leaf);
|
||||
}
|
||||
|
||||
if (route.endpoint) {
|
||||
add_assets(route.endpoint.file);
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of used_nodes) {
|
||||
const node = build_data.manifest_data.nodes[n];
|
||||
if (node?.universal) add_assets(node.universal);
|
||||
if (node?.server) add_assets(node.server);
|
||||
}
|
||||
|
||||
if (build_data.manifest_data.hooks.server) {
|
||||
add_assets(build_data.manifest_data.hooks.server);
|
||||
}
|
||||
|
||||
if (build_data.manifest_data.hooks.universal) {
|
||||
add_assets(build_data.manifest_data.hooks.universal);
|
||||
}
|
||||
|
||||
return Array.from(server_assets);
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/** @import { RemoteChunk } from 'types' */
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as mime from 'mrmime';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { get_mime_lookup } from '../utils.js';
|
||||
import { resolve_symlinks } from '../../exports/vite/build/utils.js';
|
||||
import { compact } from '../../utils/array.js';
|
||||
import { join_relative } from '../../utils/filesystem.js';
|
||||
import { dedent } from '../sync/utils.js';
|
||||
import { find_server_assets } from './find_server_assets.js';
|
||||
import { uneval } from 'devalue';
|
||||
|
||||
/**
|
||||
* Generates the data used to write the server-side manifest.js file. This data is used in the Vite
|
||||
* build process, to power routing, etc.
|
||||
* @param {{
|
||||
* build_data: import('types').BuildData;
|
||||
* prerendered: string[];
|
||||
* relative_path: string;
|
||||
* routes: import('types').RouteData[];
|
||||
* remotes: RemoteChunk[];
|
||||
* }} opts
|
||||
*/
|
||||
export function generate_manifest({ build_data, prerendered, relative_path, routes, remotes }) {
|
||||
/**
|
||||
* @type {Map<any, number>} The new index of each node in the filtered nodes array
|
||||
*/
|
||||
const reindexed = new Map();
|
||||
/**
|
||||
* All nodes actually used in the routes definition (prerendered routes are omitted).
|
||||
* Root layout/error is always included as they are needed for 404 and root errors.
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const used_nodes = new Set([0, 1]);
|
||||
|
||||
const server_assets = find_server_assets(build_data, routes);
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.page) {
|
||||
for (const i of route.page.layouts) used_nodes.add(i);
|
||||
for (const i of route.page.errors) used_nodes.add(i);
|
||||
used_nodes.add(route.page.leaf);
|
||||
}
|
||||
}
|
||||
|
||||
const node_paths = compact(
|
||||
build_data.manifest_data.nodes.map((_, i) => {
|
||||
if (used_nodes.has(i)) {
|
||||
reindexed.set(i, reindexed.size);
|
||||
return join_relative(relative_path, `/nodes/${i}.js`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/** @type {(path: string) => string} */
|
||||
const loader = (path) => `__memo(() => import('${path}'))`;
|
||||
|
||||
const assets = build_data.manifest_data.assets.map((asset) => asset.file);
|
||||
if (build_data.service_worker) {
|
||||
assets.push(build_data.service_worker);
|
||||
}
|
||||
|
||||
// In case of server-side route resolution, we need to include all matchers. Prerendered routes are not part
|
||||
// of the server manifest, and they could reference matchers that then would not be included.
|
||||
const matchers = new Set(
|
||||
build_data.client?.nodes ? Object.keys(build_data.manifest_data.matchers) : undefined
|
||||
);
|
||||
|
||||
/** @param {Array<number | undefined>} indexes */
|
||||
function get_nodes(indexes) {
|
||||
const string = indexes.map((n) => reindexed.get(n) ?? '').join(',');
|
||||
|
||||
// since JavaScript ignores trailing commas, we need to insert a dummy
|
||||
// comma so that the array has the correct length if the last item
|
||||
// is undefined
|
||||
return `[${string},]`;
|
||||
}
|
||||
|
||||
const mime_types = get_mime_lookup(build_data.manifest_data);
|
||||
|
||||
/** @type {Record<string, number>} */
|
||||
const files = {};
|
||||
for (const file of server_assets) {
|
||||
files[file] = fs.statSync(path.resolve(build_data.out_dir, 'server', file)).size;
|
||||
|
||||
const ext = path.extname(file);
|
||||
mime_types[ext] ??= mime.lookup(ext) || '';
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
// String representation of
|
||||
/** @template {import('@sveltejs/kit').SSRManifest} T */
|
||||
const manifest_expr = dedent`
|
||||
{
|
||||
appDir: ${s(build_data.app_dir)},
|
||||
appPath: ${s(build_data.app_path)},
|
||||
assets: new Set(${s(assets)}),
|
||||
mimeTypes: ${s(mime_types)},
|
||||
_: {
|
||||
client: ${uneval(build_data.client)},
|
||||
nodes: [
|
||||
${(node_paths).map(loader).join(',\n')}
|
||||
],
|
||||
remotes: {
|
||||
${remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `chunks/remote-${remote.hash}.js`))}`).join(',\n')}
|
||||
},
|
||||
routes: [
|
||||
${routes.map(route => {
|
||||
if (!route.page && !route.endpoint) return;
|
||||
|
||||
route.params.forEach(param => {
|
||||
if (param.matcher) matchers.add(param.matcher);
|
||||
});
|
||||
|
||||
return dedent`
|
||||
{
|
||||
id: ${s(route.id)},
|
||||
pattern: ${route.pattern},
|
||||
params: ${s(route.params)},
|
||||
page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${reindexed.get(route.page.leaf)} }` : 'null'},
|
||||
endpoint: ${route.endpoint ? loader(join_relative(relative_path, resolve_symlinks(build_data.server_manifest, route.endpoint.file).chunk.file)) : 'null'}
|
||||
}
|
||||
`;
|
||||
}).filter(Boolean).join(',\n')}
|
||||
],
|
||||
prerendered_routes: new Set(${s(prerendered)}),
|
||||
matchers: async () => {
|
||||
${Array.from(
|
||||
matchers,
|
||||
type => `const { match: ${type} } = await import ('${(join_relative(relative_path, `/entries/matchers/${type}.js`))}')`
|
||||
).join('\n')}
|
||||
return { ${Array.from(matchers).join(', ')} };
|
||||
},
|
||||
server_assets: ${s(files)}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Memoize the loaders to prevent Node from doing unnecessary work
|
||||
// on every dynamic import call
|
||||
return dedent`
|
||||
(() => {
|
||||
function __memo(fn) {
|
||||
let value;
|
||||
return () => value ??= (value = fn());
|
||||
}
|
||||
|
||||
return ${manifest_expr}
|
||||
})()
|
||||
`;
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/** @import { RemoteChunk } from 'types' */
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { validate_server_exports } from '../../utils/exports.js';
|
||||
import { load_config } from '../config/index.js';
|
||||
import { forked } from '../../utils/fork.js';
|
||||
import { installPolyfills } from '../../exports/node/polyfills.js';
|
||||
import { ENDPOINT_METHODS } from '../../constants.js';
|
||||
import { filter_env } from '../../utils/env.js';
|
||||
import { has_server_load, resolve_route } from '../../utils/routing.js';
|
||||
import { check_feature } from '../../utils/features.js';
|
||||
import { createReadableStream } from '@sveltejs/kit/node';
|
||||
import { PageNodes } from '../../utils/page_nodes.js';
|
||||
import { build_server_nodes } from '../../exports/vite/build/build_server.js';
|
||||
|
||||
export default forked(import.meta.url, analyse);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* hash: boolean;
|
||||
* manifest_path: string;
|
||||
* manifest_data: import('types').ManifestData;
|
||||
* server_manifest: import('vite').Manifest;
|
||||
* tracked_features: Record<string, string[]>;
|
||||
* env: Record<string, string>;
|
||||
* out: string;
|
||||
* output_config: import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>;
|
||||
* remotes: RemoteChunk[];
|
||||
* }} opts
|
||||
*/
|
||||
async function analyse({
|
||||
hash,
|
||||
manifest_path,
|
||||
manifest_data,
|
||||
server_manifest,
|
||||
tracked_features,
|
||||
env,
|
||||
out,
|
||||
output_config,
|
||||
remotes
|
||||
}) {
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
|
||||
|
||||
/** @type {import('types').ValidatedKitConfig} */
|
||||
const config = (await load_config()).kit;
|
||||
|
||||
const server_root = join(config.outDir, 'output');
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href);
|
||||
|
||||
installPolyfills();
|
||||
|
||||
// configure `import { building } from '$app/environment'` —
|
||||
// essential we do this before analysing the code
|
||||
internal.set_building();
|
||||
|
||||
// set env, `read`, and `manifest`, in case they're used in initialisation
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
|
||||
const private_env = filter_env(env, private_prefix, public_prefix);
|
||||
const public_env = filter_env(env, public_prefix, private_prefix);
|
||||
internal.set_private_env(private_env);
|
||||
internal.set_public_env(public_env);
|
||||
internal.set_manifest(manifest);
|
||||
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));
|
||||
|
||||
// first, build server nodes without the client manifest so we can analyse it
|
||||
build_server_nodes(out, config, manifest_data, server_manifest, null, null, null, output_config);
|
||||
|
||||
/** @type {import('types').ServerMetadata} */
|
||||
const metadata = {
|
||||
nodes: [],
|
||||
routes: new Map(),
|
||||
remotes: new Map()
|
||||
};
|
||||
|
||||
const nodes = await Promise.all(manifest._.nodes.map((loader) => loader()));
|
||||
|
||||
// analyse nodes
|
||||
for (const node of nodes) {
|
||||
if (hash && node.universal) {
|
||||
const options = Object.keys(node.universal).filter((o) => o !== 'load');
|
||||
if (options.length > 0) {
|
||||
throw new Error(
|
||||
`Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options
|
||||
.filter((o) => o !== 'load')
|
||||
.map((o) => `'${o}'`)
|
||||
.join(', ')})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
metadata.nodes[node.index] = {
|
||||
has_server_load: has_server_load(node),
|
||||
has_universal_load: node.universal?.load !== undefined
|
||||
};
|
||||
}
|
||||
|
||||
// analyse routes
|
||||
for (const route of manifest._.routes) {
|
||||
const page =
|
||||
route.page &&
|
||||
analyse_page(
|
||||
route.page.layouts.map((n) => (n === undefined ? n : nodes[n])),
|
||||
nodes[route.page.leaf]
|
||||
);
|
||||
|
||||
const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint());
|
||||
|
||||
if (page?.prerender && endpoint?.prerender) {
|
||||
throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`);
|
||||
}
|
||||
|
||||
if (page?.config && endpoint?.config) {
|
||||
for (const key in { ...page.config, ...endpoint.config }) {
|
||||
if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) {
|
||||
throw new Error(
|
||||
`Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const route_config = page?.config ?? endpoint?.config ?? {};
|
||||
const prerender = page?.prerender ?? endpoint?.prerender;
|
||||
|
||||
if (prerender !== true) {
|
||||
for (const feature of list_features(
|
||||
route,
|
||||
manifest_data,
|
||||
server_manifest,
|
||||
tracked_features
|
||||
)) {
|
||||
check_feature(route.id, route_config, feature, config.adapter);
|
||||
}
|
||||
}
|
||||
|
||||
const page_methods = page?.methods ?? [];
|
||||
const api_methods = endpoint?.methods ?? [];
|
||||
const entries = page?.entries ?? endpoint?.entries;
|
||||
|
||||
metadata.routes.set(route.id, {
|
||||
config: route_config,
|
||||
methods: Array.from(new Set([...page_methods, ...api_methods])),
|
||||
page: {
|
||||
methods: page_methods
|
||||
},
|
||||
api: {
|
||||
methods: api_methods
|
||||
},
|
||||
prerender,
|
||||
entries:
|
||||
entries && (await entries()).map((entry_object) => resolve_route(route.id, entry_object))
|
||||
});
|
||||
}
|
||||
|
||||
// analyse remotes
|
||||
for (const remote of remotes) {
|
||||
const loader = manifest._.remotes[remote.hash];
|
||||
const { default: functions } = await loader();
|
||||
|
||||
const exports = new Map();
|
||||
|
||||
for (const name in functions) {
|
||||
const info = /** @type {import('types').RemoteInfo} */ (functions[name].__);
|
||||
const type = info.type;
|
||||
|
||||
exports.set(name, {
|
||||
type,
|
||||
dynamic: type !== 'prerender' || info.dynamic
|
||||
});
|
||||
}
|
||||
|
||||
metadata.remotes.set(remote.hash, exports);
|
||||
}
|
||||
|
||||
return { metadata };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').SSRRoute} route
|
||||
* @param {import('types').SSREndpoint} mod
|
||||
*/
|
||||
function analyse_endpoint(route, mod) {
|
||||
validate_server_exports(mod, route.id);
|
||||
|
||||
if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
|
||||
throw new Error(
|
||||
`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {Array<import('types').HttpMethod | '*'>} */
|
||||
const methods = [];
|
||||
|
||||
for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) {
|
||||
if (mod[method]) methods.push(method);
|
||||
}
|
||||
|
||||
if (mod.fallback) {
|
||||
methods.push('*');
|
||||
}
|
||||
|
||||
return {
|
||||
config: mod.config,
|
||||
entries: mod.entries,
|
||||
methods,
|
||||
prerender: mod.prerender ?? false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<import('types').SSRNode | undefined>} layouts
|
||||
* @param {import('types').SSRNode} leaf
|
||||
*/
|
||||
function analyse_page(layouts, leaf) {
|
||||
/** @type {Array<'GET' | 'POST'>} */
|
||||
const methods = ['GET'];
|
||||
if (leaf.server?.actions) methods.push('POST');
|
||||
|
||||
const nodes = new PageNodes([...layouts, leaf]);
|
||||
nodes.validate();
|
||||
|
||||
return {
|
||||
config: nodes.get_config(),
|
||||
entries: leaf.universal?.entries ?? leaf.server?.entries,
|
||||
methods,
|
||||
prerender: nodes.prerender()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').SSRRoute} route
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {import('vite').Manifest} server_manifest
|
||||
* @param {Record<string, string[]>} tracked_features
|
||||
*/
|
||||
function list_features(route, manifest_data, server_manifest, tracked_features) {
|
||||
const features = new Set();
|
||||
|
||||
const route_data = /** @type {import('types').RouteData} */ (
|
||||
manifest_data.routes.find((r) => r.id === route.id)
|
||||
);
|
||||
|
||||
const visited = new Set();
|
||||
/** @param {string} id */
|
||||
function visit(id) {
|
||||
if (visited.has(id)) return;
|
||||
visited.add(id);
|
||||
|
||||
const chunk = server_manifest[id];
|
||||
if (!chunk) return;
|
||||
|
||||
if (chunk.file in tracked_features) {
|
||||
for (const feature of tracked_features[chunk.file]) {
|
||||
features.add(feature);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.imports) {
|
||||
for (const id of chunk.imports) {
|
||||
visit(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let page_node = route_data?.leaf;
|
||||
while (page_node) {
|
||||
if (page_node.server) visit(page_node.server);
|
||||
page_node = page_node.parent ?? null;
|
||||
}
|
||||
|
||||
if (route_data.endpoint) {
|
||||
visit(route_data.endpoint.file);
|
||||
}
|
||||
|
||||
if (manifest_data.hooks.server) {
|
||||
// TODO if hooks.server.js imports `read`, it will be in the entry chunk
|
||||
// we don't currently account for that case
|
||||
visit(manifest_data.hooks.server);
|
||||
}
|
||||
|
||||
return Array.from(features);
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
import { resolve, decode_uri } from '../../utils/url.js';
|
||||
import { decode } from './entities.js';
|
||||
|
||||
const DOCTYPE = 'DOCTYPE';
|
||||
const CDATA_OPEN = '[CDATA[';
|
||||
const CDATA_CLOSE = ']]>';
|
||||
const COMMENT_OPEN = '--';
|
||||
const COMMENT_CLOSE = '-->';
|
||||
|
||||
const TAG_OPEN = /[a-zA-Z]/;
|
||||
const TAG_CHAR = /[a-zA-Z0-9]/;
|
||||
const ATTRIBUTE_NAME = /[^\t\n\f />"'=]/;
|
||||
|
||||
const WHITESPACE = /[\s\n\r]/;
|
||||
|
||||
const CRAWLABLE_META_NAME_ATTRS = new Set([
|
||||
'og:url',
|
||||
'og:image',
|
||||
'og:image:url',
|
||||
'og:image:secure_url',
|
||||
'og:video',
|
||||
'og:video:url',
|
||||
'og:video:secure_url',
|
||||
'og:audio',
|
||||
'og:audio:url',
|
||||
'og:audio:secure_url',
|
||||
'twitter:image'
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {string} base
|
||||
*/
|
||||
export function crawl(html, base) {
|
||||
/** @type {string[]} */
|
||||
const ids = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const hrefs = [];
|
||||
|
||||
let i = 0;
|
||||
main: while (i < html.length) {
|
||||
const char = html[i];
|
||||
|
||||
if (char === '<') {
|
||||
if (html[i + 1] === '!') {
|
||||
i += 2;
|
||||
|
||||
if (html.slice(i, i + DOCTYPE.length).toUpperCase() === DOCTYPE) {
|
||||
i += DOCTYPE.length;
|
||||
while (i < html.length) {
|
||||
if (html[i++] === '>') {
|
||||
continue main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip cdata
|
||||
if (html.slice(i, i + CDATA_OPEN.length) === CDATA_OPEN) {
|
||||
i += CDATA_OPEN.length;
|
||||
while (i < html.length) {
|
||||
if (html.slice(i, i + CDATA_CLOSE.length) === CDATA_CLOSE) {
|
||||
i += CDATA_CLOSE.length;
|
||||
continue main;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// skip comments
|
||||
if (html.slice(i, i + COMMENT_OPEN.length) === COMMENT_OPEN) {
|
||||
i += COMMENT_OPEN.length;
|
||||
while (i < html.length) {
|
||||
if (html.slice(i, i + COMMENT_CLOSE.length) === COMMENT_CLOSE) {
|
||||
i += COMMENT_CLOSE.length;
|
||||
continue main;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse opening tags
|
||||
const start = ++i;
|
||||
if (TAG_OPEN.test(html[start])) {
|
||||
while (i < html.length) {
|
||||
if (!TAG_CHAR.test(html[i])) {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const tag = html.slice(start, i).toUpperCase();
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const attributes = {};
|
||||
|
||||
if (tag === 'SCRIPT' || tag === 'STYLE') {
|
||||
while (i < html.length) {
|
||||
if (
|
||||
html[i] === '<' &&
|
||||
html[i + 1] === '/' &&
|
||||
html.slice(i + 2, i + 2 + tag.length).toUpperCase() === tag
|
||||
) {
|
||||
continue main;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
while (i < html.length) {
|
||||
const start = i;
|
||||
|
||||
const char = html[start];
|
||||
if (char === '>') break;
|
||||
|
||||
if (ATTRIBUTE_NAME.test(char)) {
|
||||
i += 1;
|
||||
|
||||
while (i < html.length) {
|
||||
if (!ATTRIBUTE_NAME.test(html[i])) {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const name = html.slice(start, i).toLowerCase();
|
||||
|
||||
while (WHITESPACE.test(html[i])) i += 1;
|
||||
|
||||
if (html[i] === '=') {
|
||||
i += 1;
|
||||
while (WHITESPACE.test(html[i])) i += 1;
|
||||
|
||||
let value;
|
||||
|
||||
if (html[i] === "'" || html[i] === '"') {
|
||||
const quote = html[i++];
|
||||
|
||||
const start = i;
|
||||
let escaped = false;
|
||||
|
||||
while (i < html.length) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else {
|
||||
const char = html[i];
|
||||
|
||||
if (html[i] === quote) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = true;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
value = html.slice(start, i);
|
||||
} else {
|
||||
const start = i;
|
||||
while (html[i] !== '>' && !WHITESPACE.test(html[i])) i += 1;
|
||||
value = html.slice(start, i);
|
||||
|
||||
i -= 1;
|
||||
}
|
||||
|
||||
value = decode(value);
|
||||
attributes[name] = value;
|
||||
} else {
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const { href, id, name, property, rel, src, srcset, content } = attributes;
|
||||
|
||||
if (href) {
|
||||
if (tag === 'BASE') {
|
||||
base = resolve(base, href);
|
||||
} else if (!rel || !/\bexternal\b/i.test(rel)) {
|
||||
hrefs.push(resolve(base, href));
|
||||
}
|
||||
}
|
||||
|
||||
if (id) {
|
||||
ids.push(decode_uri(id));
|
||||
}
|
||||
|
||||
if (name && tag === 'A') {
|
||||
ids.push(decode_uri(name));
|
||||
}
|
||||
|
||||
if (src) {
|
||||
hrefs.push(resolve(base, src));
|
||||
}
|
||||
|
||||
if (srcset) {
|
||||
let value = srcset;
|
||||
const candidates = [];
|
||||
let insideURL = true;
|
||||
value = value.trim();
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (value[i] === ',' && (!insideURL || (insideURL && WHITESPACE.test(value[i + 1])))) {
|
||||
candidates.push(value.slice(0, i));
|
||||
value = value.substring(i + 1).trim();
|
||||
i = 0;
|
||||
insideURL = true;
|
||||
} else if (WHITESPACE.test(value[i])) {
|
||||
insideURL = false;
|
||||
}
|
||||
}
|
||||
candidates.push(value);
|
||||
for (const candidate of candidates) {
|
||||
const src = candidate.split(WHITESPACE)[0];
|
||||
if (src) hrefs.push(resolve(base, src));
|
||||
}
|
||||
}
|
||||
|
||||
if (tag === 'META' && content) {
|
||||
const attr = name ?? property;
|
||||
|
||||
if (attr && CRAWLABLE_META_NAME_ATTRS.has(attr)) {
|
||||
hrefs.push(resolve(base, content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return { ids, hrefs };
|
||||
}
|
||||
+2252
File diff suppressed because it is too large
Load Diff
+55
@@ -0,0 +1,55 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { installPolyfills } from '../../exports/node/polyfills.js';
|
||||
import { load_config } from '../config/index.js';
|
||||
import { forked } from '../../utils/fork.js';
|
||||
|
||||
export default forked(import.meta.url, generate_fallback);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* manifest_path: string;
|
||||
* env: Record<string, string>
|
||||
* }} opts
|
||||
*/
|
||||
async function generate_fallback({ manifest_path, env }) {
|
||||
/** @type {import('types').ValidatedKitConfig} */
|
||||
const config = (await load_config()).kit;
|
||||
|
||||
installPolyfills();
|
||||
|
||||
const server_root = join(config.outDir, 'output');
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const { set_building } = await import(pathToFileURL(`${server_root}/server/internal.js`).href);
|
||||
|
||||
/** @type {import('types').ServerModule} */
|
||||
const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href);
|
||||
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
|
||||
|
||||
set_building();
|
||||
|
||||
const server = new Server(manifest);
|
||||
await server.init({ env });
|
||||
|
||||
const response = await server.respond(new Request(config.prerender.origin + '/[fallback]'), {
|
||||
getClientAddress: () => {
|
||||
throw new Error('Cannot read clientAddress during prerendering');
|
||||
},
|
||||
prerendering: {
|
||||
fallback: true,
|
||||
dependencies: new Map(),
|
||||
remote_responses: new Map()
|
||||
},
|
||||
read: (file) => readFileSync(join(config.files.assets, file))
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
throw new Error(`Could not create a fallback page — failed with status ${response.status}`);
|
||||
}
|
||||
+593
@@ -0,0 +1,593 @@
|
||||
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { installPolyfills } from '../../exports/node/polyfills.js';
|
||||
import { mkdirp, posixify, walk } from '../../utils/filesystem.js';
|
||||
import { decode_uri, is_root_relative, resolve } from '../../utils/url.js';
|
||||
import { escape_html } from '../../utils/escape.js';
|
||||
import { logger } from '../utils.js';
|
||||
import { load_config } from '../config/index.js';
|
||||
import { get_route_segments } from '../../utils/routing.js';
|
||||
import { queue } from './queue.js';
|
||||
import { crawl } from './crawl.js';
|
||||
import { forked } from '../../utils/fork.js';
|
||||
import * as devalue from 'devalue';
|
||||
import { createReadableStream } from '@sveltejs/kit/node';
|
||||
import generate_fallback from './fallback.js';
|
||||
import { stringify_remote_arg } from '../../runtime/shared.js';
|
||||
import { filter_env } from '../../utils/env.js';
|
||||
|
||||
export default forked(import.meta.url, prerender);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#scrolling-to-a-fragment
|
||||
// "If fragment is the empty string, then return the special value top of the document."
|
||||
// ...and
|
||||
// "If decodedFragment is an ASCII case-insensitive match for the string 'top', then return the top of the document."
|
||||
const SPECIAL_HASHLINKS = new Set(['', 'top']);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* hash: boolean;
|
||||
* out: string;
|
||||
* manifest_path: string;
|
||||
* metadata: import('types').ServerMetadata;
|
||||
* verbose: boolean;
|
||||
* env: Record<string, string>
|
||||
* }} opts
|
||||
*/
|
||||
async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const internal = await import(pathToFileURL(`${out}/server/internal.js`).href);
|
||||
|
||||
/** @type {import('types').ServerModule} */
|
||||
const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
|
||||
|
||||
// configure `import { building } from '$app/environment'` —
|
||||
// essential we do this before analysing the code
|
||||
internal.set_building();
|
||||
internal.set_prerendering();
|
||||
|
||||
/**
|
||||
* @template {{message: string}} T
|
||||
* @template {Omit<T, 'message'>} K
|
||||
* @param {import('types').Logger} log
|
||||
* @param {'fail' | 'warn' | 'ignore' | ((details: T) => void)} input
|
||||
* @param {(details: K) => string} format
|
||||
* @returns {(details: K) => void}
|
||||
*/
|
||||
function normalise_error_handler(log, input, format) {
|
||||
switch (input) {
|
||||
case 'fail':
|
||||
return (details) => {
|
||||
throw new Error(format(details));
|
||||
};
|
||||
case 'warn':
|
||||
return (details) => {
|
||||
log.error(format(details));
|
||||
};
|
||||
case 'ignore':
|
||||
return () => {};
|
||||
default:
|
||||
// @ts-expect-error TS thinks T might be of a different kind, but it's not
|
||||
return (details) => input({ ...details, message: format(details) });
|
||||
}
|
||||
}
|
||||
|
||||
const OK = 2;
|
||||
const REDIRECT = 3;
|
||||
|
||||
/** @type {import('types').Prerendered} */
|
||||
const prerendered = {
|
||||
pages: new Map(),
|
||||
assets: new Map(),
|
||||
redirects: new Map(),
|
||||
paths: []
|
||||
};
|
||||
|
||||
/** @type {import('types').PrerenderMap} */
|
||||
const prerender_map = new Map();
|
||||
|
||||
for (const [id, { prerender }] of metadata.routes) {
|
||||
if (prerender !== undefined) {
|
||||
prerender_map.set(id, prerender);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const prerendered_routes = new Set();
|
||||
|
||||
/** @type {import('types').ValidatedKitConfig} */
|
||||
const config = (await load_config()).kit;
|
||||
|
||||
if (hash) {
|
||||
const fallback = await generate_fallback({
|
||||
manifest_path,
|
||||
env
|
||||
});
|
||||
|
||||
const file = output_filename('/', true);
|
||||
const dest = `${config.outDir}/output/prerendered/pages/${file}`;
|
||||
|
||||
mkdirp(dirname(dest));
|
||||
writeFileSync(dest, fallback);
|
||||
|
||||
prerendered.pages.set('/', { file });
|
||||
|
||||
return { prerendered, prerender_map };
|
||||
}
|
||||
|
||||
const emulator = await config.adapter?.emulate?.();
|
||||
|
||||
/** @type {import('types').Logger} */
|
||||
const log = logger({ verbose });
|
||||
|
||||
installPolyfills();
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const saved = new Map();
|
||||
|
||||
const handle_http_error = normalise_error_handler(
|
||||
log,
|
||||
config.prerender.handleHttpError,
|
||||
({ status, path, referrer, referenceType }) => {
|
||||
const message =
|
||||
status === 404 && !path.startsWith(config.paths.base)
|
||||
? `${path} does not begin with \`base\`, which is configured in \`paths.base\` and can be imported from \`$app/paths\` - see https://svelte.dev/docs/kit/configuration#paths for more info`
|
||||
: path;
|
||||
|
||||
return `${status} ${message}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
|
||||
}
|
||||
);
|
||||
|
||||
const handle_missing_id = normalise_error_handler(
|
||||
log,
|
||||
config.prerender.handleMissingId,
|
||||
({ path, id, referrers }) => {
|
||||
return (
|
||||
`The following pages contain links to ${path}#${id}, but no element with id="${id}" exists on ${path} - see the \`handleMissingId\` option in https://svelte.dev/docs/kit/configuration#prerender for more info:` +
|
||||
referrers.map((l) => `\n - ${l}`).join('')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const handle_entry_generator_mismatch = normalise_error_handler(
|
||||
log,
|
||||
config.prerender.handleEntryGeneratorMismatch,
|
||||
({ generatedFromId, entry, matchedId }) => {
|
||||
return `The entries export from ${generatedFromId} generated entry ${entry}, which was matched by ${matchedId} - see the \`handleEntryGeneratorMismatch\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.`;
|
||||
}
|
||||
);
|
||||
|
||||
const handle_not_prerendered_route = normalise_error_handler(
|
||||
log,
|
||||
config.prerender.handleUnseenRoutes,
|
||||
({ routes }) => {
|
||||
const list = routes.map((id) => ` - ${id}`).join('\n');
|
||||
return `The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${list}\n\nSee the \`handleUnseenRoutes\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.`;
|
||||
}
|
||||
);
|
||||
|
||||
const q = queue(config.prerender.concurrency);
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {boolean} is_html
|
||||
*/
|
||||
function output_filename(path, is_html) {
|
||||
const file = path.slice(config.paths.base.length + 1) || 'index.html';
|
||||
|
||||
if (is_html && !file.endsWith('.html')) {
|
||||
return file + (file.endsWith('/') ? 'index.html' : '.html');
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
const files = new Set(walk(`${out}/client`).map(posixify));
|
||||
files.add(`${config.appDir}/env.js`);
|
||||
|
||||
const immutable = `${config.appDir}/immutable`;
|
||||
if (existsSync(`${out}/server/${immutable}`)) {
|
||||
for (const file of walk(`${out}/server/${immutable}`)) {
|
||||
files.add(posixify(`${config.appDir}/immutable/${file}`));
|
||||
}
|
||||
}
|
||||
|
||||
const remote_prefix = `${config.paths.base}/${config.appDir}/remote/`;
|
||||
|
||||
const seen = new Set();
|
||||
const written = new Set();
|
||||
const remote_responses = new Map();
|
||||
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const expected_hashlinks = new Map();
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
const actual_hashlinks = new Map();
|
||||
|
||||
/**
|
||||
* @param {string | null} referrer
|
||||
* @param {string} decoded
|
||||
* @param {string} [encoded]
|
||||
* @param {string} [generated_from_id]
|
||||
*/
|
||||
function enqueue(referrer, decoded, encoded, generated_from_id) {
|
||||
if (seen.has(decoded)) return;
|
||||
seen.add(decoded);
|
||||
|
||||
const file = decoded.slice(config.paths.base.length + 1);
|
||||
if (files.has(file)) return;
|
||||
|
||||
return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} decoded
|
||||
* @param {string} encoded
|
||||
* @param {string?} referrer
|
||||
* @param {string} [generated_from_id]
|
||||
*/
|
||||
async function visit(decoded, encoded, referrer, generated_from_id) {
|
||||
if (!decoded.startsWith(config.paths.base)) {
|
||||
handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {Map<string, import('types').PrerenderDependency>} */
|
||||
const dependencies = new Map();
|
||||
|
||||
const response = await server.respond(new Request(config.prerender.origin + encoded), {
|
||||
getClientAddress() {
|
||||
throw new Error('Cannot read clientAddress during prerendering');
|
||||
},
|
||||
prerendering: {
|
||||
dependencies,
|
||||
remote_responses
|
||||
},
|
||||
read: (file) => {
|
||||
// stuff we just wrote
|
||||
const filepath = saved.get(file);
|
||||
if (filepath) return readFileSync(filepath);
|
||||
|
||||
// Static assets emitted during build
|
||||
if (file.startsWith(config.appDir)) {
|
||||
return readFileSync(`${out}/server/${file}`);
|
||||
}
|
||||
|
||||
// stuff in `static`
|
||||
return readFileSync(join(config.files.assets, file));
|
||||
},
|
||||
emulator
|
||||
});
|
||||
|
||||
const encoded_id = response.headers.get('x-sveltekit-routeid');
|
||||
const decoded_id = encoded_id && decode_uri(encoded_id);
|
||||
if (
|
||||
decoded_id !== null &&
|
||||
generated_from_id !== undefined &&
|
||||
decoded_id !== generated_from_id
|
||||
) {
|
||||
handle_entry_generator_mismatch({
|
||||
generatedFromId: generated_from_id,
|
||||
entry: decoded,
|
||||
matchedId: decoded_id
|
||||
});
|
||||
}
|
||||
|
||||
const body = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
const category = decoded.startsWith(remote_prefix) ? 'data' : 'pages';
|
||||
save(category, response, body, decoded, encoded, referrer, 'linked');
|
||||
|
||||
for (const [dependency_path, result] of dependencies) {
|
||||
// this seems circuitous, but using new URL allows us to not care
|
||||
// whether dependency_path is encoded or not
|
||||
const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname;
|
||||
const decoded_dependency_path = decode_uri(encoded_dependency_path);
|
||||
|
||||
const headers = Object.fromEntries(result.response.headers);
|
||||
|
||||
const prerender = headers['x-sveltekit-prerender'];
|
||||
if (prerender) {
|
||||
const encoded_route_id = headers['x-sveltekit-routeid'];
|
||||
if (encoded_route_id != null) {
|
||||
const route_id = decode_uri(encoded_route_id);
|
||||
const existing_value = prerender_map.get(route_id);
|
||||
if (existing_value !== 'auto') {
|
||||
prerender_map.set(route_id, prerender === 'true' ? true : 'auto');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const body = result.body ?? new Uint8Array(await result.response.arrayBuffer());
|
||||
|
||||
const category = decoded_dependency_path.startsWith(remote_prefix) ? 'data' : 'dependencies';
|
||||
|
||||
save(
|
||||
category,
|
||||
result.response,
|
||||
body,
|
||||
decoded_dependency_path,
|
||||
encoded_dependency_path,
|
||||
decoded,
|
||||
'fetched'
|
||||
);
|
||||
}
|
||||
|
||||
// avoid triggering `filterSerializeResponseHeaders` guard
|
||||
const headers = Object.fromEntries(response.headers);
|
||||
|
||||
// if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those
|
||||
if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') {
|
||||
const { ids, hrefs } = crawl(body.toString(), decoded);
|
||||
|
||||
actual_hashlinks.set(decoded, ids);
|
||||
|
||||
/** @param {string} href */
|
||||
const removePrerenderOrigin = (href) => {
|
||||
if (href.startsWith(config.prerender.origin)) {
|
||||
if (href === config.prerender.origin) return '/';
|
||||
if (href.at(config.prerender.origin.length) !== '/') return href;
|
||||
return href.slice(config.prerender.origin.length);
|
||||
}
|
||||
return href;
|
||||
};
|
||||
|
||||
for (const href of hrefs.map(removePrerenderOrigin)) {
|
||||
if (!is_root_relative(href)) continue;
|
||||
|
||||
const { pathname, search, hash } = new URL(href, 'http://localhost');
|
||||
|
||||
if (search) {
|
||||
// TODO warn that query strings have no effect on statically-exported pages
|
||||
}
|
||||
|
||||
if (hash) {
|
||||
const key = decode_uri(pathname + hash);
|
||||
|
||||
if (!expected_hashlinks.has(key)) {
|
||||
expected_hashlinks.set(key, new Set());
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */ (expected_hashlinks.get(key)).add(decoded);
|
||||
}
|
||||
|
||||
void enqueue(decoded, decode_uri(pathname), pathname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'pages' | 'dependencies' | 'data'} category
|
||||
* @param {Response} response
|
||||
* @param {string | Uint8Array} body
|
||||
* @param {string} decoded
|
||||
* @param {string} encoded
|
||||
* @param {string | null} referrer
|
||||
* @param {'linked' | 'fetched'} referenceType
|
||||
*/
|
||||
function save(category, response, body, decoded, encoded, referrer, referenceType) {
|
||||
const response_type = Math.floor(response.status / 100);
|
||||
const headers = Object.fromEntries(response.headers);
|
||||
|
||||
const type = headers['content-type'];
|
||||
const is_html = response_type === REDIRECT || type === 'text/html';
|
||||
|
||||
const file = output_filename(decoded, is_html);
|
||||
const dest = `${config.outDir}/output/prerendered/${category}/${file}`;
|
||||
|
||||
if (written.has(file)) return;
|
||||
|
||||
const encoded_route_id = response.headers.get('x-sveltekit-routeid');
|
||||
const route_id = encoded_route_id != null ? decode_uri(encoded_route_id) : null;
|
||||
if (route_id !== null) prerendered_routes.add(route_id);
|
||||
|
||||
if (response_type === REDIRECT) {
|
||||
const location = headers['location'];
|
||||
|
||||
if (location) {
|
||||
const resolved = resolve(encoded, location);
|
||||
if (is_root_relative(resolved)) {
|
||||
void enqueue(decoded, decode_uri(resolved), resolved);
|
||||
}
|
||||
|
||||
if (!headers['x-sveltekit-normalize']) {
|
||||
mkdirp(dirname(dest));
|
||||
|
||||
log.warn(`${response.status} ${decoded} -> ${location}`);
|
||||
|
||||
writeFileSync(
|
||||
dest,
|
||||
`<script>location.href=${devalue.uneval(
|
||||
location
|
||||
)};</script><meta http-equiv="refresh" content="${escape_html(
|
||||
`0;url=${location}`,
|
||||
true
|
||||
)}">`
|
||||
);
|
||||
|
||||
written.add(file);
|
||||
|
||||
if (!prerendered.redirects.has(decoded)) {
|
||||
prerendered.redirects.set(decoded, {
|
||||
status: response.status,
|
||||
location: resolved
|
||||
});
|
||||
|
||||
prerendered.paths.push(decoded);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn(`location header missing on redirect received from ${decoded}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
if (existsSync(dest) && statSync(dest).isDirectory()) {
|
||||
throw new Error(
|
||||
`Cannot save ${decoded} as it is already a directory. See https://svelte.dev/docs/kit/page-options#prerender-route-conflicts for more information`
|
||||
);
|
||||
}
|
||||
|
||||
const dir = dirname(dest);
|
||||
|
||||
if (existsSync(dir) && !statSync(dir).isDirectory()) {
|
||||
const parent = decoded.split('/').slice(0, -1).join('/');
|
||||
throw new Error(
|
||||
`Cannot save ${decoded} as ${parent} is already a file. See https://svelte.dev/docs/kit/page-options#prerender-route-conflicts for more information`
|
||||
);
|
||||
}
|
||||
|
||||
mkdirp(dir);
|
||||
|
||||
log.info(`${response.status} ${decoded}`);
|
||||
writeFileSync(dest, body);
|
||||
written.add(file);
|
||||
|
||||
if (is_html) {
|
||||
prerendered.pages.set(decoded, {
|
||||
file
|
||||
});
|
||||
} else {
|
||||
prerendered.assets.set(decoded, {
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
prerendered.paths.push(decoded);
|
||||
} else if (response_type !== OK) {
|
||||
handle_http_error({ status: response.status, path: decoded, referrer, referenceType });
|
||||
}
|
||||
|
||||
manifest.assets.add(file);
|
||||
saved.set(file, dest);
|
||||
}
|
||||
|
||||
/** @type {Array<{ id: string, entries: Array<string>}>} */
|
||||
const route_level_entries = [];
|
||||
for (const [id, { entries }] of metadata.routes.entries()) {
|
||||
if (entries) {
|
||||
route_level_entries.push({ id, entries });
|
||||
}
|
||||
}
|
||||
|
||||
let should_prerender = false;
|
||||
|
||||
for (const value of prerender_map.values()) {
|
||||
if (value) {
|
||||
should_prerender = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// the user's remote function modules may reference environment variables,
|
||||
// `read` or the `manifest` at the top-level so we need to set them before
|
||||
// evaluating those modules to avoid potential runtime errors
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
|
||||
const private_env = filter_env(env, private_prefix, public_prefix);
|
||||
const public_env = filter_env(env, public_prefix, private_prefix);
|
||||
internal.set_private_env(private_env);
|
||||
internal.set_public_env(public_env);
|
||||
internal.set_manifest(manifest);
|
||||
internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`));
|
||||
|
||||
/** @type {Array<import('types').RemoteInfo & { type: 'prerender'}>} */
|
||||
const prerender_functions = [];
|
||||
|
||||
for (const loader of Object.values(manifest._.remotes)) {
|
||||
const module = await loader();
|
||||
|
||||
for (const fn of Object.values(module.default)) {
|
||||
if (fn?.__?.type === 'prerender') {
|
||||
prerender_functions.push(fn.__);
|
||||
should_prerender = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!should_prerender) {
|
||||
return { prerendered, prerender_map };
|
||||
}
|
||||
|
||||
// only run the server after the `should_prerender` check so that we
|
||||
// don't run the user's init hook unnecessarily
|
||||
const server = new Server(manifest);
|
||||
await server.init({
|
||||
env,
|
||||
read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`)
|
||||
});
|
||||
|
||||
log.info('Prerendering');
|
||||
|
||||
for (const entry of config.prerender.entries) {
|
||||
if (entry === '*') {
|
||||
for (const [id, prerender] of prerender_map) {
|
||||
if (prerender) {
|
||||
// remove optional parameters from the route
|
||||
const segments = get_route_segments(id).filter((segment) => !segment.startsWith('[['));
|
||||
const processed_id = '/' + segments.join('/');
|
||||
|
||||
if (processed_id.includes('[')) continue;
|
||||
const path = `/${get_route_segments(processed_id).join('/')}`;
|
||||
void enqueue(null, config.paths.base + path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
void enqueue(null, config.paths.base + entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { id, entries } of route_level_entries) {
|
||||
for (const entry of entries) {
|
||||
void enqueue(null, config.paths.base + entry, undefined, id);
|
||||
}
|
||||
}
|
||||
|
||||
const transport = (await internal.get_hooks()).transport ?? {};
|
||||
for (const info of prerender_functions) {
|
||||
if (info.has_arg) {
|
||||
for (const arg of (await info.inputs?.()) ?? []) {
|
||||
void enqueue(null, remote_prefix + info.id + '/' + stringify_remote_arg(arg, transport));
|
||||
}
|
||||
} else {
|
||||
void enqueue(null, remote_prefix + info.id);
|
||||
}
|
||||
}
|
||||
|
||||
await q.done();
|
||||
|
||||
// handle invalid fragment links
|
||||
for (const [key, referrers] of expected_hashlinks) {
|
||||
const index = key.indexOf('#');
|
||||
const path = key.slice(0, index);
|
||||
const id = key.slice(index + 1);
|
||||
|
||||
const hashlinks = actual_hashlinks.get(path);
|
||||
// ignore fragment links to pages that were not prerendered
|
||||
if (!hashlinks) continue;
|
||||
|
||||
if (!hashlinks.includes(id) && !SPECIAL_HASHLINKS.has(id)) {
|
||||
handle_missing_id({ id, path, referrers: Array.from(referrers) });
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const not_prerendered = [];
|
||||
|
||||
for (const [route_id, prerender] of prerender_map) {
|
||||
if (prerender === true && !prerendered_routes.has(route_id)) {
|
||||
not_prerendered.push(route_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (not_prerendered.length > 0) {
|
||||
handle_not_prerendered_route({ routes: not_prerendered });
|
||||
}
|
||||
|
||||
return { prerendered, prerender_map };
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/** @import { PromiseWithResolvers } from '../../utils/promise.js' */
|
||||
import { with_resolvers } from '../../utils/promise.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* fn: () => Promise<any>,
|
||||
* fulfil: (value: any) => void,
|
||||
* reject: (error: Error) => void
|
||||
* }} Task
|
||||
*/
|
||||
|
||||
/** @param {number} concurrency */
|
||||
export function queue(concurrency) {
|
||||
/** @type {Task[]} */
|
||||
const tasks = [];
|
||||
const { promise, resolve, reject } = /** @type {PromiseWithResolvers<void>} */ (with_resolvers());
|
||||
|
||||
let current = 0;
|
||||
let closed = false;
|
||||
|
||||
promise.catch(() => {
|
||||
// this is necessary in case a catch handler is never added
|
||||
// to the done promise by the user
|
||||
});
|
||||
|
||||
function dequeue() {
|
||||
if (current < concurrency) {
|
||||
const task = tasks.shift();
|
||||
|
||||
if (task) {
|
||||
current += 1;
|
||||
const promise = Promise.resolve(task.fn());
|
||||
|
||||
void promise
|
||||
.then(task.fulfil, (err) => {
|
||||
task.reject(err);
|
||||
reject(err);
|
||||
})
|
||||
.then(() => {
|
||||
current -= 1;
|
||||
dequeue();
|
||||
});
|
||||
} else if (current === 0) {
|
||||
closed = true;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** @param {() => any} fn */
|
||||
add: (fn) => {
|
||||
if (closed) throw new Error('Cannot add tasks to a queue that has ended');
|
||||
|
||||
const promise = new Promise((fulfil, reject) => {
|
||||
tasks.push({ fn, fulfil, reject });
|
||||
});
|
||||
|
||||
dequeue();
|
||||
return promise;
|
||||
},
|
||||
|
||||
done: () => {
|
||||
if (current === 0) {
|
||||
closed = true;
|
||||
resolve();
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
}
|
||||
+613
@@ -0,0 +1,613 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import colors from 'kleur';
|
||||
import { lookup } from 'mrmime';
|
||||
import { list_files, runtime_directory } from '../../utils.js';
|
||||
import { posixify, resolve_entry } from '../../../utils/filesystem.js';
|
||||
import { parse_route_id } from '../../../utils/routing.js';
|
||||
import { sort_routes } from './sort.js';
|
||||
import { isSvelte5Plus } from '../utils.js';
|
||||
import {
|
||||
create_node_analyser,
|
||||
get_page_options
|
||||
} from '../../../exports/vite/static_analysis/index.js';
|
||||
|
||||
/**
|
||||
* Generates the manifest data used for the client-side manifest and types generation.
|
||||
* @param {{
|
||||
* config: import('types').ValidatedConfig;
|
||||
* fallback?: string;
|
||||
* cwd?: string;
|
||||
* }} opts
|
||||
* @returns {import('types').ManifestData}
|
||||
*/
|
||||
export default function create_manifest_data({
|
||||
config,
|
||||
fallback = `${runtime_directory}/components/${isSvelte5Plus() ? 'svelte-5' : 'svelte-4'}`,
|
||||
cwd = process.cwd()
|
||||
}) {
|
||||
const assets = create_assets(config);
|
||||
const hooks = create_hooks(config, cwd);
|
||||
const matchers = create_matchers(config, cwd);
|
||||
const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);
|
||||
|
||||
for (const route of routes) {
|
||||
for (const param of route.params) {
|
||||
if (param.matcher && !matchers[param.matcher]) {
|
||||
throw new Error(`No matcher found for parameter '${param.matcher}' in route ${route.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
assets,
|
||||
hooks,
|
||||
matchers,
|
||||
nodes,
|
||||
routes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function create_assets(config) {
|
||||
return list_files(config.kit.files.assets).map((file) => ({
|
||||
file,
|
||||
size: fs.statSync(path.resolve(config.kit.files.assets, file)).size,
|
||||
type: lookup(file) || null
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} cwd
|
||||
*/
|
||||
function create_hooks(config, cwd) {
|
||||
const client = resolve_entry(config.kit.files.hooks.client);
|
||||
const server = resolve_entry(config.kit.files.hooks.server);
|
||||
const universal = resolve_entry(config.kit.files.hooks.universal);
|
||||
|
||||
return {
|
||||
client: client && posixify(path.relative(cwd, client)),
|
||||
server: server && posixify(path.relative(cwd, server)),
|
||||
universal: universal && posixify(path.relative(cwd, universal))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} cwd
|
||||
*/
|
||||
function create_matchers(config, cwd) {
|
||||
const params_base = path.relative(cwd, config.kit.files.params);
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const matchers = {};
|
||||
if (fs.existsSync(config.kit.files.params)) {
|
||||
for (const file of fs.readdirSync(config.kit.files.params)) {
|
||||
const ext = path.extname(file);
|
||||
if (!config.kit.moduleExtensions.includes(ext)) continue;
|
||||
const type = file.slice(0, -ext.length);
|
||||
|
||||
if (/^\w+$/.test(type)) {
|
||||
const matcher_file = path.join(params_base, file);
|
||||
|
||||
// Disallow same matcher with different extensions
|
||||
if (matchers[type]) {
|
||||
throw new Error(`Duplicate matchers: ${matcher_file} and ${matchers[type]}`);
|
||||
} else {
|
||||
matchers[type] = matcher_file;
|
||||
}
|
||||
} else {
|
||||
// Allow for matcher test collocation
|
||||
if (type.endsWith('.test') || type.endsWith('.spec')) continue;
|
||||
|
||||
throw new Error(
|
||||
`Matcher names can only have underscores and alphanumeric characters — "${file}" is invalid`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} cwd
|
||||
* @param {string} fallback
|
||||
*/
|
||||
function create_routes_and_nodes(cwd, config, fallback) {
|
||||
/** @type {import('types').RouteData[]} */
|
||||
const routes = [];
|
||||
|
||||
const routes_base = posixify(path.relative(cwd, config.kit.files.routes));
|
||||
|
||||
const valid_extensions = [...config.extensions, ...config.kit.moduleExtensions];
|
||||
|
||||
/** @type {import('types').PageNode[]} */
|
||||
const nodes = [];
|
||||
|
||||
if (fs.existsSync(config.kit.files.routes)) {
|
||||
/**
|
||||
* @param {number} depth
|
||||
* @param {string} id
|
||||
* @param {string} segment
|
||||
* @param {import('types').RouteData | null} parent
|
||||
*/
|
||||
const walk = (depth, id, segment, parent) => {
|
||||
const unescaped = id.replace(/\[([ux])\+([^\]]+)\]/gi, (match, type, code) => {
|
||||
if (match !== match.toLowerCase()) {
|
||||
throw new Error(`Character escape sequence in ${id} must be lowercase`);
|
||||
}
|
||||
|
||||
if (!/[0-9a-f]+/.test(code)) {
|
||||
throw new Error(`Invalid character escape sequence in ${id}`);
|
||||
}
|
||||
|
||||
if (type === 'x') {
|
||||
if (code.length !== 2) {
|
||||
throw new Error(`Hexadecimal escape sequence in ${id} must be two characters`);
|
||||
}
|
||||
|
||||
return String.fromCharCode(parseInt(code, 16));
|
||||
} else {
|
||||
if (code.length < 4 || code.length > 6) {
|
||||
throw new Error(
|
||||
`Unicode escape sequence in ${id} must be between four and six characters`
|
||||
);
|
||||
}
|
||||
|
||||
return String.fromCharCode(parseInt(code, 16));
|
||||
}
|
||||
});
|
||||
|
||||
if (/\]\[/.test(unescaped)) {
|
||||
throw new Error(`Invalid route ${id} — parameters must be separated`);
|
||||
}
|
||||
|
||||
if (count_occurrences('[', id) !== count_occurrences(']', id)) {
|
||||
throw new Error(`Invalid route ${id} — brackets are unbalanced`);
|
||||
}
|
||||
|
||||
if (/#/.test(segment)) {
|
||||
// Vite will barf on files with # in them
|
||||
throw new Error(`Route ${id} should be renamed to ${id.replace(/#/g, '[x+23]')}`);
|
||||
}
|
||||
|
||||
if (/\[\.\.\.\w+\]\/\[\[/.test(id)) {
|
||||
throw new Error(
|
||||
`Invalid route ${id} — an [[optional]] route segment cannot follow a [...rest] route segment`
|
||||
);
|
||||
}
|
||||
|
||||
if (/\[\[\.\.\./.test(id)) {
|
||||
throw new Error(
|
||||
`Invalid route ${id} — a rest route segment is always optional, remove the outer square brackets`
|
||||
);
|
||||
}
|
||||
|
||||
const { pattern, params } = parse_route_id(id);
|
||||
|
||||
/** @type {import('types').RouteData} */
|
||||
const route = {
|
||||
id,
|
||||
parent,
|
||||
|
||||
segment,
|
||||
pattern,
|
||||
params,
|
||||
|
||||
layout: null,
|
||||
error: null,
|
||||
leaf: null,
|
||||
page: null,
|
||||
endpoint: null
|
||||
};
|
||||
|
||||
// important to do this before walking children, so that child
|
||||
// routes appear later
|
||||
routes.push(route);
|
||||
|
||||
// if we don't do this, the route map becomes unwieldy to console.log
|
||||
Object.defineProperty(route, 'parent', { enumerable: false });
|
||||
|
||||
const dir = path.join(cwd, routes_base, id);
|
||||
|
||||
// We can't use withFileTypes because of a NodeJs bug which returns wrong results
|
||||
// with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
|
||||
const files = fs.readdirSync(dir).map((name) => ({
|
||||
is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
|
||||
name
|
||||
}));
|
||||
|
||||
// process files first
|
||||
for (const file of files) {
|
||||
if (file.is_dir) continue;
|
||||
|
||||
const ext = valid_extensions.find((ext) => file.name.endsWith(ext));
|
||||
if (!ext) continue;
|
||||
|
||||
if (!file.name.startsWith('+')) {
|
||||
const name = file.name.slice(0, -ext.length);
|
||||
// check if it is a valid route filename but missing the + prefix
|
||||
const typo =
|
||||
/^(?:(page(?:@(.*))?)|(layout(?:@(.*))?)|(error))$/.test(name) ||
|
||||
/^(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/.test(
|
||||
name
|
||||
);
|
||||
if (typo) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Missing route file prefix. Did you mean +${file.name}?` +
|
||||
` at ${path.join(dir, file.name)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.name.endsWith('.d.ts')) {
|
||||
let name = file.name.slice(0, -5);
|
||||
const ext = valid_extensions.find((ext) => name.endsWith(ext));
|
||||
if (ext) name = name.slice(0, -ext.length);
|
||||
|
||||
const valid =
|
||||
/^\+(?:(page(?:@(.*))?)|(layout(?:@(.*))?)|(error))$/.test(name) ||
|
||||
/^\+(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/.test(
|
||||
name
|
||||
);
|
||||
|
||||
if (valid) continue;
|
||||
}
|
||||
|
||||
const project_relative = posixify(path.relative(cwd, path.join(dir, file.name)));
|
||||
|
||||
const item = analyze(
|
||||
project_relative,
|
||||
file.name,
|
||||
config.extensions,
|
||||
config.kit.moduleExtensions
|
||||
);
|
||||
|
||||
if (config.kit.router.type === 'hash' && item.kind === 'server') {
|
||||
throw new Error(
|
||||
`Cannot use server-only files in an app with \`router.type === 'hash': ${project_relative}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string} existing_file
|
||||
*/
|
||||
function duplicate_files_error(type, existing_file) {
|
||||
return new Error(
|
||||
`Multiple ${type} files found in ${routes_base}${route.id} : ${path.basename(
|
||||
existing_file
|
||||
)} and ${file.name}`
|
||||
);
|
||||
}
|
||||
|
||||
if (item.kind === 'component') {
|
||||
if (item.is_error) {
|
||||
route.error = {
|
||||
depth,
|
||||
component: project_relative
|
||||
};
|
||||
} else if (item.is_layout) {
|
||||
if (!route.layout) {
|
||||
route.layout = { depth, child_pages: [] };
|
||||
} else if (route.layout.component) {
|
||||
throw duplicate_files_error('layout component', route.layout.component);
|
||||
}
|
||||
|
||||
route.layout.component = project_relative;
|
||||
if (item.uses_layout !== undefined) route.layout.parent_id = item.uses_layout;
|
||||
} else {
|
||||
if (!route.leaf) {
|
||||
route.leaf = { depth };
|
||||
} else if (route.leaf.component) {
|
||||
throw duplicate_files_error('page component', route.leaf.component);
|
||||
}
|
||||
|
||||
route.leaf.component = project_relative;
|
||||
if (item.uses_layout !== undefined) route.leaf.parent_id = item.uses_layout;
|
||||
}
|
||||
} else if (item.is_layout) {
|
||||
if (!route.layout) {
|
||||
route.layout = { depth, child_pages: [] };
|
||||
} else if (route.layout[item.kind]) {
|
||||
throw duplicate_files_error(
|
||||
item.kind + ' layout module',
|
||||
/** @type {string} */ (route.layout[item.kind])
|
||||
);
|
||||
}
|
||||
|
||||
route.layout[item.kind] = project_relative;
|
||||
} else if (item.is_page) {
|
||||
if (!route.leaf) {
|
||||
route.leaf = { depth };
|
||||
} else if (route.leaf[item.kind]) {
|
||||
throw duplicate_files_error(
|
||||
item.kind + ' page module',
|
||||
/** @type {string} */ (route.leaf[item.kind])
|
||||
);
|
||||
}
|
||||
|
||||
route.leaf[item.kind] = project_relative;
|
||||
} else {
|
||||
if (route.endpoint) {
|
||||
throw duplicate_files_error('endpoint', route.endpoint.file);
|
||||
}
|
||||
|
||||
route.endpoint = {
|
||||
file: project_relative,
|
||||
page_options: null // will be filled later
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// then handle children
|
||||
for (const file of files) {
|
||||
if (file.is_dir) {
|
||||
walk(depth + 1, path.posix.join(id, file.name), file.name, route);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(0, '/', '', null);
|
||||
|
||||
if (routes.length === 1) {
|
||||
const root = routes[0];
|
||||
if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
|
||||
throw new Error(
|
||||
'No routes found. If you are using a custom src/routes directory, make sure it is specified in your Svelte config file'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no routes directory, we'll just create a single empty route. This ensures the root layout and
|
||||
// error components are included in the manifest, which is needed for subsequent build/dev commands to work
|
||||
routes.push({
|
||||
id: '/',
|
||||
segment: '',
|
||||
pattern: /^$/,
|
||||
params: [],
|
||||
parent: null,
|
||||
layout: null,
|
||||
error: null,
|
||||
leaf: null,
|
||||
page: null,
|
||||
endpoint: null
|
||||
});
|
||||
}
|
||||
|
||||
prevent_conflicts(routes);
|
||||
|
||||
const root = routes[0];
|
||||
|
||||
if (!root.layout?.component) {
|
||||
if (!root.layout) root.layout = { depth: 0, child_pages: [] };
|
||||
root.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
|
||||
}
|
||||
|
||||
if (!root.error?.component) {
|
||||
if (!root.error) root.error = { depth: 0 };
|
||||
root.error.component = posixify(path.relative(cwd, `${fallback}/error.svelte`));
|
||||
}
|
||||
|
||||
// we do layouts/errors first as they are more likely to be reused,
|
||||
// and smaller indexes take fewer bytes. also, this guarantees that
|
||||
// the default error/layout are 0/1
|
||||
for (const route of routes) {
|
||||
if (route.layout) {
|
||||
if (!route.layout?.component) {
|
||||
route.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
|
||||
}
|
||||
nodes.push(route.layout);
|
||||
}
|
||||
if (route.error) nodes.push(route.error);
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.leaf) nodes.push(route.leaf);
|
||||
}
|
||||
|
||||
const indexes = new Map(nodes.map((node, i) => [node, i]));
|
||||
|
||||
const node_analyser = create_node_analyser();
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.leaf) continue;
|
||||
|
||||
route.page = {
|
||||
layouts: [],
|
||||
errors: [],
|
||||
leaf: /** @type {number} */ (indexes.get(route.leaf))
|
||||
};
|
||||
|
||||
/** @type {import('types').RouteData | null} */
|
||||
let current_route = route;
|
||||
let current_node = route.leaf;
|
||||
let parent_id = route.leaf.parent_id;
|
||||
|
||||
while (current_route) {
|
||||
if (parent_id === undefined || current_route.segment === parent_id) {
|
||||
if (current_route.layout || current_route.error) {
|
||||
route.page.layouts.unshift(
|
||||
current_route.layout ? indexes.get(current_route.layout) : undefined
|
||||
);
|
||||
route.page.errors.unshift(
|
||||
current_route.error ? indexes.get(current_route.error) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
if (current_route.layout) {
|
||||
/** @type {import('types').PageNode[]} */ (current_route.layout.child_pages).push(
|
||||
route.leaf
|
||||
);
|
||||
current_node.parent = current_node = current_route.layout;
|
||||
parent_id = current_node.parent_id;
|
||||
} else {
|
||||
parent_id = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
current_route = current_route.parent;
|
||||
}
|
||||
|
||||
if (parent_id !== undefined) {
|
||||
throw new Error(`${current_node.component} references missing segment "${parent_id}"`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
node.page_options = node_analyser.get_page_options(node);
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.endpoint) {
|
||||
route.endpoint.page_options = get_page_options(route.endpoint.file);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
routes: sort_routes(routes)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} project_relative
|
||||
* @param {string} file
|
||||
* @param {string[]} component_extensions
|
||||
* @param {string[]} module_extensions
|
||||
* @returns {import('./types.js').RouteFile}
|
||||
*/
|
||||
function analyze(project_relative, file, component_extensions, module_extensions) {
|
||||
const component_extension = component_extensions.find((ext) => file.endsWith(ext));
|
||||
if (component_extension) {
|
||||
const name = file.slice(0, -component_extension.length);
|
||||
const pattern = /^\+(?:(page(?:@(.*))?)|(layout(?:@(.*))?)|(error))$/;
|
||||
const match = pattern.exec(name);
|
||||
if (!match) {
|
||||
throw new Error(`Files prefixed with + are reserved (saw ${project_relative})`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'component',
|
||||
is_page: !!match[1],
|
||||
is_layout: !!match[3],
|
||||
is_error: !!match[5],
|
||||
uses_layout: match[2] ?? match[4]
|
||||
};
|
||||
}
|
||||
|
||||
const module_extension = module_extensions.find((ext) => file.endsWith(ext));
|
||||
if (module_extension) {
|
||||
const name = file.slice(0, -module_extension.length);
|
||||
const pattern =
|
||||
/^\+(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/;
|
||||
const match = pattern.exec(name);
|
||||
if (!match) {
|
||||
throw new Error(`Files prefixed with + are reserved (saw ${project_relative})`);
|
||||
} else if (match[3] || match[6]) {
|
||||
throw new Error(
|
||||
// prettier-ignore
|
||||
`Only Svelte files can reference named layouts. Remove '${match[3] || match[6]}' from ${file} (at ${project_relative})`
|
||||
);
|
||||
}
|
||||
|
||||
const kind = match[1] || match[4] || match[7] ? 'server' : 'universal';
|
||||
|
||||
return {
|
||||
kind,
|
||||
is_page: !!match[2],
|
||||
is_layout: !!match[5]
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Files and directories prefixed with + are reserved (saw ${project_relative})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} needle
|
||||
* @param {string} haystack
|
||||
*/
|
||||
function count_occurrences(needle, haystack) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < haystack.length; i += 1) {
|
||||
if (haystack[i] === needle) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** @param {import('types').RouteData[]} routes */
|
||||
function prevent_conflicts(routes) {
|
||||
/** @type {Map<string, string>} */
|
||||
const lookup = new Map();
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.leaf && !route.endpoint) continue;
|
||||
|
||||
const normalized = normalize_route_id(route.id);
|
||||
|
||||
// find all permutations created by optional parameters
|
||||
const split = normalized.split(/<\?(.+?)>/g);
|
||||
|
||||
let permutations = [/** @type {string} */ (split[0])];
|
||||
|
||||
// turn `x/[[optional]]/y` into `x/y` and `x/[required]/y`
|
||||
for (let i = 1; i < split.length; i += 2) {
|
||||
const matcher = split[i];
|
||||
const next = split[i + 1];
|
||||
|
||||
permutations = permutations.reduce((a, b) => {
|
||||
a.push(b + next);
|
||||
if (!(matcher === '*' && b.endsWith('//'))) a.push(b + `<${matcher}>${next}`);
|
||||
return a;
|
||||
}, /** @type {string[]} */ ([]));
|
||||
}
|
||||
|
||||
for (const permutation of permutations) {
|
||||
// remove leading/trailing/duplicated slashes caused by prior
|
||||
// manipulation of optional parameters and (groups)
|
||||
const key = permutation
|
||||
.replace(/\/{2,}/, '/')
|
||||
.replace(/^\//, '')
|
||||
.replace(/\/$/, '');
|
||||
|
||||
if (lookup.has(key)) {
|
||||
throw new Error(
|
||||
`The "${lookup.get(key)}" and "${route.id}" routes conflict with each other`
|
||||
);
|
||||
}
|
||||
|
||||
lookup.set(key, route.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function normalize_route_id(id) {
|
||||
return (
|
||||
id
|
||||
// remove groups
|
||||
.replace(/(?<=^|\/)\(.+?\)(?=$|\/)/g, '')
|
||||
|
||||
.replace(/\[[ux]\+([0-9a-f]+)\]/g, (_, x) =>
|
||||
String.fromCharCode(parseInt(x, 16)).replace(/\//g, '%2f')
|
||||
)
|
||||
|
||||
// replace `[param]` with `<*>`, `[param=x]` with `<x>`, and `[[param]]` with `<?*>`
|
||||
.replace(
|
||||
/\[(?:(\[)|(\.\.\.))?.+?(=.+?)?\]\]?/g,
|
||||
(_, optional, rest, matcher) => `<${optional ? '?' : ''}${rest ?? ''}${matcher ?? '*'}>`
|
||||
)
|
||||
);
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { get_route_segments } from '../../../utils/routing.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'static' | 'required' | 'optional' | 'rest';
|
||||
* content: string;
|
||||
* matched: boolean;
|
||||
* }} Part
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Part[]} Segment
|
||||
*/
|
||||
|
||||
const EMPTY = { type: 'static', content: '', matched: false };
|
||||
|
||||
/** @param {import('types').RouteData[]} routes */
|
||||
export function sort_routes(routes) {
|
||||
/** @type {Map<string, Part[]>} */
|
||||
const segment_cache = new Map();
|
||||
|
||||
/** @param {string} segment */
|
||||
function get_parts(segment) {
|
||||
if (!segment_cache.has(segment)) {
|
||||
segment_cache.set(segment, split(segment));
|
||||
}
|
||||
|
||||
return segment_cache.get(segment);
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function split(id) {
|
||||
/** @type {Part[]} */
|
||||
const parts = [];
|
||||
|
||||
let i = 0;
|
||||
while (i <= id.length) {
|
||||
const start = id.indexOf('[', i);
|
||||
if (start === -1) {
|
||||
parts.push({ type: 'static', content: id.slice(i), matched: false });
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push({ type: 'static', content: id.slice(i, start), matched: false });
|
||||
|
||||
const type = id[start + 1] === '[' ? 'optional' : id[start + 1] === '.' ? 'rest' : 'required';
|
||||
const delimiter = type === 'optional' ? ']]' : ']';
|
||||
const end = id.indexOf(delimiter, start);
|
||||
|
||||
if (end === -1) {
|
||||
throw new Error(`Invalid route ID ${id}`);
|
||||
}
|
||||
|
||||
const content = id.slice(start, (i = end + delimiter.length));
|
||||
|
||||
parts.push({
|
||||
type,
|
||||
content,
|
||||
matched: content.includes('=')
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
return routes.sort((route_a, route_b) => {
|
||||
const segments_a = split_route_id(route_a.id).map(get_parts);
|
||||
const segments_b = split_route_id(route_b.id).map(get_parts);
|
||||
|
||||
for (let i = 0; i < Math.max(segments_a.length, segments_b.length); i += 1) {
|
||||
const segment_a = segments_a[i] ?? [EMPTY];
|
||||
const segment_b = segments_b[i] ?? [EMPTY];
|
||||
|
||||
for (let j = 0; j < Math.max(segment_a.length, segment_b.length); j += 1) {
|
||||
const a = segment_a[j];
|
||||
const b = segment_b[j];
|
||||
|
||||
// first part of each segment is always static
|
||||
// (though it may be the empty string), then
|
||||
// it alternates between dynamic and static
|
||||
// (i.e. [foo][bar] is disallowed)
|
||||
const dynamic = j % 2 === 1;
|
||||
|
||||
if (dynamic) {
|
||||
if (!a) return -1;
|
||||
if (!b) return +1;
|
||||
|
||||
// get the next static chunk, so we can handle [...rest] edge cases
|
||||
const next_a = segment_a[j + 1].content || segments_a[i + 1]?.[0].content;
|
||||
const next_b = segment_b[j + 1].content || segments_b[i + 1]?.[0].content;
|
||||
|
||||
// `[...rest]/x` outranks `[...rest]`
|
||||
if (a.type === 'rest' && b.type === 'rest') {
|
||||
if (next_a && next_b) continue;
|
||||
if (next_a) return -1;
|
||||
if (next_b) return +1;
|
||||
}
|
||||
|
||||
// `[...rest]/x` outranks `[required]` or `[required]/[required]`
|
||||
// but not `[required]/x`
|
||||
if (a.type === 'rest') {
|
||||
return next_a && !next_b ? -1 : +1;
|
||||
}
|
||||
|
||||
if (b.type === 'rest') {
|
||||
return next_b && !next_a ? +1 : -1;
|
||||
}
|
||||
|
||||
// part with matcher outranks one without
|
||||
if (a.matched !== b.matched) {
|
||||
return a.matched ? -1 : +1;
|
||||
}
|
||||
|
||||
if (a.type !== b.type) {
|
||||
// `[...rest]` has already been accounted for, so here
|
||||
// we're comparing between `[required]` and `[[optional]]`
|
||||
if (a.type === 'required') return -1;
|
||||
if (b.type === 'required') return +1;
|
||||
}
|
||||
} else if (a.content !== b.content) {
|
||||
// shallower path outranks deeper path
|
||||
if (a === EMPTY) return -1;
|
||||
if (b === EMPTY) return +1;
|
||||
|
||||
return sort_static(a.content, b.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return route_a.id < route_b.id ? +1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function split_route_id(id) {
|
||||
return get_route_segments(
|
||||
id
|
||||
// remove all [[optional]] parts unless they're at the very end
|
||||
// or it ends with a route group
|
||||
.replace(/\[\[[^\]]+\]\](?!(?:\/\([^/]+\))*$)/g, '')
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort two strings lexicographically, except `foobar` outranks `foo`
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function sort_static(a, b) {
|
||||
if (a === b) return 0;
|
||||
|
||||
for (let i = 0; true; i += 1) {
|
||||
const char_a = a[i];
|
||||
const char_b = b[i];
|
||||
|
||||
if (char_a !== char_b) {
|
||||
if (char_a === undefined) return +1;
|
||||
if (char_b === undefined) return -1;
|
||||
return char_a < char_b ? -1 : +1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { PageNode } from 'types';
|
||||
|
||||
interface Part {
|
||||
dynamic: boolean;
|
||||
optional: boolean;
|
||||
rest: boolean;
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
interface RouteTreeNode {
|
||||
error: PageNode | undefined;
|
||||
layout: PageNode | undefined;
|
||||
}
|
||||
|
||||
export type RouteTree = Map<string, RouteTreeNode>;
|
||||
|
||||
interface RouteComponent {
|
||||
kind: 'component';
|
||||
is_page: boolean;
|
||||
is_layout: boolean;
|
||||
is_error: boolean;
|
||||
uses_layout: string | undefined;
|
||||
}
|
||||
|
||||
interface RouteSharedModule {
|
||||
kind: 'universal';
|
||||
is_page: boolean;
|
||||
is_layout: boolean;
|
||||
}
|
||||
|
||||
interface RouteServerModule {
|
||||
kind: 'server';
|
||||
is_page: boolean;
|
||||
is_layout: boolean;
|
||||
}
|
||||
|
||||
export type RouteFile = RouteComponent | RouteSharedModule | RouteServerModule;
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import path from 'node:path';
|
||||
import create_manifest_data from './create_manifest_data/index.js';
|
||||
import { write_client_manifest } from './write_client_manifest.js';
|
||||
import { write_root } from './write_root.js';
|
||||
import { write_tsconfig } from './write_tsconfig.js';
|
||||
import { write_types, write_all_types } from './write_types/index.js';
|
||||
import { write_ambient } from './write_ambient.js';
|
||||
import { write_non_ambient } from './write_non_ambient.js';
|
||||
import { write_server } from './write_server.js';
|
||||
import {
|
||||
create_node_analyser,
|
||||
get_page_options
|
||||
} from '../../exports/vite/static_analysis/index.js';
|
||||
|
||||
/**
|
||||
* Initialize SvelteKit's generated files that only depend on the config and mode.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} mode
|
||||
*/
|
||||
export function init(config, mode) {
|
||||
write_tsconfig(config.kit);
|
||||
write_ambient(config.kit, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SvelteKit's generated files
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function create(config) {
|
||||
const manifest_data = create_manifest_data({ config });
|
||||
|
||||
const output = path.join(config.kit.outDir, 'generated');
|
||||
|
||||
write_client_manifest(config.kit, manifest_data, `${output}/client`);
|
||||
write_server(config, output);
|
||||
write_root(manifest_data, config, output);
|
||||
write_all_types(config, manifest_data);
|
||||
write_non_ambient(config.kit, manifest_data);
|
||||
|
||||
return { manifest_data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SvelteKit's generated files in response to a single file content update.
|
||||
* Do not call this when the file in question was created/deleted.
|
||||
*
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} file
|
||||
*/
|
||||
export function update(config, manifest_data, file) {
|
||||
const node_analyser = create_node_analyser();
|
||||
|
||||
for (const node of manifest_data.nodes) {
|
||||
node.page_options = node_analyser.get_page_options(node);
|
||||
}
|
||||
|
||||
for (const route of manifest_data.routes) {
|
||||
if (route.endpoint) {
|
||||
route.endpoint.page_options = get_page_options(route.endpoint.file);
|
||||
}
|
||||
}
|
||||
|
||||
write_types(config, manifest_data, file);
|
||||
write_non_ambient(config.kit, manifest_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run sync.init and sync.create in series, returning the result from sync.create.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} mode The Vite mode
|
||||
*/
|
||||
export function all(config, mode) {
|
||||
init(config, mode);
|
||||
return create(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run sync.init and then generate all type files.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} mode The Vite mode
|
||||
*/
|
||||
export function all_types(config, mode) {
|
||||
init(config, mode);
|
||||
const manifest_data = create_manifest_data({ config });
|
||||
write_all_types(config, manifest_data);
|
||||
write_non_ambient(config.kit, manifest_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate __SERVER__/internal.js in response to src/{app.html,error.html,service-worker.js} changing
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function server(config) {
|
||||
write_server(config, path.join(config.kit.outDir, 'generated'));
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/** @type {import('typescript')} */
|
||||
// @ts-ignore
|
||||
export let ts = undefined;
|
||||
try {
|
||||
ts = (await import('typescript')).default;
|
||||
} catch {}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { mkdirp } from '../../utils/filesystem.js';
|
||||
import { import_peer } from '../../utils/import.js';
|
||||
|
||||
/** @type {{ VERSION: string }} */
|
||||
const { VERSION } = await import_peer('svelte/compiler');
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const previous_contents = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {string} code
|
||||
*/
|
||||
export function write_if_changed(file, code) {
|
||||
if (code !== previous_contents.get(file)) {
|
||||
write(file, code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {string} code
|
||||
*/
|
||||
export function write(file, code) {
|
||||
previous_contents.set(file, code);
|
||||
mkdirp(path.dirname(file));
|
||||
fs.writeFileSync(file, code);
|
||||
}
|
||||
|
||||
/** @type {WeakMap<TemplateStringsArray, { strings: string[], indents: string[] }>} */
|
||||
const dedent_map = new WeakMap();
|
||||
|
||||
/**
|
||||
* Allows indenting template strings without the extra indentation ending up in the result.
|
||||
* Still allows indentation of lines relative to one another in the template string.
|
||||
* @param {TemplateStringsArray} strings
|
||||
* @param {any[]} values
|
||||
*/
|
||||
export function dedent(strings, ...values) {
|
||||
let dedented = dedent_map.get(strings);
|
||||
|
||||
if (!dedented) {
|
||||
const indentation = /** @type {RegExpExecArray} */ (/\n?([ \t]*)/.exec(strings[0]))[1];
|
||||
const pattern = new RegExp(`^${indentation}`, 'gm');
|
||||
|
||||
dedented = {
|
||||
strings: strings.map((str) => str.replace(pattern, '')),
|
||||
indents: []
|
||||
};
|
||||
|
||||
let current = '\n';
|
||||
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
const string = dedented.strings[i];
|
||||
const match = /\n([ \t]*)$/.exec(string);
|
||||
|
||||
if (match) current = match[0];
|
||||
dedented.indents[i] = current;
|
||||
}
|
||||
|
||||
dedent_map.set(strings, dedented);
|
||||
}
|
||||
|
||||
let str = dedented.strings[0];
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
str += String(values[i]).replace(/\n/g, dedented.indents[i]) + dedented.strings[i + 1];
|
||||
}
|
||||
|
||||
str = str.trim();
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export function isSvelte5Plus() {
|
||||
return Number(VERSION[0]) >= 5;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { get_env } from '../../exports/vite/utils.js';
|
||||
import { GENERATED_COMMENT } from '../../constants.js';
|
||||
import { create_dynamic_types, create_static_types } from '../env.js';
|
||||
import { write_if_changed } from './utils.js';
|
||||
|
||||
// TODO these types should be described in a neutral place, rather than
|
||||
// inside either `packages/kit` or `svelte.dev/docs/kit`
|
||||
const descriptions_dir = fileURLToPath(new URL('../../../src/types/synthetic', import.meta.url));
|
||||
|
||||
/** @param {string} filename */
|
||||
function read_description(filename) {
|
||||
const content = fs.readFileSync(`${descriptions_dir}/${filename}`, 'utf8');
|
||||
return `/**\n${content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => ` * ${line}`)
|
||||
.join('\n')}\n */`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').Env} env
|
||||
* @param {{
|
||||
* public_prefix: string;
|
||||
* private_prefix: string;
|
||||
* }} prefixes
|
||||
*/
|
||||
const template = (env, prefixes) => `
|
||||
${GENERATED_COMMENT}
|
||||
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
${read_description('$env+static+private.md')}
|
||||
${create_static_types('private', env)}
|
||||
|
||||
${read_description('$env+static+public.md')}
|
||||
${create_static_types('public', env)}
|
||||
|
||||
${read_description('$env+dynamic+private.md')}
|
||||
${create_dynamic_types('private', env, prefixes)}
|
||||
|
||||
${read_description('$env+dynamic+public.md')}
|
||||
${create_dynamic_types('public', env, prefixes)}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Writes ambient declarations including types reference to @sveltejs/kit,
|
||||
* and the existing environment variables in process.env to
|
||||
* $env/static/private and $env/static/public
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* @param {string} mode The Vite mode
|
||||
*/
|
||||
export function write_ambient(config, mode) {
|
||||
const env = get_env(config.env, mode);
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
|
||||
|
||||
write_if_changed(
|
||||
path.join(config.outDir, 'ambient.d.ts'),
|
||||
template(env, { public_prefix, private_prefix })
|
||||
);
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import path from 'node:path';
|
||||
import { relative_path, resolve_entry } from '../../utils/filesystem.js';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { dedent, isSvelte5Plus, write_if_changed } from './utils.js';
|
||||
import colors from 'kleur';
|
||||
|
||||
/**
|
||||
* Writes the client manifest to disk. The manifest is used to power the router. It contains the
|
||||
* list of routes and corresponding Svelte components (i.e. pages and layouts).
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} output
|
||||
* @param {import('types').ServerMetadata['nodes']} [metadata] If this is omitted, we have to assume that all routes with a `+layout/page.server.js` file have a server load function
|
||||
*/
|
||||
export function write_client_manifest(kit, manifest_data, output, metadata) {
|
||||
const client_routing = kit.router.resolution === 'client';
|
||||
|
||||
/**
|
||||
* Creates a module that exports a `CSRPageNode`
|
||||
* @param {import('types').PageNode} node
|
||||
*/
|
||||
function generate_node(node) {
|
||||
const declarations = [];
|
||||
|
||||
if (node.universal) {
|
||||
declarations.push(
|
||||
`import * as universal from ${s(relative_path(`${output}/nodes`, node.universal))};`,
|
||||
'export { universal };'
|
||||
);
|
||||
}
|
||||
|
||||
if (node.component) {
|
||||
declarations.push(
|
||||
`export { default as component } from ${s(
|
||||
relative_path(`${output}/nodes`, node.component)
|
||||
)};`
|
||||
);
|
||||
}
|
||||
|
||||
return declarations.join('\n');
|
||||
}
|
||||
|
||||
/** @type {Map<import('types').PageNode, number>} */
|
||||
const indices = new Map();
|
||||
const nodes = manifest_data.nodes
|
||||
.map((node, i) => {
|
||||
indices.set(node, i);
|
||||
|
||||
write_if_changed(`${output}/nodes/${i}.js`, generate_node(node));
|
||||
return `() => import('./nodes/${i}')`;
|
||||
})
|
||||
// If route resolution happens on the server, we only need the root layout and root error page
|
||||
// upfront, the rest is loaded on demand as the user navigates the app
|
||||
.slice(0, client_routing ? manifest_data.nodes.length : 2)
|
||||
.join(',\n');
|
||||
|
||||
const layouts_with_server_load = new Set();
|
||||
|
||||
let dictionary = dedent`
|
||||
{
|
||||
${manifest_data.routes
|
||||
.map((route) => {
|
||||
if (route.page) {
|
||||
const errors = route.page.errors.slice(1).map((n) => n ?? '');
|
||||
const layouts = route.page.layouts.slice(1).map((n) => n ?? '');
|
||||
|
||||
while (layouts.at(-1) === '') layouts.pop();
|
||||
while (errors.at(-1) === '') errors.pop();
|
||||
|
||||
let leaf_has_server_load = false;
|
||||
if (route.leaf) {
|
||||
if (metadata) {
|
||||
const i = /** @type {number} */ (indices.get(route.leaf));
|
||||
|
||||
leaf_has_server_load = metadata[i].has_server_load;
|
||||
} else if (route.leaf.server) {
|
||||
leaf_has_server_load = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode whether or not the route uses server data
|
||||
// using the ones' complement, to save space
|
||||
const array = [`${leaf_has_server_load ? '~' : ''}${route.page.leaf}`];
|
||||
|
||||
// Encode whether or not the layout uses server data.
|
||||
// It's a different method compared to pages because layouts
|
||||
// are reused across pages, so we save space by doing it this way.
|
||||
route.page.layouts.forEach((layout) => {
|
||||
if (layout == undefined) return;
|
||||
|
||||
let layout_has_server_load = false;
|
||||
|
||||
if (metadata) {
|
||||
layout_has_server_load = metadata[layout].has_server_load;
|
||||
} else if (manifest_data.nodes[layout].server) {
|
||||
layout_has_server_load = true;
|
||||
}
|
||||
|
||||
if (layout_has_server_load) {
|
||||
layouts_with_server_load.add(layout);
|
||||
}
|
||||
});
|
||||
|
||||
// only include non-root layout/error nodes if they exist
|
||||
if (layouts.length > 0 || errors.length > 0) array.push(`[${layouts.join(',')}]`);
|
||||
if (errors.length > 0) array.push(`[${errors.join(',')}]`);
|
||||
|
||||
return `${s(route.id)}: [${array.join(',')}]`;
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(',\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
if (!client_routing) {
|
||||
dictionary = '{}';
|
||||
const root_layout = layouts_with_server_load.has(0);
|
||||
layouts_with_server_load.clear();
|
||||
if (root_layout) layouts_with_server_load.add(0);
|
||||
}
|
||||
|
||||
const client_hooks_file = resolve_entry(kit.files.hooks.client);
|
||||
const universal_hooks_file = resolve_entry(kit.files.hooks.universal);
|
||||
|
||||
const typo = resolve_entry('src/+hooks.client');
|
||||
if (typo) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` +
|
||||
` at ${path.resolve(typo)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Stringified version of
|
||||
/** @type {import('../../runtime/client/types.js').SvelteKitApp} */
|
||||
write_if_changed(
|
||||
`${output}/app.js`,
|
||||
dedent`
|
||||
${
|
||||
client_hooks_file
|
||||
? `import * as client_hooks from '${relative_path(output, client_hooks_file)}';`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
universal_hooks_file
|
||||
? `import * as universal_hooks from '${relative_path(output, universal_hooks_file)}';`
|
||||
: ''
|
||||
}
|
||||
|
||||
${client_routing ? "export { matchers } from './matchers.js';" : 'export const matchers = {};'}
|
||||
|
||||
export const nodes = [
|
||||
${nodes}
|
||||
];
|
||||
|
||||
export const server_loads = [${[...layouts_with_server_load].join(',')}];
|
||||
|
||||
export const dictionary = ${dictionary};
|
||||
|
||||
export const hooks = {
|
||||
handleError: ${
|
||||
client_hooks_file ? 'client_hooks.handleError || ' : ''
|
||||
}(({ error }) => { console.error(error) }),
|
||||
${client_hooks_file ? 'init: client_hooks.init,' : ''}
|
||||
reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}),
|
||||
transport: ${universal_hooks_file ? 'universal_hooks.transport || ' : ''}{}
|
||||
};
|
||||
|
||||
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
|
||||
export const encoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.encode]));
|
||||
|
||||
export const hash = ${s(kit.router.type === 'hash')};
|
||||
|
||||
export const decode = (type, value) => decoders[type](value);
|
||||
|
||||
export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
|
||||
`
|
||||
);
|
||||
|
||||
if (client_routing) {
|
||||
// write matchers to a separate module so that we don't
|
||||
// need to worry about name conflicts
|
||||
const imports = [];
|
||||
const matchers = [];
|
||||
|
||||
for (const key in manifest_data.matchers) {
|
||||
const src = manifest_data.matchers[key];
|
||||
|
||||
imports.push(`import { match as ${key} } from ${s(relative_path(output, src))};`);
|
||||
matchers.push(key);
|
||||
}
|
||||
|
||||
const module = imports.length
|
||||
? `${imports.join('\n')}\n\nexport const matchers = { ${matchers.join(', ')} };`
|
||||
: 'export const matchers = {};';
|
||||
|
||||
write_if_changed(`${output}/matchers.js`, module);
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
import path from 'node:path';
|
||||
import { GENERATED_COMMENT } from '../../constants.js';
|
||||
import { posixify } from '../../utils/filesystem.js';
|
||||
import { write_if_changed } from './utils.js';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { get_route_segments } from '../../utils/routing.js';
|
||||
|
||||
const replace_optional_params = (/** @type {string} */ id) =>
|
||||
id.replace(/\/\[\[[^\]]+\]\]/g, '${string}');
|
||||
const replace_required_params = (/** @type {string} */ id) =>
|
||||
id.replace(/\/\[[^\]]+\]/g, '/${string}');
|
||||
/** Convert route ID to pathname by removing layout groups */
|
||||
const remove_group_segments = (/** @type {string} */ id) => {
|
||||
return '/' + get_route_segments(id).join('/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get pathnames to add based on trailingSlash settings
|
||||
* @param {string} pathname
|
||||
* @param {import('types').RouteData} route
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function get_pathnames_for_trailing_slash(pathname, route) {
|
||||
if (pathname === '/') {
|
||||
return [pathname];
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const pathnames = new Set();
|
||||
|
||||
/**
|
||||
* @param {{ trailingSlash?: import('types').TrailingSlash } | null | undefined} page_options
|
||||
*/
|
||||
const add_pathnames = (page_options) => {
|
||||
if (page_options === null || page_options?.trailingSlash === 'ignore') {
|
||||
pathnames.add(pathname);
|
||||
pathnames.add(pathname + '/');
|
||||
} else if (page_options?.trailingSlash === 'always') {
|
||||
pathnames.add(pathname + '/');
|
||||
} else {
|
||||
pathnames.add(pathname);
|
||||
}
|
||||
};
|
||||
|
||||
if (route.leaf) add_pathnames(route.leaf.page_options ?? null);
|
||||
if (route.endpoint) add_pathnames(route.endpoint.page_options);
|
||||
|
||||
return Array.from(pathnames);
|
||||
}
|
||||
|
||||
// `declare module "svelte/elements"` needs to happen in a non-ambient module, and dts-buddy generates one big ambient module,
|
||||
// so we can't add it there - therefore generate the typings ourselves here.
|
||||
// We're not using the `declare namespace svelteHTML` variant because that one doesn't augment the HTMLAttributes interface
|
||||
// people could use to type their own components.
|
||||
// The T generic is needed or else there's a "all declarations must have identical type parameters" error.
|
||||
const template = `
|
||||
${GENERATED_COMMENT}
|
||||
|
||||
declare module "svelte/elements" {
|
||||
export interface HTMLAttributes<T> {
|
||||
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
|
||||
'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;
|
||||
'data-sveltekit-preload-code'?:
|
||||
| true
|
||||
| ''
|
||||
| 'eager'
|
||||
| 'viewport'
|
||||
| 'hover'
|
||||
| 'tap'
|
||||
| 'off'
|
||||
| undefined
|
||||
| null;
|
||||
'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
|
||||
'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
|
||||
'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
`;
|
||||
|
||||
/**
|
||||
* Generate app types interface extension
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
*/
|
||||
function generate_app_types(manifest_data, config) {
|
||||
/** @param {string} matcher */
|
||||
const path_to_matcher = (matcher) =>
|
||||
posixify(path.relative(config.outDir, path.join(config.files.params, matcher + '.js')));
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const matcher_types = new Map();
|
||||
|
||||
/** @param {string | undefined} matcher */
|
||||
const get_matcher_type = (matcher) => {
|
||||
if (!matcher) return 'string';
|
||||
|
||||
let type = matcher_types.get(matcher);
|
||||
if (!type) {
|
||||
type = `MatcherParam<typeof import('${path_to_matcher(matcher)}').match>`;
|
||||
matcher_types.set(matcher, type);
|
||||
}
|
||||
|
||||
return type;
|
||||
};
|
||||
|
||||
/** @param {Set<string> | null} matchers */
|
||||
const get_matchers_type = (matchers) => {
|
||||
if (matchers === null) return 'string';
|
||||
|
||||
return Array.from(matchers)
|
||||
.map((matcher) => get_matcher_type(matcher))
|
||||
.join(' | ');
|
||||
};
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const route_ids = new Set(manifest_data.routes.map((route) => route.id));
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const get_ancestor_route_ids = (id) => {
|
||||
/** @type {string[]} */
|
||||
const ancestors = [];
|
||||
|
||||
if (route_ids.has('/')) {
|
||||
ancestors.push('/');
|
||||
}
|
||||
|
||||
let current = '';
|
||||
for (const segment of id.slice(1).split('/')) {
|
||||
if (!segment) continue;
|
||||
|
||||
current += '/' + segment;
|
||||
if (route_ids.has(current)) {
|
||||
ancestors.push(current);
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors;
|
||||
};
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const pathnames = new Set();
|
||||
|
||||
/** @type {string[]} */
|
||||
const dynamic_routes = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const layouts = [];
|
||||
|
||||
/** @type {Map<string, Map<string, { optional: boolean, matchers: Set<string> | null }>>} */
|
||||
const layout_params_by_route = new Map(
|
||||
manifest_data.routes.map((route) => [
|
||||
route.id,
|
||||
new Map(
|
||||
route.params.map((p) => [
|
||||
p.name,
|
||||
{ optional: p.optional, matchers: p.matcher ? new Set([p.matcher]) : null }
|
||||
])
|
||||
)
|
||||
])
|
||||
);
|
||||
|
||||
for (const route of manifest_data.routes) {
|
||||
const ancestors = get_ancestor_route_ids(route.id);
|
||||
|
||||
for (const ancestor_id of ancestors) {
|
||||
const ancestor_params = layout_params_by_route.get(ancestor_id);
|
||||
if (!ancestor_params) continue;
|
||||
|
||||
for (const p of route.params) {
|
||||
const matcher = p.matcher ?? null;
|
||||
const entry = ancestor_params.get(p.name);
|
||||
if (!entry) {
|
||||
ancestor_params.set(p.name, {
|
||||
optional: true,
|
||||
matchers: matcher === null ? null : new Set([matcher])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.matchers === null) continue;
|
||||
|
||||
if (matcher === null) {
|
||||
entry.matchers = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.matchers.add(matcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const route of manifest_data.routes) {
|
||||
const pathname = remove_group_segments(route.id);
|
||||
let normalized_pathname = pathname;
|
||||
|
||||
/** @type {(path: string) => string} */
|
||||
let serialise = s;
|
||||
|
||||
if (route.params.length > 0) {
|
||||
const params = route.params.map((p) => {
|
||||
const type = get_matcher_type(p.matcher);
|
||||
return `${p.name}${p.optional ? '?:' : ':'} ${type}`;
|
||||
});
|
||||
const route_type = `${s(route.id)}: { ${params.join('; ')} }`;
|
||||
|
||||
dynamic_routes.push(route_type);
|
||||
|
||||
normalized_pathname = replace_required_params(replace_optional_params(pathname));
|
||||
serialise = (p) => `\`${p}\` & {}`;
|
||||
}
|
||||
|
||||
for (const p of get_pathnames_for_trailing_slash(normalized_pathname, route)) {
|
||||
pathnames.add(serialise(p));
|
||||
}
|
||||
|
||||
let layout_type = 'Record<string, never>';
|
||||
|
||||
const layout_params = layout_params_by_route.get(route.id);
|
||||
if (layout_params) {
|
||||
const params = Array.from(layout_params)
|
||||
.map(([name, { optional, matchers }]) => {
|
||||
const type = get_matchers_type(matchers);
|
||||
return `${name}${optional ? '?:' : ':'} ${type}`;
|
||||
})
|
||||
.join('; ');
|
||||
|
||||
if (params.length > 0) layout_type = `{ ${params} }`;
|
||||
}
|
||||
|
||||
layouts.push(`${s(route.id)}: ${layout_type}`);
|
||||
}
|
||||
|
||||
const assets = manifest_data.assets.map((asset) => s('/' + asset.file));
|
||||
|
||||
return [
|
||||
'declare module "$app/types" {',
|
||||
'\ttype MatcherParam<M> = M extends (param : string) => param is (infer U extends string) ? U : string;',
|
||||
'',
|
||||
'\texport interface AppTypes {',
|
||||
`\t\tRouteId(): ${manifest_data.routes.map((r) => s(r.id)).join(' | ')};`,
|
||||
`\t\tRouteParams(): {\n\t\t\t${dynamic_routes.join(';\n\t\t\t')}\n\t\t};`,
|
||||
`\t\tLayoutParams(): {\n\t\t\t${layouts.join(';\n\t\t\t')}\n\t\t};`,
|
||||
`\t\tPathname(): ${Array.from(pathnames).join(' | ')};`,
|
||||
'\t\tResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes[\'Pathname\']>}`;',
|
||||
`\t\tAsset(): ${assets.concat('string & {}').join(' | ')};`,
|
||||
'\t}',
|
||||
'}'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes non-ambient declarations to the output directory
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
*/
|
||||
export function write_non_ambient(config, manifest_data) {
|
||||
const app_types = generate_app_types(manifest_data, config);
|
||||
const content = [template, app_types].join('\n\n');
|
||||
|
||||
write_if_changed(path.join(config.outDir, 'non-ambient.d.ts'), content);
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import { dedent, isSvelte5Plus, write_if_changed } from './utils.js';
|
||||
|
||||
/**
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} output
|
||||
*/
|
||||
export function write_root(manifest_data, config, output) {
|
||||
// TODO remove default layout altogether
|
||||
|
||||
const use_boundaries = config.kit.experimental.handleRenderingErrors && isSvelte5Plus();
|
||||
|
||||
const max_depth = Math.max(
|
||||
...manifest_data.routes.map((route) =>
|
||||
route.page ? route.page.layouts.filter(Boolean).length + 1 : 0
|
||||
),
|
||||
1
|
||||
);
|
||||
|
||||
const levels = [];
|
||||
for (let i = 0; i <= max_depth; i += 1) {
|
||||
levels.push(i);
|
||||
}
|
||||
|
||||
let l = max_depth;
|
||||
/** @type {string} */
|
||||
let pyramid;
|
||||
|
||||
if (isSvelte5Plus() && use_boundaries) {
|
||||
// with the @const we force the data[depth] access to be derived, which is important to not fire updates needlessly
|
||||
// TODO in Svelte 5 we should rethink the client.js side, we can likely make data a $state and only update indexes that changed there, simplifying this a lot
|
||||
pyramid = dedent`
|
||||
{#snippet pyramid(depth)}
|
||||
{@const Pyramid = constructors[depth]}
|
||||
{#snippet failed(error)}
|
||||
{@const ErrorPage = errors[depth]}
|
||||
<ErrorPage {error} />
|
||||
{/snippet}
|
||||
<svelte:boundary failed={errors[depth] ? failed : undefined}>
|
||||
{#if constructors[depth + 1]}
|
||||
{@const d = data[depth]}
|
||||
<!-- svelte-ignore binding_property_non_reactive -->
|
||||
<Pyramid bind:this={components[depth]} data={d} {form} params={page.params}>
|
||||
{@render pyramid(depth + 1)}
|
||||
</Pyramid>
|
||||
{:else}
|
||||
{@const d = data[depth]}
|
||||
<!-- svelte-ignore binding_property_non_reactive -->
|
||||
<Pyramid bind:this={components[depth]} data={d} {form} params={page.params} {error} />
|
||||
{/if}
|
||||
</svelte:boundary>
|
||||
{/snippet}
|
||||
|
||||
{@render pyramid(0)}
|
||||
`;
|
||||
} else {
|
||||
pyramid = dedent`
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? `<!-- svelte-ignore binding_property_non_reactive -->
|
||||
<Pyramid_${l} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />`
|
||||
: `<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />`
|
||||
}`;
|
||||
|
||||
while (l--) {
|
||||
pyramid = dedent`
|
||||
{#if constructors[${l + 1}]}
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`{@const Pyramid_${l} = constructors[${l}]}
|
||||
<!-- svelte-ignore binding_property_non_reactive -->
|
||||
<Pyramid_${l} bind:this={components[${l}]} data={data_${l}} {form} params={page.params}>
|
||||
${pyramid}
|
||||
</Pyramid_${l}>`
|
||||
: dedent`<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} params={page.params}>
|
||||
${pyramid}
|
||||
</svelte:component>`
|
||||
}
|
||||
|
||||
{:else}
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
{@const Pyramid_${l} = constructors[${l}]}
|
||||
<!-- svelte-ignore binding_property_non_reactive -->
|
||||
<Pyramid_${l} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />
|
||||
`
|
||||
: dedent`<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />`
|
||||
}
|
||||
|
||||
{/if}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
write_if_changed(
|
||||
`${output}/root.svelte`,
|
||||
dedent`
|
||||
<!-- This file is generated by @sveltejs/kit — do not edit it! -->
|
||||
${isSvelte5Plus() ? '<svelte:options runes={true} />' : ''}
|
||||
<script>
|
||||
import { setContext, ${isSvelte5Plus() ? '' : 'afterUpdate, '}onMount, tick } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// stores
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
let { stores, page, constructors, components = [], form, ${use_boundaries ? 'errors = [], error, ' : ''}${levels
|
||||
.map((l) => `data_${l} = null`)
|
||||
.join(', ')} } = $props();
|
||||
${use_boundaries ? `let data = $derived({${levels.map((l) => `'${l}': data_${l}`).join(', ')}})` : ''}
|
||||
`
|
||||
: dedent`
|
||||
export let stores;
|
||||
export let page;
|
||||
|
||||
export let constructors;
|
||||
export let components = [];
|
||||
export let form;
|
||||
${levels.map((l) => `export let data_${l} = null;`).join('\n')}
|
||||
`
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
// svelte-ignore state_referenced_locally
|
||||
setContext('__svelte__', stores);
|
||||
}
|
||||
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
if (browser) {
|
||||
$effect.pre(() => stores.page.set(page));
|
||||
} else {
|
||||
// svelte-ignore state_referenced_locally
|
||||
stores.page.set(page);
|
||||
}
|
||||
`
|
||||
: '$: stores.page.set(page);'
|
||||
}
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
$effect(() => {
|
||||
stores;page;constructors;components;form;${use_boundaries ? 'errors;error;' : ''}${levels.map((l) => `data_${l}`).join(';')};
|
||||
stores.page.notify();
|
||||
});
|
||||
`
|
||||
: 'afterUpdate(stores.page.notify);'
|
||||
}
|
||||
|
||||
let mounted = ${isSvelte5Plus() ? '$state(false)' : 'false'};
|
||||
let navigated = ${isSvelte5Plus() ? '$state(false)' : 'false'};
|
||||
let title = ${isSvelte5Plus() ? '$state(null)' : 'null'};
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = stores.page.subscribe(() => {
|
||||
if (mounted) {
|
||||
navigated = true;
|
||||
tick().then(() => {
|
||||
title = document.title || 'untitled page';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mounted = true;
|
||||
return unsubscribe;
|
||||
});
|
||||
|
||||
${isSvelte5Plus() ? `const Pyramid_${max_depth}=$derived(constructors[${max_depth}])` : ''}
|
||||
</script>
|
||||
|
||||
${pyramid}
|
||||
|
||||
{#if mounted}
|
||||
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
|
||||
{#if navigated}
|
||||
{title}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
`
|
||||
);
|
||||
|
||||
if (isSvelte5Plus()) {
|
||||
write_if_changed(
|
||||
`${output}/root.js`,
|
||||
dedent`
|
||||
import { asClassComponent } from 'svelte/legacy';
|
||||
import Root from './root.svelte';
|
||||
export default asClassComponent(Root);
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { hash } from '../../utils/hash.js';
|
||||
import { posixify, resolve_entry } from '../../utils/filesystem.js';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { load_error_page, load_template } from '../config/index.js';
|
||||
import { runtime_directory } from '../utils.js';
|
||||
import { isSvelte5Plus, write_if_changed } from './utils.js';
|
||||
import colors from 'kleur';
|
||||
import { escape_html } from '../../utils/escape.js';
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* server_hooks: string | null;
|
||||
* universal_hooks: string | null;
|
||||
* config: import('types').ValidatedConfig;
|
||||
* has_service_worker: boolean;
|
||||
* runtime_directory: string;
|
||||
* template: string;
|
||||
* error_page: string;
|
||||
* }} opts
|
||||
*/
|
||||
const server_template = ({
|
||||
config,
|
||||
server_hooks,
|
||||
universal_hooks,
|
||||
has_service_worker,
|
||||
runtime_directory,
|
||||
template,
|
||||
error_page
|
||||
}) => `
|
||||
import root from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
|
||||
import { set_building, set_prerendering } from '__sveltekit/environment';
|
||||
import { set_assets } from '$app/paths/internal/server';
|
||||
import { set_manifest, set_read_implementation } from '__sveltekit/server';
|
||||
import { set_private_env, set_public_env } from '${runtime_directory}/shared-server.js';
|
||||
|
||||
export const options = {
|
||||
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
|
||||
async: ${s(!!config.compilerOptions?.experimental?.async)},
|
||||
csp: ${s(config.kit.csp)},
|
||||
csrf_check_origin: ${s(config.kit.csrf.checkOrigin && !config.kit.csrf.trustedOrigins.includes('*'))},
|
||||
csrf_trusted_origins: ${s(config.kit.csrf.trustedOrigins)},
|
||||
embedded: ${config.kit.embedded},
|
||||
env_public_prefix: '${config.kit.env.publicPrefix}',
|
||||
env_private_prefix: '${config.kit.env.privatePrefix}',
|
||||
hash_routing: ${s(config.kit.router.type === 'hash')},
|
||||
hooks: null, // added lazily, via \`get_hooks\`
|
||||
preload_strategy: ${s(config.kit.output.preloadStrategy)},
|
||||
root,
|
||||
service_worker: ${has_service_worker},
|
||||
service_worker_options: ${config.kit.serviceWorker.register ? s(config.kit.serviceWorker.options) : 'null'},
|
||||
server_error_boundaries: ${s(!!config.kit.experimental.handleRenderingErrors)},
|
||||
templates: {
|
||||
app: ({ head, body, assets, nonce, env }) => ${s(template)
|
||||
.replace('%sveltekit.head%', '" + head + "')
|
||||
.replace('%sveltekit.body%', '" + body + "')
|
||||
.replace(/%sveltekit\.assets%/g, '" + assets + "')
|
||||
.replace(/%sveltekit\.nonce%/g, '" + nonce + "')
|
||||
.replace(/%sveltekit\.version%/g, escape_html(config.kit.version.name))
|
||||
.replace(
|
||||
/%sveltekit\.env\.([^%]+)%/g,
|
||||
(_match, capture) => `" + (env[${s(capture)}] ?? "") + "`
|
||||
)},
|
||||
error: ({ status, message }) => ${s(error_page)
|
||||
.replace(/%sveltekit\.status%/g, '" + status + "')
|
||||
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
|
||||
},
|
||||
version_hash: ${s(hash(config.kit.version.name))}
|
||||
};
|
||||
|
||||
export async function get_hooks() {
|
||||
let handle;
|
||||
let handleFetch;
|
||||
let handleError;
|
||||
let handleValidationError;
|
||||
let init;
|
||||
${server_hooks ? `({ handle, handleFetch, handleError, handleValidationError, init } = await import(${s(server_hooks)}));` : ''}
|
||||
|
||||
let reroute;
|
||||
let transport;
|
||||
${universal_hooks ? `({ reroute, transport } = await import(${s(universal_hooks)}));` : ''}
|
||||
|
||||
return {
|
||||
handle,
|
||||
handleFetch,
|
||||
handleError,
|
||||
handleValidationError,
|
||||
init,
|
||||
reroute,
|
||||
transport
|
||||
};
|
||||
}
|
||||
|
||||
export { set_assets, set_building, set_manifest, set_prerendering, set_private_env, set_public_env, set_read_implementation };
|
||||
`;
|
||||
|
||||
// TODO need to re-run this whenever src/app.html or src/error.html are
|
||||
// created or changed, or src/service-worker.js is created or deleted.
|
||||
// Also, need to check that updating hooks.server.js works
|
||||
|
||||
/**
|
||||
* Write server configuration to disk
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} output
|
||||
*/
|
||||
export function write_server(config, output) {
|
||||
const server_hooks_file = resolve_entry(config.kit.files.hooks.server);
|
||||
const universal_hooks_file = resolve_entry(config.kit.files.hooks.universal);
|
||||
|
||||
const typo = resolve_entry('src/+hooks.server');
|
||||
if (typo) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` +
|
||||
` at ${path.resolve(typo)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {string} file */
|
||||
function relative(file) {
|
||||
return posixify(path.relative(`${output}/server`, file));
|
||||
}
|
||||
|
||||
// Contains the stringified version of
|
||||
/** @type {import('types').SSROptions} */
|
||||
write_if_changed(
|
||||
`${output}/server/internal.js`,
|
||||
server_template({
|
||||
config,
|
||||
server_hooks: server_hooks_file ? relative(server_hooks_file) : null,
|
||||
universal_hooks: universal_hooks_file ? relative(universal_hooks_file) : null,
|
||||
has_service_worker:
|
||||
config.kit.serviceWorker.register && !!resolve_entry(config.kit.files.serviceWorker),
|
||||
runtime_directory: relative(runtime_directory),
|
||||
template: load_template(process.cwd(), config),
|
||||
error_page: load_error_page(config)
|
||||
})
|
||||
);
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import colors from 'kleur';
|
||||
import { posixify } from '../../utils/filesystem.js';
|
||||
import { write_if_changed } from './utils.js';
|
||||
|
||||
/**
|
||||
* @param {string} cwd
|
||||
* @param {string} file
|
||||
*/
|
||||
function maybe_file(cwd, file) {
|
||||
const resolved = path.resolve(cwd, file);
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
function project_relative(file) {
|
||||
return posixify(path.relative('.', file));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
function remove_trailing_slashstar(file) {
|
||||
if (file.endsWith('/*')) {
|
||||
return file.slice(0, -2);
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the tsconfig that the user's tsconfig inherits from.
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
*/
|
||||
export function write_tsconfig(kit, cwd = process.cwd()) {
|
||||
const out = path.join(kit.outDir, 'tsconfig.json');
|
||||
|
||||
const user_config = load_user_tsconfig(cwd);
|
||||
if (user_config) validate_user_config(cwd, out, user_config);
|
||||
|
||||
write_if_changed(out, JSON.stringify(get_tsconfig(kit), null, '\t'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the tsconfig that the user's tsconfig inherits from.
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
*/
|
||||
export function get_tsconfig(kit) {
|
||||
/** @param {string} file */
|
||||
const config_relative = (file) => posixify(path.relative(kit.outDir, file));
|
||||
|
||||
const include = new Set([
|
||||
'ambient.d.ts', // careful: changing this name would be a breaking change, because it's referenced in the service-workers documentation
|
||||
'non-ambient.d.ts',
|
||||
'./types/**/$types.d.ts',
|
||||
config_relative('vite.config.js'),
|
||||
config_relative('vite.config.ts')
|
||||
]);
|
||||
const src_includes = [kit.files.routes, kit.files.lib, kit.files.src].filter((dir) => {
|
||||
const relative = path.relative(kit.files.src, dir);
|
||||
return !relative || relative.startsWith('..');
|
||||
});
|
||||
for (const dir of src_includes) {
|
||||
include.add(config_relative(`${dir}/**/*.js`));
|
||||
include.add(config_relative(`${dir}/**/*.ts`));
|
||||
include.add(config_relative(`${dir}/**/*.svelte`));
|
||||
}
|
||||
|
||||
// Test folder is a special case - we advocate putting tests in a top-level test folder
|
||||
// and it's not configurable (should we make it?)
|
||||
const test_folder = project_relative('test');
|
||||
include.add(config_relative(`${test_folder}/**/*.js`));
|
||||
include.add(config_relative(`${test_folder}/**/*.ts`));
|
||||
include.add(config_relative(`${test_folder}/**/*.svelte`));
|
||||
const tests_folder = project_relative('tests');
|
||||
include.add(config_relative(`${tests_folder}/**/*.js`));
|
||||
include.add(config_relative(`${tests_folder}/**/*.ts`));
|
||||
include.add(config_relative(`${tests_folder}/**/*.svelte`));
|
||||
|
||||
const exclude = [config_relative('node_modules/**')];
|
||||
// Add service worker to exclude list so that worker types references in it don't spill over into the rest of the app
|
||||
// (i.e. suddenly ServiceWorkerGlobalScope would be available throughout the app, and some types might even clash)
|
||||
if (path.extname(kit.files.serviceWorker)) {
|
||||
exclude.push(config_relative(kit.files.serviceWorker));
|
||||
} else {
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}.js`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}/**/*.js`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}.ts`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}/**/*.ts`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}.d.ts`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}/**/*.d.ts`));
|
||||
}
|
||||
|
||||
const config = {
|
||||
compilerOptions: {
|
||||
// generated options
|
||||
paths: {
|
||||
...get_tsconfig_paths(kit),
|
||||
'$app/types': ['./types/index.d.ts']
|
||||
},
|
||||
rootDirs: [config_relative('.'), './types'],
|
||||
|
||||
// essential options
|
||||
// svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
||||
// to enforce using \`import type\` instead of \`import\` for Types.
|
||||
// Also, TypeScript doesn't know about import usages in the template because it only sees the
|
||||
// script of a Svelte file. Therefore preserve all value imports.
|
||||
verbatimModuleSyntax: true,
|
||||
// Vite compiles modules one at a time
|
||||
isolatedModules: true,
|
||||
|
||||
// This is required for svelte-package to work as expected
|
||||
// Can be overwritten
|
||||
lib: ['esnext', 'DOM', 'DOM.Iterable'],
|
||||
moduleResolution: 'bundler',
|
||||
module: 'esnext',
|
||||
noEmit: true, // prevent tsconfig error "overwriting input files" - Vite handles the build and ignores this
|
||||
target: 'esnext'
|
||||
},
|
||||
include: [...include],
|
||||
exclude
|
||||
};
|
||||
|
||||
return kit.typescript.config(config) ?? config;
|
||||
}
|
||||
|
||||
/** @param {string} cwd */
|
||||
function load_user_tsconfig(cwd) {
|
||||
const file = maybe_file(cwd, 'tsconfig.json') || maybe_file(cwd, 'jsconfig.json');
|
||||
|
||||
if (!file) return;
|
||||
|
||||
// we have to eval the file, since it's not parseable as JSON (contains comments)
|
||||
const json = fs.readFileSync(file, 'utf-8');
|
||||
|
||||
return {
|
||||
kind: path.basename(file),
|
||||
options: (0, eval)(`(${json})`)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cwd
|
||||
* @param {string} out
|
||||
* @param {{ kind: string, options: any }} config
|
||||
*/
|
||||
function validate_user_config(cwd, out, config) {
|
||||
// we need to check that the user's tsconfig extends the framework config
|
||||
const extend = config.options.extends;
|
||||
const extends_framework_config =
|
||||
typeof extend === 'string'
|
||||
? path.resolve(cwd, extend) === out
|
||||
: Array.isArray(extend)
|
||||
? extend.some((e) => path.resolve(cwd, e) === out)
|
||||
: false;
|
||||
|
||||
const options = config.options.compilerOptions || {};
|
||||
|
||||
if (extends_framework_config) {
|
||||
const { paths, baseUrl } = options;
|
||||
|
||||
if (baseUrl || paths) {
|
||||
console.warn(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`You have specified a baseUrl and/or paths in your ${config.kind} which interferes with SvelteKit's auto-generated tsconfig.json. ` +
|
||||
'Remove it to avoid problems with intellisense. For path aliases, use `kit.alias` instead: https://svelte.dev/docs/kit/configuration#alias'
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let relative = posixify(path.relative('.', out));
|
||||
if (!relative.startsWith('./')) relative = './' + relative;
|
||||
|
||||
console.warn(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(`Your ${config.kind} should extend the configuration generated by SvelteKit:`)
|
||||
);
|
||||
console.warn(`{\n "extends": "${relative}"\n}`);
|
||||
}
|
||||
}
|
||||
|
||||
// <something><optional /*>
|
||||
const alias_regex = /^(.+?)(\/\*)?$/;
|
||||
// <path><optional /* or .fileending>
|
||||
const value_regex = /^(.*?)((\/\*)|(\.\w+))?$/;
|
||||
|
||||
/**
|
||||
* Generates tsconfig path aliases from kit's aliases.
|
||||
* Related to vite alias creation.
|
||||
*
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
*/
|
||||
function get_tsconfig_paths(config) {
|
||||
/** @param {string} file */
|
||||
const config_relative = (file) => {
|
||||
let relative_path = path.relative(config.outDir, file);
|
||||
if (!relative_path.startsWith('..')) {
|
||||
relative_path = './' + relative_path;
|
||||
}
|
||||
return posixify(relative_path);
|
||||
};
|
||||
|
||||
const alias = { ...config.alias };
|
||||
if (fs.existsSync(project_relative(config.files.lib))) {
|
||||
alias['$lib'] = project_relative(config.files.lib);
|
||||
}
|
||||
|
||||
/** @type {Record<string, string[]>} */
|
||||
const paths = {};
|
||||
|
||||
for (const [key, value] of Object.entries(alias)) {
|
||||
const key_match = alias_regex.exec(key);
|
||||
if (!key_match) throw new Error(`Invalid alias key: ${key}`);
|
||||
|
||||
const value_match = value_regex.exec(value);
|
||||
if (!value_match) throw new Error(`Invalid alias value: ${value}`);
|
||||
|
||||
const rel_path = config_relative(remove_trailing_slashstar(value));
|
||||
const slashstar = key_match[2];
|
||||
|
||||
if (slashstar) {
|
||||
paths[key] = [rel_path + '/*'];
|
||||
} else {
|
||||
paths[key] = [rel_path];
|
||||
const fileending = value_match[4];
|
||||
|
||||
if (!fileending && !(key + '/*' in alias)) {
|
||||
paths[key + '/*'] = [rel_path + '/*'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
+869
@@ -0,0 +1,869 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import MagicString from 'magic-string';
|
||||
import { posixify, rimraf, walk } from '../../../utils/filesystem.js';
|
||||
import { compact } from '../../../utils/array.js';
|
||||
import { ts } from '../ts.js';
|
||||
const remove_relative_parent_traversals = (/** @type {string} */ path) =>
|
||||
path.replace(/\.\.\//g, '');
|
||||
const is_whitespace = (/** @type {string} */ char) => /\s/.test(char);
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* file_name: string;
|
||||
* modified: boolean;
|
||||
* code: string;
|
||||
* exports: any[];
|
||||
* } | null} Proxy
|
||||
*
|
||||
* @typedef {{
|
||||
* server: Proxy,
|
||||
* universal: Proxy
|
||||
* }} Proxies
|
||||
*
|
||||
* @typedef {Map<import('types').PageNode, {route: import('types').RouteData, proxies: Proxies}>} RoutesMap
|
||||
*/
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
/**
|
||||
* Creates types for the whole manifest
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
*/
|
||||
export function write_all_types(config, manifest_data) {
|
||||
if (!ts) return;
|
||||
|
||||
const types_dir = `${config.kit.outDir}/types`;
|
||||
|
||||
// empty out files that no longer need to exist
|
||||
const routes_dir = remove_relative_parent_traversals(
|
||||
posixify(path.relative('.', config.kit.files.routes))
|
||||
);
|
||||
const expected_directories = new Set(
|
||||
manifest_data.routes.map((route) => path.join(routes_dir, route.id))
|
||||
);
|
||||
|
||||
if (fs.existsSync(types_dir)) {
|
||||
for (const file of walk(types_dir)) {
|
||||
const dir = path.dirname(file);
|
||||
if (!expected_directories.has(dir)) {
|
||||
rimraf(path.join(types_dir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read/write meta data on each invocation, not once per node process,
|
||||
// it could be invoked by another process in the meantime.
|
||||
const meta_data_file = `${types_dir}/route_meta_data.json`;
|
||||
const has_meta_data = fs.existsSync(meta_data_file);
|
||||
const meta_data = has_meta_data
|
||||
? /** @type {Record<string, string[]>} */ (JSON.parse(fs.readFileSync(meta_data_file, 'utf-8')))
|
||||
: {};
|
||||
const routes_map = create_routes_map(manifest_data);
|
||||
// For each directory, write $types.d.ts
|
||||
for (const route of manifest_data.routes) {
|
||||
if (!route.leaf && !route.layout && !route.endpoint) continue; // nothing to do
|
||||
|
||||
const outdir = path.join(config.kit.outDir, 'types', routes_dir, route.id);
|
||||
|
||||
// check if the types are out of date
|
||||
/** @type {string[]} */
|
||||
const input_files = [];
|
||||
|
||||
/** @type {import('types').PageNode | null} */
|
||||
let node = route.leaf;
|
||||
while (node) {
|
||||
if (node.universal) input_files.push(node.universal);
|
||||
if (node.server) input_files.push(node.server);
|
||||
node = node.parent ?? null;
|
||||
}
|
||||
|
||||
/** @type {import('types').PageNode | null} */
|
||||
node = route.layout;
|
||||
while (node) {
|
||||
if (node.universal) input_files.push(node.universal);
|
||||
if (node.server) input_files.push(node.server);
|
||||
node = node.parent ?? null;
|
||||
}
|
||||
|
||||
if (route.endpoint) {
|
||||
input_files.push(route.endpoint.file);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(outdir, { recursive: true });
|
||||
} catch {}
|
||||
|
||||
const output_files = compact(
|
||||
fs.readdirSync(outdir).map((name) => {
|
||||
const stats = fs.statSync(path.join(outdir, name));
|
||||
if (stats.isDirectory()) return;
|
||||
return {
|
||||
name,
|
||||
updated: stats.mtimeMs
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const source_last_updated = Math.max(
|
||||
// ctimeMs includes move operations whereas mtimeMs does not
|
||||
...input_files.map((file) => fs.statSync(file).ctimeMs)
|
||||
);
|
||||
const types_last_updated = Math.max(...output_files.map((file) => file.updated));
|
||||
|
||||
const should_generate =
|
||||
// source files were generated more recently than the types
|
||||
source_last_updated > types_last_updated ||
|
||||
// no meta data file exists yet
|
||||
!has_meta_data ||
|
||||
// some file was deleted
|
||||
!meta_data[route.id]?.every((file) => input_files.includes(file));
|
||||
|
||||
if (should_generate) {
|
||||
// track which old files end up being surplus to requirements
|
||||
const to_delete = new Set(output_files.map((file) => file.name));
|
||||
update_types(config, routes_map, route, to_delete);
|
||||
meta_data[route.id] = input_files;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(meta_data_file, JSON.stringify(meta_data, null, '\t'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates types related to the given file. This should only be called
|
||||
* if the file in question was edited, not if it was created/deleted/moved.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} file
|
||||
*/
|
||||
export function write_types(config, manifest_data, file) {
|
||||
if (!ts) return;
|
||||
|
||||
if (!path.basename(file).startsWith('+')) {
|
||||
// Not a route file
|
||||
return;
|
||||
}
|
||||
|
||||
const id = '/' + posixify(path.relative(config.kit.files.routes, path.dirname(file)));
|
||||
|
||||
const route = manifest_data.routes.find((route) => route.id === id);
|
||||
if (!route) return;
|
||||
if (!route.leaf && !route.layout && !route.endpoint) return; // nothing to do
|
||||
|
||||
update_types(config, create_routes_map(manifest_data), route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all leafs into a leaf -> route map
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
*/
|
||||
function create_routes_map(manifest_data) {
|
||||
/** @type {RoutesMap} */
|
||||
const map = new Map();
|
||||
for (const route of manifest_data.routes) {
|
||||
if (route.leaf) {
|
||||
map.set(route.leaf, { route, proxies: { server: null, universal: null } });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update types for a specific route
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {RoutesMap} routes
|
||||
* @param {import('types').RouteData} route
|
||||
* @param {Set<string>} [to_delete]
|
||||
*/
|
||||
function update_types(config, routes, route, to_delete = new Set()) {
|
||||
const routes_dir = remove_relative_parent_traversals(
|
||||
posixify(path.relative('.', config.kit.files.routes))
|
||||
);
|
||||
const outdir = path.join(config.kit.outDir, 'types', routes_dir, route.id);
|
||||
|
||||
// now generate new types
|
||||
const imports = ["import type * as Kit from '@sveltejs/kit';"];
|
||||
|
||||
/** @type {string[]} */
|
||||
const declarations = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const exports = [];
|
||||
|
||||
// add 'Expand' helper
|
||||
// Makes sure a type is "repackaged" and therefore more readable
|
||||
declarations.push('type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;');
|
||||
|
||||
// returns the predicate of a matcher's type guard - or string if there is no type guard
|
||||
declarations.push(
|
||||
'type MatcherParam<M> = M extends (param : string) => param is (infer U extends string) ? U : string;'
|
||||
);
|
||||
|
||||
declarations.push(
|
||||
'type RouteParams = ' + generate_params_type(route.params, outdir, config) + ';'
|
||||
);
|
||||
|
||||
if (route.params.length > 0) {
|
||||
exports.push(
|
||||
'export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;'
|
||||
);
|
||||
}
|
||||
|
||||
declarations.push(`type RouteId = '${route.id}';`);
|
||||
|
||||
// These could also be placed in our public types, but it would bloat them unnecessarily and we may want to change these in the future
|
||||
if (route.layout || route.leaf) {
|
||||
declarations.push(
|
||||
// If T extends the empty object, void is also allowed as a return type
|
||||
'type MaybeWithVoid<T> = {} extends T ? T | void : T;',
|
||||
|
||||
// Returns the key of the object whose values are required.
|
||||
'export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];',
|
||||
|
||||
// Helper type to get the correct output type for load functions. It should be passed the parent type to check what types from App.PageData are still required.
|
||||
// If none, void is also allowed as a return type.
|
||||
'type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>',
|
||||
|
||||
// null & {} == null, we need to prevent that in some situations
|
||||
'type EnsureDefined<T> = T extends null | undefined ? {} : T;',
|
||||
|
||||
// Takes a union type and returns a union type where each type also has all properties
|
||||
// of all possible types (typed as undefined), making accessing them more ergonomic
|
||||
'type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;',
|
||||
|
||||
// Re-export `Snapshot` from @sveltejs/kit — in future we could use this to infer <T> from the return type of `snapshot.capture`
|
||||
'export type Snapshot<T = any> = Kit.Snapshot<T>;'
|
||||
);
|
||||
}
|
||||
|
||||
if (route.leaf) {
|
||||
let route_info = routes.get(route.leaf);
|
||||
if (!route_info) {
|
||||
// This should be defined, but belts and braces
|
||||
route_info = { route, proxies: { server: null, universal: null } };
|
||||
routes.set(route.leaf, route_info);
|
||||
}
|
||||
|
||||
const {
|
||||
declarations: d,
|
||||
exports: e,
|
||||
proxies
|
||||
} = process_node(route.leaf, outdir, true, route_info.proxies);
|
||||
|
||||
exports.push(...e);
|
||||
declarations.push(...d);
|
||||
|
||||
if (proxies.server) {
|
||||
route_info.proxies.server = proxies.server;
|
||||
if (proxies.server?.modified) to_delete.delete(proxies.server.file_name);
|
||||
}
|
||||
if (proxies.universal) {
|
||||
route_info.proxies.universal = proxies.universal;
|
||||
if (proxies.universal?.modified) to_delete.delete(proxies.universal.file_name);
|
||||
}
|
||||
|
||||
if (route.leaf.server) {
|
||||
exports.push(
|
||||
'export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>'
|
||||
);
|
||||
exports.push(
|
||||
'export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>'
|
||||
);
|
||||
}
|
||||
|
||||
if (route.leaf.server) {
|
||||
exports.push(
|
||||
'export type PageProps = { params: RouteParams; data: PageData; form: ActionData }'
|
||||
);
|
||||
} else {
|
||||
exports.push('export type PageProps = { params: RouteParams; data: PageData }');
|
||||
}
|
||||
}
|
||||
|
||||
if (route.layout) {
|
||||
let all_pages_have_load = true;
|
||||
/** @type {import('types').RouteParam[]} */
|
||||
const layout_params = [];
|
||||
const ids = ['RouteId'];
|
||||
|
||||
route.layout.child_pages?.forEach((page) => {
|
||||
const leaf = routes.get(page);
|
||||
if (leaf) {
|
||||
if (leaf.route.page) ids.push(`"${leaf.route.id}"`);
|
||||
|
||||
for (const param of leaf.route.params) {
|
||||
// skip if already added
|
||||
if (layout_params.some((p) => p.name === param.name)) continue;
|
||||
layout_params.push({ ...param, optional: true });
|
||||
}
|
||||
|
||||
ensureProxies(page, leaf.proxies);
|
||||
|
||||
if (
|
||||
// Be defensive - if a proxy doesn't exist (because it couldn't be created), assume a load function exists.
|
||||
// If we didn't and it's a false negative, the user could wrongfully get a type error on layouts.
|
||||
(leaf.proxies.server && !leaf.proxies.server.exports.includes('load')) ||
|
||||
(leaf.proxies.universal && !leaf.proxies.universal.exports.includes('load'))
|
||||
) {
|
||||
all_pages_have_load = false;
|
||||
}
|
||||
}
|
||||
if (!page.server && !page.universal) {
|
||||
all_pages_have_load = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (route.id === '/') {
|
||||
// root layout is used for fallback error page, where ID can be null
|
||||
ids.push('null');
|
||||
}
|
||||
|
||||
declarations.push(`type LayoutRouteId = ${ids.join(' | ')}`);
|
||||
|
||||
declarations.push(
|
||||
'type LayoutParams = RouteParams & ' + generate_params_type(layout_params, outdir, config)
|
||||
);
|
||||
|
||||
const {
|
||||
exports: e,
|
||||
declarations: d,
|
||||
proxies
|
||||
} = process_node(
|
||||
route.layout,
|
||||
outdir,
|
||||
false,
|
||||
{ server: null, universal: null },
|
||||
all_pages_have_load
|
||||
);
|
||||
|
||||
exports.push(...e);
|
||||
declarations.push(...d);
|
||||
|
||||
if (proxies.server?.modified) to_delete.delete(proxies.server.file_name);
|
||||
if (proxies.universal?.modified) to_delete.delete(proxies.universal.file_name);
|
||||
|
||||
exports.push(
|
||||
'export type LayoutProps = { params: LayoutParams; data: LayoutData; children: import("svelte").Snippet }'
|
||||
);
|
||||
}
|
||||
|
||||
if (route.endpoint) {
|
||||
exports.push('export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;');
|
||||
}
|
||||
|
||||
if (route.leaf?.server || route.layout?.server || route.endpoint) {
|
||||
exports.push('export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;');
|
||||
}
|
||||
|
||||
const output = [imports.join('\n'), declarations.join('\n'), exports.join('\n')]
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
fs.writeFileSync(`${outdir}/$types.d.ts`, output);
|
||||
to_delete.delete('$types.d.ts');
|
||||
|
||||
for (const file of to_delete) {
|
||||
fs.unlinkSync(path.join(outdir, file));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').PageNode} node
|
||||
* @param {string} outdir
|
||||
* @param {boolean} is_page
|
||||
* @param {Proxies} proxies
|
||||
* @param {boolean} [all_pages_have_load]
|
||||
*/
|
||||
function process_node(node, outdir, is_page, proxies, all_pages_have_load = true) {
|
||||
const params = `${is_page ? 'Route' : 'Layout'}Params`;
|
||||
const prefix = is_page ? 'Page' : 'Layout';
|
||||
|
||||
const route_id = is_page ? 'RouteId' : 'LayoutRouteId';
|
||||
|
||||
/** @type {string[]} */
|
||||
const declarations = [];
|
||||
/** @type {string[]} */
|
||||
const exports = [];
|
||||
|
||||
/** @type {string} */
|
||||
let server_data;
|
||||
/** @type {string} */
|
||||
let data;
|
||||
|
||||
ensureProxies(node, proxies);
|
||||
|
||||
if (node.server) {
|
||||
const basename = path.basename(node.server);
|
||||
const proxy = proxies.server;
|
||||
if (proxy?.modified) {
|
||||
fs.writeFileSync(`${outdir}/proxy${basename}`, proxy.code);
|
||||
}
|
||||
|
||||
server_data = get_data_type(node.server, 'null', proxy, true);
|
||||
|
||||
const parent_type = `${prefix}ServerParentData`;
|
||||
|
||||
declarations.push(`type ${parent_type} = ${get_parent_type(node, 'LayoutServerData')};`);
|
||||
|
||||
// +page.js load present -> server can return all-optional data
|
||||
const output_data_shape =
|
||||
node.universal || (!is_page && all_pages_have_load)
|
||||
? 'Partial<App.PageData> & Record<string, any> | void'
|
||||
: `OutputDataShape<${parent_type}>`;
|
||||
exports.push(
|
||||
`export type ${prefix}ServerLoad<OutputData extends ${output_data_shape} = ${output_data_shape}> = Kit.ServerLoad<${params}, ${parent_type}, OutputData, ${route_id}>;`
|
||||
);
|
||||
|
||||
exports.push(`export type ${prefix}ServerLoadEvent = Parameters<${prefix}ServerLoad>[0];`);
|
||||
|
||||
if (is_page) {
|
||||
let type = 'unknown';
|
||||
if (proxy && proxy.exports.includes('actions')) {
|
||||
// If the file wasn't tweaked, we can use the return type of the original file.
|
||||
// The advantage is that type updates are reflected without saving.
|
||||
const from = proxy.modified
|
||||
? `./proxy${replace_ext_with_js(basename)}`
|
||||
: path_to_original(outdir, node.server);
|
||||
|
||||
exports.push(
|
||||
'type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;',
|
||||
'type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];',
|
||||
'type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;',
|
||||
'type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];',
|
||||
`type ActionsExport = typeof import('${from}').actions`,
|
||||
'export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>'
|
||||
);
|
||||
|
||||
type = 'Expand<Kit.AwaitedActions<ActionsExport>> | null';
|
||||
}
|
||||
exports.push(`export type ActionData = ${type};`);
|
||||
}
|
||||
} else {
|
||||
server_data = 'null';
|
||||
}
|
||||
exports.push(`export type ${prefix}ServerData = ${server_data};`);
|
||||
|
||||
const parent_type = `${prefix}ParentData`;
|
||||
declarations.push(`type ${parent_type} = ${get_parent_type(node, 'LayoutData')};`);
|
||||
|
||||
if (node.universal) {
|
||||
const proxy = proxies.universal;
|
||||
if (proxy?.modified) {
|
||||
fs.writeFileSync(`${outdir}/proxy${path.basename(node.universal)}`, proxy.code);
|
||||
}
|
||||
|
||||
const type = get_data_type(
|
||||
node.universal,
|
||||
`${parent_type} & EnsureDefined<${prefix}ServerData>`,
|
||||
proxy
|
||||
);
|
||||
|
||||
data = `Expand<Omit<${parent_type}, keyof ${type}> & OptionalUnion<EnsureDefined<${type}>>>`;
|
||||
|
||||
const output_data_shape =
|
||||
!is_page && all_pages_have_load
|
||||
? 'Partial<App.PageData> & Record<string, any> | void'
|
||||
: `OutputDataShape<${parent_type}>`;
|
||||
exports.push(
|
||||
`export type ${prefix}Load<OutputData extends ${output_data_shape} = ${output_data_shape}> = Kit.Load<${params}, ${prefix}ServerData, ${parent_type}, OutputData, ${route_id}>;`
|
||||
);
|
||||
|
||||
exports.push(`export type ${prefix}LoadEvent = Parameters<${prefix}Load>[0];`);
|
||||
} else if (server_data === 'null') {
|
||||
data = `Expand<${parent_type}>`;
|
||||
} else {
|
||||
data = `Expand<Omit<${parent_type}, keyof ${prefix}ServerData> & EnsureDefined<${prefix}ServerData>>`;
|
||||
}
|
||||
|
||||
exports.push(`export type ${prefix}Data = ${data};`);
|
||||
|
||||
return { declarations, exports, proxies };
|
||||
|
||||
/**
|
||||
* @param {string} file_path
|
||||
* @param {string} fallback
|
||||
* @param {Proxy} proxy
|
||||
* @param {boolean} expand
|
||||
*/
|
||||
function get_data_type(file_path, fallback, proxy, expand = false) {
|
||||
if (proxy) {
|
||||
if (proxy.exports.includes('load')) {
|
||||
// If the file wasn't tweaked, we can use the return type of the original file.
|
||||
// The advantage is that type updates are reflected without saving.
|
||||
const from = proxy.modified
|
||||
? `./proxy${replace_ext_with_js(path.basename(file_path))}`
|
||||
: path_to_original(outdir, file_path);
|
||||
const type = `Kit.LoadProperties<Awaited<ReturnType<typeof import('${from}').load>>>`;
|
||||
return expand ? `Expand<OptionalUnion<EnsureDefined<${type}>>>` : type;
|
||||
} else {
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function populates the proxies object, if necessary and not already done.
|
||||
* Proxies are used to tweak the code of a file before it's typechecked.
|
||||
* They are needed in two places - when generating the types for a page or layout.
|
||||
* To not do the same work twice, we generate the proxies once and pass them around.
|
||||
*
|
||||
* @param {import('types').PageNode} node
|
||||
* @param {Proxies} proxies
|
||||
*/
|
||||
function ensureProxies(node, proxies) {
|
||||
if (node.server && !proxies.server) {
|
||||
proxies.server = createProxy(node.server, true);
|
||||
}
|
||||
|
||||
if (node.universal && !proxies.universal) {
|
||||
proxies.universal = createProxy(node.universal, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file_path
|
||||
* @param {boolean} is_server
|
||||
* @returns {Proxy}
|
||||
*/
|
||||
function createProxy(file_path, is_server) {
|
||||
const proxy = tweak_types(fs.readFileSync(file_path, 'utf8'), is_server);
|
||||
if (proxy) {
|
||||
return {
|
||||
...proxy,
|
||||
file_name: `proxy${path.basename(file_path)}`
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent type string by recursively looking up the parent layout and accumulate them to one type.
|
||||
* @param {import('types').PageNode} node
|
||||
* @param {string} type
|
||||
*/
|
||||
function get_parent_type(node, type) {
|
||||
const parent_imports = [];
|
||||
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent) {
|
||||
const d = node.depth - parent.depth;
|
||||
// unshift because we need it the other way round for the import string
|
||||
parent_imports.unshift(
|
||||
`${d === 0 ? '' : `import('${'../'.repeat(d)}${'$types.js'}').`}${type}`
|
||||
);
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
let parent_str = `EnsureDefined<${parent_imports[0] || '{}'}>`;
|
||||
for (let i = 1; i < parent_imports.length; i++) {
|
||||
// Omit is necessary because a parent could have a property with the same key which would
|
||||
// cause a type conflict. At runtime the child overwrites the parent property in this case,
|
||||
// so reflect that in the type definition.
|
||||
// EnsureDefined is necessary because {something: string} & null becomes null.
|
||||
// Output types of server loads can be null but when passed in through the `parent` parameter they are the empty object instead.
|
||||
parent_str = `Omit<${parent_str}, keyof ${parent_imports[i]}> & EnsureDefined<${parent_imports[i]}>`;
|
||||
}
|
||||
return parent_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} outdir
|
||||
* @param {string} file_path
|
||||
*/
|
||||
function path_to_original(outdir, file_path) {
|
||||
return posixify(path.relative(outdir, path.join(cwd, replace_ext_with_js(file_path))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file_path
|
||||
*/
|
||||
function replace_ext_with_js(file_path) {
|
||||
// Another extension than `.js` (or nothing, but that fails with node16 moduleResolution)
|
||||
// will result in TS failing to lookup the file
|
||||
const ext = path.extname(file_path);
|
||||
return file_path.slice(0, -ext.length) + '.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').RouteParam[]} params
|
||||
* @param {string} outdir
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
function generate_params_type(params, outdir, config) {
|
||||
/** @param {string} matcher */
|
||||
const path_to_matcher = (matcher) =>
|
||||
posixify(path.relative(outdir, path.join(config.kit.files.params, matcher + '.js')));
|
||||
|
||||
return `{ ${params
|
||||
.map(
|
||||
(param) =>
|
||||
`${param.name}${param.optional ? '?' : ''}: ${
|
||||
param.matcher
|
||||
? `MatcherParam<typeof import('${path_to_matcher(param.matcher)}').match>`
|
||||
: 'string'
|
||||
}`
|
||||
)
|
||||
.join('; ')} }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {boolean} is_server
|
||||
* @returns {Omit<NonNullable<Proxy>, 'file_name'> | null}
|
||||
*/
|
||||
export function tweak_types(content, is_server) {
|
||||
const names = new Set(is_server ? ['load', 'actions'] : ['load']);
|
||||
|
||||
try {
|
||||
let modified = false;
|
||||
|
||||
const ast = ts.createSourceFile(
|
||||
'filename.ts',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
|
||||
const code = new MagicString(content);
|
||||
|
||||
const exports = new Map();
|
||||
|
||||
ast.forEachChild((node) => {
|
||||
if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
node.exportClause &&
|
||||
ts.isNamedExports(node.exportClause)
|
||||
) {
|
||||
node.exportClause.elements.forEach((element) => {
|
||||
const exported = element.name;
|
||||
if (names.has(element.name.text)) {
|
||||
const local = element.propertyName || element.name;
|
||||
exports.set(exported.text, local.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
ts.canHaveModifiers(node) &&
|
||||
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
||||
) {
|
||||
if (ts.isFunctionDeclaration(node) && node.name?.text && names.has(node.name?.text)) {
|
||||
exports.set(node.name.text, node.name.text);
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(node)) {
|
||||
node.declarationList.declarations.forEach((declaration) => {
|
||||
if (ts.isIdentifier(declaration.name) && names.has(declaration.name.text)) {
|
||||
exports.set(declaration.name.text, declaration.name.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('typescript').Node} node
|
||||
* @param {import('typescript').Node} value
|
||||
*/
|
||||
function replace_jsdoc_type_tags(node, value) {
|
||||
let _modified = false;
|
||||
// @ts-ignore
|
||||
if (node.jsDoc) {
|
||||
// @ts-ignore
|
||||
for (const comment of node.jsDoc) {
|
||||
for (const tag of comment.tags ?? []) {
|
||||
if (ts.isJSDocTypeTag(tag)) {
|
||||
const is_fn =
|
||||
ts.isFunctionDeclaration(value) ||
|
||||
ts.isFunctionExpression(value) ||
|
||||
ts.isArrowFunction(value);
|
||||
|
||||
if (is_fn && value.parameters?.length > 0) {
|
||||
const name = ts.isIdentifier(value.parameters[0].name)
|
||||
? value.parameters[0].name.text
|
||||
: 'event';
|
||||
code.overwrite(tag.tagName.pos, tag.tagName.end, 'param');
|
||||
code.prependRight(tag.typeExpression.pos + 1, 'Parameters<');
|
||||
code.appendLeft(tag.typeExpression.end - 1, '>[0]');
|
||||
code.appendLeft(tag.typeExpression.end, ` ${name}`);
|
||||
} else {
|
||||
code.overwrite(tag.pos, tag.end, '');
|
||||
}
|
||||
_modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
modified = modified || _modified;
|
||||
return _modified;
|
||||
}
|
||||
|
||||
ast.forEachChild((node) => {
|
||||
if (ts.isFunctionDeclaration(node) && node.name?.text && node.name?.text === 'load') {
|
||||
// remove JSDoc comment above `export function load ...`
|
||||
replace_jsdoc_type_tags(node, node);
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(node)) {
|
||||
// remove JSDoc comment above `export const load = ...`
|
||||
if (
|
||||
ts.isIdentifier(node.declarationList.declarations[0].name) &&
|
||||
names.has(node.declarationList.declarations[0].name.text) &&
|
||||
node.declarationList.declarations[0].initializer
|
||||
) {
|
||||
replace_jsdoc_type_tags(node, node.declarationList.declarations[0].initializer);
|
||||
}
|
||||
|
||||
for (const declaration of node.declarationList.declarations) {
|
||||
if (
|
||||
ts.isIdentifier(declaration.name) &&
|
||||
declaration.name.text === 'load' &&
|
||||
declaration.initializer
|
||||
) {
|
||||
// edge case — remove JSDoc comment above individual export
|
||||
replace_jsdoc_type_tags(declaration, declaration.initializer);
|
||||
|
||||
// remove type from `export const load: Load ...`
|
||||
if (declaration.type) {
|
||||
let a = declaration.type.pos;
|
||||
const b = declaration.type.end;
|
||||
while (is_whitespace(content[a])) a += 1;
|
||||
|
||||
const type = content.slice(a, b);
|
||||
code.remove(declaration.name.end, declaration.type.end);
|
||||
|
||||
const rhs = declaration.initializer;
|
||||
|
||||
if (
|
||||
rhs &&
|
||||
(ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) &&
|
||||
rhs.parameters.length
|
||||
) {
|
||||
const arg = rhs.parameters[0];
|
||||
const add_parens = content[arg.pos - 1] !== '(';
|
||||
|
||||
if (add_parens) code.prependRight(arg.pos, '(');
|
||||
|
||||
if (arg && !arg.type) {
|
||||
code.appendLeft(
|
||||
arg.name.end,
|
||||
`: Parameters<${type}>[0]` + (add_parens ? ')' : '')
|
||||
);
|
||||
} else {
|
||||
// prevent "type X is imported but not used" (isn't silenced by @ts-nocheck) when svelte-check runs
|
||||
code.append(`;null as any as ${type};`);
|
||||
}
|
||||
} else {
|
||||
// prevent "type X is imported but not used" (isn't silenced by @ts-nocheck) when svelte-check runs
|
||||
code.append(`;null as any as ${type};`);
|
||||
}
|
||||
|
||||
modified = true;
|
||||
}
|
||||
} else if (
|
||||
is_server &&
|
||||
ts.isIdentifier(declaration.name) &&
|
||||
declaration.name?.text === 'actions' &&
|
||||
declaration.initializer
|
||||
) {
|
||||
// remove JSDoc comment from `export const actions = ..`
|
||||
const removed = replace_jsdoc_type_tags(node, declaration.initializer);
|
||||
// ... and move type to each individual action
|
||||
if (removed) {
|
||||
const rhs = declaration.initializer;
|
||||
if (ts.isObjectLiteralExpression(rhs)) {
|
||||
for (const prop of rhs.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const rhs = prop.initializer;
|
||||
const replaced = replace_jsdoc_type_tags(prop, rhs);
|
||||
if (
|
||||
!replaced &&
|
||||
rhs &&
|
||||
(ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) &&
|
||||
rhs.parameters?.[0]
|
||||
) {
|
||||
const name = ts.isIdentifier(rhs.parameters[0].name)
|
||||
? rhs.parameters[0].name.text
|
||||
: 'event';
|
||||
code.prependRight(
|
||||
rhs.pos,
|
||||
`/** @param {import('./$types').RequestEvent} ${name} */ `
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove type from `export const actions: Actions ...`
|
||||
if (declaration.type) {
|
||||
let a = declaration.type.pos;
|
||||
const b = declaration.type.end;
|
||||
while (is_whitespace(content[a])) a += 1;
|
||||
|
||||
const type = content.slice(a, b);
|
||||
code.remove(declaration.name.end, declaration.type.end);
|
||||
code.append(`;null as any as ${type};`);
|
||||
modified = true;
|
||||
|
||||
// ... and move type to each individual action
|
||||
const rhs = declaration.initializer;
|
||||
if (ts.isObjectLiteralExpression(rhs)) {
|
||||
for (const prop of rhs.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const rhs = prop.initializer;
|
||||
|
||||
if (
|
||||
rhs &&
|
||||
(ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) &&
|
||||
rhs.parameters.length
|
||||
) {
|
||||
const arg = rhs.parameters[0];
|
||||
const add_parens = content[arg.pos - 1] !== '(';
|
||||
|
||||
if (add_parens) code.prependRight(arg.pos, '(');
|
||||
|
||||
if (arg && !arg.type) {
|
||||
code.appendLeft(
|
||||
arg.name.end,
|
||||
": import('./$types').RequestEvent" + (add_parens ? ')' : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
// Ignore all type errors so they don't show up twice when svelte-check runs
|
||||
// Account for possible @ts-check which would overwrite @ts-nocheck
|
||||
if (code.original.startsWith('// @ts-check')) {
|
||||
code.prependLeft('// @ts-check'.length, '\n// @ts-nocheck\n');
|
||||
} else {
|
||||
code.prepend('// @ts-nocheck\n');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
modified,
|
||||
code: code.toString(),
|
||||
exports: Array.from(exports.keys())
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import colors from 'kleur';
|
||||
import { posixify, to_fs } from '../utils/filesystem.js';
|
||||
|
||||
/**
|
||||
* Resolved path of the `runtime` directory
|
||||
*
|
||||
* TODO Windows issue:
|
||||
* Vite or sth else somehow sets the driver letter inconsistently to lower or upper case depending on the run environment.
|
||||
* In playwright debug mode run through VS Code this a root-to-lowercase conversion is needed in order for the tests to run.
|
||||
* If we do this conversion in other cases it has the opposite effect though and fails.
|
||||
*/
|
||||
export const runtime_directory = posixify(fileURLToPath(new URL('../runtime', import.meta.url)));
|
||||
|
||||
/**
|
||||
* This allows us to import SvelteKit internals that aren't exposed via `pkg.exports` in a
|
||||
* way that works whether `@sveltejs/kit` is installed inside the project's `node_modules`
|
||||
* or in a workspace root
|
||||
*/
|
||||
export const runtime_base = runtime_directory.startsWith(process.cwd())
|
||||
? `/${path.relative('.', runtime_directory)}`
|
||||
: to_fs(runtime_directory);
|
||||
|
||||
function noop() {}
|
||||
|
||||
/** @param {{ verbose: boolean }} opts */
|
||||
export function logger({ verbose }) {
|
||||
/** @type {import('types').Logger} */
|
||||
const log = (msg) => console.log(msg.replace(/^/gm, ' '));
|
||||
|
||||
/** @param {string} msg */
|
||||
const err = (msg) => console.error(msg.replace(/^/gm, ' '));
|
||||
|
||||
log.success = (msg) => log(colors.green(`✔ ${msg}`));
|
||||
log.error = (msg) => err(colors.bold().red(msg));
|
||||
log.warn = (msg) => log(colors.bold().yellow(msg));
|
||||
|
||||
log.minor = verbose ? (msg) => log(colors.grey(msg)) : noop;
|
||||
log.info = verbose ? log : noop;
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
/** @param {import('types').ManifestData} manifest_data */
|
||||
export function get_mime_lookup(manifest_data) {
|
||||
/** @type {Record<string, string>} */
|
||||
const mime = {};
|
||||
|
||||
manifest_data.assets.forEach((asset) => {
|
||||
if (asset.type) {
|
||||
const ext = path.extname(asset.file);
|
||||
mime[ext] = asset.type;
|
||||
}
|
||||
});
|
||||
|
||||
return mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {(file: string) => boolean} [filter]
|
||||
*/
|
||||
export function list_files(dir, filter) {
|
||||
/** @type {string[]} */
|
||||
const files = [];
|
||||
|
||||
/** @param {string} current */
|
||||
function walk(current) {
|
||||
for (const file of fs.readdirSync(path.resolve(dir, current))) {
|
||||
const child = path.posix.join(current, file);
|
||||
if (fs.statSync(path.resolve(dir, child)).isDirectory()) {
|
||||
walk(child);
|
||||
} else {
|
||||
if (!filter || filter(child)) {
|
||||
files.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(dir)) walk('');
|
||||
|
||||
return files;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { sequence } from './sequence.js';
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/** @import { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit' */
|
||||
/** @import { MaybePromise } from 'types' */
|
||||
import {
|
||||
merge_tracing,
|
||||
get_request_store,
|
||||
with_request_store
|
||||
} from '@sveltejs/kit/internal/server';
|
||||
|
||||
/**
|
||||
* A helper function for sequencing multiple `handle` calls in a middleware-like manner.
|
||||
* The behavior for the `handle` options is as follows:
|
||||
* - `transformPageChunk` is applied in reverse order and merged
|
||||
* - `preload` is applied in forward order, the first option "wins" and no `preload` options after it are called
|
||||
* - `filterSerializedResponseHeaders` behaves the same as `preload`
|
||||
*
|
||||
* ```js
|
||||
* /// file: src/hooks.server.js
|
||||
* import { sequence } from '@sveltejs/kit/hooks';
|
||||
*
|
||||
* /// type: import('@sveltejs/kit').Handle
|
||||
* async function first({ event, resolve }) {
|
||||
* console.log('first pre-processing');
|
||||
* const result = await resolve(event, {
|
||||
* transformPageChunk: ({ html }) => {
|
||||
* // transforms are applied in reverse order
|
||||
* console.log('first transform');
|
||||
* return html;
|
||||
* },
|
||||
* preload: () => {
|
||||
* // this one wins as it's the first defined in the chain
|
||||
* console.log('first preload');
|
||||
* return true;
|
||||
* }
|
||||
* });
|
||||
* console.log('first post-processing');
|
||||
* return result;
|
||||
* }
|
||||
*
|
||||
* /// type: import('@sveltejs/kit').Handle
|
||||
* async function second({ event, resolve }) {
|
||||
* console.log('second pre-processing');
|
||||
* const result = await resolve(event, {
|
||||
* transformPageChunk: ({ html }) => {
|
||||
* console.log('second transform');
|
||||
* return html;
|
||||
* },
|
||||
* preload: () => {
|
||||
* console.log('second preload');
|
||||
* return true;
|
||||
* },
|
||||
* filterSerializedResponseHeaders: () => {
|
||||
* // this one wins as it's the first defined in the chain
|
||||
* console.log('second filterSerializedResponseHeaders');
|
||||
* return true;
|
||||
* }
|
||||
* });
|
||||
* console.log('second post-processing');
|
||||
* return result;
|
||||
* }
|
||||
*
|
||||
* export const handle = sequence(first, second);
|
||||
* ```
|
||||
*
|
||||
* The example above would print:
|
||||
*
|
||||
* ```
|
||||
* first pre-processing
|
||||
* first preload
|
||||
* second pre-processing
|
||||
* second filterSerializedResponseHeaders
|
||||
* second transform
|
||||
* first transform
|
||||
* second post-processing
|
||||
* first post-processing
|
||||
* ```
|
||||
*
|
||||
* @param {...Handle} handlers The chain of `handle` functions
|
||||
* @returns {Handle}
|
||||
*/
|
||||
export function sequence(...handlers) {
|
||||
const length = handlers.length;
|
||||
if (!length) return ({ event, resolve }) => resolve(event);
|
||||
|
||||
return ({ event, resolve }) => {
|
||||
const { state } = get_request_store();
|
||||
return apply_handle(0, event, {});
|
||||
|
||||
/**
|
||||
* @param {number} i
|
||||
* @param {RequestEvent} event
|
||||
* @param {ResolveOptions | undefined} parent_options
|
||||
* @returns {MaybePromise<Response>}
|
||||
*/
|
||||
function apply_handle(i, event, parent_options) {
|
||||
const handle = handlers[i];
|
||||
|
||||
return state.tracing.record_span({
|
||||
name: `sveltekit.handle.sequenced.${handle.name ? handle.name : i}`,
|
||||
attributes: {},
|
||||
fn: async (current) => {
|
||||
const traced_event = merge_tracing(event, current);
|
||||
return await with_request_store({ event: traced_event, state }, () =>
|
||||
handle({
|
||||
event: traced_event,
|
||||
resolve: (event, options) => {
|
||||
/** @type {ResolveOptions['transformPageChunk']} */
|
||||
const transformPageChunk = async ({ html, done }) => {
|
||||
if (options?.transformPageChunk) {
|
||||
html = (await options.transformPageChunk({ html, done })) ?? '';
|
||||
}
|
||||
|
||||
if (parent_options?.transformPageChunk) {
|
||||
html = (await parent_options.transformPageChunk({ html, done })) ?? '';
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
/** @type {ResolveOptions['filterSerializedResponseHeaders']} */
|
||||
const filterSerializedResponseHeaders =
|
||||
parent_options?.filterSerializedResponseHeaders ??
|
||||
options?.filterSerializedResponseHeaders;
|
||||
|
||||
/** @type {ResolveOptions['preload']} */
|
||||
const preload = parent_options?.preload ?? options?.preload;
|
||||
|
||||
return i < length - 1
|
||||
? apply_handle(i + 1, event, {
|
||||
transformPageChunk,
|
||||
filterSerializedResponseHeaders,
|
||||
preload
|
||||
})
|
||||
: resolve(event, {
|
||||
transformPageChunk,
|
||||
filterSerializedResponseHeaders,
|
||||
preload
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
|
||||
import { HttpError, Redirect, ActionFailure, ValidationError } from './internal/index.js';
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import {
|
||||
add_data_suffix,
|
||||
add_resolution_suffix,
|
||||
has_data_suffix,
|
||||
has_resolution_suffix,
|
||||
strip_data_suffix,
|
||||
strip_resolution_suffix
|
||||
} from '../runtime/pathname.js';
|
||||
import { text_encoder } from '../runtime/utils.js';
|
||||
|
||||
export { VERSION } from '../version.js';
|
||||
|
||||
// TODO 3.0: remove these types as they are not used anymore (we can't remove them yet because that would be a breaking change)
|
||||
/**
|
||||
* @template {number} TNumber
|
||||
* @template {any[]} [TArray=[]]
|
||||
* @typedef {TNumber extends TArray['length'] ? TArray[number] : LessThan<TNumber, [...TArray, TArray['length']]>} LessThan
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {number} TStart
|
||||
* @template {number} TEnd
|
||||
* @typedef {Exclude<TEnd | LessThan<TEnd>, LessThan<TStart>>} NumericRange
|
||||
*/
|
||||
|
||||
// Keep the status codes as `number` because restricting to certain numbers makes it unnecessarily hard to use compared to the benefits
|
||||
// (we have runtime errors already to check for invalid codes). Also see https://github.com/sveltejs/kit/issues/11780
|
||||
|
||||
// we have to repeat the JSDoc because the display for function overloads is broken
|
||||
// see https://github.com/microsoft/TypeScript/issues/55056
|
||||
|
||||
/**
|
||||
* Throws an error with a HTTP status code and an optional message.
|
||||
* When called during request handling, this will cause SvelteKit to
|
||||
* return an error response without invoking `handleError`.
|
||||
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
||||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
||||
* @param {App.Error} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
||||
* @overload
|
||||
* @param {number} status
|
||||
* @param {App.Error} body
|
||||
* @return {never}
|
||||
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
||||
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
||||
*/
|
||||
/**
|
||||
* Throws an error with a HTTP status code and an optional message.
|
||||
* When called during request handling, this will cause SvelteKit to
|
||||
* return an error response without invoking `handleError`.
|
||||
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
||||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
||||
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
||||
* @overload
|
||||
* @param {number} status
|
||||
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body]
|
||||
* @return {never}
|
||||
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
||||
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
||||
*/
|
||||
/**
|
||||
* Throws an error with a HTTP status code and an optional message.
|
||||
* When called during request handling, this will cause SvelteKit to
|
||||
* return an error response without invoking `handleError`.
|
||||
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
||||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
||||
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
||||
* @return {never}
|
||||
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
||||
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
||||
*/
|
||||
export function error(status, body) {
|
||||
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
|
||||
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
|
||||
}
|
||||
|
||||
throw new HttpError(status, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this is an error thrown by {@link error}.
|
||||
* @template {number} T
|
||||
* @param {unknown} e
|
||||
* @param {T} [status] The status to filter for.
|
||||
* @return {e is (HttpError & { status: T extends undefined ? never : T })}
|
||||
*/
|
||||
export function isHttpError(e, status) {
|
||||
if (!(e instanceof HttpError)) return false;
|
||||
return !status || e.status === status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect a request. When called during request handling, SvelteKit will return a redirect response.
|
||||
* Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it.
|
||||
*
|
||||
* Most common status codes:
|
||||
* * `303 See Other`: redirect as a GET request (often used after a form POST request)
|
||||
* * `307 Temporary Redirect`: redirect will keep the request method
|
||||
* * `308 Permanent Redirect`: redirect will keep the request method, SEO will be transferred to the new page
|
||||
*
|
||||
* [See all redirect status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages)
|
||||
*
|
||||
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number)} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308.
|
||||
* @param {string | URL} location The location to redirect to.
|
||||
* @throws {Redirect} This error instructs SvelteKit to redirect to the specified location.
|
||||
* @throws {Error} If the provided status is invalid.
|
||||
* @return {never}
|
||||
*/
|
||||
export function redirect(status, location) {
|
||||
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
|
||||
throw new Error('Invalid status code');
|
||||
}
|
||||
|
||||
throw new Redirect(
|
||||
// @ts-ignore
|
||||
status,
|
||||
location.toString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this is a redirect thrown by {@link redirect}.
|
||||
* @param {unknown} e The object to check.
|
||||
* @return {e is Redirect}
|
||||
*/
|
||||
export function isRedirect(e) {
|
||||
return e instanceof Redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON `Response` object from the supplied data.
|
||||
* @param {any} data The value that will be serialized as JSON.
|
||||
* @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. `Content-Type: application/json` and `Content-Length` headers will be added automatically.
|
||||
*/
|
||||
export function json(data, init) {
|
||||
// TODO deprecate this in favour of `Response.json` when it's
|
||||
// more widely supported
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
// we can't just do `text(JSON.stringify(data), init)` because
|
||||
// it will set a default `content-type` header. duplicated code
|
||||
// means less duplicated work
|
||||
const headers = new Headers(init?.headers);
|
||||
if (!headers.has('content-length')) {
|
||||
headers.set('content-length', text_encoder.encode(body).byteLength.toString());
|
||||
}
|
||||
|
||||
if (!headers.has('content-type')) {
|
||||
headers.set('content-type', 'application/json');
|
||||
}
|
||||
|
||||
return new Response(body, {
|
||||
...init,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `Response` object from the supplied body.
|
||||
* @param {string} body The value that will be used as-is.
|
||||
* @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. A `Content-Length` header will be added automatically.
|
||||
*/
|
||||
export function text(body, init) {
|
||||
const headers = new Headers(init?.headers);
|
||||
if (!headers.has('content-length')) {
|
||||
const encoded = text_encoder.encode(body);
|
||||
headers.set('content-length', encoded.byteLength.toString());
|
||||
return new Response(encoded, {
|
||||
...init,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(body, {
|
||||
...init,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an `ActionFailure` object. Call when form submission fails.
|
||||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
||||
* @overload
|
||||
* @param {number} status
|
||||
* @returns {import('./public.js').ActionFailure<undefined>}
|
||||
*/
|
||||
/**
|
||||
* Create an `ActionFailure` object. Call when form submission fails.
|
||||
* @template [T=undefined]
|
||||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
||||
* @param {T} data Data associated with the failure (e.g. validation errors)
|
||||
* @overload
|
||||
* @param {number} status
|
||||
* @param {T} data
|
||||
* @returns {import('./public.js').ActionFailure<T>}
|
||||
*/
|
||||
/**
|
||||
* Create an `ActionFailure` object. Call when form submission fails.
|
||||
* @param {number} status
|
||||
* @param {any} [data]
|
||||
* @returns {import('./public.js').ActionFailure<any>}
|
||||
*/
|
||||
export function fail(status, data) {
|
||||
// @ts-expect-error unique symbol missing
|
||||
return new ActionFailure(status, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this is an action failure thrown by {@link fail}.
|
||||
* @param {unknown} e The object to check.
|
||||
* @return {e is import('./public.js').ActionFailure}
|
||||
*/
|
||||
export function isActionFailure(e) {
|
||||
return e instanceof ActionFailure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to throw a validation error to imperatively fail form validation.
|
||||
* Can be used in combination with `issue` passed to form actions to create field-specific issues.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { invalid } from '@sveltejs/kit';
|
||||
* import { form } from '$app/server';
|
||||
* import { tryLogin } from '$lib/server/auth';
|
||||
* import * as v from 'valibot';
|
||||
*
|
||||
* export const login = form(
|
||||
* v.object({ name: v.string(), _password: v.string() }),
|
||||
* async ({ name, _password }) => {
|
||||
* const success = tryLogin(name, _password);
|
||||
* if (!success) {
|
||||
* invalid('Incorrect username or password');
|
||||
* }
|
||||
*
|
||||
* // ...
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
* @param {...(StandardSchemaV1.Issue | string)} issues
|
||||
* @returns {never}
|
||||
* @since 2.47.3
|
||||
*/
|
||||
export function invalid(...issues) {
|
||||
throw new ValidationError(
|
||||
issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this is an validation error thrown by {@link invalid}.
|
||||
* @param {unknown} e The object to check.
|
||||
* @return {e is import('./public.js').ActionFailure}
|
||||
* @since 2.47.3
|
||||
*/
|
||||
export function isValidationError(e) {
|
||||
return e instanceof ValidationError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
|
||||
* Returns the normalized URL as well as a method for adding the potential suffix back
|
||||
* based on a new pathname (possibly including search) or URL.
|
||||
* ```js
|
||||
* import { normalizeUrl } from '@sveltejs/kit';
|
||||
*
|
||||
* const { url, denormalize } = normalizeUrl('/blog/post/__data.json');
|
||||
* console.log(url.pathname); // /blog/post
|
||||
* console.log(denormalize('/blog/post/a')); // /blog/post/a/__data.json
|
||||
* ```
|
||||
* @param {URL | string} url
|
||||
* @returns {{ url: URL, wasNormalized: boolean, denormalize: (url?: string | URL) => URL }}
|
||||
* @since 2.18.0
|
||||
*/
|
||||
export function normalizeUrl(url) {
|
||||
url = new URL(url, 'a://a');
|
||||
|
||||
const is_route_resolution = has_resolution_suffix(url.pathname);
|
||||
const is_data_request = has_data_suffix(url.pathname);
|
||||
const has_trailing_slash = url.pathname !== '/' && url.pathname.endsWith('/');
|
||||
|
||||
if (is_route_resolution) {
|
||||
url.pathname = strip_resolution_suffix(url.pathname);
|
||||
} else if (is_data_request) {
|
||||
url.pathname = strip_data_suffix(url.pathname);
|
||||
} else if (has_trailing_slash) {
|
||||
url.pathname = url.pathname.slice(0, -1);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
wasNormalized: is_data_request || is_route_resolution || has_trailing_slash,
|
||||
denormalize: (new_url = url) => {
|
||||
new_url = new URL(new_url, url);
|
||||
if (is_route_resolution) {
|
||||
new_url.pathname = add_resolution_suffix(new_url.pathname);
|
||||
} else if (is_data_request) {
|
||||
new_url.pathname = add_data_suffix(new_url.pathname);
|
||||
} else if (has_trailing_slash && !new_url.pathname.endsWith('/')) {
|
||||
new_url.pathname += '/';
|
||||
}
|
||||
return new_url;
|
||||
}
|
||||
};
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/** @import { RequestEvent } from '@sveltejs/kit' */
|
||||
/** @import { RequestStore } from 'types' */
|
||||
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
|
||||
|
||||
import { IN_WEBCONTAINER } from '../../runtime/server/constants.js';
|
||||
|
||||
/** @type {RequestStore | null} */
|
||||
let sync_store = null;
|
||||
|
||||
/** @type {AsyncLocalStorage<RequestStore | null> | null} */
|
||||
let als;
|
||||
|
||||
import('node:async_hooks')
|
||||
.then((hooks) => (als = new hooks.AsyncLocalStorage()))
|
||||
.catch(() => {
|
||||
// can't use AsyncLocalStorage, but can still call getRequestEvent synchronously.
|
||||
// this isn't behind `supports` because it's basically just StackBlitz (i.e.
|
||||
// in-browser usage) that doesn't support it AFAICT
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the current `RequestEvent`. Can be used inside server hooks, server `load` functions, actions, and endpoints (and functions called by them).
|
||||
*
|
||||
* In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`).
|
||||
* @since 2.20.0
|
||||
*
|
||||
* @returns {RequestEvent}
|
||||
*/
|
||||
export function getRequestEvent() {
|
||||
const event = try_get_request_store()?.event;
|
||||
|
||||
if (!event) {
|
||||
let message =
|
||||
'Can only read the current request event inside functions invoked during `handle`, such as server `load` functions, actions, endpoints, and other server hooks.';
|
||||
|
||||
if (!als) {
|
||||
message +=
|
||||
' In environments without `AsyncLocalStorage`, the event must be read synchronously, not after an `await`.';
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function get_request_store() {
|
||||
const result = try_get_request_store();
|
||||
if (!result) {
|
||||
let message = 'Could not get the request store.';
|
||||
|
||||
if (als) {
|
||||
message += ' This is an internal error.';
|
||||
} else {
|
||||
message +=
|
||||
' In environments without `AsyncLocalStorage`, the request store (used by e.g. remote functions) must be accessed synchronously, not after an `await`.' +
|
||||
' If it was accessed synchronously then this is an internal error.';
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function try_get_request_store() {
|
||||
return sync_store ?? als?.getStore() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {RequestStore | null} store
|
||||
* @param {() => T} fn
|
||||
*/
|
||||
export function with_request_store(store, fn) {
|
||||
try {
|
||||
sync_store = store;
|
||||
return als ? als.run(store, fn) : fn();
|
||||
} finally {
|
||||
// Since AsyncLocalStorage is not working in webcontainers, we don't reset `sync_store`
|
||||
// and handle only one request at a time in `src/runtime/server/index.js`.
|
||||
if (!IN_WEBCONTAINER) {
|
||||
sync_store = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
|
||||
export class HttpError {
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body
|
||||
*/
|
||||
constructor(status, body) {
|
||||
this.status = status;
|
||||
if (typeof body === 'string') {
|
||||
this.body = { message: body };
|
||||
} else if (body) {
|
||||
this.body = body;
|
||||
} else {
|
||||
this.body = { message: `Error: ${status}` };
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.body);
|
||||
}
|
||||
}
|
||||
|
||||
export class Redirect {
|
||||
/**
|
||||
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status
|
||||
* @param {string} location
|
||||
*/
|
||||
constructor(status, location) {
|
||||
this.status = status;
|
||||
this.location = location;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error that was thrown from within the SvelteKit runtime that is not fatal and doesn't result in a 500, such as a 404.
|
||||
* `SvelteKitError` goes through `handleError`.
|
||||
* @extends Error
|
||||
*/
|
||||
export class SvelteKitError extends Error {
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {string} text
|
||||
* @param {string} message
|
||||
*/
|
||||
constructor(status, text, message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template [T=undefined]
|
||||
*/
|
||||
export class ActionFailure {
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {T} data
|
||||
*/
|
||||
constructor(status, data) {
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when form validation fails imperatively
|
||||
*/
|
||||
export class ValidationError extends Error {
|
||||
/**
|
||||
* @param {StandardSchemaV1.Issue[]} issues
|
||||
*/
|
||||
constructor(issues) {
|
||||
super('Validation failed');
|
||||
this.name = 'ValidationError';
|
||||
this.issues = issues;
|
||||
}
|
||||
}
|
||||
|
||||
export { init_remote_functions } from './remote-functions.js';
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/** @import { RemoteInfo } from 'types' */
|
||||
|
||||
/** @type {RemoteInfo['type'][]} */
|
||||
const types = ['command', 'form', 'prerender', 'query', 'query_batch'];
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} module
|
||||
* @param {string} file
|
||||
* @param {string} hash
|
||||
*/
|
||||
export function init_remote_functions(module, file, hash) {
|
||||
if (module.default) {
|
||||
throw new Error(
|
||||
`Cannot export \`default\` from a remote module (${file}) — please use named exports instead`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [name, fn] of Object.entries(module)) {
|
||||
if (!types.includes(fn?.__?.type)) {
|
||||
throw new Error(
|
||||
`\`${name}\` exported from ${file} is invalid — all exports from this file must be remote functions`
|
||||
);
|
||||
}
|
||||
|
||||
fn.__.id = `${hash}/${name}`;
|
||||
fn.__.name = name;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @template {{ tracing: { enabled: boolean, root: import('@opentelemetry/api').Span, current: import('@opentelemetry/api').Span } }} T
|
||||
* @param {T} event_like
|
||||
* @param {import('@opentelemetry/api').Span} current
|
||||
* @returns {T}
|
||||
*/
|
||||
export function merge_tracing(event_like, current) {
|
||||
return {
|
||||
...event_like,
|
||||
tracing: {
|
||||
...event_like.tracing,
|
||||
current
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
with_request_store,
|
||||
getRequestEvent,
|
||||
get_request_store,
|
||||
try_get_request_store
|
||||
} from './event.js';
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { Readable } from 'node:stream';
|
||||
import * as set_cookie_parser from 'set-cookie-parser';
|
||||
import { SvelteKitError } from '../internal/index.js';
|
||||
|
||||
/**
|
||||
* @param {import('http').IncomingMessage} req
|
||||
* @param {number} [body_size_limit]
|
||||
*/
|
||||
function get_raw_body(req, body_size_limit) {
|
||||
const h = req.headers;
|
||||
|
||||
if (!h['content-type']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content_length = Number(h['content-length']);
|
||||
|
||||
// check if no request body
|
||||
if (
|
||||
(req.httpVersionMajor === 1 && isNaN(content_length) && h['transfer-encoding'] == null) ||
|
||||
content_length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (req.destroyed) {
|
||||
const readable = new ReadableStream();
|
||||
void readable.cancel();
|
||||
return readable;
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
let cancelled = false;
|
||||
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
if (body_size_limit !== undefined && content_length > body_size_limit) {
|
||||
let message = `Content-length of ${content_length} exceeds limit of ${body_size_limit} bytes.`;
|
||||
|
||||
if (body_size_limit === 0) {
|
||||
// https://github.com/sveltejs/kit/pull/11589
|
||||
// TODO this exists to aid migration — remove in a future version
|
||||
message += ' To disable body size limits, specify Infinity rather than 0.';
|
||||
}
|
||||
|
||||
const error = new SvelteKitError(413, 'Payload Too Large', message);
|
||||
|
||||
controller.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
req.on('error', (error) => {
|
||||
cancelled = true;
|
||||
controller.error(error);
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
if (cancelled) return;
|
||||
controller.close();
|
||||
});
|
||||
|
||||
req.on('data', (chunk) => {
|
||||
if (cancelled) return;
|
||||
|
||||
size += chunk.length;
|
||||
if (size > content_length) {
|
||||
cancelled = true;
|
||||
|
||||
const constraint = content_length ? 'content-length' : 'BODY_SIZE_LIMIT';
|
||||
const message = `request body size exceeded ${constraint} of ${content_length}`;
|
||||
|
||||
const error = new SvelteKitError(413, 'Payload Too Large', message);
|
||||
controller.error(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(chunk);
|
||||
|
||||
if (controller.desiredSize === null || controller.desiredSize <= 0) {
|
||||
req.pause();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
pull() {
|
||||
req.resume();
|
||||
},
|
||||
|
||||
cancel(reason) {
|
||||
cancelled = true;
|
||||
req.destroy(reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* request: import('http').IncomingMessage;
|
||||
* base: string;
|
||||
* bodySizeLimit?: number;
|
||||
* }} options
|
||||
* @returns {Promise<Request>}
|
||||
*/
|
||||
// TODO 3.0 make the signature synchronous?
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export async function getRequest({ request, base, bodySizeLimit }) {
|
||||
let headers = /** @type {Record<string, string>} */ (request.headers);
|
||||
if (request.httpVersionMajor >= 2) {
|
||||
// the Request constructor rejects headers with ':' in the name
|
||||
headers = Object.assign({}, headers);
|
||||
// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.5
|
||||
if (headers[':authority']) {
|
||||
headers.host = headers[':authority'];
|
||||
}
|
||||
delete headers[':authority'];
|
||||
delete headers[':method'];
|
||||
delete headers[':path'];
|
||||
delete headers[':scheme'];
|
||||
}
|
||||
|
||||
// TODO: Whenever Node >=22 is minimum supported version, we can use `request.readableAborted`
|
||||
// @see https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/lib/internal/streams/readable.js#L1443-L1453
|
||||
const controller = new AbortController();
|
||||
let errored = false;
|
||||
let end_emitted = false;
|
||||
request.once('error', () => (errored = true));
|
||||
request.once('end', () => (end_emitted = true));
|
||||
request.once('close', () => {
|
||||
if ((errored || request.destroyed) && !end_emitted) {
|
||||
controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
return new Request(base + request.url, {
|
||||
// @ts-expect-error
|
||||
duplex: 'half',
|
||||
method: request.method,
|
||||
headers: Object.entries(headers),
|
||||
signal: controller.signal,
|
||||
body:
|
||||
request.method === 'GET' || request.method === 'HEAD'
|
||||
? undefined
|
||||
: get_raw_body(request, bodySizeLimit)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('http').ServerResponse} res
|
||||
* @param {Response} response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
// TODO 3.0 make the signature synchronous?
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export async function setResponse(res, response) {
|
||||
for (const [key, value] of response.headers) {
|
||||
try {
|
||||
res.setHeader(
|
||||
key,
|
||||
key === 'set-cookie'
|
||||
? set_cookie_parser.splitCookiesString(
|
||||
// This is absurd but necessary, TODO: investigate why
|
||||
/** @type {string}*/ (response.headers.get(key))
|
||||
)
|
||||
: value
|
||||
);
|
||||
} catch (error) {
|
||||
res.getHeaderNames().forEach((name) => res.removeHeader(name));
|
||||
res.writeHead(500).end(String(error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(response.status);
|
||||
|
||||
if (!response.body) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body.locked) {
|
||||
res.end(
|
||||
'Fatal error: Response body is locked. ' +
|
||||
"This can happen when the response was already read (for example through 'response.json()' or 'response.text()')."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
||||
if (res.destroyed) {
|
||||
void reader.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const cancel = (/** @type {Error|undefined} */ error) => {
|
||||
res.off('close', cancel);
|
||||
res.off('error', cancel);
|
||||
|
||||
// If the reader has already been interrupted with an error earlier,
|
||||
// then it will appear here, it is useless, but it needs to be catch.
|
||||
reader.cancel(error).catch(() => {});
|
||||
if (error) res.destroy(error);
|
||||
};
|
||||
|
||||
res.on('close', cancel);
|
||||
res.on('error', cancel);
|
||||
|
||||
void next();
|
||||
async function next() {
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
if (!res.write(value)) {
|
||||
res.once('drain', next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
} catch (error) {
|
||||
cancel(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file on disk to a readable stream
|
||||
* @param {string} file
|
||||
* @returns {ReadableStream}
|
||||
* @since 2.4.0
|
||||
*/
|
||||
export function createReadableStream(file) {
|
||||
return /** @type {ReadableStream} */ (Readable.toWeb(createReadStream(file)));
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import buffer from 'node:buffer';
|
||||
import { webcrypto as crypto } from 'node:crypto';
|
||||
|
||||
// `buffer.File` was added in Node 18.13.0 while the `File` global was added in Node 20.0.0
|
||||
const File = /** @type {import('node:buffer') & { File?: File}} */ (buffer).File;
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
const globals = {
|
||||
crypto,
|
||||
File
|
||||
};
|
||||
|
||||
// exported for dev/preview and node environments
|
||||
/**
|
||||
* Make various web APIs available as globals:
|
||||
* - `crypto`
|
||||
* - `File`
|
||||
*/
|
||||
export function installPolyfills() {
|
||||
for (const name in globals) {
|
||||
if (name in globalThis) continue;
|
||||
|
||||
Object.defineProperty(globalThis, name, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: globals[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
+2186
File diff suppressed because it is too large
Load Diff
+240
@@ -0,0 +1,240 @@
|
||||
import fs from 'node:fs';
|
||||
import { mkdirp } from '../../../utils/filesystem.js';
|
||||
import { create_function_as_string, filter_fonts, find_deps, resolve_symlinks } from './utils.js';
|
||||
import { s } from '../../../utils/misc.js';
|
||||
import { normalizePath } from 'vite';
|
||||
import { basename } from 'node:path';
|
||||
import { fix_css_urls } from '../../../utils/css.js';
|
||||
|
||||
/**
|
||||
* @param {string} out
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {import('vite').Manifest} server_manifest
|
||||
* @param {import('vite').Manifest | null} client_manifest
|
||||
* @param {string | null} assets_path
|
||||
* @param {import('vite').Rollup.RollupOutput['output'] | null} client_chunks
|
||||
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
|
||||
*/
|
||||
export function build_server_nodes(
|
||||
out,
|
||||
kit,
|
||||
manifest_data,
|
||||
server_manifest,
|
||||
client_manifest,
|
||||
assets_path,
|
||||
client_chunks,
|
||||
output_config
|
||||
) {
|
||||
mkdirp(`${out}/server/nodes`);
|
||||
mkdirp(`${out}/server/stylesheets`);
|
||||
|
||||
/**
|
||||
* Stylesheet names and their contents which are below the inline threshold
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
const stylesheets_to_inline = new Map();
|
||||
|
||||
/**
|
||||
* For CSS inlining, we either store a string or a function that returns the
|
||||
* styles with the correct relative URLs
|
||||
* @type {(css: string, eager_assets: Set<string>) => string}
|
||||
*/
|
||||
let prepare_css_for_inlining = (css) => s(css);
|
||||
|
||||
if (client_chunks && kit.inlineStyleThreshold > 0 && output_config.bundleStrategy === 'split') {
|
||||
for (const chunk of client_chunks) {
|
||||
if (chunk.type !== 'asset' || !chunk.fileName.endsWith('.css')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = chunk.source.toString();
|
||||
if (source.length < kit.inlineStyleThreshold) {
|
||||
stylesheets_to_inline.set(chunk.fileName, source);
|
||||
}
|
||||
}
|
||||
|
||||
// If the client CSS has URL references to assets, we need to adjust the
|
||||
// relative path so that they are correct when inlined into the document.
|
||||
// Although `paths.assets` is static, we need to pass in a fake path
|
||||
// `/_svelte_kit_assets` at runtime when running `vite preview`
|
||||
if (kit.paths.assets || kit.paths.relative) {
|
||||
const static_assets = new Set(
|
||||
manifest_data.assets.map((asset) => decodeURIComponent(asset.file))
|
||||
);
|
||||
|
||||
const segments = /** @type {string} */ (assets_path).split('/');
|
||||
const static_asset_prefix = segments.map(() => '..').join('/') + '/';
|
||||
|
||||
prepare_css_for_inlining = (css, eager_assets) => {
|
||||
const transformed_css = fix_css_urls({
|
||||
css,
|
||||
vite_assets: eager_assets,
|
||||
static_assets,
|
||||
paths_assets: '${assets}',
|
||||
base: '${base}',
|
||||
static_asset_prefix
|
||||
});
|
||||
|
||||
// only convert to a function if we have adjusted any URLs
|
||||
if (css !== transformed_css) {
|
||||
return create_function_as_string('css', ['assets', 'base'], transformed_css);
|
||||
}
|
||||
|
||||
return s(css);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < manifest_data.nodes.length; i++) {
|
||||
const node = manifest_data.nodes[i];
|
||||
|
||||
/** @type {string[]} */
|
||||
const imports = [];
|
||||
|
||||
// String representation of
|
||||
/** @type {import('types').SSRNode} */
|
||||
/** @type {string[]} */
|
||||
const exports = [`export const index = ${i};`];
|
||||
|
||||
/** @type {string[]} */
|
||||
let imported = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
let stylesheets = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
let fonts = [];
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const eager_assets = new Set();
|
||||
|
||||
if (node.component && client_manifest) {
|
||||
exports.push(
|
||||
'let component_cache;',
|
||||
`export const component = async () => component_cache ??= (await import('../${
|
||||
resolve_symlinks(server_manifest, node.component).chunk.file
|
||||
}')).default;`
|
||||
);
|
||||
}
|
||||
|
||||
if (node.universal) {
|
||||
if (!!node.page_options && node.page_options.ssr === false) {
|
||||
exports.push(`export const universal = ${s(node.page_options, null, 2)};`);
|
||||
} else {
|
||||
imports.push(
|
||||
`import * as universal from '../${resolve_symlinks(server_manifest, node.universal).chunk.file}';`
|
||||
);
|
||||
// TODO: when building for analysis, explain why the file was loaded on the server if we fail to load it
|
||||
exports.push('export { universal };');
|
||||
}
|
||||
exports.push(`export const universal_id = ${s(node.universal)};`);
|
||||
}
|
||||
|
||||
if (node.server) {
|
||||
imports.push(
|
||||
`import * as server from '../${resolve_symlinks(server_manifest, node.server).chunk.file}';`
|
||||
);
|
||||
exports.push('export { server };');
|
||||
exports.push(`export const server_id = ${s(node.server)};`);
|
||||
}
|
||||
|
||||
if (
|
||||
client_manifest &&
|
||||
(node.universal || node.component) &&
|
||||
output_config.bundleStrategy === 'split'
|
||||
) {
|
||||
const entry_path = `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`;
|
||||
const entry = find_deps(client_manifest, entry_path, true);
|
||||
|
||||
// Eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC.
|
||||
// However, if it is not used during SSR (not present in the server manifest),
|
||||
// then it can be lazily loaded in the browser.
|
||||
|
||||
/** @type {import('types').AssetDependencies | undefined} */
|
||||
let component;
|
||||
if (node.component) {
|
||||
component = find_deps(server_manifest, node.component, true);
|
||||
}
|
||||
|
||||
/** @type {import('types').AssetDependencies | undefined} */
|
||||
let universal;
|
||||
if (node.universal) {
|
||||
universal = find_deps(server_manifest, node.universal, true);
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const eager_css = new Set();
|
||||
|
||||
entry.stylesheet_map.forEach((value, filepath) => {
|
||||
// pages and layouts are renamed to node indexes when optimised for the client
|
||||
// so we use the original filename instead to check against the server manifest
|
||||
if (filepath === entry_path) {
|
||||
filepath = node.component ?? filepath;
|
||||
}
|
||||
|
||||
if (component?.stylesheet_map.has(filepath) || universal?.stylesheet_map.has(filepath)) {
|
||||
value.css.forEach((file) => eager_css.add(file));
|
||||
value.assets.forEach((file) => eager_assets.add(file));
|
||||
}
|
||||
});
|
||||
|
||||
imported = entry.imports;
|
||||
stylesheets = Array.from(eager_css);
|
||||
fonts = filter_fonts(Array.from(eager_assets));
|
||||
}
|
||||
|
||||
exports.push(
|
||||
`export const imports = ${s(imported)};`,
|
||||
`export const stylesheets = ${s(stylesheets)};`,
|
||||
`export const fonts = ${s(fonts)};`
|
||||
);
|
||||
|
||||
/**
|
||||
* Assets that have been processed by Vite (decoded and with the asset path stripped)
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
let vite_assets = new Set();
|
||||
|
||||
// Keep track of Vite asset filenames so that we avoid touching unrelated ones
|
||||
// when adjusting the inlined CSS
|
||||
if (stylesheets_to_inline.size && assets_path && eager_assets.size) {
|
||||
vite_assets = new Set(
|
||||
Array.from(eager_assets).map((asset) => {
|
||||
return decodeURIComponent(asset.replace(`${assets_path}/`, ''));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (stylesheets_to_inline.size) {
|
||||
/** @type {string[]} */
|
||||
const inline_styles = [];
|
||||
|
||||
stylesheets.forEach((file, i) => {
|
||||
if (stylesheets_to_inline.has(file)) {
|
||||
const filename = basename(file);
|
||||
const dest = `${out}/server/stylesheets/${filename}.js`;
|
||||
|
||||
const css = /** @type {string} */ (stylesheets_to_inline.get(file));
|
||||
|
||||
fs.writeFileSync(
|
||||
dest,
|
||||
`// ${filename}\nexport default ${prepare_css_for_inlining(css, vite_assets)};`
|
||||
);
|
||||
const name = `stylesheet_${i}`;
|
||||
imports.push(`import ${name} from '../stylesheets/${filename}.js';`);
|
||||
inline_styles.push(`\t${s(file)}: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (inline_styles.length > 0) {
|
||||
exports.push(`export const inline_styles = () => ({\n${inline_styles.join(',\n')}\n});`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
`${out}/server/nodes/${i}.js`,
|
||||
`${imports.join('\n')}\n\n${exports.join('\n')}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import fs from 'node:fs';
|
||||
import process from 'node:process';
|
||||
import * as vite from 'vite';
|
||||
import { dedent } from '../../../core/sync/utils.js';
|
||||
import { s } from '../../../utils/misc.js';
|
||||
import { get_config_aliases, strip_virtual_prefix, get_env, normalize_id } from '../utils.js';
|
||||
import { create_static_module } from '../../../core/env.js';
|
||||
import { env_static_public, service_worker } from '../module_ids.js';
|
||||
|
||||
// @ts-ignore `vite.rolldownVersion` only exists in `rolldown-vite`
|
||||
const isRolldown = !!vite.rolldownVersion;
|
||||
|
||||
/**
|
||||
* @param {string} out
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} service_worker_entry_file
|
||||
* @param {import('types').Prerendered} prerendered
|
||||
* @param {import('vite').Manifest} client_manifest
|
||||
*/
|
||||
export async function build_service_worker(
|
||||
out,
|
||||
kit,
|
||||
vite_config,
|
||||
manifest_data,
|
||||
service_worker_entry_file,
|
||||
prerendered,
|
||||
client_manifest
|
||||
) {
|
||||
const build = new Set();
|
||||
for (const key in client_manifest) {
|
||||
const { file, css = [], assets = [] } = client_manifest[key];
|
||||
build.add(file);
|
||||
css.forEach((file) => build.add(file));
|
||||
assets.forEach((file) => build.add(file));
|
||||
}
|
||||
|
||||
// in a service worker, `location` is the location of the service worker itself,
|
||||
// which is guaranteed to be `<base>/service-worker.js`
|
||||
const base = "location.pathname.split('/').slice(0, -1).join('/')";
|
||||
|
||||
const service_worker_code = dedent`
|
||||
export const base = /*@__PURE__*/ ${base};
|
||||
|
||||
export const build = [
|
||||
${Array.from(build)
|
||||
.map((file) => `base + ${s(`/${file}`)}`)
|
||||
.join(',\n')}
|
||||
];
|
||||
|
||||
export const files = [
|
||||
${manifest_data.assets
|
||||
.filter((asset) => kit.serviceWorker.files(asset.file))
|
||||
.map((asset) => `base + ${s(`/${asset.file}`)}`)
|
||||
.join(',\n')}
|
||||
];
|
||||
|
||||
export const prerendered = [
|
||||
${prerendered.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')}
|
||||
];
|
||||
|
||||
export const version = ${s(kit.version.name)};
|
||||
`;
|
||||
|
||||
const env = get_env(kit.env, vite_config.mode);
|
||||
|
||||
/**
|
||||
* @type {import('vite').Plugin}
|
||||
*/
|
||||
const sw_virtual_modules = {
|
||||
name: 'service-worker-build-virtual-modules',
|
||||
resolveId(id) {
|
||||
if (id.startsWith('$env/') || id.startsWith('$app/') || id === '$service-worker') {
|
||||
// ids with :$ don't work with reverse proxies like nginx
|
||||
return `\0virtual:${id.substring(1)}`;
|
||||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
if (!id.startsWith('\0virtual:')) return;
|
||||
|
||||
if (id === service_worker) {
|
||||
return service_worker_code;
|
||||
}
|
||||
|
||||
if (id === env_static_public) {
|
||||
return create_static_module('$env/static/public', env.public);
|
||||
}
|
||||
|
||||
const normalized_cwd = vite.normalizePath(process.cwd());
|
||||
const normalized_lib = vite.normalizePath(kit.files.lib);
|
||||
const relative = normalize_id(id, normalized_lib, normalized_cwd);
|
||||
const stripped = strip_virtual_prefix(relative);
|
||||
throw new Error(
|
||||
`Cannot import ${stripped} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await vite.build({
|
||||
build: {
|
||||
modulePreload: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
'service-worker': service_worker_entry_file
|
||||
},
|
||||
output: {
|
||||
// .mjs so that esbuild doesn't incorrectly inject `export` https://github.com/vitejs/vite/issues/15379
|
||||
entryFileNames: `service-worker.${isRolldown ? 'js' : 'mjs'}`,
|
||||
assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`,
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
},
|
||||
outDir: `${out}/client`,
|
||||
emptyOutDir: false,
|
||||
minify: vite_config.build.minify
|
||||
},
|
||||
configFile: false,
|
||||
define: vite_config.define,
|
||||
publicDir: false,
|
||||
plugins: [sw_virtual_modules],
|
||||
resolve: {
|
||||
alias: [...get_config_aliases(kit)]
|
||||
},
|
||||
experimental: {
|
||||
renderBuiltUrl(filename) {
|
||||
return {
|
||||
runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname`
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// rename .mjs to .js to avoid incorrect MIME types with ancient webservers
|
||||
if (!isRolldown) {
|
||||
fs.renameSync(`${out}/client/service-worker.mjs`, `${out}/client/service-worker.js`);
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { normalizePath } from 'vite';
|
||||
import { s } from '../../../utils/misc.js';
|
||||
|
||||
/**
|
||||
* Adds transitive JS and CSS dependencies to the js and css inputs.
|
||||
* @param {import('vite').Manifest} manifest
|
||||
* @param {string} entry
|
||||
* @param {boolean} add_dynamic_css
|
||||
* @returns {import('types').AssetDependencies}
|
||||
*/
|
||||
export function find_deps(manifest, entry, add_dynamic_css) {
|
||||
/** @type {Set<string>} */
|
||||
const seen = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const imports = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const stylesheets = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const imported_assets = new Set();
|
||||
|
||||
/** @type {Map<string, { css: Set<string>; assets: Set<string> }>} */
|
||||
const stylesheet_map = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} current
|
||||
* @param {boolean} add_js
|
||||
* @param {string} initial_importer
|
||||
* @param {number} dynamic_import_depth
|
||||
*/
|
||||
function traverse(current, add_js, initial_importer, dynamic_import_depth) {
|
||||
if (seen.has(current)) return;
|
||||
seen.add(current);
|
||||
|
||||
const { chunk } = resolve_symlinks(manifest, current);
|
||||
|
||||
if (add_js) imports.add(chunk.file);
|
||||
|
||||
if (chunk.assets) {
|
||||
chunk.assets.forEach((asset) => imported_assets.add(asset));
|
||||
}
|
||||
|
||||
if (chunk.css) {
|
||||
chunk.css.forEach((file) => stylesheets.add(file));
|
||||
}
|
||||
|
||||
if (chunk.imports) {
|
||||
chunk.imports.forEach((file) =>
|
||||
traverse(file, add_js, initial_importer, dynamic_import_depth)
|
||||
);
|
||||
}
|
||||
|
||||
if (!add_dynamic_css) return;
|
||||
|
||||
if ((chunk.css || chunk.assets) && dynamic_import_depth <= 1) {
|
||||
// group files based on the initial importer because if a file is only ever
|
||||
// a transitive dependency, it doesn't have a suitable name we can map back to
|
||||
// the server manifest
|
||||
if (stylesheet_map.has(initial_importer)) {
|
||||
const { css, assets } = /** @type {{ css: Set<string>; assets: Set<string> }} */ (
|
||||
stylesheet_map.get(initial_importer)
|
||||
);
|
||||
if (chunk.css) chunk.css.forEach((file) => css.add(file));
|
||||
if (chunk.assets) chunk.assets.forEach((file) => assets.add(file));
|
||||
} else {
|
||||
stylesheet_map.set(initial_importer, {
|
||||
css: new Set(chunk.css),
|
||||
assets: new Set(chunk.assets)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.dynamicImports) {
|
||||
dynamic_import_depth++;
|
||||
chunk.dynamicImports.forEach((file) => {
|
||||
traverse(file, false, file, dynamic_import_depth);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { chunk, file } = resolve_symlinks(manifest, entry);
|
||||
|
||||
traverse(file, true, entry, 0);
|
||||
|
||||
const assets = Array.from(imported_assets);
|
||||
|
||||
return {
|
||||
assets,
|
||||
file: chunk.file,
|
||||
imports: Array.from(imports),
|
||||
stylesheets: Array.from(stylesheets),
|
||||
// TODO do we need this separately?
|
||||
fonts: filter_fonts(assets),
|
||||
stylesheet_map
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vite').Manifest} manifest
|
||||
* @param {string} file
|
||||
*/
|
||||
export function resolve_symlinks(manifest, file) {
|
||||
while (!manifest[file]) {
|
||||
const next = normalizePath(path.relative('.', fs.realpathSync(file)));
|
||||
if (next === file) throw new Error(`Could not find file "${file}" in Vite manifest`);
|
||||
file = next;
|
||||
}
|
||||
|
||||
const chunk = manifest[file];
|
||||
|
||||
return { chunk, file };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} assets
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function filter_fonts(assets) {
|
||||
return assets.filter((asset) => /\.(woff2?|ttf|otf)$/.test(asset));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* @returns {string}
|
||||
*/
|
||||
export function assets_base(config) {
|
||||
return (config.paths.assets || config.paths.base || '.') + '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a function with arguments used by a template literal.
|
||||
* This helps us store strings in a module and inject values at runtime.
|
||||
* @param {string} name The name of the function
|
||||
* @param {string[]} placeholder_names The names of the placeholders in the string
|
||||
* @param {string} str A string with placeholders such as "Hello ${arg0}".
|
||||
* It must have backticks and dollar signs escaped.
|
||||
* @returns {string} The function written as a string
|
||||
*/
|
||||
export function create_function_as_string(name, placeholder_names, str) {
|
||||
str = s(str).slice(1, -1);
|
||||
const args = placeholder_names ? placeholder_names.join(', ') : '';
|
||||
return `function ${name}(${args}) { return \`${str}\`; }`;
|
||||
}
|
||||
+669
@@ -0,0 +1,669 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { URL } from 'node:url';
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import colors from 'kleur';
|
||||
import sirv from 'sirv';
|
||||
import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite';
|
||||
import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
|
||||
import { installPolyfills } from '../../../exports/node/polyfills.js';
|
||||
import { coalesce_to_error } from '../../../utils/error.js';
|
||||
import { from_fs, posixify, resolve_entry, to_fs } from '../../../utils/filesystem.js';
|
||||
import { load_error_page } from '../../../core/config/index.js';
|
||||
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
|
||||
import * as sync from '../../../core/sync/sync.js';
|
||||
import { get_mime_lookup, runtime_base } from '../../../core/utils.js';
|
||||
import { compact } from '../../../utils/array.js';
|
||||
import { not_found } from '../utils.js';
|
||||
import { SCHEME } from '../../../utils/url.js';
|
||||
import { check_feature } from '../../../utils/features.js';
|
||||
import { escape_html } from '../../../utils/escape.js';
|
||||
|
||||
const cwd = process.cwd();
|
||||
// vite-specifc queries that we should skip handling for css urls
|
||||
const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/;
|
||||
|
||||
/**
|
||||
* @param {import('vite').ViteDevServer} vite
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
* @param {import('types').ValidatedConfig} svelte_config
|
||||
* @param {() => Array<{ hash: string, file: string }>} get_remotes
|
||||
* @return {Promise<Promise<() => void>>}
|
||||
*/
|
||||
export async function dev(vite, vite_config, svelte_config, get_remotes) {
|
||||
installPolyfills();
|
||||
|
||||
const async_local_storage = new AsyncLocalStorage();
|
||||
|
||||
globalThis.__SVELTEKIT_TRACK__ = (label) => {
|
||||
const context = async_local_storage.getStore();
|
||||
if (!context || context.prerender === true) return;
|
||||
|
||||
check_feature(context.event.route.id, context.config, label, svelte_config.kit.adapter);
|
||||
};
|
||||
|
||||
const fetch = globalThis.fetch;
|
||||
globalThis.fetch = (info, init) => {
|
||||
if (typeof info === 'string' && !SCHEME.test(info)) {
|
||||
throw new Error(
|
||||
`Cannot use relative URL (${info}) with global fetch — use \`event.fetch\` instead: https://svelte.dev/docs/kit/web-standards#fetch-apis`
|
||||
);
|
||||
}
|
||||
|
||||
return fetch(info, init);
|
||||
};
|
||||
|
||||
sync.init(svelte_config, vite_config.mode);
|
||||
|
||||
/** @type {import('types').ManifestData} */
|
||||
let manifest_data;
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
let manifest;
|
||||
|
||||
/** @type {Error | null} */
|
||||
let manifest_error = null;
|
||||
|
||||
/** @param {string} url */
|
||||
async function loud_ssr_load_module(url) {
|
||||
try {
|
||||
return await vite.ssrLoadModule(url, { fixStacktrace: true });
|
||||
} catch (/** @type {any} */ err) {
|
||||
const msg = buildErrorMessage(err, [colors.red(`Internal server error: ${err.message}`)]);
|
||||
|
||||
if (!vite.config.logger.hasErrorLogged(err)) {
|
||||
vite.config.logger.error(msg, { error: err });
|
||||
}
|
||||
|
||||
vite.ws.send({
|
||||
type: 'error',
|
||||
err: {
|
||||
...err,
|
||||
// these properties are non-enumerable and will
|
||||
// not be serialized unless we explicitly include them
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
}
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
async function resolve(id) {
|
||||
const url = id.startsWith('..') ? to_fs(path.posix.resolve(id)) : `/${id}`;
|
||||
|
||||
const module = await loud_ssr_load_module(url);
|
||||
|
||||
const module_node = await vite.moduleGraph.getModuleByUrl(url);
|
||||
if (!module_node) throw new Error(`Could not find node for ${url}`);
|
||||
|
||||
return { module, module_node, url };
|
||||
}
|
||||
|
||||
function update_manifest() {
|
||||
try {
|
||||
({ manifest_data } = sync.create(svelte_config));
|
||||
|
||||
if (manifest_error) {
|
||||
manifest_error = null;
|
||||
vite.ws.send({ type: 'full-reload' });
|
||||
}
|
||||
} catch (error) {
|
||||
manifest_error = /** @type {Error} */ (error);
|
||||
|
||||
console.error(colors.bold().red(manifest_error.message));
|
||||
vite.ws.send({
|
||||
type: 'error',
|
||||
err: {
|
||||
message: manifest_error.message ?? 'Invalid routes',
|
||||
stack: ''
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
manifest = {
|
||||
appDir: svelte_config.kit.appDir,
|
||||
appPath: svelte_config.kit.appDir,
|
||||
assets: new Set(manifest_data.assets.map((asset) => asset.file)),
|
||||
mimeTypes: get_mime_lookup(manifest_data),
|
||||
_: {
|
||||
client: {
|
||||
start: `${runtime_base}/client/entry.js`,
|
||||
app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`,
|
||||
imports: [],
|
||||
stylesheets: [],
|
||||
fonts: [],
|
||||
uses_env_dynamic_public: true,
|
||||
nodes:
|
||||
svelte_config.kit.router.resolution === 'client'
|
||||
? undefined
|
||||
: manifest_data.nodes.map((node, i) => {
|
||||
if (node.component || node.universal) {
|
||||
return `${svelte_config.kit.paths.base}${to_fs(svelte_config.kit.outDir)}/generated/client/nodes/${i}.js`;
|
||||
}
|
||||
}),
|
||||
// `css` is not necessary in dev, as the JS file from `nodes` will reference the CSS file
|
||||
routes:
|
||||
svelte_config.kit.router.resolution === 'client'
|
||||
? undefined
|
||||
: compact(
|
||||
manifest_data.routes.map((route) => {
|
||||
if (!route.page) return;
|
||||
|
||||
return {
|
||||
id: route.id,
|
||||
pattern: route.pattern,
|
||||
params: route.params,
|
||||
layouts: route.page.layouts.map((l) =>
|
||||
l !== undefined ? [!!manifest_data.nodes[l].server, l] : undefined
|
||||
),
|
||||
errors: route.page.errors,
|
||||
leaf: [!!manifest_data.nodes[route.page.leaf].server, route.page.leaf]
|
||||
};
|
||||
})
|
||||
)
|
||||
},
|
||||
server_assets: new Proxy(
|
||||
{},
|
||||
{
|
||||
has: (_, /** @type {string} */ file) => fs.existsSync(from_fs(file)),
|
||||
get: (_, /** @type {string} */ file) => fs.statSync(from_fs(file)).size
|
||||
}
|
||||
),
|
||||
nodes: manifest_data.nodes.map((node, index) => {
|
||||
return async () => {
|
||||
/** @type {import('types').SSRNode} */
|
||||
const result = {};
|
||||
result.index = index;
|
||||
result.universal_id = node.universal;
|
||||
result.server_id = node.server;
|
||||
|
||||
// these are unused in dev, but it's easier to include them
|
||||
result.imports = [];
|
||||
result.stylesheets = [];
|
||||
result.fonts = [];
|
||||
|
||||
/** @type {import('vite').ModuleNode[]} */
|
||||
const module_nodes = [];
|
||||
|
||||
if (node.component) {
|
||||
result.component = async () => {
|
||||
const { module_node, module } = await resolve(
|
||||
/** @type {string} */ (node.component)
|
||||
);
|
||||
|
||||
module_nodes.push(module_node);
|
||||
|
||||
return module.default;
|
||||
};
|
||||
}
|
||||
|
||||
if (node.universal) {
|
||||
if (node.page_options?.ssr === false) {
|
||||
result.universal = node.page_options;
|
||||
} else {
|
||||
// TODO: explain why the file was loaded on the server if we fail to load it
|
||||
const { module, module_node } = await resolve(node.universal);
|
||||
module_nodes.push(module_node);
|
||||
result.universal = module;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.server) {
|
||||
const { module } = await resolve(node.server);
|
||||
result.server = module;
|
||||
}
|
||||
|
||||
// in dev we inline all styles to avoid FOUC. this gets populated lazily so that
|
||||
// components/stylesheets loaded via import() during `load` are included
|
||||
result.inline_styles = async () => {
|
||||
/** @type {Set<import('vite').ModuleNode | import('vite').EnvironmentModuleNode>} */
|
||||
const deps = new Set();
|
||||
|
||||
for (const module_node of module_nodes) {
|
||||
await find_deps(vite, module_node, deps);
|
||||
}
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const styles = {};
|
||||
|
||||
for (const dep of deps) {
|
||||
if (isCSSRequest(dep.url) && !vite_css_query_regex.test(dep.url)) {
|
||||
const inlineCssUrl = dep.url.includes('?')
|
||||
? dep.url.replace('?', '?inline&')
|
||||
: dep.url + '?inline';
|
||||
try {
|
||||
const mod = await vite.ssrLoadModule(inlineCssUrl);
|
||||
styles[dep.url] = 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 styles;
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
}),
|
||||
prerendered_routes: new Set(),
|
||||
get remotes() {
|
||||
return Object.fromEntries(
|
||||
get_remotes().map((remote) => [
|
||||
remote.hash,
|
||||
() => vite.ssrLoadModule(remote.file).then((module) => ({ default: module }))
|
||||
])
|
||||
);
|
||||
},
|
||||
routes: compact(
|
||||
manifest_data.routes.map((route) => {
|
||||
if (!route.page && !route.endpoint) return null;
|
||||
|
||||
const endpoint = route.endpoint;
|
||||
|
||||
return {
|
||||
id: route.id,
|
||||
pattern: route.pattern,
|
||||
params: route.params,
|
||||
page: route.page,
|
||||
endpoint: endpoint
|
||||
? async () => {
|
||||
const url = path.resolve(cwd, endpoint.file);
|
||||
return await loud_ssr_load_module(url);
|
||||
}
|
||||
: null,
|
||||
endpoint_id: endpoint?.file
|
||||
};
|
||||
})
|
||||
),
|
||||
matchers: async () => {
|
||||
/** @type {Record<string, import('@sveltejs/kit').ParamMatcher>} */
|
||||
const matchers = {};
|
||||
|
||||
for (const key in manifest_data.matchers) {
|
||||
const file = manifest_data.matchers[key];
|
||||
const url = path.resolve(cwd, file);
|
||||
const module = await vite.ssrLoadModule(url, { fixStacktrace: true });
|
||||
|
||||
if (module.match) {
|
||||
matchers[key] = module.match;
|
||||
} else {
|
||||
throw new Error(`${file} does not export a \`match\` function`);
|
||||
}
|
||||
}
|
||||
|
||||
return matchers;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {Error} error */
|
||||
function fix_stack_trace(error) {
|
||||
try {
|
||||
vite.ssrFixStacktrace(error);
|
||||
} catch {
|
||||
// ssrFixStacktrace can fail on StackBlitz web containers and we don't know why
|
||||
// by ignoring it the line numbers are wrong, but at least we can show the error
|
||||
}
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
update_manifest();
|
||||
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {(file: string) => void} cb
|
||||
*/
|
||||
const watch = (event, cb) => {
|
||||
vite.watcher.on(event, (file) => {
|
||||
if (
|
||||
file.startsWith(svelte_config.kit.files.routes + path.sep) ||
|
||||
file.startsWith(svelte_config.kit.files.params + path.sep) ||
|
||||
svelte_config.kit.moduleExtensions.some((ext) => file.endsWith(`.remote${ext}`)) ||
|
||||
// in contrast to server hooks, client hooks are written to the client manifest
|
||||
// and therefore need rebuilding when they are added/removed
|
||||
file.startsWith(svelte_config.kit.files.hooks.client)
|
||||
) {
|
||||
cb(file);
|
||||
}
|
||||
});
|
||||
};
|
||||
/** @type {NodeJS.Timeout | null } */
|
||||
let timeout = null;
|
||||
/** @param {() => void} to_run */
|
||||
const debounce = (to_run) => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
to_run();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// flag to skip watchers if server is already restarting
|
||||
let restarting = false;
|
||||
|
||||
// Debounce add/unlink events because in case of folder deletion or moves
|
||||
// they fire in rapid succession, causing needless invocations.
|
||||
// These watchers only run for routes, param matchers, and client hooks.
|
||||
watch('add', () => debounce(update_manifest));
|
||||
watch('unlink', () => debounce(update_manifest));
|
||||
watch('change', (file) => {
|
||||
// Don't run for a single file if the whole manifest is about to get updated
|
||||
// Unless it's a file where the trailing slash page option might have changed
|
||||
if (timeout || restarting || !/\+(page|layout|server).*$/.test(file)) return;
|
||||
sync.update(svelte_config, manifest_data, file);
|
||||
});
|
||||
|
||||
const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files;
|
||||
|
||||
// vite client only executes a full reload if the triggering html file path is index.html
|
||||
// kit defaults to src/app.html, so unless user changed that to index.html
|
||||
// send the vite client a full-reload event without path being set
|
||||
if (appTemplate !== 'index.html') {
|
||||
vite.watcher.on('change', (file) => {
|
||||
if (file === appTemplate && !restarting) {
|
||||
vite.ws.send({ type: 'full-reload' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
vite.watcher.on('all', (_, file) => {
|
||||
if (
|
||||
file === appTemplate ||
|
||||
file === errorTemplate ||
|
||||
file.startsWith(serviceWorker) ||
|
||||
file.startsWith(hooks.server)
|
||||
) {
|
||||
sync.server(svelte_config);
|
||||
}
|
||||
});
|
||||
|
||||
vite.watcher.on('change', async (file) => {
|
||||
// changing the svelte config requires restarting the dev server
|
||||
// the config is only read on start and passed on to vite-plugin-svelte
|
||||
// which needs up-to-date values to operate correctly
|
||||
if (file.match(/[/\\]svelte\.config\.[jt]s$/)) {
|
||||
console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`);
|
||||
restarting = true;
|
||||
await vite.restart();
|
||||
}
|
||||
});
|
||||
|
||||
const assets = svelte_config.kit.paths.assets ? SVELTE_KIT_ASSETS : svelte_config.kit.paths.base;
|
||||
const asset_server = sirv(svelte_config.kit.files.assets, {
|
||||
dev: true,
|
||||
etag: true,
|
||||
maxAge: 0,
|
||||
extensions: [],
|
||||
setHeaders: (res) => {
|
||||
res.setHeader('access-control-allow-origin', '*');
|
||||
}
|
||||
});
|
||||
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
const base = `${vite.config.server.https ? 'https' : 'http'}://${
|
||||
req.headers[':authority'] || req.headers.host
|
||||
}`;
|
||||
|
||||
const decoded = decodeURI(new URL(base + req.url).pathname);
|
||||
|
||||
if (decoded.startsWith(assets)) {
|
||||
const pathname = decoded.slice(assets.length);
|
||||
const file = svelte_config.kit.files.assets + pathname;
|
||||
|
||||
if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) {
|
||||
if (has_correct_case(file, svelte_config.kit.files.assets)) {
|
||||
req.url = encodeURI(pathname); // don't need query/hash
|
||||
asset_server(req, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, '');
|
||||
const emulator = await svelte_config.kit.adapter?.emulate?.();
|
||||
|
||||
return () => {
|
||||
const serve_static_middleware = vite.middlewares.stack.find(
|
||||
(middleware) =>
|
||||
/** @type {function} */ (middleware.handle).name === 'viteServeStaticMiddleware'
|
||||
);
|
||||
|
||||
// Vite will give a 403 on URLs like /test, /static, and /package.json preventing us from
|
||||
// serving routes with those names. See https://github.com/vitejs/vite/issues/7363
|
||||
remove_static_middlewares(vite.middlewares);
|
||||
|
||||
vite.middlewares.use(async (req, res) => {
|
||||
// Vite's base middleware strips out the base path. Restore it
|
||||
const original_url = req.url;
|
||||
req.url = req.originalUrl;
|
||||
try {
|
||||
const base = `${vite.config.server.https ? 'https' : 'http'}://${
|
||||
req.headers[':authority'] || req.headers.host
|
||||
}`;
|
||||
|
||||
const decoded = decodeURI(new URL(base + req.url).pathname);
|
||||
const file = posixify(path.resolve(decoded.slice(svelte_config.kit.paths.base.length + 1)));
|
||||
const is_file = fs.existsSync(file) && !fs.statSync(file).isDirectory();
|
||||
const allowed =
|
||||
!vite_config.server.fs.strict ||
|
||||
vite_config.server.fs.allow.some((dir) => file.startsWith(dir));
|
||||
|
||||
if (is_file && allowed) {
|
||||
req.url = original_url;
|
||||
// @ts-expect-error
|
||||
serve_static_middleware.handle(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decoded.startsWith(svelte_config.kit.paths.base)) {
|
||||
return not_found(req, res, svelte_config.kit.paths.base);
|
||||
}
|
||||
|
||||
if (decoded === svelte_config.kit.paths.base + '/service-worker.js') {
|
||||
const resolved = resolve_entry(svelte_config.kit.files.serviceWorker);
|
||||
|
||||
if (resolved) {
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/javascript'
|
||||
});
|
||||
res.end(`import '${svelte_config.kit.paths.base}${to_fs(resolved)}';`);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved_instrumentation = resolve_entry(
|
||||
path.join(svelte_config.kit.files.src, 'instrumentation.server')
|
||||
);
|
||||
|
||||
if (resolved_instrumentation) {
|
||||
await vite.ssrLoadModule(resolved_instrumentation);
|
||||
}
|
||||
|
||||
// we have to import `Server` before calling `set_assets`
|
||||
const { Server } = /** @type {import('types').ServerModule} */ (
|
||||
await vite.ssrLoadModule(`${runtime_base}/server/index.js`, { fixStacktrace: true })
|
||||
);
|
||||
|
||||
const { set_fix_stack_trace } = await vite.ssrLoadModule(
|
||||
`${runtime_base}/shared-server.js`
|
||||
);
|
||||
set_fix_stack_trace(fix_stack_trace);
|
||||
|
||||
const { set_assets } = await vite.ssrLoadModule('$app/paths/internal/server');
|
||||
set_assets(assets);
|
||||
|
||||
const server = new Server(manifest);
|
||||
|
||||
await server.init({
|
||||
env,
|
||||
read: (file) => createReadableStream(from_fs(file))
|
||||
});
|
||||
|
||||
const request = await getRequest({
|
||||
base,
|
||||
request: req
|
||||
});
|
||||
|
||||
if (manifest_error) {
|
||||
console.error(colors.bold().red(manifest_error.message));
|
||||
|
||||
const error_page = load_error_page(svelte_config);
|
||||
|
||||
/** @param {{ status: number; message: string }} opts */
|
||||
const error_template = ({ status, message }) => {
|
||||
return error_page
|
||||
.replace(/%sveltekit\.status%/g, String(status))
|
||||
.replace(/%sveltekit\.error\.message%/g, escape_html(message));
|
||||
};
|
||||
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
});
|
||||
res.end(
|
||||
error_template({ status: 500, message: manifest_error.message ?? 'Invalid routes' })
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const rendered = await server.respond(request, {
|
||||
getClientAddress: () => {
|
||||
const { remoteAddress } = req.socket;
|
||||
if (remoteAddress) return remoteAddress;
|
||||
throw new Error('Could not determine clientAddress');
|
||||
},
|
||||
read: (file) => {
|
||||
if (file in manifest._.server_assets) {
|
||||
return fs.readFileSync(from_fs(file));
|
||||
}
|
||||
|
||||
return fs.readFileSync(path.join(svelte_config.kit.files.assets, file));
|
||||
},
|
||||
before_handle: (event, config, prerender) => {
|
||||
async_local_storage.enterWith({ event, config, prerender });
|
||||
},
|
||||
emulator
|
||||
});
|
||||
|
||||
if (rendered.status === 404) {
|
||||
// @ts-expect-error
|
||||
serve_static_middleware.handle(req, res, () => {
|
||||
void setResponse(res, rendered);
|
||||
});
|
||||
} else {
|
||||
void setResponse(res, rendered);
|
||||
}
|
||||
} catch (e) {
|
||||
const error = coalesce_to_error(e);
|
||||
res.statusCode = 500;
|
||||
res.end(fix_stack_trace(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('connect').Server} server
|
||||
*/
|
||||
function remove_static_middlewares(server) {
|
||||
const static_middlewares = ['viteServeStaticMiddleware', 'viteServePublicMiddleware'];
|
||||
for (let i = server.stack.length - 1; i > 0; i--) {
|
||||
// @ts-expect-error using internals
|
||||
if (static_middlewares.includes(server.stack[i].handle.name)) {
|
||||
server.stack.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vite').ViteDevServer} vite
|
||||
* @param {import('vite').ModuleNode | import('vite').EnvironmentModuleNode} node
|
||||
* @param {Set<import('vite').ModuleNode | import('vite').EnvironmentModuleNode>} deps
|
||||
*/
|
||||
async function find_deps(vite, node, deps) {
|
||||
// since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous.
|
||||
// instead of using `await`, we resolve all branches in parallel.
|
||||
/** @type {Promise<void>[]} */
|
||||
const branches = [];
|
||||
|
||||
/** @param {import('vite').ModuleNode | import('vite').EnvironmentModuleNode} node */
|
||||
async function add(node) {
|
||||
if (!deps.has(node)) {
|
||||
deps.add(node);
|
||||
await find_deps(vite, node, deps);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} url */
|
||||
async function add_by_url(url) {
|
||||
const node = await get_server_module_by_url(vite, url);
|
||||
|
||||
if (node) {
|
||||
await add(node);
|
||||
}
|
||||
}
|
||||
|
||||
const transform_result =
|
||||
/** @type {import('vite').ModuleNode} */ (node).ssrTransformResult || node.transformResult;
|
||||
|
||||
if (transform_result) {
|
||||
if (transform_result.deps) {
|
||||
transform_result.deps.forEach((url) => branches.push(add_by_url(url)));
|
||||
}
|
||||
|
||||
if (transform_result.dynamicDeps) {
|
||||
transform_result.dynamicDeps.forEach((url) => branches.push(add_by_url(url)));
|
||||
}
|
||||
} else {
|
||||
node.importedModules.forEach((node) => branches.push(add(node)));
|
||||
}
|
||||
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vite').ViteDevServer} vite
|
||||
* @param {string} url
|
||||
*/
|
||||
function get_server_module_by_url(vite, url) {
|
||||
return vite.environments
|
||||
? vite.environments.ssr.moduleGraph.getModuleByUrl(url)
|
||||
: vite.moduleGraph.getModuleByUrl(url, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a file is being requested with the correct case,
|
||||
* to ensure consistent behaviour between dev and prod and across
|
||||
* operating systems. Note that we can't use realpath here,
|
||||
* because we don't want to follow symlinks
|
||||
* @param {string} file
|
||||
* @param {string} assets
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function has_correct_case(file, assets) {
|
||||
if (file === assets) return true;
|
||||
|
||||
const parent = path.dirname(file);
|
||||
|
||||
if (fs.readdirSync(parent).includes(path.basename(file))) {
|
||||
return has_correct_case(parent, assets);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+1464
File diff suppressed because it is too large
Load Diff
+16
@@ -0,0 +1,16 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { posixify } from '../../utils/filesystem.js';
|
||||
|
||||
export const env_static_private = '\0virtual:env/static/private';
|
||||
export const env_static_public = '\0virtual:env/static/public';
|
||||
export const env_dynamic_private = '\0virtual:env/dynamic/private';
|
||||
export const env_dynamic_public = '\0virtual:env/dynamic/public';
|
||||
|
||||
export const service_worker = '\0virtual:service-worker';
|
||||
|
||||
export const sveltekit_environment = '\0virtual:__sveltekit/environment';
|
||||
export const sveltekit_server = '\0virtual:__sveltekit/server';
|
||||
|
||||
export const app_server = posixify(
|
||||
fileURLToPath(new URL('../../runtime/app/server/index.js', import.meta.url))
|
||||
);
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import fs from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { lookup } from 'mrmime';
|
||||
import sirv from 'sirv';
|
||||
import { loadEnv, normalizePath } from 'vite';
|
||||
import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
|
||||
import { installPolyfills } from '../../../exports/node/polyfills.js';
|
||||
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
|
||||
import { not_found } from '../utils.js';
|
||||
|
||||
/** @typedef {import('http').IncomingMessage} Req */
|
||||
/** @typedef {import('http').ServerResponse} Res */
|
||||
/** @typedef {(req: Req, res: Res, next: () => void) => void} Handler */
|
||||
|
||||
/**
|
||||
* @param {import('vite').PreviewServer} vite
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
* @param {import('types').ValidatedConfig} svelte_config
|
||||
*/
|
||||
export async function preview(vite, vite_config, svelte_config) {
|
||||
installPolyfills();
|
||||
|
||||
const { paths } = svelte_config.kit;
|
||||
const base = paths.base;
|
||||
const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base;
|
||||
|
||||
const protocol = vite_config.preview.https ? 'https' : 'http';
|
||||
|
||||
const etag = `"${Date.now()}"`;
|
||||
|
||||
const dir = join(svelte_config.kit.outDir, 'output/server');
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
throw new Error(`Server files not found at ${dir}, did you run \`build\` first?`);
|
||||
}
|
||||
|
||||
const instrumentation = join(dir, 'instrumentation.server.js');
|
||||
if (fs.existsSync(instrumentation)) {
|
||||
await import(pathToFileURL(instrumentation).href);
|
||||
}
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const { set_assets } = await import(pathToFileURL(join(dir, 'internal.js')).href);
|
||||
|
||||
/** @type {import('types').ServerModule} */
|
||||
const { Server } = await import(pathToFileURL(join(dir, 'index.js')).href);
|
||||
|
||||
const { manifest } = await import(pathToFileURL(join(dir, 'manifest.js')).href);
|
||||
|
||||
set_assets(assets);
|
||||
|
||||
const server = new Server(manifest);
|
||||
await server.init({
|
||||
env: loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''),
|
||||
read: (file) => createReadableStream(`${dir}/${file}`)
|
||||
});
|
||||
|
||||
const emulator = await svelte_config.kit.adapter?.emulate?.();
|
||||
|
||||
return () => {
|
||||
// Remove the base middleware. It screws with the URL.
|
||||
// It also only lets through requests beginning with the base path, so that requests beginning
|
||||
// with the assets URL never reach us. We could serve assets separately before the base
|
||||
// middleware, but we'd need that to occur after the compression and cors middlewares, so would
|
||||
// need to insert it manually into the stack, which would be at least as bad as doing this.
|
||||
for (let i = vite.middlewares.stack.length - 1; i > 0; i--) {
|
||||
// @ts-expect-error using internals
|
||||
if (vite.middlewares.stack[i].handle.name === 'viteBaseMiddleware') {
|
||||
vite.middlewares.stack.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// generated client assets and the contents of `static`
|
||||
vite.middlewares.use(
|
||||
scoped(
|
||||
assets,
|
||||
sirv(join(svelte_config.kit.outDir, 'output/client'), {
|
||||
setHeaders: (res, pathname) => {
|
||||
// only apply to immutable directory, not e.g. version.json
|
||||
if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) {
|
||||
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
const original_url = /** @type {string} */ (req.url);
|
||||
const { pathname, search } = new URL(original_url, 'http://dummy');
|
||||
|
||||
// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
|
||||
// regardless of the `trailingSlash` route option
|
||||
if (base.length > 1 && pathname === base) {
|
||||
let location = base + '/';
|
||||
if (search) location += search;
|
||||
res.writeHead(307, {
|
||||
location
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.startsWith(base)) {
|
||||
next();
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
not_found(req, res, base);
|
||||
}
|
||||
});
|
||||
|
||||
// prerendered dependencies
|
||||
vite.middlewares.use(
|
||||
scoped(base, mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies')))
|
||||
);
|
||||
|
||||
// prerendered pages (we can't just use sirv because we need to
|
||||
// preserve the correct trailingSlash behaviour)
|
||||
vite.middlewares.use(
|
||||
scoped(base, (req, res, next) => {
|
||||
let if_none_match_value = req.headers['if-none-match'];
|
||||
|
||||
if (if_none_match_value?.startsWith('W/"')) {
|
||||
if_none_match_value = if_none_match_value.substring(2);
|
||||
}
|
||||
|
||||
if (if_none_match_value === etag) {
|
||||
res.statusCode = 304;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy');
|
||||
|
||||
const dir = pathname.startsWith(`/${svelte_config.kit.appDir}/remote/`) ? 'data' : 'pages';
|
||||
|
||||
let filename = normalizePath(
|
||||
join(svelte_config.kit.outDir, `output/prerendered/${dir}` + pathname)
|
||||
);
|
||||
|
||||
try {
|
||||
filename = decodeURI(filename);
|
||||
} catch {
|
||||
// malformed URI
|
||||
}
|
||||
|
||||
let prerendered = is_file(filename);
|
||||
|
||||
if (!prerendered) {
|
||||
const has_trailing_slash = pathname.endsWith('/');
|
||||
const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`;
|
||||
|
||||
/** @type {string | undefined} */
|
||||
let redirect;
|
||||
|
||||
if (is_file(html_filename)) {
|
||||
filename = html_filename;
|
||||
prerendered = true;
|
||||
} else if (has_trailing_slash) {
|
||||
if (is_file(filename.slice(0, -1) + '.html')) {
|
||||
redirect = pathname.slice(0, -1);
|
||||
}
|
||||
} else if (is_file(filename + '/index.html')) {
|
||||
redirect = pathname + '/';
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
if (search) redirect += search;
|
||||
res.writeHead(307, {
|
||||
location: redirect
|
||||
});
|
||||
|
||||
res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (prerendered) {
|
||||
res.writeHead(200, {
|
||||
'content-type': lookup(pathname) || 'text/html',
|
||||
etag
|
||||
});
|
||||
|
||||
fs.createReadStream(filename).pipe(res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// SSR
|
||||
vite.middlewares.use(async (req, res) => {
|
||||
const host = req.headers[':authority'] || req.headers.host;
|
||||
|
||||
const request = await getRequest({
|
||||
base: `${protocol}://${host}`,
|
||||
request: req
|
||||
});
|
||||
|
||||
await setResponse(
|
||||
res,
|
||||
await server.respond(request, {
|
||||
getClientAddress: () => {
|
||||
const { remoteAddress } = req.socket;
|
||||
if (remoteAddress) return remoteAddress;
|
||||
throw new Error('Could not determine clientAddress');
|
||||
},
|
||||
read: (file) => {
|
||||
if (file in manifest._.server_assets) {
|
||||
return fs.readFileSync(join(dir, file));
|
||||
}
|
||||
|
||||
return fs.readFileSync(join(svelte_config.kit.files.assets, file));
|
||||
},
|
||||
emulator
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @returns {Handler}
|
||||
*/
|
||||
const mutable = (dir) =>
|
||||
fs.existsSync(dir)
|
||||
? sirv(dir, {
|
||||
etag: true,
|
||||
maxAge: 0
|
||||
})
|
||||
: (_req, _res, next) => next();
|
||||
|
||||
/**
|
||||
* @param {string} scope
|
||||
* @param {Handler} handler
|
||||
* @returns {Handler}
|
||||
*/
|
||||
function scoped(scope, handler) {
|
||||
if (scope === '') return handler;
|
||||
|
||||
return (req, res, next) => {
|
||||
if (req.url?.startsWith(scope)) {
|
||||
const original_url = req.url;
|
||||
req.url = req.url.slice(scope.length);
|
||||
handler(req, res, () => {
|
||||
req.url = original_url;
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {string} path */
|
||||
function is_file(path) {
|
||||
return fs.existsSync(path) && !fs.statSync(path).isDirectory();
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
||||
import { Parser } from 'acorn';
|
||||
import { read } from '../../../utils/filesystem.js';
|
||||
|
||||
const valid_page_options_array = /** @type {const} */ ([
|
||||
'ssr',
|
||||
'prerender',
|
||||
'csr',
|
||||
'trailingSlash',
|
||||
'config',
|
||||
'entries',
|
||||
'load'
|
||||
]);
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const valid_page_options = new Set(valid_page_options_array);
|
||||
|
||||
/** @typedef {typeof valid_page_options_array[number]} ValidPageOption */
|
||||
/** @typedef {Partial<Record<ValidPageOption, any>>} PageOptions */
|
||||
|
||||
const skip_parsing_regex = new RegExp(
|
||||
`${Array.from(valid_page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)`
|
||||
);
|
||||
|
||||
const parser = Parser.extend(tsPlugin());
|
||||
|
||||
/**
|
||||
* Collects page options from a +page.js/+layout.js file, ignoring reassignments
|
||||
* and using the declared value (except for load functions, for which the value is `true`).
|
||||
* Returns `null` if any export is too difficult to analyse.
|
||||
* @param {string} filename The name of the file to report when an error occurs
|
||||
* @param {string} input
|
||||
* @returns {PageOptions | null}
|
||||
*/
|
||||
export function statically_analyse_page_options(filename, input) {
|
||||
// if there's a chance there are no page exports or an unparseable
|
||||
// export all declaration, then we can skip the AST parsing which is expensive
|
||||
if (!skip_parsing_regex.test(input)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const source = parser.parse(input, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest'
|
||||
});
|
||||
|
||||
/** @type {Map<string, import('acorn').Literal['value']>} */
|
||||
const page_options = new Map();
|
||||
|
||||
for (const statement of source.body) {
|
||||
// ignore export all declarations with aliases that are not page options
|
||||
if (
|
||||
statement.type === 'ExportAllDeclaration' &&
|
||||
statement.exported &&
|
||||
!valid_page_options.has(get_name(statement.exported))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
statement.type === 'ExportDefaultDeclaration' ||
|
||||
statement.type === 'ExportAllDeclaration'
|
||||
) {
|
||||
return null;
|
||||
} else if (statement.type !== 'ExportNamedDeclaration') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.specifiers.length) {
|
||||
/** @type {Map<string, string>} */
|
||||
const export_specifiers = new Map();
|
||||
for (const specifier of statement.specifiers) {
|
||||
const exported_name = get_name(specifier.exported);
|
||||
if (!valid_page_options.has(exported_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export_specifiers.set(get_name(specifier.local), exported_name);
|
||||
}
|
||||
|
||||
for (const statement of source.body) {
|
||||
switch (statement.type) {
|
||||
case 'ImportDeclaration': {
|
||||
for (const import_specifier of statement.specifiers) {
|
||||
if (export_specifiers.has(import_specifier.local.name)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'VariableDeclaration':
|
||||
case 'FunctionDeclaration':
|
||||
case 'ClassDeclaration': {
|
||||
const declaration =
|
||||
statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
|
||||
|
||||
if (!declaration) {
|
||||
break;
|
||||
}
|
||||
|
||||
// class and function declarations
|
||||
if (declaration.type !== 'VariableDeclaration') {
|
||||
if (export_specifiers.has(declaration.id.name)) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for (const variable_declarator of declaration.declarations) {
|
||||
if (
|
||||
variable_declarator.id.type !== 'Identifier' ||
|
||||
!export_specifiers.has(variable_declarator.id.name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variable_declarator.init?.type === 'Literal') {
|
||||
page_options.set(
|
||||
/** @type {string} */ (export_specifiers.get(variable_declarator.id.name)),
|
||||
variable_declarator.init.value
|
||||
);
|
||||
export_specifiers.delete(variable_declarator.id.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
|
||||
if (variable_declarator.id.name === 'load') {
|
||||
page_options.set('load', null);
|
||||
export_specifiers.delete('load');
|
||||
continue;
|
||||
}
|
||||
|
||||
// references a declaration we can't easily evaluate statically
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// there were some export specifiers that we couldn't resolve
|
||||
if (export_specifiers.size) {
|
||||
return null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statement.declaration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// class and function declarations
|
||||
if (statement.declaration.type !== 'VariableDeclaration') {
|
||||
if (valid_page_options.has(statement.declaration.id.name)) {
|
||||
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
|
||||
if (statement.declaration.id.name === 'load') {
|
||||
page_options.set('load', null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const declaration of statement.declaration.declarations) {
|
||||
if (declaration.id.type !== 'Identifier') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!valid_page_options.has(declaration.id.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (declaration.init?.type === 'Literal') {
|
||||
page_options.set(declaration.id.name, declaration.init.value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
|
||||
if (declaration.id.name === 'load') {
|
||||
page_options.set('load', null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// references a declaration we can't easily evaluate statically
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(page_options);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
error.message = `Failed to statically analyse page options for ${filename}. ${error.message}`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('acorn').Identifier | import('acorn').Literal} node
|
||||
* @returns {string}
|
||||
*/
|
||||
function get_name(node) {
|
||||
return node.type === 'Identifier' ? node.name : /** @type {string} */ (node.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and statically analyses a file for page options
|
||||
* @param {string} filepath
|
||||
* @returns {PageOptions | null} Returns the page options for the file or `null` if unanalysable
|
||||
*/
|
||||
export function get_page_options(filepath) {
|
||||
try {
|
||||
const input = read(filepath);
|
||||
const page_options = statically_analyse_page_options(filepath, input);
|
||||
if (page_options === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return page_options;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function create_node_analyser() {
|
||||
const static_exports = new Map();
|
||||
|
||||
/**
|
||||
* @param {string | undefined} key
|
||||
* @param {PageOptions | null} page_options
|
||||
*/
|
||||
const cache = (key, page_options) => {
|
||||
if (key) static_exports.set(key, { page_options, children: [] });
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the final page options (may include load function as `load: null`; special case) for a node (if possible). Otherwise, returns `null`.
|
||||
* @param {import('types').PageNode} node
|
||||
* @returns {PageOptions | null}
|
||||
*/
|
||||
const crawl = (node) => {
|
||||
const key = node.universal || node.server;
|
||||
if (key && static_exports.has(key)) {
|
||||
return { ...static_exports.get(key)?.page_options };
|
||||
}
|
||||
|
||||
/** @type {PageOptions} */
|
||||
let page_options = {};
|
||||
|
||||
if (node.parent) {
|
||||
const parent_options = crawl(node.parent);
|
||||
|
||||
const parent_key = node.parent.universal || node.parent.server;
|
||||
if (key && parent_key) {
|
||||
static_exports.get(parent_key)?.children.push(key);
|
||||
}
|
||||
|
||||
if (parent_options === null) {
|
||||
// if the parent cannot be analysed, we can't know what page options
|
||||
// the child node inherits, so we also mark it as unanalysable
|
||||
cache(key, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
page_options = { ...parent_options };
|
||||
}
|
||||
|
||||
if (node.server) {
|
||||
const server_page_options = get_page_options(node.server);
|
||||
if (server_page_options === null) {
|
||||
cache(key, null);
|
||||
return null;
|
||||
}
|
||||
page_options = { ...page_options, ...server_page_options };
|
||||
}
|
||||
|
||||
if (node.universal) {
|
||||
const universal_page_options = get_page_options(node.universal);
|
||||
if (universal_page_options === null) {
|
||||
cache(key, null);
|
||||
return null;
|
||||
}
|
||||
page_options = { ...page_options, ...universal_page_options };
|
||||
}
|
||||
|
||||
cache(key, page_options);
|
||||
|
||||
return page_options;
|
||||
};
|
||||
|
||||
return {
|
||||
get_page_options: crawl
|
||||
};
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Check if content has children rendering (slot, @render, or children prop forwarding)
|
||||
* @param {string} content - The markup content
|
||||
* @param {boolean} is_svelte_5_plus - Whether the project uses Svelte 5+
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function has_children(content, is_svelte_5_plus) {
|
||||
return (
|
||||
content.includes('<slot') ||
|
||||
(is_svelte_5_plus &&
|
||||
(content.includes('{@render') ||
|
||||
// children may be forwarded to a child component as a prop
|
||||
content.includes('{children}') ||
|
||||
content.includes('children={')))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a match position is within a comment or a string
|
||||
* @param {string} content - The full content
|
||||
* @param {number} match_index - The index where the match starts
|
||||
* @returns {boolean} - True if the match is within a comment
|
||||
*/
|
||||
export function should_ignore(content, match_index) {
|
||||
// Track if we're inside different types of quotes and comments
|
||||
let in_single_quote = false;
|
||||
let in_double_quote = false;
|
||||
let in_template_literal = false;
|
||||
let in_single_line_comment = false;
|
||||
let in_multi_line_comment = false;
|
||||
let in_html_comment = false;
|
||||
|
||||
for (let i = 0; i < match_index; i++) {
|
||||
const char = content[i];
|
||||
const next_two = content.slice(i, i + 2);
|
||||
const next_four = content.slice(i, i + 4);
|
||||
|
||||
// Handle end of single line comment
|
||||
if (in_single_line_comment && char === '\n') {
|
||||
in_single_line_comment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle end of multi-line comment
|
||||
if (in_multi_line_comment && next_two === '*/') {
|
||||
in_multi_line_comment = false;
|
||||
i++; // Skip the '/' part
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle end of HTML comment
|
||||
if (in_html_comment && content.slice(i, i + 3) === '-->') {
|
||||
in_html_comment = false;
|
||||
i += 2; // Skip the '-->' part
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in any comment, skip processing
|
||||
if (in_single_line_comment || in_multi_line_comment || in_html_comment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle escape sequences in strings
|
||||
if ((in_single_quote || in_double_quote || in_template_literal) && char === '\\') {
|
||||
i++; // Skip the escaped character
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle string boundaries
|
||||
if (!in_double_quote && !in_template_literal && char === "'") {
|
||||
in_single_quote = !in_single_quote;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_single_quote && !in_template_literal && char === '"') {
|
||||
in_double_quote = !in_double_quote;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_single_quote && !in_double_quote && char === '`') {
|
||||
in_template_literal = !in_template_literal;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're inside any string, don't process comment delimiters
|
||||
if (in_single_quote || in_double_quote || in_template_literal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for comment starts
|
||||
if (next_two === '//') {
|
||||
in_single_line_comment = true;
|
||||
i++; // Skip the second '/'
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next_two === '/*') {
|
||||
in_multi_line_comment = true;
|
||||
i++; // Skip the '*'
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next_four === '<!--') {
|
||||
in_html_comment = true;
|
||||
i += 3; // Skip the '<!--'
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
in_single_line_comment ||
|
||||
in_multi_line_comment ||
|
||||
in_html_comment ||
|
||||
in_single_quote ||
|
||||
in_double_quote ||
|
||||
in_template_literal
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export interface EnforcedConfig {
|
||||
[key: string]: EnforcedConfig | true;
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import path from 'node:path';
|
||||
import { loadEnv } from 'vite';
|
||||
import { posixify } from '../../utils/filesystem.js';
|
||||
import { negotiate } from '../../utils/http.js';
|
||||
import { filter_env } from '../../utils/env.js';
|
||||
import { escape_html } from '../../utils/escape.js';
|
||||
import { dedent } from '../../core/sync/utils.js';
|
||||
import {
|
||||
app_server,
|
||||
env_dynamic_private,
|
||||
env_dynamic_public,
|
||||
env_static_private,
|
||||
env_static_public,
|
||||
service_worker
|
||||
} from './module_ids.js';
|
||||
|
||||
/**
|
||||
* Transforms kit.alias to a valid vite.resolve.alias array.
|
||||
*
|
||||
* Related to tsconfig path alias creation.
|
||||
*
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* */
|
||||
export function get_config_aliases(config) {
|
||||
/** @type {import('vite').Alias[]} */
|
||||
const alias = [
|
||||
// For now, we handle `$lib` specially here rather than make it a default value for
|
||||
// `config.kit.alias` since it has special meaning for packaging, etc.
|
||||
{ find: '$lib', replacement: config.files.lib }
|
||||
];
|
||||
|
||||
for (let [key, value] of Object.entries(config.alias)) {
|
||||
value = posixify(value);
|
||||
if (value.endsWith('/*')) {
|
||||
value = value.slice(0, -2);
|
||||
}
|
||||
if (key.endsWith('/*')) {
|
||||
// Doing just `{ find: key.slice(0, -2) ,..}` would mean `import .. from "key"` would also be matched, which we don't want
|
||||
alias.push({
|
||||
find: new RegExp(`^${escape_for_regexp(key.slice(0, -2))}\\/(.+)$`),
|
||||
replacement: `${path.resolve(value)}/$1`
|
||||
});
|
||||
} else if (key + '/*' in config.alias) {
|
||||
// key and key/* both exist -> the replacement for key needs to happen _only_ on import .. from "key"
|
||||
alias.push({
|
||||
find: new RegExp(`^${escape_for_regexp(key)}$`),
|
||||
replacement: path.resolve(value)
|
||||
});
|
||||
} else {
|
||||
alias.push({ find: key, replacement: path.resolve(value) });
|
||||
}
|
||||
}
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
function escape_for_regexp(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load environment variables from process.env and .env files
|
||||
* @param {import('types').ValidatedKitConfig['env']} env_config
|
||||
* @param {string} mode
|
||||
*/
|
||||
export function get_env(env_config, mode) {
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = env_config;
|
||||
const env = loadEnv(mode, env_config.dir, '');
|
||||
|
||||
return {
|
||||
public: filter_env(env, public_prefix, private_prefix),
|
||||
private: filter_env(env, private_prefix, public_prefix)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('http').IncomingMessage} req
|
||||
* @param {import('http').ServerResponse} res
|
||||
* @param {string} base
|
||||
*/
|
||||
export function not_found(req, res, base) {
|
||||
const type = negotiate(req.headers.accept ?? '*', ['text/plain', 'text/html']);
|
||||
|
||||
// special case — handle `/` request automatically
|
||||
if (req.url === '/' && type === 'text/html') {
|
||||
res.statusCode = 307;
|
||||
res.setHeader('location', base);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 404;
|
||||
|
||||
const prefixed = base + req.url;
|
||||
|
||||
if (type === 'text/html') {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(
|
||||
`The server is configured with a public base URL of ${escape_html(
|
||||
base
|
||||
)} - did you mean to visit <a href="${escape_html(prefixed, true)}">${escape_html(
|
||||
prefixed
|
||||
)}</a> instead?`
|
||||
);
|
||||
} else {
|
||||
res.end(
|
||||
`The server is configured with a public base URL of ${escape_html(
|
||||
base
|
||||
)} - did you mean to visit ${escape_html(prefixed)} instead?`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const query_pattern = /\?.*$/s;
|
||||
|
||||
/**
|
||||
* Removes cwd/lib path from the start of the id
|
||||
* @param {string} id
|
||||
* @param {string} lib
|
||||
* @param {string} cwd
|
||||
*/
|
||||
export function normalize_id(id, lib, cwd) {
|
||||
id = id.replace(query_pattern, '');
|
||||
|
||||
if (id.startsWith(lib)) {
|
||||
id = id.replace(lib, '$lib');
|
||||
}
|
||||
|
||||
if (id.startsWith(cwd)) {
|
||||
id = path.relative(cwd, id);
|
||||
}
|
||||
|
||||
if (id === app_server) {
|
||||
return '$app/server';
|
||||
}
|
||||
|
||||
if (id === env_static_private) {
|
||||
return '$env/static/private';
|
||||
}
|
||||
|
||||
if (id === env_static_public) {
|
||||
return '$env/static/public';
|
||||
}
|
||||
|
||||
if (id === env_dynamic_private) {
|
||||
return '$env/dynamic/private';
|
||||
}
|
||||
|
||||
if (id === env_dynamic_public) {
|
||||
return '$env/dynamic/public';
|
||||
}
|
||||
|
||||
if (id === service_worker) {
|
||||
return '$service-worker';
|
||||
}
|
||||
|
||||
return posixify(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* For times when you need to throw an error, but without
|
||||
* displaying a useless stack trace (since the developer
|
||||
* can't do anything useful with it)
|
||||
* @param {string} message
|
||||
*/
|
||||
export function stackless(message) {
|
||||
const error = new Error(message);
|
||||
error.stack = '';
|
||||
return error;
|
||||
}
|
||||
|
||||
export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', '');
|
||||
|
||||
/**
|
||||
* For `error_for_missing_config('instrumentation.server.js', 'kit.experimental.instrumentation.server', true)`,
|
||||
* returns:
|
||||
*
|
||||
* ```
|
||||
* To enable `instrumentation.server.js`, add the following to your `svelte.config.js`:
|
||||
*
|
||||
*\`\`\`js
|
||||
* kit:
|
||||
* experimental:
|
||||
* instrumentation:
|
||||
* server: true
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*\`\`\`
|
||||
*```
|
||||
* @param {string} feature_name
|
||||
* @param {string} path
|
||||
* @param {string} value
|
||||
* @returns {never}
|
||||
*/
|
||||
export function error_for_missing_config(feature_name, path, value) {
|
||||
const hole = '__HOLE__';
|
||||
|
||||
const result = path.split('.').reduce((acc, part, i, parts) => {
|
||||
const indent = ' '.repeat(i);
|
||||
const rhs = i === parts.length - 1 ? value : `{\n${hole}\n${indent}}`;
|
||||
|
||||
return acc.replace(hole, `${indent}${part}: ${rhs}`);
|
||||
}, hole);
|
||||
|
||||
throw stackless(
|
||||
dedent`\
|
||||
To enable ${feature_name}, add the following to your \`svelte.config.js\`:
|
||||
|
||||
${result}
|
||||
`
|
||||
);
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { BROWSER as browser, DEV as dev } from 'esm-env';
|
||||
export { building, version } from '__sveltekit/environment';
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* `true` if the app is running in the browser.
|
||||
*/
|
||||
export const browser: boolean;
|
||||
|
||||
/**
|
||||
* Whether the dev server is running. This is not guaranteed to correspond to `NODE_ENV` or `MODE`.
|
||||
*/
|
||||
export const dev: boolean;
|
||||
|
||||
/**
|
||||
* SvelteKit analyses your app during the `build` step by running it. During this process, `building` is `true`. This also applies during prerendering.
|
||||
*/
|
||||
export const building: boolean;
|
||||
|
||||
/**
|
||||
* The value of `config.kit.version.name`.
|
||||
*/
|
||||
export const version: string;
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
import * as devalue from 'devalue';
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import { invalidateAll } from './navigation.js';
|
||||
import { app as client_app, applyAction } from '../client/client.js';
|
||||
import { app as server_app } from '../server/app.js';
|
||||
|
||||
export { applyAction };
|
||||
|
||||
/**
|
||||
* Use this function to deserialize the response from a form submission.
|
||||
* Usage:
|
||||
*
|
||||
* ```js
|
||||
* import { deserialize } from '$app/forms';
|
||||
*
|
||||
* async function handleSubmit(event) {
|
||||
* const response = await fetch('/form?/action', {
|
||||
* method: 'POST',
|
||||
* body: new FormData(event.target)
|
||||
* });
|
||||
*
|
||||
* const result = deserialize(await response.text());
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
* @template {Record<string, unknown> | undefined} Success
|
||||
* @template {Record<string, unknown> | undefined} Failure
|
||||
* @param {string} result
|
||||
* @returns {import('@sveltejs/kit').ActionResult<Success, Failure>}
|
||||
*/
|
||||
export function deserialize(result) {
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
if (parsed.data) {
|
||||
// the decoders should never be initialised at the top-level because `app`
|
||||
// will not be initialised yet if `kit.output.bundleStrategy` is 'single' or 'inline'
|
||||
parsed.data = devalue.parse(parsed.data, BROWSER ? client_app.decoders : server_app.decoders);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow clone an element, so that we can access e.g. `form.action` without worrying
|
||||
* that someone has added an `<input name="action">` (https://github.com/sveltejs/kit/issues/7593)
|
||||
* @template {HTMLElement} T
|
||||
* @param {T} element
|
||||
* @returns {T}
|
||||
*/
|
||||
function clone(element) {
|
||||
return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element));
|
||||
}
|
||||
|
||||
/**
|
||||
* This action enhances a `<form>` element that otherwise would work without JavaScript.
|
||||
*
|
||||
* The `submit` function is called upon submission with the given FormData and the `action` that should be triggered.
|
||||
* If `cancel` is called, the form will not be submitted.
|
||||
* You can use the abort `controller` to cancel the submission in case another one starts.
|
||||
* If a function is returned, that function is called with the response from the server.
|
||||
* If nothing is returned, the fallback will be used.
|
||||
*
|
||||
* If this function or its return value isn't set, it
|
||||
* - falls back to updating the `form` prop with the returned data if the action is on the same page as the form
|
||||
* - updates `page.status`
|
||||
* - resets the `<form>` element and invalidates all data in case of successful submission with no redirect response
|
||||
* - redirects in case of a redirect response
|
||||
* - redirects to the nearest error page in case of an unexpected error
|
||||
*
|
||||
* If you provide a custom function with a callback and want to use the default behavior, invoke `update` in your callback.
|
||||
* It accepts an options object
|
||||
* - `reset: false` if you don't want the `<form>` values to be reset after a successful submission
|
||||
* - `invalidateAll: false` if you don't want the action to call `invalidateAll` after submission
|
||||
* @template {Record<string, unknown> | undefined} Success
|
||||
* @template {Record<string, unknown> | undefined} Failure
|
||||
* @param {HTMLFormElement} form_element The form element
|
||||
* @param {import('@sveltejs/kit').SubmitFunction<Success, Failure>} submit Submit callback
|
||||
*/
|
||||
export function enhance(form_element, submit = () => {}) {
|
||||
if (DEV && clone(form_element).method !== 'post') {
|
||||
throw new Error('use:enhance can only be used on <form> fields with method="POST"');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* action: URL;
|
||||
* invalidateAll?: boolean;
|
||||
* result: import('@sveltejs/kit').ActionResult;
|
||||
* reset?: boolean
|
||||
* }} opts
|
||||
*/
|
||||
const fallback_callback = async ({
|
||||
action,
|
||||
result,
|
||||
reset = true,
|
||||
invalidateAll: shouldInvalidateAll = true
|
||||
}) => {
|
||||
if (result.type === 'success') {
|
||||
if (reset) {
|
||||
// We call reset from the prototype to avoid DOM clobbering
|
||||
HTMLFormElement.prototype.reset.call(form_element);
|
||||
}
|
||||
if (shouldInvalidateAll) {
|
||||
await invalidateAll();
|
||||
}
|
||||
}
|
||||
|
||||
// For success/failure results, only apply action if it belongs to the
|
||||
// current page, otherwise `form` will be updated erroneously
|
||||
if (
|
||||
location.origin + location.pathname === action.origin + action.pathname ||
|
||||
result.type === 'redirect' ||
|
||||
result.type === 'error'
|
||||
) {
|
||||
await applyAction(result);
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {SubmitEvent} event */
|
||||
async function handle_submit(event) {
|
||||
const method = event.submitter?.hasAttribute('formmethod')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
|
||||
: clone(form_element).method;
|
||||
if (method !== 'post') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const action = new URL(
|
||||
// We can't do submitter.formAction directly because that property is always set
|
||||
event.submitter?.hasAttribute('formaction')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
|
||||
: clone(form_element).action
|
||||
);
|
||||
|
||||
const enctype = event.submitter?.hasAttribute('formenctype')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formEnctype
|
||||
: clone(form_element).enctype;
|
||||
|
||||
const form_data = new FormData(form_element, event.submitter);
|
||||
|
||||
if (DEV && enctype !== 'multipart/form-data') {
|
||||
for (const value of form_data.values()) {
|
||||
if (value instanceof File) {
|
||||
throw new Error(
|
||||
'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
let cancelled = false;
|
||||
const cancel = () => (cancelled = true);
|
||||
|
||||
const callback =
|
||||
(await submit({
|
||||
action,
|
||||
cancel,
|
||||
controller,
|
||||
formData: form_data,
|
||||
formElement: form_element,
|
||||
submitter: event.submitter
|
||||
})) ?? fallback_callback;
|
||||
if (cancelled) return;
|
||||
|
||||
/** @type {import('@sveltejs/kit').ActionResult} */
|
||||
let result;
|
||||
|
||||
try {
|
||||
const headers = new Headers({
|
||||
accept: 'application/json',
|
||||
'x-sveltekit-action': 'true'
|
||||
});
|
||||
|
||||
// do not explicitly set the `Content-Type` header when sending `FormData`
|
||||
// or else it will interfere with the browser's header setting
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#sect4
|
||||
if (enctype !== 'multipart/form-data') {
|
||||
headers.set(
|
||||
'Content-Type',
|
||||
/^(:?application\/x-www-form-urlencoded|text\/plain)$/.test(enctype)
|
||||
? enctype
|
||||
: 'application/x-www-form-urlencoded'
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error `URLSearchParams(form_data)` is kosher, but typescript doesn't know that
|
||||
const body = enctype === 'multipart/form-data' ? form_data : new URLSearchParams(form_data);
|
||||
|
||||
const response = await fetch(action, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
body,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
result = deserialize(await response.text());
|
||||
if (result.type === 'error') result.status = response.status;
|
||||
} catch (error) {
|
||||
if (/** @type {any} */ (error)?.name === 'AbortError') return;
|
||||
result = { type: 'error', error };
|
||||
}
|
||||
|
||||
await callback({
|
||||
action,
|
||||
formData: form_data,
|
||||
formElement: form_element,
|
||||
update: (opts) =>
|
||||
fallback_callback({
|
||||
action,
|
||||
result,
|
||||
reset: opts?.reset,
|
||||
invalidateAll: opts?.invalidateAll
|
||||
}),
|
||||
// @ts-expect-error generic constraints stuff we don't care about
|
||||
result
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
HTMLFormElement.prototype.addEventListener.call(form_element, 'submit', handle_submit);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
// @ts-expect-error
|
||||
HTMLFormElement.prototype.removeEventListener.call(form_element, 'submit', handle_submit);
|
||||
}
|
||||
};
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export {
|
||||
afterNavigate,
|
||||
beforeNavigate,
|
||||
disableScrollHandling,
|
||||
goto,
|
||||
invalidate,
|
||||
invalidateAll,
|
||||
refreshAll,
|
||||
onNavigate,
|
||||
preloadCode,
|
||||
preloadData,
|
||||
pushState,
|
||||
replaceState
|
||||
} from '../client/client.js';
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/** @import { Asset, RouteId, RouteIdWithSearchOrHash, Pathname, PathnameWithSearchOrHash, ResolvedPathname } from '$app/types' */
|
||||
/** @import { ResolveArgs } from './types.js' */
|
||||
import { base, assets, hash_routing } from './internal/client.js';
|
||||
import { resolve_route } from '../../../utils/routing.js';
|
||||
import { get_navigation_intent } from '../../client/client.js';
|
||||
|
||||
/**
|
||||
* Resolve the URL of an asset in your `static` directory, by prefixing it with [`config.kit.paths.assets`](https://svelte.dev/docs/kit/configuration#paths) if configured, or otherwise by prefixing it with the base path.
|
||||
*
|
||||
* During server rendering, the base path is relative and depends on the page currently being rendered.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { asset } from '$app/paths';
|
||||
* </script>
|
||||
*
|
||||
* <img alt="a potato" src={asset('/potato.jpg')} />
|
||||
* ```
|
||||
* @since 2.26
|
||||
*
|
||||
* @param {Asset} file
|
||||
* @returns {string}
|
||||
*/
|
||||
export function asset(file) {
|
||||
return (assets || base) + file;
|
||||
}
|
||||
|
||||
const pathname_prefix = hash_routing ? '#' : '';
|
||||
|
||||
/**
|
||||
* Resolve a pathname by prefixing it with the base path, if any, or resolve a route ID by populating dynamic segments with parameters.
|
||||
*
|
||||
* During server rendering, the base path is relative and depends on the page currently being rendered.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* import { resolve } from '$app/paths';
|
||||
*
|
||||
* // using a pathname
|
||||
* const resolved = resolve(`/blog/hello-world`);
|
||||
*
|
||||
* // using a route ID plus parameters
|
||||
* const resolved = resolve('/blog/[slug]', {
|
||||
* slug: 'hello-world'
|
||||
* });
|
||||
* ```
|
||||
* @since 2.26
|
||||
*
|
||||
* @template {RouteIdWithSearchOrHash | PathnameWithSearchOrHash} T
|
||||
* @param {ResolveArgs<T>} args
|
||||
* @returns {ResolvedPathname}
|
||||
*/
|
||||
export function resolve(...args) {
|
||||
// The type error is correct here, and if someone doesn't pass params when they should there's a runtime error,
|
||||
// but we don't want to adjust the internal resolve_route function to accept `undefined`, hence the type cast.
|
||||
return (
|
||||
base + pathname_prefix + resolve_route(args[0], /** @type {Record<string, string>} */ (args[1]))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a path or URL to a route ID and extracts any parameters.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* import { match } from '$app/paths';
|
||||
*
|
||||
* const route = await match('/blog/hello-world');
|
||||
*
|
||||
* if (route?.id === '/blog/[slug]') {
|
||||
* const slug = route.params.slug;
|
||||
* const response = await fetch(`/api/posts/${slug}`);
|
||||
* const post = await response.json();
|
||||
* }
|
||||
* ```
|
||||
* @since 2.52.0
|
||||
*
|
||||
* @param {Pathname | URL | (string & {})} url
|
||||
* @returns {Promise<{ id: RouteId, params: Record<string, string> } | null>}
|
||||
*/
|
||||
export async function match(url) {
|
||||
if (typeof url === 'string') {
|
||||
url = new URL(url, location.href);
|
||||
}
|
||||
|
||||
const intent = await get_navigation_intent(url, false);
|
||||
|
||||
if (intent) {
|
||||
return {
|
||||
id: /** @type {RouteId} */ (intent.route.id),
|
||||
params: intent.params
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export { base, assets, resolve as resolveRoute };
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from '#app/paths';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const base = __SVELTEKIT_PAYLOAD__?.base ?? __SVELTEKIT_PATHS_BASE__;
|
||||
export const assets = __SVELTEKIT_PAYLOAD__?.assets ?? base ?? __SVELTEKIT_PATHS_ASSETS__;
|
||||
export const app_dir = __SVELTEKIT_APP_DIR__;
|
||||
export const hash_routing = __SVELTEKIT_HASH_ROUTING__;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
export let base = __SVELTEKIT_PATHS_BASE__;
|
||||
export let assets = __SVELTEKIT_PATHS_ASSETS__ || base;
|
||||
export const app_dir = __SVELTEKIT_APP_DIR__;
|
||||
export const relative = __SVELTEKIT_PATHS_RELATIVE__;
|
||||
|
||||
const initial = { base, assets };
|
||||
|
||||
/**
|
||||
* `base` could be overridden during rendering to be relative;
|
||||
* this one's the original non-relative base path
|
||||
*/
|
||||
export const initial_base = initial.base;
|
||||
|
||||
/**
|
||||
* @param {{ base: string, assets: string }} paths
|
||||
*/
|
||||
export function override(paths) {
|
||||
base = paths.base;
|
||||
assets = paths.assets;
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
base = initial.base;
|
||||
assets = initial.assets;
|
||||
}
|
||||
|
||||
/** @param {string} path */
|
||||
export function set_assets(path) {
|
||||
assets = initial.assets = path;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { RouteIdWithSearchOrHash, PathnameWithSearchOrHash, ResolvedPathname } from '$app/types';
|
||||
import { ResolveArgs } from './types.js';
|
||||
|
||||
export { resolve, asset, match } from './client.js';
|
||||
|
||||
/**
|
||||
* A string that matches [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths).
|
||||
*
|
||||
* Example usage: `<a href="{base}/your-page">Link</a>`
|
||||
*
|
||||
* @deprecated Use [`resolve(...)`](https://svelte.dev/docs/kit/$app-paths#resolve) instead
|
||||
*/
|
||||
export let base: '' | `/${string}`;
|
||||
|
||||
/**
|
||||
* An absolute path that matches [`config.kit.paths.assets`](https://svelte.dev/docs/kit/configuration#paths).
|
||||
*
|
||||
* > [!NOTE] If a value for `config.kit.paths.assets` is specified, it will be replaced with `'/_svelte_kit_assets'` during `vite dev` or `vite preview`, since the assets don't yet live at their eventual URL.
|
||||
*
|
||||
* @deprecated Use [`asset(...)`](https://svelte.dev/docs/kit/$app-paths#asset) instead
|
||||
*/
|
||||
export let assets: '' | `https://${string}` | `http://${string}` | '/_svelte_kit_assets';
|
||||
|
||||
/**
|
||||
* @deprecated Use [`resolve(...)`](https://svelte.dev/docs/kit/$app-paths#resolve) instead
|
||||
*/
|
||||
export function resolveRoute<T extends RouteIdWithSearchOrHash | PathnameWithSearchOrHash>(
|
||||
...args: ResolveArgs<T>
|
||||
): ResolvedPathname;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { base, assets, relative, initial_base } from './internal/server.js';
|
||||
import { resolve_route, find_route } from '../../../utils/routing.js';
|
||||
import { decode_pathname } from '../../../utils/url.js';
|
||||
import { try_get_request_store } from '@sveltejs/kit/internal/server';
|
||||
import { manifest } from '__sveltekit/server';
|
||||
import { get_hooks } from '__SERVER__/internal.js';
|
||||
|
||||
/** @type {import('./client.js').asset} */
|
||||
export function asset(file) {
|
||||
// @ts-expect-error we use the `resolve` mechanism, but with the 'wrong' input
|
||||
return assets ? assets + file : resolve(file);
|
||||
}
|
||||
|
||||
/** @type {import('./client.js').resolve} */
|
||||
export function resolve(id, params) {
|
||||
const resolved = resolve_route(id, /** @type {Record<string, string>} */ (params));
|
||||
|
||||
if (relative) {
|
||||
const store = try_get_request_store();
|
||||
|
||||
if (store && !store.state.prerendering?.fallback) {
|
||||
const after_base = store.event.url.pathname.slice(initial_base.length);
|
||||
const segments = after_base.split('/').slice(2);
|
||||
const prefix = segments.map(() => '..').join('/') || '.';
|
||||
|
||||
return prefix + resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return base + resolved;
|
||||
}
|
||||
|
||||
/** @type {import('./client.js').match} */
|
||||
export async function match(url) {
|
||||
const store = try_get_request_store();
|
||||
|
||||
if (typeof url === 'string') {
|
||||
const origin = store?.event.url.origin ?? 'a://a';
|
||||
url = new URL(url, origin);
|
||||
}
|
||||
|
||||
const { reroute } = await get_hooks();
|
||||
|
||||
let resolved_path;
|
||||
|
||||
try {
|
||||
resolved_path = decode_pathname(
|
||||
(await reroute?.({ url: new URL(url), fetch: store?.event.fetch ?? fetch })) ?? url.pathname
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (base && resolved_path.startsWith(base)) {
|
||||
resolved_path = resolved_path.slice(base.length) || '/';
|
||||
}
|
||||
|
||||
const matchers = await manifest._.matchers();
|
||||
const result = find_route(resolved_path, manifest._.routes, matchers);
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
id: /** @type {import('$app/types').RouteId} */ (result.route.id),
|
||||
params: result.params
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export { base, assets, resolve as resolveRoute };
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
PathnameWithSearchOrHash,
|
||||
RouteId,
|
||||
RouteIdWithSearchOrHash,
|
||||
RouteParams
|
||||
} from '$app/types';
|
||||
|
||||
type StripSearchOrHash<T extends string> = T extends `${infer Pathname}?${string}`
|
||||
? Pathname
|
||||
: T extends `${infer Pathname}#${string}`
|
||||
? Pathname
|
||||
: T;
|
||||
|
||||
export type ResolveArgs<T extends RouteIdWithSearchOrHash | PathnameWithSearchOrHash> =
|
||||
T extends RouteId
|
||||
? RouteParams<T> extends Record<string, never>
|
||||
? [route: T]
|
||||
: [route: T, params: RouteParams<T>]
|
||||
: StripSearchOrHash<T> extends infer U extends RouteId
|
||||
? RouteParams<U> extends Record<string, never>
|
||||
? [route: T]
|
||||
: [route: T, params: RouteParams<U>]
|
||||
: [route: T];
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { read_implementation, manifest } from '__sveltekit/server';
|
||||
import { base } from '$app/paths';
|
||||
import { DEV } from 'esm-env';
|
||||
import { base64_decode } from '../../utils.js';
|
||||
|
||||
/**
|
||||
* Read the contents of an imported asset from the filesystem
|
||||
* @example
|
||||
* ```js
|
||||
* import { read } from '$app/server';
|
||||
* import somefile from './somefile.txt';
|
||||
*
|
||||
* const asset = read(somefile);
|
||||
* const text = await asset.text();
|
||||
* ```
|
||||
* @param {string} asset
|
||||
* @returns {Response}
|
||||
* @since 2.4.0
|
||||
*/
|
||||
export function read(asset) {
|
||||
__SVELTEKIT_TRACK__('$app/server:read');
|
||||
|
||||
if (!read_implementation) {
|
||||
throw new Error(
|
||||
'No `read` implementation was provided. Please ensure that your adapter is up to date and supports this feature'
|
||||
);
|
||||
}
|
||||
|
||||
// handle inline assets internally
|
||||
const match = /^data:([^;,]+)?(;base64)?,/.exec(asset);
|
||||
if (match) {
|
||||
const type = match[1] ?? 'application/octet-stream';
|
||||
const data = asset.slice(match[0].length);
|
||||
|
||||
if (match[2] !== undefined) {
|
||||
const decoded = base64_decode(data);
|
||||
|
||||
// @ts-ignore passing a Uint8Array to `new Response(...)` is fine
|
||||
return new Response(decoded, {
|
||||
headers: {
|
||||
'Content-Length': decoded.byteLength.toString(),
|
||||
'Content-Type': type
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const decoded = decodeURIComponent(data);
|
||||
|
||||
return new Response(decoded, {
|
||||
headers: {
|
||||
'Content-Length': decoded.length.toString(),
|
||||
'Content-Type': type
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const file = decodeURIComponent(
|
||||
DEV && asset.startsWith('/@fs') ? asset : asset.slice(base.length + 1)
|
||||
);
|
||||
|
||||
if (file in manifest._.server_assets) {
|
||||
const length = manifest._.server_assets[file];
|
||||
const type = manifest.mimeTypes[file.slice(file.lastIndexOf('.'))];
|
||||
|
||||
return new Response(read_implementation(file), {
|
||||
headers: {
|
||||
'Content-Length': '' + length,
|
||||
'Content-Type': type
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Asset does not exist: ${file}`);
|
||||
}
|
||||
|
||||
export { getRequestEvent } from '@sveltejs/kit/internal/server';
|
||||
|
||||
export { query, prerender, command, form } from './remote/index.js';
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/** @import { RemoteCommand } from '@sveltejs/kit' */
|
||||
/** @import { RemoteInfo, MaybePromise } from 'types' */
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
import { get_request_store } from '@sveltejs/kit/internal/server';
|
||||
import { create_validator, run_remote_function } from './shared.js';
|
||||
import { MUTATIVE_METHODS } from '../../../../constants.js';
|
||||
|
||||
/**
|
||||
* Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
|
||||
*
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {() => Output} fn
|
||||
* @returns {RemoteCommand<void, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
|
||||
*
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {'unchecked'} validate
|
||||
* @param {(arg: Input) => Output} fn
|
||||
* @returns {RemoteCommand<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
|
||||
*
|
||||
* @template {StandardSchemaV1} Schema
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {Schema} validate
|
||||
* @param {(arg: StandardSchemaV1.InferOutput<Schema>) => Output} fn
|
||||
* @returns {RemoteCommand<StandardSchemaV1.InferInput<Schema>, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @param {any} validate_or_fn
|
||||
* @param {(arg?: Input) => Output} [maybe_fn]
|
||||
* @returns {RemoteCommand<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/*@__NO_SIDE_EFFECTS__*/
|
||||
export function command(validate_or_fn, maybe_fn) {
|
||||
/** @type {(arg?: Input) => Output} */
|
||||
const fn = maybe_fn ?? validate_or_fn;
|
||||
|
||||
/** @type {(arg?: any) => MaybePromise<Input>} */
|
||||
const validate = create_validator(validate_or_fn, maybe_fn);
|
||||
|
||||
/** @type {RemoteInfo} */
|
||||
const __ = { type: 'command', id: '', name: '' };
|
||||
|
||||
/** @type {RemoteCommand<Input, Output> & { __: RemoteInfo }} */
|
||||
const wrapper = (arg) => {
|
||||
const { event, state } = get_request_store();
|
||||
|
||||
if (!state.allows_commands) {
|
||||
const disallowed_method = !MUTATIVE_METHODS.includes(event.request.method);
|
||||
throw new Error(
|
||||
`Cannot call a command (\`${__.name}(${maybe_fn ? '...' : ''})\`) ${disallowed_method ? `from a ${event.request.method} handler or ` : ''}during server-side rendering`
|
||||
);
|
||||
}
|
||||
|
||||
state.refreshes ??= {};
|
||||
|
||||
const promise = Promise.resolve(
|
||||
run_remote_function(event, state, true, () => validate(arg), fn)
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
promise.updates = () => {
|
||||
throw new Error(`Cannot call '${__.name}(...).updates(...)' on the server`);
|
||||
};
|
||||
|
||||
return /** @type {ReturnType<RemoteCommand<Input, Output>>} */ (promise);
|
||||
};
|
||||
|
||||
Object.defineProperty(wrapper, '__', { value: __ });
|
||||
|
||||
// On the server, pending is always 0
|
||||
Object.defineProperty(wrapper, 'pending', {
|
||||
get: () => 0
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
/** @import { RemoteFormInput, RemoteForm, InvalidField } from '@sveltejs/kit' */
|
||||
/** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
import { get_request_store } from '@sveltejs/kit/internal/server';
|
||||
import { DEV } from 'esm-env';
|
||||
import {
|
||||
create_field_proxy,
|
||||
set_nested_value,
|
||||
throw_on_old_property_access,
|
||||
deep_set,
|
||||
normalize_issue,
|
||||
flatten_issues
|
||||
} from '../../../form-utils.js';
|
||||
import { get_cache, run_remote_function } from './shared.js';
|
||||
import { ValidationError } from '@sveltejs/kit/internal';
|
||||
|
||||
/**
|
||||
* Creates a form object that can be spread onto a `<form>` element.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
|
||||
*
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {() => MaybePromise<Output>} fn
|
||||
* @returns {RemoteForm<void, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a form object that can be spread onto a `<form>` element.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
|
||||
*
|
||||
* @template {RemoteFormInput} Input
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {'unchecked'} validate
|
||||
* @param {(data: Input, issue: InvalidField<Input>) => MaybePromise<Output>} fn
|
||||
* @returns {RemoteForm<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a form object that can be spread onto a `<form>` element.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
|
||||
*
|
||||
* @template {StandardSchemaV1<RemoteFormInput, Record<string, any>>} Schema
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {Schema} validate
|
||||
* @param {(data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
|
||||
* @returns {RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* @template {RemoteFormInput} Input
|
||||
* @template Output
|
||||
* @param {any} validate_or_fn
|
||||
* @param {(data_or_issue: any, issue?: any) => MaybePromise<Output>} [maybe_fn]
|
||||
* @returns {RemoteForm<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/*@__NO_SIDE_EFFECTS__*/
|
||||
// @ts-ignore we don't want to prefix `fn` with an underscore, as that will be user-visible
|
||||
export function form(validate_or_fn, maybe_fn) {
|
||||
/** @type {any} */
|
||||
const fn = maybe_fn ?? validate_or_fn;
|
||||
|
||||
/** @type {StandardSchemaV1 | null} */
|
||||
const schema =
|
||||
!maybe_fn || validate_or_fn === 'unchecked' ? null : /** @type {any} */ (validate_or_fn);
|
||||
|
||||
/**
|
||||
* @param {string | number | boolean} [key]
|
||||
*/
|
||||
function create_instance(key) {
|
||||
/** @type {RemoteForm<Input, Output>} */
|
||||
const instance = {};
|
||||
|
||||
instance.method = 'POST';
|
||||
|
||||
Object.defineProperty(instance, 'enhance', {
|
||||
value: () => {
|
||||
return { action: instance.action, method: instance.method };
|
||||
}
|
||||
});
|
||||
|
||||
/** @type {RemoteInfo} */
|
||||
const __ = {
|
||||
type: 'form',
|
||||
name: '',
|
||||
id: '',
|
||||
fn: async (data, meta, form_data) => {
|
||||
// TODO 3.0 remove this warning
|
||||
if (DEV && !data) {
|
||||
const error = () => {
|
||||
throw new Error(
|
||||
'Remote form functions no longer get passed a FormData object. ' +
|
||||
"`form` now has the same signature as `query` or `command`, i.e. it expects to be invoked like `form(schema, callback)` or `form('unchecked', callback)`. " +
|
||||
'The payload of the callback function is now a POJO instead of a FormData object. See https://kit.svelte.dev/docs/remote-functions#form for details.'
|
||||
);
|
||||
};
|
||||
data = {};
|
||||
for (const key of [
|
||||
'append',
|
||||
'delete',
|
||||
'entries',
|
||||
'forEach',
|
||||
'get',
|
||||
'getAll',
|
||||
'has',
|
||||
'keys',
|
||||
'set',
|
||||
'values'
|
||||
]) {
|
||||
Object.defineProperty(data, key, { get: error });
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {{ submission: true, input?: Record<string, any>, issues?: InternalRemoteFormIssue[], result: Output }} */
|
||||
const output = {};
|
||||
|
||||
// make it possible to differentiate between user submission and programmatic `field.set(...)` updates
|
||||
output.submission = true;
|
||||
|
||||
const { event, state } = get_request_store();
|
||||
const validated = await schema?.['~standard'].validate(data);
|
||||
|
||||
if (meta.validate_only) {
|
||||
return validated?.issues?.map((issue) => normalize_issue(issue, true)) ?? [];
|
||||
}
|
||||
|
||||
if (validated?.issues !== undefined) {
|
||||
handle_issues(output, validated.issues, form_data);
|
||||
} else {
|
||||
if (validated !== undefined) {
|
||||
data = validated.value;
|
||||
}
|
||||
|
||||
state.refreshes ??= {};
|
||||
|
||||
const issue = create_issues();
|
||||
|
||||
try {
|
||||
output.result = await run_remote_function(
|
||||
event,
|
||||
state,
|
||||
true,
|
||||
() => data,
|
||||
(data) => (!maybe_fn ? fn() : fn(data, issue))
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
handle_issues(output, e.issues, form_data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads
|
||||
// where only one form submission is active at the same time
|
||||
if (!event.isRemoteRequest) {
|
||||
get_cache(__, state)[''] ??= output;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(instance, '__', { value: __ });
|
||||
|
||||
Object.defineProperty(instance, 'action', {
|
||||
get: () => `?/remote=${__.id}`,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(instance, 'fields', {
|
||||
get() {
|
||||
return create_field_proxy(
|
||||
{},
|
||||
() => get_cache(__)?.['']?.input ?? {},
|
||||
(path, value) => {
|
||||
const cache = get_cache(__);
|
||||
const data = cache[''];
|
||||
|
||||
if (data?.submission) {
|
||||
// don't override a submission
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.length === 0) {
|
||||
(cache[''] ??= {}).input = value;
|
||||
return;
|
||||
}
|
||||
|
||||
const input = data?.input ?? {};
|
||||
deep_set(input, path.map(String), value);
|
||||
(cache[''] ??= {}).input = input;
|
||||
},
|
||||
() => flatten_issues(get_cache(__)?.['']?.issues ?? [])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO 3.0 remove
|
||||
if (DEV) {
|
||||
throw_on_old_property_access(instance);
|
||||
|
||||
Object.defineProperty(instance, 'buttonProps', {
|
||||
get() {
|
||||
throw new Error(
|
||||
'`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
|
||||
' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(instance, 'result', {
|
||||
get() {
|
||||
try {
|
||||
return get_cache(__)?.['']?.result;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On the server, pending is always 0
|
||||
Object.defineProperty(instance, 'pending', {
|
||||
get: () => 0
|
||||
});
|
||||
|
||||
Object.defineProperty(instance, 'preflight', {
|
||||
// preflight is a noop on the server
|
||||
value: () => instance
|
||||
});
|
||||
|
||||
Object.defineProperty(instance, 'validate', {
|
||||
value: () => {
|
||||
throw new Error('Cannot call validate() on the server');
|
||||
}
|
||||
});
|
||||
|
||||
if (key == undefined) {
|
||||
Object.defineProperty(instance, 'for', {
|
||||
/** @type {RemoteForm<any, any>['for']} */
|
||||
value: (key) => {
|
||||
const { state } = get_request_store();
|
||||
const cache_key = __.id + '|' + JSON.stringify(key);
|
||||
let instance = (state.form_instances ??= new Map()).get(cache_key);
|
||||
|
||||
if (!instance) {
|
||||
instance = create_instance(key);
|
||||
instance.__.id = `${__.id}/${encodeURIComponent(JSON.stringify(key))}`;
|
||||
instance.__.name = __.name;
|
||||
|
||||
state.form_instances.set(cache_key, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
return create_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ issues?: InternalRemoteFormIssue[], input?: Record<string, any>, result: any }} output
|
||||
* @param {readonly StandardSchemaV1.Issue[]} issues
|
||||
* @param {FormData | null} form_data - null if the form is progressively enhanced
|
||||
*/
|
||||
function handle_issues(output, issues, form_data) {
|
||||
output.issues = issues.map((issue) => normalize_issue(issue, true));
|
||||
|
||||
// if it was a progressively-enhanced submission, we don't need
|
||||
// to return the input — it's already there
|
||||
if (form_data) {
|
||||
output.input = {};
|
||||
|
||||
for (let key of form_data.keys()) {
|
||||
// redact sensitive fields
|
||||
if (/^[.\]]?_/.test(key)) continue;
|
||||
|
||||
const is_array = key.endsWith('[]');
|
||||
const values = form_data.getAll(key).filter((value) => typeof value === 'string');
|
||||
|
||||
if (is_array) key = key.slice(0, -2);
|
||||
|
||||
set_nested_value(
|
||||
/** @type {Record<string, any>} */ (output.input),
|
||||
key,
|
||||
is_array ? values : values[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an invalid function that can be used to imperatively mark form fields as invalid
|
||||
* @returns {InvalidField<any>}
|
||||
*/
|
||||
function create_issues() {
|
||||
return /** @type {InvalidField<any>} */ (
|
||||
new Proxy(
|
||||
/** @param {string} message */
|
||||
(message) => {
|
||||
// TODO 3.0 remove
|
||||
if (typeof message !== 'string') {
|
||||
throw new Error(
|
||||
'`invalid` should now be imported from `@sveltejs/kit` to throw validation issues. ' +
|
||||
"The second parameter provided to the form function (renamed to `issue`) is still used to construct issues, e.g. `invalid(issue.field('message'))`. " +
|
||||
'For more info see https://github.com/sveltejs/kit/pulls/14768'
|
||||
);
|
||||
}
|
||||
|
||||
return create_issue(message);
|
||||
},
|
||||
{
|
||||
get(target, prop) {
|
||||
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
|
||||
|
||||
return create_issue_proxy(prop, []);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {(string | number)[]} path
|
||||
* @returns {StandardSchemaV1.Issue}
|
||||
*/
|
||||
function create_issue(message, path = []) {
|
||||
return {
|
||||
message,
|
||||
path
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a proxy that builds up a path and returns a function to create an issue
|
||||
* @param {string | number} key
|
||||
* @param {(string | number)[]} path
|
||||
*/
|
||||
function create_issue_proxy(key, path) {
|
||||
const new_path = [...path, key];
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @returns {StandardSchemaV1.Issue}
|
||||
*/
|
||||
const issue_func = (message) => create_issue(message, new_path);
|
||||
|
||||
return new Proxy(issue_func, {
|
||||
get(target, prop) {
|
||||
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
|
||||
|
||||
// Handle array access like invalid.items[0]
|
||||
if (/^\d+$/.test(prop)) {
|
||||
return create_issue_proxy(parseInt(prop, 10), new_path);
|
||||
}
|
||||
|
||||
// Handle property access like invalid.field.nested
|
||||
return create_issue_proxy(prop, new_path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export { command } from './command.js';
|
||||
export { form } from './form.js';
|
||||
export { prerender } from './prerender.js';
|
||||
export { query } from './query.js';
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/** @import { RemoteResource, RemotePrerenderFunction } from '@sveltejs/kit' */
|
||||
/** @import { RemotePrerenderInputsGenerator, RemoteInfo, MaybePromise } from 'types' */
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { DEV } from 'esm-env';
|
||||
import { get_request_store } from '@sveltejs/kit/internal/server';
|
||||
import { stringify, stringify_remote_arg } from '../../../shared.js';
|
||||
import { app_dir, base } from '$app/paths/internal/server';
|
||||
import {
|
||||
create_validator,
|
||||
get_cache,
|
||||
get_response,
|
||||
parse_remote_response,
|
||||
run_remote_function
|
||||
} from './shared.js';
|
||||
|
||||
/**
|
||||
* Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
|
||||
*
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {() => MaybePromise<Output>} fn
|
||||
* @param {{ inputs?: RemotePrerenderInputsGenerator<void>, dynamic?: boolean }} [options]
|
||||
* @returns {RemotePrerenderFunction<void, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
|
||||
*
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {'unchecked'} validate
|
||||
* @param {(arg: Input) => MaybePromise<Output>} fn
|
||||
* @param {{ inputs?: RemotePrerenderInputsGenerator<Input>, dynamic?: boolean }} [options]
|
||||
* @returns {RemotePrerenderFunction<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
|
||||
*
|
||||
* @template {StandardSchemaV1} Schema
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {Schema} schema
|
||||
* @param {(arg: StandardSchemaV1.InferOutput<Schema>) => MaybePromise<Output>} fn
|
||||
* @param {{ inputs?: RemotePrerenderInputsGenerator<StandardSchemaV1.InferInput<Schema>>, dynamic?: boolean }} [options]
|
||||
* @returns {RemotePrerenderFunction<StandardSchemaV1.InferInput<Schema>, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @param {any} validate_or_fn
|
||||
* @param {any} [fn_or_options]
|
||||
* @param {{ inputs?: RemotePrerenderInputsGenerator<Input>, dynamic?: boolean }} [maybe_options]
|
||||
* @returns {RemotePrerenderFunction<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/*@__NO_SIDE_EFFECTS__*/
|
||||
export function prerender(validate_or_fn, fn_or_options, maybe_options) {
|
||||
const maybe_fn = typeof fn_or_options === 'function' ? fn_or_options : undefined;
|
||||
|
||||
/** @type {typeof maybe_options} */
|
||||
const options = maybe_options ?? (maybe_fn ? undefined : fn_or_options);
|
||||
|
||||
/** @type {(arg?: Input) => MaybePromise<Output>} */
|
||||
const fn = maybe_fn ?? validate_or_fn;
|
||||
|
||||
/** @type {(arg?: any) => MaybePromise<Input>} */
|
||||
const validate = create_validator(validate_or_fn, maybe_fn);
|
||||
|
||||
/** @type {RemoteInfo} */
|
||||
const __ = {
|
||||
type: 'prerender',
|
||||
id: '',
|
||||
name: '',
|
||||
has_arg: !!maybe_fn,
|
||||
inputs: options?.inputs,
|
||||
dynamic: options?.dynamic
|
||||
};
|
||||
|
||||
/** @type {RemotePrerenderFunction<Input, Output> & { __: RemoteInfo }} */
|
||||
const wrapper = (arg) => {
|
||||
/** @type {Promise<Output> & Partial<RemoteResource<Output>>} */
|
||||
const promise = (async () => {
|
||||
const { event, state } = get_request_store();
|
||||
const payload = stringify_remote_arg(arg, state.transport);
|
||||
const id = __.id;
|
||||
const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
|
||||
|
||||
if (!state.prerendering && !DEV && !event.isRemoteRequest) {
|
||||
try {
|
||||
return await get_response(__, arg, state, async () => {
|
||||
const key = stringify_remote_arg(arg, state.transport);
|
||||
const cache = get_cache(__, state);
|
||||
|
||||
// TODO adapters can provide prerendered data more efficiently than
|
||||
// fetching from the public internet
|
||||
const promise = (cache[key] ??= fetch(new URL(url, event.url.origin).href).then(
|
||||
async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Prerendered response not found');
|
||||
}
|
||||
|
||||
const prerendered = await response.json();
|
||||
|
||||
if (prerendered.type === 'error') {
|
||||
error(prerendered.status, prerendered.error);
|
||||
}
|
||||
|
||||
return prerendered.result;
|
||||
}
|
||||
));
|
||||
|
||||
return parse_remote_response(await promise, state.transport);
|
||||
});
|
||||
} catch {
|
||||
// not available prerendered, fallback to normal function
|
||||
}
|
||||
}
|
||||
|
||||
if (state.prerendering?.remote_responses.has(url)) {
|
||||
return /** @type {Promise<any>} */ (state.prerendering.remote_responses.get(url));
|
||||
}
|
||||
|
||||
const promise = get_response(__, arg, state, () =>
|
||||
run_remote_function(event, state, false, () => validate(arg), fn)
|
||||
);
|
||||
|
||||
if (state.prerendering) {
|
||||
state.prerendering.remote_responses.set(url, promise);
|
||||
}
|
||||
|
||||
const result = await promise;
|
||||
|
||||
if (state.prerendering) {
|
||||
const body = { type: 'result', result: stringify(result, state.transport) };
|
||||
state.prerendering.dependencies.set(url, {
|
||||
body: JSON.stringify(body),
|
||||
response: json(body)
|
||||
});
|
||||
}
|
||||
|
||||
// TODO this is missing error/loading/current/status
|
||||
return result;
|
||||
})();
|
||||
|
||||
promise.catch(() => {});
|
||||
|
||||
return /** @type {RemoteResource<Output>} */ (promise);
|
||||
};
|
||||
|
||||
Object.defineProperty(wrapper, '__', { value: __ });
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
/** @import { RemoteQuery, RemoteQueryFunction } from '@sveltejs/kit' */
|
||||
/** @import { RemoteInfo, MaybePromise } from 'types' */
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
import { get_request_store } from '@sveltejs/kit/internal/server';
|
||||
import { create_remote_key, stringify_remote_arg } from '../../../shared.js';
|
||||
import { prerendering } from '__sveltekit/environment';
|
||||
import { create_validator, get_cache, get_response, run_remote_function } from './shared.js';
|
||||
import { handle_error_and_jsonify } from '../../../server/utils.js';
|
||||
import { HttpError, SvelteKitError } from '@sveltejs/kit/internal';
|
||||
|
||||
/**
|
||||
* Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation.
|
||||
*
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {() => MaybePromise<Output>} fn
|
||||
* @returns {RemoteQueryFunction<void, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation.
|
||||
*
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {'unchecked'} validate
|
||||
* @param {(arg: Input) => MaybePromise<Output>} fn
|
||||
* @returns {RemoteQueryFunction<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation.
|
||||
*
|
||||
* @template {StandardSchemaV1} Schema
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {Schema} schema
|
||||
* @param {(arg: StandardSchemaV1.InferOutput<Schema>) => MaybePromise<Output>} fn
|
||||
* @returns {RemoteQueryFunction<StandardSchemaV1.InferInput<Schema>, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/**
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @param {any} validate_or_fn
|
||||
* @param {(args?: Input) => MaybePromise<Output>} [maybe_fn]
|
||||
* @returns {RemoteQueryFunction<Input, Output>}
|
||||
* @since 2.27
|
||||
*/
|
||||
/*@__NO_SIDE_EFFECTS__*/
|
||||
export function query(validate_or_fn, maybe_fn) {
|
||||
/** @type {(arg?: Input) => Output} */
|
||||
const fn = maybe_fn ?? validate_or_fn;
|
||||
|
||||
/** @type {(arg?: any) => MaybePromise<Input>} */
|
||||
const validate = create_validator(validate_or_fn, maybe_fn);
|
||||
|
||||
/** @type {RemoteInfo} */
|
||||
const __ = { type: 'query', id: '', name: '' };
|
||||
|
||||
/** @type {RemoteQueryFunction<Input, Output> & { __: RemoteInfo }} */
|
||||
const wrapper = (arg) => {
|
||||
if (prerendering) {
|
||||
throw new Error(
|
||||
`Cannot call query '${__.name}' while prerendering, as prerendered pages need static data. Use 'prerender' from $app/server instead`
|
||||
);
|
||||
}
|
||||
|
||||
const { event, state } = get_request_store();
|
||||
|
||||
const get_remote_function_result = () =>
|
||||
run_remote_function(event, state, false, () => validate(arg), fn);
|
||||
|
||||
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
|
||||
const promise = get_response(__, arg, state, get_remote_function_result);
|
||||
|
||||
promise.catch(() => {});
|
||||
|
||||
promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);
|
||||
|
||||
promise.refresh = () => {
|
||||
const refresh_context = get_refresh_context(__, 'refresh', arg);
|
||||
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
|
||||
const value = is_immediate_refresh ? promise : get_remote_function_result();
|
||||
return update_refresh_value(refresh_context, value, is_immediate_refresh);
|
||||
};
|
||||
|
||||
promise.withOverride = () => {
|
||||
throw new Error(`Cannot call '${__.name}.withOverride()' on the server`);
|
||||
};
|
||||
|
||||
return /** @type {RemoteQuery<Output>} */ (promise);
|
||||
};
|
||||
|
||||
Object.defineProperty(wrapper, '__', { value: __ });
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a batch query function that collects multiple calls and executes them in a single request
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.batch) for full documentation.
|
||||
*
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {'unchecked'} validate
|
||||
* @param {(args: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} fn
|
||||
* @returns {RemoteQueryFunction<Input, Output>}
|
||||
* @since 2.35
|
||||
*/
|
||||
/**
|
||||
* Creates a batch query function that collects multiple calls and executes them in a single request
|
||||
*
|
||||
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.batch) for full documentation.
|
||||
*
|
||||
* @template {StandardSchemaV1} Schema
|
||||
* @template Output
|
||||
* @overload
|
||||
* @param {Schema} schema
|
||||
* @param {(args: StandardSchemaV1.InferOutput<Schema>[]) => MaybePromise<(arg: StandardSchemaV1.InferOutput<Schema>, idx: number) => Output>} fn
|
||||
* @returns {RemoteQueryFunction<StandardSchemaV1.InferInput<Schema>, Output>}
|
||||
* @since 2.35
|
||||
*/
|
||||
/**
|
||||
* @template Input
|
||||
* @template Output
|
||||
* @param {any} validate_or_fn
|
||||
* @param {(args?: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} [maybe_fn]
|
||||
* @returns {RemoteQueryFunction<Input, Output>}
|
||||
* @since 2.35
|
||||
*/
|
||||
/*@__NO_SIDE_EFFECTS__*/
|
||||
function batch(validate_or_fn, maybe_fn) {
|
||||
/** @type {(args?: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} */
|
||||
const fn = maybe_fn ?? validate_or_fn;
|
||||
|
||||
/** @type {(arg?: any) => MaybePromise<Input>} */
|
||||
const validate = create_validator(validate_or_fn, maybe_fn);
|
||||
|
||||
/** @type {RemoteInfo & { type: 'query_batch' }} */
|
||||
const __ = {
|
||||
type: 'query_batch',
|
||||
id: '',
|
||||
name: '',
|
||||
run: async (args, options) => {
|
||||
const { event, state } = get_request_store();
|
||||
|
||||
return run_remote_function(
|
||||
event,
|
||||
state,
|
||||
false,
|
||||
async () => Promise.all(args.map(validate)),
|
||||
async (/** @type {any[]} */ input) => {
|
||||
const get_result = await fn(input);
|
||||
|
||||
return Promise.all(
|
||||
input.map(async (arg, i) => {
|
||||
try {
|
||||
return { type: 'result', data: get_result(arg, i) };
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'error',
|
||||
error: await handle_error_and_jsonify(event, state, options, error),
|
||||
status:
|
||||
error instanceof HttpError || error instanceof SvelteKitError
|
||||
? error.status
|
||||
: 500
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {{ args: any[], resolvers: Array<{resolve: (value: any) => void, reject: (error: any) => void}> }} */
|
||||
let batching = { args: [], resolvers: [] };
|
||||
|
||||
/** @type {RemoteQueryFunction<Input, Output> & { __: RemoteInfo }} */
|
||||
const wrapper = (arg) => {
|
||||
if (prerendering) {
|
||||
throw new Error(
|
||||
`Cannot call query.batch '${__.name}' while prerendering, as prerendered pages need static data. Use 'prerender' from $app/server instead`
|
||||
);
|
||||
}
|
||||
|
||||
const { event, state } = get_request_store();
|
||||
|
||||
const get_remote_function_result = () => {
|
||||
// Collect all the calls to the same query in the same macrotask,
|
||||
// then execute them as one backend request.
|
||||
return new Promise((resolve, reject) => {
|
||||
// We don't need to deduplicate args here, because get_response already caches/reuses identical calls
|
||||
batching.args.push(arg);
|
||||
batching.resolvers.push({ resolve, reject });
|
||||
|
||||
if (batching.args.length > 1) return;
|
||||
|
||||
setTimeout(async () => {
|
||||
const batched = batching;
|
||||
batching = { args: [], resolvers: [] };
|
||||
|
||||
try {
|
||||
return await run_remote_function(
|
||||
event,
|
||||
state,
|
||||
false,
|
||||
async () => Promise.all(batched.args.map(validate)),
|
||||
async (input) => {
|
||||
const get_result = await fn(input);
|
||||
|
||||
for (let i = 0; i < batched.resolvers.length; i++) {
|
||||
try {
|
||||
batched.resolvers[i].resolve(get_result(input[i], i));
|
||||
} catch (error) {
|
||||
batched.resolvers[i].reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
for (const resolver of batched.resolvers) {
|
||||
resolver.reject(error);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
|
||||
const promise = get_response(__, arg, state, get_remote_function_result);
|
||||
|
||||
promise.catch(() => {});
|
||||
|
||||
promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);
|
||||
|
||||
promise.refresh = () => {
|
||||
const refresh_context = get_refresh_context(__, 'refresh', arg);
|
||||
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
|
||||
const value = is_immediate_refresh ? promise : get_remote_function_result();
|
||||
return update_refresh_value(refresh_context, value, is_immediate_refresh);
|
||||
};
|
||||
|
||||
promise.withOverride = () => {
|
||||
throw new Error(`Cannot call '${__.name}.withOverride()' on the server`);
|
||||
};
|
||||
|
||||
return /** @type {RemoteQuery<Output>} */ (promise);
|
||||
};
|
||||
|
||||
Object.defineProperty(wrapper, '__', { value: __ });
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Add batch as a property to the query function
|
||||
Object.defineProperty(query, 'batch', { value: batch, enumerable: true });
|
||||
|
||||
/**
|
||||
* @param {RemoteInfo} __
|
||||
* @param {'set' | 'refresh'} action
|
||||
* @param {any} [arg]
|
||||
* @returns {{ __: RemoteInfo; state: any; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }}
|
||||
*/
|
||||
function get_refresh_context(__, action, arg) {
|
||||
const { state } = get_request_store();
|
||||
const { refreshes } = state;
|
||||
|
||||
if (!refreshes) {
|
||||
const name = __.type === 'query_batch' ? `query.batch '${__.name}'` : `query '${__.name}'`;
|
||||
throw new Error(
|
||||
`Cannot call ${action} on ${name} because it is not executed in the context of a command/form remote function`
|
||||
);
|
||||
}
|
||||
|
||||
const cache = get_cache(__, state);
|
||||
const cache_key = stringify_remote_arg(arg, state.transport);
|
||||
const refreshes_key = create_remote_key(__.id, cache_key);
|
||||
|
||||
return { __, state, refreshes, refreshes_key, cache, cache_key };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ __: RemoteInfo; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }} context
|
||||
* @param {any} value
|
||||
* @param {boolean} [is_immediate_refresh=false]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function update_refresh_value(
|
||||
{ __, refreshes, refreshes_key, cache, cache_key },
|
||||
value,
|
||||
is_immediate_refresh = false
|
||||
) {
|
||||
const promise = Promise.resolve(value);
|
||||
|
||||
if (!is_immediate_refresh) {
|
||||
cache[cache_key] = promise;
|
||||
}
|
||||
|
||||
if (__.id) {
|
||||
refreshes[refreshes_key] = promise;
|
||||
}
|
||||
|
||||
return promise.then(() => {});
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/** @import { RequestEvent } from '@sveltejs/kit' */
|
||||
/** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo, RequestStore } from 'types' */
|
||||
import { parse } from 'devalue';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server';
|
||||
import { stringify_remote_arg } from '../../../shared.js';
|
||||
|
||||
/**
|
||||
* @param {any} validate_or_fn
|
||||
* @param {(arg?: any) => any} [maybe_fn]
|
||||
* @returns {(arg?: any) => MaybePromise<any>}
|
||||
*/
|
||||
export function create_validator(validate_or_fn, maybe_fn) {
|
||||
// prevent functions without validators being called with arguments
|
||||
if (!maybe_fn) {
|
||||
return (arg) => {
|
||||
if (arg !== undefined) {
|
||||
error(400, 'Bad Request');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// if 'unchecked', pass input through without validating
|
||||
if (validate_or_fn === 'unchecked') {
|
||||
return (arg) => arg;
|
||||
}
|
||||
|
||||
// use https://standardschema.dev validator if provided
|
||||
if ('~standard' in validate_or_fn) {
|
||||
return async (arg) => {
|
||||
// Get event before async validation to ensure it's available in server environments without AsyncLocalStorage, too
|
||||
const { event, state } = get_request_store();
|
||||
// access property and call method in one go to preserve potential this context
|
||||
const result = await validate_or_fn['~standard'].validate(arg);
|
||||
|
||||
// if the `issues` field exists, the validation failed
|
||||
if (result.issues) {
|
||||
error(
|
||||
400,
|
||||
await state.handleValidationError({
|
||||
issues: result.issues,
|
||||
event
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return result.value;
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Invalid validator passed to remote function. Expected "unchecked" or a Standard Schema (https://standardschema.dev)'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of a single remote function call, just returns the result.
|
||||
*
|
||||
* In case of a full page reload, returns the response for a remote function call,
|
||||
* either from the cache or by invoking the function.
|
||||
* Also saves an uneval'ed version of the result for later HTML inlining for hydration.
|
||||
*
|
||||
* @template {MaybePromise<any>} T
|
||||
* @param {RemoteInfo} info
|
||||
* @param {any} arg
|
||||
* @param {RequestState} state
|
||||
* @param {() => Promise<T>} get_result
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
export async function get_response(info, arg, state, get_result) {
|
||||
// wait a beat, in case `myQuery().set(...)` or `myQuery().refresh()` is immediately called
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await 0;
|
||||
|
||||
const cache = get_cache(info, state);
|
||||
|
||||
return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} data
|
||||
* @param {ServerHooks['transport']} transport
|
||||
*/
|
||||
export function parse_remote_response(data, transport) {
|
||||
/** @type {Record<string, any>} */
|
||||
const revivers = {};
|
||||
for (const key in transport) {
|
||||
revivers[key] = transport[key].decode;
|
||||
}
|
||||
|
||||
return parse(data, revivers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
|
||||
* @template T
|
||||
* @param {RequestEvent} event
|
||||
* @param {RequestState} state
|
||||
* @param {boolean} allow_cookies
|
||||
* @param {() => any} get_input
|
||||
* @param {(arg?: any) => T} fn
|
||||
*/
|
||||
export async function run_remote_function(event, state, allow_cookies, get_input, fn) {
|
||||
/** @type {RequestStore} */
|
||||
const store = {
|
||||
event: {
|
||||
...event,
|
||||
setHeaders: () => {
|
||||
throw new Error('setHeaders is not allowed in remote functions');
|
||||
},
|
||||
cookies: {
|
||||
...event.cookies,
|
||||
set: (name, value, opts) => {
|
||||
if (!allow_cookies) {
|
||||
throw new Error('Cannot set cookies in `query` or `prerender` functions');
|
||||
}
|
||||
|
||||
if (opts.path && !opts.path.startsWith('/')) {
|
||||
throw new Error('Cookies set in remote functions must have an absolute path');
|
||||
}
|
||||
|
||||
return event.cookies.set(name, value, opts);
|
||||
},
|
||||
delete: (name, opts) => {
|
||||
if (!allow_cookies) {
|
||||
throw new Error('Cannot delete cookies in `query` or `prerender` functions');
|
||||
}
|
||||
|
||||
if (opts.path && !opts.path.startsWith('/')) {
|
||||
throw new Error('Cookies deleted in remote functions must have an absolute path');
|
||||
}
|
||||
|
||||
return event.cookies.delete(name, opts);
|
||||
}
|
||||
}
|
||||
},
|
||||
state: {
|
||||
...state,
|
||||
is_in_remote_function: true
|
||||
}
|
||||
};
|
||||
|
||||
// In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function
|
||||
const input = await with_request_store(store, get_input);
|
||||
return with_request_store(store, () => fn(input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RemoteInfo} info
|
||||
* @param {RequestState} state
|
||||
*/
|
||||
export function get_cache(info, state = get_request_store().state) {
|
||||
let cache = state.remote_data?.get(info);
|
||||
|
||||
if (cache === undefined) {
|
||||
cache = {};
|
||||
(state.remote_data ??= new Map()).set(info, cache);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
page as _page,
|
||||
navigating as _navigating,
|
||||
updated as _updated
|
||||
} from '../../client/state.svelte.js';
|
||||
import { stores } from '../../client/client.js';
|
||||
|
||||
export const page = {
|
||||
get data() {
|
||||
return _page.data;
|
||||
},
|
||||
get error() {
|
||||
return _page.error;
|
||||
},
|
||||
get form() {
|
||||
return _page.form;
|
||||
},
|
||||
get params() {
|
||||
return _page.params;
|
||||
},
|
||||
get route() {
|
||||
return _page.route;
|
||||
},
|
||||
get state() {
|
||||
return _page.state;
|
||||
},
|
||||
get status() {
|
||||
return _page.status;
|
||||
},
|
||||
get url() {
|
||||
return _page.url;
|
||||
}
|
||||
};
|
||||
|
||||
export const navigating = {
|
||||
get from() {
|
||||
return _navigating.current ? _navigating.current.from : null;
|
||||
},
|
||||
get to() {
|
||||
return _navigating.current ? _navigating.current.to : null;
|
||||
},
|
||||
get type() {
|
||||
return _navigating.current ? _navigating.current.type : null;
|
||||
},
|
||||
get willUnload() {
|
||||
return _navigating.current ? _navigating.current.willUnload : null;
|
||||
},
|
||||
get delta() {
|
||||
return _navigating.current ? _navigating.current.delta : null;
|
||||
},
|
||||
get complete() {
|
||||
return _navigating.current ? _navigating.current.complete : null;
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(navigating, 'current', {
|
||||
get() {
|
||||
// between 2.12.0 and 2.12.1 `navigating.current` existed
|
||||
throw new Error('Replace navigating.current.<prop> with navigating.<prop>');
|
||||
}
|
||||
});
|
||||
|
||||
export const updated = {
|
||||
get current() {
|
||||
return _updated.current;
|
||||
},
|
||||
check: stores.updated.check
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
page as client_page,
|
||||
navigating as client_navigating,
|
||||
updated as client_updated
|
||||
} from './client.js';
|
||||
import {
|
||||
page as server_page,
|
||||
navigating as server_navigating,
|
||||
updated as server_updated
|
||||
} from './server.js';
|
||||
import { BROWSER } from 'esm-env';
|
||||
|
||||
/**
|
||||
* A read-only reactive object with information about the current page, serving several use cases:
|
||||
* - retrieving the combined `data` of all pages/layouts anywhere in your component tree (also see [loading data](https://svelte.dev/docs/kit/load))
|
||||
* - retrieving the current value of the `form` prop anywhere in your component tree (also see [form actions](https://svelte.dev/docs/kit/form-actions))
|
||||
* - retrieving the page state that was set through `goto`, `pushState` or `replaceState` (also see [goto](https://svelte.dev/docs/kit/$app-navigation#goto) and [shallow routing](https://svelte.dev/docs/kit/shallow-routing))
|
||||
* - retrieving metadata such as the URL you're on, the current route and its parameters, and whether or not there was an error
|
||||
*
|
||||
* ```svelte
|
||||
* <!--- file: +layout.svelte --->
|
||||
* <script>
|
||||
* import { page } from '$app/state';
|
||||
* </script>
|
||||
*
|
||||
* <p>Currently at {page.url.pathname}</p>
|
||||
*
|
||||
* {#if page.error}
|
||||
* <span class="red">Problem detected</span>
|
||||
* {:else}
|
||||
* <span class="small">All systems operational</span>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* Changes to `page` are available exclusively with runes. (The legacy reactivity syntax will not reflect any changes)
|
||||
*
|
||||
* ```svelte
|
||||
* <!--- file: +page.svelte --->
|
||||
* <script>
|
||||
* import { page } from '$app/state';
|
||||
* const id = $derived(page.params.id); // This will correctly update id for usage on this page
|
||||
* $: badId = page.params.id; // Do not use; will never update after initial load
|
||||
* </script>
|
||||
* ```
|
||||
*
|
||||
* On the server, values can only be read during rendering (in other words _not_ in e.g. `load` functions). In the browser, the values can be read at any time.
|
||||
*
|
||||
* @type {import('@sveltejs/kit').Page}
|
||||
*/
|
||||
export const page = BROWSER ? client_page : server_page;
|
||||
|
||||
/**
|
||||
* A read-only object representing an in-progress navigation, with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties.
|
||||
* Values are `null` when no navigation is occurring, or during server rendering.
|
||||
* @type {import('@sveltejs/kit').Navigation | { from: null, to: null, type: null, willUnload: null, delta: null, complete: null }}
|
||||
*/
|
||||
// @ts-expect-error
|
||||
export const navigating = BROWSER ? client_navigating : server_navigating;
|
||||
|
||||
/**
|
||||
* A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling.
|
||||
* @type {{ get current(): boolean; check(): Promise<boolean>; }}
|
||||
*/
|
||||
export const updated = BROWSER ? client_updated : server_updated;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
function context() {
|
||||
return getContext('__request__');
|
||||
}
|
||||
|
||||
/** @param {string} name */
|
||||
function context_dev(name) {
|
||||
try {
|
||||
return context();
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Can only read '${name}' on the server during rendering (not in e.g. \`load\` functions), as it is bound to the current request via component context. This prevents state from leaking between users. ` +
|
||||
'For more information, see https://svelte.dev/docs/kit/state-management#avoid-shared-state-on-the-server'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const page = {
|
||||
get data() {
|
||||
return (DEV ? context_dev('page.data') : context()).page.data;
|
||||
},
|
||||
get error() {
|
||||
return (DEV ? context_dev('page.error') : context()).page.error;
|
||||
},
|
||||
get form() {
|
||||
return (DEV ? context_dev('page.form') : context()).page.form;
|
||||
},
|
||||
get params() {
|
||||
return (DEV ? context_dev('page.params') : context()).page.params;
|
||||
},
|
||||
get route() {
|
||||
return (DEV ? context_dev('page.route') : context()).page.route;
|
||||
},
|
||||
get state() {
|
||||
return (DEV ? context_dev('page.state') : context()).page.state;
|
||||
},
|
||||
get status() {
|
||||
return (DEV ? context_dev('page.status') : context()).page.status;
|
||||
},
|
||||
get url() {
|
||||
return (DEV ? context_dev('page.url') : context()).page.url;
|
||||
}
|
||||
};
|
||||
|
||||
export const navigating = {
|
||||
from: null,
|
||||
to: null,
|
||||
type: null,
|
||||
willUnload: null,
|
||||
delta: null,
|
||||
complete: null
|
||||
};
|
||||
|
||||
export const updated = {
|
||||
get current() {
|
||||
return false;
|
||||
},
|
||||
check: () => {
|
||||
throw new Error('Can only call updated.check() in the browser');
|
||||
}
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { getContext } from 'svelte';
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import { stores as browser_stores } from '../client/client.js';
|
||||
|
||||
/**
|
||||
* A function that returns all of the contextual stores. On the server, this must be called during component initialization.
|
||||
* Only use this if you need to defer store subscription until after the component has mounted, for some reason.
|
||||
*
|
||||
* @deprecated Use `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated))
|
||||
*/
|
||||
export const getStores = () => {
|
||||
const stores = BROWSER ? browser_stores : getContext('__svelte__');
|
||||
|
||||
return {
|
||||
/** @type {typeof page} */
|
||||
page: {
|
||||
subscribe: stores.page.subscribe
|
||||
},
|
||||
/** @type {typeof navigating} */
|
||||
navigating: {
|
||||
subscribe: stores.navigating.subscribe
|
||||
},
|
||||
/** @type {typeof updated} */
|
||||
updated: stores.updated
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A readable store whose value contains page data.
|
||||
*
|
||||
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time.
|
||||
*
|
||||
* @deprecated Use `page` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated))
|
||||
* @type {import('svelte/store').Readable<import('@sveltejs/kit').Page>}
|
||||
*/
|
||||
export const page = {
|
||||
subscribe(fn) {
|
||||
const store = DEV ? get_store('page') : getStores().page;
|
||||
return store.subscribe(fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A readable store.
|
||||
* When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties.
|
||||
* When navigating finishes, its value reverts to `null`.
|
||||
*
|
||||
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time.
|
||||
*
|
||||
* @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated))
|
||||
* @type {import('svelte/store').Readable<import('@sveltejs/kit').Navigation | null>}
|
||||
*/
|
||||
export const navigating = {
|
||||
subscribe(fn) {
|
||||
const store = DEV ? get_store('navigating') : getStores().navigating;
|
||||
return store.subscribe(fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A readable store whose initial value is `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling.
|
||||
*
|
||||
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time.
|
||||
*
|
||||
* @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated))
|
||||
* @type {import('svelte/store').Readable<boolean> & { check(): Promise<boolean> }}
|
||||
*/
|
||||
export const updated = {
|
||||
subscribe(fn) {
|
||||
const store = DEV ? get_store('updated') : getStores().updated;
|
||||
|
||||
if (BROWSER) {
|
||||
updated.check = store.check;
|
||||
}
|
||||
|
||||
return store.subscribe(fn);
|
||||
},
|
||||
check: () => {
|
||||
throw new Error(
|
||||
BROWSER
|
||||
? 'Cannot check updated store before subscribing'
|
||||
: 'Can only check updated store in browser'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @template {keyof ReturnType<typeof getStores>} Name
|
||||
* @param {Name} name
|
||||
* @returns {ReturnType<typeof getStores>[Name]}
|
||||
*/
|
||||
function get_store(name) {
|
||||
try {
|
||||
return getStores()[name];
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Cannot subscribe to '${name}' store on the server outside of a Svelte component, as it is bound to the current request via component context. This prevents state from leaking between users.` +
|
||||
'For more information, see https://svelte.dev/docs/kit/state-management#avoid-shared-state-on-the-server'
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/* if `bundleStrategy` is 'single' or 'inline', this file is used as the entry point */
|
||||
|
||||
import * as kit from './entry.js';
|
||||
|
||||
// @ts-expect-error
|
||||
import * as app from '__sveltekit/manifest';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {import('./types.js').HydrateOptions} options
|
||||
*/
|
||||
export function start(element, options) {
|
||||
void kit.start(app, element, options);
|
||||
}
|
||||
|
||||
export { app };
|
||||
+3309
File diff suppressed because it is too large
Load Diff
+16
@@ -0,0 +1,16 @@
|
||||
export const SNAPSHOT_KEY = 'sveltekit:snapshot';
|
||||
export const SCROLL_KEY = 'sveltekit:scroll';
|
||||
export const STATES_KEY = 'sveltekit:states';
|
||||
export const PAGE_URL_KEY = 'sveltekit:pageurl';
|
||||
|
||||
export const HISTORY_INDEX = 'sveltekit:history';
|
||||
export const NAVIGATION_INDEX = 'sveltekit:navigation';
|
||||
|
||||
export const PRELOAD_PRIORITIES = /** @type {const} */ ({
|
||||
tap: 1,
|
||||
hover: 2,
|
||||
viewport: 3,
|
||||
eager: 4,
|
||||
off: -1,
|
||||
false: -1
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// we expose this as a separate entry point (rather than treating client.js as the entry point)
|
||||
// so that everything other than `start`/`load_css` can be treeshaken
|
||||
export { start, load_css } from './client.js';
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import { hash } from '../../utils/hash.js';
|
||||
import { base64_decode } from '../utils.js';
|
||||
|
||||
let loading = 0;
|
||||
|
||||
/** @type {typeof fetch} */
|
||||
const native_fetch = BROWSER ? window.fetch : /** @type {any} */ (() => {});
|
||||
|
||||
export function lock_fetch() {
|
||||
loading += 1;
|
||||
}
|
||||
|
||||
export function unlock_fetch() {
|
||||
loading -= 1;
|
||||
}
|
||||
|
||||
if (DEV && BROWSER) {
|
||||
let can_inspect_stack_trace = false;
|
||||
|
||||
// detect whether async stack traces work
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
const check_stack_trace = async () => {
|
||||
const stack = /** @type {string} */ (new Error().stack);
|
||||
can_inspect_stack_trace = stack.includes('check_stack_trace');
|
||||
};
|
||||
|
||||
void check_stack_trace();
|
||||
|
||||
/**
|
||||
* @param {RequestInfo | URL} input
|
||||
* @param {RequestInit & Record<string, any> | undefined} init
|
||||
*/
|
||||
window.fetch = (input, init) => {
|
||||
// Check if fetch was called via load_node. the lock method only checks if it was called at the
|
||||
// same time, but not necessarily if it was called from `load`.
|
||||
// We use just the filename as the method name sometimes does not appear on the CI.
|
||||
const url = input instanceof Request ? input.url : input.toString();
|
||||
const stack_array = /** @type {string} */ (new Error().stack).split('\n');
|
||||
// We need to do a cutoff because Safari and Firefox maintain the stack
|
||||
// across events and for example traces a `fetch` call triggered from a button
|
||||
// back to the creation of the event listener and the element creation itself,
|
||||
// where at some point client.js will show up, leading to false positives.
|
||||
const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load'));
|
||||
const stack = stack_array.slice(0, cutoff + 2).join('\n');
|
||||
|
||||
const in_load_heuristic = can_inspect_stack_trace
|
||||
? stack.includes('src/runtime/client/client.js')
|
||||
: loading;
|
||||
|
||||
// This flag is set in initial_fetch and subsequent_fetch
|
||||
const used_kit_fetch = init?.__sveltekit_fetch__;
|
||||
|
||||
if (in_load_heuristic && !used_kit_fetch) {
|
||||
console.warn(
|
||||
`Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://svelte.dev/docs/kit/load#making-fetch-requests`
|
||||
);
|
||||
}
|
||||
|
||||
const method = input instanceof Request ? input.method : init?.method || 'GET';
|
||||
|
||||
if (method !== 'GET') {
|
||||
cache.delete(build_selector(input));
|
||||
}
|
||||
|
||||
return native_fetch(input, init);
|
||||
};
|
||||
} else if (BROWSER) {
|
||||
window.fetch = (input, init) => {
|
||||
const method = input instanceof Request ? input.method : init?.method || 'GET';
|
||||
|
||||
if (method !== 'GET') {
|
||||
cache.delete(build_selector(input));
|
||||
}
|
||||
|
||||
return native_fetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
/**
|
||||
* Should be called on the initial run of load functions that hydrate the page.
|
||||
* Saves any requests with cache-control max-age to the cache.
|
||||
* @param {URL | string} resource
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
export function initial_fetch(resource, opts) {
|
||||
const selector = build_selector(resource, opts);
|
||||
|
||||
const script = document.querySelector(selector);
|
||||
if (script?.textContent) {
|
||||
script.remove(); // In case multiple script tags match the same selector
|
||||
let { body, ...init } = JSON.parse(script.textContent);
|
||||
|
||||
const ttl = script.getAttribute('data-ttl');
|
||||
if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
|
||||
const b64 = script.getAttribute('data-b64');
|
||||
if (b64 !== null) {
|
||||
// Can't use native_fetch('data:...;base64,${body}')
|
||||
// csp can block the request
|
||||
body = base64_decode(body);
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(body, init));
|
||||
}
|
||||
|
||||
return DEV ? dev_fetch(resource, opts) : window.fetch(resource, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get the response from the cache, if max-age allows it, else does a fetch.
|
||||
* @param {URL | string} resource
|
||||
* @param {string} resolved
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
export function subsequent_fetch(resource, resolved, opts) {
|
||||
if (cache.size > 0) {
|
||||
const selector = build_selector(resource, opts);
|
||||
const cached = cache.get(selector);
|
||||
if (cached) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/cache#value
|
||||
if (
|
||||
performance.now() < cached.ttl &&
|
||||
['default', 'force-cache', 'only-if-cached', undefined].includes(opts?.cache)
|
||||
) {
|
||||
return new Response(cached.body, cached.init);
|
||||
}
|
||||
|
||||
cache.delete(selector);
|
||||
}
|
||||
}
|
||||
|
||||
return DEV ? dev_fetch(resolved, opts) : window.fetch(resolved, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RequestInfo | URL} resource
|
||||
* @param {RequestInit & Record<string, any> | undefined} opts
|
||||
*/
|
||||
export function dev_fetch(resource, opts) {
|
||||
const patched_opts = { ...opts };
|
||||
// This assigns the __sveltekit_fetch__ flag and makes it non-enumerable
|
||||
Object.defineProperty(patched_opts, '__sveltekit_fetch__', {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
return window.fetch(resource, patched_opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the cache key for a given request
|
||||
* @param {URL | RequestInfo} resource
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
function build_selector(resource, opts) {
|
||||
const url = JSON.stringify(resource instanceof Request ? resource.url : resource);
|
||||
|
||||
let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
|
||||
|
||||
if (opts?.headers || opts?.body) {
|
||||
/** @type {import('types').StrictBody[]} */
|
||||
const values = [];
|
||||
|
||||
if (opts.headers) {
|
||||
values.push([...new Headers(opts.headers)].join(','));
|
||||
}
|
||||
|
||||
if (opts.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
|
||||
values.push(opts.body);
|
||||
}
|
||||
|
||||
selector += `[data-hash="${hash(...values)}"]`;
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { exec, parse_route_id } from '../../utils/routing.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types.js').SvelteKitApp} app
|
||||
* @returns {import('types').CSRRoute[]}
|
||||
*/
|
||||
export function parse({ nodes, server_loads, dictionary, matchers }) {
|
||||
const layouts_with_server_load = new Set(server_loads);
|
||||
|
||||
return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
|
||||
const { pattern, params } = parse_route_id(id);
|
||||
|
||||
/** @type {import('types').CSRRoute} */
|
||||
const route = {
|
||||
id,
|
||||
/** @param {string} path */
|
||||
exec: (path) => {
|
||||
const match = pattern.exec(path);
|
||||
if (match) return exec(match, params, matchers);
|
||||
},
|
||||
errors: [1, ...(errors || [])].map((n) => nodes[n]),
|
||||
layouts: [0, ...(layouts || [])].map(create_layout_loader),
|
||||
leaf: create_leaf_loader(leaf)
|
||||
};
|
||||
|
||||
// bit of a hack, but ensures that layout/error node lists are the same
|
||||
// length, without which the wrong data will be applied if the route
|
||||
// manifest looks like `[[a, b], [c,], d]`
|
||||
route.errors.length = route.layouts.length = Math.max(
|
||||
route.errors.length,
|
||||
route.layouts.length
|
||||
);
|
||||
|
||||
return route;
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {[boolean, import('types').CSRPageNodeLoader]}
|
||||
*/
|
||||
function create_leaf_loader(id) {
|
||||
// whether or not the route uses the server data is
|
||||
// encoded using the ones' complement, to save space
|
||||
const uses_server_data = id < 0;
|
||||
if (uses_server_data) id = ~id;
|
||||
return [uses_server_data, nodes[id]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number | undefined} id
|
||||
* @returns {[boolean, import('types').CSRPageNodeLoader] | undefined}
|
||||
*/
|
||||
function create_layout_loader(id) {
|
||||
// whether or not the layout uses the server data is
|
||||
// encoded in the layouts array, to save space
|
||||
return id === undefined ? id : [layouts_with_server_load.has(id), nodes[id]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').CSRRouteServer} input
|
||||
* @param {import('types').CSRPageNodeLoader[]} app_nodes Will be modified if a new node is loaded that's not already in the array
|
||||
* @returns {import('types').CSRRoute}
|
||||
*/
|
||||
export function parse_server_route({ nodes, id, leaf, layouts, errors }, app_nodes) {
|
||||
return {
|
||||
id,
|
||||
exec: () => ({}), // dummy function; exec already happened on the server
|
||||
// By writing to app_nodes only when a loader at that index is not already defined,
|
||||
// we ensure that loaders have referential equality when they load the same node.
|
||||
// Code elsewhere in client.js relies on this referential equality to determine
|
||||
// if a loader is different and should therefore (re-)run.
|
||||
errors: errors.map((n) => (n ? (app_nodes[n] ||= nodes[n]) : undefined)),
|
||||
layouts: layouts.map((n) => (n ? [n[0], (app_nodes[n[1]] ||= nodes[n[1]])] : undefined)),
|
||||
leaf: [leaf[0], (app_nodes[leaf[1]] ||= nodes[leaf[1]])]
|
||||
};
|
||||
}
|
||||
Generated
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
/** @import { RemoteCommand, RemoteQueryOverride } from '@sveltejs/kit' */
|
||||
/** @import { RemoteFunctionResponse } from 'types' */
|
||||
/** @import { Query } from './query.svelte.js' */
|
||||
import { app_dir, base } from '$app/paths/internal/client';
|
||||
import * as devalue from 'devalue';
|
||||
import { HttpError } from '@sveltejs/kit/internal';
|
||||
import { app } from '../client.js';
|
||||
import { stringify_remote_arg } from '../../shared.js';
|
||||
import { get_remote_request_headers, refresh_queries, release_overrides } from './shared.svelte.js';
|
||||
|
||||
/**
|
||||
* Client-version of the `command` function from `$app/server`.
|
||||
* @param {string} id
|
||||
* @returns {RemoteCommand<any, any>}
|
||||
*/
|
||||
export function command(id) {
|
||||
/** @type {number} */
|
||||
let pending_count = $state(0);
|
||||
|
||||
// Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with an updates() method.
|
||||
// If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want.
|
||||
/** @type {RemoteCommand<any, any>} */
|
||||
const command_function = (arg) => {
|
||||
/** @type {Array<Query<any> | RemoteQueryOverride>} */
|
||||
let updates = [];
|
||||
|
||||
// Increment pending count when command starts
|
||||
pending_count++;
|
||||
|
||||
// Noone should call commands during rendering but belts and braces.
|
||||
// Do this here, after await Svelte' reactivity context is gone.
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...get_remote_request_headers()
|
||||
};
|
||||
|
||||
/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
|
||||
const promise = (async () => {
|
||||
try {
|
||||
// Wait a tick to give room for the `updates` method to be called
|
||||
await Promise.resolve();
|
||||
|
||||
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
payload: stringify_remote_arg(arg, app.hooks.transport),
|
||||
refreshes: updates.map((u) => u._key)
|
||||
}),
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
release_overrides(updates);
|
||||
// We only end up here in case of a network error or if the server has an internal error
|
||||
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
|
||||
throw new Error('Failed to execute remote function');
|
||||
}
|
||||
|
||||
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
||||
if (result.type === 'redirect') {
|
||||
release_overrides(updates);
|
||||
throw new Error(
|
||||
'Redirects are not allowed in commands. Return a result instead and use goto on the client'
|
||||
);
|
||||
} else if (result.type === 'error') {
|
||||
release_overrides(updates);
|
||||
throw new HttpError(result.status ?? 500, result.error);
|
||||
} else {
|
||||
if (result.refreshes) {
|
||||
refresh_queries(result.refreshes, updates);
|
||||
}
|
||||
|
||||
return devalue.parse(result.result, app.decoders);
|
||||
}
|
||||
} finally {
|
||||
// Decrement pending count when command completes
|
||||
pending_count--;
|
||||
}
|
||||
})();
|
||||
|
||||
promise.updates = (/** @type {any} */ ...args) => {
|
||||
updates = args;
|
||||
// @ts-expect-error Don't allow updates to be called multiple times
|
||||
delete promise.updates;
|
||||
return promise;
|
||||
};
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
Object.defineProperty(command_function, 'pending', {
|
||||
get: () => pending_count
|
||||
});
|
||||
|
||||
return command_function;
|
||||
}
|
||||
Generated
Vendored
+650
@@ -0,0 +1,650 @@
|
||||
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
||||
/** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
|
||||
/** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
|
||||
/** @import { Query } from './query.svelte.js' */
|
||||
import { app_dir, base } from '$app/paths/internal/client';
|
||||
import * as devalue from 'devalue';
|
||||
import { DEV } from 'esm-env';
|
||||
import { HttpError } from '@sveltejs/kit/internal';
|
||||
import { app, remote_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
|
||||
import { tick } from 'svelte';
|
||||
import { refresh_queries, release_overrides } from './shared.svelte.js';
|
||||
import { createAttachmentKey } from 'svelte/attachments';
|
||||
import {
|
||||
convert_formdata,
|
||||
flatten_issues,
|
||||
create_field_proxy,
|
||||
deep_set,
|
||||
set_nested_value,
|
||||
throw_on_old_property_access,
|
||||
build_path_string,
|
||||
normalize_issue,
|
||||
serialize_binary_form,
|
||||
BINARY_FORM_CONTENT_TYPE
|
||||
} from '../../form-utils.js';
|
||||
|
||||
/**
|
||||
* Merge client issues into server issues. Server issues are persisted unless
|
||||
* a client-issue exists for the same path, in which case the client-issue overrides it.
|
||||
* @param {FormData} form_data
|
||||
* @param {InternalRemoteFormIssue[]} current_issues
|
||||
* @param {InternalRemoteFormIssue[]} client_issues
|
||||
* @returns {InternalRemoteFormIssue[]}
|
||||
*/
|
||||
function merge_with_server_issues(form_data, current_issues, client_issues) {
|
||||
const merged = [
|
||||
...current_issues.filter(
|
||||
(issue) => issue.server && !client_issues.some((i) => i.name === issue.name)
|
||||
),
|
||||
...client_issues
|
||||
];
|
||||
|
||||
const keys = Array.from(form_data.keys());
|
||||
|
||||
return merged.sort((a, b) => keys.indexOf(a.name) - keys.indexOf(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-version of the `form` function from `$app/server`.
|
||||
* @template {RemoteFormInput} T
|
||||
* @template U
|
||||
* @param {string} id
|
||||
* @returns {RemoteForm<T, U>}
|
||||
*/
|
||||
export function form(id) {
|
||||
/** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- we don't need reactivity for this
|
||||
const instances = new Map();
|
||||
|
||||
/** @param {string | number | boolean} [key] */
|
||||
function create_instance(key) {
|
||||
const action_id_without_key = id;
|
||||
const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
|
||||
const action = '?/remote=' + encodeURIComponent(action_id);
|
||||
|
||||
/**
|
||||
* @type {Record<string, string | string[] | File | File[]>}
|
||||
*/
|
||||
let input = $state({});
|
||||
|
||||
/** @type {InternalRemoteFormIssue[]} */
|
||||
let raw_issues = $state.raw([]);
|
||||
|
||||
const issues = $derived(flatten_issues(raw_issues));
|
||||
|
||||
/** @type {any} */
|
||||
let result = $state.raw(remote_responses[action_id]);
|
||||
|
||||
/** @type {number} */
|
||||
let pending_count = $state(0);
|
||||
|
||||
/** @type {StandardSchemaV1 | undefined} */
|
||||
let preflight_schema = undefined;
|
||||
|
||||
/** @type {HTMLFormElement | null} */
|
||||
let element = null;
|
||||
|
||||
/** @type {Record<string, boolean>} */
|
||||
let touched = {};
|
||||
|
||||
let submitted = false;
|
||||
|
||||
/**
|
||||
* @param {FormData} form_data
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
function convert(form_data) {
|
||||
const data = convert_formdata(form_data);
|
||||
if (key !== undefined && !form_data.has('id')) {
|
||||
data.id = key;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement} form
|
||||
* @param {FormData} form_data
|
||||
* @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback
|
||||
*/
|
||||
async function handle_submit(form, form_data, callback) {
|
||||
const data = convert(form_data);
|
||||
|
||||
submitted = true;
|
||||
|
||||
// Increment pending count immediately so that `pending` reflects
|
||||
// the in-progress state during async preflight validation
|
||||
pending_count++;
|
||||
|
||||
const validated = await preflight_schema?.['~standard'].validate(data);
|
||||
|
||||
if (validated?.issues) {
|
||||
raw_issues = merge_with_server_issues(
|
||||
form_data,
|
||||
raw_issues,
|
||||
validated.issues.map((issue) => normalize_issue(issue, false))
|
||||
);
|
||||
pending_count--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Preflight passed - clear stale client-side preflight issues
|
||||
if (preflight_schema) {
|
||||
raw_issues = raw_issues.filter((issue) => issue.server);
|
||||
}
|
||||
|
||||
// TODO 3.0 remove this warning
|
||||
if (DEV) {
|
||||
const error = () => {
|
||||
throw new Error(
|
||||
'Remote form functions no longer get passed a FormData object. The payload is now a POJO. See https://kit.svelte.dev/docs/remote-functions#form for details.'
|
||||
);
|
||||
};
|
||||
for (const key of [
|
||||
'append',
|
||||
'delete',
|
||||
'entries',
|
||||
'forEach',
|
||||
'get',
|
||||
'getAll',
|
||||
'has',
|
||||
'keys',
|
||||
'set',
|
||||
'values'
|
||||
]) {
|
||||
if (!(key in data)) {
|
||||
Object.defineProperty(data, key, { get: error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await callback({
|
||||
form,
|
||||
data,
|
||||
submit: () => submit(form_data)
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
|
||||
const status = e instanceof HttpError ? e.status : 500;
|
||||
void set_nearest_error_page(error, status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} data
|
||||
* @returns {Promise<any> & { updates: (...args: any[]) => any }}
|
||||
*/
|
||||
function submit(data) {
|
||||
// Store a reference to the current instance and increment the usage count for the duration
|
||||
// of the request. This ensures that the instance is not deleted in case of an optimistic update
|
||||
// (e.g. when deleting an item in a list) that fails and wants to surface an error to the user afterwards.
|
||||
// If the instance would be deleted in the meantime, the error property would be assigned to the old,
|
||||
// no-longer-visible instance, so it would never be shown to the user.
|
||||
const entry = instances.get(key);
|
||||
if (entry) {
|
||||
entry.count++;
|
||||
}
|
||||
|
||||
/** @type {Array<Query<any> | RemoteQueryOverride>} */
|
||||
let updates = [];
|
||||
|
||||
/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
|
||||
const promise = (async () => {
|
||||
try {
|
||||
await Promise.resolve();
|
||||
|
||||
const { blob } = serialize_binary_form(convert(data), {
|
||||
remote_refreshes: updates.map((u) => u._key)
|
||||
});
|
||||
|
||||
const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': BINARY_FORM_CONTENT_TYPE,
|
||||
// Forms cannot be called during rendering, so it's save to use location here
|
||||
'x-sveltekit-pathname': location.pathname,
|
||||
'x-sveltekit-search': location.search
|
||||
},
|
||||
body: blob
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// We only end up here in case of a network error or if the server has an internal error
|
||||
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
|
||||
throw new Error('Failed to execute remote function');
|
||||
}
|
||||
|
||||
const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
|
||||
|
||||
// reset issues in case it's a redirect or error (but issues passed in that case)
|
||||
raw_issues = [];
|
||||
|
||||
if (form_result.type === 'result') {
|
||||
({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
|
||||
|
||||
if (issues.$) {
|
||||
release_overrides(updates);
|
||||
} else {
|
||||
if (form_result.refreshes) {
|
||||
refresh_queries(form_result.refreshes, updates);
|
||||
} else {
|
||||
void invalidateAll();
|
||||
}
|
||||
}
|
||||
} else if (form_result.type === 'redirect') {
|
||||
const refreshes = form_result.refreshes ?? '';
|
||||
const invalidateAll = !refreshes && updates.length === 0;
|
||||
if (!invalidateAll) {
|
||||
refresh_queries(refreshes, updates);
|
||||
}
|
||||
// Use internal version to allow redirects to external URLs
|
||||
void _goto(form_result.location, { invalidateAll }, 0);
|
||||
} else {
|
||||
throw new HttpError(form_result.status ?? 500, form_result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
result = undefined;
|
||||
release_overrides(updates);
|
||||
throw e;
|
||||
} finally {
|
||||
// Decrement pending count when submission completes
|
||||
pending_count--;
|
||||
|
||||
void tick().then(() => {
|
||||
if (entry) {
|
||||
entry.count--;
|
||||
if (entry.count === 0) {
|
||||
instances.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
promise.updates = (...args) => {
|
||||
updates = args;
|
||||
return promise;
|
||||
};
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** @type {RemoteForm<T, U>} */
|
||||
const instance = {};
|
||||
|
||||
instance.method = 'POST';
|
||||
instance.action = action;
|
||||
|
||||
/** @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback */
|
||||
const form_onsubmit = (callback) => {
|
||||
/** @param {SubmitEvent} event */
|
||||
return async (event) => {
|
||||
const form = /** @type {HTMLFormElement} */ (event.target);
|
||||
const method = event.submitter?.hasAttribute('formmethod')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
|
||||
: clone(form).method;
|
||||
|
||||
if (method !== 'post') return;
|
||||
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const action = new URL(
|
||||
// We can't do submitter.formAction directly because that property is always set
|
||||
event.submitter?.hasAttribute('formaction')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
|
||||
: clone(form).action
|
||||
);
|
||||
|
||||
if (action.searchParams.get('/remote') !== action_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.submitter?.hasAttribute('formtarget')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formTarget
|
||||
: clone(form).target;
|
||||
|
||||
if (target === '_blank') {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const form_data = new FormData(form, event.submitter);
|
||||
|
||||
if (DEV) {
|
||||
validate_form_data(form_data, clone(form).enctype);
|
||||
}
|
||||
|
||||
await handle_submit(form, form_data, callback);
|
||||
};
|
||||
};
|
||||
|
||||
/** @param {(event: SubmitEvent) => void} onsubmit */
|
||||
function create_attachment(onsubmit) {
|
||||
return (/** @type {HTMLFormElement} */ form) => {
|
||||
if (element) {
|
||||
let message = `A form object can only be attached to a single \`<form>\` element`;
|
||||
if (DEV && !key) {
|
||||
const name = id.split('/').pop();
|
||||
message += `. To create multiple instances, use \`${name}.for(key)\``;
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
element = form;
|
||||
|
||||
touched = {};
|
||||
|
||||
form.addEventListener('submit', onsubmit);
|
||||
|
||||
/** @param {Event} e */
|
||||
const handle_input = (e) => {
|
||||
// strictly speaking it can be an HTMLTextAreaElement or HTMLSelectElement
|
||||
// but that makes the types unnecessarily awkward
|
||||
const element = /** @type {HTMLInputElement} */ (e.target);
|
||||
|
||||
let name = element.name;
|
||||
if (!name) return;
|
||||
|
||||
const is_array = name.endsWith('[]');
|
||||
if (is_array) name = name.slice(0, -2);
|
||||
|
||||
const is_file = element.type === 'file';
|
||||
|
||||
touched[name] = true;
|
||||
|
||||
if (is_array) {
|
||||
let value;
|
||||
|
||||
if (element.tagName === 'SELECT') {
|
||||
value = Array.from(
|
||||
element.querySelectorAll('option:checked'),
|
||||
(e) => /** @type {HTMLOptionElement} */ (e).value
|
||||
);
|
||||
} else {
|
||||
const elements = /** @type {HTMLInputElement[]} */ (
|
||||
Array.from(form.querySelectorAll(`[name="${name}[]"]`))
|
||||
);
|
||||
|
||||
if (DEV) {
|
||||
for (const e of elements) {
|
||||
if ((e.type === 'file') !== is_file) {
|
||||
throw new Error(
|
||||
`Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = is_file
|
||||
? elements.map((input) => Array.from(input.files ?? [])).flat()
|
||||
: elements.map((element) => element.value);
|
||||
if (element.type === 'checkbox') {
|
||||
value = /** @type {string[]} */ (value.filter((_, i) => elements[i].checked));
|
||||
}
|
||||
}
|
||||
|
||||
set_nested_value(input, name, value);
|
||||
} else if (is_file) {
|
||||
if (DEV && element.multiple) {
|
||||
throw new Error(
|
||||
`Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix — consider changing "${name}" to "${name}[]"`
|
||||
);
|
||||
}
|
||||
|
||||
const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
|
||||
|
||||
if (file) {
|
||||
set_nested_value(input, name, file);
|
||||
} else {
|
||||
// Remove the property by setting to undefined and clean up
|
||||
const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
|
||||
let current = /** @type {any} */ (input);
|
||||
for (let i = 0; i < path_parts.length - 1; i++) {
|
||||
if (current[path_parts[i]] == null) return;
|
||||
current = current[path_parts[i]];
|
||||
}
|
||||
delete current[path_parts[path_parts.length - 1]];
|
||||
}
|
||||
} else {
|
||||
set_nested_value(
|
||||
input,
|
||||
name,
|
||||
element.type === 'checkbox' && !element.checked ? null : element.value
|
||||
);
|
||||
}
|
||||
|
||||
name = name.replace(/^[nb]:/, '');
|
||||
|
||||
touched[name] = true;
|
||||
};
|
||||
|
||||
form.addEventListener('input', handle_input);
|
||||
|
||||
const handle_reset = async () => {
|
||||
// need to wait a moment, because the `reset` event occurs before
|
||||
// the inputs are actually updated (so that it can be cancelled)
|
||||
await tick();
|
||||
|
||||
input = convert_formdata(new FormData(form));
|
||||
};
|
||||
|
||||
form.addEventListener('reset', handle_reset);
|
||||
|
||||
return () => {
|
||||
form.removeEventListener('submit', onsubmit);
|
||||
form.removeEventListener('input', handle_input);
|
||||
form.removeEventListener('reset', handle_reset);
|
||||
element = null;
|
||||
preflight_schema = undefined;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
instance[createAttachmentKey()] = create_attachment(
|
||||
form_onsubmit(({ submit, form }) =>
|
||||
submit().then(() => {
|
||||
if (!issues.$) {
|
||||
form.reset();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
let validate_id = 0;
|
||||
|
||||
// TODO 3.0 remove
|
||||
if (DEV) {
|
||||
throw_on_old_property_access(instance);
|
||||
|
||||
Object.defineProperty(instance, 'buttonProps', {
|
||||
get() {
|
||||
throw new Error(
|
||||
'`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
|
||||
' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperties(instance, {
|
||||
fields: {
|
||||
get: () =>
|
||||
create_field_proxy(
|
||||
{},
|
||||
() => input,
|
||||
(path, value) => {
|
||||
if (path.length === 0) {
|
||||
input = value;
|
||||
} else {
|
||||
deep_set(input, path.map(String), value);
|
||||
|
||||
const key = build_path_string(path);
|
||||
touched[key] = true;
|
||||
}
|
||||
},
|
||||
() => issues
|
||||
)
|
||||
},
|
||||
result: {
|
||||
get: () => result
|
||||
},
|
||||
pending: {
|
||||
get: () => pending_count
|
||||
},
|
||||
preflight: {
|
||||
/** @type {RemoteForm<T, U>['preflight']} */
|
||||
value: (schema) => {
|
||||
preflight_schema = schema;
|
||||
return instance;
|
||||
}
|
||||
},
|
||||
validate: {
|
||||
/** @type {RemoteForm<any, any>['validate']} */
|
||||
value: async ({ includeUntouched = false, preflightOnly = false } = {}) => {
|
||||
if (!element) return;
|
||||
|
||||
const id = ++validate_id;
|
||||
|
||||
// wait a tick in case the user is calling validate() right after set() which takes time to propagate
|
||||
await tick();
|
||||
|
||||
const default_submitter = /** @type {HTMLElement | undefined} */ (
|
||||
element.querySelector('button:not([type]), [type="submit"]')
|
||||
);
|
||||
|
||||
const form_data = new FormData(element, default_submitter);
|
||||
|
||||
/** @type {InternalRemoteFormIssue[]} */
|
||||
let array = [];
|
||||
|
||||
const data = convert(form_data);
|
||||
|
||||
const validated = await preflight_schema?.['~standard'].validate(data);
|
||||
|
||||
if (validate_id !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validated?.issues) {
|
||||
array = validated.issues.map((issue) => normalize_issue(issue, false));
|
||||
} else if (!preflightOnly) {
|
||||
const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': BINARY_FORM_CONTENT_TYPE,
|
||||
// Validation should not be and will not be called during rendering, so it's save to use location here
|
||||
'x-sveltekit-pathname': location.pathname,
|
||||
'x-sveltekit-search': location.search
|
||||
},
|
||||
body: serialize_binary_form(data, {
|
||||
validate_only: true
|
||||
}).blob
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (validate_id !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'result') {
|
||||
array = /** @type {InternalRemoteFormIssue[]} */ (
|
||||
devalue.parse(result.result, app.decoders)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!includeUntouched && !submitted) {
|
||||
array = array.filter((issue) => touched[issue.name]);
|
||||
}
|
||||
|
||||
const is_server_validation = !validated?.issues && !preflightOnly;
|
||||
|
||||
raw_issues = is_server_validation
|
||||
? array
|
||||
: merge_with_server_issues(form_data, raw_issues, array);
|
||||
}
|
||||
},
|
||||
enhance: {
|
||||
/** @type {RemoteForm<any, any>['enhance']} */
|
||||
value: (callback) => {
|
||||
return {
|
||||
method: 'POST',
|
||||
action,
|
||||
[createAttachmentKey()]: create_attachment(form_onsubmit(callback))
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
const instance = create_instance();
|
||||
|
||||
Object.defineProperty(instance, 'for', {
|
||||
/** @type {RemoteForm<T, U>['for']} */
|
||||
value: (key) => {
|
||||
const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) };
|
||||
|
||||
try {
|
||||
$effect.pre(() => {
|
||||
return () => {
|
||||
entry.count--;
|
||||
|
||||
void tick().then(() => {
|
||||
if (entry.count === 0) {
|
||||
instances.delete(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
entry.count += 1;
|
||||
instances.set(key, entry);
|
||||
} catch {
|
||||
// not in an effect context
|
||||
}
|
||||
|
||||
return entry.instance;
|
||||
}
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow clone an element, so that we can access e.g. `form.action` without worrying
|
||||
* that someone has added an `<input name="action">` (https://github.com/sveltejs/kit/issues/7593)
|
||||
* @template {HTMLElement} T
|
||||
* @param {T} element
|
||||
* @returns {T}
|
||||
*/
|
||||
function clone(element) {
|
||||
return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} form_data
|
||||
* @param {string} enctype
|
||||
*/
|
||||
function validate_form_data(form_data, enctype) {
|
||||
for (const key of form_data.keys()) {
|
||||
if (/^\$[.[]?/.test(key)) {
|
||||
throw new Error(
|
||||
'`$` is used to collect all FormData validation issues and cannot be used as the `name` of a form control'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (enctype !== 'multipart/form-data') {
|
||||
for (const value of form_data.values()) {
|
||||
if (value instanceof File) {
|
||||
throw new Error(
|
||||
'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export { command } from './command.svelte.js';
|
||||
export { form } from './form.svelte.js';
|
||||
export { prerender } from './prerender.svelte.js';
|
||||
export { query, query_batch } from './query.svelte.js';
|
||||
Generated
Vendored
+184
@@ -0,0 +1,184 @@
|
||||
import { app_dir, base } from '$app/paths/internal/client';
|
||||
import { version } from '__sveltekit/environment';
|
||||
import * as devalue from 'devalue';
|
||||
import { DEV } from 'esm-env';
|
||||
import { app, remote_responses } from '../client.js';
|
||||
import {
|
||||
create_remote_function,
|
||||
get_remote_request_headers,
|
||||
remote_request
|
||||
} from './shared.svelte.js';
|
||||
|
||||
// Initialize Cache API for prerender functions
|
||||
const CACHE_NAME = DEV ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
|
||||
/** @type {Cache | undefined} */
|
||||
let prerender_cache;
|
||||
|
||||
const prerender_cache_ready = (async () => {
|
||||
if (typeof caches !== 'undefined') {
|
||||
try {
|
||||
prerender_cache = await caches.open(CACHE_NAME);
|
||||
|
||||
// Clean up old cache versions
|
||||
const cache_names = await caches.keys();
|
||||
for (const cache_name of cache_names) {
|
||||
if (cache_name.startsWith('sveltekit:') && cache_name !== CACHE_NAME) {
|
||||
await caches.delete(cache_name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize SvelteKit cache:', error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @implements {Partial<Promise<T>>}
|
||||
*/
|
||||
class Prerender {
|
||||
/** @type {Promise<T>} */
|
||||
#promise;
|
||||
|
||||
#loading = $state(true);
|
||||
#ready = $state(false);
|
||||
|
||||
/** @type {T | undefined} */
|
||||
#current = $state.raw();
|
||||
|
||||
#error = $state.raw(undefined);
|
||||
|
||||
/**
|
||||
* @param {() => Promise<T>} fn
|
||||
*/
|
||||
constructor(fn) {
|
||||
this.#promise = fn().then(
|
||||
(value) => {
|
||||
this.#loading = false;
|
||||
this.#ready = true;
|
||||
this.#current = value;
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
this.#loading = false;
|
||||
this.#error = error;
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {((value: any) => any) | null | undefined} onfulfilled
|
||||
* @param {((reason: any) => any) | null | undefined} [onrejected]
|
||||
* @returns
|
||||
*/
|
||||
then(onfulfilled, onrejected) {
|
||||
return this.#promise.then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {((reason: any) => any) | null | undefined} onrejected
|
||||
*/
|
||||
catch(onrejected) {
|
||||
return this.#promise.catch(onrejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(() => any) | null | undefined} onfinally
|
||||
*/
|
||||
finally(onfinally) {
|
||||
return this.#promise.finally(onfinally);
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this.#error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the resource is loading.
|
||||
*/
|
||||
get loading() {
|
||||
return this.#loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true once the resource has been loaded.
|
||||
*/
|
||||
get ready() {
|
||||
return this.#ready;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} encoded
|
||||
*/
|
||||
function put(url, encoded) {
|
||||
return /** @type {Cache} */ (prerender_cache)
|
||||
.put(
|
||||
url,
|
||||
// We need to create a new response because the original response is already consumed
|
||||
new Response(encoded, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch(() => {
|
||||
// Nothing we can do here
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export function prerender(id) {
|
||||
return create_remote_function(id, (cache_key, payload) => {
|
||||
return new Prerender(async () => {
|
||||
await prerender_cache_ready;
|
||||
|
||||
const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
|
||||
|
||||
if (Object.hasOwn(remote_responses, cache_key)) {
|
||||
const data = remote_responses[cache_key];
|
||||
|
||||
if (prerender_cache) {
|
||||
void put(url, devalue.stringify(data, app.encoders));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Do this here, after await Svelte' reactivity context is gone.
|
||||
const headers = get_remote_request_headers();
|
||||
|
||||
// Check the Cache API first
|
||||
if (prerender_cache) {
|
||||
try {
|
||||
const cached_response = await prerender_cache.match(url);
|
||||
|
||||
if (cached_response) {
|
||||
const cached_result = await cached_response.text();
|
||||
return devalue.parse(cached_result, app.decoders);
|
||||
}
|
||||
} catch {
|
||||
// Nothing we can do here
|
||||
}
|
||||
}
|
||||
|
||||
const encoded = await remote_request(url, headers);
|
||||
|
||||
// For successful prerender requests, save to cache
|
||||
if (prerender_cache) {
|
||||
void put(url, encoded);
|
||||
}
|
||||
|
||||
return devalue.parse(encoded, app.decoders);
|
||||
});
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+341
@@ -0,0 +1,341 @@
|
||||
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
||||
/** @import { RemoteFunctionResponse } from 'types' */
|
||||
import { app_dir, base } from '$app/paths/internal/client';
|
||||
import { app, goto, query_map, remote_responses } from '../client.js';
|
||||
import { tick } from 'svelte';
|
||||
import {
|
||||
create_remote_function,
|
||||
get_remote_request_headers,
|
||||
remote_request
|
||||
} from './shared.svelte.js';
|
||||
import * as devalue from 'devalue';
|
||||
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
||||
import { DEV } from 'esm-env';
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {RemoteQueryFunction<any, any>}
|
||||
*/
|
||||
export function query(id) {
|
||||
if (DEV) {
|
||||
// If this reruns as part of HMR, refresh the query
|
||||
for (const [key, entry] of query_map) {
|
||||
if (key === id || key.startsWith(id + '/')) {
|
||||
// use optional chaining in case a prerender function was turned into a query
|
||||
entry.resource.refresh?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return create_remote_function(id, (cache_key, payload) => {
|
||||
return new Query(cache_key, async () => {
|
||||
if (Object.hasOwn(remote_responses, cache_key)) {
|
||||
return remote_responses[cache_key];
|
||||
}
|
||||
|
||||
const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
|
||||
|
||||
const result = await remote_request(url, get_remote_request_headers());
|
||||
return devalue.parse(result, app.decoders);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {(arg: any) => Query<any>}
|
||||
*/
|
||||
export function query_batch(id) {
|
||||
/** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- we don't need reactivity for this
|
||||
let batching = new Map();
|
||||
|
||||
return create_remote_function(id, (cache_key, payload) => {
|
||||
return new Query(cache_key, () => {
|
||||
if (Object.hasOwn(remote_responses, cache_key)) {
|
||||
return remote_responses[cache_key];
|
||||
}
|
||||
|
||||
// Collect all the calls to the same query in the same macrotask,
|
||||
// then execute them as one backend request.
|
||||
return new Promise((resolve, reject) => {
|
||||
// create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
|
||||
// is invoked multiple times with the same payload, so we need to deduplicate here
|
||||
const entry = batching.get(payload) ?? [];
|
||||
entry.push({ resolve, reject });
|
||||
batching.set(payload, entry);
|
||||
|
||||
if (batching.size > 1) return;
|
||||
|
||||
// Do this here, after await Svelte' reactivity context is gone.
|
||||
// TODO is it possible to have batches of the same key
|
||||
// but in different forks/async contexts and in the same macrotask?
|
||||
// If so this would potentially be buggy
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...get_remote_request_headers()
|
||||
};
|
||||
|
||||
// Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
|
||||
// and flushes could reveal more queries that should be batched.
|
||||
setTimeout(async () => {
|
||||
const batched = batching;
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
batching = new Map();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
payloads: Array.from(batched.keys())
|
||||
}),
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to execute batch query');
|
||||
}
|
||||
|
||||
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
||||
if (result.type === 'error') {
|
||||
throw new HttpError(result.status ?? 500, result.error);
|
||||
}
|
||||
|
||||
if (result.type === 'redirect') {
|
||||
await goto(result.location);
|
||||
throw new Redirect(307, result.location);
|
||||
}
|
||||
|
||||
const results = devalue.parse(result.result, app.decoders);
|
||||
|
||||
// Resolve individual queries
|
||||
// Maps guarantee insertion order so we can do it like this
|
||||
let i = 0;
|
||||
|
||||
for (const resolvers of batched.values()) {
|
||||
for (const { resolve, reject } of resolvers) {
|
||||
if (results[i].type === 'error') {
|
||||
reject(new HttpError(results[i].status, results[i].error));
|
||||
} else {
|
||||
resolve(results[i].data);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} catch (error) {
|
||||
// Reject all queries in the batch
|
||||
for (const resolver of batched.values()) {
|
||||
for (const { reject } of resolver) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @implements {Partial<Promise<T>>}
|
||||
*/
|
||||
export class Query {
|
||||
/** @type {string} */
|
||||
_key;
|
||||
|
||||
#init = false;
|
||||
/** @type {() => Promise<T>} */
|
||||
#fn;
|
||||
#loading = $state(true);
|
||||
/** @type {Array<() => void>} */
|
||||
#latest = [];
|
||||
|
||||
/** @type {boolean} */
|
||||
#ready = $state(false);
|
||||
/** @type {T | undefined} */
|
||||
#raw = $state.raw();
|
||||
/** @type {Promise<void>} */
|
||||
#promise;
|
||||
/** @type {Array<(old: T) => T>} */
|
||||
#overrides = $state([]);
|
||||
|
||||
/** @type {T | undefined} */
|
||||
#current = $derived.by(() => {
|
||||
// don't reduce undefined value
|
||||
if (!this.#ready) return undefined;
|
||||
|
||||
return this.#overrides.reduce((v, r) => r(v), /** @type {T} */ (this.#raw));
|
||||
});
|
||||
|
||||
#error = $state.raw(undefined);
|
||||
|
||||
/** @type {Promise<T>['then']} */
|
||||
// @ts-expect-error TS doesn't understand that the promise returns something
|
||||
#then = $derived.by(() => {
|
||||
const p = this.#promise;
|
||||
this.#overrides.length;
|
||||
|
||||
return (resolve, reject) => {
|
||||
const result = (async () => {
|
||||
await p;
|
||||
// svelte-ignore await_reactivity_loss
|
||||
await tick();
|
||||
return /** @type {T} */ (this.#current);
|
||||
})();
|
||||
|
||||
if (resolve || reject) {
|
||||
return result.then(resolve, reject);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {() => Promise<T>} fn
|
||||
*/
|
||||
constructor(key, fn) {
|
||||
this._key = key;
|
||||
this.#fn = fn;
|
||||
this.#promise = $state.raw(this.#run());
|
||||
}
|
||||
|
||||
#run() {
|
||||
// Prevent state_unsafe_mutation error on first run when the resource is created within the template
|
||||
if (this.#init) {
|
||||
this.#loading = true;
|
||||
} else {
|
||||
this.#init = true;
|
||||
}
|
||||
|
||||
// Don't use Promise.withResolvers, it's too new still
|
||||
/** @type {() => void} */
|
||||
let resolve;
|
||||
/** @type {(e?: any) => void} */
|
||||
let reject;
|
||||
/** @type {Promise<void>} */
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
this.#latest.push(
|
||||
// @ts-expect-error it's defined at this point
|
||||
resolve
|
||||
);
|
||||
|
||||
Promise.resolve(this.#fn())
|
||||
.then((value) => {
|
||||
// Skip the response if resource was refreshed with a later promise while we were waiting for this one to resolve
|
||||
const idx = this.#latest.indexOf(resolve);
|
||||
if (idx === -1) return;
|
||||
|
||||
this.#latest.splice(0, idx).forEach((r) => r());
|
||||
this.#ready = true;
|
||||
this.#loading = false;
|
||||
this.#raw = value;
|
||||
this.#error = undefined;
|
||||
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
const idx = this.#latest.indexOf(resolve);
|
||||
if (idx === -1) return;
|
||||
|
||||
this.#latest.splice(0, idx).forEach((r) => r());
|
||||
this.#error = e;
|
||||
this.#loading = false;
|
||||
reject(e);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
get then() {
|
||||
return this.#then;
|
||||
}
|
||||
|
||||
get catch() {
|
||||
this.#then;
|
||||
return (/** @type {any} */ reject) => {
|
||||
return this.#then(undefined, reject);
|
||||
};
|
||||
}
|
||||
|
||||
get finally() {
|
||||
this.#then;
|
||||
return (/** @type {any} */ fn) => {
|
||||
return this.#then(
|
||||
(value) => {
|
||||
fn();
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
fn();
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this.#error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the resource is loading or reloading.
|
||||
*/
|
||||
get loading() {
|
||||
return this.#loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true once the resource has been loaded for the first time.
|
||||
*/
|
||||
get ready() {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
refresh() {
|
||||
delete remote_responses[this._key];
|
||||
return (this.#promise = this.#run());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
*/
|
||||
set(value) {
|
||||
this.#ready = true;
|
||||
this.#loading = false;
|
||||
this.#error = undefined;
|
||||
this.#raw = value;
|
||||
this.#promise = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(old: T) => T} fn
|
||||
*/
|
||||
withOverride(fn) {
|
||||
this.#overrides.push(fn);
|
||||
|
||||
return {
|
||||
_key: this._key,
|
||||
release: () => {
|
||||
const i = this.#overrides.indexOf(fn);
|
||||
|
||||
if (i !== -1) {
|
||||
this.#overrides.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user