@hashrock/ono 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/builder.js CHANGED
@@ -1,63 +1,103 @@
1
1
  /**
2
2
  * Build utilities for Ono SSG
3
+ *
4
+ * Pages are bundled with the browser-compatible mini bundler
5
+ * (bundler.js + parser.js), written to a single temp file, and imported.
6
+ * Package (bare) imports are hoisted to the top of the bundle where
7
+ * Node's own resolution handles them.
3
8
  */
4
- import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
5
- import { resolve, join, dirname, basename, relative, extname } from "node:path";
6
- import { transformJSX } from "./transformer.js";
9
+ import { readFile, writeFile, mkdir, rm } from "node:fs/promises";
10
+ import { resolve, join, dirname, basename, relative } from "node:path";
11
+ import { pathToFileURL } from "node:url";
7
12
  import { renderToString } from "./renderer.js";
8
- import { generateCSSFromFiles } from "./unocss.js";
13
+ import { generateCSSFromFiles, loadUnoConfig } from "./unocss.js";
14
+ import { transformJSX } from "./transformer.js";
15
+ import { bundle } from "./bundler.js";
16
+ import { getFilesRecursively, isJSXFile, isHTMLFile } from "./utils.js";
17
+ import { DIRS } from "./constants.js";
18
+
19
+ /** Import statement for the real JSX runtime, injected into compiled pages */
20
+ const JSX_RUNTIME_IMPORT = `import { h, Fragment } from ${JSON.stringify(
21
+ new URL("./jsx-runtime.js", import.meta.url).href,
22
+ )};\n`;
23
+
24
+ /** Directory (inside the project) that holds per-build temp files */
25
+ const TEMP_DIR = ".ono";
26
+
27
+ let buildCounter = 0;
9
28
 
10
29
  /**
11
- * Check if a route is dynamic (contains [param])
30
+ * Bundle an entry file into a single ES module source string
31
+ * @param {string} entryFile - Absolute path to the entry file
32
+ * @returns {Promise<string>} Bundled module source
12
33
  */
13
- export function isDynamicRoute(filePath) {
14
- return /\[([^\]]+)\]/.test(filePath);
34
+ export async function compileBundle(entryFile) {
35
+ const { code, entryExports, externalBindings } = await bundle({
36
+ entry: entryFile,
37
+ resolve: (specifier, fromId) => resolve(dirname(fromId), specifier),
38
+ load: async (id) => {
39
+ let source;
40
+ try {
41
+ source = await readFile(id, "utf-8");
42
+ } catch (error) {
43
+ throw new Error(`Cannot read file: ${id}\n${error.message}`);
44
+ }
45
+ return transformJSX(source, id);
46
+ },
47
+ });
48
+
49
+ // Provide h/Fragment unless a page pulls in the runtime itself
50
+ const header =
51
+ externalBindings.has("h") || externalBindings.has("Fragment") ? "" : JSX_RUNTIME_IMPORT;
52
+
53
+ // Re-export the entry's exports so the bundle behaves like the entry module
54
+ const footer = entryExports
55
+ .map((name) =>
56
+ name === "default"
57
+ ? "export default __ono_entry.default;"
58
+ : `export const ${name} = __ono_entry[${JSON.stringify(name)}];`,
59
+ )
60
+ .join("\n");
61
+
62
+ return `${header}${code}\n${footer}\n`;
15
63
  }
16
64
 
17
- // Inline JSX runtime for bundled output
18
- const INLINE_JSX_RUNTIME = `
19
- function flattenChildren(children) {
20
- const result = [];
21
- for (const child of children) {
22
- if (child === null || child === undefined || typeof child === 'boolean') continue;
23
- if (Array.isArray(child)) {
24
- result.push(...flattenChildren(child));
25
- } else {
26
- result.push(child);
27
- }
65
+ /**
66
+ * Bundle a JSX entry file (with its local imports) and import it.
67
+ * A unique temp file per build doubles as ESM cache-busting for rebuilds.
68
+ * @param {string} entryFile - Path to the entry file
69
+ * @returns {Promise<any>} The imported module namespace
70
+ */
71
+ export async function importJSXModule(entryFile) {
72
+ const resolvedEntry = resolve(process.cwd(), entryFile);
73
+ const code = await compileBundle(resolvedEntry);
74
+
75
+ const tempFile = join(process.cwd(), TEMP_DIR, `build-${process.pid}-${buildCounter++}.js`);
76
+ await mkdir(dirname(tempFile), { recursive: true });
77
+ await writeFile(tempFile, code);
78
+ try {
79
+ return await import(pathToFileURL(tempFile).href);
80
+ } finally {
81
+ await rm(tempFile, { force: true });
28
82
  }
29
- return result;
30
83
  }
31
- function h(tag, props, ...children) {
32
- return { tag, props: props || {}, children: flattenChildren(children) };
33
- }
34
- `;
35
84
 
