Skip to content

Commit 478bb61

Browse files
committed
chore: server splitting (even more performance)
1 parent 3f864e0 commit 478bb61

File tree

5 files changed

+179
-180
lines changed

5 files changed

+179
-180
lines changed

src/generators/web/index.mjs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import { processJSXEntries } from './utils/processing.mjs';
77
import { safeWrite } from '../../utils/safeWrite.mjs';
88

99
/**
10-
* This generator transforms JSX AST (Abstract Syntax Tree) entries into a complete
11-
* web bundle, including server-side rendered HTML, client-side JavaScript, and CSS.
10+
* Web generator - transforms JSX AST entries into complete web bundles.
11+
*
12+
* This generator processes JSX AST entries and produces:
13+
* - Server-side rendered HTML pages
14+
* - Client-side JavaScript with code splitting
15+
* - Bundled CSS styles
1216
*
1317
* @type {GeneratorMetadata<Input, string>}
1418
*/
@@ -19,25 +23,28 @@ export default {
1923
dependsOn: 'jsx-ast',
2024

2125
/**
22-
* The main generation function for the 'web' generator.
23-
* It processes an array of JSX AST entries, converting each into a standalone HTML page
24-
* with embedded client-side JavaScript and linked CSS.
26+
* Main generation function that processes JSX AST entries into web bundles.
2527
*
26-
* @param {import('../jsx-ast/utils/buildContent.mjs').JSXContent[]} entries
27-
* @param {Partial<GeneratorOptions>} options
28+
* @param {import('../jsx-ast/utils/buildContent.mjs').JSXContent[]} entries - JSX AST entries to process.
29+
* @param {Partial<GeneratorOptions>} options - Generator options.
30+
* @param {string} [options.output] - Output directory for generated files.
31+
* @param {string} options.version - Documentation version string.
32+
* @returns {Promise<Array<{html: Buffer, css: string}>>} Generated HTML and CSS.
2833
*/
2934
async generate(entries, { output, version }) {
30-
// Load the HTML template
35+
// Load the HTML template with placeholders
3136
const template = await readFile(
3237
new URL('template.html', import.meta.url),
3338
'utf-8'
3439
);
3540

41+
// Create AST builders for server and client programs
3642
const astBuilders = createASTBuilder();
3743

44+
// Create require function for resolving external packages in server code
3845
const requireFn = createRequire(import.meta.url);
3946

40-
// Process all entries at once
47+
// Process all entries: convert JSX to HTML/CSS/JS
4148
const { results, css, jsChunks } = await processJSXEntries(
4249
entries,
4350
template,
@@ -46,22 +53,23 @@ export default {
4653
{ version }
4754
);
4855

49-
// Write all files if output directory is specified
56+
// Write files to disk if output directory is specified
5057
if (output) {
51-
// Write all HTML files
58+
// Write HTML files
5259
for (const { html, api } of results) {
5360
safeWrite(join(output, `${api}.html`), html, 'utf-8');
5461
}
5562

56-
// Write all JS chunks
63+
// Write code-split JavaScript chunks
5764
for (const chunk of jsChunks) {
5865
safeWrite(join(output, chunk.fileName), chunk.code, 'utf-8');
5966
}
6067

61-
// Write the CSS file
68+
// Write CSS bundle
6269
safeWrite(join(output, 'styles.css'), css, 'utf-8');
6370
}
6471

72+
// Return HTML and CSS for each entry
6573
return results.map(({ html }) => ({ html, css }));
6674
},
6775
};

src/generators/web/utils/bundle.mjs

Lines changed: 43 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,185 +8,145 @@ import staticData from './data.mjs';
88
* Asynchronously bundles JavaScript source code (and its CSS imports),
99
* targeting either browser (client) or server (Node.js) environments.
1010
*
11-
* @param {Map<string, string> | string} codeOrMap - Map of {fileName: code} or single code string for server builds.
12-
* @param {{ server: boolean }} options - Build configuration object.
11+
* @param {Map<string, string>} codeMap - Map of {fileName: code} for all builds.
12+
* @param {Object} [options] - Build configuration object.
13+
* @param {boolean} [options.server=false] - Whether this is a server-side build.
1314
*/
14-
export default async function bundleCode(codeOrMap, { server = false } = {}) {
15+
export default async function bundleCode(codeMap, { server = false } = {}) {
1516
// Store the import map HTML for later extraction
1617
let importMapHtml = '';
1718

18-
// Convert input to Map format
19-
const codeMap =
20-
codeOrMap instanceof Map
21-
? codeOrMap
22-
: new Map([['entrypoint.jsx', codeOrMap]]);
23-
2419
/** @type {import('rolldown').OutputOptions} */
2520
const serverOutputConfig = {
21+
// Inline all dynamic imports to create a single self-contained bundle
2622
inlineDynamicImports: true,
2723
};
2824

2925
/** @type {import('rolldown').InputOptions['experimental']} */
3026
const clientExperimentalConfig = {
27+
// Generate an import map for cache-busted module resolution in browsers
28+
// https://rolldown.rs/options/experimental#chunkimportmap
3129
chunkImportMap: !server && {
3230
baseUrl: './',
3331
fileName: 'importmap.json',
3432
},
3533
};
3634

3735
const result = await build({
38-
// Define the entry point module names — these are virtual (not real files).
39-
// The virtual plugin will provide the actual code string under these names.
36+
// Entry points: array of virtual module names that the virtual plugin provides
4037
input: Array.from(codeMap.keys()),
4138

42-
// Enable experimental chunk import map for cache-busted module resolution
43-
// https://rolldown.rs/options/experimental#chunkimportmap
44-
// Also enable incremental builds for faster rebuilds (similar to Rollup's cache)
45-
// https://rolldown.rs/options/experimental#incrementalbuild
39+
// Experimental features: import maps for client, none for server
4640
experimental: server ? {} : clientExperimentalConfig,
4741

48-
// Configuration for the output bundle
42+
// Output configuration
4943
output: {
50-
// Output module format:
51-
// - "cjs" for CommonJS (used in Node.js environments)
52-
// - "esm" for browser with dynamic imports (allows code splitting)
44+
// CommonJS for Node.js server, ESM for browser with code splitting support
5345
format: server ? 'cjs' : 'esm',
5446

55-
// Minify output only for browser builds to optimize file size.
56-
// Server builds are usually not minified to preserve stack traces and debuggability.
47+
// Minify only browser builds to reduce file size
5748
minify: !server,
5849

59-
// Enable code splitting for client builds to allow dynamic imports
60-
// For server builds, inline everything into a single bundle
50+
// Environment-specific output configuration
6151
...(server ? serverOutputConfig : {}),
6252
},
6353

64-
// Platform informs Rolldown of the environment-specific code behavior:
65-
// - 'node' enables things like `require`, and skips polyfills.
66-
// - 'browser' enables inlining of polyfills and uses native browser features.
54+
// Target platform affects polyfills, globals, and bundling behavior
6755
platform: server ? 'node' : 'browser',
6856

69-
// External dependencies to exclude from bundling.
70-
// These are expected to be available at runtime in the server environment.
71-
// This reduces bundle size and avoids bundling shared server libs.
57+
// External dependencies (not bundled) for server builds
58+
// These must be available in the Node.js runtime environment
7259
external: server
7360
? ['preact', 'preact-render-to-string', '@node-core/ui-components']
7461
: [],
7562

76-
// Transform configuration
63+
// Transform and define configuration
7764
transform: {
78-
// Inject global compile-time constants that will be replaced in code.
79-
// These are useful for tree-shaking and conditional branching.
80-
// Be sure to update type declarations (`types.d.ts`) if these change.
65+
// Compile-time constants replaced during bundling
66+
// Update types.d.ts if these change
8167
define: {
82-
// Static data injected directly into the bundle (as a literal or serialized JSON).
68+
// Static data as a JSON literal
8369
__STATIC_DATA__: staticData,
8470

85-
// Boolean flags used for conditional logic in source code:
86-
// Example: `if (SERVER) {...}` or `if (CLIENT) {...}`
87-
// These flags help split logic for server/client environments.
88-
// Unused branches will be removed via tree-shaking.
71+
// Environment flags for conditional logic and tree-shaking
8972
SERVER: String(server),
9073
CLIENT: String(!server),
9174
},
9275

93-
// JSX transformation configuration.
94-
// `'react-jsx'` enables the automatic JSX runtime, which doesn't require `import React`.
95-
// Since we're using Preact via aliasing, this setting works well with `preact/compat`.
76+
// Use automatic JSX runtime (no need to import React/Preact)
9677
jsx: 'react-jsx',
9778
},
9879

99-
// Module resolution aliases.
100-
// This tells the bundler to use `preact/compat` wherever `react` or `react-dom` is imported.
101-
// Allows you to write React-style code but ship much smaller Preact bundles.
80+
// Module resolution: alias React imports to Preact
10281
resolve: {
10382
alias: {
10483
react: 'preact/compat',
10584
'react-dom': 'preact/compat',
10685
},
10786
},
10887

109-
// Array of plugins to apply during the build.
88+
// Build plugins
11089
plugins: [
111-
// The virtual plugin lets us define virtual files
112-
// with the contents provided by the `codeMap` argument.
113-
// These become the root modules for the bundler.
90+
// Virtual plugin: provides in-memory modules from codeMap
11491
virtual(Object.fromEntries(codeMap)),
11592

116-
// Load CSS imports via the custom plugin.
117-
// This plugin will collect imported CSS files and return them as `source` chunks.
93+
// CSS loader: collects and bundles imported CSS files
11894
cssLoader(),
11995

120-
// Extract import map from Rolldown's chunkImportMap output
121-
// https://rolldown.rs/options/experimental#chunkimportmap
96+
// Extract and transform the import map generated by Rolldown
12297
{
12398
name: 'extract-import-map',
12499
/**
125-
* Extract import map from bundle
126-
* @param {*} _ - Options (unused)
127-
* @param {*} bundle - Bundle object
100+
* Extracts import map from bundle and converts to HTML script tag.
101+
*
102+
* @param {import('rolldown').NormalizedOutputOptions} _ - Output options (unused).
103+
* @param {import('rolldown').OutputBundle} bundle - Bundle object containing all output chunks.
128104
*/
129105
generateBundle(_, bundle) {
130106
const chunkImportMap = bundle['importmap.json'];
131107

132108
if (chunkImportMap?.type === 'asset') {
133-
// Parse the import map and filter out virtual entries
134-
const importMapData = JSON.parse(chunkImportMap.source);
135-
136-
// Remove any references to _virtual_ or virtual entrypoint files
137-
if (importMapData.imports) {
138-
for (const key in importMapData.imports) {
139-
if (key.includes('_virtual_') || key.includes('entrypoint')) {
140-
delete importMapData.imports[key];
141-
}
142-
}
143-
}
144-
145-
// Extract the import map and convert to HTML script tag
146-
importMapHtml = `<script type="importmap">${JSON.stringify(importMapData)}</script>`;
147-
148-
// Remove from bundle so it's not written as a separate file
109+
// Convert to HTML script tag for inline inclusion
110+
importMapHtml = `<script type="importmap">${chunkImportMap.source}</script>`;
111+
112+
// Remove from bundle to prevent writing as separate file
149113
delete bundle['importmap.json'];
150114
}
151115
},
152116
},
153117
],
154118

155-
// Enable tree-shaking to eliminate unused imports, functions, and branches.
156-
// This works best when all dependencies are marked as having no side effects.
157-
// `sideEffects: false` in the package.json confirms this is safe to do.
119+
// Enable tree-shaking to remove unused code
158120
treeshake: true,
159121

160-
// Disable writing output to disk.
161-
// Instead, the compiled chunks are returned in memory (ideal for dev tools or sandboxing).
122+
// Return chunks in memory instead of writing to disk
162123
write: false,
163124
});
164125

165-
// Destructure the result to get the output chunks.
166-
// Separate entry chunks from other chunks (CSS and code-split JS)
126+
// Separate entry chunks (main modules) from other chunks (CSS, code-split JS)
167127
const entryChunks = [];
168128
const otherChunks = [];
169129

170-
// Separate the entrypoints from remaining JavaScript files
171130
for (const chunk of result.output) {
172131
const chunkTarget =
173132
chunk.type === 'chunk' && chunk.isEntry ? entryChunks : otherChunks;
174133

175-
chunkTarget['push'](chunk);
134+
chunkTarget.push(chunk);
176135
}
177136

178-
// Separate CSS files from JS chunks
137+
// Separate CSS assets from JavaScript chunks
179138
const cssFiles = otherChunks.filter(chunk => chunk.type === 'asset');
180139
const jsChunks = otherChunks.filter(chunk => chunk.type === 'chunk');
181140

182-
// For client builds, create a map of entry code by original fileName
141+
// Create a map of entry code by original fileName
183142
const jsMap = {};
184143

185144
for (const chunk of entryChunks) {
186-
// Map back to original fileName from facadeModuleId
145+
// Extract original fileName from virtual module ID
187146
const originalFileName =
188147
chunk.facadeModuleId?.split('/').pop() || chunk.fileName;
189148

149+
// Remove virtual: prefix from module IDs
190150
jsMap[originalFileName.replace('\x00virtual:', '')] = chunk.code;
191151
}
192152

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Creates an enhanced require function that can resolve code-split chunks
3+
* from a virtual file system before falling back to Node.js require.
4+
*
5+
* @param {Array<{fileName: string, code: string}>} jsChunks - Array of code-split chunks from bundler.
6+
* @param {ReturnType<import('node:module').createRequire>} requireFn - Node.js require function for external packages.
7+
*/
8+
export function createEnhancedRequire(jsChunks, requireFn) {
9+
// Create a virtual file system from code-split chunks
10+
const chunkModules = Object.fromEntries(
11+
jsChunks.map(c => [`./${c.fileName}`, c.code])
12+
);
13+
14+
/**
15+
* Enhanced require function that resolves code-split chunks from virtual file system.
16+
*
17+
* @param {string} modulePath - Module path to require.
18+
* @returns {*} Module exports.
19+
*/
20+
const enhancedRequire = modulePath => {
21+
// Check virtual file system first for code-split chunks
22+
if (chunkModules[modulePath]) {
23+
const moduleExports = {};
24+
const module = { exports: moduleExports };
25+
26+
// Execute chunk code in isolated context with its own module.exports
27+
const chunkFn = new Function(
28+
'module',
29+
'exports',
30+
'require',
31+
chunkModules[modulePath]
32+
);
33+
34+
chunkFn(module, moduleExports, enhancedRequire);
35+
36+
return module.exports;
37+
}
38+
39+
// Fall back to Node.js require for external packages
40+
return requireFn(modulePath);
41+
};
42+
43+
return enhancedRequire;
44+
}

0 commit comments

Comments
 (0)