From 6a712314eb36ee7085fc9e99d6daf58ecd1e1322 Mon Sep 17 00:00:00 2001 From: Chew Tee Ming Date: Thu, 27 Mar 2025 17:53:20 +0800 Subject: [PATCH 1/2] wip --- packages/kit/package.json | 2 + .../src/exports/vite/build/build_server.js | 1 + packages/kit/src/exports/vite/dev/index.js | 1 + packages/kit/src/exports/vite/utils.js | 50 ++++++++++++++++++- packages/kit/src/runtime/server/respond.js | 2 +- packages/kit/src/types/internal.d.ts | 6 +++ packages/kit/src/utils/page_nodes.js | 2 + pnpm-lock.yaml | 6 +++ 8 files changed, 68 insertions(+), 2 deletions(-) diff --git a/packages/kit/package.json b/packages/kit/package.json index 105470b94170..2f7cb31bfafe 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -18,7 +18,9 @@ "homepage": "https://svelte.dev", "type": "module", "dependencies": { + "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index ca3eedc18cb3..bc37bd01f291 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -101,6 +101,7 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli } if (node.universal) { + // TODO: avoid loading the module if ssr is false imports.push( `import * as universal from '../${ resolve_symlinks(server_manifest, node.universal).chunk.file diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 7049d8910508..0cfbfd2344ba 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -202,6 +202,7 @@ export async function dev(vite, vite_config, svelte_config) { } if (node.universal) { + // TODO: avoid loading the module if ssr is false const { module, module_node } = await resolve(node.universal); module_nodes.push(module_node); result.universal = module; diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index 02916e4d85c5..747f81b9d500 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -1,6 +1,8 @@ import path from 'node:path'; +import { tsPlugin } from '@sveltejs/acorn-typescript'; +import { Parser } from 'acorn'; import { loadEnv } from 'vite'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify, read } from '../../utils/filesystem.js'; import { negotiate } from '../../utils/http.js'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; import { escape_html } from '../../utils/escape.js'; @@ -156,3 +158,49 @@ export function normalize_id(id, lib, cwd) { } export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', ''); + +const parser = Parser.extend(tsPlugin()); + +/** + * @param {string} node_path + */ +export function statically_analyse_exports(node_path) { + const input = read(node_path); + + const node = parser.parse(input, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true + }); + + /** @type {Map} */ + const exports = new Map(); + + node.body.forEach((statement) => { + // TODO: get all exports so we can validate them? + if (statement.type !== 'ExportNamedDeclaration') { + return; + } + + // TODO: handle export specifiers referencing identifiers in the same file? + + if ( + statement.declaration?.type !== 'VariableDeclaration' || + statement.declaration.kind !== 'const' + ) { + return; + } + + for (const declaration of statement.declaration.declarations) { + if (!declaration.init || declaration.init.type !== 'Literal') { + continue; + } + + if (declaration.id.type === 'Identifier') { + exports.set(declaration.id.name, declaration.init.value); + } + } + }); + + return exports; +} diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 81b30e0756a5..ad945974ef58 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -288,7 +288,7 @@ export async function respond(request, options, manifest, state) { let trailing_slash = 'never'; try { - /** @type {PageNodes|undefined} */ + /** @type {PageNodes | undefined} */ const page_nodes = route?.page ? new PageNodes(await load_page_nodes(route.page, manifest)) : undefined; diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 2d54b37ac145..c421da10eff6 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -409,6 +409,12 @@ export interface SSRNode { universal?: UniversalNode; /** +page.server.js, +layout.server.js, or +server.js */ server?: ServerNode; + statically_analysed_option?: { + ssr?: boolean; + csr?: boolean; + prerender?: boolean; + trailingSlash?: TrailingSlash; + }; } export type SSRNodeLoader = () => Promise; diff --git a/packages/kit/src/utils/page_nodes.js b/packages/kit/src/utils/page_nodes.js index f68f1aac0ac9..47193a59a1d4 100644 --- a/packages/kit/src/utils/page_nodes.js +++ b/packages/kit/src/utils/page_nodes.js @@ -27,6 +27,7 @@ export class PageNodes { for (const layout of this.layouts()) { if (layout) { validate_layout_server_exports(layout.server, /** @type {string} */ (layout.server_id)); + // TODO: validate exports without loading the module? validate_layout_exports(layout.universal, /** @type {string} */ (layout.universal_id)); } } @@ -34,6 +35,7 @@ export class PageNodes { const page = this.page(); if (page) { validate_page_server_exports(page.server, /** @type {string} */ (page.server_id)); + // TODO: validate exports without loading the module? validate_page_exports(page.universal, /** @type {string} */ (page.universal_id)); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ce13f0751d3..dec789f2036c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -336,9 +336,15 @@ importers: packages/kit: dependencies: + '@sveltejs/acorn-typescript': + specifier: ^1.0.5 + version: 1.0.5(acorn@8.14.1) '@types/cookie': specifier: ^0.6.0 version: 0.6.0 + acorn: + specifier: ^8.14.1 + version: 8.14.1 cookie: specifier: ^0.6.0 version: 0.6.0 From 2027c7d041813760d84e936bdb1689d68cb65745 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 3 Apr 2025 14:33:37 +0200 Subject: [PATCH 2/2] wip --- .../src/exports/vite/build/build_server.js | 18 ++++--- packages/kit/src/exports/vite/index.js | 21 +++++++- packages/kit/src/exports/vite/utils.js | 53 ++++++++++--------- packages/kit/src/types/internal.d.ts | 2 + 4 files changed, 60 insertions(+), 34 deletions(-) diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index bc37bd01f291..297b86777091 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -101,13 +101,17 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli } if (node.universal) { - // TODO: avoid loading the module if ssr is false - imports.push( - `import * as universal from '../${ - resolve_symlinks(server_manifest, node.universal).chunk.file - }';` - ); - exports.push('export { universal };'); + if (node.universal_static_exports?.ssr === false) { + const universal_exports = Object.entries(node.universal_static_exports).map(([name, value]) => `${name}: ${s(value)}`); + exports.push(`export const universal = { ${universal_exports} };`); + } else { + imports.push( + `import * as universal from '../${ + resolve_symlinks(server_manifest, node.universal).chunk.file + }';` + ); + exports.push('export { universal };'); + } exports.push(`export const universal_id = ${s(node.universal)};`); } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index bdb37b1f9cff..cbeae94b9d7f 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -17,7 +17,13 @@ import { assets_base, find_deps, resolve_symlinks } from './build/utils.js'; import { dev } from './dev/index.js'; import { is_illegal, module_guard } from './graph_analysis/index.js'; import { preview } from './preview/index.js'; -import { get_config_aliases, get_env, normalize_id, strip_virtual_prefix } from './utils.js'; +import { + get_config_aliases, + get_env, + normalize_id, + statically_analyse_exports, + strip_virtual_prefix +} from './utils.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; @@ -731,6 +737,19 @@ Tips: return preview(vite, vite_config, svelte_config); }, + transform(code, id) { + const route_path = id.slice(process.cwd().length + 1); + const node = manifest_data.nodes.find( + (node) => node.universal && node.universal === route_path + ); + if (!node) return; + + const exports = statically_analyse_exports(this.parse(code)); + if (exports) { + node.universal_static_exports = Object.fromEntries(exports); + } + }, + /** * Clears the output directories. */ diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index 747f81b9d500..32acff5b09c9 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -1,6 +1,4 @@ import path from 'node:path'; -import { tsPlugin } from '@sveltejs/acorn-typescript'; -import { Parser } from 'acorn'; import { loadEnv } from 'vite'; import { posixify, read } from '../../utils/filesystem.js'; import { negotiate } from '../../utils/http.js'; @@ -159,48 +157,51 @@ export function normalize_id(id, lib, cwd) { export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', ''); -const parser = Parser.extend(tsPlugin()); - /** - * @param {string} node_path + * Collect all public exports (i.e. not starting with `_`) from a +page.js/+layout.js file. + * Returns `null` if those exports cannot be statically analyzed. + * @param {import('rollup').ProgramNode} ast */ -export function statically_analyse_exports(node_path) { - const input = read(node_path); - - const node = parser.parse(input, { - sourceType: 'module', - ecmaVersion: 'latest', - locations: true - }); - +export function statically_analyse_exports(ast) { /** @type {Map} */ const exports = new Map(); - node.body.forEach((statement) => { - // TODO: get all exports so we can validate them? - if (statement.type !== 'ExportNamedDeclaration') { - return; - } - + for (const statement of ast.body) { + if (statement.type === 'ExportAllDeclaration') return null; + if (statement.type !== 'ExportNamedDeclaration') continue; // TODO: handle export specifiers referencing identifiers in the same file? + if (statement.specifiers.length > 0) return null; + + if (statement.declaration?.type === 'FunctionDeclaration') { + if (statement.declaration.id.name.startsWith('_')) continue; + // We need to load the file during prerendering + if (statement.declaration.id.name === 'entries') return null; + // This includes load functions but also other invalid public function exports which our validator will catch + // TODO should instead just bail on everything invalid, to indirectly trigger validation? or error directly here? + exports.set(statement.declaration.id.name, 'function'); + continue; + } if ( statement.declaration?.type !== 'VariableDeclaration' || statement.declaration.kind !== 'const' ) { - return; + // TODO analyze that variable is not reassigned, i.e. so that `let` is also allowed? + return null; } for (const declaration of statement.declaration.declarations) { + if (declaration.id.type !== 'Identifier') return null; + if (declaration.id.name.startsWith('_') || declaration.id.name === 'load') continue; + if (!declaration.init || declaration.init.type !== 'Literal') { - continue; + // TODO try to statically analyze ObjectExpression in case of `export const config = { ... }` + return null; } - if (declaration.id.type === 'Identifier') { - exports.set(declaration.id.name, declaration.init.value); - } + exports.set(declaration.id.name, declaration.init.value); } - }); + } return exports; } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index c421da10eff6..91b7e30f5026 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -199,6 +199,8 @@ export interface PageNode { component?: string; // TODO supply default component if it's missing (bit of an edge case) /** The `+page/layout.js/.ts`. */ universal?: string; + /** The `+page/layout.js/.ts` exports we could analyze statically, and their values */ + universal_static_exports?: Omit & Record; /** The `+page/layout.server.js/ts`. */ server?: string; parent_id?: string;