-
Notifications
You must be signed in to change notification settings - Fork 46
Expand file tree
/
Copy pathprocessing.mjs
More file actions
135 lines (113 loc) · 5.05 KB
/
processing.mjs
File metadata and controls
135 lines (113 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { randomUUID } from 'node:crypto';
import { createRequire } from 'node:module';
import { jsx, toJs } from 'estree-util-to-js';
import { transform } from 'lightningcss-wasm';
import bundleCode from './bundle.mjs';
import { createChunkedRequire } from './chunks.mjs';
import createConfigSource from './config.mjs';
import createASTBuilder from './generate.mjs';
import getConfig from '../../../utils/configuration/index.mjs';
import { populate } from '../../../utils/configuration/templates.mjs';
import { minifyHTML } from '../../../utils/html-minifier.mjs';
import { relative } from '../../../utils/url.mjs';
import { SPECULATION_RULES } from '../constants.mjs';
/**
* Converts JSX AST entries to server and client JavaScript code.
*
* @param {Array<import('../../jsx-ast/utils/buildContent.mjs').JSXContent>} entries - JSX AST entries
* @param {function} buildServerProgram - Wraps code for server execution
* @param {function} buildClientProgram - Wraps code for client hydration
* @returns {{serverCodeMap: Map<string, string>, clientCodeMap: Map<string, string>}}
*/
function convertJSXToCode(entries, { buildServerProgram, buildClientProgram }) {
const serverCodeMap = new Map();
const clientCodeMap = new Map();
for (const entry of entries) {
const fileName = `${entry.data.api}.jsx`;
// Convert AST to JavaScript string with JSX syntax
const { value: code } = toJs(entry, { handlers: jsx });
// Prepare code for server-side execution (wrapped for SSR)
serverCodeMap.set(fileName, buildServerProgram(code));
// Prepare code for client-side execution (wrapped for hydration)
clientCodeMap.set(fileName, buildClientProgram(code));
}
return { serverCodeMap, clientCodeMap };
}
/**
* Bundles and executes server-side code, returning dehydrated HTML pages.
*
* @param {Map<string, string>} serverCodeMap - Map of fileName to server-side JavaScript code.
* @param {ReturnType<import('node:module').createRequire>} requireFn - Node.js require function for external packages.
* @param {Object} virtualImports - virtual imports to pass to Rolldown
* @returns {{ pages: Map<string, string>, css: string }}
*/
async function executeServerCode(serverCodeMap, requireFn, virtualImports) {
// Bundle all server-side code, which may produce code-split chunks
const { chunks, css } = await bundleCode(serverCodeMap, virtualImports, {
server: true,
});
const entryChunks = chunks.filter(c => c.isEntry);
const otherChunks = chunks.filter(c => !c.isEntry);
// Create enhanced require function that can resolve code-split chunks
const enhancedRequire = createChunkedRequire(otherChunks, requireFn);
const pages = new Map();
// Execute each bundled entry and collect dehydrated HTML results
for (const chunk of entryChunks) {
const executedFunction = new Function('require', chunk.code);
pages.set(chunk.fileName, executedFunction(enhancedRequire));
}
return { pages, css };
}
/**
* Processes JSX AST entries into complete HTML pages, client JS bundles, and CSS.
*
* @param {Array<import('../../jsx-ast/utils/buildContent.mjs').JSXContent>} entries - The JSX AST entries to process.
* @param {string} template - The HTML template string for the output pages.
*/
export async function processJSXEntries(entries, template) {
const { version, titleSuffix: titleSuffixTemplate } = getConfig('web');
const astBuilders = createASTBuilder();
const requireFn = createRequire(import.meta.url);
const virtualImports = {
'#theme/config': createConfigSource(entries),
};
// Step 1: Convert JSX AST to JavaScript
const { serverCodeMap, clientCodeMap } = convertJSXToCode(
entries,
astBuilders
);
// Step 2: Bundle server and client code in parallel
// Both need all entries for code-splitting, but are independent of each other
const [serverBundle, clientBundle] = await Promise.all([
executeServerCode(serverCodeMap, requireFn, virtualImports),
bundleCode(clientCodeMap, virtualImports),
]);
const titleSuffix = populate(titleSuffixTemplate, {
version: version.version,
});
// Step 3: Render final HTML pages
const results = await Promise.all(
entries.map(async ({ data: { api, path, heading } }) => {
const title = `${heading.data.name} | ${titleSuffix}`;
const root = `${relative('/', path)}/`;
// Replace template placeholders with actual content
const renderedHtml = template
.replace('{{title}}', title)
.replace('{{dehydrated}}', serverBundle.pages.get(`${api}.js`) ?? '')
.replace(
'{{importMap}}',
clientBundle.importMap?.replaceAll('/', root) ?? ''
)
.replace('{{entrypoint}}', `${api}.js?${randomUUID()}`)
.replace('{{speculationRules}}', SPECULATION_RULES)
.replace('{{ogTitle}}', title)
.replaceAll('{{root}}', root);
return { html: await minifyHTML(renderedHtml), path };
})
);
const { code: minifiedCSS } = transform({
code: Buffer.from(`${serverBundle.css}\n${clientBundle.css}`),
minify: true,
});
return { results, chunks: clientBundle.chunks, css: minifiedCSS };
}