diff --git a/.changeset/heavy-poems-retire.md b/.changeset/heavy-poems-retire.md new file mode 100644 index 000000000000..161890a71325 --- /dev/null +++ b/.changeset/heavy-poems-retire.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: only fetch `__data.json` files for routes with a server `load` function diff --git a/.changeset/young-planes-flash.md b/.changeset/young-planes-flash.md new file mode 100644 index 000000000000..cdc2e8f354e1 --- /dev/null +++ b/.changeset/young-planes-flash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: install polyfills when analysing code diff --git a/package.json b/package.json index 1c28198cce8a..190a29e7080e 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "description": "monorepo for @sveltejs/kit and friends", "private": true, "scripts": { - "test": "turbo run test --filter=./packages/*", - "test:cross-platform": "turbo run test:cross-platform --filter=./packages/*", + "test": "turbo run test --filter=./packages/* --force", + "test:cross-platform": "turbo run test:cross-platform --filter=./packages/* --force", "test:vite-ecosystem-ci": "pnpm test --dir packages/kit", - "check": "turbo run check", - "lint": "turbo run lint", + "check": "turbo run check --force", + "lint": "turbo run lint --force", "format": "turbo run format", "precommit": "turbo run precommit", "release": "changeset publish" diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index c83e72765b3e..22266070fec3 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -17,13 +17,14 @@ const pipe = promisify(pipeline); * @param {{ * config: import('types').ValidatedConfig; * build_data: import('types').BuildData; + * server_metadata: import('types').ServerMetadata; * routes: import('types').RouteData[]; * prerendered: import('types').Prerendered; * log: import('types').Logger; * }} opts * @returns {import('types').Builder} */ -export function create_builder({ config, build_data, routes, prerendered, log }) { +export function create_builder({ config, build_data, server_metadata, routes, prerendered, log }) { return { log, rimraf, @@ -53,18 +54,9 @@ export function create_builder({ config, build_data, routes, prerendered, log }) async createEntries(fn) { /** @type {import('types').RouteDefinition[]} */ const facades = routes.map((route) => { - /** @type {Set} */ - const methods = new Set(); - - if (route.page) { - methods.add('GET'); - } - - if (route.endpoint) { - for (const method of build_data.server.methods[route.endpoint.file]) { - methods.add(method); - } - } + const methods = + /** @type {import('types').HttpMethod[]} */ + (server_metadata.routes.get(route.id)?.methods); return { id: route.id, @@ -74,7 +66,7 @@ export function create_builder({ config, build_data, routes, prerendered, log }) content: segment })), pattern: route.pattern, - methods: Array.from(methods) + methods }; }); diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index af35cc19fd51..2f10ff4b22f1 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -29,6 +29,8 @@ test('copy files', () => { config: /** @type {import('types').ValidatedConfig} */ (mocked), // @ts-expect-error build_data: {}, + // @ts-expect-error + server_metadata: {}, routes: [], // @ts-expect-error prerendered: { diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 26feb567498b..430054830748 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -4,11 +4,12 @@ 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 {{ log: import('types').Logger }} opts + * @param {import('types').Logger} log */ -export async function adapt(config, build_data, prerendered, prerender_map, { log }) { +export async function adapt(config, build_data, server_metadata, prerendered, prerender_map, log) { const { name, adapt } = config.kit.adapter; console.log(colors.bold().cyan(`\n> Using ${name}`)); @@ -16,6 +17,7 @@ export async function adapt(config, build_data, prerendered, prerender_map, { lo const builder = create_builder({ config, build_data, + server_metadata, routes: build_data.manifest_data.routes.filter((route) => { if (!route.page && !route.endpoint) return false; diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index c4ace9567103..80c3ba9ef2d1 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -86,7 +86,7 @@ export function generate_manifest({ build_data, relative_path, routes }) { assets: new Set(${s(assets)}), mimeTypes: ${s(get_mime_lookup(build_data.manifest_data))}, _: { - entry: ${s(build_data.client.entry)}, + entry: ${s(build_data.client_entry)}, nodes: [ ${(node_paths).map(loader).join(',\n\t\t\t\t')} ], @@ -103,7 +103,7 @@ export function generate_manifest({ build_data, relative_path, routes }) { 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.vite_manifest, route.endpoint.file).chunk.file)) : '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\t\t\t\t')} ], diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js new file mode 100644 index 000000000000..4a1cd2600769 --- /dev/null +++ b/packages/kit/src/core/postbuild/analyse.js @@ -0,0 +1,135 @@ +import { join } from 'path'; +import { pathToFileURL } from 'url'; +import { get_option } from '../../runtime/server/utils.js'; +import { + validate_common_exports, + validate_page_server_exports, + validate_server_exports +} from '../../utils/exports.js'; +import { load_config } from '../config/index.js'; +import { forked } from '../../utils/fork.js'; +import { should_polyfill } from '../../utils/platform.js'; +import { installPolyfills } from '../../exports/node/polyfills.js'; + +export default forked(import.meta.url, analyse); + +/** + * @param {{ + * manifest_path: string; + * env: Record + * }} opts + */ +async function analyse({ manifest_path, env }) { + /** @type {import('types').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); + + if (should_polyfill) { + installPolyfills(); + } + + // configure `import { building } from '$app/environment'` — + // essential we do this before analysing the code + internal.set_building(true); + + // set env, in case it's used in initialisation + const entries = Object.entries(env); + const prefix = config.env.publicPrefix; + internal.set_private_env(Object.fromEntries(entries.filter(([k]) => !k.startsWith(prefix)))); + internal.set_public_env(Object.fromEntries(entries.filter(([k]) => k.startsWith(prefix)))); + + /** @type {import('types').ServerMetadata} */ + const metadata = { + nodes: [], + routes: new Map() + }; + + // analyse nodes + for (const loader of manifest._.nodes) { + const node = await loader(); + + metadata.nodes.push({ + has_server_load: node.server?.load !== undefined + }); + } + + // analyse routes + for (const route of manifest._.routes) { + /** @type {Set} */ + const methods = new Set(); + + /** @type {import('types').PrerenderOption | undefined} */ + let prerender = undefined; + + if (route.endpoint) { + const mod = await route.endpoint(); + if (mod.prerender !== undefined) { + 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})` + ); + } + + prerender = mod.prerender; + } + + if (mod.GET) methods.add('GET'); + if (mod.POST) methods.add('POST'); + if (mod.PUT) methods.add('PUT'); + if (mod.PATCH) methods.add('PATCH'); + if (mod.DELETE) methods.add('DELETE'); + } + + if (route.page) { + const nodes = await Promise.all( + [...route.page.layouts, route.page.leaf].map((n) => { + if (n !== undefined) return manifest._.nodes[n](); + }) + ); + + const layouts = nodes.slice(0, -1); + const page = nodes.at(-1); + + for (const layout of layouts) { + if (layout) { + validate_common_exports(layout.server, route.id); + validate_common_exports(layout.universal, route.id); + } + } + + if (page) { + methods.add('GET'); + if (page.server?.actions) methods.add('POST'); + + validate_page_server_exports(page.server, route.id); + validate_common_exports(page.universal, route.id); + } + + const should_prerender = get_option(nodes, 'prerender'); + prerender = + should_prerender === true || + // Try prerendering if ssr is false and no server needed. Set it to 'auto' so that + // the route is not removed from the manifest, there could be a server load function. + // People can opt out of this behavior by explicitly setting prerender to false + (should_prerender !== false && get_option(nodes, 'ssr') === false && !page?.server?.actions + ? 'auto' + : should_prerender ?? false); + } + + metadata.routes.set(route.id, { + prerender, + methods: Array.from(methods) + }); + } + + return metadata; +} diff --git a/packages/kit/src/core/postbuild/index.js b/packages/kit/src/core/postbuild/index.js deleted file mode 100644 index 05424a242f31..000000000000 --- a/packages/kit/src/core/postbuild/index.js +++ /dev/null @@ -1,113 +0,0 @@ -import { writeFileSync } from 'fs'; -import { join } from 'path'; -import { pathToFileURL } from 'url'; -import { get_option } from '../../runtime/server/utils.js'; -import { - validate_common_exports, - validate_page_server_exports, - validate_server_exports -} from '../../utils/exports.js'; -import { load_config } from '../config/index.js'; -import { prerender } from './prerender.js'; - -const [, , client_out_dir, manifest_path, results_path, verbose, env_json] = process.argv; -const env = JSON.parse(env_json); - -/** @type {import('types').SSRManifest} */ -const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; - -/** @type {import('types').PrerenderMap} */ -const prerender_map = new Map(); - -/** @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); - -/** @type {import('types').ServerModule} */ -const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href); - -// configure `import { building } from '$app/environment'` — -// essential we do this before analysing the code -internal.set_building(true); - -// set env, in case it's used in initialisation -const entries = Object.entries(env); -const prefix = config.env.publicPrefix; -internal.set_private_env(Object.fromEntries(entries.filter(([k]) => !k.startsWith(prefix)))); -internal.set_public_env(Object.fromEntries(entries.filter(([k]) => k.startsWith(prefix)))); - -// analyse routes -for (const route of manifest._.routes) { - if (route.endpoint) { - const mod = await route.endpoint(); - if (mod.prerender !== undefined) { - 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})` - ); - } - - prerender_map.set(route.id, mod.prerender); - } - } - - if (route.page) { - const nodes = await Promise.all( - [...route.page.layouts, route.page.leaf].map((n) => { - if (n !== undefined) return manifest._.nodes[n](); - }) - ); - - const layouts = nodes.slice(0, -1); - const page = nodes.at(-1); - - for (const layout of layouts) { - if (layout) { - validate_common_exports(layout.server, route.id); - validate_common_exports(layout.universal, route.id); - } - } - - if (page) { - validate_page_server_exports(page.server, route.id); - validate_common_exports(page.universal, route.id); - } - - const should_prerender = get_option(nodes, 'prerender'); - const prerender = - should_prerender === true || - // Try prerendering if ssr is false and no server needed. Set it to 'auto' so that - // the route is not removed from the manifest, there could be a server load function. - // People can opt out of this behavior by explicitly setting prerender to false - (should_prerender !== false && get_option(nodes, 'ssr') === false && !page?.server?.actions - ? 'auto' - : should_prerender ?? false); - - prerender_map.set(route.id, prerender); - } -} - -const { prerendered } = await prerender({ - Server, - internal, - manifest, - prerender_map, - client_out_dir, - verbose, - env -}); - -writeFileSync( - results_path, - JSON.stringify({ prerendered, prerender_map }, (_key, value) => - value instanceof Map ? Array.from(value.entries()) : value - ) -); - -process.exit(0); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 4af035800b48..61faeba7a7b7 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -1,68 +1,72 @@ -import { readFileSync, writeFileSync } from 'fs'; -import { dirname, join } from 'path'; -import { URL } from 'url'; +import { readFileSync, 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 { should_polyfill } from '../../utils/platform.js'; import { is_root_relative, resolve } from '../../utils/url.js'; -import { queue } from './queue.js'; -import { crawl } from './crawl.js'; import { escape_html_attr } 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'; -/** - * @template {{message: string}} T - * @template {Omit} 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; +export default forked(import.meta.url, prerender); /** - * * @param {{ - * Server: typeof import('types').InternalServer; - * internal: import('types').ServerInternalModule; - * manifest: import('types').SSRManifest; - * prerender_map: import('types').PrerenderMap; - * client_out_dir: string; - * verbose: string; - * env: Record; + * out: string; + * manifest_path: string; + * metadata: import('types').ServerMetadata; + * verbose: boolean; + * env: Record * }} opts - * @returns */ -export async function prerender({ - Server, - internal, - manifest, - prerender_map, - client_out_dir, - verbose, - env -}) { +async function prerender({ out, manifest_path, metadata, verbose, env }) { + /** @type {import('types').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(true); + + /** + * @template {{message: string}} T + * @template {Omit} 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(), @@ -71,6 +75,15 @@ export async function prerender({ 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} */ const prerendered_routes = new Set(); @@ -78,9 +91,7 @@ export async function prerender({ const config = (await load_config()).kit; /** @type {import('types').Logger} */ - const log = logger({ - verbose: verbose === 'true' - }); + const log = logger({ verbose }); if (should_polyfill) { installPolyfills(); @@ -134,7 +145,7 @@ export async function prerender({ return file; } - const files = new Set(walk(client_out_dir).map(posixify)); + const files = new Set(walk(`${out}/client`).map(posixify)); const seen = new Set(); const written = new Set(); diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index d99c3315adc3..52edaa8164a8 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -1,7 +1,6 @@ import path from 'path'; import create_manifest_data from './create_manifest_data/index.js'; import { write_client_manifest } from './write_client_manifest.js'; -import { write_matchers } from './write_matchers.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'; @@ -27,10 +26,9 @@ export async function create(config) { const output = path.join(config.kit.outDir, 'generated'); - write_client_manifest(config, manifest_data, output); + write_client_manifest(config.kit, manifest_data, `${output}/client`); write_server(config, output); write_root(manifest_data, output); - write_matchers(manifest_data, output); await write_all_types(config, manifest_data); return { manifest_data }; @@ -72,7 +70,7 @@ export async function all_types(config, mode) { } /** - * Regenerate server-internal.js in response to src/{app.html,error.html,service-worker.js} changing + * 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) { diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 4675bb4b3243..06c32d8a48d8 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -5,11 +5,12 @@ import { trim, write_if_changed } from './utils.js'; /** * 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').ValidatedConfig} config + * @param {import('types').ValidatedKitConfig} kit * @param {import('types').ManifestData} manifest_data * @param {string} output + * @param {Array<{ has_server_load: boolean }>} [metadata] */ -export function write_client_manifest(config, manifest_data, output) { +export function write_client_manifest(kit, manifest_data, output, metadata) { /** * Creates a module that exports a `CSRPageNode` * @param {import('types').PageNode} node @@ -32,15 +33,16 @@ export function write_client_manifest(config, manifest_data, output) { ); } - if (node.server) { - declarations.push(`export const has_server_load = true;`); - } - return declarations.join('\n'); } + /** @type {Map} */ + 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}')`; }) @@ -58,14 +60,35 @@ export function write_client_manifest(config, manifest_data, output) { 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 = [`${route.leaf?.server ? '~' : ''}${route.page.leaf}`]; + 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 safe space by doing it this way. + // are reused across pages, so we save space by doing it this way. route.page.layouts.forEach((layout) => { - if (layout != undefined && manifest_data.nodes[layout].server) { + 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); } }); @@ -81,15 +104,15 @@ export function write_client_manifest(config, manifest_data, output) { .join(',\n\t\t')} }`.replace(/^\t/gm, ''); - const hooks_file = resolve_entry(config.kit.files.hooks.client); + const hooks_file = resolve_entry(kit.files.hooks.client); - // String representation of __GENERATED__/client-manifest.js + // String representation of __CLIENT__/manifest.js write_if_changed( - `${output}/client-manifest.js`, + `${output}/manifest.js`, trim(` ${hooks_file ? `import * as client_hooks from '${relative_path(output, hooks_file)}';` : ''} - export { matchers } from './client-matchers.js'; + export { matchers } from './matchers.js'; export const nodes = [${nodes}]; @@ -104,4 +127,22 @@ export function write_client_manifest(config, manifest_data, output) { }; `) ); + + // 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); } diff --git a/packages/kit/src/core/sync/write_matchers.js b/packages/kit/src/core/sync/write_matchers.js deleted file mode 100644 index a11a92c78755..000000000000 --- a/packages/kit/src/core/sync/write_matchers.js +++ /dev/null @@ -1,25 +0,0 @@ -import { s } from '../../utils/misc.js'; -import { relative_path } from '../../utils/filesystem.js'; -import { write_if_changed } from './utils.js'; - -/** - * @param {import('types').ManifestData} manifest_data - * @param {string} output - */ -export function write_matchers(manifest_data, output) { - 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}/client-matchers.js`, module); -} diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 1b80f7a5a049..395a80f080f5 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -4,6 +4,7 @@ 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 { write_if_changed } from './utils.js'; /** * @param {{ @@ -23,7 +24,7 @@ const server_template = ({ template, error_page }) => ` -import root from './root.svelte'; +import root from '../root.svelte'; import { set_building, set_paths, set_private_env, set_public_env, set_version } from '${runtime_directory}/shared.js'; set_paths(${s(config.kit.paths)}); @@ -75,11 +76,11 @@ export function write_server(config, output) { /** @param {string} file */ function relative(file) { - return posixify(path.relative(output, file)); + return posixify(path.relative(`${output}/server`, file)); } - fs.writeFileSync( - `${output}/server-internal.js`, + write_if_changed( + `${output}/server/internal.js`, server_template({ config, hooks: fs.existsSync(hooks_file) ? relative(hooks_file) : null, diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 940f7719f155..5d3b03bad95e 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -1,61 +1,33 @@ import fs from 'node:fs'; -import path from 'node:path'; -import * as vite from 'vite'; -import { mkdirp, posixify } from '../../../utils/filesystem.js'; -import { find_deps, is_http_method, resolve_symlinks } from './utils.js'; +import { mkdirp } from '../../../utils/filesystem.js'; +import { find_deps, resolve_symlinks } from './utils.js'; import { s } from '../../../utils/misc.js'; /** - * @param {{ - * config: import('types').ValidatedConfig; - * vite_config: import('vite').ResolvedConfig; - * vite_config_env: import('vite').ConfigEnv; - * manifest_data: import('types').ManifestData; - * output_dir: string; - * }} options - * @param {{ vite_manifest: import('vite').Manifest, assets: import('rollup').OutputAsset[] }} client + * @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 {import('rollup').OutputAsset[] | null} css */ -export async function build_server(options, client) { - const { config, vite_config, vite_config_env, manifest_data, output_dir } = options; - - const { output } = /** @type {import('rollup').RollupOutput} */ ( - await vite.build({ - // CLI args - configFile: vite_config.configFile, - mode: vite_config_env.mode, - logLevel: config.logLevel, - clearScreen: config.clearScreen, - build: { - ssr: true - } - }) - ); - - const chunks = /** @type {import('rollup').OutputChunk[]} */ ( - output.filter((chunk) => chunk.type === 'chunk') - ); - - /** @type {import('vite').Manifest} */ - const vite_manifest = JSON.parse( - fs.readFileSync(`${output_dir}/server/${vite_config.build.manifest}`, 'utf-8') - ); - - mkdirp(`${output_dir}/server/nodes`); - mkdirp(`${output_dir}/server/stylesheets`); +export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css) { + mkdirp(`${out}/server/nodes`); + mkdirp(`${out}/server/stylesheets`); const stylesheet_lookup = new Map(); - client.assets.forEach((asset) => { - if (asset.fileName.endsWith('.css')) { - if (asset.source.length < config.kit.inlineStyleThreshold) { + if (css) { + css.forEach((asset) => { + if (asset.source.length < kit.inlineStyleThreshold) { const index = stylesheet_lookup.size; - const file = `${output_dir}/server/stylesheets/${index}.js`; + const file = `${out}/server/stylesheets/${index}.js`; fs.writeFileSync(file, `// ${asset.fileName}\nexport default ${s(asset.source)};`); stylesheet_lookup.set(asset.fileName, index); } - } - }); + }); + } manifest_data.nodes.forEach((node, i) => { /** @type {string[]} */ @@ -75,8 +47,8 @@ export async function build_server(options, client) { /** @type {string[]} */ const fonts = []; - if (node.component) { - const entry = find_deps(client.vite_manifest, node.component, true); + if (node.component && client_manifest) { + const entry = find_deps(client_manifest, node.component, true); imported.push(...entry.imports); stylesheets.push(...entry.stylesheets); @@ -84,25 +56,27 @@ export async function build_server(options, client) { exports.push( `export const component = async () => (await import('../${ - resolve_symlinks(vite_manifest, node.component).chunk.file + resolve_symlinks(server_manifest, node.component).chunk.file }')).default;`, `export const file = '${entry.file}';` // TODO what is this? ); } if (node.universal) { - const entry = find_deps(client.vite_manifest, node.universal, true); + if (client_manifest) { + const entry = find_deps(client_manifest, node.universal, true); - imported.push(...entry.imports); - stylesheets.push(...entry.stylesheets); - fonts.push(...entry.fonts); + imported.push(...entry.imports); + stylesheets.push(...entry.stylesheets); + fonts.push(...entry.fonts); + } - imports.push(`import * as universal from '../${vite_manifest[node.universal].file}';`); + imports.push(`import * as universal from '../${server_manifest[node.universal].file}';`); exports.push(`export { universal };`); } if (node.server) { - imports.push(`import * as server from '../${vite_manifest[node.server].file}';`); + imports.push(`import * as server from '../${server_manifest[node.server].file}';`); exports.push(`export { server };`); } @@ -128,45 +102,9 @@ export async function build_server(options, client) { exports.push(`export const inline_styles = () => ({\n${styles.join(',\n')}\n});`); } - const out = `${output_dir}/server/nodes/${i}.js`; - fs.writeFileSync(out, `${imports.join('\n')}\n\n${exports.join('\n')}\n`); - }); - - return { - chunks, - vite_manifest, - methods: get_methods(chunks, manifest_data) - }; -} - -/** - * @param {import('rollup').OutputChunk[]} output - * @param {import('types').ManifestData} manifest_data - */ -function get_methods(output, manifest_data) { - /** @type {Record} */ - const lookup = {}; - output.forEach((chunk) => { - if (!chunk.facadeModuleId) return; - const id = posixify(path.relative('.', chunk.facadeModuleId)); - lookup[id] = chunk.exports; - }); - - /** @type {Record} */ - const methods = {}; - manifest_data.routes.forEach((route) => { - if (route.endpoint) { - if (lookup[route.endpoint.file]) { - methods[route.endpoint.file] = lookup[route.endpoint.file].filter(is_http_method); - } - } - - if (route.leaf?.server) { - if (lookup[route.leaf.server]) { - methods[route.leaf.server] = lookup[route.leaf.server].filter(is_http_method); - } - } + fs.writeFileSync( + `${out}/server/nodes/${i}.js`, + `${imports.join('\n')}\n\n${exports.join('\n')}\n` + ); }); - - return methods; } diff --git a/packages/kit/src/exports/vite/build/build_service_worker.js b/packages/kit/src/exports/vite/build/build_service_worker.js index 10c6e892272e..97fcf4726d8b 100644 --- a/packages/kit/src/exports/vite/build/build_service_worker.js +++ b/packages/kit/src/exports/vite/build/build_service_worker.js @@ -5,19 +5,19 @@ import { get_config_aliases } from '../utils.js'; import { assets_base } from './utils.js'; /** - * @param {{ - * config: import('types').ValidatedConfig; - * vite_config: import('vite').ResolvedConfig; - * vite_config_env: import('vite').ConfigEnv; - * manifest_data: import('types').ManifestData; - * output_dir: string; - * }} options + * @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( - { config, vite_config, manifest_data, output_dir }, + out, + kit, + vite_config, + manifest_data, service_worker_entry_file, prerendered, client_manifest @@ -30,21 +30,21 @@ export async function build_service_worker( assets.forEach((file) => build.add(file)); } - const service_worker = `${config.kit.outDir}/generated/service-worker.js`; + const service_worker = `${kit.outDir}/generated/service-worker.js`; fs.writeFileSync( service_worker, ` export const build = [ ${Array.from(build) - .map((file) => `${s(`${config.kit.paths.base}/${file}`)}`) + .map((file) => `${s(`${kit.paths.base}/${file}`)}`) .join(',\n\t\t\t\t')} ]; export const files = [ ${manifest_data.assets - .filter((asset) => config.kit.serviceWorker.files(asset.file)) - .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`) + .filter((asset) => kit.serviceWorker.files(asset.file)) + .map((asset) => `${s(`${kit.paths.base}/${asset.file}`)}`) .join(',\n\t\t\t\t')} ]; @@ -52,14 +52,14 @@ export async function build_service_worker( ${prerendered.paths.map((path) => s(path)).join(',\n\t\t\t\t')} ]; - export const version = ${s(config.kit.version.name)}; + export const version = ${s(kit.version.name)}; ` .replace(/^\t{3}/gm, '') .trim() ); await vite.build({ - base: assets_base(config.kit), + base: assets_base(kit), build: { lib: { entry: /** @type {string} */ (service_worker_entry_file), @@ -71,16 +71,13 @@ export async function build_service_worker( entryFileNames: 'service-worker.js' } }, - outDir: `${output_dir}/client`, + outDir: `${out}/client`, emptyOutDir: false }, define: vite_config.define, configFile: false, resolve: { - alias: [ - ...get_config_aliases(config.kit), - { find: '$service-worker', replacement: service_worker } - ] + alias: [...get_config_aliases(kit), { find: '$service-worker', replacement: service_worker }] } }); } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 409f2399b6a8..e2537f8ff333 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1,26 +1,27 @@ -import { fork } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import colors from 'kleur'; import * as vite from 'vite'; -import { mkdirp, posixify, resolve_entry, rimraf } from '../../utils/filesystem.js'; +import { mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js'; import { create_static_module, create_dynamic_module } from '../../core/env.js'; import * as sync from '../../core/sync/sync.js'; import { create_assets } from '../../core/sync/create_manifest_data/index.js'; import { runtime_directory, logger } from '../../core/utils.js'; import { load_config } from '../../core/config/index.js'; import { generate_manifest } from '../../core/generate_manifest/index.js'; -import { build_server } from './build/build_server.js'; +import { build_server_nodes } from './build/build_server.js'; import { build_service_worker } from './build/build_service_worker.js'; import { assets_base, find_deps } from './build/utils.js'; import { dev } from './dev/index.js'; import { is_illegal, module_guard, normalize_id } from './graph_analysis/index.js'; import { preview } from './preview/index.js'; import { get_config_aliases, get_env } 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'; export { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; @@ -138,6 +139,11 @@ export async function sveltekit() { return [...svelte(vite_plugin_svelte_options), ...kit({ svelte_config })]; } +/** + * If `true`, the server build has been completed and we're creating the client build + */ +let secondary_build = false; + /** * Returns the SvelteKit Vite plugin. Vite executes Rollup hooks as well as some of its own. * Background reading is available at: @@ -167,22 +173,13 @@ function kit({ svelte_config }) { /** @type {boolean} */ let is_build; - /** @type {import('types').Logger} */ - let log; - - /** @type {import('types').Prerendered} */ - let prerendered; - - /** @type {import('types').PrerenderMap} */ - let prerender_map; - - /** @type {import('types').BuildData} */ - let build_data; - /** @type {{ public: Record; private: Record }} */ let env; - let completed_build = false; + /** @type {(() => Promise) | null} */ + let finalise = null; + + const service_worker_entry_file = resolve_entry(kit.files.serviceWorker); /** @type {import('vite').Plugin} */ const plugin_setup = { @@ -212,12 +209,19 @@ function kit({ svelte_config }) { const client_hooks = resolve_entry(kit.files.hooks.client); if (client_hooks) allow.add(path.dirname(client_hooks)); + const generated = path.posix.join(kit.outDir, 'generated'); + // dev and preview config can be shared /** @type {import('vite').UserConfig} */ const new_config = { resolve: { alias: [ - { find: '__GENERATED__', replacement: path.posix.join(kit.outDir, 'generated') }, + { + find: '__CLIENT__', + replacement: `${generated}/${is_build ? 'client-optimized' : 'client'}` + }, + { find: '__SERVER__', replacement: `${generated}/server` }, + { find: '__GENERATED__', replacement: generated }, { find: '$app', replacement: `${runtime_directory}/app` }, ...get_config_aliases(kit) ] @@ -246,6 +250,9 @@ function kit({ svelte_config }) { }; if (is_build) { + if (!new_config.build) new_config.build = {}; + new_config.build.ssr = !secondary_build; + new_config.define = { __SVELTEKIT_ADAPTER_NAME__: JSON.stringify(kit.adapter?.name), __SVELTEKIT_APP_VERSION_FILE__: JSON.stringify(`${kit.appDir}/version.json`), @@ -345,6 +352,35 @@ function kit({ svelte_config }) { } }; + /** + * Ensures that client-side code can't accidentally import server-side code, + * whether in `*.server.js` files, `$lib/server`, or `$env/[static|dynamic]/private` + * @type {import('vite').Plugin} + */ + const plugin_guard = { + name: 'vite-plugin-sveltekit-guard', + + writeBundle: { + sequential: true, + async handler(_options) { + if (!secondary_build) return; + + const guard = module_guard(this, { + cwd: vite.normalizePath(process.cwd()), + lib: vite.normalizePath(kit.files.lib) + }); + + manifest_data.nodes.forEach((_node, i) => { + const id = vite.normalizePath( + path.resolve(kit.outDir, `generated/client-optimized/nodes/${i}.js`) + ); + + guard.check(id); + }); + } + } + }; + /** @type {import('vite').Plugin} */ const plugin_compile = { name: 'vite-plugin-sveltekit-compile', @@ -360,7 +396,7 @@ function kit({ svelte_config }) { if (is_build) { manifest_data = (await sync.all(svelte_config, config_env.mode)).manifest_data; - const ssr = config.build?.ssr ?? false; + const ssr = /** @type {boolean} */ (config.build?.ssr); const prefix = `${kit.appDir}/immutable`; /** @type {Record} */ @@ -368,7 +404,7 @@ function kit({ svelte_config }) { if (ssr) { input.index = `${runtime_directory}/server/index.js`; - input.internal = `${kit.outDir}/generated/server-internal.js`; + input.internal = `${kit.outDir}/generated/server/internal.js`; // add entry points for every endpoint... manifest_data.routes.forEach((route) => { @@ -499,10 +535,10 @@ function kit({ svelte_config }) { * Clears the output directories. */ buildStart() { - if (vite_config.build.ssr) return; + if (secondary_build) return; - // Reset for new build. Goes here because `build --watch` calls buildStart but not config - completed_build = false; + // reset (here, not in `config`, because `build --watch` skips `config`) + finalise = null; if (is_build) { if (!vite_config.build.watch) { @@ -513,7 +549,7 @@ function kit({ svelte_config }) { }, generateBundle() { - if (vite_config.build.ssr) return; + if (!secondary_build) return; this.emitFile({ type: 'asset', @@ -529,71 +565,77 @@ function kit({ svelte_config }) { */ writeBundle: { sequential: true, - async handler(_options, bundle) { - if (vite_config.build.ssr) return; + async handler(_options) { + if (secondary_build) return; // only run this once + secondary_build = true; - const guard = module_guard(this, { - cwd: vite.normalizePath(process.cwd()), - lib: vite.normalizePath(kit.files.lib) - }); - - manifest_data.nodes.forEach((_node, i) => { - const id = vite.normalizePath(path.resolve(kit.outDir, `generated/nodes/${i}.js`)); + const verbose = vite_config.logLevel === 'info'; + const log = logger({ verbose }); - guard.check(id); - }); + /** @type {import('vite').Manifest} */ + const server_manifest = JSON.parse(read(`${out}/server/${vite_config.build.manifest}`)); - const verbose = vite_config.logLevel === 'info'; - log = logger({ - verbose - }); + /** @type {import('types').BuildData} */ + const build_data = { + app_dir: kit.appDir, + app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, + manifest_data, + service_worker: !!service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? + client_entry: null, + server_manifest + }; - const { assets, chunks } = collect_output(bundle); - log.info( - `Client build completed. Wrote ${chunks.length} chunks and ${assets.length} assets` + const manifest_path = `${out}/server/manifest-full.js`; + fs.writeFileSync( + manifest_path, + `export const manifest = ${generate_manifest({ + build_data, + relative_path: '.', + routes: manifest_data.routes + })};\n` ); - log.info('Building server'); + // first, build server nodes without the client manifest so we can analyse it + build_server_nodes(out, kit, manifest_data, server_manifest, null, null); - const options = { - config: svelte_config, - vite_config, - vite_config_env, - manifest_data, - output_dir: out - }; + const metadata = await analyse({ + manifest_path, + env: { ...env.private, ...env.public } + }); - /** @type {import('vite').Manifest} */ - const vite_manifest = JSON.parse( - fs.readFileSync(`${out}/client/${vite_config.build.manifest}`, 'utf-8') + // create client build + write_client_manifest( + kit, + manifest_data, + `${kit.outDir}/generated/client-optimized`, + metadata.nodes ); - const client = { - assets, - chunks, - entry: find_deps( - vite_manifest, - posixify(path.relative('.', `${runtime_directory}/client/start.js`)), - false - ), - vite_manifest - }; + const { output } = /** @type {import('rollup').RollupOutput} */ ( + await vite.build({ + configFile: vite_config.configFile, + // CLI args + mode: vite_config_env.mode, + logLevel: vite_config.logLevel, + clearScreen: vite_config.clearScreen + }) + ); - const server = await build_server(options, client); + /** @type {import('vite').Manifest} */ + const client_manifest = JSON.parse(read(`${out}/client/${vite_config.build.manifest}`)); - const service_worker_entry_file = resolve_entry(kit.files.serviceWorker); + build_data.client_entry = find_deps( + client_manifest, + posixify(path.relative('.', `${runtime_directory}/client/start.js`)), + false + ); - /** @type {import('types').BuildData} */ - build_data = { - app_dir: kit.appDir, - app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, - manifest_data, - service_worker: !!service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? - client, - server - }; + const css = output.filter( + /** @type {(value: any) => value is import('rollup').OutputAsset} */ + (value) => value.type === 'asset' && value.fileName.endsWith('.css') + ); - const manifest_path = `${out}/server/manifest-full.js`; + // regenerate manifest now that we have client entry... fs.writeFileSync( manifest_path, `export const manifest = ${generate_manifest({ @@ -603,44 +645,18 @@ function kit({ svelte_config }) { })};\n` ); + // regenerate nodes with the client manifest... + build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css); + + // ...and prerender log.info('Prerendering'); - await new Promise((fulfil, reject) => { - const results_path = `${kit.outDir}/generated/prerendered.json`; - - // do prerendering in a subprocess so any dangling stuff gets killed upon completion - const script = fileURLToPath(new URL('../../core/postbuild/index.js', import.meta.url)); - - const child = fork( - script, - [ - vite_config.build.outDir, - manifest_path, - results_path, - '' + verbose, - JSON.stringify({ ...env.private, ...env.public }) - ], - { - stdio: 'inherit' - } - ); - child.on('exit', (code) => { - if (code) { - reject(new Error(`Prerendering failed with code ${code}`)); - } else { - const results = JSON.parse(fs.readFileSync(results_path, 'utf8'), (key, value) => { - if (key === 'pages' || key === 'assets' || key === 'redirects') { - return new Map(value); - } - return value; - }); - - prerendered = results.prerendered; - prerender_map = new Map(results.prerender_map); - - fulfil(undefined); - } - }); + const { prerendered, prerender_map } = await prerender({ + out, + manifest_path, + metadata, + verbose, + env: { ...env.private, ...env.public } }); // generate a new manifest that doesn't include prerendered pages @@ -661,18 +677,41 @@ function kit({ svelte_config }) { log.info('Building service worker'); await build_service_worker( - options, + out, + kit, + vite_config, + manifest_data, service_worker_entry_file, prerendered, - client.vite_manifest + client_manifest ); } - console.log( - `\nRun ${colors.bold().cyan('npm run preview')} to preview your production build locally.` - ); + // we need to defer this to closeBundle, so that adapters copy files + // created by other Vite plugins + finalise = async () => { + console.log( + `\nRun ${colors + .bold() + .cyan('npm run preview')} to preview your production build locally.` + ); + + if (kit.adapter) { + const { adapt } = await import('../../core/adapt/index.js'); + await adapt(svelte_config, build_data, metadata, prerendered, prerender_map, log); + } else { + console.log(colors.bold().yellow('\nNo adapter specified')); - completed_build = true; + const link = colors.bold().cyan('https://kit.svelte.dev/docs/adapters'); + console.log( + `See ${link} to learn how to configure your app to run on the platform of your choosing` + ); + } + + // avoid making the manifest available to users + fs.unlinkSync(`${out}/client/${vite_config.build.manifest}`); + fs.unlinkSync(`${out}/server/${vite_config.build.manifest}`); + }; } }, @@ -682,50 +721,12 @@ function kit({ svelte_config }) { closeBundle: { sequential: true, async handler() { - // vite calls closeBundle when dev-server restarts, ignore that, - // and only adapt when build successfully completes. - const is_restart = !completed_build; - if (vite_config.build.ssr || is_restart) { - return; - } - - if (kit.adapter) { - const { adapt } = await import('../../core/adapt/index.js'); - await adapt(svelte_config, build_data, prerendered, prerender_map, { log }); - } else { - console.log(colors.bold().yellow('\nNo adapter specified')); - - const link = colors.bold().cyan('https://kit.svelte.dev/docs/adapters'); - console.log( - `See ${link} to learn how to configure your app to run on the platform of your choosing` - ); - } - - // avoid making the manifest available to users - fs.unlinkSync(`${out}/client/${vite_config.build.manifest}`); - fs.unlinkSync(`${out}/server/${vite_config.build.manifest}`); + finalise?.(); } } }; - return [plugin_setup, plugin_virtual_modules, plugin_compile]; -} - -/** @param {import('rollup').OutputBundle} bundle */ -function collect_output(bundle) { - /** @type {import('rollup').OutputChunk[]} */ - const chunks = []; - /** @type {import('rollup').OutputAsset[]} */ - const assets = []; - for (const value of Object.values(bundle)) { - // collect asset and output chunks - if (value.type === 'asset') { - assets.push(value); - } else { - chunks.push(value); - } - } - return { assets, chunks }; + return [plugin_setup, plugin_virtual_modules, plugin_guard, plugin_compile]; } /** diff --git a/packages/kit/src/runtime/client/ambient.d.ts b/packages/kit/src/runtime/client/ambient.d.ts index f4698f2d8424..5f98d9891506 100644 --- a/packages/kit/src/runtime/client/ambient.d.ts +++ b/packages/kit/src/runtime/client/ambient.d.ts @@ -1,4 +1,4 @@ -declare module '__GENERATED__/client-manifest.js' { +declare module '__CLIENT__/manifest.js' { import { CSRPageNodeLoader, ClientHooks, ParamMatcher } from 'types'; /** diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c3fb0bbc22b2..4154615b71a8 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -25,7 +25,7 @@ import { import { parse } from './parse.js'; import Root from '__GENERATED__/root.svelte'; -import { nodes, server_loads, dictionary, matchers, hooks } from '__GENERATED__/client-manifest.js'; +import { nodes, server_loads, dictionary, matchers, hooks } from '__CLIENT__/manifest.js'; import { HttpError, Redirect } from '../control.js'; import { stores } from './singletons.js'; import { unwrap_promises } from '../../utils/promises.js'; @@ -921,12 +921,12 @@ export function create_client({ target, base }) { /** @type {Record} */ const params = {}; // error page does not have params - const node = await default_layout_loader(); - /** @type {import('types').ServerDataNode | null} */ let server_data_node = null; - if (node.has_server_load) { + const default_layout_has_server_load = server_loads[0] === 0; + + if (default_layout_has_server_load) { // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use // existing root layout data try { diff --git a/packages/kit/src/runtime/client/parse.js b/packages/kit/src/runtime/client/parse.js index b452e3b87604..5a776d306246 100644 --- a/packages/kit/src/runtime/client/parse.js +++ b/packages/kit/src/runtime/client/parse.js @@ -3,7 +3,7 @@ import { exec, parse_route_id } from '../../utils/routing.js'; /** * @param {import('types').CSRPageNodeLoader[]} nodes * @param {number[]} server_loads - * @param {typeof import('__GENERATED__/client-manifest.js').dictionary} dictionary + * @param {typeof import('__CLIENT__/manifest.js').dictionary} dictionary * @param {Record boolean>} matchers * @returns {import('types').CSRRoute[]} */ diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index a51063b2d72f..cc7b0f428d47 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -1,4 +1,4 @@ -declare module '__GENERATED__/server-internal.js' { +declare module '__SERVER__/internal.js' { export const options: import('types').SSROptions; export const get_hooks: () => Promise<{ handle?: import('types').Handle; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index cd2db2db8f9e..24d16cd3c077 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,6 +1,6 @@ import { respond } from './respond.js'; import { set_private_env, set_public_env } from '../shared.js'; -import { options, get_hooks } from '__GENERATED__/server-internal.js'; +import { options, get_hooks } from '__SERVER__/internal.js'; export class Server { /** @type {import('types').SSROptions} */ diff --git a/packages/kit/src/utils/filesystem.js b/packages/kit/src/utils/filesystem.js index f53b10452a36..f7d6efe17b3d 100644 --- a/packages/kit/src/utils/filesystem.js +++ b/packages/kit/src/utils/filesystem.js @@ -176,3 +176,8 @@ export function resolve_entry(entry) { return null; } + +/** @param {string} file */ +export function read(file) { + return fs.readFileSync(file, 'utf-8'); +} diff --git a/packages/kit/src/utils/fork.js b/packages/kit/src/utils/fork.js new file mode 100644 index 000000000000..2ab346dc7b97 --- /dev/null +++ b/packages/kit/src/utils/fork.js @@ -0,0 +1,76 @@ +import { fileURLToPath } from 'node:url'; +import child_process from 'node:child_process'; + +/** + * Runs a task in a subprocess so any dangling stuff gets killed upon completion. + * The subprocess needs to be the file `forked` is called in, and `forked` needs to be called eagerly at the top level. + * @template T + * @template U + * @param {string} module `import.meta.url` of the file + * @param {(opts: T) => U} callback The function that is invoked in the subprocess + * @returns {(opts: T) => Promise} A function that when called starts the subprocess + */ +export function forked(module, callback) { + if (process.env.SVELTEKIT_FORK && process.send) { + process.send({ type: 'ready', module }); + + process.on( + 'message', + /** @param {any} data */ async (data) => { + if (data?.type === 'args' && data.module === module) { + if (process.send) { + process.send({ + type: 'result', + module, + payload: await callback(data.payload) + }); + } + } + } + ); + } + + /** + * @param {T} opts + * @returns {Promise} + */ + const fn = function (opts) { + return new Promise((fulfil, reject) => { + const script = fileURLToPath(new URL(module, import.meta.url)); + + const child = child_process.fork(script, { + stdio: 'inherit', + env: { + SVELTEKIT_FORK: 'true' + }, + serialization: 'advanced' + }); + + child.on( + 'message', + /** @param {any} data */ (data) => { + if (data?.type === 'ready' && data.module === module) { + child.send({ + type: 'args', + module, + payload: opts + }); + } + + if (data?.type === 'result' && data.module === module) { + child.kill(); + fulfil(data.payload); + } + } + ); + + child.on('exit', (code) => { + if (code) { + reject(new Error(`Failed with code ${code}`)); + } + }); + }); + }; + + return fn; +} diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 91b4beec802a..3a806e066826 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -227,7 +227,7 @@ test.describe('Load', () => { } if (!process.env.DEV) { - test.skip('does not fetch __data.json if no server load function exists', async ({ + test('does not fetch __data.json if no server load function exists', async ({ page, clicknav }) => { diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 267b400a679f..32f384b42f54 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,4 +1,4 @@ -import { OutputAsset, OutputChunk } from 'rollup'; +import { OutputChunk } from 'rollup'; import { SvelteComponent } from 'svelte/internal'; import { Config, @@ -7,7 +7,6 @@ import { HandleServerError, KitConfig, Load, - RequestEvent, RequestHandler, ResolveOptions, Server, @@ -49,22 +48,13 @@ export interface BuildData { app_path: string; manifest_data: ManifestData; service_worker: string | null; - client: { - assets: OutputAsset[]; - chunks: OutputChunk[]; - entry: { - file: string; - imports: string[]; - stylesheets: string[]; - fonts: string[]; - }; - vite_manifest: import('vite').Manifest; - }; - server: { - chunks: OutputChunk[]; - methods: Record; - vite_manifest: import('vite').Manifest; - }; + client_entry: { + file: string; + imports: string[]; + stylesheets: string[]; + fonts: string[]; + } | null; + server_manifest: import('vite').Manifest; } export interface CSRPageNode { @@ -73,7 +63,6 @@ export interface CSRPageNode { load?: Load; trailingSlash?: TrailingSlash; }; - has_server_load: boolean; } export type CSRPageNodeLoader = () => Promise; @@ -244,6 +233,17 @@ export interface ServerErrorNode { status?: number; } +export interface ServerMetadata { + nodes: Array<{ has_server_load: boolean }>; + routes: Map< + string, + { + prerender: PrerenderOption | undefined; + methods: HttpMethod[]; + } + >; +} + export interface SSRComponent { default: { render(props: Record): { @@ -261,7 +261,7 @@ export type SSRComponentLoader = () => Promise; export interface SSRNode { component: SSRComponentLoader; - /** index into the `components` array in client-manifest.js */ + /** index into the `components` array in client/manifest.js */ index: number; /** client-side module URL for this component */ file: string;