@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/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
- * Bundler - Bundle JSX files into a single executable module
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
- import fs from "node:fs/promises";
6
- import { collectDependencies } from "./resolver.js";
7
- import { transformJSX } from "./transformer.js";
21
+ const isRelative = (specifier) =>
22
+ specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
8
23
 
9
- /**
10
- * Bundle a JSX file and all its dependencies
11
- * @param {string} entryFile - Absolute path to entry file
12
- * @returns {Promise<string>} Bundled JavaScript code
13
- */
14
- export async function bundle(entryFile) {
15
- // Collect all dependencies
16
- const { order } = await collectDependencies(entryFile);
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
- modules.push({
38
- path: filePath,
39
- code: withoutExports,
40
- isEntry
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
- // Combine all modules into one
45
- const bundledCode = modules.map(m => m.code).join("\n\n");
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
- return bundledCode;
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
- * Remove import statements from code (but keep package imports)
52
- * @param {string} code - JavaScript code
53
- * @returns {string} Code with relative imports removed, package imports kept
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 removeImports(code) {
56
- // Remove only relative import statements, keep package imports
57
- const lines = code.split("\n");
58
- const filteredLines = lines.filter(line => {
59
- const trimmed = line.trim();
60
- if (!trimmed.startsWith("import ")) return true;
61
-
62
- // Keep package imports (not starting with . or /)
63
- const importMatch = trimmed.match(/from\s+['"]([^'"]+)['"]/);
64
- if (importMatch && !importMatch[1].startsWith('.') && !importMatch[1].startsWith('/')) {
65
- return true; // Keep package import
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
- return false; // Remove relative import
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
- return filteredLines.join("\n");
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
- * Remove export default from code
76
- * @param {string} code - JavaScript code
77
- * @returns {string} Code without export default
78
- */
79
- function removeExportDefault(code) {
80
- return code.replace(/export\s+default\s+/g, "");
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 { resolve } from "node:path";
7
- import { copyFile, mkdir, readdir, stat } from "node:fs/promises";
8
- import { existsSync } from "node:fs";
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
- async function copyPublicFiles(outputDir = "dist") {
19
- const publicDir = resolve(process.cwd(), "public");
20
- const outDir = resolve(process.cwd(), outputDir);
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: pages)
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: pages)
28
+ input: file or directory (default: ${DIRS.PAGES})
204
29
 
205
30
  Options:
206
- --output <dir> Output directory (default: dist)
207
- --port <number> Server port (default: 3000)
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 pages/ directory
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
- // Main CLI handler
219
- (async () => {
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
+ }