@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/bundler.js
CHANGED
|
@@ -1,81 +1,232 @@
|
|
|
1
|
+
// @ts-nocheck — consumes the parser's anonymous-tuple combinator results;
|
|
2
|
+
// checkJs adds noise here without catching real bugs (see parser.js).
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Mini bundler - browser-compatible.
|
|
5
|
+
*
|
|
6
|
+
* Each module is wrapped in a factory function and linked with a tiny
|
|
7
|
+
* lazy require(), so no topological sort is needed and import cycles
|
|
8
|
+
* behave like CommonJS. Module syntax is parsed with the parser
|
|
9
|
+
* combinators in parser.js — no regular expressions over source code.
|
|
10
|
+
*
|
|
11
|
+
* The host environment supplies I/O:
|
|
12
|
+
* - load(id): return the module's JavaScript source (JSX already transformed)
|
|
13
|
+
* - resolve(specifier, fromId): turn a relative specifier into a module id
|
|
14
|
+
*
|
|
15
|
+
* Known limitations (deliberate, for simplicity): no top-level await,
|
|
16
|
+
* no destructuring in exported declarations, `export *` copies a
|
|
17
|
+
* snapshot of the source module.
|
|
3
18
|
*/
|
|
19
|
+
import { parseModule, isIdentifierName } from "./parser.js";
|
|
4
20
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { transformJSX } from "./transformer.js";
|
|
21
|
+
const isRelative = (specifier) =>
|
|
22
|
+
specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
|
|
8
23
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const modules = [];
|
|
19
|
-
|
|
20
|
-
// Process each module in dependency order
|
|
21
|
-
for (let i = 0; i < order.length; i++) {
|
|
22
|
-
const filePath = order[i];
|
|
23
|
-
const isEntry = i === order.length - 1; // Last file is entry
|
|
24
|
-
|
|
25
|
-
// Read the file
|
|
26
|
-
const source = await fs.readFile(filePath, "utf-8");
|
|
27
|
-
|
|
28
|
-
// Transform JSX to JS
|
|
29
|
-
const transformed = transformJSX(source, filePath);
|
|
30
|
-
|
|
31
|
-
// Remove import statements (they're already resolved)
|
|
32
|
-
const withoutImports = removeImports(transformed);
|
|
33
|
-
|
|
34
|
-
// Remove export default from non-entry files
|
|
35
|
-
const withoutExports = isEntry ? withoutImports : removeExportDefault(withoutImports);
|
|
24
|
+
/** Apply text replacements (non-overlapping) to a source string */
|
|
25
|
+
function applyEdits(source, edits) {
|
|
26
|
+
let result = source;
|
|
27
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
28
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
/** Generate the require/destructuring lines that replace an import statement */
|
|
34
|
+
function importReplacement(imp, requireCall) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
if (imp.defaultBinding) parts.push(`const ${imp.defaultBinding} = ${requireCall}.default;`);
|
|
37
|
+
if (imp.namespace) parts.push(`const ${imp.namespace} = ${requireCall};`);
|
|
38
|
+
if (imp.named && imp.named.length > 0) {
|
|
39
|
+
const bindings = imp.named
|
|
40
|
+
.map(({ imported, local }) =>
|
|
41
|
+
imported === local ? imported : `${JSON.stringify(imported)}: ${local}`,
|
|
42
|
+
)
|
|
43
|
+
.join(", ");
|
|
44
|
+
parts.push(`const { ${bindings} } = ${requireCall};`);
|
|
42
45
|
}
|
|
46
|
+
if (parts.length === 0) parts.push(`${requireCall};`); // side-effect only
|
|
47
|
+
return parts.join(" ");
|
|
48
|
+
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
/** Record the top-level names an import statement binds */
|
|
51
|
+
function collectBindings(imp, bindings) {
|
|
52
|
+
if (imp.defaultBinding) bindings.add(imp.defaultBinding);
|
|
53
|
+
if (imp.namespace) bindings.add(imp.namespace);
|
|
54
|
+
for (const { local } of imp.named ?? []) bindings.add(local);
|
|
55
|
+
}
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
const LINKER_RUNTIME = `const __ono_cache = new Map();
|
|
58
|
+
function __ono_require(id) {
|
|
59
|
+
let record = __ono_cache.get(id);
|
|
60
|
+
if (!record) {
|
|
61
|
+
record = { exports: {} };
|
|
62
|
+
__ono_cache.set(id, record);
|
|
63
|
+
__ono_modules[id](record.exports, __ono_require);
|
|
64
|
+
}
|
|
65
|
+
return record.exports;
|
|
48
66
|
}
|
|
67
|
+
function __ono_export_star(target, source) {
|
|
68
|
+
for (const key of Object.keys(source)) {
|
|
69
|
+
if (key !== "default") target[key] = source[key];
|
|
70
|
+
}
|
|
71
|
+
}`;
|
|
49
72
|
|
|
50
73
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* @
|
|
74
|
+
* Bundle an entry module and its local imports into one script.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} options
|
|
77
|
+
* @param {string} options.entry - Module id of the entry point
|
|
78
|
+
* @param {(id: string) => string | Promise<string>} options.load - Return a module's JS source
|
|
79
|
+
* @param {(specifier: string, fromId: string) => string} options.resolve - Resolve a relative specifier
|
|
80
|
+
* @param {"hoist"|"error"} [options.onExternal] - Bare (package) imports: hoist to the
|
|
81
|
+
* bundle top (needs an ESM host, e.g. Node) or throw (e.g. the browser REPL)
|
|
82
|
+
* @param {boolean} [options.exposeEntryFunctions] - Also export the entry's top-level
|
|
83
|
+
* function declarations (REPL convenience for code without exports)
|
|
84
|
+
* @returns {Promise<{code: string, entryId: string, entryExports: string[], externalBindings: Set<string>}>}
|
|
85
|
+
* `code` defines __ono_modules/__ono_require and ends by evaluating the
|
|
86
|
+
* entry into `__ono_entry`. The caller decides how to expose it.
|
|
54
87
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
export async function bundle(options) {
|
|
89
|
+
const { entry, load, resolve, onExternal = "hoist", exposeEntryFunctions = false } = options;
|
|
90
|
+
|
|
91
|
+
const moduleCodes = new Map();
|
|
92
|
+
const externalImports = [];
|
|
93
|
+
const externalBindings = new Set();
|
|
94
|
+
const entryExports = [];
|
|
95
|
+
const addEntryExport = (name) => {
|
|
96
|
+
if (!entryExports.includes(name)) entryExports.push(name);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const queue = [entry];
|
|
100
|
+
while (queue.length > 0) {
|
|
101
|
+
const id = queue.shift();
|
|
102
|
+
if (moduleCodes.has(id)) continue;
|
|
103
|
+
|
|
104
|
+
const source = await load(id);
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = parseModule(source);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(`${error.message} (in ${id})`);
|
|
66
110
|
}
|
|
67
111
|
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
const isEntry = id === entry;
|
|
113
|
+
const edits = [];
|
|
114
|
+
// Function declarations are hoisted, so their export assignments go at
|
|
115
|
+
// the top of the factory — this keeps them visible across import cycles,
|
|
116
|
+
// mirroring ESM hoisting. Everything else is assigned at the end.
|
|
117
|
+
const head = [];
|
|
118
|
+
const tail = [];
|
|
119
|
+
|
|
120
|
+
for (const imp of parsed.imports) {
|
|
121
|
+
if (isRelative(imp.specifier)) {
|
|
122
|
+
const depId = resolve(imp.specifier, id);
|
|
123
|
+
queue.push(depId);
|
|
124
|
+
const requireCall = `__ono_require(${JSON.stringify(depId)})`;
|
|
125
|
+
edits.push({ start: imp.start, end: imp.end, text: importReplacement(imp, requireCall) });
|
|
126
|
+
} else if (onExternal === "hoist") {
|
|
127
|
+
// Package import: move it to the top of the bundle, outside the factories
|
|
128
|
+
const statement = source.slice(imp.start, imp.end).trim();
|
|
129
|
+
if (!externalImports.includes(statement)) externalImports.push(statement);
|
|
130
|
+
collectBindings(imp, externalBindings);
|
|
131
|
+
edits.push({ start: imp.start, end: imp.end, text: "" });
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`Cannot bundle package import "${imp.specifier}" (in ${id})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
70
136
|
|
|
71
|
-
|
|
72
|
-
|
|
137
|
+
for (const exp of parsed.exports) {
|
|
138
|
+
switch (exp.type) {
|
|
139
|
+
case "exportStarFrom": {
|
|
140
|
+
if (!isRelative(exp.specifier)) {
|
|
141
|
+
throw new Error(`"export * from" a package is not supported (in ${id})`);
|
|
142
|
+
}
|
|
143
|
+
const depId = resolve(exp.specifier, id);
|
|
144
|
+
queue.push(depId);
|
|
145
|
+
const requireCall = `__ono_require(${JSON.stringify(depId)})`;
|
|
146
|
+
const text = exp.alias
|
|
147
|
+
? `__ono_exports[${JSON.stringify(exp.alias)}] = ${requireCall};`
|
|
148
|
+
: `__ono_export_star(__ono_exports, ${requireCall});`;
|
|
149
|
+
edits.push({ start: exp.start, end: exp.end, text });
|
|
150
|
+
if (isEntry && exp.alias) addEntryExport(exp.alias);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "exportNamedFrom": {
|
|
154
|
+
if (!isRelative(exp.specifier)) {
|
|
155
|
+
throw new Error(`Re-exporting from a package is not supported (in ${id})`);
|
|
156
|
+
}
|
|
157
|
+
const depId = resolve(exp.specifier, id);
|
|
158
|
+
queue.push(depId);
|
|
159
|
+
const requireCall = `__ono_require(${JSON.stringify(depId)})`;
|
|
160
|
+
const text = exp.named
|
|
161
|
+
.map(
|
|
162
|
+
({ local, exported }) =>
|
|
163
|
+
`__ono_exports[${JSON.stringify(exported)}] = ${requireCall}[${JSON.stringify(local)}];`,
|
|
164
|
+
)
|
|
165
|
+
.join(" ");
|
|
166
|
+
edits.push({ start: exp.start, end: exp.end, text });
|
|
167
|
+
if (isEntry) exp.named.forEach(({ exported }) => addEntryExport(exported));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "exportNamed": {
|
|
171
|
+
edits.push({ start: exp.start, end: exp.end, text: "" });
|
|
172
|
+
for (const { local, exported } of exp.named) {
|
|
173
|
+
tail.push(`__ono_exports[${JSON.stringify(exported)}] = ${local};`);
|
|
174
|
+
if (isEntry) addEntryExport(exported);
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "exportDefaultDeclaration": {
|
|
179
|
+
edits.push({ start: exp.start, end: exp.headerEnd, text: "" });
|
|
180
|
+
const target = exp.declarationKind === "function" ? head : tail;
|
|
181
|
+
target.push(`__ono_exports.default = ${exp.name};`);
|
|
182
|
+
if (isEntry) addEntryExport("default");
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "exportDefaultExpression": {
|
|
186
|
+
edits.push({ start: exp.start, end: exp.headerEnd, text: "__ono_exports.default =" });
|
|
187
|
+
if (isEntry) addEntryExport("default");
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "exportDeclaration": {
|
|
191
|
+
edits.push({ start: exp.start, end: exp.headerEnd, text: "" });
|
|
192
|
+
const target = exp.declarationKind === "function" ? head : tail;
|
|
193
|
+
for (const name of exp.names) {
|
|
194
|
+
target.push(`__ono_exports[${JSON.stringify(name)}] = ${name};`);
|
|
195
|
+
if (isEntry) addEntryExport(name);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
73
201
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
202
|
+
if (exposeEntryFunctions && isEntry) {
|
|
203
|
+
for (const name of parsed.topLevelFunctions) {
|
|
204
|
+
if (!entryExports.includes(name)) {
|
|
205
|
+
tail.push(`__ono_exports[${JSON.stringify(name)}] = ${name};`);
|
|
206
|
+
addEntryExport(name);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let code = applyEdits(source, edits);
|
|
212
|
+
if (head.length > 0) code = `${head.join("\n")}\n${code}`;
|
|
213
|
+
if (tail.length > 0) code += `\n${tail.join("\n")}`;
|
|
214
|
+
moduleCodes.set(id, code);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const parts = [];
|
|
218
|
+
if (externalImports.length > 0) parts.push(externalImports.join("\n"));
|
|
219
|
+
parts.push("const __ono_modules = {};");
|
|
220
|
+
for (const [id, code] of moduleCodes) {
|
|
221
|
+
parts.push(`__ono_modules[${JSON.stringify(id)}] = function (__ono_exports, __ono_require) {\n${code}\n};`);
|
|
222
|
+
}
|
|
223
|
+
parts.push(LINKER_RUNTIME);
|
|
224
|
+
parts.push(`const __ono_entry = __ono_require(${JSON.stringify(entry)});`);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
code: parts.join("\n\n"),
|
|
228
|
+
entryId: entry,
|
|
229
|
+
entryExports: entryExports.filter((name) => name === "default" || isIdentifierName(name)),
|
|
230
|
+
externalBindings,
|
|
231
|
+
};
|
|
81
232
|
}
|
package/src/cli.js
CHANGED
|
@@ -3,191 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Ono CLI - Minimalist SSG framework
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { join, relative } from "node:path";
|
|
10
|
-
import { loadUnoConfig } from "./unocss.js";
|
|
11
|
-
import { createDevServer } from "./server.js";
|
|
12
|
-
import { buildFile, buildFiles, generateUnoCSS } from "./builder.js";
|
|
13
|
-
import { watchFile, watchFiles, createWebSocketServer } from "./watcher.js";
|
|
6
|
+
import { runBuildCommand } from "./commands/build.js";
|
|
7
|
+
import { runDevCommand } from "./commands/dev.js";
|
|
8
|
+
import { DIRS, PORTS } from "./constants.js";
|
|
14
9
|
|
|
15
10
|
const args = process.argv.slice(2);
|
|
16
11
|
const command = args[0];
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!existsSync(publicDir)) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function copyRecursive(src, dest) {
|
|
27
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
28
|
-
|
|
29
|
-
for (const entry of entries) {
|
|
30
|
-
const srcPath = join(src, entry.name);
|
|
31
|
-
const destPath = join(dest, entry.name);
|
|
32
|
-
|
|
33
|
-
if (entry.isDirectory()) {
|
|
34
|
-
await mkdir(destPath, { recursive: true });
|
|
35
|
-
await copyRecursive(srcPath, destPath);
|
|
36
|
-
} else {
|
|
37
|
-
await mkdir(dest, { recursive: true });
|
|
38
|
-
await copyFile(srcPath, destPath);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await copyRecursive(publicDir, outDir);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function runBuildCommand() {
|
|
47
|
-
// Check for help flag
|
|
48
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
49
|
-
console.log(`
|
|
50
|
-
Usage: ono build [input] [options]
|
|
51
|
-
|
|
52
|
-
Build JSX files to static HTML
|
|
53
|
-
|
|
54
|
-
Arguments:
|
|
55
|
-
input File or directory to build (default: pages)
|
|
56
|
-
|
|
57
|
-
Options:
|
|
58
|
-
--output <dir> Output directory (default: dist)
|
|
59
|
-
--help Show this help message
|
|
60
|
-
|
|
61
|
-
Examples:
|
|
62
|
-
ono build Build all pages in pages/ directory
|
|
63
|
-
ono build pages/index.jsx Build a single file
|
|
64
|
-
ono build --output build Build to build/ directory
|
|
65
|
-
`);
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Filter out options to get the input argument
|
|
70
|
-
const nonOptionArgs = args.slice(1).filter(arg => !arg.startsWith("--"));
|
|
71
|
-
const input = nonOptionArgs[0] || "pages";
|
|
72
|
-
const outputDir = args.includes("--output")
|
|
73
|
-
? args[args.indexOf("--output") + 1]
|
|
74
|
-
: "dist";
|
|
75
|
-
|
|
76
|
-
const unocssConfig = await loadUnoConfig();
|
|
77
|
-
|
|
78
|
-
// Check if input is a directory or a file
|
|
79
|
-
const inputPath = resolve(process.cwd(), input);
|
|
80
|
-
const inputStat = await stat(inputPath);
|
|
81
|
-
|
|
82
|
-
if (inputStat.isDirectory()) {
|
|
83
|
-
// Build all files in directory
|
|
84
|
-
await buildFiles(input, { outputDir, unocssConfig });
|
|
85
|
-
} else {
|
|
86
|
-
// Build single file
|
|
87
|
-
await buildFile(input, { outputDir, unocssConfig });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Copy public files
|
|
91
|
-
await copyPublicFiles(outputDir);
|
|
92
|
-
|
|
93
|
-
// Generate UnoCSS
|
|
94
|
-
await generateUnoCSS({ outputDir, unocssConfig });
|
|
95
|
-
|
|
96
|
-
console.log("\n✨ Build complete!");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
async function runDevCommand() {
|
|
102
|
-
const input = args[1] || "pages";
|
|
103
|
-
const port = args.includes("--port")
|
|
104
|
-
? parseInt(args[args.indexOf("--port") + 1])
|
|
105
|
-
: 3000;
|
|
106
|
-
const outputDir = args.includes("--output")
|
|
107
|
-
? args[args.indexOf("--output") + 1]
|
|
108
|
-
: "dist";
|
|
109
|
-
|
|
110
|
-
const unocssConfig = await loadUnoConfig();
|
|
111
|
-
|
|
112
|
-
// Initial build
|
|
113
|
-
const inputPath = resolve(process.cwd(), input);
|
|
114
|
-
const inputStat = await stat(inputPath);
|
|
115
|
-
|
|
116
|
-
if (inputStat.isDirectory()) {
|
|
117
|
-
await buildFiles(input, { outputDir, unocssConfig });
|
|
118
|
-
} else {
|
|
119
|
-
await buildFile(input, { outputDir, unocssConfig });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
await copyPublicFiles(outputDir);
|
|
123
|
-
await generateUnoCSS({ outputDir, unocssConfig });
|
|
124
|
-
|
|
125
|
-
// Create WebSocket server for live reload
|
|
126
|
-
const { wss, port: wsPort } = createWebSocketServer();
|
|
127
|
-
|
|
128
|
-
// Start dev server
|
|
129
|
-
const mode = inputStat.isDirectory() ? "pages" : "single";
|
|
130
|
-
const indexFile = inputStat.isFile()
|
|
131
|
-
? relative(
|
|
132
|
-
outputDir,
|
|
133
|
-
(await buildFile(input, { outputDir, unocssConfig, silent: true }))
|
|
134
|
-
.outputPath
|
|
135
|
-
)
|
|
136
|
-
: "index.html";
|
|
137
|
-
|
|
138
|
-
let serverPort = port;
|
|
139
|
-
try {
|
|
140
|
-
const { server, app, port: actualPort } = await createDevServer({
|
|
141
|
-
outputDir,
|
|
142
|
-
port,
|
|
143
|
-
mode,
|
|
144
|
-
indexFile,
|
|
145
|
-
});
|
|
146
|
-
serverPort = actualPort;
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (error.code === "EADDRINUSE") {
|
|
149
|
-
serverPort = port + 1;
|
|
150
|
-
console.log(
|
|
151
|
-
`ℹ️ Port ${port} is busy, using port ${serverPort} instead`
|
|
152
|
-
);
|
|
153
|
-
await createDevServer({
|
|
154
|
-
outputDir,
|
|
155
|
-
port: serverPort,
|
|
156
|
-
mode,
|
|
157
|
-
indexFile,
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Watch for changes
|
|
165
|
-
if (inputStat.isDirectory()) {
|
|
166
|
-
await watchFiles(input, {
|
|
167
|
-
outputDir,
|
|
168
|
-
unocssConfig,
|
|
169
|
-
wss,
|
|
170
|
-
onRebuild: async () => {
|
|
171
|
-
await copyPublicFiles(outputDir);
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
} else {
|
|
175
|
-
await watchFile(input, {
|
|
176
|
-
outputDir,
|
|
177
|
-
unocssConfig,
|
|
178
|
-
wss,
|
|
179
|
-
onRebuild: async () => {
|
|
180
|
-
await copyPublicFiles(outputDir);
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
console.log(`\n🚀 Server running at http://localhost:${serverPort}`);
|
|
186
|
-
console.log(`📝 Serving: ${input}/ → ${outputDir}/`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Show main help message
|
|
15
|
+
*/
|
|
191
16
|
function showHelp() {
|
|
192
17
|
console.log(`
|
|
193
18
|
Ono - Minimalist SSG Framework
|
|
@@ -197,39 +22,43 @@ Usage:
|
|
|
197
22
|
|
|
198
23
|
Commands:
|
|
199
24
|
build [input] Build JSX files to static HTML
|
|
200
|
-
input: file or directory (default:
|
|
25
|
+
input: file or directory (default: ${DIRS.PAGES})
|
|
201
26
|
|
|
202
27
|
dev [input] Build, watch, and serve with live reload
|
|
203
|
-
input: file or directory (default:
|
|
28
|
+
input: file or directory (default: ${DIRS.PAGES})
|
|
204
29
|
|
|
205
30
|
Options:
|
|
206
|
-
--output <dir> Output directory (default:
|
|
207
|
-
--port <number> Server port (default:
|
|
31
|
+
--output <dir> Output directory (default: ${DIRS.OUTPUT})
|
|
32
|
+
--port <number> Server port (default: ${PORTS.SERVER})
|
|
208
33
|
--help Show this help message
|
|
209
34
|
|
|
210
35
|
Examples:
|
|
211
|
-
ono build Build all pages in
|
|
36
|
+
ono build Build all pages in ${DIRS.PAGES}/ directory
|
|
212
37
|
ono build pages/index.jsx Build a single file
|
|
213
38
|
ono dev Start dev server with live reload
|
|
214
39
|
ono dev --port 8080 Start dev server on port 8080
|
|
215
40
|
`);
|
|
216
41
|
}
|
|
217
42
|
|
|
218
|
-
|
|
219
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Main CLI entry point
|
|
45
|
+
*/
|
|
46
|
+
async function main() {
|
|
220
47
|
try {
|
|
221
48
|
if (!command || command === "--help" || command === "-h") {
|
|
222
49
|
showHelp();
|
|
223
50
|
process.exit(0);
|
|
224
51
|
}
|
|
225
52
|
|
|
53
|
+
const commandArgs = args.slice(1);
|
|
54
|
+
|
|
226
55
|
switch (command) {
|
|
227
56
|
case "build":
|
|
228
|
-
await runBuildCommand();
|
|
57
|
+
await runBuildCommand(commandArgs);
|
|
229
58
|
break;
|
|
230
59
|
|
|
231
60
|
case "dev":
|
|
232
|
-
await runDevCommand();
|
|
61
|
+
await runDevCommand(commandArgs);
|
|
233
62
|
break;
|
|
234
63
|
|
|
235
64
|
default:
|
|
@@ -244,4 +73,6 @@ Examples:
|
|
|
244
73
|
}
|
|
245
74
|
process.exit(1);
|
|
246
75
|
}
|
|
247
|
-
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build command for Ono CLI
|
|
3
|
+
*/
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
import { copyFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { buildFile, buildFiles, generateUnoCSS } from "../builder.js";
|
|
9
|
+
import { generateBarrels } from "../barrels.js";
|
|
10
|
+
import { DIRS, PORTS } from "../constants.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse arguments shared by the build and dev commands
|
|
14
|
+
* @param {string[]} args - Command line arguments
|
|
15
|
+
* @returns {{ input: string, outputDir: string, port: number, showHelp: boolean }}
|
|
16
|
+
*/
|
|
17
|
+
export function parseCommandArgs(args) {
|
|
18
|
+
const { values, positionals } = parseArgs({
|
|
19
|
+
args,
|
|
20
|
+
options: {
|
|
21
|
+
output: { type: "string", default: DIRS.OUTPUT },
|
|
22
|
+
port: { type: "string", default: String(PORTS.SERVER) },
|
|
23
|
+
help: { type: "boolean", short: "h", default: false },
|
|
24
|
+
},
|
|
25
|
+
allowPositionals: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
input: positionals[0] || DIRS.PAGES,
|
|
30
|
+
outputDir: values.output,
|
|
31
|
+
port: Number.parseInt(values.port, 10),
|
|
32
|
+
showHelp: values.help,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Show build command help
|
|
38
|
+
*/
|
|
39
|
+
export function showBuildHelp() {
|
|
40
|
+
console.log(`
|
|
41
|
+
Usage: ono build [input] [options]
|
|
42
|
+
|
|
43
|
+
Build JSX files to static HTML
|
|
44
|
+
|
|
45
|
+
Arguments:
|
|
46
|
+
input File or directory to build (default: ${DIRS.PAGES})
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--output <dir> Output directory (default: ${DIRS.OUTPUT})
|
|
50
|
+
--help Show this help message
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
ono build Build all pages in ${DIRS.PAGES}/ directory
|
|
54
|
+
ono build pages/index.jsx Build a single file
|
|
55
|
+
ono build --output build Build to build/ directory
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Copy public files to output directory
|
|
61
|
+
* @param {string} outputDir - Output directory path
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export async function copyPublicFiles(outputDir = DIRS.OUTPUT) {
|
|
65
|
+
const publicDir = resolve(process.cwd(), DIRS.PUBLIC);
|
|
66
|
+
const outDir = resolve(process.cwd(), outputDir);
|
|
67
|
+
|
|
68
|
+
if (!existsSync(publicDir)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} src
|
|
74
|
+
* @param {string} dest
|
|
75
|
+
*/
|
|
76
|
+
async function copyRecursive(src, dest) {
|
|
77
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const srcPath = join(src, entry.name);
|
|
81
|
+
const destPath = join(dest, entry.name);
|
|
82
|
+
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
await mkdir(destPath, { recursive: true });
|
|
85
|
+
await copyRecursive(srcPath, destPath);
|
|
86
|
+
} else {
|
|
87
|
+
await mkdir(dest, { recursive: true });
|
|
88
|
+
await copyFile(srcPath, destPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await copyRecursive(publicDir, outDir);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize barrels if barrels directory exists
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
100
|
+
export async function initializeBarrels() {
|
|
101
|
+
const barrelsDir = resolve(process.cwd(), DIRS.BARRELS);
|
|
102
|
+
if (existsSync(barrelsDir)) {
|
|
103
|
+
console.log("Generating barrel files...");
|
|
104
|
+
await generateBarrels(barrelsDir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run the build command
|
|
110
|
+
* @param {string[]} args - Command line arguments (after 'build')
|
|
111
|
+
* @returns {Promise<void>}
|
|
112
|
+
*/
|
|
113
|
+
export async function runBuildCommand(args) {
|
|
114
|
+
const { input, outputDir, showHelp } = parseCommandArgs(args);
|
|
115
|
+
|
|
116
|
+
if (showHelp) {
|
|
117
|
+
showBuildHelp();
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Generate barrel files if barrels directory exists
|
|
122
|
+
await initializeBarrels();
|
|
123
|
+
|
|
124
|
+
// Check if input is a directory or a file
|
|
125
|
+
const inputPath = resolve(process.cwd(), input);
|
|
126
|
+
const inputStat = await stat(inputPath);
|
|
127
|
+
|
|
128
|
+
if (inputStat.isDirectory()) {
|
|
129
|
+
await buildFiles(input, { outputDir });
|
|
130
|
+
} else {
|
|
131
|
+
await buildFile(input, { outputDir });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Copy public files
|
|
135
|
+
await copyPublicFiles(outputDir);
|
|
136
|
+
|
|
137
|
+
// Generate UnoCSS
|
|
138
|
+
await generateUnoCSS({ outputDir });
|
|
139
|
+
|
|
140
|
+
console.log("\n✨ Build complete!");
|
|
141
|
+
}
|