|
1 | 1 | import { readFile, writeFile } from 'node:fs/promises'; |
| 2 | +import { createRequire } from 'node:module'; |
2 | 3 | import { join } from 'node:path'; |
3 | 4 |
|
4 | | -import { generate } from '@babel/generator'; |
5 | 5 | import { estreeToBabel } from 'estree-to-babel'; |
6 | 6 | import Mustache from 'mustache'; |
7 | 7 |
|
| 8 | +import { ESBUILD_RESOLVE_DIR } from './constants.mjs'; |
8 | 9 | import createASTBuilder from './utils/build.mjs'; |
9 | 10 | import bundleCode from './utils/bundle.mjs'; |
10 | 11 |
|
11 | 12 | /** |
12 | | - * This generator generates a JavaScript / HTML / CSS bundle from the input JSX AST |
13 | | - * |
14 | | - * @typedef {Array<ApiDocMetadataEntry>} Input |
15 | | - * |
16 | | - * @type {GeneratorMetadata<Input, string>} |
| 13 | + * Executes server-side code in a safe, isolated context |
| 14 | + * @param {string} serverCode - The server code to execute |
| 15 | + * @param {Function} require - Node.js require function for dependencies |
| 16 | + * @returns {Promise<string>} The rendered HTML output |
| 17 | + */ |
| 18 | +async function executeServerCode(serverCode, require) { |
| 19 | + // Bundle the server code for execution |
| 20 | + const { js: bundledServer } = await bundleCode(serverCode, true); |
| 21 | + |
| 22 | + // Create a safe execution context that returns the rendered content |
| 23 | + const executedFunction = new Function( |
| 24 | + 'require', |
| 25 | + ` |
| 26 | + let code; |
| 27 | + ${bundledServer} |
| 28 | + return code; |
| 29 | + ` |
| 30 | + ); |
| 31 | + |
| 32 | + return executedFunction(require); |
| 33 | +} |
| 34 | + |
| 35 | +/** |
| 36 | + * Web bundle generator - converts JSX AST entries into HTML/CSS/JS bundles |
| 37 | + * Generates static HTML files with embedded JavaScript and CSS |
17 | 38 | */ |
18 | 39 | export default { |
19 | 40 | name: 'web', |
20 | 41 | version: '1.0.0', |
21 | | - description: |
22 | | - 'Generates a JavaScript / HTML / CSS bundle from the input JSX AST', |
| 42 | + description: 'Generates HTML/CSS/JS bundles from JSX AST entries', |
23 | 43 | dependsOn: 'jsx-ast', |
24 | 44 |
|
25 | 45 | /** |
26 | | - * Generates a JavaScript / HTML / CSS bundle |
27 | | - * |
28 | | - * @param {import('../jsx-ast/utils/buildContent.mjs').JSXContent[]} entries |
29 | | - * @param {Partial<GeneratorOptions>} options |
| 46 | + * Main generation function - processes JSX entries into web bundles |
| 47 | + * @param {Array} entries - JSX content entries to process |
| 48 | + * @param {Object} options - Generation options |
| 49 | + * @param {string} options.output - Output directory path |
| 50 | + * @returns {Promise<string[]>} Array of rendered HTML strings |
30 | 51 | */ |
31 | 52 | async generate(entries, { output }) { |
| 53 | + // Load the HTML template |
32 | 54 | const template = await readFile( |
33 | 55 | new URL('template.html', import.meta.url), |
34 | 56 | 'utf-8' |
35 | 57 | ); |
36 | 58 |
|
37 | | - const { buildClientProgram } = createASTBuilder(); |
| 59 | + // Set up AST builders for server and client code |
| 60 | + const { buildServerProgram, buildClientProgram } = createASTBuilder(); |
| 61 | + const require = createRequire(ESBUILD_RESOLVE_DIR); |
38 | 62 |
|
39 | | - let css; |
| 63 | + let css; // Will store CSS from the first bundle |
40 | 64 |
|
| 65 | + // Process each entry in parallel |
41 | 66 | const bundles = await Promise.all( |
42 | 67 | entries.map(async entry => { |
| 68 | + // Convert JSX AST to Babel AST |
43 | 69 | const { program } = estreeToBabel(entry); |
44 | | - const clientCode = generate(buildClientProgram(program)).code; |
45 | 70 |
|
46 | | - const bundled = await bundleCode(clientCode); |
| 71 | + // Generate and execute server-side code for SSR |
| 72 | + const serverCode = buildServerProgram(program); |
| 73 | + const serverRenderedHTML = await executeServerCode(serverCode, require); |
| 74 | + |
| 75 | + // Generate and bundle client-side code |
| 76 | + const clientCode = buildClientProgram(program); |
| 77 | + const clientBundle = await bundleCode(clientCode); |
47 | 78 |
|
48 | | - // Extract CSS from first bundle only |
49 | | - css ??= bundled.css; |
| 79 | + // Extract CSS only from the first bundle to avoid duplicates |
| 80 | + css ??= clientBundle.css; |
50 | 81 |
|
51 | | - // TODO: Remove mustache |
52 | | - const rendered = Mustache.render(template, { |
| 82 | + // Render the final HTML using the template |
| 83 | + const finalHTML = Mustache.render(template, { |
53 | 84 | title: entry.data.heading.data.name, |
54 | | - javascript: bundled.js, |
| 85 | + javascript: clientBundle.js, |
| 86 | + dehydrated: serverRenderedHTML, |
55 | 87 | }); |
56 | 88 |
|
| 89 | + // Write individual HTML file if output directory is specified |
57 | 90 | if (output) { |
58 | | - await writeFile(join(output, `${entry.data.api}.html`), rendered); |
| 91 | + const filename = `${entry.data.api}.html`; |
| 92 | + await writeFile(join(output, filename), finalHTML); |
59 | 93 | } |
60 | 94 |
|
61 | | - return rendered; |
| 95 | + return finalHTML; |
62 | 96 | }) |
63 | 97 | ); |
64 | 98 |
|
| 99 | + // Write shared CSS file if we have CSS and an output directory |
65 | 100 | if (output && css) { |
66 | 101 | await writeFile(join(output, 'styles.css'), css); |
67 | 102 | } |
|
0 commit comments