UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

261 lines (222 loc) • 8.1 kB
import fs from 'node:fs'; import { mkdirp } from '../../../utils/filesystem.js'; import { create_function_as_string, filter_fonts, find_deps, generate_placeholder, resolve_symlinks } from './utils.js'; import { s } from '../../../utils/misc.js'; import { normalizePath } from 'vite'; import { basename } from 'node:path'; import { fix_css_urls } from '../../../utils/css.js'; import { escape_for_interpolation } from '../../../utils/escape.js'; /** * @param {string} out * @param {import('types').ValidatedKitConfig} kit * @param {import('types').ManifestData} manifest_data * @param {import('vite').Manifest} server_manifest * @param {import('vite').Manifest | null} client_manifest * @param {string | null} assets_path * @param {import('vite').Rollup.RollupOutput['output'] | null} client_chunks * @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config */ export function build_server_nodes( out, kit, manifest_data, server_manifest, client_manifest, assets_path, client_chunks, output_config ) { mkdirp(`${out}/server/nodes`); mkdirp(`${out}/server/stylesheets`); /** * Stylesheet names and their contents which are below the inline threshold * @type {Map<string, string>} */ const stylesheets_to_inline = new Map(); /** * For CSS inlining, we either store a string or a function that returns the * styles with the correct relative URLs * @type {(css: string, eager_assets: Set<string>) => string} */ let prepare_css_for_inlining = (css) => s(css); if (client_chunks && kit.inlineStyleThreshold > 0 && output_config.bundleStrategy === 'split') { for (const chunk of client_chunks) { if (chunk.type !== 'asset' || !chunk.fileName.endsWith('.css')) { continue; } const source = chunk.source.toString(); if (source.length < kit.inlineStyleThreshold) { stylesheets_to_inline.set(chunk.fileName, source); } } // If the client CSS has URL references to assets, we need to adjust the // relative path so that they are correct when inlined into the document. // Although `paths.assets` is static, we need to pass in a fake path // `/_svelte_kit_assets` at runtime when running `vite preview` if (kit.paths.assets || kit.paths.relative) { const static_assets = new Set( manifest_data.assets.map((asset) => decodeURIComponent(asset.file)) ); const segments = /** @type {string} */ (assets_path).split('/'); const static_asset_prefix = segments.map(() => '..').join('/') + '/'; prepare_css_for_inlining = (css, eager_assets) => { const assets_placeholder = generate_placeholder(css, 'ASSETS'); const base_placeholder = generate_placeholder(css, 'BASE'); const transformed_css = fix_css_urls({ css, vite_assets: eager_assets, static_assets, paths_assets: assets_placeholder, base: base_placeholder, static_asset_prefix }); // only convert to a function if we have adjusted any URLs if (css !== transformed_css) { const escaped = escape_for_interpolation(transformed_css, [ { placeholder: assets_placeholder, replacement: '${assets}' }, { placeholder: base_placeholder, replacement: '${base}' } ]); return create_function_as_string('css', ['assets', 'base'], escaped); } return s(css); }; } } for (let i = 0; i < manifest_data.nodes.length; i++) { const node = manifest_data.nodes[i]; /** @type {string[]} */ const imports = []; // String representation of /** @type {import('types').SSRNode} */ /** @type {string[]} */ const exports = [`export const index = ${i};`]; /** @type {string[]} */ let imported = []; /** @type {string[]} */ let stylesheets = []; /** @type {string[]} */ let fonts = []; /** @type {Set<string>} */ const eager_assets = new Set(); if (node.component && client_manifest) { exports.push( 'let component_cache;', `export const component = async () => component_cache ??= (await import('../${ resolve_symlinks(server_manifest, node.component).chunk.file }')).default;` ); } if (node.universal) { if (!!node.page_options && node.page_options.ssr === false) { exports.push(`export const universal = ${s(node.page_options, null, 2)};`); } else { imports.push( `import * as universal from '../${resolve_symlinks(server_manifest, node.universal).chunk.file}';` ); // TODO: when building for analysis, explain why the file was loaded on the server if we fail to load it exports.push('export { universal };'); } exports.push(`export const universal_id = ${s(node.universal)};`); } if (node.server) { imports.push( `import * as server from '../${resolve_symlinks(server_manifest, node.server).chunk.file}';` ); exports.push('export { server };'); exports.push(`export const server_id = ${s(node.server)};`); } if ( client_manifest && (node.universal || node.component) && output_config.bundleStrategy === 'split' ) { const entry_path = `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`; const entry = find_deps(client_manifest, entry_path, true); // Eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC. // However, if it is not used during SSR (not present in the server manifest), // then it can be lazily loaded in the browser. /** @type {import('types').AssetDependencies | undefined} */ let component; if (node.component) { component = find_deps(server_manifest, node.component, true); } /** @type {import('types').AssetDependencies | undefined} */ let universal; if (node.universal) { universal = find_deps(server_manifest, node.universal, true); } /** @type {Set<string>} */ const eager_css = new Set(); entry.stylesheet_map.forEach((value, filepath) => { // pages and layouts are renamed to node indexes when optimised for the client // so we use the original filename instead to check against the server manifest if (filepath === entry_path) { filepath = node.component ?? filepath; } if (component?.stylesheet_map.has(filepath) || universal?.stylesheet_map.has(filepath)) { value.css.forEach((file) => eager_css.add(file)); value.assets.forEach((file) => eager_assets.add(file)); } }); imported = entry.imports; stylesheets = Array.from(eager_css); fonts = filter_fonts(Array.from(eager_assets)); } exports.push( `export const imports = ${s(imported)};`, `export const stylesheets = ${s(stylesheets)};`, `export const fonts = ${s(fonts)};` ); /** * Assets that have been processed by Vite (decoded and with the asset path stripped) * @type {Set<string>} */ let vite_assets = new Set(); // Keep track of Vite asset filenames so that we avoid touching unrelated ones // when adjusting the inlined CSS if (stylesheets_to_inline.size && assets_path && eager_assets.size) { vite_assets = new Set( Array.from(eager_assets).map((asset) => { return decodeURIComponent(asset.replace(`${assets_path}/`, '')); }) ); } if (stylesheets_to_inline.size) { /** @type {string[]} */ const inline_styles = []; stylesheets.forEach((file, i) => { if (stylesheets_to_inline.has(file)) { const filename = basename(file); const dest = `${out}/server/stylesheets/${filename}.js`; const css = /** @type {string} */ (stylesheets_to_inline.get(file)); fs.writeFileSync( dest, `// ${filename}\nexport default ${prepare_css_for_inlining(css, vite_assets)};` ); const name = `stylesheet_${i}`; imports.push(`import ${name} from '../stylesheets/${filename}.js';`); inline_styles.push(`\t${s(file)}: ${name}`); } }); if (inline_styles.length > 0) { exports.push(`export const inline_styles = () => ({\n${inline_styles.join(',\n')}\n});`); } } fs.writeFileSync( `${out}/server/nodes/${i}.js`, `${imports.join('\n')}\n\n${exports.join('\n')}\n` ); } }