36
85
  /**
37
86
  * Build a single JSX file
87
+ * @param {string} inputFile - Path to the JSX file
88
+ * @param {Object} options - Build options
89
+ * @param {string} [options.outputDir] - Output directory
90
+ * @param {string} [options.inputRoot] - Root directory the page paths are relative to
91
+ * @param {boolean} [options.silent] - Suppress console output
92
+ * @returns {Promise<{outputPath: string, html: string}>}
38
93
  */
39
94
  export async function buildFile(inputFile, options = {}) {
40
- const { outputDir = "dist", unocssConfig, silent = false } = options;
95
+ const { outputDir = DIRS.OUTPUT, inputRoot, silent = false } = options;
41
96
 
42
97
  const outDir = resolve(process.cwd(), outputDir);
43
98
  const resolvedInput = resolve(process.cwd(), inputFile);
44
99
 
45
- // Read and transform JSX
46
- const jsx = await readFile(resolvedInput, "utf-8");
47
- const transformed = await transformJSX(jsx, resolvedInput);
48
-
49
- // Remove import statements for local components (they need to be bundled separately)
50
- // For now, just add the h function inline
51
- const codeWithRuntime = INLINE_JSX_RUNTIME + transformed;
52
-
53
- // Write transformed JS temporarily
54
- const tempFile = join(outDir, `_temp_${Date.now()}.js`);
55
- await mkdir(dirname(tempFile), { recursive: true });
56
- await writeFile(tempFile, codeWithRuntime);
57
-
58
- // Import and render
59
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
60
- const module = await import(moduleUrl.href);
100
+ const module = await importJSXModule(resolvedInput);
61
101
 
62
102
  const App = module.default;
63
103
  if (!App) {
@@ -71,10 +111,11 @@ export async function buildFile(inputFile, options = {}) {
71
111
 
72
112
  const html = renderToString(vnode);
73
113
 
74
- // Determine output path
75
- const baseName = basename(inputFile, ".jsx");
76
- const outputFileName = baseName === "index" ? "index.html" : `${baseName}.html`;
77
- const outputPath = join(outDir, outputFileName);
114
+ // Determine output path, preserving directory structure relative to inputRoot
115
+ const relPath = inputRoot
116
+ ? relative(resolve(process.cwd(), inputRoot), resolvedInput)
117
+ : basename(resolvedInput);
118
+ const outputPath = join(outDir, relPath.replace(/\.(jsx|tsx)$/, ".html"));
78
119
 
79
120
  await mkdir(dirname(outputPath), { recursive: true });
80
121
  await writeFile(outputPath, html);
@@ -83,198 +124,61 @@ export async function buildFile(inputFile, options = {}) {
83
124
  console.log(`✓ Built successfully: ${relative(process.cwd(), outputPath)}`);
84
125
  }
85
126
 
86
- // Clean up temp file
87
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
88
-
89
127
  return { outputPath, html };
90
128
  }
91
129
 
92
- /**
93
- * Build a dynamic route (e.g., [slug].jsx)
94
- */
95
- export async function buildDynamicRoute(inputFile, options = {}) {
96
- const { outputDir = "dist", silent = false } = options;
97
-
98
- const outDir = resolve(process.cwd(), outputDir);
99
- const resolvedInput = resolve(process.cwd(), inputFile);
100
-
101
- // Read and transform JSX
102
- const jsx = await readFile(resolvedInput, "utf-8");
103
- const transformed = await transformJSX(jsx, resolvedInput);
104
-
105
- // Add inline JSX runtime
106
- const codeWithRuntime = INLINE_JSX_RUNTIME + transformed;
107
-
108
- // Write transformed JS temporarily
109
- const tempFile = join(outDir, `_temp_${Date.now()}.js`);
110
- await mkdir(dirname(tempFile), { recursive: true });
111
- await writeFile(tempFile, codeWithRuntime);
112
-
113
- // Import module
114
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
115
- const module = await import(moduleUrl.href);
116
-
117
- if (!module.getStaticPaths) {
118
- throw new Error(
119
- `Dynamic route ${inputFile} must export a getStaticPaths function`
120
- );
121
- }
122
-
123
- const pathsData = await module.getStaticPaths();
124
- const paths = Array.isArray(pathsData) ? pathsData : pathsData.paths || [];
125
-
126
- const App = module.default;
127
- if (!App) {
128
- throw new Error(`No default export found in ${inputFile}`);
129
- }
130
-
131
- const outputs = [];
132
-
133
- for (const pathData of paths) {
134
- const params = pathData.params || {};
135
-
136
- let vnode = typeof App === "function" ? App({ params }) : App;
137
- if (vnode instanceof Promise) {
138
- vnode = await vnode;
139
- }
140
-
141
- const html = renderToString(vnode);
142
-
143
- // Determine output path from params
144
- const routeDir = dirname(relative(join(process.cwd(), "pages"), resolvedInput));
145
- const fileName = basename(resolvedInput, ".jsx");
146
-
147
- // Replace [param] with actual value
148
- let outputPath = fileName;
149
- for (const [key, value] of Object.entries(params)) {
150
- outputPath = outputPath.replace(`[${key}]`, value);
151
- }
152
-
153
- const fullOutputPath = join(
154
- outDir,
155
- routeDir === "." ? "" : routeDir,
156
- `${outputPath}.html`
157
- );
158
-
159
- await mkdir(dirname(fullOutputPath), { recursive: true });
160
- await writeFile(fullOutputPath, html);
161
-
162
- outputs.push({ outputPath: fullOutputPath, html, params });
163
-
164
- if (!silent) {
165
- console.log(` ✓ ${relative(process.cwd(), fullOutputPath)}`);
166
- }
167
- }
168
-
169
- // Clean up temp file
170
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
171
-
172
- return outputs;
173
- }
174
-
175
130
  /**
176
131
  * Build multiple JSX files
132
+ * @param {string} inputPattern - Input directory path
133
+ * @param {Object} options - Build options
134
+ * @param {string} [options.outputDir] - Output directory
135
+ * @param {boolean} [options.silent] - Suppress console output
136
+ * @returns {Promise<Array<{outputPath: string, html: string}>>}
177
137
  */
178
138
  export async function buildFiles(inputPattern, options = {}) {
179
- const { outputDir = "dist", unocssConfig, silent = false } = options;
139
+ const { outputDir = DIRS.OUTPUT, silent = false } = options;
180
140
 
181
141
  const pagesDir = resolve(process.cwd(), inputPattern);
182
- const files = await getAllJSXFiles(pagesDir);
142
+ const files = await getFilesRecursively(pagesDir, isJSXFile);
183
143
 
184
144
  if (!silent) {
185
145
  console.log(`Found ${files.length} page(s) in ${inputPattern}/\n`);
186
- }
187
-
188
- const results = [];
189
-
190
- for (const file of files) {
191
- if (isDynamicRoute(file)) {
192
- if (!silent) {
193
- const relativePath = relative(process.cwd(), file);
194
- const pathsData = await getDynamicRoutePaths(file);
195
- const count = Array.isArray(pathsData) ? pathsData.length : pathsData.paths?.length || 0;
196
- console.log(`Building dynamic route ${relativePath} (${count} pages)...`);
197
- }
198
- const outputs = await buildDynamicRoute(file, { outputDir, silent: true });
199
- results.push(...outputs);
200
- } else {
201
- if (!silent) {
202
- console.log(`Building ${relative(process.cwd(), file)}...`);
203
- }
204
- const result = await buildFile(file, { outputDir, unocssConfig, silent: true });
205
- results.push(result);
146
+ for (const file of files) {
147
+ console.log(`Building ${relative(process.cwd(), file)}...`);
206
148
  }
207
149
  }
208
150
 
209
- return results;
151
+ return Promise.all(
152
+ files.map((file) =>
153
+ buildFile(file, { outputDir, inputRoot: pagesDir, silent: true }),
154
+ ),
155
+ );
210
156
  }
211
157
 
212
158
  /**
213
- * Get all JSX files recursively
214
- */
215
- async function getAllJSXFiles(dir) {
216
- const files = [];
217
- const entries = await readdir(dir, { withFileTypes: true });
218
-
219
- for (const entry of entries) {
220
- const fullPath = join(dir, entry.name);
221
-
222
- if (entry.isDirectory()) {
223
- const subFiles = await getAllJSXFiles(fullPath);
224
- files.push(...subFiles);
225
- } else if (entry.isFile() && entry.name.endsWith(".jsx")) {
226
- files.push(fullPath);
227
- }
228
- }
229
-
230
- return files;
231
- }
232
-
233
- /**
234
- * Helper to get paths from a dynamic route
235
- */
236
- /**
237
- * Helper to get paths from a dynamic route
238
- */
239
- export async function getDynamicRoutePaths(file) {
240
- const outDir = resolve(process.cwd(), "dist");
241
- const jsx = await readFile(file, "utf-8");
242
- const transformed = await transformJSX(jsx, file);
243
-
244
- // Add inline JSX runtime
245
- const codeWithRuntime = INLINE_JSX_RUNTIME + transformed;
246
-
247
- const tempFile = join(outDir, `_temp_paths_${Date.now()}.js`);
248
- await mkdir(dirname(tempFile), { recursive: true });
249
- await writeFile(tempFile, codeWithRuntime);
250
-
251
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
252
- const module = await import(moduleUrl.href);
253
-
254
- const pathsData = module.getStaticPaths ? await module.getStaticPaths() : [];
255
-
256
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
257
-
258
- return pathsData;
259
- }
260
-
261
- /**
262
- * Generate UnoCSS file
159
+ * Generate UnoCSS file from the built HTML.
160
+ * Loads uno.config.js from the project root unless a config is passed in.
161
+ * @param {Object} options - Generation options
162
+ * @param {string} [options.outputDir] - Output directory
163
+ * @param {Object} [options.config] - UnoCSS configuration override
164
+ * @param {boolean} [options.silent] - Suppress console output
165
+ * @returns {Promise<string|null>} CSS file path or null
263
166
  */
264
167
  export async function generateUnoCSS(options = {}) {
265
- const { outputDir = "dist", unocssConfig, silent = false } = options;
168
+ const { outputDir = DIRS.OUTPUT, silent = false } = options;
169
+ const config = options.config ?? (await loadUnoConfig());
266
170
 
267
171
  const outDir = resolve(process.cwd(), outputDir);
268
172
 
269
173
  // Scan all HTML files
270
- const htmlFiles = await getAllHTMLFiles(outDir);
174
+ const htmlFiles = await getFilesRecursively(outDir, isHTMLFile);
271
175
 
272
176
  if (htmlFiles.length === 0) {
273
177
  return null;
274
178
  }
275
179
 
276
180
  // Generate CSS from HTML files
277
- const css = await generateCSSFromFiles(htmlFiles, unocssConfig);
181
+ const css = await generateCSSFromFiles(htmlFiles, config);
278
182
 
279
183
  if (!css) {
280
184
  return null;
@@ -289,29 +193,3 @@ export async function generateUnoCSS(options = {}) {
289
193
 
290
194
  return cssPath;
291
195
  }
292
-
293
- /**
294
- * Get all HTML files recursively
295
- */
296
- async function getAllHTMLFiles(dir) {
297
- const files = [];
298
-
299
- try {
300
- const entries = await readdir(dir, { withFileTypes: true });
301
-
302
- for (const entry of entries) {
303
- const fullPath = join(dir, entry.name);
304
-
305
- if (entry.isDirectory()) {
306
- const subFiles = await getAllHTMLFiles(fullPath);
307
- files.push(...subFiles);
308
- } else if (entry.isFile() && entry.name.endsWith(".html")) {
309
- files.push(fullPath);
310
- }
311
- }
312
- } catch (error) {
313
- // Directory might not exist yet
314
- }
315
-
316
- return files;
317
- }