@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/README.md +56 -0
- 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 +109 -230
- 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,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,
|
|
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";
|
|
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
|
-
*
|
|
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
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
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 =
|
|
139
|
+
const { outputDir = DIRS.OUTPUT, silent = false } = options;
|
|
178
140
|
|
|
179
141
|
const pagesDir = resolve(process.cwd(), inputPattern);
|
|
180
|
-
const files = await
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
}
|