Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 83 additions & 88 deletions npm-shrinkwrap.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
"rolldown": "^1.0.0-beta.40",
"rolldown": "^1.0.0-beta.47",
"semver": "^7.7.2",
"shiki": "^3.15.0",
"unified": "^11.0.5",
Expand Down
2 changes: 1 addition & 1 deletion src/generators/legacy-html/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import HTMLMinifier from '@minify-html/node';

import buildContent from './utils/buildContent.mjs';
import dropdowns from './utils/buildDropdowns.mjs';
import { safeCopy } from './utils/safeCopy.mjs';
import tableOfContents from './utils/tableOfContents.mjs';
import { groupNodesByModule } from '../../utils/generators.mjs';
import { getRemarkRehype } from '../../utils/remark.mjs';
import { safeCopy } from '../../utils/safeCopy.mjs';

/**
* @typedef {{
Expand Down
75 changes: 38 additions & 37 deletions src/generators/web/index.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { readFile, writeFile } from 'node:fs/promises';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { join } from 'node:path';

import createASTBuilder from './utils/generate.mjs';
import { processJSXEntry } from './utils/processing.mjs';
import { processJSXEntries } from './utils/processing.mjs';
import { safeWrite } from '../../utils/safeWrite.mjs';

/**
* This generator transforms JSX AST (Abstract Syntax Tree) entries into a complete
* web bundle, including server-side rendered HTML, client-side JavaScript, and CSS.
* Web generator - transforms JSX AST entries into complete web bundles.
*
* This generator processes JSX AST entries and produces:
* - Server-side rendered HTML pages
* - Client-side JavaScript with code splitting
* - Bundled CSS styles
*
* @type {GeneratorMetadata<Input, string>}
*/
Expand All @@ -18,57 +23,53 @@ export default {
dependsOn: 'jsx-ast',

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

// These builders are responsible for converting the JSX AST into executable
// JavaScript code for both server-side rendering and client-side hydration.
// Create AST builders for server and client programs
const astBuilders = createASTBuilder();

// This is necessary for the `executeServerCode` function to resolve modules
// within the dynamically executed server-side code.
// Create require function for resolving external packages in server code
const requireFn = createRequire(import.meta.url);

const results = [];
let mainCss = '';

for (const entry of entries) {
const { html, css } = await processJSXEntry(
entry,
template,
astBuilders,
requireFn,
version
);
results.push({ html, css });
// Process all entries: convert JSX to HTML/CSS/JS
const { results, css, jsChunks } = await processJSXEntries(
entries,
template,
astBuilders,
requireFn,
{ version }
);

// Capture the main CSS bundle from the first processed entry.
if (!mainCss && css) {
mainCss = css;
// Write files to disk if output directory is specified
if (output) {
// Write HTML files
for (const { html, api } of results) {
await safeWrite(join(output, `${api}.html`), html, 'utf-8');
}

// Write HTML file if output directory is specified
if (output) {
await writeFile(join(output, `${entry.data.api}.html`), html, 'utf-8');
// Write code-split JavaScript chunks
for (const chunk of jsChunks) {
await safeWrite(join(output, chunk.fileName), chunk.code, 'utf-8');
}
}

if (output && mainCss) {
const filePath = join(output, 'styles.css');
await writeFile(filePath, mainCss, 'utf-8');
// Write CSS bundle
await safeWrite(join(output, 'styles.css'), css, 'utf-8');
}

return results;
// Return HTML and CSS for each entry
return results.map(({ html }) => ({ html, css }));
},
};
4 changes: 3 additions & 1 deletion src/generators/web/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
<script>document.documentElement.setAttribute("data-theme",localStorage.getItem("theme")||(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"));</script>

{{importMap}}
</head>

<body>
<div id="root">{{dehydrated}}</div>
<script>{{clientBundleJs}}</script>
<script type="module">{{mainJsCode}}</script>
</body>
</html>
12 changes: 8 additions & 4 deletions src/generators/web/ui/components/CodeBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export const getLanguageDisplayName = language => {
};

/** @param {import('react').PropsWithChildren<{ className: string }>} props */
export default ({ className, ...props }) => {
export default ({ className, children, ...props }) => {
const matches = className?.match(/language-(?<language>[a-zA-Z]+)/);

const language = matches?.groups?.language ?? '';

const notify = useNotification();
Expand All @@ -30,7 +31,7 @@ export default ({ className, ...props }) => {
await navigator.clipboard.writeText(text);

notify({
duration: 300,
duration: 3000,
message: (
<div className="flex items-center gap-3">
<CodeBracketIcon className={styles.icon} />
Expand All @@ -44,8 +45,11 @@ export default ({ className, ...props }) => {
<BaseCodeBox
onCopy={onCopy}
language={getLanguageDisplayName(language)}
{...props}
className={className}
buttonText="Copy to clipboard"
/>
{...props}
>
{children}
</BaseCodeBox>
);
};
1 change: 1 addition & 0 deletions src/generators/web/ui/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import '@node-core/ui-components/styles/index.css';
@import '@node-core/rehype-shiki/index.css';

/* Fonts */
:root {
Expand Down
Loading
Loading