|
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