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..297b86777091 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -101,12 +101,17 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli } if (node.universal) { - 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/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/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 02916e4d85c5..32acff5b09c9 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -1,6 +1,6 @@ import path from 'node:path'; 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 +156,52 @@ export function normalize_id(id, lib, cwd) { } export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', ''); + +/** + * 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(ast) { + /** @type {Map} */ + const exports = new Map(); + + 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' + ) { + // 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') { + // TODO try to statically analyze ObjectExpression in case of `export const config = { ... }` + return null; + } + + 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..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; @@ -409,6 +411,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