@hashrock/ono 0.1.3 → 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,62 +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";
9
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;
10
28
 
11
29
  /**
12
- * 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
13
33
  */
14
- export function isDynamicRoute(filePath) {
15
- 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`;
16
63
  }
17
64
 
18
- // Inline JSX runtime for bundled output
19
- const INLINE_JSX_RUNTIME = `
20
- function flattenChildren(children) {
21
- const result = [];
22
- for (const child of children) {
23
- if (child === null || child === undefined || typeof child === 'boolean') continue;
24
- if (Array.isArray(child)) {
25
- result.push(...flattenChildren(child));
26
- } else {
27
- result.push(child);
28
- }
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 });
29
82
  }
30
- return result;
31
83
  }
32
- function h(tag, props, ...children) {
33
- return { tag, props: props || {}, children: flattenChildren(children) };
34
- }
35
- `;
36
84
 
37
85
  /**
38
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}>}
39
93
  */
40
94
  export async function buildFile(inputFile, options = {}) {
41
- const { outputDir = "dist", unocssConfig, silent = false } = options;
95
+ const { outputDir = DIRS.OUTPUT, inputRoot, silent = false } = options;
42
96
 
43
97
  const outDir = resolve(process.cwd(), outputDir);
44
98
  const resolvedInput = resolve(process.cwd(), inputFile);
45
99
 
46
- // Bundle the file with all its dependencies
47
- const bundledCode = await bundle(resolvedInput);
48
-
49
- // Add inline JSX runtime
50
- const codeWithRuntime = INLINE_JSX_RUNTIME + bundledCode;
51
-
52
- // Write transformed JS temporarily
53
- const tempFile = join(outDir, `_temp_${Date.now()}.js`);
54
- await mkdir(dirname(tempFile), { recursive: true });
55
- await writeFile(tempFile, codeWithRuntime);
56
-
57
- // Import and render
58
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
59
- const module = await import(moduleUrl.href);
100
+ const module = await importJSXModule(resolvedInput);
60
101
 
61
102
  const App = module.default;
62
103
  if (!App) {
@@ -70,10 +111,11 @@ export async function buildFile(inputFile, options = {}) {
70
111
 
71
112
  const html = renderToString(vnode);
72
113
 
73
- // Determine output path
74
- const baseName = basename(inputFile, ".jsx");
75
- const outputFileName = baseName === "index" ? "index.html" : `${baseName}.html`;
76
- 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"));
77
119
 
78
120
  await mkdir(dirname(outputPath), { recursive: true });
79
121
  await writeFile(outputPath, html);
@@ -82,198 +124,61 @@ export async function buildFile(inputFile, options = {}) {
82
124
  console.log(`✓ Built successfully: ${relative(process.cwd(), outputPath)}`);
83
125
  }
84
126
 
85
- // Clean up temp file
86
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
87
-
88
127
  return { outputPath, html };
89
128
  }
90
129
 
91
- /**
92
- * Build a dynamic route (e.g., [slug].jsx)
93
- */
94
- export async function buildDynamicRoute(inputFile, options = {}) {
95
- const { outputDir = "dist", silent = false } = options;
96
-
97
- const outDir = resolve(process.cwd(), outputDir);
98
- const resolvedInput = resolve(process.cwd(), inputFile);
99
-
100
- // Bundle the file with all its dependencies
101
- const bundledCode = await bundle(resolvedInput);
102
-
103
- // Add inline JSX runtime
104
- const codeWithRuntime = INLINE_JSX_RUNTIME + bundledCode;
105
-
106
- // Write transformed JS temporarily
107
- const tempFile = join(outDir, `_temp_${Date.now()}.js`);
108
- await mkdir(dirname(tempFile), { recursive: true });
109
- await writeFile(tempFile, codeWithRuntime);
110
-
111
- // Import module
112
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
113
- const module = await import(moduleUrl.href);
114
-
115
- if (!module.getStaticPaths) {
116
- throw new Error(
117
- `Dynamic route ${inputFile} must export a getStaticPaths function`
118
- );
119
- }
120
-
121
- const pathsData = await module.getStaticPaths();
122
- const paths = Array.isArray(pathsData) ? pathsData : pathsData.paths || [];
123
-
124
- const App = module.default;
125
- if (!App) {
126
- throw new Error(`No default export found in ${inputFile}`);
127
- }
128
-
129
- const outputs = [];
130
-
131
- for (const pathData of paths) {
132
- const params = pathData.params || {};
133
-
134
- let vnode = typeof App === "function" ? App({ params }) : App;
135
- if (vnode instanceof Promise) {
136
- vnode = await vnode;
137
- }
138
-
139
- const html = renderToString(vnode);
140
-
141
- // Determine output path from params
142
- const routeDir = dirname(relative(join(process.cwd(), "pages"), resolvedInput));
143
- const fileName = basename(resolvedInput, ".jsx");
144
-
145
- // Replace [param] with actual value
146
- let outputPath = fileName;
147
- for (const [key, value] of Object.entries(params)) {
148
- outputPath = outputPath.replace(`[${key}]`, value);
149
- }
150
-
151
- const fullOutputPath = join(
152
- outDir,
153
- routeDir === "." ? "" : routeDir,
154
- `${outputPath}.html`
155
- );
156
-
157
- await mkdir(dirname(fullOutputPath), { recursive: true });
158
- await writeFile(fullOutputPath, html);
159
-
160
- outputs.push({ outputPath: fullOutputPath, html, params });
161
-
162
- if (!silent) {
163
- console.log(` ✓ ${relative(process.cwd(), fullOutputPath)}`);
164
- }
165
- }
166
-
167
- // Clean up temp file
168
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
169
-
170
- return outputs;
171
- }
172
-
173
130
  /**
174
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}>>}
175
137
  */
176
138
  export async function buildFiles(inputPattern, options = {}) {
177
- const { outputDir = "dist", unocssConfig, silent = false } = options;
139
+ const { outputDir = DIRS.OUTPUT, silent = false } = options;
178
140
 
179
141
  const pagesDir = resolve(process.cwd(), inputPattern);
180
- const files = await getAllJSXFiles(pagesDir);
142
+ const files = await getFilesRecursively(pagesDir, isJSXFile);
181
143
 
182
144
  if (!silent) {
183
145
  console.log(`Found ${files.length} page(s) in ${inputPattern}/\n`);
184
- }
185
-
186
- const results = [];
187
-
188
- for (const file of files) {
189
- if (isDynamicRoute(file)) {
190
- if (!silent) {
191
- const relativePath = relative(process.cwd(), file);
192
- const pathsData = await getDynamicRoutePaths(file);
193
- const count = Array.isArray(pathsData) ? pathsData.length : pathsData.paths?.length || 0;
194
- console.log(`Building dynamic route ${relativePath} (${count} pages)...`);
195
- }
196
- const outputs = await buildDynamicRoute(file, { outputDir, silent: true });
197
- results.push(...outputs);
198
- } else {
199
- if (!silent) {
200
- console.log(`Building ${relative(process.cwd(), file)}...`);
201
- }
202
- const result = await buildFile(file, { outputDir, unocssConfig, silent: true });
203
- results.push(result);
146
+ for (const file of files) {
147
+ console.log(`Building ${relative(process.cwd(), file)}...`);
204
148
  }
205
149
  }
206
150
 
207
- return results;
208
- }
209
-
210
- /**
211
- * Get all JSX files recursively
212
- */
213
- async function getAllJSXFiles(dir) {
214
- const files = [];
215
- const entries = await readdir(dir, { withFileTypes: true });
216
-
217
- for (const entry of entries) {
218
- const fullPath = join(dir, entry.name);
219
-
220
- if (entry.isDirectory()) {
221
- const subFiles = await getAllJSXFiles(fullPath);
222
- files.push(...subFiles);
223
- } else if (entry.isFile() && entry.name.endsWith(".jsx")) {
224
- files.push(fullPath);
225
- }
226
- }
227
-
228
- return files;
229
- }
230
-
231
- /**
232
- * Helper to get paths from a dynamic route
233
- */
234
- /**
235
- * Helper to get paths from a dynamic route
236
- */
237
- export async function getDynamicRoutePaths(file) {
238
- const outDir = resolve(process.cwd(), "dist");
239
-
240
- // Bundle the file with all its dependencies
241
- const bundledCode = await bundle(file);
242
-
243
- // Add inline JSX runtime
244
- const codeWithRuntime = INLINE_JSX_RUNTIME + bundledCode;
245
-
246
- const tempFile = join(outDir, `_temp_paths_${Date.now()}.js`);
247
- await mkdir(dirname(tempFile), { recursive: true });
248
- await writeFile(tempFile, codeWithRuntime);
249
-
250
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
251
- const module = await import(moduleUrl.href);
252
-
253
- const pathsData = module.getStaticPaths ? await module.getStaticPaths() : [];
254
-
255
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
256
-
257
- return pathsData;
151
+ return Promise.all(
152
+ files.map((file) =>
153
+ buildFile(file, { outputDir, inputRoot: pagesDir, silent: true }),
154
+ ),
155
+ );
258
156
  }
259
157
 
260
158
  /**
261
- * 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
262
166
  */
263
167
  export async function generateUnoCSS(options = {}) {
264
- const { outputDir = "dist", unocssConfig, silent = false } = options;
168
+ const { outputDir = DIRS.OUTPUT, silent = false } = options;
169
+ const config = options.config ?? (await loadUnoConfig());
265
170
 
266
171
  const outDir = resolve(process.cwd(), outputDir);
267
172
 
268
173
  // Scan all HTML files
269
- const htmlFiles = await getAllHTMLFiles(outDir);
174
+ const htmlFiles = await getFilesRecursively(outDir, isHTMLFile);
270
175
 
271
176
  if (htmlFiles.length === 0) {
272
177
  return null;
273
178
  }
274
179
 
275
180
  // Generate CSS from HTML files
276
- const css = await generateCSSFromFiles(htmlFiles, unocssConfig);
181
+ const css = await generateCSSFromFiles(htmlFiles, config);
277
182
 
278
183
  if (!css) {
279
184
  return null;
@@ -288,29 +193,3 @@ export async function generateUnoCSS(options = {}) {
288
193
 
289
194
  return cssPath;
290
195
  }
291
-
292
- /**
293
- * Get all HTML files recursively
294
- */
295
- async function getAllHTMLFiles(dir) {
296
- const files = [];
297
-
298
- try {
299
- const entries = await readdir(dir, { withFileTypes: true });
300
-
301
- for (const entry of entries) {
302
- const fullPath = join(dir, entry.name);
303
-
304
- if (entry.isDirectory()) {
305
- const subFiles = await getAllHTMLFiles(fullPath);
306
- files.push(...subFiles);
307
- } else if (entry.isFile() && entry.name.endsWith(".html")) {
308
- files.push(fullPath);
309
- }
310
- }
311
- } catch (error) {
312
- // Directory might not exist yet
313
- }
314
-
315
- return files;
316
- }