diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index dd55f160..53eff0d7 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -1,3 +1,17 @@ +import { parse, relative, sep, dirname } from 'node:path'; +import { resolve } from 'node:path/posix'; +import { fileURLToPath } from 'node:url'; + +// Convert the current module's URL to a filesystem path, +// then calculate the relative path from the system root directory +// to this file. This relative path uses platform-specific separators, +// so replace them with forward slashes ("/") for consistency and web compatibility. +// Finally, prepend a leading slash to form an absolute root-relative path string. +// +// This produces a POSIX-style absolute path, even on Windows systems. +const dir = dirname(fileURLToPath(import.meta.url)); +export const ROOT = '/' + relative(parse(dir).root, dir).replaceAll(sep, '/'); + /** * @typedef {Object} JSXImportConfig * @property {string} name - The name of the component to be imported. @@ -12,19 +26,19 @@ export const JSX_IMPORTS = { NavBar: { name: 'NavBar', - source: new URL('./ui/components/NavBar', import.meta.url).pathname, + source: resolve(ROOT, './ui/components/NavBar'), }, SideBar: { name: 'SideBar', - source: new URL('./ui/components/SideBar', import.meta.url).pathname, + source: resolve(ROOT, './ui/components/SideBar'), }, MetaBar: { name: 'MetaBar', - source: new URL('./ui/components/MetaBar', import.meta.url).pathname, + source: resolve(ROOT, './ui/components/MetaBar'), }, CodeBox: { name: 'CodeBox', - source: new URL('./ui/components/CodeBox', import.meta.url).pathname, + source: resolve(ROOT, './ui/components/CodeBox'), }, CodeTabs: { name: 'CodeTabs', diff --git a/src/generators/web/utils/bundle.mjs b/src/generators/web/utils/bundle.mjs index 02be35a0..5aa297cf 100644 --- a/src/generators/web/utils/bundle.mjs +++ b/src/generators/web/utils/bundle.mjs @@ -37,7 +37,9 @@ export default async function bundleCode(code, { server = false } = {}) { // External dependencies to exclude from bundling. // These are expected to be available at runtime in the server environment. // This reduces bundle size and avoids bundling shared server libs. - external: server ? ['preact', '@node-core/ui-components'] : [], + external: server + ? ['preact', 'preact-render-to-string', '@node-core/ui-components'] + : [], // Inject global compile-time constants that will be replaced in code. // These are useful for tree-shaking and conditional branching. diff --git a/src/generators/web/utils/generate.mjs b/src/generators/web/utils/generate.mjs index 6ee31891..55aa3141 100644 --- a/src/generators/web/utils/generate.mjs +++ b/src/generators/web/utils/generate.mjs @@ -1,4 +1,6 @@ -import { JSX_IMPORTS } from '../constants.mjs'; +import { resolve } from 'node:path/posix'; + +import { JSX_IMPORTS, ROOT } from '../constants.mjs'; /** * Creates an ES Module `import` statement as a string, based on parameters. @@ -54,43 +56,35 @@ export default () => { // Import client-side CSS styles. // This ensures that styles used in the rendered app are loaded on the client. // The use of `new URL(...).pathname` resolves the absolute path for `entrypoint.jsx`. - createImportDeclaration( - null, - new URL('../ui/index.css', import.meta.url).pathname - ), + createImportDeclaration(null, resolve(ROOT, './ui/index.css')), // Import `hydrate()` from Preact — needed to attach to server-rendered HTML. // This is a named import (not default), hence `false` as the third argument. createImportDeclaration('hydrate', 'preact', false), - '', - // Hydration call: binds the component to an element with ID "root" // This assumes SSR has placed matching HTML there, which, it has. `hydrate(${componentCode}, document.getElementById("root"));`, - ].join('\n'); + ].join(''); }; /** * Builds a server-side rendering (SSR) program. * * @param {string} componentCode - Code expression representing a JSX component + * @param {string} variable - The variable to output it to */ - const buildServerProgram = componentCode => { + const buildServerProgram = (componentCode, variable) => { return [ // JSX component imports ...baseImports, - // Import `renderToStringAsync()` from Preact's SSR module - createImportDeclaration( - 'renderToStringAsync', - 'preact-render-to-string', - false - ), + // Import Preact's SSR module + createImportDeclaration('render', 'preact-render-to-string', false), // Render the component to an HTML string // The output can be embedded directly into the server's HTML template - `const code = renderToStringAsync(${componentCode});`, + `const ${variable} = render(${componentCode});`, ].join('\n'); }; diff --git a/src/generators/web/utils/processing.mjs b/src/generators/web/utils/processing.mjs index e2380f93..ccbba5d9 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -4,6 +4,10 @@ import Mustache from 'mustache'; import bundleCode from './bundle.mjs'; +// Generate a unique variable name to capture the result of the server-side code. +// This prevents naming conflicts. +const SSRvariable = `_${Math.random().toString(36).slice(2)}`; + /** * Executes server-side JavaScript code in a safe, isolated context. * This function takes a string of JavaScript code, bundles it, and then runs it @@ -20,10 +24,6 @@ export async function executeServerCode(serverCode, requireFn) { // for execution, ensuring all necessary dependencies are self-contained. const { js: bundledServer } = await bundleCode(serverCode, { server: true }); - // Generate a unique variable name to capture the result of the server-side code. - // This prevents naming conflicts. - const variable = `_${Math.random().toString(36).slice(2)}`; - // Create a new Function from the bundled server code. // The `require` argument is passed into the function's scope, allowing the // `bundledServer` code to use it for dynamic imports. @@ -31,7 +31,7 @@ export async function executeServerCode(serverCode, requireFn) { // the dynamic variable within the `bundledServer` code is returned by this function. const executedFunction = new Function( 'require', - `let ${variable};${bundledServer}return ${variable};` + `${bundledServer}\nreturn ${SSRvariable};` ); // Execute the dynamically created function with the provided `requireFn`. @@ -63,7 +63,7 @@ export async function processJSXEntry( // `buildServerProgram` takes the JSX-derived code and prepares it for server execution. // `executeServerCode` then runs this code in a Node.js environment to produce // the initial HTML content (dehydrated state) that will be sent to the client. - const serverCode = buildServerProgram(code); + const serverCode = buildServerProgram(code, SSRvariable); const dehydrated = await executeServerCode(serverCode, requireFn); // `buildClientProgram` prepares the JSX-derived code for client-side execution.