Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 11 additions & 6 deletions packages/kit/src/exports/vite/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)};`);
}

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 20 additions & 1 deletion packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
*/
Expand Down
51 changes: 50 additions & 1 deletion packages/kit/src/exports/vite/utils.js
Original file line number Diff line number Diff line change
@@ -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';

Check failure on line 3 in packages/kit/src/exports/vite/utils.js

View workflow job for this annotation

GitHub Actions / lint-all

'read' is defined but never used
import { negotiate } from '../../utils/http.js';
import { filter_private_env, filter_public_env } from '../../utils/env.js';
import { escape_html } from '../../utils/escape.js';
Expand Down Expand Up @@ -156,3 +156,52 @@
}

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<string, any>} */
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;
}
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UniversalNode, 'load' | 'entries'> & Record<string, any>;
/** The `+page/layout.server.js/ts`. */
server?: string;
parent_id?: string;
Expand Down Expand Up @@ -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<SSRNode>;
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/utils/page_nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ 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));
}
}

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));
}
}
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading