From 84184c1cd6b681bdcafe8dd90d01fff2a3e970b4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 21 Aug 2025 20:46:04 -0400 Subject: [PATCH 01/17] WIP lazy discovery of remote functions --- .../kit/src/core/generate_manifest/index.js | 2 +- .../core/sync/create_manifest_data/index.js | 33 +---- packages/kit/src/exports/vite/index.js | 115 +++++++++++++++++- 3 files changed, 113 insertions(+), 37 deletions(-) diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index 8b775584c3a1..ea18c6a33053 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -101,7 +101,7 @@ export function generate_manifest({ build_data, prerendered, relative_path, rout ${(node_paths).map(loader).join(',\n')} ], remotes: { - ${build_data.manifest_data.remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, resolve_symlinks(build_data.server_manifest, remote.file).chunk.file))}`).join(',\n')} + ${build_data.manifest_data.remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `remote/${remote.hash}.js`))}`).join(',\n')} }, routes: [ ${routes.map(route => { diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index efc55fc5c574..499de0c8b7e9 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -4,11 +4,10 @@ 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, walk } from '../../../utils/filesystem.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 { hash } from '../../../utils/hash.js'; /** * Generates the manifest data used for the client-side manifest and types generation. @@ -28,7 +27,6 @@ export default function create_manifest_data({ const hooks = create_hooks(config, cwd); const matchers = create_matchers(config, cwd); const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback); - const remotes = create_remotes(config, cwd); for (const route of routes) { for (const param of route.params) { @@ -43,7 +41,7 @@ export default function create_manifest_data({ hooks, matchers, nodes, - remotes, + remotes: [], routes }; } @@ -468,33 +466,6 @@ function create_routes_and_nodes(cwd, config, fallback) { }; } -/** - * @param {import('types').ValidatedConfig} config - * @param {string} cwd - */ -function create_remotes(config, cwd) { - if (!config.kit.experimental.remoteFunctions) return []; - - const extensions = config.kit.moduleExtensions.map((ext) => `.remote${ext}`); - - /** @type {import('types').ManifestData['remotes']} */ - const remotes = []; - - // TODO could files live in other directories, including node_modules? - for (const file of walk(config.kit.files.src)) { - if (extensions.some((ext) => file.endsWith(ext))) { - const posixified = posixify(path.relative(cwd, `${config.kit.files.src}/${file}`)); - - remotes.push({ - hash: hash(posixified), - file: posixified - }); - } - } - - return remotes; -} - /** * @param {string} project_relative * @param {string} file diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index bab1f31b0e9b..af925b9bbfe7 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -656,10 +656,68 @@ async function kit({ svelte_config }) { /** @type {import('vite').ViteDevServer} */ let dev_server; + /** @type {Array<{ hash: string, file: string }>} */ + const remotes = []; + /** @type {import('vite').Plugin} */ const plugin_remote = { name: 'vite-plugin-sveltekit-remote', + config(config) { + // Ensure build.rollupOptions.output exists + config.build ??= {}; + config.build.rollupOptions ??= {}; + config.build.rollupOptions.output ??= {}; + + if (Array.isArray(config.build.rollupOptions.output)) { + // TODO I have no idea how this could occur + throw new Error('rollupOptions.output cannot be an array'); + } + + // Set up manualChunks to isolate *.remote.ts files + const { manualChunks } = config.build.rollupOptions.output; + + config.build.rollupOptions.output = { + ...config.build.rollupOptions.output, + manualChunks(id, meta) { + if (id === `${runtime_directory}/app/server/index.js`) { + return 'app-server'; + } + + // Check if this is a *.remote.ts file + if (id.endsWith('.remote.ts')) { + const relative = posixify(path.relative(cwd, id)); + + const remote = { + hash: hash(relative), + file: relative + }; + + remotes.push(remote); + + return `remote-${remote.hash}`; + } + + // If there was an existing manualChunks function, call it + if (typeof manualChunks === 'function') { + return manualChunks(id, meta); + } + + // If manualChunks is an object, check if this module matches any patterns + if (manualChunks) { + for (const name in manualChunks) { + const patterns = manualChunks[name]; + + // TODO is `id.includes(pattern)` correct? + if (patterns.some((pattern) => id.includes(pattern))) { + return name; + } + } + } + } + }; + }, + configureServer(_dev_server) { dev_server = _dev_server; }, @@ -813,11 +871,6 @@ async function kit({ svelte_config }) { } input['instrumentation.server'] = server_instrumentation; } - - // ...and every .remote file - for (const remote of manifest_data.remotes) { - input[`remote/${remote.hash}`] = path.resolve(remote.file); - } } else if (svelte_config.kit.output.bundleStrategy !== 'split') { input['bundle'] = `${runtime_directory}/client/bundle.js`; } else { @@ -978,6 +1031,58 @@ async function kit({ svelte_config }) { async handler(_options, bundle) { if (secondary_build_started) return; // only run this once + if (kit.experimental.remoteFunctions) { + for (const key in bundle) { + if (key.startsWith('chunks/remote-')) { + const chunk = bundle[key]; + if (chunk.type !== 'chunk') continue; + + const entries = Object.entries(chunk.modules).filter(([id]) => + svelte_config.kit.moduleExtensions.some((ext) => id.endsWith(`.remote${ext}`)) + ); + + if (entries.length !== 1) { + // this is impossible — the `manualChunks` step means that every remote file + // will be in its own chunk. but just to be safe, throw an error + throw new Error('An impossible situation occurred'); + } + + const entry = entries[0][1]; + + // this bit is a smidge hacky. we need to reconstruct the original exports + // by finding the `export` declaration and seeing how things were renamed. + // hopefully the way in which chunks are rendered doesn't change + const match = /export {([^}]+)};\n/.exec(chunk.code); + + if (!match) { + throw new Error('An impossible situation occurred'); + } + + const re_exports = []; + + for (const specifier of match[1].trim().split(',')) { + const [local, exported = local] = specifier.trim().split(' as '); + + if (entry.renderedExports.includes(local)) { + re_exports.push(local === exported ? local : `${exported} as ${local}`); + } + } + + try { + fs.mkdirSync(`${out}/server/remote`); + } catch {} + + fs.writeFileSync( + `${out}/server/remote/${key.slice('chunks/remote-'.length)}`, + `export { ${re_exports.join(', ')} } from '../${key}';` + ); + } + } + + // TODO this is kinda messy, but was the quickest way to see something working + manifest_data.remotes = remotes; + } + const verbose = vite_config.logLevel === 'info'; const log = logger({ verbose }); From ed741e1489d10d305c6cd1dec9d38dd5c5d1fa9a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 21 Aug 2025 22:08:10 -0400 Subject: [PATCH 02/17] fix --- packages/kit/src/exports/vite/dev/index.js | 14 ++++----- packages/kit/src/exports/vite/index.js | 35 +++++++++++----------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index e7a27a72e5bb..635131bc3355 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -29,9 +29,10 @@ 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 void>>} */ -export async function dev(vite, vite_config, svelte_config) { +export async function dev(vite, vite_config, svelte_config, get_remotes) { installPolyfills(); const async_local_storage = new AsyncLocalStorage(); @@ -266,12 +267,11 @@ export async function dev(vite, vite_config, svelte_config) { }; }), prerendered_routes: new Set(), - remotes: Object.fromEntries( - manifest_data.remotes.map((remote) => [ - remote.hash, - () => vite.ssrLoadModule(remote.file) - ]) - ), + get remotes() { + return Object.fromEntries( + get_remotes().map((remote) => [remote.hash, () => vite.ssrLoadModule(remote.file)]) + ); + }, routes: compact( manifest_data.routes.map((route) => { if (!route.page && !route.endpoint) return null; diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index af925b9bbfe7..234856d5f876 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -688,14 +688,7 @@ async function kit({ svelte_config }) { if (id.endsWith('.remote.ts')) { const relative = posixify(path.relative(cwd, id)); - const remote = { - hash: hash(relative), - file: relative - }; - - remotes.push(remote); - - return `remote-${remote.hash}`; + return `remote-${hash(relative)}`; } // If there was an existing manualChunks function, call it @@ -728,7 +721,13 @@ async function kit({ svelte_config }) { } const file = posixify(path.relative(cwd, id)); - const hashed = hash(file); + + const remote = { + hash: hash(file), + file + }; + + remotes.push(remote); if (opts?.ssr) { // in dev, add metadata to remote functions by self-importing @@ -742,7 +741,7 @@ async function kit({ svelte_config }) { $$_validate_$$($$_self_$$, ${s(file)}); for (const [name, fn] of Object.entries($$_self_$$)) { - fn.__.id = ${s(hashed)} + '/' + name; + fn.__.id = ${s(remote.hash)} + '/' + name; fn.__.name = name; } ` @@ -757,7 +756,7 @@ async function kit({ svelte_config }) { // For the client, read the exports and create a new module that only contains fetch functions with the correct metadata /** @type {Map} */ - const remotes = new Map(); + const map = new Map(); // in dev, load the server module here (which will result in this hook // being called again with `opts.ssr === true` if the module isn't @@ -768,7 +767,7 @@ async function kit({ svelte_config }) { for (const [name, value] of Object.entries(module)) { const type = value?.__?.type; if (type) { - remotes.set(name, type); + map.set(name, type); } } } @@ -776,20 +775,20 @@ async function kit({ svelte_config }) { // in prod, we already built and analysed the server code before // building the client code, so `remote_exports` is populated else if (remote_exports) { - const exports = remote_exports.get(hashed); + const exports = remote_exports.get(remote.hash); if (!exports) throw new Error('Expected to find metadata for remote file ' + id); for (const [name, value] of exports) { - remotes.set(name, value.type); + map.set(name, value.type); } } let namespace = '__remote'; let uid = 1; - while (remotes.has(namespace)) namespace = `__remote${uid++}`; + while (map.has(namespace)) namespace = `__remote${uid++}`; - const exports = Array.from(remotes).map(([name, type]) => { - return `export const ${name} = ${namespace}.${type}('${hashed}/${name}');`; + const exports = Array.from(map).map(([name, type]) => { + return `export const ${name} = ${namespace}.${type}('${remote.hash}/${name}');`; }); return { @@ -972,7 +971,7 @@ async function kit({ svelte_config }) { * @see https://vitejs.dev/guide/api-plugin.html#configureserver */ async configureServer(vite) { - return await dev(vite, vite_config, svelte_config); + return await dev(vite, vite_config, svelte_config, () => remotes); }, /** From 47ad01a78b1ba947d9f469a9134cb59189a66b4a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 09:01:58 -0400 Subject: [PATCH 03/17] partial fix --- packages/kit/src/exports/vite/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 234856d5f876..3b43f8212915 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -685,7 +685,7 @@ async function kit({ svelte_config }) { } // Check if this is a *.remote.ts file - if (id.endsWith('.remote.ts')) { + if (svelte_config.kit.moduleExtensions.some((ext) => id.endsWith(`.remote${ext}`))) { const relative = posixify(path.relative(cwd, id)); return `remote-${hash(relative)}`; From d2bc046f13c7ef2120faef845455ba3b3897b4fc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 10:52:25 -0400 Subject: [PATCH 04/17] some progress --- packages/kit/src/exports/vite/index.js | 59 +++++++++++++++++--------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 3b43f8212915..579237f99fdb 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -748,6 +748,14 @@ async function kit({ svelte_config }) { ); } + return ( + code + + dedent` + import * as $$_self_$$ from './${path.basename(id)}'; + $$_export_$$($$_self_$$); + ` + ); + // in prod, return as-is, and augment the build result instead. // this allows us to treeshake non-dynamic `prerender` functions return; @@ -1036,37 +1044,46 @@ async function kit({ svelte_config }) { const chunk = bundle[key]; if (chunk.type !== 'chunk') continue; - const entries = Object.entries(chunk.modules).filter(([id]) => - svelte_config.kit.moduleExtensions.some((ext) => id.endsWith(`.remote${ext}`)) - ); + /** @type {string[]} */ + const exports = []; - if (entries.length !== 1) { - // this is impossible — the `manualChunks` step means that every remote file - // will be in its own chunk. but just to be safe, throw an error - throw new Error('An impossible situation occurred'); - } - - const entry = entries[0][1]; + /** @type {string[]} */ + const re_exports = []; // this bit is a smidge hacky. we need to reconstruct the original exports - // by finding the `export` declaration and seeing how things were renamed. - // hopefully the way in which chunks are rendered doesn't change - const match = /export {([^}]+)};\n/.exec(chunk.code); + // from the injected `const $$_self_$$` declaration + let transformed = chunk.code.replace( + /const \$\$_self_\$\$ = [^]+?{([^]+?)}, Symbol\.toStringTag/, + (_, self) => { + // the self-import will look like a series of `get foo() { return foo }` + const getters = Array.from(self.matchAll(/get (\w+)/g)).map((m) => m[1]); + const returns = Array.from(self.matchAll(/return (\w+)/g)).map((m) => m[1]); - if (!match) { - throw new Error('An impossible situation occurred'); - } + for (let i = 0; i < getters.length; i += 1) { + const exported = getters[i]; + const local = returns[i]; - const re_exports = []; + // TODO do we need to guard against conflicts with the chunk's existing exports? + exports.push(local === exported ? local : `${local} as ${exported}`); - for (const specifier of match[1].trim().split(',')) { - const [local, exported = local] = specifier.trim().split(' as '); + re_exports.push(exported); + } - if (entry.renderedExports.includes(local)) { - re_exports.push(local === exported ? local : `${exported} as ${local}`); + return '// ' + _.replaceAll('\n', '\n// '); } + ); + + if (transformed === chunk.code) { + throw new Error('An impossible situation occurred (no self-import was found)'); } + transformed = transformed.replace( + '$$_export_$$($$_self_$$)', + `export { ${exports.join(', ')} };` + ); + + fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); + try { fs.mkdirSync(`${out}/server/remote`); } catch {} From 73c8834ebef693f8892c7fb477d7ab2b41ff2e48 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 15:38:03 -0400 Subject: [PATCH 05/17] WIP --- packages/kit/src/exports/vite/index.js | 42 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 579237f99fdb..7dcb06959e50 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1050,23 +1050,41 @@ async function kit({ svelte_config }) { /** @type {string[]} */ const re_exports = []; + /** @type {Map} */ + const existing_exports = new Map(); + + const match = /export {([^}]+)};\n$/.exec(chunk.code); + + if (match) { + for (const specifier of match[1].trim().split(',')) { + const [local, exported = local] = specifier.trim().split(' as '); + existing_exports.set(local, exported); + } + } + // this bit is a smidge hacky. we need to reconstruct the original exports // from the injected `const $$_self_$$` declaration let transformed = chunk.code.replace( /const \$\$_self_\$\$ = [^]+?{([^]+?)}, Symbol\.toStringTag/, (_, self) => { // the self-import will look like a series of `get foo() { return foo }` - const getters = Array.from(self.matchAll(/get (\w+)/g)).map((m) => m[1]); - const returns = Array.from(self.matchAll(/return (\w+)/g)).map((m) => m[1]); - - for (let i = 0; i < getters.length; i += 1) { - const exported = getters[i]; - const local = returns[i]; - - // TODO do we need to guard against conflicts with the chunk's existing exports? - exports.push(local === exported ? local : `${local} as ${exported}`); - - re_exports.push(exported); + const names = Array.from(self.matchAll(/get (\w+)/g)).map((m) => m[1]); + const values = Array.from(self.matchAll(/return (\w+)/g)).map((m) => m[1]); + + for (let i = 0; i < names.length; i += 1) { + const name = names[i]; + const value = values[i]; + + const existing_export = existing_exports.get(value); + + if (existing_export) { + re_exports.push( + existing_export === name ? name : `${existing_export} as ${name}` + ); + } else { + exports.push(value === name ? name : `${value} as ${name}`); + re_exports.push(name); + } } return '// ' + _.replaceAll('\n', '\n// '); @@ -1079,7 +1097,7 @@ async function kit({ svelte_config }) { transformed = transformed.replace( '$$_export_$$($$_self_$$)', - `export { ${exports.join(', ')} };` + `export { ${exports.join(', ')} }` ); fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); From 6a0065674a51afb741a4a3091413dbf123a16127 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 15:44:53 -0400 Subject: [PATCH 06/17] tweak --- packages/kit/src/exports/vite/index.js | 118 ++++++++++++------------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 7dcb06959e50..5885152d05bf 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1039,82 +1039,80 @@ async function kit({ svelte_config }) { if (secondary_build_started) return; // only run this once if (kit.experimental.remoteFunctions) { - for (const key in bundle) { - if (key.startsWith('chunks/remote-')) { - const chunk = bundle[key]; - if (chunk.type !== 'chunk') continue; + // TODO this is kinda messy, but was the quickest way to see something working + manifest_data.remotes = remotes; - /** @type {string[]} */ - const exports = []; + for (const remote of remotes) { + const chunk = bundle[`chunks/remote-${remote.hash}.js`]; + if (chunk.type !== 'chunk') continue; - /** @type {string[]} */ - const re_exports = []; + /** @type {string[]} */ + const exports = []; - /** @type {Map} */ - const existing_exports = new Map(); + /** @type {string[]} */ + const re_exports = []; - const match = /export {([^}]+)};\n$/.exec(chunk.code); + /** @type {Map} */ + const existing_exports = new Map(); - if (match) { - for (const specifier of match[1].trim().split(',')) { - const [local, exported = local] = specifier.trim().split(' as '); - existing_exports.set(local, exported); - } + const match = /export {([^}]+)};\n$/.exec(chunk.code); + + if (match) { + for (const specifier of match[1].trim().split(',')) { + const [local, exported = local] = specifier.trim().split(' as '); + existing_exports.set(local, exported); } + } - // this bit is a smidge hacky. we need to reconstruct the original exports - // from the injected `const $$_self_$$` declaration - let transformed = chunk.code.replace( - /const \$\$_self_\$\$ = [^]+?{([^]+?)}, Symbol\.toStringTag/, - (_, self) => { - // the self-import will look like a series of `get foo() { return foo }` - const names = Array.from(self.matchAll(/get (\w+)/g)).map((m) => m[1]); - const values = Array.from(self.matchAll(/return (\w+)/g)).map((m) => m[1]); - - for (let i = 0; i < names.length; i += 1) { - const name = names[i]; - const value = values[i]; - - const existing_export = existing_exports.get(value); - - if (existing_export) { - re_exports.push( - existing_export === name ? name : `${existing_export} as ${name}` - ); - } else { - exports.push(value === name ? name : `${value} as ${name}`); - re_exports.push(name); - } + // this bit is a smidge hacky. we need to reconstruct the original exports + // from the injected `const $$_self_$$` declaration + let transformed = chunk.code.replace( + /const \$\$_self_\$\$ = [^]+?{([^]+?)}, Symbol\.toStringTag/, + (_, self) => { + // the self-import will look like a series of `get foo() { return foo }` + const names = Array.from(self.matchAll(/get (\w+)/g)).map((m) => m[1]); + const values = Array.from(self.matchAll(/return (\w+)/g)).map((m) => m[1]); + + for (let i = 0; i < names.length; i += 1) { + const name = names[i]; + const value = values[i]; + + const existing_export = existing_exports.get(value); + + if (existing_export) { + re_exports.push( + existing_export === name ? name : `${existing_export} as ${name}` + ); + } else { + exports.push(value === name ? name : `${value} as ${name}`); + re_exports.push(name); } - - return '// ' + _.replaceAll('\n', '\n// '); } - ); - if (transformed === chunk.code) { - throw new Error('An impossible situation occurred (no self-import was found)'); + return '// ' + _.replaceAll('\n', '\n// '); } + ); - transformed = transformed.replace( - '$$_export_$$($$_self_$$)', - `export { ${exports.join(', ')} }` - ); + if (transformed === chunk.code) { + throw new Error('An impossible situation occurred (no self-import was found)'); + } - fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); + transformed = transformed.replace( + '$$_export_$$($$_self_$$)', + `export { ${exports.join(', ')} }` + ); - try { - fs.mkdirSync(`${out}/server/remote`); - } catch {} + fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); - fs.writeFileSync( - `${out}/server/remote/${key.slice('chunks/remote-'.length)}`, - `export { ${re_exports.join(', ')} } from '../${key}';` - ); - } - } + try { + fs.mkdirSync(`${out}/server/remote`); + } catch {} - // TODO this is kinda messy, but was the quickest way to see something working - manifest_data.remotes = remotes; + fs.writeFileSync( + `${out}/server/remote/${remote.hash}.js`, + `export { ${re_exports.join(', ')} } from '../chunks/remote-${remote.hash}.js';` + ); + } } const verbose = vite_config.logLevel === 'info'; From eaae248f1267fc619fa0118d4d29588a4b2d6410 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 16:49:56 -0400 Subject: [PATCH 07/17] working but messy --- packages/kit/src/core/postbuild/analyse.js | 2 +- .../src/exports/vite/build/build_remote.js | 53 +++++++++----- packages/kit/src/exports/vite/index.js | 70 +++++++++++++------ packages/kit/src/runtime/server/remote.js | 6 +- 4 files changed, 89 insertions(+), 42 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index e991996eeb42..5747dabcad00 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -168,7 +168,7 @@ async function analyse({ // analyse remotes for (const remote of manifest_data.remotes) { const loader = manifest._.remotes[remote.hash]; - const module = await loader(); + const module = (await loader()).default; validate_remote_functions(module, remote.file); diff --git a/packages/kit/src/exports/vite/build/build_remote.js b/packages/kit/src/exports/vite/build/build_remote.js index 668859858810..2606e5fe92c8 100644 --- a/packages/kit/src/exports/vite/build/build_remote.js +++ b/packages/kit/src/exports/vite/build/build_remote.js @@ -12,27 +12,39 @@ import { import_peer } from '../../../utils/import.js'; * later, which wouldn't work if we do a self-import and iterate over all exports (since we're reading them then). * @param {string} out * @param {ManifestData} manifest_data + * @param {Map>} chunks */ -export function build_remotes(out, manifest_data) { +export function build_remotes(out, manifest_data, chunks) { const dir = `${out}/server/remote`; for (const remote of manifest_data.remotes) { const entry = `${dir}/${remote.hash}.js`; - const tmp = `${remote.hash}.tmp.js`; + // const tmp = `${remote.hash}.tmp.js`; - fs.renameSync(entry, `${dir}/${tmp}`); - fs.writeFileSync( - entry, - dedent` - import * as $$_self_$$ from './${tmp}'; + const chunk_exports = /** @type {Map} */ (chunks.get(remote.hash)); - for (const [name, fn] of Object.entries($$_self_$$)) { - fn.__.id = '${remote.hash}/' + name; - fn.__.name = name; - } + // const imports = Array.from(chunk_exports).map(([name, exported]) => name === exported ? name : `${exported} as ${name}`); + // const exports = Array.from(chunk_exports.keys()); - export * from './${tmp}'; - ` + // // fs.renameSync(entry, `${dir}/${tmp}`); + // fs.writeFileSync( + // entry, + // dedent` + // import { ${imports.join(', ')} } from '../chunks/remote-${remote.hash}.js'; + + // for (const [name, fn] of Object.entries({ ${exports.join(', ')} })) { + // fn.__.id = '${remote.hash}/' + name; + // fn.__.name = name; + // } + + // export { ${exports.join(', ')} }; + // ` + // ); + + // fs.renameSync(entry, `${dir}/${tmp}`); + fs.writeFileSync( + entry, + `export { default } from '../chunks/remote-${remote.hash}.js';` ); } } @@ -47,8 +59,10 @@ export function build_remotes(out, manifest_data) { * @param {string} out * @param {ManifestData} manifest_data * @param {ServerMetadata} metadata + * @param {Map>} chunks */ -export async function treeshake_prerendered_remotes(out, manifest_data, metadata) { +export async function treeshake_prerendered_remotes(out, manifest_data, metadata, chunks) { + return; if (manifest_data.remotes.length === 0) { return; } @@ -80,12 +94,19 @@ export async function treeshake_prerendered_remotes(out, manifest_data, metadata (value.dynamic ? dynamic : prerendered).push(name); } + const chunk_exports = /** @type {Map} */ (chunks.get(remote.hash)); + + const imports = dynamic.map((name) => { + const exported = chunk_exports.get(name); + return exported === name ? name : `${exported} as ${name}`; + }); + const remote_file = posixify(`${dir}/${remote.hash}.js`); fs.writeFileSync( remote_file, dedent` - import { ${dynamic.join(', ')} } from './${remote.hash}.tmp.js'; + import { ${imports.join(', ')} } from '../chunks/remote-${remote.hash}.js'; import { prerender } from '../${path.basename(remote_entry)}'; ${prerendered.map((name) => `export const ${name} = prerender('unchecked', () => { throw new Error('Unexpectedly called prerender function. Did you forget to set { dynamic: true } ?') });`).join('\n')} @@ -124,6 +145,6 @@ export async function treeshake_prerendered_remotes(out, manifest_data, metadata } for (const remote of manifest_data.remotes) { - fs.unlinkSync(`${dir}/${remote.hash}.tmp.js`); + // fs.unlinkSync(`${dir}/${remote.hash}.tmp.js`); } } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 5885152d05bf..ee8f73d018ec 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -46,6 +46,8 @@ import { should_ignore } from './static_analysis/utils.js'; const cwd = process.cwd(); +Error.stackTraceLimit = Infinity; + /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { appType: true, @@ -738,12 +740,15 @@ async function kit({ svelte_config }) { import * as $$_self_$$ from './${path.basename(id)}'; import { validate_remote_functions as $$_validate_$$ } from '@sveltejs/kit/internal'; - $$_validate_$$($$_self_$$, ${s(file)}); + // $$_validate_$$($$_self_$$, ${s(file)}); for (const [name, fn] of Object.entries($$_self_$$)) { + if (name === 'default') continue; fn.__.id = ${s(remote.hash)} + '/' + name; fn.__.name = name; } + + export default $$_self_$$; ` ); } @@ -1038,14 +1043,29 @@ async function kit({ svelte_config }) { async handler(_options, bundle) { if (secondary_build_started) return; // only run this once + /** + * A name -> export map for every remote chunk + * @type {Map>} + */ + const remote_chunks = new Map(); + if (kit.experimental.remoteFunctions) { // TODO this is kinda messy, but was the quickest way to see something working manifest_data.remotes = remotes; + try { + fs.mkdirSync(`${out}/server/remote`); + } catch {} + for (const remote of remotes) { const chunk = bundle[`chunks/remote-${remote.hash}.js`]; if (chunk.type !== 'chunk') continue; + /** @type {Map} */ + const chunk_exports = new Map(); + + remote_chunks.set(remote.hash, chunk_exports); + /** @type {string[]} */ const exports = []; @@ -1077,16 +1097,24 @@ async function kit({ svelte_config }) { const name = names[i]; const value = values[i]; - const existing_export = existing_exports.get(value); + exports.push(value === name ? name : `${name}: ${value}`); - if (existing_export) { - re_exports.push( - existing_export === name ? name : `${existing_export} as ${name}` - ); - } else { - exports.push(value === name ? name : `${value} as ${name}`); - re_exports.push(name); - } + // const existing_export = existing_exports.get(value); + + // if (existing_export) { + // re_exports.push( + // existing_export === name ? name : `${existing_export} as ${name}` + // ); + + // exports.push(value === name ? name : `${name}: ${value}`); + + // chunk_exports.set(name, existing_export); + // } else { + // exports.push(value === name ? name : `${name}: ${value}`); + // re_exports.push(name); + + // chunk_exports.set(name, value); + // } } return '// ' + _.replaceAll('\n', '\n// '); @@ -1099,22 +1127,23 @@ async function kit({ svelte_config }) { transformed = transformed.replace( '$$_export_$$($$_self_$$)', - `export { ${exports.join(', ')} }` + `const $$_functions_$$ = { ${exports.join(', ')} }; for (const [name, fn] of Object.entries($$_functions_$$)) { fn.__.id = '${remote.hash}/' + name; fn.__.name = name; }; export default $$_functions_$$;` ); fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); - try { - fs.mkdirSync(`${out}/server/remote`); - } catch {} + // fs.writeFileSync( + // `${out}/server/remote/${remote.hash}.js`, + // `export { ${re_exports.join(', ')} } from '../chunks/remote-${remote.hash}.js';` + // ); - fs.writeFileSync( - `${out}/server/remote/${remote.hash}.js`, - `export { ${re_exports.join(', ')} } from '../chunks/remote-${remote.hash}.js';` - ); + // console.log(remote.hash, chunk_exports); } } + // ...make sure remote exports have their IDs assigned... + build_remotes(out, manifest_data, remote_chunks); + const verbose = vite_config.logLevel === 'info'; const log = logger({ verbose }); @@ -1355,9 +1384,6 @@ async function kit({ svelte_config }) { static_exports ); - // ...make sure remote exports have their IDs assigned... - build_remotes(out, manifest_data); - // ...and prerender const { prerendered, prerender_map } = await prerender({ hash: kit.router.type === 'hash', @@ -1380,7 +1406,7 @@ async function kit({ svelte_config }) { ); // remove prerendered remote functions - await treeshake_prerendered_remotes(out, manifest_data, metadata); + await treeshake_prerendered_remotes(out, manifest_data, metadata, remote_chunks); if (service_worker_entry_file) { if (kit.paths.assets) { diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 6a5ec140a990..595846116555 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -43,7 +43,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id) if (!remotes[hash]) error(404); const module = await remotes[hash](); - const fn = module[name]; + const fn = module.default[name]; if (!fn) error(404); @@ -161,7 +161,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id) if (!loader) error(400, 'Bad Request'); const module = await loader(); - const fn = module[name]; + const fn = module.default[name]; if (!fn) error(400, 'Bad Request'); @@ -208,7 +208,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) { const remotes = manifest._.remotes; const module = await remotes[hash]?.(); - let form = /** @type {RemoteForm} */ (module?.[name]); + let form = /** @type {RemoteForm} */ (module?.default[name]); if (!form) { event.setHeaders({ From c4b92524b15a7ba292cc1bc75d8aff3123ae1096 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 17:11:39 -0400 Subject: [PATCH 08/17] WIP --- .../kit/src/core/generate_manifest/index.js | 2 +- .../src/exports/vite/build/build_remote.js | 150 ------------------ packages/kit/src/exports/vite/index.js | 83 +--------- 3 files changed, 5 insertions(+), 230 deletions(-) delete mode 100644 packages/kit/src/exports/vite/build/build_remote.js diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index ea18c6a33053..9b5bfb94dda9 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -101,7 +101,7 @@ export function generate_manifest({ build_data, prerendered, relative_path, rout ${(node_paths).map(loader).join(',\n')} ], remotes: { - ${build_data.manifest_data.remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `remote/${remote.hash}.js`))}`).join(',\n')} + ${build_data.manifest_data.remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `chunks/remote-${remote.hash}.js`))}`).join(',\n')} }, routes: [ ${routes.map(route => { diff --git a/packages/kit/src/exports/vite/build/build_remote.js b/packages/kit/src/exports/vite/build/build_remote.js deleted file mode 100644 index 2606e5fe92c8..000000000000 --- a/packages/kit/src/exports/vite/build/build_remote.js +++ /dev/null @@ -1,150 +0,0 @@ -/** @import { ManifestData, ServerMetadata } from 'types' */ -import fs from 'node:fs'; -import path from 'node:path'; -import { posixify } from '../../../utils/filesystem.js'; -import { dedent } from '../../../core/sync/utils.js'; -import { import_peer } from '../../../utils/import.js'; - -/** - * Moves the remote files to a sibling file and rewrites the original remote file to import from that sibling file, - * enhancing the remote functions with their hashed ID. - * This is not done through a self-import like during DEV because we want to treeshake prerendered remote functions - * later, which wouldn't work if we do a self-import and iterate over all exports (since we're reading them then). - * @param {string} out - * @param {ManifestData} manifest_data - * @param {Map>} chunks - */ -export function build_remotes(out, manifest_data, chunks) { - const dir = `${out}/server/remote`; - - for (const remote of manifest_data.remotes) { - const entry = `${dir}/${remote.hash}.js`; - // const tmp = `${remote.hash}.tmp.js`; - - const chunk_exports = /** @type {Map} */ (chunks.get(remote.hash)); - - // const imports = Array.from(chunk_exports).map(([name, exported]) => name === exported ? name : `${exported} as ${name}`); - // const exports = Array.from(chunk_exports.keys()); - - // // fs.renameSync(entry, `${dir}/${tmp}`); - // fs.writeFileSync( - // entry, - // dedent` - // import { ${imports.join(', ')} } from '../chunks/remote-${remote.hash}.js'; - - // for (const [name, fn] of Object.entries({ ${exports.join(', ')} })) { - // fn.__.id = '${remote.hash}/' + name; - // fn.__.name = name; - // } - - // export { ${exports.join(', ')} }; - // ` - // ); - - // fs.renameSync(entry, `${dir}/${tmp}`); - fs.writeFileSync( - entry, - `export { default } from '../chunks/remote-${remote.hash}.js';` - ); - } -} - - -/** - * For each remote module, checks if there are treeshakeable prerendered remote functions, - * then accomplishes the treeshaking by rewriting the remote files to only include the non-prerendered imports, - * replacing the prerendered remote functions with a dummy function that should never be called, - * and doing a Vite build. This will not treeshake perfectly yet as everything except the remote files are treated as external, - * so it will not go into those files to check what can be treeshaken inside them. - * @param {string} out - * @param {ManifestData} manifest_data - * @param {ServerMetadata} metadata - * @param {Map>} chunks - */ -export async function treeshake_prerendered_remotes(out, manifest_data, metadata, chunks) { - return; - if (manifest_data.remotes.length === 0) { - return; - } - - const dir = posixify(`${out}/server/remote`); - - const vite = /** @type {typeof import('vite')} */ (await import_peer('vite')); - const remote_entry = posixify(`${out}/server/remote-entry.js`); - - const prefix = 'optimized/'; - - const input = { - // include this file in the bundle, so that Rollup understands - // that functions like `prerender` are side-effect free - [path.basename(remote_entry.slice(0, -3))]: remote_entry - }; - - for (const remote of manifest_data.remotes) { - const exports = metadata.remotes.get(remote.hash); - if (!exports) throw new Error('An impossible situation occurred'); - - /** @type {string[]} */ - const dynamic = []; - - /** @type {string[]} */ - const prerendered = []; - - for (const [name, value] of exports) { - (value.dynamic ? dynamic : prerendered).push(name); - } - - const chunk_exports = /** @type {Map} */ (chunks.get(remote.hash)); - - const imports = dynamic.map((name) => { - const exported = chunk_exports.get(name); - return exported === name ? name : `${exported} as ${name}`; - }); - - const remote_file = posixify(`${dir}/${remote.hash}.js`); - - fs.writeFileSync( - remote_file, - dedent` - import { ${imports.join(', ')} } from '../chunks/remote-${remote.hash}.js'; - import { prerender } from '../${path.basename(remote_entry)}'; - - ${prerendered.map((name) => `export const ${name} = prerender('unchecked', () => { throw new Error('Unexpectedly called prerender function. Did you forget to set { dynamic: true } ?') });`).join('\n')} - - for (const [name, fn] of Object.entries({ ${Array.from(exports.keys()).join(', ')} })) { - fn.__.id = '${remote.hash}/' + name; - fn.__.name = name; - } - - export { ${dynamic.join(', ')} }; - ` - ); - - input[prefix + remote.hash] = remote_file; - } - - const bundle = /** @type {import('vite').Rollup.RollupOutput} */ (await vite.build({ - configFile: false, - build: { - write: false, - ssr: true, - rollupOptions: { - external: (id) => { - if (id[0] === '.') return; - return !id.startsWith(dir); - }, - input - } - } - })); - - for (const chunk of bundle.output) { - if (chunk.type === 'chunk' && chunk.name.startsWith(prefix)) { - fs.writeFileSync(`${dir}/${chunk.fileName.slice(prefix.length)}`, chunk.code); - } - } - - for (const remote of manifest_data.remotes) { - // fs.unlinkSync(`${dir}/${remote.hash}.tmp.js`); - } -} diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index ee8f73d018ec..0d1f3223969c 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -41,7 +41,6 @@ import { } from './module_ids.js'; import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; -import { build_remotes, treeshake_prerendered_remotes } from './build/build_remote.js'; import { should_ignore } from './static_analysis/utils.js'; const cwd = process.cwd(); @@ -753,6 +752,7 @@ async function kit({ svelte_config }) { ); } + // in prod, return as-is, and augment the build result instead return ( code + dedent` @@ -760,10 +760,6 @@ async function kit({ svelte_config }) { $$_export_$$($$_self_$$); ` ); - - // in prod, return as-is, and augment the build result instead. - // this allows us to treeshake non-dynamic `prerender` functions - return; } // For the client, read the exports and create a new module that only contains fetch functions with the correct metadata @@ -1066,84 +1062,16 @@ async function kit({ svelte_config }) { remote_chunks.set(remote.hash, chunk_exports); - /** @type {string[]} */ - const exports = []; - - /** @type {string[]} */ - const re_exports = []; - - /** @type {Map} */ - const existing_exports = new Map(); - - const match = /export {([^}]+)};\n$/.exec(chunk.code); - - if (match) { - for (const specifier of match[1].trim().split(',')) { - const [local, exported = local] = specifier.trim().split(' as '); - existing_exports.set(local, exported); - } - } - - // this bit is a smidge hacky. we need to reconstruct the original exports - // from the injected `const $$_self_$$` declaration - let transformed = chunk.code.replace( - /const \$\$_self_\$\$ = [^]+?{([^]+?)}, Symbol\.toStringTag/, - (_, self) => { - // the self-import will look like a series of `get foo() { return foo }` - const names = Array.from(self.matchAll(/get (\w+)/g)).map((m) => m[1]); - const values = Array.from(self.matchAll(/return (\w+)/g)).map((m) => m[1]); - - for (let i = 0; i < names.length; i += 1) { - const name = names[i]; - const value = values[i]; - - exports.push(value === name ? name : `${name}: ${value}`); - - // const existing_export = existing_exports.get(value); - - // if (existing_export) { - // re_exports.push( - // existing_export === name ? name : `${existing_export} as ${name}` - // ); - - // exports.push(value === name ? name : `${name}: ${value}`); - - // chunk_exports.set(name, existing_export); - // } else { - // exports.push(value === name ? name : `${name}: ${value}`); - // re_exports.push(name); - - // chunk_exports.set(name, value); - // } - } - - return '// ' + _.replaceAll('\n', '\n// '); - } - ); - - if (transformed === chunk.code) { - throw new Error('An impossible situation occurred (no self-import was found)'); - } - - transformed = transformed.replace( + const transformed = chunk.code.replace( '$$_export_$$($$_self_$$)', - `const $$_functions_$$ = { ${exports.join(', ')} }; for (const [name, fn] of Object.entries($$_functions_$$)) { fn.__.id = '${remote.hash}/' + name; fn.__.name = name; }; export default $$_functions_$$;` + () => + `for (const [name, fn] of Object.entries($$_self_$$)) { fn.__.id = '${remote.hash}/' + name; fn.__.name = name; }; export default $$_self_$$;` ); fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); - - // fs.writeFileSync( - // `${out}/server/remote/${remote.hash}.js`, - // `export { ${re_exports.join(', ')} } from '../chunks/remote-${remote.hash}.js';` - // ); - - // console.log(remote.hash, chunk_exports); } } - // ...make sure remote exports have their IDs assigned... - build_remotes(out, manifest_data, remote_chunks); - const verbose = vite_config.logLevel === 'info'; const log = logger({ verbose }); @@ -1405,9 +1333,6 @@ async function kit({ svelte_config }) { })};\n` ); - // remove prerendered remote functions - await treeshake_prerendered_remotes(out, manifest_data, metadata, remote_chunks); - if (service_worker_entry_file) { if (kit.paths.assets) { throw new Error('Cannot use service worker alongside config.kit.paths.assets'); From 4114a894b599aa1e710b22b6f0e84ab4d34820cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 17:19:25 -0400 Subject: [PATCH 09/17] WIP --- packages/kit/src/exports/vite/dev/index.js | 5 ++++- packages/kit/src/exports/vite/index.js | 5 +---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 635131bc3355..2d9324e65201 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -269,7 +269,10 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { prerendered_routes: new Set(), get remotes() { return Object.fromEntries( - get_remotes().map((remote) => [remote.hash, () => vite.ssrLoadModule(remote.file)]) + get_remotes().map((remote) => [ + remote.hash, + () => vite.ssrLoadModule(remote.file).then((module) => ({ default: module })) + ]) ); }, routes: compact( diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0d1f3223969c..3b7f68eb4e76 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -739,15 +739,12 @@ async function kit({ svelte_config }) { import * as $$_self_$$ from './${path.basename(id)}'; import { validate_remote_functions as $$_validate_$$ } from '@sveltejs/kit/internal'; - // $$_validate_$$($$_self_$$, ${s(file)}); + $$_validate_$$($$_self_$$, ${s(file)}); for (const [name, fn] of Object.entries($$_self_$$)) { - if (name === 'default') continue; fn.__.id = ${s(remote.hash)} + '/' + name; fn.__.name = name; } - - export default $$_self_$$; ` ); } From 367a6a83e0ac3099184d04c55167f77d5eae12d8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 17:22:40 -0400 Subject: [PATCH 10/17] unused --- packages/kit/src/exports/vite/index.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 3b7f68eb4e76..5688573b3307 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1036,12 +1036,6 @@ async function kit({ svelte_config }) { async handler(_options, bundle) { if (secondary_build_started) return; // only run this once - /** - * A name -> export map for every remote chunk - * @type {Map>} - */ - const remote_chunks = new Map(); - if (kit.experimental.remoteFunctions) { // TODO this is kinda messy, but was the quickest way to see something working manifest_data.remotes = remotes; @@ -1054,11 +1048,6 @@ async function kit({ svelte_config }) { const chunk = bundle[`chunks/remote-${remote.hash}.js`]; if (chunk.type !== 'chunk') continue; - /** @type {Map} */ - const chunk_exports = new Map(); - - remote_chunks.set(remote.hash, chunk_exports); - const transformed = chunk.code.replace( '$$_export_$$($$_self_$$)', () => From 42cb3e784d159784d9c47818e901de21df53758f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 17:42:00 -0400 Subject: [PATCH 11/17] WIP --- packages/kit/src/core/postbuild/analyse.js | 9 ++-- packages/kit/src/exports/internal/index.js | 2 +- .../src/exports/internal/remote-functions.js | 17 +++++-- packages/kit/src/exports/vite/index.js | 48 +++++++------------ 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 5747dabcad00..fc46abc8058e 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -11,7 +11,6 @@ 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'; -import { validate_remote_functions } from '@sveltejs/kit/internal'; export default forked(import.meta.url, analyse); @@ -168,14 +167,12 @@ async function analyse({ // analyse remotes for (const remote of manifest_data.remotes) { const loader = manifest._.remotes[remote.hash]; - const module = (await loader()).default; - - validate_remote_functions(module, remote.file); + const { default: functions } = await loader(); const exports = new Map(); - for (const name in module) { - const info = /** @type {import('types').RemoteInfo} */ (module[name].__); + for (const name in functions) { + const info = /** @type {import('types').RemoteInfo} */ (functions[name].__); const type = info.type; exports.set(name, { diff --git a/packages/kit/src/exports/internal/index.js b/packages/kit/src/exports/internal/index.js index c358bca93251..b87448b30914 100644 --- a/packages/kit/src/exports/internal/index.js +++ b/packages/kit/src/exports/internal/index.js @@ -62,4 +62,4 @@ export class ActionFailure { } } -export { validate_remote_functions } from './remote-functions.js'; +export { init_remote_functions } from './remote-functions.js'; diff --git a/packages/kit/src/exports/internal/remote-functions.js b/packages/kit/src/exports/internal/remote-functions.js index ad7962399cb8..751ef47d2cc8 100644 --- a/packages/kit/src/exports/internal/remote-functions.js +++ b/packages/kit/src/exports/internal/remote-functions.js @@ -1,21 +1,28 @@ +/** @import { RemoteInfo } from 'types' */ + +/** @type {RemoteInfo['type'][]} */ +const types = ['command', 'form', 'prerender', 'query']; + /** * @param {Record} module * @param {string} file + * @param {string} hash */ -export function validate_remote_functions(module, file) { +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 in module) { - const type = module[name]?.__?.type; - - if (type !== 'form' && type !== 'command' && type !== 'query' && type !== 'prerender') { + 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; } } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 5688573b3307..a09d304b2d4a 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -731,32 +731,25 @@ async function kit({ svelte_config }) { remotes.push(remote); if (opts?.ssr) { - // in dev, add metadata to remote functions by self-importing - if (dev_server) { - return ( - code + - dedent` - import * as $$_self_$$ from './${path.basename(id)}'; - import { validate_remote_functions as $$_validate_$$ } from '@sveltejs/kit/internal'; - - $$_validate_$$($$_self_$$, ${s(file)}); - - for (const [name, fn] of Object.entries($$_self_$$)) { - fn.__.id = ${s(remote.hash)} + '/' + name; - fn.__.name = name; - } - ` - ); + code += dedent` + import * as $$_self_$$ from './${path.basename(id)}'; + import { init_remote_functions as $$_init_$$ } from '@sveltejs/kit/internal'; + + $$_init_$$($$_self_$$, ${s(file)}, ${s(remote.hash)}); + + for (const [name, fn] of Object.entries($$_self_$$)) { + fn.__.id = ${s(remote.hash)} + '/' + name; + fn.__.name = name; + } + `; + + if (!dev_server) { + // in prod, prevent the functions from being treeshaken. This will + // be replaced with an `export default` in the `writeBundle` hook + code += `$$_export_$$($$_self_$$);`; } - // in prod, return as-is, and augment the build result instead - return ( - code + - dedent` - import * as $$_self_$$ from './${path.basename(id)}'; - $$_export_$$($$_self_$$); - ` - ); + return code; } // For the client, read the exports and create a new module that only contains fetch functions with the correct metadata @@ -1040,18 +1033,13 @@ async function kit({ svelte_config }) { // TODO this is kinda messy, but was the quickest way to see something working manifest_data.remotes = remotes; - try { - fs.mkdirSync(`${out}/server/remote`); - } catch {} - for (const remote of remotes) { const chunk = bundle[`chunks/remote-${remote.hash}.js`]; if (chunk.type !== 'chunk') continue; const transformed = chunk.code.replace( '$$_export_$$($$_self_$$)', - () => - `for (const [name, fn] of Object.entries($$_self_$$)) { fn.__.id = '${remote.hash}/' + name; fn.__.name = name; }; export default $$_self_$$;` + () => `export default $$_self_$$;` ); fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); From 2695fa0da33845baf6ca773af93ce26c4cfdff8c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 17:58:38 -0400 Subject: [PATCH 12/17] more --- .../kit/src/core/generate_manifest/index.js | 6 +++-- packages/kit/src/core/postbuild/analyse.js | 7 +++-- .../core/sync/create_manifest_data/index.js | 1 - packages/kit/src/exports/vite/index.js | 27 +++++++++---------- packages/kit/src/types/internal.d.ts | 9 ++++--- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index 9b5bfb94dda9..6b2445aa2107 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -1,3 +1,4 @@ +/** @import { RemoteChunk } from 'types' */ import fs from 'node:fs'; import path from 'node:path'; import * as mime from 'mrmime'; @@ -18,9 +19,10 @@ import { uneval } from 'devalue'; * prerendered: string[]; * relative_path: string; * routes: import('types').RouteData[]; + * remotes: RemoteChunk[]; * }} opts */ -export function generate_manifest({ build_data, prerendered, relative_path, routes }) { +export function generate_manifest({ build_data, prerendered, relative_path, routes, remotes }) { /** * @type {Map} The new index of each node in the filtered nodes array */ @@ -101,7 +103,7 @@ export function generate_manifest({ build_data, prerendered, relative_path, rout ${(node_paths).map(loader).join(',\n')} ], remotes: { - ${build_data.manifest_data.remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `chunks/remote-${remote.hash}.js`))}`).join(',\n')} + ${remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `chunks/remote-${remote.hash}.js`))}`).join(',\n')} }, routes: [ ${routes.map(route => { diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index fc46abc8058e..d1c4800d1161 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -1,3 +1,4 @@ +/** @import { RemoteChunk } from 'types' */ import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { validate_server_exports } from '../../utils/exports.js'; @@ -24,6 +25,7 @@ export default forked(import.meta.url, analyse); * env: Record; * out: string; * output_config: import('types').RecursiveRequired; + * remotes: RemoteChunk[]; * }} opts */ async function analyse({ @@ -34,7 +36,8 @@ async function analyse({ tracked_features, env, out, - output_config + output_config, + remotes }) { /** @type {import('@sveltejs/kit').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; @@ -165,7 +168,7 @@ async function analyse({ } // analyse remotes - for (const remote of manifest_data.remotes) { + for (const remote of remotes) { const loader = manifest._.remotes[remote.hash]; const { default: functions } = await loader(); diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 499de0c8b7e9..b7c5e93d658d 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -41,7 +41,6 @@ export default function create_manifest_data({ hooks, matchers, nodes, - remotes: [], routes }; } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index a09d304b2d4a..462a6a3685f5 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1030,19 +1030,14 @@ async function kit({ svelte_config }) { if (secondary_build_started) return; // only run this once if (kit.experimental.remoteFunctions) { - // TODO this is kinda messy, but was the quickest way to see something working - manifest_data.remotes = remotes; - for (const remote of remotes) { - const chunk = bundle[`chunks/remote-${remote.hash}.js`]; - if (chunk.type !== 'chunk') continue; + const file = `${out}/server/chunks/remote-${remote.hash}.js`; + const code = fs.readFileSync(file, 'utf-8'); - const transformed = chunk.code.replace( - '$$_export_$$($$_self_$$)', - () => `export default $$_self_$$;` + fs.writeFileSync( + file, + code.replace('$$_export_$$($$_self_$$)', () => `export default $$_self_$$;`) ); - - fs.writeFileSync(`${out}/server/${chunk.fileName}`, transformed); } } @@ -1070,7 +1065,8 @@ async function kit({ svelte_config }) { build_data, prerendered: [], relative_path: '.', - routes: manifest_data.routes + routes: manifest_data.routes, + remotes })};\n` ); @@ -1084,7 +1080,8 @@ async function kit({ svelte_config }) { tracked_features, env: { ...env.private, ...env.public }, out, - output_config: svelte_config.output + output_config: svelte_config.output, + remotes }); remote_exports = metadata.remotes; @@ -1269,7 +1266,8 @@ async function kit({ svelte_config }) { build_data, prerendered: [], relative_path: '.', - routes: manifest_data.routes + routes: manifest_data.routes, + remotes })};\n` ); @@ -1303,7 +1301,8 @@ async function kit({ svelte_config }) { build_data, prerendered: prerendered.paths, relative_path: '.', - routes: manifest_data.routes.filter((route) => prerender_map.get(route.id) !== true) + routes: manifest_data.routes.filter((route) => prerender_map.get(route.id) !== true), + remotes })};\n` ); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 9e215926ce1a..4ecf67ff2280 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -192,14 +192,15 @@ export interface ManifestData { universal: string | null; }; nodes: PageNode[]; - remotes: Array<{ - file: string; - hash: string; - }>; routes: RouteData[]; matchers: Record; } +export interface RemoteChunk { + hash: string; + file: string; +} + export interface PageNode { depth: number; /** The `+page/layout.svelte`. */ From 3a1afe71b7b58afded656f0ad991c92a15b72f5b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 18:01:59 -0400 Subject: [PATCH 13/17] tidy up --- packages/kit/src/exports/vite/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 462a6a3685f5..6c253fe7e868 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -45,8 +45,6 @@ import { should_ignore } from './static_analysis/utils.js'; const cwd = process.cwd(); -Error.stackTraceLimit = Infinity; - /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { appType: true, From ad110b525eb960df3ae76ba03924bf84dc975165 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Aug 2025 18:08:15 -0400 Subject: [PATCH 14/17] fix adapter, make self-contained --- packages/kit/src/core/adapt/builder.js | 12 ++++++++---- packages/kit/src/core/adapt/index.js | 3 +++ packages/kit/src/exports/vite/index.js | 25 +++++++++++++------------ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 3d0de4472e73..01904d9501eb 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -1,7 +1,7 @@ /** @import { Builder } from '@sveltejs/kit' */ /** @import { ResolvedConfig } from 'vite' */ /** @import { RouteDefinition } from '@sveltejs/kit' */ -/** @import { RouteData, ValidatedConfig, BuildData, ServerMetadata, ServerMetadataRoute, Prerendered, PrerenderMap, Logger } from 'types' */ +/** @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'; @@ -32,6 +32,7 @@ const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.w * prerender_map: PrerenderMap; * log: Logger; * vite_config: ResolvedConfig; + * remotes: RemoteChunk[] * }} opts * @returns {Builder} */ @@ -43,7 +44,8 @@ export function create_builder({ prerendered, prerender_map, log, - vite_config + vite_config, + remotes }) { /** @type {Map} */ const lookup = new Map(); @@ -145,7 +147,8 @@ export function create_builder({ build_data, prerendered: [], relative_path: relativePath, - routes: Array.from(filtered) + routes: Array.from(filtered), + remotes }) }); } @@ -195,7 +198,8 @@ export function create_builder({ 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) + : route_data.filter((route) => prerender_map.get(route.id) !== true), + remotes }); }, diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 3cfe52753248..48d14116369e 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -8,6 +8,7 @@ import { create_builder } from './builder.js'; * @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( @@ -17,6 +18,7 @@ export async function adapt( prerendered, prerender_map, log, + remotes, vite_config ) { // This is only called when adapter is truthy, so the cast is safe @@ -32,6 +34,7 @@ export async function adapt( prerendered, prerender_map, log, + remotes, vite_config }); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 6c253fe7e868..38d0376e6d53 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -791,6 +791,18 @@ async function kit({ svelte_config }) { return { code: `import * as ${namespace} from '__sveltekit/remote';\n\n${exports.join('\n')}\n` }; + }, + + writeBundle() { + for (const remote of remotes) { + const file = `${out}/server/chunks/remote-${remote.hash}.js`; + const code = fs.readFileSync(file, 'utf-8'); + + fs.writeFileSync( + file, + code.replace('$$_export_$$($$_self_$$)', () => `export default $$_self_$$;`) + ); + } } }; @@ -1027,18 +1039,6 @@ async function kit({ svelte_config }) { async handler(_options, bundle) { if (secondary_build_started) return; // only run this once - if (kit.experimental.remoteFunctions) { - for (const remote of remotes) { - const file = `${out}/server/chunks/remote-${remote.hash}.js`; - const code = fs.readFileSync(file, 'utf-8'); - - fs.writeFileSync( - file, - code.replace('$$_export_$$($$_self_$$)', () => `export default $$_self_$$;`) - ); - } - } - const verbose = vite_config.logLevel === 'info'; const log = logger({ verbose }); @@ -1346,6 +1346,7 @@ async function kit({ svelte_config }) { prerendered, prerender_map, log, + remotes, vite_config ); } else { From 1a92bd9751c87c6db19a54cad9dcc013ee44a5ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Aug 2025 10:53:14 -0400 Subject: [PATCH 15/17] regenerate --- packages/kit/types/index.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3a7b23f3d882..80f4ae5c04be 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2114,10 +2114,6 @@ declare module '@sveltejs/kit' { universal: string | null; }; nodes: PageNode[]; - remotes: Array<{ - file: string; - hash: string; - }>; routes: RouteData[]; matchers: Record; } From 24dc8c27a96038b0d06ad41b990655da31b88882 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Aug 2025 11:46:36 -0400 Subject: [PATCH 16/17] fix --- packages/kit/src/core/postbuild/prerender.js | 2 +- packages/kit/src/exports/vite/index.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 0e0b2e9b653a..adb45960725a 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -495,7 +495,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { for (const loader of Object.values(manifest._.remotes)) { const module = await loader(); - for (const fn of Object.values(module)) { + for (const fn of Object.values(module.default)) { if (fn?.__?.type === 'prerender') { prerender_functions.push(fn.__); should_prerender = true; diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 38d0376e6d53..a07451909111 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -663,6 +663,11 @@ async function kit({ svelte_config }) { name: 'vite-plugin-sveltekit-remote', config(config) { + if (!config.build?.ssr) { + // only set manualChunks for the SSR build + return; + } + // Ensure build.rollupOptions.output exists config.build ??= {}; config.build.rollupOptions ??= {}; From 428a9b7376630d9b00f0a8b38859543aec7366cd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Aug 2025 12:10:13 -0400 Subject: [PATCH 17/17] changeset --- .changeset/clear-wolves-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clear-wolves-worry.md diff --git a/.changeset/clear-wolves-worry.md b/.changeset/clear-wolves-worry.md new file mode 100644 index 000000000000..7c10119d5094 --- /dev/null +++ b/.changeset/clear-wolves-worry.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: lazy discovery of remote functions