@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/README.md +35 -320
- package/package.json +14 -13
- package/src/barrels.js +230 -0
- package/src/browser/compiler.js +71 -193
- package/src/browser/unocss.js +5 -1
- package/src/builder.js +110 -232
- package/src/bundler.js +215 -64
- package/src/cli.js +22 -191
- package/src/commands/build.js +141 -0
- package/src/commands/dev.js +54 -0
- package/src/constants.js +74 -0
- package/src/jsx-runtime.js +15 -5
- package/src/parser.js +555 -0
- package/src/renderer.js +29 -48
- package/src/server.js +88 -74
- package/src/transformer.js +1 -18
- package/src/unocss.js +20 -24
- package/src/utils.js +55 -0
- package/src/watcher.js +98 -125
- package/src/content.js +0 -272
- package/src/resolver.js +0 -137
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,
|
|
5
|
-
import { resolve, join, dirname, basename, relative
|
|
6
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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 =
|
|
139
|
+
const { outputDir = DIRS.OUTPUT, silent = false } = options;
|
|
180
140
|
|
|
181
141
|
const pagesDir = resolve(process.cwd(), inputPattern);
|
|
182
|
-
const files = await
|
|
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
|
|
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
|
-
*
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
}
|