@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
1,170 lines (1,027 loc) • 38.6 kB
JavaScript
/** @import { TopLevelFilterExpression } from '@rolldown/pluginutils' */
/** @import { EnvVarConfig } from '@sveltejs/kit/env' */
/** @import { BuildData, ManifestData, Prerendered, ServerMetadata, RemoteChunk, ValidatedConfig } from 'types' */
/** @import { Manifest, Plugin, ResolvedConfig, Rolldown, UserConfig } from 'vite' */
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { styleText } from 'node:util';
import { code, include } from '@rolldown/pluginutils';
import { build_server_nodes } from './build_server.js';
import { treeshake_prerendered_remotes } from './remote.js';
import { find_deps, resolve_symlinks } from './utils.js';
import { warn_overridden_config } from '../utils.js';
import { adapt } from '../../../core/adapt/index.js';
import { generate_manifest } from '../../../core/generate_manifest/index.js';
import analyse from '../../../core/postbuild/analyse.js';
import prerender from '../../../core/postbuild/prerender.js';
import * as sync from '../../../core/sync/sync.js';
import create_manifest_data from '../../../core/sync/create_manifest_data/index.js';
import { get_manifest_routes } from '../../../core/sync/write_app_manifest.js';
import { write_client_manifest } from '../../../core/sync/write_client_manifest.js';
import { logger, runtime_directory } from '../../../core/utils.js';
import { compact } from '../../../utils/array.js';
import { copy, read, resolve_entry, walk } from '../../../utils/filesystem.js';
import { load_and_validate_params } from '../../../core/params.js';
import { posixify } from '../../../utils/os.js';
import { stackless } from '../../../utils/error.js';
import * as e from '../../../messages/build-errors.js';
import * as w from '../../../messages/build-warnings.js';
import { s } from '../../../utils/misc.js';
/**
* @typedef {object} Config
* @property {typeof import('vite')} vite
* @property {UserConfig} initial_config
* @property {string} root
* @property {string} global_name
* @property {string} kit_global
* @property {string | null} service_worker_entry_file
* @property {(relative_path: string) => boolean} sourcemapIgnoreList
*/
/**
* @param {ValidatedConfig} kit
* @param {() => Config} get_config
* @param {(metadata: ServerMetadata) => void} set_build_metadata
* @param {(data: ManifestData) => void} set_manifest_data
* @param {() => Record<string, EnvVarConfig<any>> | null} get_explicit_env_config
* @param {() => ({ remotes: RemoteChunk[]; remote_original_by_hash: Map<string, string> })} get_remote_metadata
* @param {(fn: () => Promise<void>) => void} set_finalise
* @returns {Plugin}
*/
export function plugin_compile(
kit,
get_config,
set_build_metadata,
set_manifest_data,
get_explicit_env_config,
get_remote_metadata,
set_finalise
) {
/** @type {typeof import('vite')} */
let vite;
/** @type {UserConfig} */
let initial_config;
/** @type {string} */
let root;
/** @type {string} */
let global_name;
/** @type {string | null} */
let service_worker_entry_file;
/** @type {string} */
let out_dir;
/** @type {string} */
let out;
/** @type {Record<string, string>} */
let env;
/** @type {ManifestData} */
let manifest_data;
/** @type {ResolvedConfig} */
let vite_config;
/** @type {Map<string, Rolldown.RolldownOutput['output']>} */
const watch_build_output = new Map();
/**
* A map showing which features (such as `$app/server:read`) are defined
* in which chunks, so that we can later determine which routes use which features
* @type {Record<string, string[]>}
*/
let tracked_features = {};
/** @type {Manifest} */
let vite_server_manifest;
/** @type {Manifest | null} */
let vite_client_manifest = null;
/** @type {Prerendered} */
let prerendered;
return {
name: 'vite-plugin-sveltekit-compile',
apply: 'build',
/**
* Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file.
* @see https://vitejs.dev/guide/api-plugin.html#config
*/
config: {
// avoids overwriting the base setting that's also set by Vitest
order: 'pre',
handler(config, config_env) {
let kit_global;
let sourcemapIgnoreList;
({
root,
global_name,
kit_global,
vite,
initial_config,
service_worker_entry_file,
sourcemapIgnoreList
} = get_config());
const app_immutable = `${kit.appDir}/immutable`;
out_dir = posixify(kit.outDir);
out = `${out_dir}/output`;
env = vite.loadEnv(config_env.mode, kit.env.dir, '');
/** @type {Record<string, string>} */
const server_input = {
index: `${runtime_directory}/server/index.js`,
env: '<sveltekit:generated>/env/config.js',
['remote-entry']: `${runtime_directory}/app/server/remote/index.js`
};
manifest_data = create_manifest_data(kit, root);
set_manifest_data(manifest_data);
sync.all(kit, root, manifest_data);
// add entry points for every endpoint...
manifest_data.routes.forEach((route) => {
if (route.endpoint) {
const resolved = path.resolve(root, route.endpoint.file);
const relative = decodeURIComponent(path.relative(kit.files.routes, resolved));
const name = posixify(path.join('entries/endpoints', relative.replace(/\.js$/, '')));
server_input[name] = resolved;
}
});
// ...and every component used by pages...
manifest_data.nodes.forEach((node) => {
for (const file of [node.component, node.universal, node.server]) {
if (file) {
const resolved = path.resolve(root, file);
const relative = decodeURIComponent(path.relative(kit.files.routes, resolved));
const name = relative.startsWith('..')
? posixify(path.join('entries/fallbacks', path.basename(file)))
: posixify(path.join('entries/pages', relative.replace(/\.js$/, '')));
server_input[name] = resolved;
}
}
});
// ...and the params file
if (manifest_data.params) {
server_input['entries/params'] = path.resolve(root, manifest_data.params);
}
// ...and the hooks files
if (manifest_data.hooks.server) {
server_input['entries/hooks.server'] = path.resolve(root, manifest_data.hooks.server);
}
if (manifest_data.hooks.universal) {
server_input['entries/hooks.universal'] = path.resolve(
root,
manifest_data.hooks.universal
);
}
// ...and the server instrumentation file
const server_instrumentation = resolve_entry(
path.join(kit.files.src, 'instrumentation.server'),
kit.moduleExtensions
);
if (server_instrumentation) {
if (kit.adapter && !kit.adapter.supports?.instrumentation?.()) {
e.adapter_instrumentation_unsupported({
file: server_instrumentation,
adapter: kit.adapter.name
});
}
server_input['instrumentation.server'] = server_instrumentation;
}
/** @type {Record<string, string>} */
const client_input = {};
if (kit.output.bundleStrategy !== 'split') {
client_input['bundle'] = `${runtime_directory}/client/bundle.js`;
} else {
client_input['entry/start'] = `${runtime_directory}/client/entry.js`;
client_input['entry/payload'] = `${runtime_directory}/client/payload.js`;
client_input['entry/app'] = `${out_dir}/generated/build/client-optimized/app.js`;
manifest_data.nodes.forEach((node, i) => {
if (node.component || node.universal) {
client_input[`nodes/${i}`] =
`${out_dir}/generated/build/client-optimized/nodes/${i}.js`;
}
});
}
const inline = kit.output.bundleStrategy === 'inline';
/** @type {string} */
const base = (kit.paths.assets || kit.paths.base) + '/';
const root_to_assets = app_immutable + '/assets/';
const assets_to_root =
app_immutable
.split('/')
.map(() => '..')
.join('/') + '/../';
const relative = kit.paths.relative !== false || !!kit.paths.assets;
/** @satisfies {UserConfig} */
const new_config = {
// Affects how Vite loads JS assets on the client.
// If the initial HTML we render uses an absolute path for assets,
// the additional chunks Vite loads must also use an absolute path.
// Otherwise, you end up with additional chunks being loaded relative
// to the current chunk rather than the root.
base: relative ? './' : base,
build: {
cssCodeSplit: !inline,
cssMinify: initial_config.build?.minify == null ? true : !!initial_config.build.minify,
manifest: true,
rolldownOptions: {
output: {
name: `${global_name}.app`,
assetFileNames: `${app_immutable}/assets/[name].[hash][extname]`,
sourcemapIgnoreList
},
preserveEntrySignatures: 'strict',
onwarn(warning, handler) {
if (
warning.code === 'IMPORT_IS_UNDEFINED' &&
warning.id === `${out_dir}/generated/build/client-optimized/app.js`
) {
// ignore e.g. undefined `handleError` hook when
// referencing `client_hooks.handleError`
return;
}
handler(warning);
},
watch: {
exclude: [
// Ignore all siblings of config.outDir/generated
`${out_dir}/generated/**`
]
}
},
emptyOutDir: false,
ssrEmitAssets: true
},
worker: {
rolldownOptions: {
output: {
entryFileNames: `${app_immutable}/workers/[name]-[hash].js`,
chunkFileNames: `${app_immutable}/workers/chunks/[hash].js`,
assetFileNames: `${app_immutable}/workers/assets/[name]-[hash][extname]`
}
}
},
builder: {
sharedConfigBuild: true,
sharedPlugins: true
},
environments: {
ssr: {
build: {
copyPublicDir: false,
outDir: `${out}/server`,
target: 'node22',
rolldownOptions: {
input: server_input,
output: {
entryFileNames: '[name].js',
chunkFileNames: 'chunks/[name].js'
}
}
},
// these are stubs that will be replaced after the initial server build
define: {
__SVELTEKIT_HAS_SERVER_LOAD__: 'true',
__SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true',
__SVELTEKIT_PAYLOAD__: '{}'
}
},
client: {
build: {
outDir: `${out}/client`,
rolldownOptions: {
input: inline ? client_input['bundle'] : client_input,
output: {
format: inline ? 'iife' : 'esm',
entryFileNames: `${app_immutable}/[name].[hash].js`,
chunkFileNames: (/** @type {Rolldown.PreRenderedChunk} */ chunk_info) => {
// The manifest data chunk gets a fixed (non-hashed) filename so
// that importers' content hashes are stable regardless of the
// manifest content — this breaks the content-hash feedback loop
if (chunk_info.name === 'sveltekit-manifest') {
return `${kit.appDir}/manifest.js`;
}
return `${app_immutable}/chunks/[hash].js`;
},
codeSplitting:
kit.output.bundleStrategy === 'split'
? {
groups: [
{
name: 'sveltekit-manifest',
test: '<sveltekit:generated>/app-manifest.js'
}
]
}
: false
},
// This silences Rolldown warnings about not supporting `import.meta`
// for the `iife` output format. We don't care because it's
// only used in development and will be treeshaken away
transform: inline
? {
define: {
'import.meta': '{}'
}
}
: undefined
}
},
define: {
__SVELTEKIT_PAYLOAD__:
kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined'
}
}
},
experimental: {
// Allows us to use relative paths in as many places as we can
renderBuiltUrl(filename, { ssr, hostType }) {
if (hostType === 'js') {
// SSR builds should use an absolute path in JS modules to
// match the default Vite behaviour
if (ssr) return base + filename;
// We could always use a relative asset base path here, but it's better for performance not to.
// E.g. Vite generates `new URL('/asset.png', import.meta).href` for a relative path vs just '/asset.png'.
// That's larger and takes longer to run and also causes an HTML diff between SSR and client
// causing us to do a more expensive hydration check.
return { relative };
}
if (!relative) return;
// ensure assets loaded by CSS files are loaded relative to the
// CSS file rather than the default of relative to the root
// _app/immutable/assets files
if (filename.startsWith(root_to_assets)) {
return `./${filename.slice(root_to_assets.length)}`;
}
// static dir files
return assets_to_root + filename;
}
}
};
warn_overridden_config(config, new_config);
return new_config;
}
},
configResolved(resolved_config) {
vite_config = resolved_config;
},
applyToEnvironment(environment) {
return environment.name !== 'serviceWorker';
},
renderChunk: {
// composable filters are not accepted type-wise but still work during build
// see https://github.com/vitejs/rolldown-vite/issues/605
filter: /** @type {any} */ (
/** @satisfies {TopLevelFilterExpression[]} */ ([include(code('__SVELTEKIT_TRACK__'))])
),
handler(code, chunk) {
// composable filters only work during build so we still need this guard for dev
// see https://github.com/vitejs/rolldown-vite/issues/605
if (code.includes('__SVELTEKIT_TRACK__')) {
return {
// Rolldown changes our single quotes to double quotes so we need it in the regex too
code: code.replace(/__SVELTEKIT_TRACK__\(['"](.+?)['"]\)/g, (_, label) => {
(tracked_features[chunk.name + '.js'] ??= []).push(label);
// put extra whitespace at the end of the comment to preserve the source size and avoid interfering with source maps
return `/* track ${label} */`;
}),
map: null // TODO we may need to generate a sourcemap in future
};
}
}
},
generateBundle(_options, bundle) {
// a watched build returns a watcher rather than the build output from
// `builder.build` so we need to retrieve it from the generateBundle hook
if (this.meta.watchMode) {
watch_build_output.set(
this.environment.name,
/** @type {Rolldown.RolldownOutput['output']} */ (Object.values(bundle))
);
}
if (this.environment.config.consumer !== 'client') return;
this.emitFile({
type: 'asset',
fileName: `${kit.appDir}/version.json`,
source: s({ version: kit.version.name })
});
},
async buildApp(builder) {
fs.rmSync(out, { force: true, recursive: true });
fs.mkdirSync(out, { recursive: true });
const verbose = builder.config.logLevel === 'info' || builder.config.logLevel === undefined;
const log = logger({ verbose });
/** @type {(() => Promise<void>) | null} */
let finalise;
let ssr_build = await builder.build(builder.environments.ssr);
/** @param {Rolldown.RolldownOutput['output']} server_chunks */
const process_ssr_build = async (server_chunks) => {
// Replace manifest placeholders in SSR output. `assets` and `routes`
// are known from `manifest_data`. `immutable` and `prerendered` are not
// known yet — they get sentinel strings that are replaced after
// the client build and after prerendering respectively.
replace_manifest_placeholder_variables(server_chunks, `${out}/server`, {
assets: manifest_data.assets.map((asset) => ({ path: asset.file })),
routes: get_manifest_routes(manifest_data.routes)
});
vite_server_manifest = /** @type {Manifest} */ (
JSON.parse(read(`${out}/server/.vite/manifest.json`))
);
const manifest_path = `${out}/server/manifest-full.js`;
const assets_path = `${kit.appDir}/immutable/assets`;
const workers_path = `${kit.appDir}/immutable/workers`;
/** @type {BuildData} */
const build_data = {
app_dir: kit.appDir,
app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`,
manifest_data,
out_dir: out,
service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable?
client: null,
server_manifest: vite_server_manifest
};
const { remotes, remote_original_by_hash } = get_remote_metadata();
fs.writeFileSync(
manifest_path,
`export const manifest = ${generate_manifest({
build_data,
prerendered: [],
relative_path: '.',
routes: manifest_data.routes,
remotes,
root
})};\n`
);
// first, build server nodes without the client manifest so we can analyse it
build_server_nodes(
out,
kit,
manifest_data,
vite_server_manifest,
null,
assets_path,
server_chunks,
root
);
log.info('Analysing routes');
const { metadata } = await analyse({
hash: kit.router.type === 'hash',
manifest_path,
manifest_data,
server_manifest: vite_server_manifest,
tracked_features,
env,
remotes,
vite_config_file: vite_config.configFile
});
set_build_metadata(metadata);
log.info('Building app');
/** @type {Record<string, EnvVarConfig<any>> | null} */
const explicit_env_config = get_explicit_env_config();
/** @type {Array<{ path: string }> | null} */
let immutable = null;
const server_assets = `${out}/server/${assets_path}`;
const client_assets = `${out}/client/${assets_path}`;
const skip_client_build = manifest_data.nodes.every(
(node) => node.page_options?.csr === false
);
if (
!skip_client_build &&
kit.csp.directives['require-trusted-types-for']?.includes('script') &&
!kit.csp.directives['trusted-types']?.includes('svelte-trusted-html')
) {
e.config_csp_trusted_html_missing();
}
if (skip_client_build) {
copy(server_assets, client_assets);
copy(`${out}/server/${workers_path}`, `${out}/client/${workers_path}`);
copy(kit.files.assets, `${out}/client`);
} else {
// ...and build the client
write_client_manifest(
kit,
manifest_data,
`${out_dir}/generated/build/client-optimized`,
root,
metadata.nodes
);
// Through the finished analysis we can now check if any node has server or universal load functions
const nodes = Object.values(metadata.nodes);
const has_server_load = nodes.some((node) => node.has_server_load);
const has_universal_load = nodes.some((node) => node.has_universal_load);
if (builder.environments.client.config.define) {
builder.environments.client.config.define.__SVELTEKIT_HAS_SERVER_LOAD__ =
s(has_server_load);
builder.environments.client.config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ =
s(has_universal_load);
}
const client_build = await builder.build(builder.environments.client);
const client_chunks = await normalise_build(
builder.environments.client.name,
client_build,
watch_build_output
);
// We use `build.ssrEmitAssets` so that asset URLs created from
// imports in server-only modules correspond to files in the build,
// but we don't want to copy over CSS imports as these are already
// accounted for in the client bundle. In most cases it would be
// a no-op, but for SSR builds `url(...)` paths are handled
// differently (relative for client, absolute for server)
// resulting in different hashes, and thus duplication
const ssr_stylesheets = new Set(
Object.values(vite_server_manifest)
.map((chunk) => chunk.css ?? [])
.flat()
);
if (fs.existsSync(server_assets)) {
for (const file of fs.readdirSync(server_assets)) {
const src = `${server_assets}/${file}`;
const dest = `${client_assets}/${file}`;
if (fs.existsSync(dest) || ssr_stylesheets.has(`${assets_path}/${file}`)) {
continue;
}
copy(src, dest);
}
}
// worker files emitted by the server build (e.g. `?worker&url` imports
// in server-only modules) must be served to the client, too
copy(`${out}/server/${workers_path}`, `${out}/client/${workers_path}`);
vite_client_manifest = /** @type {Manifest} */ (
JSON.parse(read(`${out}/client/.vite/manifest.json`))
);
/**
* @param {string} entry
* @param {boolean} [add_dynamic_css]
*/
const deps_of = (entry, add_dynamic_css = false) =>
find_deps(
/** @type {Manifest} */ (vite_client_manifest),
posixify(path.relative(root, entry)),
add_dynamic_css,
root
);
// the inline bundle and stylesheet are deleted further down, after
// being inlined into the page, so they must not appear in `immutable`
/** @type {Set<string>} */
const inlined = new Set();
/** @type {Rolldown.OutputAsset | undefined} */
let inline_style;
if (kit.output.bundleStrategy === 'inline') {
inline_style = /** @type {Rolldown.OutputAsset | undefined} */ (
client_chunks.find(
(chunk) =>
chunk.type === 'asset' &&
chunk.names.length === 1 &&
chunk.names[0] === 'style.css'
)
);
inlined.add(deps_of(`${runtime_directory}/client/bundle.js`).file);
if (inline_style) inlined.add(inline_style.fileName);
}
// Replace manifest placeholders in client output. `immutable` is
// computed from the Vite client manifest, `assets` and `routes`
// from `manifest_data`. `prerendered` is left as a placeholder
// for now — it's replaced after prerendering completes.
immutable = collect_immutable(vite_client_manifest, kit.appDir, inlined);
replace_manifest_placeholder_variables(client_chunks, `${out}/client`, {
immutable,
assets: manifest_data.assets.map((asset) => ({ path: asset.file })),
routes: get_manifest_routes(manifest_data.routes)
});
// Now that the client build is done, replace the `build` sentinel
// in the SSR output with the real build files
replace_manifest_placeholder_strings(`${out}/server`, { immutable });
const has_explicit_dynamic_public_env = Object.values(explicit_env_config ?? {}).some(
(variable) => variable.public && !variable.static
);
// the app only depends on runtime public env if it imports `$app/env/public`
// *and* at least one public env var is actually dynamic (non-static)
const uses_env_dynamic_public =
has_explicit_dynamic_public_env &&
client_chunks.some(
(chunk) =>
chunk.type === 'chunk' &&
chunk.modules[
posixify(fs.realpathSync(`${out_dir}/generated/build/env/public/client.js`))
]
);
if (kit.output.bundleStrategy === 'split') {
const start_entry = posixify(
path.relative(root, `${runtime_directory}/client/entry.js`)
);
const start = find_deps(vite_client_manifest, start_entry, false, root);
const runtime_entry = resolve_symlinks(vite_client_manifest, start_entry, root).chunk
.dynamicImports?.[0]; // client/entry.js dynamically imports client/client-entry.js
if (!runtime_entry) throw new Error('Could not find the client runtime chunk');
const runtime = find_deps(vite_client_manifest, runtime_entry, false, root);
const app = deps_of(`${out_dir}/generated/build/client-optimized/app.js`);
build_data.client = {
start: start.file,
app: app.file,
imports: Array.from(
new Set([
...start.imports,
runtime.file,
...runtime.imports,
app.file,
...app.imports
])
),
stylesheets: [...start.stylesheets, ...runtime.stylesheets, ...app.stylesheets],
fonts: [...start.fonts, ...runtime.fonts, ...app.fonts],
uses_env_dynamic_public
};
// In case of server-side route resolution, we create a purpose-built route manifest that is
// similar to that on the client, with as much information computed upfront so that we
// don't need to include any code of the actual routes in the server bundle.
if (kit.router.resolution === 'server') {
const nodes = manifest_data.nodes.map((node, i) => {
if (node.component || node.universal) {
const entry = `${out_dir}/generated/build/client-optimized/nodes/${i}.js`;
const deps = deps_of(entry, true);
const file = resolve_symlinks(
/** @type {Manifest} */ (vite_client_manifest),
`${out_dir}/generated/build/client-optimized/nodes/${i}.js`,
root
).chunk.file;
return { file, css: deps.stylesheets };
}
});
build_data.client.nodes = nodes.map((node) => node?.file);
build_data.client.css = nodes.map((node) => node?.css);
build_data.client.routes = compact(
manifest_data.routes.map((route) => {
if (!route.page) return;
return {
id: route.id,
pattern: route.pattern,
params: route.params,
layouts: route.page.layouts.map((l) =>
l !== undefined ? [metadata.nodes[l].has_server_load, l] : undefined
),
errors: route.page.errors,
leaf: [metadata.nodes[route.page.leaf].has_server_load, route.page.leaf]
};
})
);
}
} else {
const start = deps_of(`${runtime_directory}/client/bundle.js`);
build_data.client = {
start: start.file,
imports: start.imports,
stylesheets: start.stylesheets,
fonts: start.fonts,
uses_env_dynamic_public
};
if (kit.output.bundleStrategy === 'inline') {
build_data.client.inline = {
script: read(`${out}/client/${start.file}`),
style: /** @type {string | undefined} */ (inline_style?.source)
};
// the bundle and stylesheet are inlined into the page, so the
// emitted files are never loaded
fs.unlinkSync(`${out}/client/${start.file}`);
fs.rmSync(`${out}/client/${start.file}.map`, { force: true });
if (inline_style) fs.unlinkSync(`${out}/client/${inline_style.fileName}`);
}
}
// regenerate manifest now that we have client entry...
fs.writeFileSync(
manifest_path,
`export const manifest = ${generate_manifest({
build_data,
prerendered: [],
relative_path: '.',
routes: manifest_data.routes,
remotes,
root
})};\n`
);
// regenerate nodes with the client manifest...
build_server_nodes(
out,
kit,
manifest_data,
vite_server_manifest,
vite_client_manifest,
assets_path,
client_chunks,
root
);
}
// ...and prerender
let prerender_results;
try {
prerender_results = await prerender({
hash: kit.router.type === 'hash',
out,
manifest_path,
metadata,
verbose,
env,
vite_config_file: vite_config.configFile,
is_tty: process.stdout.isTTY
});
// this silly hack is necessary to ensure that stderr from prerender is flushed before we continue
await new Promise((f) => setTimeout(f, 0));
} catch (e) {
if (e instanceof Error && e.message === '__handled__') {
// error details are already logged inside `prerender`, don't duplicate them
throw stackless('Prerendering failed');
} else {
// Unforeseen error, rethrow as-is
throw e;
}
}
prerendered = prerender_results.prerendered;
// Replace the `prerendered` sentinel in both SSR and client output
// with the real prerendered paths. The other sentinels (`build`)
// were already replaced after the client build.
const prerendered_paths = prerendered.paths.map((p) => {
return { path: p.replace(kit.paths.base, '').slice(1) };
});
replace_manifest_placeholder_strings(`${out}/server`, { prerendered: prerendered_paths });
replace_manifest_placeholder_strings(`${out}/client`, { prerendered: prerendered_paths });
// For `inline` strategy, the entry file was deleted and read into
// `build_data.client.inline.script` — replace the sentinel there too
if (build_data.client?.inline?.script) {
build_data.client.inline.script = build_data.client.inline.script.replaceAll(
'"__sveltekit_manifest_prerendered__"',
JSON.stringify(prerendered_paths)
);
}
// generate a new manifest that doesn't include prerendered pages
fs.writeFileSync(
`${out}/server/manifest.js`,
`export const manifest = ${generate_manifest({
build_data,
prerendered: prerendered.paths,
relative_path: '.',
routes: manifest_data.routes.filter(
(route) => prerender_results.prerender_map.get(route.id) !== true
),
remotes,
root
})};\n`
);
await treeshake_prerendered_remotes(
vite,
out,
remotes,
remote_original_by_hash,
metadata,
process.cwd(),
server_chunks,
vite_config.build.sourcemap
);
// defer until after other buildApp hooks have run
finalise = async () => {
/** @type {typeof import('$app/manifest')} */
const app_manifest = {
assets: manifest_data.assets.map((asset) => ({ path: asset.file })),
immutable: immutable ?? [],
prerendered: prerendered_paths,
routes: get_manifest_routes(manifest_data.routes)
};
// defer creating the service worker to avoid other plugins from
// overwriting it if they run a client environment build
if (service_worker_entry_file) {
log.info('Building service worker');
// Add defines for `$app/manifest`
builder.environments.serviceWorker.config.define = {
...builder.environments.serviceWorker.config.define,
__SVELTEKIT_MANIFEST_ASSETS__: s(app_manifest.assets),
__SVELTEKIT_MANIFEST_IMMUTABLE__: s(app_manifest.immutable),
__SVELTEKIT_MANIFEST_PRERENDERED__: s(app_manifest.prerendered),
__SVELTEKIT_MANIFEST_ROUTES__: s(app_manifest.routes)
};
// we have to overwrite this because it can't be configured per environment in the config hook
builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => {
return {
runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname`
};
};
const service_worker_build = await builder.build(builder.environments.serviceWorker);
await normalise_build(
builder.environments.serviceWorker.name,
service_worker_build,
watch_build_output
);
}
console.log(
`\nRun ${styleText(['bold', 'cyan'], 'npm run preview')} to preview your production build locally.`
);
if (kit.adapter) {
await adapt(
kit,
build_data,
metadata,
prerendered,
prerender_results.prerender_map,
app_manifest,
log,
remotes,
vite_config,
explicit_env_config
);
} else {
w.adapter_missing();
}
};
set_finalise(finalise);
};
// `vite build`
ssr_build = Array.isArray(ssr_build) ? ssr_build[0] : ssr_build;
if ('output' in ssr_build) {
await load_and_validate_params({
routes: manifest_data.routes,
params_path: manifest_data.params,
root
});
return await process_ssr_build(ssr_build.output);
}
// `vite build --watch`
let rebuild = false;
const before_ssr_build_rerun = async () => {
rebuild = true;
// these are set once per plugin initialisation or when the config hook
// runs. However, those don't re-run during watch mode. So, we need to
// re-initialise them manually here
manifest_data = create_manifest_data(kit, root);
set_manifest_data(manifest_data);
sync.all(kit, root, manifest_data);
tracked_features = {};
finalise = null;
fs.mkdirSync(out, { recursive: true });
await load_and_validate_params({
routes: manifest_data.routes,
params_path: manifest_data.params,
root
});
};
ssr_build.on('change', before_ssr_build_rerun);
ssr_build.on('restart', before_ssr_build_rerun);
/** @type {PromiseWithResolvers<void>} */
const task = Promise.withResolvers();
ssr_build.on('event', async (event) => {
if (event.code === 'ERROR') {
return task.reject();
}
if (event.code === 'BUNDLE_END') {
try {
await process_ssr_build(
/** @type {Rolldown.RolldownOutput['output']} */ (
watch_build_output.get(builder.environments.ssr.name)
)
);
// buildApp hooks don't rerun in watch mode so we need to run
// the deferred steps here on subsequent builds
if (rebuild) await finalise?.();
} catch (e) {
return task.reject(e);
} finally {
await event.result.close();
}
return task.resolve();
}
});
await task.promise;
}
};
}
/**
* @param {() => Promise<void>} finalise
* @returns {Plugin}
*/
export function plugin_adapter(finalise) {
return {
name: 'vite-plugin-sveltekit-adapter',
apply: 'build',
buildApp: {
// this will run after any buildApp hooks provided by other Vite plugins
// see https://vite.dev/guide/api-environment-frameworks#environments-during-build
order: 'post',
async handler() {
await finalise();
}
}
};
}
/**
* Collects the content-hashed files of a Vite manifest, in the shape of
* `$app/manifest`'s `immutable` export.
*
* @param {Manifest} manifest
* @param {string} app_dir
* @param {Set<string>} inlined files deleted from the output after being inlined into the page
* @returns {Array<{ path: string }>}
*/
const collect_immutable = (manifest, app_dir, inlined) => {
const prefix = `${app_dir}/immutable`;
/** @type {Set<string>} */
const files = new Set();
/** @param {string} file */
const add = (file) => {
if (file.startsWith(prefix) && !inlined.has(file)) files.add(file);
};
for (const key in manifest) {
const { file, css, assets } = manifest[key];
add(file);
if (css) for (let i = 0; i < css.length; i++) add(css[i]);
if (assets) for (let i = 0; i < assets.length; i++) add(assets[i]);
}
return Array.from(files, (path) => ({ path }));
};
/**
* Replaces manifest data placeholder identifiers in output chunks with real
* values, or strings that are valid JS (so thecode doesn't crash during prerendering)
* but findable on disk for later replacement by `replace_manifest_placeholder_strings`.
*
* @param {Rolldown.RolldownOutput['output']} chunks
* @param {string} output_dir
* @param {{
* immutable?: Array<{ path: string }>;
* assets?: Array<{ path: string }>;
* prerendered?: Array<{ path: string }>;
* routes?: Array<{ id: string; page: boolean; endpoint: boolean }>;
* }} values
*/
const replace_manifest_placeholder_variables = (chunks, output_dir, values) => {
/** @type {Record<string, string>} */
const replacements = {
__SVELTEKIT_MANIFEST_IMMUTABLE__: JSON.stringify(
values.immutable ?? '__sveltekit_manifest_build__'
),
__SVELTEKIT_MANIFEST_ASSETS__: JSON.stringify(values.assets ?? '__sveltekit_manifest_files__'),
__SVELTEKIT_MANIFEST_PRERENDERED__: JSON.stringify(
values.prerendered ?? '__sveltekit_manifest_prerendered__'
),
__SVELTEKIT_MANIFEST_ROUTES__: JSON.stringify(values.routes ?? '__sveltekit_manifest_routes__')
};
for (const chunk of chunks) {
if (chunk.type !== 'chunk') continue;
if (!chunk.code.includes('__SVELTEKIT_MANIFEST_')) continue;
let code = chunk.code;
for (const [identifier, replacement] of Object.entries(replacements)) {
code = code.replaceAll(identifier, replacement);
}
const file_path = `${output_dir}/${chunk.fileName}`;
fs.writeFileSync(file_path, code);
}
};
/**
* Replaces manifest sentinel strings in files on disk with real values.
* This is used for values that weren't known when the first replacement
* pass ran (e.g. `build` wasn't known until after the client build,
* `prerendered` wasn't known until after prerendering).
*
* @param {string} dir Directory to scan for .js files
* @param {{
* immutable?: Array<{ path: string }>;
* prerendered?: Array<{ path: string }>;
* }} values
*/
const replace_manifest_placeholder_strings = (dir, values) => {
/** @type {Array<[string, string]>} */
const replacements = [];
if (values.immutable !== undefined) {
replacements.push(['__sveltekit_manifest_build__', JSON.stringify(values.immutable)]);
}
if (values.prerendered !== undefined) {
replacements.push(['__sveltekit_manifest_prerendered__', JSON.stringify(values.prerendered)]);
}
for (const file of walk(dir)) {
if (!file.endsWith('.js')) continue;
const file_path = `${dir}/${file}`;
let code = read(file_path);
let changed = false;
for (const [sentinel, replacement] of replacements) {
if (code.includes(sentinel)) {
code = code.replaceAll(`"${sentinel}"`, replacement);
changed = true;
}
}
if (changed) {
fs.writeFileSync(file_path, code);
}
}
};
/**
* Normalises the build output to a consistent format, handling watch mode and multiple environments
* @param {string} name The name of the environment
* @param {Rolldown.RolldownOutput | Rolldown.RolldownOutput[] | Rolldown.RolldownWatcher} build The return value of builder.build
* @param {Map<string, Rolldown.RolldownOutput['output']>} build_output_map
* @returns {Promise<Rolldown.RolldownOutput['output']>}
*/
async function normalise_build(name, build, build_output_map) {
if ('output' in build) {
return build.output;
}
if (Array.isArray(build)) {
return build[0].output;
}
/** @type {PromiseWithResolvers<void>} */
const bundling = Promise.withResolvers();
build.on('event', async (event) => {
if (event.code === 'ERROR') {
await build.close();
return bundling.reject(event.error);
}
if (event.code === 'BUNDLE_END') {
await build.close();
return bundling.resolve();
}
});
await bundling.promise;
return /** @type {Rolldown.RolldownOutput['output']} */ (build_output_map.get(name));
}