@ionify/ionify 0.1.1 → 0.1.2

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/dist/cli/index.js CHANGED
@@ -1,4 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ computeGraphVersion,
4
+ ensureNativeGraph,
5
+ getCacheKey,
6
+ native,
7
+ readCache,
8
+ tryBundleNodeModule,
9
+ tryNativeTransform,
10
+ writeCache
11
+ } from "../chunk-GOZUBOYH.js";
12
+ import {
13
+ getCasArtifactPath
14
+ } from "../chunk-X5UIMJDA.js";
2
15
 
3
16
  // src/cli/index.ts
4
17
  import { Command } from "commander";
@@ -8,83 +21,391 @@ import chalk from "chalk";
8
21
  function logInfo(message) {
9
22
  console.log(chalk.cyan(`[Ionify] ${message}`));
10
23
  }
24
+ function logWarn(message) {
25
+ console.warn(chalk.yellow(`[Ionify] ${message}`));
26
+ }
11
27
  function logError(message, err) {
12
28
  console.error(chalk.red(`[Ionify] ${message}`));
13
29
  if (err) console.error(err);
14
30
  }
15
31
 
16
- // src/native/index.ts
32
+ // src/cli/commands/dev.ts
33
+ import http from "http";
34
+ import url from "url";
35
+ import fs7 from "fs";
36
+ import path9 from "path";
37
+ import { fileURLToPath as fileURLToPath2 } from "url";
38
+ import { createRequire as createRequire2 } from "module";
39
+
40
+ // src/core/graph.ts
17
41
  import fs from "fs";
18
42
  import path from "path";
19
- import { createRequire } from "module";
20
-
21
- // src/core/version.ts
22
- import { createHash } from "crypto";
23
-
24
- // src/native/index.ts
25
- function resolveCandidates() {
26
- const cwd = process.cwd();
27
- const releaseDir = path.resolve(cwd, "target", "release");
28
- const debugDir = path.resolve(cwd, "target", "debug");
29
- const nativeDir = path.resolve(cwd, "native");
30
- const moduleDir = path.dirname(new URL(import.meta.url).pathname);
31
- const packageNativeDir = path.resolve(moduleDir, "..", "native");
32
- const packageDistDir = path.resolve(moduleDir, "..");
33
- const platformFile = process.platform === "win32" ? "ionify_core.dll" : process.platform === "darwin" ? "libionify_core.dylib" : "libionify_core.so";
34
- const candidates = [
35
- // Installed package locations (checked first)
36
- path.join(packageDistDir, "ionify_core.node"),
37
- path.join(packageNativeDir, "ionify_core.node"),
38
- // Development locations
39
- path.join(nativeDir, "ionify_core.node"),
40
- path.join(releaseDir, "ionify_core.node"),
41
- path.join(releaseDir, platformFile),
42
- path.join(debugDir, "ionify_core.node"),
43
- path.join(debugDir, platformFile)
44
- ];
45
- return candidates.filter((candidate) => {
43
+ var IONIFY_DIR = path.join(process.cwd(), ".ionify");
44
+ var GRAPH_FILE = path.join(IONIFY_DIR, "graph.json");
45
+ var GRAPH_DB_FILE = path.join(IONIFY_DIR, "graph.db");
46
+ function ensureIonifyDir() {
47
+ if (!fs.existsSync(IONIFY_DIR)) fs.mkdirSync(IONIFY_DIR, { recursive: true });
48
+ }
49
+ var Graph = class {
50
+ nodes = /* @__PURE__ */ new Map();
51
+ dirty = false;
52
+ saveTimer = null;
53
+ native = native ?? null;
54
+ nativeFlushTimer = null;
55
+ queueSave() {
56
+ if (this.native) return;
57
+ this.dirty = true;
58
+ if (this.saveTimer) return;
59
+ this.saveTimer = setTimeout(() => this.save(), 300);
60
+ }
61
+ constructor(versionInputs) {
62
+ ensureIonifyDir();
63
+ if (this.native) {
64
+ const version = versionInputs ? computeGraphVersion(versionInputs) : void 0;
65
+ ensureNativeGraph(GRAPH_DB_FILE, version);
66
+ }
67
+ this.load();
68
+ }
69
+ load() {
70
+ if (this.native) {
71
+ try {
72
+ const snapshot = this.native.graphLoad();
73
+ for (const node of snapshot) {
74
+ const stat = fs.existsSync(node.id) ? fs.statSync(node.id) : null;
75
+ this.nodes.set(node.id, {
76
+ id: node.id,
77
+ hash: node.hash,
78
+ deps: node.deps,
79
+ dynamicDeps: node.dynamicDeps,
80
+ kind: node.kind,
81
+ configHash: node.config_hash ?? node.configHash ?? null,
82
+ mtimeMs: stat ? stat.mtimeMs : null
83
+ });
84
+ }
85
+ } catch {
86
+ this.loadFromDisk();
87
+ }
88
+ return;
89
+ }
90
+ this.loadFromDisk();
91
+ }
92
+ loadFromDisk() {
93
+ if (!fs.existsSync(GRAPH_FILE)) return;
46
94
  try {
47
- return fs.existsSync(candidate) && fs.statSync(candidate).isFile();
95
+ const raw = fs.readFileSync(GRAPH_FILE, "utf8");
96
+ const snap = JSON.parse(raw);
97
+ if (snap.version === 1 && snap.nodes) {
98
+ for (const [id, node] of Object.entries(snap.nodes)) {
99
+ this.nodes.set(id, node);
100
+ }
101
+ }
48
102
  } catch {
49
- return false;
50
103
  }
51
- });
52
- }
53
- var nativeBinding = null;
54
- (() => {
55
- const require2 = createRequire(import.meta.url);
56
- for (const candidate of resolveCandidates()) {
57
- try {
58
- const mod = require2(candidate);
59
- if (mod) {
60
- nativeBinding = mod;
61
- break;
104
+ }
105
+ scheduleNativeFlush() {
106
+ if (!this.native?.graphFlush) return;
107
+ if (this.nativeFlushTimer) return;
108
+ this.nativeFlushTimer = setTimeout(() => {
109
+ this.nativeFlushTimer = null;
110
+ try {
111
+ this.native?.graphFlush?.();
112
+ } catch {
62
113
  }
114
+ }, 250);
115
+ }
116
+ scheduleSave() {
117
+ if (this.native) return;
118
+ this.queueSave();
119
+ }
120
+ save() {
121
+ if (this.native) return;
122
+ try {
123
+ const snap = {
124
+ version: 1,
125
+ nodes: Object.fromEntries(this.nodes.entries())
126
+ };
127
+ fs.writeFileSync(GRAPH_FILE, JSON.stringify(snap, null, 2), "utf8");
128
+ this.dirty = false;
63
129
  } catch {
130
+ } finally {
131
+ if (this.saveTimer) {
132
+ clearTimeout(this.saveTimer);
133
+ this.saveTimer = null;
134
+ }
64
135
  }
65
136
  }
66
- })();
67
- var native = nativeBinding;
68
-
69
- // src/cli/utils/config.ts
70
- import fs2 from "fs";
71
- import path3 from "path";
72
- import { pathToFileURL } from "url";
73
- import { build } from "esbuild";
137
+ /** Upsert a node and its deps; returns true if hash changed */
138
+ recordFile(absPath, contentHash, depsAbs, dynamicDeps, kind) {
139
+ const stat = fs.existsSync(absPath) ? fs.statSync(absPath) : null;
140
+ const mtimeMs = stat ? stat.mtimeMs : null;
141
+ const configHash = process.env.IONIFY_CONFIG_HASH || null;
142
+ const prev = this.nodes.get(absPath);
143
+ let changed = !prev || prev.hash !== contentHash;
144
+ const node = {
145
+ id: absPath,
146
+ hash: contentHash,
147
+ deps: Array.from(new Set(depsAbs)),
148
+ dynamicDeps: dynamicDeps ? Array.from(new Set(dynamicDeps)) : void 0,
149
+ kind: kind || this.inferKind(absPath),
150
+ configHash,
151
+ mtimeMs
152
+ };
153
+ this.nodes.set(absPath, node);
154
+ if (this.native) {
155
+ try {
156
+ changed = this.native.graphRecord(
157
+ absPath,
158
+ contentHash,
159
+ node.deps,
160
+ node.dynamicDeps || [],
161
+ node.kind,
162
+ node.configHash ?? null
163
+ );
164
+ this.scheduleNativeFlush();
165
+ } catch (err) {
166
+ console.error(`[Graph] Failed to record ${absPath}:`, err);
167
+ }
168
+ }
169
+ this.scheduleSave();
170
+ return changed;
171
+ }
172
+ /** Infer module kind from file extension */
173
+ inferKind(absPath) {
174
+ const ext = path.extname(absPath).toLowerCase();
175
+ if (/\.(module)\.css$/i.test(absPath)) return "css-module";
176
+ if ([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"].includes(ext)) return "js";
177
+ if (ext === ".css") return "css";
178
+ if ([".json"].includes(ext)) return "json";
179
+ return "asset";
180
+ }
181
+ getNode(absPath) {
182
+ return this.nodes.get(absPath);
183
+ }
184
+ getDeps(absPath) {
185
+ return this.nodes.get(absPath)?.deps ?? [];
186
+ }
187
+ /** Reverse edges: who depends on target? */
188
+ getDependents(targetAbs) {
189
+ const candidates = /* @__PURE__ */ new Set();
190
+ if (this.native?.graphDependents) {
191
+ try {
192
+ for (const dep of this.native.graphDependents(targetAbs) ?? []) {
193
+ candidates.add(dep);
194
+ }
195
+ } catch {
196
+ }
197
+ }
198
+ for (const [id, node] of this.nodes) {
199
+ if (node.deps.includes(targetAbs)) candidates.add(id);
200
+ }
201
+ return Array.from(candidates);
202
+ }
203
+ /** Collect dependents recursively (breadth-first) */
204
+ collectDependentsDeep(targetAbs) {
205
+ const result = /* @__PURE__ */ new Set();
206
+ const queue = [targetAbs];
207
+ while (queue.length) {
208
+ const current = queue.shift();
209
+ for (const dep of this.getDependents(current)) {
210
+ if (!result.has(dep)) {
211
+ result.add(dep);
212
+ queue.push(dep);
213
+ }
214
+ }
215
+ }
216
+ return Array.from(result);
217
+ }
218
+ /** Includes changed files and all dependents */
219
+ collectAffected(changed) {
220
+ const result = /* @__PURE__ */ new Set();
221
+ let usedNative = false;
222
+ if (this.native?.graphCollectAffected) {
223
+ try {
224
+ const nativeList = this.native.graphCollectAffected(changed);
225
+ for (const item of nativeList ?? []) {
226
+ result.add(item);
227
+ }
228
+ usedNative = true;
229
+ } catch {
230
+ }
231
+ }
232
+ for (const target of changed) {
233
+ result.add(target);
234
+ }
235
+ if (!usedNative || result.size === 0) {
236
+ for (const target of changed) {
237
+ result.add(target);
238
+ for (const dep of this.collectDependentsDeep(target)) {
239
+ result.add(dep);
240
+ }
241
+ }
242
+ }
243
+ return Array.from(result);
244
+ }
245
+ /** Remove file from graph and clean up dependents lists */
246
+ removeFile(absPath) {
247
+ const existed = this.nodes.delete(absPath);
248
+ if (existed) {
249
+ for (const node of this.nodes.values()) {
250
+ if (node.deps.includes(absPath)) {
251
+ node.deps = node.deps.filter((dep) => dep !== absPath);
252
+ }
253
+ }
254
+ if (this.native) {
255
+ try {
256
+ this.native.graphRemove(absPath);
257
+ this.scheduleNativeFlush();
258
+ } catch {
259
+ }
260
+ }
261
+ this.queueSave();
262
+ }
263
+ }
264
+ /** Persist immediately (e.g., on shutdown) */
265
+ flush() {
266
+ if (this.nativeFlushTimer) {
267
+ clearTimeout(this.nativeFlushTimer);
268
+ this.nativeFlushTimer = null;
269
+ }
270
+ if (this.native?.graphFlush) {
271
+ try {
272
+ this.native.graphFlush();
273
+ } catch {
274
+ }
275
+ }
276
+ if (this.dirty) this.save();
277
+ }
278
+ };
74
279
 
75
280
  // src/core/resolver.ts
281
+ import fs2 from "fs";
76
282
  import path2 from "path";
77
- import { createRequire as createRequire2 } from "module";
283
+ import { createRequire } from "module";
284
+ import { pathToFileURL, fileURLToPath } from "url";
285
+ var SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
286
+ var CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
78
287
  var swc = null;
79
288
  (() => {
80
289
  try {
81
- const require2 = createRequire2(import.meta.url);
290
+ const require2 = createRequire(import.meta.url);
82
291
  swc = require2("@swc/core");
83
292
  } catch {
84
293
  swc = null;
85
294
  }
86
295
  })();
296
+ function extractImports(source, filename = "inline.ts") {
297
+ if (native?.parseModuleIr) {
298
+ try {
299
+ const result = native.parseModuleIr(filename, source);
300
+ return result.dependencies.map((dep) => dep.specifier);
301
+ } catch {
302
+ }
303
+ }
304
+ const deps = /* @__PURE__ */ new Set();
305
+ const fallbackRegex = () => {
306
+ const re = /(?:import\s+(?:[^'"]+\s+from\s+)??['"]([^'"]+)['"])|(?:export\s+[^'"]+\s+from\s+['"]([^'"]+)['"])|(?:import\s*?\(\s*?['"]([^'"]+)['"]\s*?\))/g;
307
+ let m;
308
+ while (m = re.exec(source)) {
309
+ const spec = m[1] || m[2] || m[3];
310
+ if (spec) deps.add(spec);
311
+ }
312
+ };
313
+ try {
314
+ const parseSync = swc?.parseSync;
315
+ if (parseSync) {
316
+ const ast = parseSync(source, {
317
+ filename,
318
+ isModule: true,
319
+ target: "es2022",
320
+ syntax: "typescript",
321
+ tsx: true,
322
+ decorators: true,
323
+ dynamicImport: true
324
+ });
325
+ const visit = (node) => {
326
+ if (!node || typeof node !== "object") return;
327
+ const anyNode = node;
328
+ const type = anyNode.type;
329
+ if (type === "ImportDeclaration" && anyNode.source && typeof anyNode.source.value === "string") {
330
+ deps.add(anyNode.source.value);
331
+ } else if (type === "ExportAllDeclaration" && anyNode.source && typeof anyNode.source.value === "string") {
332
+ deps.add(anyNode.source.value);
333
+ } else if (type === "ExportNamedDeclaration" && anyNode.source && typeof anyNode.source.value === "string") {
334
+ deps.add(anyNode.source.value);
335
+ } else if (type === "CallExpression") {
336
+ const callee = anyNode.callee ?? {};
337
+ if (callee.type === "Import") {
338
+ const args = anyNode.arguments ?? [];
339
+ const first = args[0];
340
+ if (first && typeof first === "object") {
341
+ const expr = first.expression;
342
+ if (expr && expr.type === "StringLiteral" && typeof expr.value === "string") {
343
+ deps.add(expr.value);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ for (const value of Object.values(anyNode)) {
349
+ if (!value) continue;
350
+ if (Array.isArray(value)) {
351
+ for (const item of value) visit(item);
352
+ } else if (typeof value === "object") {
353
+ visit(value);
354
+ }
355
+ }
356
+ };
357
+ visit(ast);
358
+ } else {
359
+ fallbackRegex();
360
+ }
361
+ } catch {
362
+ fallbackRegex();
363
+ }
364
+ if (!deps.size) {
365
+ fallbackRegex();
366
+ }
367
+ return Array.from(deps);
368
+ }
369
+ function tryFile(p) {
370
+ if (fs2.existsSync(p) && fs2.statSync(p).isFile()) return p;
371
+ return null;
372
+ }
373
+ function tryWithExt(p) {
374
+ if (tryFile(p)) return p;
375
+ for (const ext of SUPPORTED_EXTS) {
376
+ const cand = p.endsWith(ext) ? p : p + ext;
377
+ const found = tryFile(cand);
378
+ if (found) return found;
379
+ }
380
+ if (fs2.existsSync(p) && fs2.statSync(p).isDirectory()) {
381
+ const pkgPath = path2.join(p, "package.json");
382
+ if (fs2.existsSync(pkgPath)) {
383
+ try {
384
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
385
+ if (pkg.main) {
386
+ const mainPath = path2.join(p, pkg.main);
387
+ const mainResolved = tryFile(mainPath) || tryWithExt(mainPath);
388
+ if (mainResolved) return mainResolved;
389
+ }
390
+ if (pkg.module) {
391
+ const modulePath = path2.join(p, pkg.module);
392
+ const moduleResolved = tryFile(modulePath) || tryWithExt(modulePath);
393
+ if (moduleResolved) return moduleResolved;
394
+ }
395
+ } catch {
396
+ }
397
+ }
398
+ for (const ext of SUPPORTED_EXTS) {
399
+ const idx = path2.join(p, "index" + ext);
400
+ const found = tryFile(idx);
401
+ if (found) return found;
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+ var cachedTsconfigAliases;
87
407
  var customAliasEntries = [];
408
+ var resolvePathCache = /* @__PURE__ */ new Map();
88
409
  function createAliasEntry(pattern, targets) {
89
410
  const hasWildcard = pattern.includes("*");
90
411
  if (hasWildcard) {
@@ -134,11 +455,988 @@ function buildAliasEntries(aliases, baseDir) {
134
455
  }
135
456
  return entries;
136
457
  }
458
+ function loadTsconfigAliases() {
459
+ if (cachedTsconfigAliases !== void 0) {
460
+ return cachedTsconfigAliases ?? [];
461
+ }
462
+ const rootDir = process.cwd();
463
+ for (const configName of CONFIG_FILES) {
464
+ const candidate = path2.resolve(rootDir, configName);
465
+ if (!fs2.existsSync(candidate) || !fs2.statSync(candidate).isFile()) {
466
+ continue;
467
+ }
468
+ try {
469
+ const raw = fs2.readFileSync(candidate, "utf8");
470
+ const parsed = JSON.parse(raw);
471
+ const compilerOptions = parsed?.compilerOptions ?? {};
472
+ const baseUrl = compilerOptions.baseUrl ? path2.resolve(path2.dirname(candidate), compilerOptions.baseUrl) : path2.dirname(candidate);
473
+ const paths = compilerOptions.paths ?? {};
474
+ cachedTsconfigAliases = buildAliasEntries(paths, baseUrl);
475
+ return cachedTsconfigAliases;
476
+ } catch {
477
+ }
478
+ }
479
+ cachedTsconfigAliases = [];
480
+ return cachedTsconfigAliases;
481
+ }
482
+ function resolveFromEntries(entries, specifier) {
483
+ for (const entry of entries) {
484
+ const candidates = entry.resolveCandidates(specifier);
485
+ for (const candidate of candidates) {
486
+ const resolved = tryWithExt(candidate);
487
+ if (resolved) return resolved;
488
+ }
489
+ }
490
+ return null;
491
+ }
492
+ function resolveWithAliases(specifier) {
493
+ const custom = resolveFromEntries(customAliasEntries, specifier);
494
+ if (custom) return custom;
495
+ const tsconfigEntries = loadTsconfigAliases();
496
+ return resolveFromEntries(tsconfigEntries, specifier);
497
+ }
137
498
  function configureResolverAliases(aliases, baseDir) {
138
499
  customAliasEntries = aliases ? buildAliasEntries(aliases, baseDir) : [];
139
500
  }
501
+ function resolveImport(specifier, importerAbs) {
502
+ const cacheKey = `${importerAbs}\0${specifier}`;
503
+ if (resolvePathCache.has(cacheKey)) {
504
+ return resolvePathCache.get(cacheKey) ?? null;
505
+ }
506
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
507
+ const aliasResolved = resolveWithAliases(specifier);
508
+ if (aliasResolved) {
509
+ resolvePathCache.set(cacheKey, aliasResolved);
510
+ return aliasResolved;
511
+ }
512
+ try {
513
+ const require2 = createRequire(importerAbs);
514
+ const resolved2 = require2.resolve(specifier);
515
+ resolvePathCache.set(cacheKey, resolved2);
516
+ return resolved2;
517
+ } catch {
518
+ try {
519
+ const importerUrl = pathToFileURL(importerAbs).href;
520
+ const resolvedUrl = import.meta.resolve(specifier, importerUrl);
521
+ if (resolvedUrl.startsWith("file://")) {
522
+ const resolved2 = fileURLToPath(resolvedUrl);
523
+ resolvePathCache.set(cacheKey, resolved2);
524
+ return resolved2;
525
+ }
526
+ resolvePathCache.set(cacheKey, resolvedUrl);
527
+ return resolvedUrl;
528
+ } catch {
529
+ const nodeModulesPath = path2.join(path2.dirname(importerAbs), "node_modules", specifier);
530
+ const resolvedNodeModules = tryWithExt(nodeModulesPath);
531
+ if (resolvedNodeModules) {
532
+ resolvePathCache.set(cacheKey, resolvedNodeModules);
533
+ return resolvedNodeModules;
534
+ }
535
+ const srcPath = path2.join(process.cwd(), "src", specifier);
536
+ const resolvedSrc = tryWithExt(srcPath);
537
+ if (resolvedSrc) {
538
+ resolvePathCache.set(cacheKey, resolvedSrc);
539
+ return resolvedSrc;
540
+ }
541
+ const rootPath = path2.join(process.cwd(), specifier);
542
+ const resolvedRoot = tryWithExt(rootPath);
543
+ if (resolvedRoot) {
544
+ resolvePathCache.set(cacheKey, resolvedRoot);
545
+ return resolvedRoot;
546
+ }
547
+ resolvePathCache.set(cacheKey, null);
548
+ return null;
549
+ }
550
+ }
551
+ }
552
+ const baseDir = path2.dirname(importerAbs);
553
+ const target = path2.resolve(baseDir, specifier);
554
+ const resolved = tryWithExt(target);
555
+ resolvePathCache.set(cacheKey, resolved);
556
+ return resolved;
557
+ }
558
+ function resolveImports(specs, importerAbs) {
559
+ const abs = specs.map((s) => resolveImport(s, importerAbs)).filter((x) => !!x);
560
+ return Array.from(new Set(abs));
561
+ }
562
+
563
+ // src/core/resolver/module-resolver.ts
564
+ import path3 from "path";
565
+ import fs3 from "fs";
566
+ var DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".mjs"];
567
+ var DEFAULT_CONDITIONS = ["import", "default"];
568
+ var DEFAULT_MAIN_FIELDS = ["module", "main"];
569
+ var ModuleResolver = class {
570
+ options;
571
+ rootDir;
572
+ constructor(rootDir, options = {}) {
573
+ this.rootDir = rootDir;
574
+ this.options = {
575
+ baseUrl: options.baseUrl || ".",
576
+ paths: options.paths || {},
577
+ extensions: options.extensions || DEFAULT_EXTENSIONS,
578
+ alias: options.alias || {},
579
+ conditions: options.conditions || DEFAULT_CONDITIONS,
580
+ mainFields: options.mainFields || DEFAULT_MAIN_FIELDS
581
+ };
582
+ }
583
+ resolve(importSpecifier, importer) {
584
+ if (path3.isAbsolute(importSpecifier)) {
585
+ return this.tryResolveFile(importSpecifier);
586
+ }
587
+ const aliasResolved = this.resolveAlias(importSpecifier);
588
+ if (aliasResolved) {
589
+ return this.tryResolveFile(aliasResolved);
590
+ }
591
+ if (importSpecifier.startsWith(".")) {
592
+ const resolvedPath = path3.resolve(path3.dirname(importer), importSpecifier);
593
+ return this.tryResolveFile(resolvedPath);
594
+ }
595
+ return this.resolveBareModule(importSpecifier, importer);
596
+ }
597
+ resolveAlias(specifier) {
598
+ for (const [alias, target] of Object.entries(this.options.alias)) {
599
+ if (specifier === alias || specifier.startsWith(`${alias}/`)) {
600
+ const relativePath = specifier.slice(alias.length);
601
+ const targets = Array.isArray(target) ? target : [target];
602
+ for (const t of targets) {
603
+ const resolved = path3.join(this.rootDir, t, relativePath);
604
+ if (fs3.existsSync(resolved)) {
605
+ return resolved;
606
+ }
607
+ }
608
+ }
609
+ }
610
+ for (const [pattern, targets] of Object.entries(this.options.paths)) {
611
+ const wildcardIndex = pattern.indexOf("*");
612
+ if (wildcardIndex === -1) {
613
+ if (specifier === pattern) {
614
+ return path3.join(this.rootDir, this.options.baseUrl, targets[0]);
615
+ }
616
+ } else {
617
+ const prefix = pattern.slice(0, wildcardIndex);
618
+ const suffix = pattern.slice(wildcardIndex + 1);
619
+ if (specifier.startsWith(prefix) && specifier.endsWith(suffix)) {
620
+ const matchedPortion = specifier.slice(prefix.length, -suffix.length || void 0);
621
+ for (const target of targets) {
622
+ const resolved = path3.join(
623
+ this.rootDir,
624
+ this.options.baseUrl,
625
+ target.replace("*", matchedPortion)
626
+ );
627
+ if (fs3.existsSync(resolved)) {
628
+ return resolved;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ }
634
+ return null;
635
+ }
636
+ resolveBareModule(specifier, importer) {
637
+ const parts = specifier.split("/");
638
+ const packageName = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
639
+ const subpath = parts.slice(packageName.startsWith("@") ? 2 : 1).join("/");
640
+ let dir = path3.dirname(importer);
641
+ while (dir !== "/") {
642
+ const nodeModulesPath = path3.join(dir, "node_modules", packageName);
643
+ if (fs3.existsSync(nodeModulesPath)) {
644
+ if (subpath) {
645
+ return this.tryResolveFile(path3.join(nodeModulesPath, subpath));
646
+ }
647
+ return this.resolvePackageMain(nodeModulesPath);
648
+ }
649
+ dir = path3.dirname(dir);
650
+ }
651
+ return null;
652
+ }
653
+ resolvePackageMain(packageDir) {
654
+ const pkgJsonPath = path3.join(packageDir, "package.json");
655
+ if (fs3.existsSync(pkgJsonPath)) {
656
+ try {
657
+ const pkg = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
658
+ if (pkg.exports) {
659
+ const resolved = this.resolveExports(pkg.exports, packageDir);
660
+ if (resolved) return resolved;
661
+ }
662
+ for (const field of this.options.mainFields) {
663
+ if (pkg[field]) {
664
+ const resolved = this.tryResolveFile(path3.join(packageDir, pkg[field]));
665
+ if (resolved) return resolved;
666
+ }
667
+ }
668
+ } catch {
669
+ }
670
+ }
671
+ return this.tryResolveFile(path3.join(packageDir, "index"));
672
+ }
673
+ resolveExports(exports, packageDir) {
674
+ if (typeof exports === "string") {
675
+ return this.tryResolveFile(path3.join(packageDir, exports));
676
+ }
677
+ if (Array.isArray(exports)) {
678
+ for (const exp of exports) {
679
+ const resolved = this.resolveExports(exp, packageDir);
680
+ if (resolved) return resolved;
681
+ }
682
+ return null;
683
+ }
684
+ if (typeof exports === "object") {
685
+ for (const condition of this.options.conditions) {
686
+ if (condition in exports) {
687
+ const resolved = this.resolveExports(exports[condition], packageDir);
688
+ if (resolved) return resolved;
689
+ }
690
+ }
691
+ if ("default" in exports) {
692
+ return this.resolveExports(exports.default, packageDir);
693
+ }
694
+ }
695
+ return null;
696
+ }
697
+ tryResolveFile(filepath) {
698
+ if (fs3.existsSync(filepath) && fs3.statSync(filepath).isFile()) {
699
+ return filepath;
700
+ }
701
+ for (const ext of this.options.extensions) {
702
+ const withExt = `${filepath}${ext}`;
703
+ if (fs3.existsSync(withExt) && fs3.statSync(withExt).isFile()) {
704
+ return withExt;
705
+ }
706
+ }
707
+ if (fs3.existsSync(filepath) && fs3.statSync(filepath).isDirectory()) {
708
+ for (const ext of this.options.extensions) {
709
+ const indexFile = path3.join(filepath, `index${ext}`);
710
+ if (fs3.existsSync(indexFile) && fs3.statSync(indexFile).isFile()) {
711
+ return indexFile;
712
+ }
713
+ }
714
+ }
715
+ return null;
716
+ }
717
+ };
718
+
719
+ // src/core/watcher.ts
720
+ import fs4 from "fs";
721
+ import path4 from "path";
722
+ import { EventEmitter } from "events";
723
+ var IonifyWatcher = class extends EventEmitter {
724
+ constructor(rootDir) {
725
+ super();
726
+ this.rootDir = rootDir;
727
+ }
728
+ watchers = /* @__PURE__ */ new Map();
729
+ debounce = /* @__PURE__ */ new Map();
730
+ polled = /* @__PURE__ */ new Set();
731
+ watchFile(filePath) {
732
+ const abs = path4.resolve(filePath);
733
+ if (this.watchers.has(abs)) return;
734
+ if (/(node_modules|\.git|\.ionify|dist)/.test(abs)) return;
735
+ if (!fs4.existsSync(abs)) return;
736
+ try {
737
+ const dir = path4.dirname(abs);
738
+ const watcher = fs4.watch(dir, (event, filename) => {
739
+ if (!filename) return;
740
+ const full = path4.join(dir, filename.toString());
741
+ if (full !== abs) return;
742
+ const now = Date.now();
743
+ const last = this.debounce.get(abs) || 0;
744
+ if (now - last < 100) return;
745
+ this.debounce.set(abs, now);
746
+ const exists = fs4.existsSync(abs);
747
+ this.emit("change", abs, exists ? "changed" : "deleted");
748
+ });
749
+ this.watchers.set(abs, watcher);
750
+ this.polled.add(abs);
751
+ fs4.watchFile(abs, { interval: 5e3 }, (curr, prev) => {
752
+ if (curr.mtimeMs !== prev.mtimeMs) {
753
+ this.emit("change", abs, "changed");
754
+ }
755
+ });
756
+ } catch {
757
+ this.polled.add(abs);
758
+ fs4.watchFile(abs, { interval: 8e3 }, (curr, prev) => {
759
+ if (curr.mtimeMs !== prev.mtimeMs) {
760
+ this.emit("change", abs, "changed");
761
+ }
762
+ });
763
+ }
764
+ }
765
+ unwatchFile(filePath) {
766
+ const abs = path4.resolve(filePath);
767
+ const watcher = this.watchers.get(abs);
768
+ if (watcher) watcher.close();
769
+ fs4.unwatchFile(abs);
770
+ this.watchers.delete(abs);
771
+ this.polled.delete(abs);
772
+ }
773
+ closeAll() {
774
+ for (const [abs, w] of this.watchers) {
775
+ w.close();
776
+ fs4.unwatchFile(abs);
777
+ }
778
+ this.watchers.clear();
779
+ for (const abs of this.polled) {
780
+ fs4.unwatchFile(abs);
781
+ }
782
+ this.polled.clear();
783
+ }
784
+ };
785
+
786
+ // src/core/transform.ts
787
+ var TransformCache = class {
788
+ store = /* @__PURE__ */ new Map();
789
+ hits = 0;
790
+ misses = 0;
791
+ maxEntries;
792
+ constructor(maxEntries) {
793
+ const envMax = process.env.IONIFY_DEV_TRANSFORM_CACHE_MAX;
794
+ const parsedEnv = envMax ? parseInt(envMax, 10) : NaN;
795
+ this.maxEntries = Number.isFinite(parsedEnv) ? parsedEnv : maxEntries ?? 5e3;
796
+ }
797
+ setMaxEntries(maxEntries) {
798
+ this.maxEntries = maxEntries;
799
+ this.prune();
800
+ }
801
+ get(key) {
802
+ const entry = this.store.get(key);
803
+ if (entry) {
804
+ this.hits += 1;
805
+ entry.timestamp = Date.now();
806
+ return entry;
807
+ }
808
+ this.misses += 1;
809
+ return null;
810
+ }
811
+ set(key, entry) {
812
+ this.store.set(key, { ...entry, timestamp: Date.now() });
813
+ this.prune();
814
+ }
815
+ prune(maxEntries) {
816
+ const limit = maxEntries ?? this.maxEntries;
817
+ if (this.store.size <= limit) return;
818
+ const sorted = Array.from(this.store.entries()).sort(
819
+ (a, b) => a[1].timestamp - b[1].timestamp
820
+ );
821
+ const removeCount = this.store.size - limit;
822
+ for (let i = 0; i < removeCount; i++) {
823
+ this.store.delete(sorted[i][0]);
824
+ }
825
+ }
826
+ metrics() {
827
+ return {
828
+ hits: this.hits,
829
+ misses: this.misses,
830
+ size: this.store.size,
831
+ max: this.maxEntries
832
+ };
833
+ }
834
+ };
835
+ var transformCache = new TransformCache();
836
+ var TransformEngine = class {
837
+ loaders = [];
838
+ cacheEnabled;
839
+ cacheVersion = "v1";
840
+ casRoot;
841
+ versionHash;
842
+ constructor(options) {
843
+ this.cacheEnabled = options?.cache ?? true;
844
+ this.casRoot = options?.casRoot;
845
+ this.versionHash = options?.versionHash;
846
+ }
847
+ useLoader(loader) {
848
+ this.loaders.push(loader);
849
+ this.loaders.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
850
+ }
851
+ async run(ctx) {
852
+ const { getCacheKey: getCacheKey2 } = await import("../cache-Y4NMRSZO.js");
853
+ const path13 = await import("path");
854
+ const fs11 = await import("fs");
855
+ const { getCasArtifactPath: getCasArtifactPath2 } = await import("../cas-FEOXFD7R.js");
856
+ const moduleHash = ctx.moduleHash || getCacheKey2(ctx.code);
857
+ const loaderSig = this.loaders.map((l) => l.name || "loader").join("|");
858
+ const loaderHash = getCacheKey2(loaderSig);
859
+ const memKey = `${moduleHash}-${loaderHash}`;
860
+ const casDir = this.casRoot && this.versionHash ? getCasArtifactPath2(this.casRoot, this.versionHash, moduleHash) : null;
861
+ const casFile = casDir ? path13.join(casDir, "transformed.js") : null;
862
+ const casMapFile = casDir ? path13.join(casDir, "transformed.js.map") : null;
863
+ const debug = process.env.IONIFY_DEV_TRANSFORM_CACHE_DEBUG === "1";
864
+ if (this.cacheEnabled) {
865
+ const memHit = transformCache.get(memKey);
866
+ if (memHit) {
867
+ if (debug) {
868
+ console.log(`[Dev Cache] HIT mem key=${memKey} size=${transformCache.metrics().size}`);
869
+ }
870
+ return { code: memHit.transformed, map: memHit.map };
871
+ }
872
+ if (casFile && fs11.existsSync(casFile)) {
873
+ try {
874
+ const code = fs11.readFileSync(casFile, "utf8");
875
+ const map = casMapFile && fs11.existsSync(casMapFile) ? fs11.readFileSync(casMapFile, "utf8") : void 0;
876
+ const parsed = { code, map };
877
+ transformCache.set(memKey, {
878
+ hash: moduleHash,
879
+ loaderHash,
880
+ transformed: parsed.code,
881
+ map: parsed.map,
882
+ timestamp: Date.now()
883
+ });
884
+ if (debug) {
885
+ console.log(`[Dev Cache] HIT cas key=${memKey} size=${transformCache.metrics().size}`);
886
+ }
887
+ return parsed;
888
+ } catch {
889
+ }
890
+ }
891
+ }
892
+ let working = { ...ctx };
893
+ let result = { code: ctx.code };
894
+ for (const loader of this.loaders) {
895
+ if (!loader.test(working)) continue;
896
+ const output = await loader.transform({ ...working, code: result.code });
897
+ if (output && output.code !== void 0) {
898
+ result = { ...result, ...output };
899
+ working = { ...working, code: result.code };
900
+ }
901
+ }
902
+ if (this.cacheEnabled) {
903
+ transformCache.set(memKey, {
904
+ hash: moduleHash,
905
+ loaderHash,
906
+ transformed: result.code,
907
+ map: result.map,
908
+ timestamp: Date.now()
909
+ });
910
+ if (casFile) {
911
+ try {
912
+ fs11.mkdirSync(path13.dirname(casFile), { recursive: true });
913
+ fs11.writeFileSync(casFile, result.code, "utf8");
914
+ if (result.map && casMapFile) {
915
+ fs11.writeFileSync(casMapFile, typeof result.map === "string" ? result.map : JSON.stringify(result.map), "utf8");
916
+ }
917
+ } catch {
918
+ }
919
+ }
920
+ if (debug) {
921
+ const m = transformCache.metrics();
922
+ console.log(`[Dev Cache] MISS stored key=${memKey} size=${m.size} hits=${m.hits} misses=${m.misses}`);
923
+ }
924
+ }
925
+ return result;
926
+ }
927
+ };
928
+
929
+ // src/core/hmr.ts
930
+ var HMRServer = class {
931
+ clients = /* @__PURE__ */ new Set();
932
+ pending = /* @__PURE__ */ new Map();
933
+ nextId = 1;
934
+ closed = false;
935
+ /** Handle an incoming SSE subscription request */
936
+ handleSSE(req, res) {
937
+ if (this.closed) {
938
+ res.writeHead(503);
939
+ res.end();
940
+ return;
941
+ }
942
+ res.writeHead(200, {
943
+ "Content-Type": "text/event-stream",
944
+ "Cache-Control": "no-cache, no-transform",
945
+ Connection: "keep-alive",
946
+ "X-Accel-Buffering": "no"
947
+ });
948
+ res.write(`event: ready
949
+ data: "ok"
950
+
951
+ `);
952
+ this.clients.add(res);
953
+ req.on("close", () => {
954
+ this.clients.delete(res);
955
+ try {
956
+ res.end();
957
+ } catch {
958
+ }
959
+ });
960
+ }
961
+ send(event, payload) {
962
+ const data = (event ? `event: ${event}
963
+ ` : "") + `data: ${JSON.stringify(payload)}
964
+
965
+ `;
966
+ for (const client of this.clients) {
967
+ try {
968
+ client.write(data);
969
+ } catch {
970
+ }
971
+ }
972
+ }
973
+ /** Broadcast a JSON event to all SSE clients */
974
+ broadcast(payload) {
975
+ this.send(null, payload);
976
+ }
977
+ broadcastEvent(event, payload) {
978
+ this.send(event, payload);
979
+ }
980
+ queueUpdate(modules) {
981
+ if (!modules.length) return null;
982
+ const timestamp = Date.now();
983
+ const id = `${timestamp}-${this.nextId++}`;
984
+ const summary = {
985
+ type: "update",
986
+ id,
987
+ timestamp,
988
+ modules: modules.map(({ url: url2, hash, reason }) => ({ url: url2, hash, reason }))
989
+ };
990
+ this.pending.set(id, { summary, modules, createdAt: timestamp });
991
+ this.broadcastEvent("update", summary);
992
+ return summary;
993
+ }
994
+ consumeUpdate(id) {
995
+ const pending = this.pending.get(id);
996
+ if (pending) {
997
+ this.pending.delete(id);
998
+ }
999
+ return pending;
1000
+ }
1001
+ broadcastError(payload) {
1002
+ this.broadcastEvent("error", { type: "error", ...payload });
1003
+ }
1004
+ close() {
1005
+ this.closed = true;
1006
+ for (const client of this.clients) {
1007
+ try {
1008
+ client.end();
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ this.clients.clear();
1013
+ this.pending.clear();
1014
+ }
1015
+ };
1016
+ function injectHMRClient(html) {
1017
+ const tag = `<script type="module" src="/__ionify_hmr_client.js"></script>`;
1018
+ return html.includes("</body>") ? html.replace("</body>", `${tag}
1019
+ </body>`) : html + "\n" + tag;
1020
+ }
1021
+
1022
+ // src/core/loaders/css.ts
1023
+ import path5 from "path";
1024
+ import crypto from "crypto";
1025
+ import postcss from "postcss";
1026
+ import postcssLoadConfig from "postcss-load-config";
1027
+ import postcssModules from "postcss-modules";
1028
+ var cachedConfig = null;
1029
+ var configFailed = false;
1030
+ async function getPostcssConfig(rootDir) {
1031
+ if (cachedConfig) return cachedConfig;
1032
+ if (configFailed) return { plugins: [], options: {} };
1033
+ try {
1034
+ const result = await postcssLoadConfig({}, rootDir);
1035
+ cachedConfig = {
1036
+ plugins: Array.isArray(result.plugins) ? result.plugins : [],
1037
+ options: result.options ?? {}
1038
+ };
1039
+ } catch {
1040
+ configFailed = true;
1041
+ cachedConfig = { plugins: [], options: {} };
1042
+ }
1043
+ return cachedConfig;
1044
+ }
1045
+ async function compileCss({
1046
+ code,
1047
+ filePath,
1048
+ rootDir,
1049
+ modules = false
1050
+ }) {
1051
+ const loaderHash = getCacheKey(JSON.stringify({ modules, filePath: filePath.replace(/\\+/g, "/") }));
1052
+ const contentHash = getCacheKey(code);
1053
+ const cacheKey = `${contentHash}-${loaderHash}`;
1054
+ const cached = transformCache.get(cacheKey);
1055
+ if (cached) {
1056
+ try {
1057
+ const parsed = JSON.parse(cached.transformed);
1058
+ return parsed;
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ const { plugins, options } = await getPostcssConfig(rootDir);
1063
+ const pipeline = [...plugins];
1064
+ let tokens;
1065
+ if (modules) {
1066
+ const scopedName = (name, filename) => {
1067
+ const relative = path5.relative(rootDir, filename || filePath).replace(/\\+/g, "/");
1068
+ const seed = crypto.createHash("sha1").update(relative).digest("hex").slice(0, 6);
1069
+ return `${name}___${seed}`;
1070
+ };
1071
+ pipeline.push(
1072
+ postcssModules({
1073
+ generateScopedName: scopedName,
1074
+ getJSON(_filename, json) {
1075
+ tokens = json;
1076
+ }
1077
+ })
1078
+ );
1079
+ }
1080
+ const runner = postcss(pipeline);
1081
+ const result = await runner.process(code, {
1082
+ ...options,
1083
+ from: filePath,
1084
+ map: false
1085
+ });
1086
+ const compiled = {
1087
+ css: result.css,
1088
+ tokens
1089
+ };
1090
+ transformCache.set(cacheKey, {
1091
+ hash: contentHash,
1092
+ loaderHash,
1093
+ transformed: JSON.stringify(compiled),
1094
+ timestamp: Date.now()
1095
+ });
1096
+ return compiled;
1097
+ }
1098
+ function renderCssModule({
1099
+ css,
1100
+ filePath,
1101
+ tokens
1102
+ }) {
1103
+ const cssJson = JSON.stringify(css);
1104
+ const styleId = `ionify-css-${getCacheKey(filePath).slice(0, 8)}`;
1105
+ const tokensJson = tokens ? JSON.stringify(tokens) : "null";
1106
+ return `
1107
+ const cssText = ${cssJson};
1108
+ const styleId = ${JSON.stringify(styleId)};
1109
+ let style = document.querySelector(\`style[data-ionify-id="\${styleId}"]\`);
1110
+ if (!style) {
1111
+ style = document.createElement("style");
1112
+ style.setAttribute("data-ionify-id", styleId);
1113
+ document.head.appendChild(style);
1114
+ }
1115
+ style.textContent = cssText;
1116
+ ${tokens ? `const tokens = ${tokensJson};` : ""}
1117
+ export const css = cssText;
1118
+ ${tokens ? `export const classes = tokens;
1119
+ export default tokens;` : `export default cssText;`}
1120
+ if (import.meta.hot) {
1121
+ import.meta.hot.accept();
1122
+ import.meta.hot.dispose(() => {
1123
+ const existing = document.querySelector(\`style[data-ionify-id="\${styleId}"]\`);
1124
+ if (existing) existing.remove();
1125
+ });
1126
+ }
1127
+ `.trim();
1128
+ }
1129
+
1130
+ // src/core/utils/public-path.ts
1131
+ import path6 from "path";
1132
+ var MODULE_PREFIX = "/__ionify__/modules/";
1133
+ function publicPathForFile(rootDir, absPath) {
1134
+ const normalizedRoot = path6.resolve(rootDir);
1135
+ const normalizedFile = path6.resolve(absPath);
1136
+ if (normalizedFile.startsWith(normalizedRoot + path6.sep) || normalizedFile === normalizedRoot) {
1137
+ const relative = path6.relative(normalizedRoot, normalizedFile).split(path6.sep).join("/");
1138
+ return "/" + (relative.length ? relative : "");
1139
+ }
1140
+ const encoded = Buffer.from(normalizedFile).toString("base64url");
1141
+ return MODULE_PREFIX + encoded;
1142
+ }
1143
+ function decodePublicPath(rootDir, urlPath) {
1144
+ if (urlPath.startsWith(MODULE_PREFIX)) {
1145
+ const encoded = urlPath.slice(MODULE_PREFIX.length);
1146
+ try {
1147
+ const decoded = Buffer.from(encoded, "base64url").toString("utf8");
1148
+ return path6.resolve(decoded);
1149
+ } catch {
1150
+ return null;
1151
+ }
1152
+ }
1153
+ const normalizedRoot = path6.resolve(rootDir);
1154
+ const joined = path6.resolve(normalizedRoot, "." + urlPath);
1155
+ if (!joined.startsWith(normalizedRoot + path6.sep) && joined !== normalizedRoot) {
1156
+ return null;
1157
+ }
1158
+ return joined;
1159
+ }
1160
+
1161
+ // src/core/loaders/asset.ts
1162
+ function assetAsModule(urlPath) {
1163
+ const safe = urlPath.replace(/"/g, "%22");
1164
+ return `export default "${safe}";`;
1165
+ }
1166
+ function isAssetExt(ext) {
1167
+ return [
1168
+ ".png",
1169
+ ".jpg",
1170
+ ".jpeg",
1171
+ ".gif",
1172
+ ".svg",
1173
+ ".ico",
1174
+ ".webp",
1175
+ ".avif",
1176
+ ".woff",
1177
+ ".woff2",
1178
+ ".ttf",
1179
+ ".otf",
1180
+ ".eot"
1181
+ ].includes(ext);
1182
+ }
1183
+ function contentTypeForAsset(ext) {
1184
+ switch (ext) {
1185
+ case ".png":
1186
+ return "image/png";
1187
+ case ".jpg":
1188
+ case ".jpeg":
1189
+ return "image/jpeg";
1190
+ case ".gif":
1191
+ return "image/gif";
1192
+ case ".svg":
1193
+ return "image/svg+xml";
1194
+ case ".ico":
1195
+ return "image/x-icon";
1196
+ case ".webp":
1197
+ return "image/webp";
1198
+ case ".avif":
1199
+ return "image/avif";
1200
+ case ".woff":
1201
+ return "font/woff";
1202
+ case ".woff2":
1203
+ return "font/woff2";
1204
+ case ".ttf":
1205
+ return "font/ttf";
1206
+ case ".otf":
1207
+ return "font/otf";
1208
+ case ".eot":
1209
+ return "application/vnd.ms-fontobject";
1210
+ default:
1211
+ return "application/octet-stream";
1212
+ }
1213
+ }
1214
+ function normalizeUrlFromFs(rootDir, fsPath) {
1215
+ return publicPathForFile(rootDir, fsPath);
1216
+ }
1217
+
1218
+ // src/core/loaders/js.ts
1219
+ import { transform as swcTransform } from "@swc/core";
1220
+ import { init, parse } from "es-module-lexer";
1221
+ var JS_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1222
+ function needsReactRefresh(ext) {
1223
+ if (ext === ".jsx" || ext === ".tsx") return true;
1224
+ if (!ext.endsWith("x")) return false;
1225
+ return false;
1226
+ }
1227
+ function shouldTransform(ext, filePath) {
1228
+ if (!JS_EXTENSIONS.has(ext)) return false;
1229
+ if (filePath.endsWith(".d.ts")) return false;
1230
+ return true;
1231
+ }
1232
+ async function swcTranspile(code, filePath, ext, reactRefresh) {
1233
+ const isTypeScript = ext === ".ts" || ext === ".tsx";
1234
+ const isTsx = ext === ".tsx";
1235
+ const isJsx = ext === ".jsx";
1236
+ const swcParser = isTypeScript ? {
1237
+ syntax: "typescript",
1238
+ tsx: isTsx,
1239
+ decorators: true,
1240
+ dynamicImport: true
1241
+ } : {
1242
+ syntax: "ecmascript",
1243
+ jsx: isJsx,
1244
+ decorators: true,
1245
+ dynamicImport: true
1246
+ };
1247
+ const result = await swcTransform(code, {
1248
+ filename: filePath,
1249
+ jsc: {
1250
+ parser: swcParser,
1251
+ target: "es2022",
1252
+ transform: reactRefresh ? {
1253
+ react: {
1254
+ development: true,
1255
+ refresh: true,
1256
+ runtime: "automatic"
1257
+ }
1258
+ } : void 0
1259
+ },
1260
+ sourceMaps: false,
1261
+ module: {
1262
+ type: "es6"
1263
+ }
1264
+ });
1265
+ return result.code ?? code;
1266
+ }
1267
+ function currentMode() {
1268
+ const mode = (process.env.IONIFY_PARSER || "hybrid").toLowerCase();
1269
+ if (mode === "swc") return "swc";
1270
+ if (mode === "oxc") return "oxc";
1271
+ return "hybrid";
1272
+ }
1273
+ var jsLoader = {
1274
+ name: "js",
1275
+ order: 0,
1276
+ test: ({ ext, path: filePath }) => shouldTransform(ext, filePath),
1277
+ transform: async ({ path: filePath, code, ext }) => {
1278
+ const isNodeModules = filePath.includes("node_modules");
1279
+ let output = code;
1280
+ if (isNodeModules) {
1281
+ const bundled = tryBundleNodeModule(filePath, code);
1282
+ if (bundled) {
1283
+ output = bundled;
1284
+ } else {
1285
+ output = code;
1286
+ }
1287
+ } else {
1288
+ const reactRefresh = needsReactRefresh(ext);
1289
+ const mode = currentMode();
1290
+ const nativeResult = tryNativeTransform(mode, code, {
1291
+ filename: filePath,
1292
+ jsx: ext === ".jsx" || ext === ".tsx",
1293
+ typescript: ext === ".ts" || ext === ".tsx",
1294
+ react_refresh: reactRefresh
1295
+ });
1296
+ if (nativeResult) {
1297
+ output = nativeResult.code ?? code;
1298
+ } else {
1299
+ output = await swcTranspile(code, filePath, ext, reactRefresh);
1300
+ }
1301
+ if (reactRefresh) {
1302
+ const prologue = `import { setupReactRefresh } from "/__ionify_react_refresh.js";
1303
+ const __ionifyRefresh__ = setupReactRefresh(import.meta.hot, import.meta.url);
1304
+ `;
1305
+ const epilogue = `
1306
+ __ionifyRefresh__?.finalize?.();
1307
+
1308
+ if (import.meta.hot) {
1309
+ import.meta.hot.accept((newModule) => {
1310
+ __ionifyRefresh__?.refresh?.(newModule);
1311
+ });
1312
+ import.meta.hot.dispose(() => {
1313
+ __ionifyRefresh__?.dispose?.();
1314
+ });
1315
+ }
1316
+ `;
1317
+ output = prologue + output + epilogue;
1318
+ } else {
1319
+ output += `
1320
+ if (import.meta.hot) {
1321
+ import.meta.hot.accept();
1322
+ }
1323
+ `;
1324
+ }
1325
+ }
1326
+ await init;
1327
+ const [imports] = parse(output);
1328
+ if (imports.length) {
1329
+ const rootDir = process.cwd();
1330
+ let rewritten = "";
1331
+ let lastIndex = 0;
1332
+ let mutated = false;
1333
+ for (const record of imports) {
1334
+ if (!record.n) continue;
1335
+ const spec = record.n;
1336
+ if (spec.startsWith("http://") || spec.startsWith("https://") || spec.startsWith(MODULE_PREFIX)) {
1337
+ continue;
1338
+ }
1339
+ let pathPart = spec;
1340
+ let suffix = "";
1341
+ const queryIndex = spec.indexOf("?");
1342
+ const hashIndex = spec.indexOf("#");
1343
+ const splitIndex = queryIndex === -1 ? hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
1344
+ if (splitIndex !== -1) {
1345
+ pathPart = spec.slice(0, splitIndex);
1346
+ suffix = spec.slice(splitIndex);
1347
+ }
1348
+ const resolved = resolveImport(pathPart, filePath);
1349
+ if (!resolved) continue;
1350
+ const resolvedExt = resolved.slice(resolved.lastIndexOf("."));
1351
+ let augmentedSuffix = suffix;
1352
+ if (resolvedExt === ".css" && !suffix) {
1353
+ augmentedSuffix = "?inline";
1354
+ }
1355
+ const assetExts = [
1356
+ ".png",
1357
+ ".jpg",
1358
+ ".jpeg",
1359
+ ".gif",
1360
+ ".svg",
1361
+ ".ico",
1362
+ ".webp",
1363
+ ".avif",
1364
+ ".woff",
1365
+ ".woff2",
1366
+ ".ttf",
1367
+ ".otf",
1368
+ ".eot"
1369
+ ];
1370
+ if (assetExts.includes(resolvedExt) && !suffix) {
1371
+ augmentedSuffix = "?import";
1372
+ }
1373
+ const replacementPath = publicPathForFile(rootDir, resolved);
1374
+ const replacement = replacementPath + augmentedSuffix;
1375
+ if (replacement === spec) continue;
1376
+ if (!mutated) {
1377
+ mutated = true;
1378
+ }
1379
+ if (record.t === 2) {
1380
+ rewritten += output.slice(lastIndex, record.s + 1);
1381
+ rewritten += replacement;
1382
+ rewritten += output[record.e - 1];
1383
+ lastIndex = record.e;
1384
+ } else {
1385
+ rewritten += output.slice(lastIndex, record.s);
1386
+ rewritten += replacement;
1387
+ lastIndex = record.e;
1388
+ }
1389
+ }
1390
+ if (mutated) {
1391
+ rewritten += output.slice(lastIndex);
1392
+ output = rewritten;
1393
+ }
1394
+ }
1395
+ return { code: output };
1396
+ }
1397
+ };
1398
+
1399
+ // src/core/loaders/registry.ts
1400
+ var registry = [];
1401
+ function registerLoader(registration) {
1402
+ registry.push(registration);
1403
+ }
1404
+ async function applyRegisteredLoaders(engine, config) {
1405
+ for (const registration of registry) {
1406
+ await registration(engine, config ?? null);
1407
+ }
1408
+ if (config?.plugins) {
1409
+ for (const plugin of config.plugins) {
1410
+ if (plugin.loaders) {
1411
+ for (const loader of plugin.loaders) {
1412
+ engine.useLoader(loader);
1413
+ }
1414
+ }
1415
+ if (plugin.setup) {
1416
+ const context = {
1417
+ registerLoader: (loader) => {
1418
+ engine.useLoader(loader);
1419
+ }
1420
+ };
1421
+ await plugin.setup(context);
1422
+ }
1423
+ }
1424
+ }
1425
+ if (config?.loaders) {
1426
+ for (const loader of config.loaders) {
1427
+ engine.useLoader(loader);
1428
+ }
1429
+ }
1430
+ }
1431
+ registerLoader((engine) => {
1432
+ engine.useLoader(jsLoader);
1433
+ });
140
1434
 
141
1435
  // src/cli/utils/config.ts
1436
+ import fs5 from "fs";
1437
+ import path7 from "path";
1438
+ import { pathToFileURL as pathToFileURL2 } from "url";
1439
+ import { build } from "esbuild";
142
1440
  var CONFIG_BASENAMES = [
143
1441
  "ionify.config.ts",
144
1442
  "ionify.config.mts",
@@ -146,10 +1444,10 @@ var CONFIG_BASENAMES = [
146
1444
  "ionify.config.mjs",
147
1445
  "ionify.config.cjs"
148
1446
  ];
149
- var cachedConfig = null;
1447
+ var cachedConfig2 = null;
150
1448
  var configLoaded = false;
151
1449
  async function bundleConfig(entry) {
152
- const absDir = path3.dirname(entry);
1450
+ const absDir = path7.dirname(entry);
153
1451
  const inlineIonifyPlugin = {
154
1452
  name: "inline-ionify",
155
1453
  setup(build2) {
@@ -183,7 +1481,7 @@ async function bundleConfig(entry) {
183
1481
  if (!output) throw new Error("Failed to bundle ionify config");
184
1482
  const dirnameLiteral = JSON.stringify(absDir);
185
1483
  const filenameLiteral = JSON.stringify(entry);
186
- const importMetaLiteral = JSON.stringify(pathToFileURL(entry).href);
1484
+ const importMetaLiteral = JSON.stringify(pathToFileURL2(entry).href);
187
1485
  let contents = output.text;
188
1486
  if (contents.includes("import.meta.url")) {
189
1487
  contents = contents.replace(/import\.meta\.url/g, "__IONIFY_IMPORT_META_URL");
@@ -197,21 +1495,21 @@ const __filename = ${filenameLiteral};
197
1495
  }
198
1496
  function findConfigFile(cwd) {
199
1497
  for (const name of CONFIG_BASENAMES) {
200
- const candidate = path3.resolve(cwd, name);
201
- if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
1498
+ const candidate = path7.resolve(cwd, name);
1499
+ if (fs5.existsSync(candidate) && fs5.statSync(candidate).isFile()) {
202
1500
  return candidate;
203
1501
  }
204
1502
  }
205
1503
  return null;
206
1504
  }
207
1505
  async function loadIonifyConfig(cwd = process.cwd()) {
208
- if (configLoaded) return cachedConfig;
1506
+ if (configLoaded) return cachedConfig2;
209
1507
  configLoaded = true;
210
1508
  const configPath = findConfigFile(cwd);
211
1509
  if (!configPath) {
212
- cachedConfig = null;
1510
+ cachedConfig2 = null;
213
1511
  configureResolverAliases(void 0, cwd);
214
- return cachedConfig;
1512
+ return cachedConfig2;
215
1513
  }
216
1514
  try {
217
1515
  const bundled = await bundleConfig(configPath);
@@ -221,78 +1519,926 @@ async function loadIonifyConfig(cwd = process.cwd()) {
221
1519
  if (resolved && typeof resolved === "function") {
222
1520
  resolved = resolved({ mode: process.env.NODE_ENV || "development" });
223
1521
  }
224
- if (resolved && typeof resolved?.then === "function") {
225
- resolved = await resolved;
1522
+ if (resolved && typeof resolved?.then === "function") {
1523
+ resolved = await resolved;
1524
+ }
1525
+ if (resolved && typeof resolved === "object") {
1526
+ cachedConfig2 = resolved;
1527
+ const baseDir = path7.dirname(configPath);
1528
+ const aliases = resolved?.resolve?.alias;
1529
+ if (aliases && typeof aliases === "object") {
1530
+ configureResolverAliases(aliases, baseDir);
1531
+ } else {
1532
+ configureResolverAliases(void 0, baseDir);
1533
+ }
1534
+ logInfo(`Loaded ionify config from ${path7.relative(cwd, configPath)}`);
1535
+ } else {
1536
+ throw new Error("Config did not export an object");
1537
+ }
1538
+ } catch (err) {
1539
+ logError("Failed to load ionify.config", err);
1540
+ cachedConfig2 = null;
1541
+ configureResolverAliases(void 0, cwd);
1542
+ }
1543
+ return cachedConfig2;
1544
+ }
1545
+
1546
+ // src/cli/utils/minifier.ts
1547
+ function normalize(value) {
1548
+ if (value === "oxc" || value === "swc" || value === "auto") return value;
1549
+ if (typeof value === "string") {
1550
+ const v = value.toLowerCase();
1551
+ if (v === "oxc" || v === "swc" || v === "auto") return v;
1552
+ }
1553
+ return null;
1554
+ }
1555
+ function resolveMinifier(config, opts = {}) {
1556
+ const fromCli = normalize(opts.cliFlag);
1557
+ if (fromCli) return fromCli;
1558
+ const fromEnv = normalize(opts.envVar);
1559
+ if (fromEnv) return fromEnv;
1560
+ const fromConfig = normalize(config?.minifier);
1561
+ if (fromConfig) return fromConfig;
1562
+ return "auto";
1563
+ }
1564
+ function applyMinifierEnv(choice) {
1565
+ process.env.IONIFY_MINIFIER = choice;
1566
+ }
1567
+
1568
+ // src/cli/utils/env.ts
1569
+ import fs6 from "fs";
1570
+ import path8 from "path";
1571
+ function parseValue(raw) {
1572
+ let value = raw.trim();
1573
+ if (!value) return "";
1574
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1575
+ value = value.slice(1, -1);
1576
+ }
1577
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
1578
+ return value;
1579
+ }
1580
+ function parseEnvFile(source) {
1581
+ const env = {};
1582
+ const lines = source.split(/\r?\n/);
1583
+ for (const line of lines) {
1584
+ const trimmed = line.trim();
1585
+ if (!trimmed || trimmed.startsWith("#")) continue;
1586
+ const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_\.]*)\s*=\s*(.*)$/);
1587
+ if (!match) continue;
1588
+ const [, key, rest] = match;
1589
+ env[key] = parseValue(rest);
1590
+ }
1591
+ return env;
1592
+ }
1593
+ function loadEnv(mode = "development", rootDir = process.cwd()) {
1594
+ const candidates = [
1595
+ ".env",
1596
+ ".env.local",
1597
+ `.env.${mode}`,
1598
+ `.env.${mode}.local`
1599
+ ];
1600
+ const merged = {};
1601
+ for (const name of candidates) {
1602
+ const filePath = path8.resolve(rootDir, name);
1603
+ if (!fs6.existsSync(filePath) || !fs6.statSync(filePath).isFile()) {
1604
+ continue;
1605
+ }
1606
+ const contents = fs6.readFileSync(filePath, "utf8");
1607
+ const parsed = parseEnvFile(contents);
1608
+ Object.assign(merged, parsed);
1609
+ }
1610
+ for (const [key, value] of Object.entries(merged)) {
1611
+ process.env[key] = value;
1612
+ }
1613
+ return {
1614
+ ...merged
1615
+ };
1616
+ }
1617
+
1618
+ // src/cli/utils/treeshake.ts
1619
+ var DEFAULT_RESOLUTION = {
1620
+ mode: "safe",
1621
+ include: [],
1622
+ exclude: []
1623
+ };
1624
+ function parseMode(value) {
1625
+ if (!value) return null;
1626
+ switch (value.toLowerCase()) {
1627
+ case "off":
1628
+ case "false":
1629
+ return "off";
1630
+ case "aggressive":
1631
+ return "aggressive";
1632
+ case "safe":
1633
+ case "true":
1634
+ return "safe";
1635
+ default:
1636
+ return null;
1637
+ }
1638
+ }
1639
+ function normalizeList(value) {
1640
+ if (!value) return [];
1641
+ if (Array.isArray(value)) {
1642
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
1643
+ }
1644
+ return [];
1645
+ }
1646
+ function parseEnvList(raw) {
1647
+ if (!raw || !raw.trim()) return null;
1648
+ try {
1649
+ const parsed = JSON.parse(raw);
1650
+ return normalizeList(parsed);
1651
+ } catch {
1652
+ return null;
1653
+ }
1654
+ }
1655
+ function extractConfigObject(value) {
1656
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1657
+ return value;
1658
+ }
1659
+ return null;
1660
+ }
1661
+ function resolveTreeshake(input, options = {}) {
1662
+ let resolved = { ...DEFAULT_RESOLUTION };
1663
+ const objectValue = extractConfigObject(input);
1664
+ if (objectValue) {
1665
+ resolved.include = normalizeList(objectValue.include);
1666
+ resolved.exclude = normalizeList(objectValue.exclude);
1667
+ if (objectValue.mode) {
1668
+ const objectMode = parseMode(objectValue.mode);
1669
+ if (objectMode) {
1670
+ resolved.mode = objectMode;
1671
+ }
1672
+ }
1673
+ } else if (typeof input === "boolean") {
1674
+ resolved.mode = input ? "safe" : "off";
1675
+ } else if (typeof input === "string") {
1676
+ resolved.mode = parseMode(input) ?? DEFAULT_RESOLUTION.mode;
1677
+ }
1678
+ const envMode = parseMode(options.envMode);
1679
+ if (envMode) {
1680
+ resolved.mode = envMode;
1681
+ }
1682
+ const includeOverride = parseEnvList(options.includeEnv);
1683
+ if (includeOverride) {
1684
+ resolved.include = includeOverride;
1685
+ }
1686
+ const excludeOverride = parseEnvList(options.excludeEnv);
1687
+ if (excludeOverride) {
1688
+ resolved.exclude = excludeOverride;
1689
+ }
1690
+ return resolved;
1691
+ }
1692
+ function applyTreeshakeEnv(resolved) {
1693
+ process.env.IONIFY_TREESHAKE = resolved.mode;
1694
+ process.env.IONIFY_TREESHAKE_INCLUDE = JSON.stringify(resolved.include);
1695
+ process.env.IONIFY_TREESHAKE_EXCLUDE = JSON.stringify(resolved.exclude);
1696
+ }
1697
+
1698
+ // src/cli/utils/scope-hoist.ts
1699
+ var DEFAULT_SCOPE_HOIST = {
1700
+ enable: true,
1701
+ inlineFunctions: true,
1702
+ constantFolding: true,
1703
+ combineVariables: true
1704
+ };
1705
+ function parseBool(value) {
1706
+ if (value === true || value === false) return value;
1707
+ if (typeof value === "string") {
1708
+ const normalized = value.toLowerCase();
1709
+ if (["true", "1", "yes", "on", "enable"].includes(normalized)) return true;
1710
+ if (["false", "0", "no", "off", "disable"].includes(normalized)) return false;
1711
+ }
1712
+ return null;
1713
+ }
1714
+ function parseEnvFlag(value) {
1715
+ if (!value) return null;
1716
+ return parseBool(value);
1717
+ }
1718
+ function resolveScopeHoist(configValue, options = {}) {
1719
+ let resolved = { ...DEFAULT_SCOPE_HOIST };
1720
+ const scopeConfig = configValue;
1721
+ if (typeof scopeConfig === "boolean") {
1722
+ resolved.enable = scopeConfig;
1723
+ } else if (scopeConfig && typeof scopeConfig === "object") {
1724
+ resolved.enable = true;
1725
+ if (scopeConfig.inlineFunctions !== void 0) {
1726
+ resolved.inlineFunctions = !!scopeConfig.inlineFunctions;
1727
+ }
1728
+ if (scopeConfig.constantFolding !== void 0) {
1729
+ resolved.constantFolding = !!scopeConfig.constantFolding;
1730
+ }
1731
+ if (scopeConfig.combineVariables !== void 0) {
1732
+ resolved.combineVariables = !!scopeConfig.combineVariables;
1733
+ }
1734
+ }
1735
+ const envMode = parseEnvFlag(options.envMode);
1736
+ if (envMode !== null) {
1737
+ resolved.enable = envMode;
1738
+ }
1739
+ const inlineEnv = parseEnvFlag(options.inlineEnv);
1740
+ if (inlineEnv !== null) {
1741
+ resolved.inlineFunctions = inlineEnv;
1742
+ } else if (!resolved.enable) {
1743
+ resolved.inlineFunctions = false;
1744
+ }
1745
+ const constantEnv = parseEnvFlag(options.constantEnv);
1746
+ if (constantEnv !== null) {
1747
+ resolved.constantFolding = constantEnv;
1748
+ } else if (!resolved.enable) {
1749
+ resolved.constantFolding = false;
1750
+ }
1751
+ const combineEnv = parseEnvFlag(options.combineEnv);
1752
+ if (combineEnv !== null) {
1753
+ resolved.combineVariables = combineEnv;
1754
+ } else if (!resolved.enable) {
1755
+ resolved.combineVariables = false;
1756
+ }
1757
+ return resolved;
1758
+ }
1759
+ function applyScopeHoistEnv(result) {
1760
+ process.env.IONIFY_SCOPE_HOIST = result.enable ? "true" : "false";
1761
+ process.env.IONIFY_SCOPE_HOIST_INLINE = result.inlineFunctions ? "true" : "false";
1762
+ process.env.IONIFY_SCOPE_HOIST_CONST = result.constantFolding ? "true" : "false";
1763
+ process.env.IONIFY_SCOPE_HOIST_COMBINE = result.combineVariables ? "true" : "false";
1764
+ }
1765
+
1766
+ // src/cli/utils/parser.ts
1767
+ function normalize2(mode) {
1768
+ if (typeof mode !== "string") return null;
1769
+ const lower = mode.toLowerCase();
1770
+ if (lower === "swc") return "swc";
1771
+ if (lower === "hybrid") return "hybrid";
1772
+ if (lower === "oxc") return "oxc";
1773
+ return null;
1774
+ }
1775
+ function resolveParser(config, opts) {
1776
+ const envRaw = opts?.envMode ?? process.env.IONIFY_PARSER;
1777
+ const env = normalize2(envRaw);
1778
+ if (env) return env;
1779
+ const fromConfig = normalize2(config?.parser);
1780
+ return fromConfig ?? "hybrid";
1781
+ }
1782
+ function applyParserEnv(mode) {
1783
+ process.env.IONIFY_PARSER = mode;
1784
+ }
1785
+
1786
+ // src/cli/commands/dev.ts
1787
+ import crypto2 from "crypto";
1788
+ var __filename2 = fileURLToPath2(import.meta.url);
1789
+ var __dirname2 = path9.dirname(__filename2);
1790
+ var CLIENT_DIR = path9.resolve(__dirname2, "../client");
1791
+ var CLIENT_FALLBACK_DIR = path9.resolve(process.cwd(), "src/client");
1792
+ function readClientAssetFile(fileName) {
1793
+ const primary = path9.join(CLIENT_DIR, fileName);
1794
+ if (fs7.existsSync(primary)) {
1795
+ return { filePath: primary, code: fs7.readFileSync(primary, "utf8") };
1796
+ }
1797
+ const fallback = path9.join(CLIENT_FALLBACK_DIR, fileName);
1798
+ if (fs7.existsSync(fallback)) {
1799
+ return { filePath: fallback, code: fs7.readFileSync(fallback, "utf8") };
1800
+ }
1801
+ throw new Error(`Missing Ionify client asset: ${fileName}`);
1802
+ }
1803
+ function readClientAsset(fileName) {
1804
+ return readClientAssetFile(fileName).code;
1805
+ }
1806
+ function guessContentType(filePath) {
1807
+ const ext = path9.extname(filePath);
1808
+ if (ext === ".html") return "text/html; charset=utf-8";
1809
+ if (ext === ".css") return "text/css; charset=utf-8";
1810
+ if (ext === ".json") return "application/json; charset=utf-8";
1811
+ if ([".mjs", ".js", ".ts", ".tsx", ".jsx", ".cjs", ".mts", ".cts"].includes(ext))
1812
+ return "application/javascript; charset=utf-8";
1813
+ if ([".wasm"].includes(ext))
1814
+ return "application/wasm";
1815
+ if ([".map"].includes(ext))
1816
+ return "application/json; charset=utf-8";
1817
+ return "text/plain; charset=utf-8";
1818
+ }
1819
+ async function startDevServer({
1820
+ port = 5173,
1821
+ enableSignalHandlers = true
1822
+ } = {}) {
1823
+ const rootDir = process.cwd();
1824
+ const watcher = new IonifyWatcher(rootDir);
1825
+ const cacheDebug = process.env.IONIFY_DEV_TRANSFORM_CACHE_DEBUG === "1";
1826
+ const userConfig = await loadIonifyConfig();
1827
+ const minifier = resolveMinifier(userConfig, { envVar: process.env.IONIFY_MINIFIER });
1828
+ applyMinifierEnv(minifier);
1829
+ const parserMode = resolveParser(userConfig, { envMode: process.env.IONIFY_PARSER });
1830
+ applyParserEnv(parserMode);
1831
+ const treeshake = resolveTreeshake(userConfig?.treeshake, {
1832
+ envMode: process.env.IONIFY_TREESHAKE,
1833
+ includeEnv: process.env.IONIFY_TREESHAKE_INCLUDE,
1834
+ excludeEnv: process.env.IONIFY_TREESHAKE_EXCLUDE
1835
+ });
1836
+ applyTreeshakeEnv(treeshake);
1837
+ const scopeHoist = resolveScopeHoist(userConfig?.scopeHoist, {
1838
+ envMode: process.env.IONIFY_SCOPE_HOIST,
1839
+ inlineEnv: process.env.IONIFY_SCOPE_HOIST_INLINE,
1840
+ constantEnv: process.env.IONIFY_SCOPE_HOIST_CONST,
1841
+ combineEnv: process.env.IONIFY_SCOPE_HOIST_COMBINE
1842
+ });
1843
+ applyScopeHoistEnv(scopeHoist);
1844
+ const resolvedEntry = userConfig?.entry ? userConfig.entry.startsWith("/") ? path9.join(rootDir, userConfig.entry) : path9.resolve(rootDir, userConfig.entry) : void 0;
1845
+ const pluginNames = Array.isArray(userConfig?.plugins) ? userConfig.plugins.map((p) => typeof p === "string" ? p : p?.name).filter((name) => typeof name === "string" && name.length > 0) : void 0;
1846
+ const rawVersionInputs = {
1847
+ parserMode,
1848
+ minifier,
1849
+ treeshake,
1850
+ scopeHoist,
1851
+ plugins: pluginNames,
1852
+ entry: resolvedEntry ? [resolvedEntry] : null,
1853
+ cssOptions: userConfig?.css,
1854
+ assetOptions: userConfig?.assets ?? userConfig?.asset
1855
+ };
1856
+ const configHash = computeGraphVersion(rawVersionInputs);
1857
+ logInfo(`[Dev] Version hash: ${configHash}`);
1858
+ process.env.IONIFY_CONFIG_HASH = configHash;
1859
+ const casRoot = path9.join(rootDir, ".ionify", "cas");
1860
+ const transformer = new TransformEngine({ casRoot, versionHash: configHash });
1861
+ const graph = new Graph(rawVersionInputs);
1862
+ if (native?.initAstCache) {
1863
+ const versionHash = JSON.stringify(rawVersionInputs);
1864
+ native.initAstCache(versionHash);
1865
+ logInfo(`AST cache initialized with version hash`);
1866
+ if (native?.astCacheWarmup) {
1867
+ try {
1868
+ native.astCacheWarmup();
1869
+ } catch (err) {
1870
+ logWarn(`AST cache warmup skipped: ${err}`);
1871
+ }
1872
+ }
1873
+ if (native?.astCacheStats) {
1874
+ try {
1875
+ const stats = native.astCacheStats();
1876
+ const entries = stats.total_entries ?? stats.totalEntries ?? 0;
1877
+ const sizeBytes = stats.total_size_bytes ?? stats.totalSizeBytes ?? 0;
1878
+ const hits = stats.total_hits ?? stats.totalHits ?? 0;
1879
+ const hitRate = stats.hit_rate ?? stats.hitRate ?? 0;
1880
+ logInfo(`[AST Cache] entries=${entries}, size=${sizeBytes} bytes, hits=${hits}, hitRate=${hitRate}`);
1881
+ } catch {
1882
+ }
1883
+ }
1884
+ }
1885
+ const moduleResolver = new ModuleResolver(rootDir, {
1886
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".mjs"],
1887
+ conditions: ["import", "default"],
1888
+ mainFields: ["module", "main"],
1889
+ ...userConfig?.resolve || {}
1890
+ });
1891
+ await applyRegisteredLoaders(transformer, userConfig);
1892
+ const hmr = new HMRServer();
1893
+ const envFromFiles = loadEnv("development", rootDir);
1894
+ process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
1895
+ process.env.MODE = process.env.MODE ?? "development";
1896
+ const envValues = {
1897
+ ...envFromFiles,
1898
+ NODE_ENV: process.env.NODE_ENV,
1899
+ MODE: process.env.MODE
1900
+ };
1901
+ const envPlaceholderPattern = /%([A-Z0-9_]+)%/g;
1902
+ const envEnabledExts = /* @__PURE__ */ new Set([
1903
+ ".html",
1904
+ ".js",
1905
+ ".mjs",
1906
+ ".cjs",
1907
+ ".ts",
1908
+ ".tsx",
1909
+ ".jsx"
1910
+ ]);
1911
+ const applyEnvPlaceholders = (input, extname) => {
1912
+ if (!envEnabledExts.has(extname)) return input;
1913
+ return input.replace(envPlaceholderPattern, (match, key) => {
1914
+ if (key === "NODE_ENV" || key === "MODE" || key.startsWith("VITE_") || key.startsWith("IONIFY_")) {
1915
+ const replacement = envValues[key];
1916
+ return replacement !== void 0 ? replacement : match;
1917
+ }
1918
+ return match;
1919
+ });
1920
+ };
1921
+ const parseJsonBody = async (req) => {
1922
+ const chunks = [];
1923
+ await new Promise((resolve, reject) => {
1924
+ req.on("data", (chunk) => chunks.push(chunk));
1925
+ req.on("end", () => resolve());
1926
+ req.on("error", (err) => reject(err));
1927
+ });
1928
+ if (!chunks.length) return null;
1929
+ const raw = Buffer.concat(chunks).toString("utf8");
1930
+ if (!raw.trim()) return null;
1931
+ return JSON.parse(raw);
1932
+ };
1933
+ const sendJson = (res, status, payload) => {
1934
+ const body = JSON.stringify(payload);
1935
+ res.writeHead(status, {
1936
+ "Content-Type": "application/json; charset=utf-8",
1937
+ "Cache-Control": "no-store"
1938
+ });
1939
+ res.end(body);
1940
+ };
1941
+ const buildUpdatePayload = async (modules) => {
1942
+ const updates = [];
1943
+ for (const mod of modules) {
1944
+ const exists = fs7.existsSync(mod.absPath);
1945
+ if (mod.reason === "deleted" || !exists) {
1946
+ graph.removeFile(mod.absPath);
1947
+ watcher.unwatchFile(mod.absPath);
1948
+ updates.push({
1949
+ url: mod.url,
1950
+ hash: null,
1951
+ deps: [],
1952
+ reason: mod.reason,
1953
+ status: "deleted"
1954
+ });
1955
+ continue;
1956
+ }
1957
+ watcher.watchFile(mod.absPath);
1958
+ let code;
1959
+ try {
1960
+ code = fs7.readFileSync(mod.absPath, "utf8");
1961
+ } catch (err) {
1962
+ logError("Failed to read module during HMR apply", err);
1963
+ throw err;
1964
+ }
1965
+ let hash;
1966
+ let specs;
1967
+ if (native?.parseModuleIr) {
1968
+ try {
1969
+ const ir = native.parseModuleIr(mod.absPath, code);
1970
+ hash = ir.hash;
1971
+ specs = ir.dependencies.map((dep) => dep.specifier);
1972
+ } catch {
1973
+ hash = getCacheKey(code);
1974
+ specs = extractImports(code, mod.absPath);
1975
+ }
1976
+ } else {
1977
+ hash = getCacheKey(code);
1978
+ specs = extractImports(code, mod.absPath);
1979
+ }
1980
+ const depsAbs = resolveImports(specs, mod.absPath);
1981
+ graph.recordFile(mod.absPath, hash, depsAbs);
1982
+ for (const dep of depsAbs) {
1983
+ watcher.watchFile(dep);
1984
+ }
1985
+ const result = await transformer.run({
1986
+ path: mod.absPath,
1987
+ code,
1988
+ ext: path9.extname(mod.absPath),
1989
+ moduleHash: hash
1990
+ });
1991
+ const transformed = result.code;
1992
+ const envApplied = applyEnvPlaceholders(
1993
+ transformed,
1994
+ path9.extname(mod.absPath)
1995
+ );
1996
+ updates.push({
1997
+ url: mod.url,
1998
+ hash,
1999
+ deps: depsAbs.map((dep) => normalizeUrlFromFs(rootDir, dep)),
2000
+ reason: mod.reason,
2001
+ status: "updated",
2002
+ code: envApplied
2003
+ });
2004
+ }
2005
+ return updates;
2006
+ };
2007
+ const server = http.createServer(async (req, res) => {
2008
+ try {
2009
+ const parsed = url.parse(req.url || "/", true);
2010
+ let reqPath = parsed.pathname || "/";
2011
+ try {
2012
+ reqPath = decodeURIComponent(reqPath);
2013
+ } catch {
2014
+ }
2015
+ const q = parsed.query || {};
2016
+ if (reqPath === "/__ionify_hmr") {
2017
+ hmr.handleSSE(req, res);
2018
+ return;
2019
+ }
2020
+ if (reqPath === "/__ionify_hmr_client.js") {
2021
+ res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
2022
+ res.end(readClientAsset("hmr.js"));
2023
+ return;
2024
+ }
2025
+ if (reqPath === "/__ionify_overlay.js") {
2026
+ res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
2027
+ res.end(readClientAsset("overlay.js"));
2028
+ return;
2029
+ }
2030
+ if (reqPath === "/__ionify_react_refresh.js") {
2031
+ try {
2032
+ const asset = readClientAssetFile("react-refresh-runtime.js");
2033
+ let reactRefreshPath;
2034
+ try {
2035
+ const projectRequire = createRequire2(path9.join(rootDir, "package.json"));
2036
+ reactRefreshPath = projectRequire.resolve("react-refresh/runtime");
2037
+ } catch (err) {
2038
+ logError("Failed to resolve react-refresh/runtime", err);
2039
+ res.statusCode = 500;
2040
+ res.end("Failed to resolve react-refresh/runtime. Make sure react-refresh is installed.");
2041
+ return;
2042
+ }
2043
+ const reactRefreshUrl = normalizeUrlFromFs(rootDir, reactRefreshPath);
2044
+ let code2 = asset.code.replace(
2045
+ 'import RefreshRuntime from "react-refresh/runtime"',
2046
+ `import RefreshRuntime from "${reactRefreshUrl}"`
2047
+ );
2048
+ res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
2049
+ res.end(code2);
2050
+ } catch (err) {
2051
+ logError("Failed to serve react refresh runtime", err);
2052
+ res.statusCode = 500;
2053
+ res.end("Internal Server Error");
2054
+ }
2055
+ return;
2056
+ }
2057
+ if (reqPath === "/__ionify_hmr/apply") {
2058
+ if (req.method !== "POST") {
2059
+ res.writeHead(405, { Allow: "POST" });
2060
+ res.end("Method Not Allowed");
2061
+ return;
2062
+ }
2063
+ let body;
2064
+ try {
2065
+ body = await parseJsonBody(req);
2066
+ } catch (err) {
2067
+ logError("Invalid JSON body for HMR apply", err);
2068
+ sendJson(res, 400, { error: "Invalid JSON body" });
2069
+ return;
2070
+ }
2071
+ const id = typeof body?.id === "string" ? body.id : null;
2072
+ if (!id) {
2073
+ sendJson(res, 400, { error: "Missing update id" });
2074
+ return;
2075
+ }
2076
+ const pending = hmr.consumeUpdate(id);
2077
+ if (!pending) {
2078
+ sendJson(res, 404, { error: "Update not found", id });
2079
+ return;
2080
+ }
2081
+ try {
2082
+ const modules = await buildUpdatePayload(pending.modules);
2083
+ sendJson(res, 200, {
2084
+ type: "update",
2085
+ id: pending.summary.id,
2086
+ timestamp: Date.now(),
2087
+ modules
2088
+ });
2089
+ } catch (err) {
2090
+ logError("Failed to build HMR update payload", err);
2091
+ hmr.broadcastError({
2092
+ id,
2093
+ message: "Failed to compile update; falling back to full reload"
2094
+ });
2095
+ sendJson(res, 500, { error: "Failed to compile update", id });
2096
+ }
2097
+ return;
2098
+ }
2099
+ if (reqPath === "/__ionify_hmr/error") {
2100
+ if (req.method !== "POST") {
2101
+ res.writeHead(405, { Allow: "POST" });
2102
+ res.end("Method Not Allowed");
2103
+ return;
2104
+ }
2105
+ let body;
2106
+ try {
2107
+ body = await parseJsonBody(req);
2108
+ } catch {
2109
+ body = null;
2110
+ }
2111
+ const id = typeof body?.id === "string" ? body.id : void 0;
2112
+ const message = typeof body?.message === "string" ? body.message : "Unknown HMR error";
2113
+ logError(`[HMR] client reported error${id ? ` ${id}` : ""}: ${message}`);
2114
+ hmr.broadcastError({ id, message });
2115
+ sendJson(res, 200, { ok: true });
2116
+ return;
2117
+ }
2118
+ const fsPath = decodePublicPath(rootDir, reqPath);
2119
+ if (!fsPath) {
2120
+ res.statusCode = 404;
2121
+ res.end("Not found");
2122
+ return;
2123
+ }
2124
+ let effectiveFsPath = fsPath;
2125
+ let effectiveUrlPath = reqPath;
2126
+ if (fs7.existsSync(effectiveFsPath) && fs7.statSync(effectiveFsPath).isDirectory()) {
2127
+ const indexExtensions = [".html", ".js", ".ts", ".tsx", ".jsx"];
2128
+ let found = false;
2129
+ for (const ext2 of indexExtensions) {
2130
+ const indexFile = path9.join(effectiveFsPath, `index${ext2}`);
2131
+ if (fs7.existsSync(indexFile)) {
2132
+ effectiveFsPath = indexFile;
2133
+ effectiveUrlPath = effectiveUrlPath.endsWith("/") ? `${effectiveUrlPath}index${ext2}` : `${effectiveUrlPath}/index${ext2}`;
2134
+ found = true;
2135
+ break;
2136
+ }
2137
+ }
2138
+ if (!found) {
2139
+ const packageJson = path9.join(effectiveFsPath, "package.json");
2140
+ if (fs7.existsSync(packageJson)) {
2141
+ try {
2142
+ const pkg = JSON.parse(fs7.readFileSync(packageJson, "utf8"));
2143
+ if (pkg.main) {
2144
+ const mainFile = path9.join(effectiveFsPath, pkg.main);
2145
+ if (fs7.existsSync(mainFile)) {
2146
+ effectiveFsPath = mainFile;
2147
+ found = true;
2148
+ }
2149
+ }
2150
+ } catch (e) {
2151
+ }
2152
+ }
2153
+ }
2154
+ if (!found) {
2155
+ for (const ext2 of indexExtensions) {
2156
+ const moduleFile = path9.join(effectiveFsPath, `module${ext2}`);
2157
+ if (fs7.existsSync(moduleFile)) {
2158
+ effectiveFsPath = moduleFile;
2159
+ found = true;
2160
+ break;
2161
+ }
2162
+ }
2163
+ }
2164
+ if (!found) {
2165
+ res.statusCode = 404;
2166
+ res.end("Module not found");
2167
+ return;
2168
+ }
2169
+ }
2170
+ if (!fs7.existsSync(effectiveFsPath)) {
2171
+ res.statusCode = 404;
2172
+ res.end("Not found");
2173
+ return;
2174
+ }
2175
+ const ext = path9.extname(effectiveFsPath);
2176
+ if (isAssetExt(ext)) {
2177
+ try {
2178
+ const data = fs7.readFileSync(effectiveFsPath);
2179
+ const assetHash = crypto2.createHash("sha256").update(data).digest("hex");
2180
+ const kind = "asset";
2181
+ const changed2 = graph.recordFile(effectiveFsPath, assetHash, [], [], kind);
2182
+ watcher.watchFile(effectiveFsPath);
2183
+ if (changed2) {
2184
+ logInfo(`[Graph] Asset updated: ${effectiveFsPath}`);
2185
+ }
2186
+ } catch {
2187
+ }
2188
+ if ("import" in q) {
2189
+ const js = assetAsModule(normalizeUrlFromFs(rootDir, effectiveFsPath));
2190
+ res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
2191
+ res.end(js);
2192
+ return;
2193
+ } else {
2194
+ res.writeHead(200, { "Content-Type": contentTypeForAsset(ext) });
2195
+ fs7.createReadStream(effectiveFsPath).pipe(res);
2196
+ return;
2197
+ }
2198
+ }
2199
+ if (ext === ".css") {
2200
+ try {
2201
+ const cssSource = fs7.readFileSync(effectiveFsPath, "utf8");
2202
+ const isModule = "module" in q || /\.module\.css$/i.test(effectiveFsPath);
2203
+ const isInline = "inline" in q;
2204
+ const mode = isModule ? "css:module" : isInline ? "css:inline" : "css:raw";
2205
+ const contentHash = getCacheKey(cssSource);
2206
+ watcher.watchFile(effectiveFsPath);
2207
+ const kind = isModule ? "css-module" : "css";
2208
+ const changed2 = graph.recordFile(effectiveFsPath, contentHash, [], [], kind);
2209
+ const casDir = getCasArtifactPath(casRoot, configHash, contentHash);
2210
+ const casFile = path9.join(casDir, "transformed.js");
2211
+ let finalBuffer = null;
2212
+ if (fs7.existsSync(casFile)) {
2213
+ try {
2214
+ finalBuffer = fs7.readFileSync(casFile);
2215
+ res.setHeader("X-Ionify-Cache", "HIT");
2216
+ } catch {
2217
+ finalBuffer = null;
2218
+ }
2219
+ }
2220
+ if (!finalBuffer) {
2221
+ const { css: compiledCss, tokens } = await compileCss({
2222
+ code: cssSource,
2223
+ filePath: effectiveFsPath,
2224
+ rootDir,
2225
+ modules: isModule
2226
+ });
2227
+ const body = isModule || isInline ? renderCssModule({
2228
+ css: compiledCss,
2229
+ filePath: effectiveFsPath,
2230
+ tokens: isModule ? tokens ?? {} : void 0
2231
+ }) : compiledCss;
2232
+ finalBuffer = Buffer.from(body, "utf8");
2233
+ res.setHeader("X-Ionify-Cache", "MISS");
2234
+ try {
2235
+ fs7.mkdirSync(casDir, { recursive: true });
2236
+ fs7.writeFileSync(casFile, finalBuffer);
2237
+ } catch {
2238
+ }
2239
+ }
2240
+ if (isModule || isInline) {
2241
+ res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
2242
+ } else {
2243
+ res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
2244
+ }
2245
+ res.end(finalBuffer);
2246
+ logInfo(`Served: ${effectiveUrlPath} deps:0 ${changed2 ? "(updated)" : "(cached)"}`);
2247
+ return;
2248
+ } catch (err) {
2249
+ logError("Failed to process CSS", err);
2250
+ res.statusCode = 500;
2251
+ res.end("Failed to process CSS");
2252
+ return;
2253
+ }
2254
+ }
2255
+ const code = fs7.readFileSync(effectiveFsPath, "utf8");
2256
+ let hash;
2257
+ let specs;
2258
+ if (native?.parseModuleIr) {
2259
+ try {
2260
+ const ir = native.parseModuleIr(effectiveFsPath, code);
2261
+ hash = ir.hash;
2262
+ specs = ir.dependencies.map((dep) => dep.specifier);
2263
+ } catch {
2264
+ hash = getCacheKey(code);
2265
+ specs = extractImports(code, effectiveFsPath);
2266
+ }
2267
+ } else {
2268
+ hash = getCacheKey(code);
2269
+ specs = extractImports(code, effectiveFsPath);
2270
+ }
2271
+ const depsAbs = resolveImports(specs, effectiveFsPath);
2272
+ const changed = graph.recordFile(effectiveFsPath, hash, depsAbs);
2273
+ watcher.watchFile(effectiveFsPath);
2274
+ for (const dep of depsAbs) {
2275
+ watcher.watchFile(dep);
2276
+ }
2277
+ const result = await transformer.run({
2278
+ path: effectiveFsPath,
2279
+ code,
2280
+ ext,
2281
+ moduleHash: hash
2282
+ });
2283
+ const transformedCode = result.code;
2284
+ res.setHeader("X-Ionify-Cache", changed ? "MISS" : "HIT");
2285
+ const envApplied = applyEnvPlaceholders(transformedCode, ext);
2286
+ if (path9.extname(effectiveFsPath) === ".html") {
2287
+ const injected = injectHMRClient(envApplied);
2288
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2289
+ res.end(injected);
2290
+ } else {
2291
+ const finalBuffer = Buffer.from(envApplied);
2292
+ res.setHeader("Content-Type", guessContentType(effectiveFsPath));
2293
+ res.end(finalBuffer);
2294
+ }
2295
+ logInfo(`Served: ${effectiveUrlPath} deps:${depsAbs.length} ${changed ? "(updated)" : "(cached)"}`);
2296
+ if (cacheDebug) {
2297
+ const m = transformCache.metrics();
2298
+ logInfo(`[Ionify][Dev Cache] hits:${m.hits} misses:${m.misses} size:${m.size}`);
2299
+ }
2300
+ } catch (err) {
2301
+ logError("Error serving request:", err);
2302
+ res.statusCode = 500;
2303
+ res.end("Internal Server Error");
2304
+ }
2305
+ });
2306
+ watcher.on("change", (file, status) => {
2307
+ logInfo(`[Watcher] ${status}: ${file}`);
2308
+ const affected = graph.collectAffected([file]);
2309
+ if (!affected.includes(file)) {
2310
+ affected.unshift(file);
226
2311
  }
227
- if (resolved && typeof resolved === "object") {
228
- cachedConfig = resolved;
229
- const baseDir = path3.dirname(configPath);
230
- const aliases = resolved?.resolve?.alias;
231
- if (aliases && typeof aliases === "object") {
232
- configureResolverAliases(aliases, baseDir);
233
- } else {
234
- configureResolverAliases(void 0, baseDir);
2312
+ const modules = [];
2313
+ for (const absPath of affected) {
2314
+ const reason = absPath === file ? status === "deleted" ? "deleted" : "changed" : "dependent";
2315
+ let hash = null;
2316
+ if (reason !== "deleted") {
2317
+ if (absPath === file) {
2318
+ try {
2319
+ const code = fs7.readFileSync(absPath, "utf8");
2320
+ hash = getCacheKey(code);
2321
+ } catch {
2322
+ hash = graph.getNode(absPath)?.hash ?? null;
2323
+ }
2324
+ } else {
2325
+ hash = graph.getNode(absPath)?.hash ?? null;
2326
+ }
235
2327
  }
236
- logInfo(`Loaded ionify config from ${path3.relative(cwd, configPath)}`);
237
- } else {
238
- throw new Error("Config did not export an object");
2328
+ modules.push({
2329
+ absPath,
2330
+ url: normalizeUrlFromFs(rootDir, absPath),
2331
+ hash,
2332
+ reason
2333
+ });
239
2334
  }
240
- } catch (err) {
241
- logError("Failed to load ionify.config", err);
242
- cachedConfig = null;
243
- configureResolverAliases(void 0, cwd);
244
- }
245
- return cachedConfig;
246
- }
247
-
248
- // src/cli/commands/dev.ts
249
- async function startDevServer(options = {}) {
250
- try {
251
- const config = await loadIonifyConfig();
252
- const port = parseInt(options.port || config?.server?.port || "5173");
253
- logInfo(`Starting Ionify dev server on port ${port}...`);
254
- const result = native.startDevServer({
255
- port,
256
- root: process.cwd(),
257
- config: config || {}
258
- });
259
- logInfo(`Dev server running at http://localhost:${port}`);
260
- return result;
261
- } catch (err) {
262
- logError(`Dev server failed: ${err.message}`);
263
- throw err;
264
- }
265
- }
266
-
267
- // src/cli/commands/build.ts
268
- async function runBuildCommand(options = {}) {
269
- try {
270
- const config = await loadIonifyConfig();
271
- const outDir = options.outDir || config?.build?.outDir || "dist";
272
- const level = options.level ?? config?.optimizationLevel ?? 3;
273
- logInfo(`Building for production (optimization level: ${level})...`);
274
- const result = native.build({
275
- root: process.cwd(),
276
- outDir,
277
- optimizationLevel: level,
278
- config: config || {}
279
- });
280
- logInfo(`\u2705 Build complete! Output: ${outDir}/`);
281
- return result;
282
- } catch (err) {
283
- logError(`Build failed: ${err.message}`);
284
- throw err;
2335
+ const summary = hmr.queueUpdate(modules);
2336
+ if (summary) {
2337
+ logInfo(
2338
+ `[HMR] update ${summary.id} -> ${summary.modules.length} module(s) queued`
2339
+ );
2340
+ }
2341
+ if (status === "deleted") {
2342
+ graph.removeFile(file);
2343
+ watcher.unwatchFile(file);
2344
+ }
2345
+ });
2346
+ let closingPromise = null;
2347
+ let cleanedUp = false;
2348
+ const signalHandlers = [];
2349
+ const cleanup = (force = false) => {
2350
+ if (cleanedUp) return;
2351
+ cleanedUp = true;
2352
+ if (force) {
2353
+ server.getConnections((err, count) => {
2354
+ if (!err && count > 0) {
2355
+ server.closeAllConnections();
2356
+ }
2357
+ });
2358
+ }
2359
+ try {
2360
+ watcher.closeAll();
2361
+ } catch (err) {
2362
+ logError("Error closing watcher:", err);
2363
+ }
2364
+ try {
2365
+ hmr.close();
2366
+ } catch (err) {
2367
+ logError("Error closing HMR:", err);
2368
+ }
2369
+ graph.flush();
2370
+ for (const { event, handler } of signalHandlers) {
2371
+ process.off(event, handler);
2372
+ }
2373
+ };
2374
+ server.on("close", () => cleanup(false));
2375
+ const shutdown = async (exitProcess) => {
2376
+ if (!closingPromise) {
2377
+ closingPromise = new Promise((resolve, reject) => {
2378
+ const timeoutId = setTimeout(() => {
2379
+ logInfo("Server shutdown taking too long, forcing cleanup...");
2380
+ cleanup(true);
2381
+ resolve();
2382
+ }, 3e3);
2383
+ server.close((err) => {
2384
+ clearTimeout(timeoutId);
2385
+ if (err) {
2386
+ logError("Error during server shutdown:", err);
2387
+ reject(err);
2388
+ } else {
2389
+ resolve();
2390
+ }
2391
+ });
2392
+ });
2393
+ }
2394
+ try {
2395
+ await Promise.race([
2396
+ closingPromise,
2397
+ new Promise(
2398
+ (_, reject) => setTimeout(() => reject(new Error("Shutdown timeout")), 5e3)
2399
+ )
2400
+ ]);
2401
+ } catch (err) {
2402
+ logError("Shutdown error:", err);
2403
+ cleanup(true);
2404
+ }
2405
+ if (exitProcess) {
2406
+ setTimeout(() => process.exit(0), 100);
2407
+ }
2408
+ };
2409
+ if (enableSignalHandlers) {
2410
+ const onSignal = () => {
2411
+ void shutdown(true);
2412
+ };
2413
+ process.on("SIGINT", onSignal);
2414
+ process.on("SIGTERM", onSignal);
2415
+ signalHandlers.push({ event: "SIGINT", handler: onSignal });
2416
+ signalHandlers.push({ event: "SIGTERM", handler: onSignal });
285
2417
  }
2418
+ await new Promise((resolve) => {
2419
+ server.listen(port, () => resolve());
2420
+ });
2421
+ const address = server.address();
2422
+ const actualPort = address && typeof address === "object" && address?.port ? address.port : port;
2423
+ logInfo(`Ionify Dev Server (Phase 2) at http://localhost:${actualPort}`);
2424
+ logInfo(`HMR listening at /__ionify_hmr (SSE)`);
2425
+ return {
2426
+ server,
2427
+ port: actualPort,
2428
+ close: async () => {
2429
+ await shutdown(false);
2430
+ }
2431
+ };
286
2432
  }
287
2433
 
288
2434
  // src/cli/commands/analyze.ts
289
- import fs3 from "fs";
290
- import path4 from "path";
2435
+ import fs8 from "fs";
2436
+ import path10 from "path";
291
2437
  function readGraphFromDisk(root) {
292
- const file = path4.join(root, ".ionify", "graph.json");
293
- if (!fs3.existsSync(file)) return null;
2438
+ const file = path10.join(root, ".ionify", "graph.json");
2439
+ if (!fs8.existsSync(file)) return null;
294
2440
  try {
295
- const raw = fs3.readFileSync(file, "utf8");
2441
+ const raw = fs8.readFileSync(file, "utf8");
296
2442
  const snapshot = JSON.parse(raw);
297
2443
  if (snapshot?.version !== 1 || !snapshot?.nodes) return null;
298
2444
  return Object.entries(snapshot.nodes).map(([id, node]) => ({
@@ -387,31 +2533,971 @@ async function runAnalyzeCommand(options = {}) {
387
2533
  }
388
2534
  }
389
2535
 
2536
+ // src/cli/commands/build.ts
2537
+ import fs10 from "fs";
2538
+ import path12 from "path";
2539
+
2540
+ // src/cli/utils/optimization-level.ts
2541
+ function getOptimizationPreset(level) {
2542
+ switch (level) {
2543
+ case 0:
2544
+ return {
2545
+ minifier: "swc",
2546
+ treeshake: {
2547
+ mode: "off",
2548
+ include: [],
2549
+ exclude: []
2550
+ },
2551
+ scopeHoist: {
2552
+ enable: false,
2553
+ inlineFunctions: false,
2554
+ constantFolding: false,
2555
+ combineVariables: false
2556
+ }
2557
+ };
2558
+ case 1:
2559
+ return {
2560
+ minifier: "oxc",
2561
+ treeshake: {
2562
+ mode: "safe",
2563
+ include: [],
2564
+ exclude: []
2565
+ },
2566
+ scopeHoist: {
2567
+ enable: true,
2568
+ inlineFunctions: true,
2569
+ constantFolding: false,
2570
+ combineVariables: false
2571
+ }
2572
+ };
2573
+ case 2:
2574
+ return {
2575
+ minifier: "oxc",
2576
+ treeshake: {
2577
+ mode: "safe",
2578
+ include: [],
2579
+ exclude: []
2580
+ },
2581
+ scopeHoist: {
2582
+ enable: true,
2583
+ inlineFunctions: true,
2584
+ constantFolding: true,
2585
+ combineVariables: true
2586
+ }
2587
+ };
2588
+ case 3:
2589
+ return {
2590
+ minifier: "oxc",
2591
+ treeshake: {
2592
+ mode: "aggressive",
2593
+ include: [],
2594
+ exclude: []
2595
+ },
2596
+ scopeHoist: {
2597
+ enable: true,
2598
+ inlineFunctions: true,
2599
+ constantFolding: true,
2600
+ combineVariables: true
2601
+ }
2602
+ };
2603
+ default:
2604
+ return getOptimizationPreset(2);
2605
+ }
2606
+ }
2607
+ function resolveOptimizationLevel(configLevel, options = {}) {
2608
+ if (options.cliLevel !== void 0) {
2609
+ const parsed = typeof options.cliLevel === "number" ? options.cliLevel : parseInt(options.cliLevel, 10);
2610
+ if ([0, 1, 2, 3].includes(parsed)) {
2611
+ return parsed;
2612
+ }
2613
+ }
2614
+ if (options.envLevel) {
2615
+ const parsed = parseInt(options.envLevel, 10);
2616
+ if ([0, 1, 2, 3].includes(parsed)) {
2617
+ return parsed;
2618
+ }
2619
+ }
2620
+ if (configLevel !== void 0 && [0, 1, 2, 3].includes(configLevel)) {
2621
+ return configLevel;
2622
+ }
2623
+ return null;
2624
+ }
2625
+
2626
+ // src/core/bundler.ts
2627
+ import fs9 from "fs";
2628
+ import path11 from "path";
2629
+ import crypto3 from "crypto";
2630
+ function readGraphSnapshot() {
2631
+ if (native?.graphLoadMap) {
2632
+ try {
2633
+ const nativeMap = native.graphLoadMap();
2634
+ if (nativeMap && Object.keys(nativeMap).length > 0) {
2635
+ return Object.values(nativeMap).map((node) => ({
2636
+ id: node.id,
2637
+ hash: node.hash,
2638
+ deps: node.deps || [],
2639
+ dynamicDeps: node.dynamicDeps || [],
2640
+ kind: node.kind
2641
+ }));
2642
+ }
2643
+ } catch (err) {
2644
+ logWarn(`Failed to load native graph: ${String(err)}`);
2645
+ }
2646
+ }
2647
+ const file = path11.join(process.cwd(), ".ionify", "graph.json");
2648
+ if (!fs9.existsSync(file)) return [];
2649
+ try {
2650
+ const raw = fs9.readFileSync(file, "utf8");
2651
+ const snapshot = JSON.parse(raw);
2652
+ if (snapshot?.version !== 1 || !snapshot?.nodes) return [];
2653
+ return Object.entries(snapshot.nodes).map(([id, node]) => ({
2654
+ id,
2655
+ hash: typeof node.hash === "string" ? node.hash : null,
2656
+ deps: Array.isArray(node.deps) ? node.deps : []
2657
+ }));
2658
+ } catch (err) {
2659
+ logWarn(`Failed to read graph snapshot: ${String(err)}`);
2660
+ return [];
2661
+ }
2662
+ }
2663
+ var JS_EXTENSIONS2 = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"]);
2664
+ var CSS_EXTENSIONS = /* @__PURE__ */ new Set([".css"]);
2665
+ function classifyModuleKind(id) {
2666
+ const ext = path11.extname(id).toLowerCase();
2667
+ if (CSS_EXTENSIONS.has(ext)) return "css";
2668
+ if (JS_EXTENSIONS2.has(ext)) return "js";
2669
+ return "asset";
2670
+ }
2671
+ var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
2672
+ var toPosix = (p) => p.split(path11.sep).join("/");
2673
+ function minifyCss(input) {
2674
+ return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/\s*([{};:,])\s*/g, "$1").trim();
2675
+ }
2676
+ function orderCssModules(chunk) {
2677
+ const cssModules = chunk.modules.filter((m) => m.kind === "css");
2678
+ const cssSet = new Set(cssModules.map((m) => m.id));
2679
+ const adj = /* @__PURE__ */ new Map();
2680
+ for (const mod of cssModules) {
2681
+ const deps = [...mod.deps || [], ...mod.dynamicDeps || []].filter((d) => cssSet.has(d));
2682
+ deps.sort();
2683
+ adj.set(mod.id, deps);
2684
+ }
2685
+ const visited = /* @__PURE__ */ new Set();
2686
+ const temp = /* @__PURE__ */ new Set();
2687
+ const ordered = [];
2688
+ const dfs = (id) => {
2689
+ if (visited.has(id) || temp.has(id)) return;
2690
+ temp.add(id);
2691
+ const edges = adj.get(id) || [];
2692
+ for (const dep of edges) dfs(dep);
2693
+ temp.delete(id);
2694
+ visited.add(id);
2695
+ ordered.push(id);
2696
+ };
2697
+ const sorted = [...cssModules.map((m) => m.id)].sort();
2698
+ for (const id of sorted) {
2699
+ dfs(id);
2700
+ }
2701
+ return ordered;
2702
+ }
2703
+ function normalizeModules(rawModules) {
2704
+ const modules = [];
2705
+ for (const raw of rawModules) {
2706
+ if (typeof raw === "string") {
2707
+ modules.push({
2708
+ id: raw,
2709
+ hash: null,
2710
+ kind: classifyModuleKind(raw),
2711
+ deps: [],
2712
+ dynamicDeps: []
2713
+ });
2714
+ continue;
2715
+ }
2716
+ if (!raw || typeof raw !== "object") continue;
2717
+ const id = typeof raw.id === "string" ? raw.id : null;
2718
+ if (!id) continue;
2719
+ const rawKind = typeof raw.kind === "string" ? raw.kind : classifyModuleKind(id);
2720
+ const kind = rawKind === "css" || rawKind === "asset" ? rawKind : "js";
2721
+ const deps = Array.isArray(raw.deps) ? raw.deps.filter(isNonEmptyString) : [];
2722
+ const dynamicSource = Array.isArray(raw.dynamicDeps) ? raw.dynamicDeps : Array.isArray(raw.dynamic_deps) ? raw.dynamic_deps : [];
2723
+ const dynamicDeps = dynamicSource.filter(isNonEmptyString);
2724
+ const hash = typeof raw.hash === "string" && raw.hash.length ? raw.hash : null;
2725
+ modules.push({
2726
+ id,
2727
+ hash,
2728
+ kind,
2729
+ deps,
2730
+ dynamicDeps
2731
+ });
2732
+ }
2733
+ return modules;
2734
+ }
2735
+ function normalizePlan(plan) {
2736
+ const entries = Array.isArray(plan?.entries) ? Array.from(new Set(plan.entries.filter(isNonEmptyString))) : [];
2737
+ const rawChunks = Array.isArray(plan?.chunks) ? plan.chunks : [];
2738
+ const normalizedChunks = rawChunks.map((chunk, index) => {
2739
+ const id = typeof chunk?.id === "string" && chunk.id.length ? chunk.id : `chunk-${index}`;
2740
+ const modules = normalizeModules(Array.isArray(chunk?.modules) ? chunk.modules : []);
2741
+ const consumersRaw = Array.isArray(chunk?.consumers) ? chunk.consumers.filter(isNonEmptyString) : null;
2742
+ const cssRaw = Array.isArray(chunk?.css) ? chunk.css.filter(isNonEmptyString) : null;
2743
+ const assetsRaw = Array.isArray(chunk?.assets) ? chunk.assets.filter(isNonEmptyString) : null;
2744
+ const consumers = consumersRaw && consumersRaw.length ? Array.from(new Set(consumersRaw)) : [...entries];
2745
+ const inferredCss = cssRaw && cssRaw.length ? cssRaw : modules.filter((m) => m.kind === "css").map((m) => m.id);
2746
+ const inferredAssets = assetsRaw && assetsRaw.length ? assetsRaw : modules.filter((m) => m.kind === "asset").map((m) => m.id);
2747
+ return {
2748
+ id,
2749
+ modules,
2750
+ entry: chunk?.entry === true,
2751
+ shared: chunk?.shared === true,
2752
+ consumers,
2753
+ css: inferredCss,
2754
+ assets: inferredAssets
2755
+ };
2756
+ });
2757
+ return {
2758
+ entries,
2759
+ chunks: normalizedChunks
2760
+ };
2761
+ }
2762
+ function fallbackPlan(entries) {
2763
+ const nodes = readGraphSnapshot();
2764
+ logInfo(`[Fallback] modules: ${nodes.length}, entries: ${entries?.length ?? 0}`);
2765
+ logInfo(`[Fallback] module IDs: ${nodes.map((n) => n.id).join(", ")}`);
2766
+ logInfo(`[Fallback] entry IDs: ${entries?.join(", ") ?? "none"}`);
2767
+ const modules = nodes.map((n) => n.id);
2768
+ const deps = /* @__PURE__ */ new Set();
2769
+ for (const node of nodes) {
2770
+ for (const dep of node.deps) deps.add(dep);
2771
+ }
2772
+ let finalEntries = entries && entries.length ? [...entries] : modules.filter((m) => !deps.has(m));
2773
+ if (!finalEntries.length && modules.length) {
2774
+ finalEntries = [modules[0]];
2775
+ }
2776
+ const nodeMap = new Map(nodes.map((node) => [node.id, node]));
2777
+ const planModules = modules.map((id) => {
2778
+ const node = nodeMap.get(id);
2779
+ return {
2780
+ id,
2781
+ hash: node?.hash ?? null,
2782
+ kind: node?.kind ?? classifyModuleKind(id),
2783
+ deps: node?.deps ?? [],
2784
+ dynamicDeps: node?.dynamicDeps ?? []
2785
+ };
2786
+ });
2787
+ const css = planModules.filter((m) => m.kind === "css").map((m) => m.id);
2788
+ const assets = planModules.filter((m) => m.kind === "asset").map((m) => m.id);
2789
+ return normalizePlan({
2790
+ entries: finalEntries,
2791
+ chunks: [
2792
+ {
2793
+ id: "chunk-main",
2794
+ modules: planModules,
2795
+ entry: true,
2796
+ shared: false,
2797
+ consumers: finalEntries,
2798
+ css,
2799
+ assets
2800
+ }
2801
+ ]
2802
+ });
2803
+ }
2804
+ async function generateBuildPlan(entries, versionInputs) {
2805
+ const version = versionInputs ? computeGraphVersion(versionInputs) : void 0;
2806
+ logInfo(`Graph version: ${version || "default"}`);
2807
+ const graphDbPath = path11.join(process.cwd(), ".ionify", "graph.db");
2808
+ ensureNativeGraph(graphDbPath, version);
2809
+ let moduleCount = 0;
2810
+ if (native?.graphLoadMap) {
2811
+ try {
2812
+ const persistedGraph = native.graphLoadMap();
2813
+ const graphSize = persistedGraph ? Object.keys(persistedGraph).length : 0;
2814
+ moduleCount = graphSize;
2815
+ logInfo(`Native graph loaded: ${graphSize} modules`);
2816
+ if (persistedGraph && graphSize > 0) {
2817
+ logInfo(`Loaded persisted graph with ${graphSize} modules`);
2818
+ }
2819
+ } catch (err) {
2820
+ logWarn(`Failed to load persisted graph: ${String(err)}`);
2821
+ }
2822
+ } else {
2823
+ logWarn(`graphLoadMap not available, native binding: ${!!native}`);
2824
+ }
2825
+ if (moduleCount === 0 && entries?.length && native) {
2826
+ logWarn(`[Build] Graph is empty \u2014 rebuilding dependency graph from entries...`);
2827
+ const queue = [...entries];
2828
+ const seen = new Set(queue);
2829
+ while (queue.length) {
2830
+ const file = queue.shift();
2831
+ if (!fs9.existsSync(file)) continue;
2832
+ const code = fs9.readFileSync(file, "utf8");
2833
+ let hash = getCacheKey(code);
2834
+ let specs = [];
2835
+ if (native.parseModuleIr) {
2836
+ try {
2837
+ const ir = native.parseModuleIr(file, code);
2838
+ hash = ir.hash;
2839
+ specs = ir.dependencies.map((d) => d.specifier);
2840
+ } catch {
2841
+ specs = extractImports(code, file);
2842
+ }
2843
+ } else {
2844
+ specs = extractImports(code, file);
2845
+ }
2846
+ const depsAbs = resolveImports(specs, file);
2847
+ if (typeof native.graphRecord === "function") {
2848
+ native.graphRecord(file, hash, depsAbs, [], "module");
2849
+ } else if (typeof native.recordFile === "function") {
2850
+ native.recordFile(file, hash, depsAbs, [], "module");
2851
+ }
2852
+ for (const dep of depsAbs) {
2853
+ if (!seen.has(dep)) {
2854
+ seen.add(dep);
2855
+ queue.push(dep);
2856
+ }
2857
+ }
2858
+ }
2859
+ try {
2860
+ if (typeof native.loadModulesCount === "function") {
2861
+ moduleCount = native.loadModulesCount() ?? moduleCount;
2862
+ } else if (native.graphLoadMap) {
2863
+ const persistedGraph = native.graphLoadMap();
2864
+ moduleCount = persistedGraph ? Object.keys(persistedGraph).length : moduleCount;
2865
+ }
2866
+ } catch {
2867
+ }
2868
+ logInfo(`[Build] Dependency graph rebuilt: ${moduleCount} modules`);
2869
+ }
2870
+ if (native?.plannerPlanBuild) {
2871
+ try {
2872
+ const start = Date.now();
2873
+ logInfo(`[Planner] Calling native plannerPlanBuild with ${entries?.length ?? 0} entries`);
2874
+ const plan = native.plannerPlanBuild(entries ?? []);
2875
+ logInfo(`[Planner] Native plan returned: ${plan.entries.length} entries, ${plan.chunks.length} chunks in ${Date.now() - start}ms`);
2876
+ return normalizePlan(plan);
2877
+ } catch (err) {
2878
+ logWarn(`plannerPlanBuild failed, falling back to JS planner: ${String(err)}`);
2879
+ }
2880
+ }
2881
+ return fallbackPlan(entries);
2882
+ }
2883
+ async function writeBuildManifest(outputDir, plan, artifacts) {
2884
+ const filesByChunk = /* @__PURE__ */ new Map();
2885
+ for (const artifact of artifacts) {
2886
+ filesByChunk.set(artifact.id, artifact.files);
2887
+ }
2888
+ const manifest = {
2889
+ entries: plan.entries,
2890
+ chunks: plan.chunks.map((chunk) => ({
2891
+ id: chunk.id,
2892
+ entry: chunk.entry,
2893
+ shared: chunk.shared,
2894
+ consumers: chunk.consumers,
2895
+ modules: chunk.modules.map((mod) => ({
2896
+ id: mod.id,
2897
+ kind: mod.kind,
2898
+ deps: mod.deps,
2899
+ dynamicDeps: mod.dynamicDeps
2900
+ })),
2901
+ files: filesByChunk.get(chunk.id) ?? { js: [], css: [], assets: [] }
2902
+ }))
2903
+ };
2904
+ const dir = path11.resolve(outputDir);
2905
+ await fs9.promises.mkdir(dir, { recursive: true });
2906
+ const file = path11.join(dir, "manifest.json");
2907
+ await fs9.promises.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
2908
+ }
2909
+ async function emitChunks(outputDir, plan, moduleOutputs, opts) {
2910
+ if (!native?.buildChunks) {
2911
+ logWarn("Native buildChunks binding is not available; using JS fallback emitter.");
2912
+ const rawArtifacts2 = buildJsFallbackArtifacts(plan, moduleOutputs);
2913
+ return emitChunksFromArtifacts(outputDir, plan, moduleOutputs, rawArtifacts2);
2914
+ }
2915
+ const start = Date.now();
2916
+ const rawArtifacts = native.buildChunks(plan, opts?.casRoot, opts?.versionHash) ?? [];
2917
+ logInfo(`[Bundler] buildChunks completed in ${Date.now() - start}ms (native)`);
2918
+ return emitChunksFromArtifacts(outputDir, plan, moduleOutputs, rawArtifacts);
2919
+ }
2920
+ function buildJsFallbackArtifacts(plan, moduleOutputs) {
2921
+ const artifacts = [];
2922
+ for (const chunk of plan.chunks) {
2923
+ const jsParts = [];
2924
+ const assets = [];
2925
+ for (const mod of chunk.modules) {
2926
+ const output = moduleOutputs.get(mod.id);
2927
+ if (output?.type === "js") {
2928
+ jsParts.push(`// ${mod.id}
2929
+ ${output.code}`);
2930
+ }
2931
+ }
2932
+ for (const assetPath of chunk.assets) {
2933
+ try {
2934
+ const data = fs9.readFileSync(assetPath);
2935
+ if (data.length < 4096) {
2936
+ const mime = "application/octet-stream";
2937
+ const inline = `data:${mime};base64,${data.toString("base64")}`;
2938
+ jsParts.push(`// ${assetPath}
2939
+ export const __ionify_asset = "${inline}";`);
2940
+ continue;
2941
+ }
2942
+ const hash = crypto3.createHash("sha256").update(data).digest("hex").slice(0, 16);
2943
+ const ext = path11.extname(assetPath) || ".bin";
2944
+ const fileName = `assets/${hash}${ext}`;
2945
+ assets.push({
2946
+ source: assetPath,
2947
+ file_name: fileName
2948
+ });
2949
+ } catch {
2950
+ const fileName = path11.basename(assetPath) || "asset";
2951
+ assets.push({
2952
+ source: assetPath,
2953
+ file_name: fileName
2954
+ });
2955
+ }
2956
+ }
2957
+ const code = jsParts.length ? jsParts.join("\n\n") : `// Ionify JS fallback for ${chunk.id}
2958
+ export default {};`;
2959
+ artifacts.push({
2960
+ id: chunk.id,
2961
+ file_name: `${chunk.id}.fallback.js`,
2962
+ code,
2963
+ map: null,
2964
+ assets,
2965
+ code_bytes: Buffer.byteLength(code, "utf8"),
2966
+ map_bytes: 0
2967
+ });
2968
+ }
2969
+ return artifacts;
2970
+ }
2971
+ function normalizeNativeArtifact(raw) {
2972
+ const id = raw.id;
2973
+ if (!id) {
2974
+ throw new Error("Native artifact missing id");
2975
+ }
2976
+ const file_name = raw.file_name ?? `${id.replace(/::/g, ".")}.native.js`;
2977
+ const code = raw.code ?? "";
2978
+ const map = raw.map ?? null;
2979
+ const code_bytes = typeof raw.code_bytes === "number" ? raw.code_bytes : Buffer.byteLength(code, "utf8");
2980
+ const map_bytes = typeof raw.map_bytes === "number" ? raw.map_bytes : map ? Buffer.byteLength(map, "utf8") : 0;
2981
+ const assets = Array.isArray(raw.assets) ? raw.assets.map((asset) => ({
2982
+ source: asset.source,
2983
+ file_name: asset.file_name ?? asset.fileName ?? path11.basename(asset.source ?? "asset")
2984
+ })) : [];
2985
+ return { id, file_name, code, map, assets, code_bytes, map_bytes };
2986
+ }
2987
+ async function emitChunksFromArtifacts(outputDir, plan, moduleOutputs, rawArtifacts) {
2988
+ const chunkDir = path11.join(outputDir, "chunks");
2989
+ await fs9.promises.mkdir(chunkDir, { recursive: true });
2990
+ const assetsDir = path11.join(outputDir, "assets");
2991
+ await fs9.promises.mkdir(assetsDir, { recursive: true });
2992
+ const enableSourceMaps = process.env.IONIFY_SOURCEMAPS === "true";
2993
+ const grouped = /* @__PURE__ */ new Map();
2994
+ for (const raw of rawArtifacts) {
2995
+ const artifact = normalizeNativeArtifact(raw);
2996
+ const baseId = artifact.id.split("::")[0] ?? artifact.id;
2997
+ const bucket = grouped.get(baseId);
2998
+ if (bucket) bucket.push(artifact);
2999
+ else grouped.set(baseId, [artifact]);
3000
+ }
3001
+ const buildStats = {};
3002
+ const results = [];
3003
+ for (const chunk of plan.chunks) {
3004
+ const artifacts = grouped.get(chunk.id);
3005
+ if (!artifacts || !artifacts.length) {
3006
+ throw new Error(`Native bundler did not emit artifacts for ${chunk.id}`);
3007
+ }
3008
+ const chunkOutDir = path11.join(chunkDir, chunk.id);
3009
+ await fs9.promises.mkdir(chunkOutDir, { recursive: true });
3010
+ artifacts.sort((a, b) => {
3011
+ if (a.id === chunk.id) return -1;
3012
+ if (b.id === chunk.id) return 1;
3013
+ return a.id.localeCompare(b.id);
3014
+ });
3015
+ const jsFiles = [];
3016
+ const cssFiles = [];
3017
+ const assetFiles = [];
3018
+ const assetWritten = /* @__PURE__ */ new Set();
3019
+ const copyAssets = async (assets) => {
3020
+ for (const asset of assets) {
3021
+ if (!asset?.source) continue;
3022
+ const relName = asset.file_name ?? path11.basename(asset.source);
3023
+ const assetFile = path11.join(outputDir, relName);
3024
+ if (assetWritten.has(assetFile)) continue;
3025
+ try {
3026
+ const data = await fs9.promises.readFile(asset.source);
3027
+ await fs9.promises.mkdir(path11.dirname(assetFile), { recursive: true });
3028
+ await fs9.promises.writeFile(assetFile, data);
3029
+ const rel = toPosix(path11.relative(outputDir, assetFile));
3030
+ buildStats[rel] = {
3031
+ bytes: data.length,
3032
+ emitter: "native",
3033
+ type: "asset"
3034
+ };
3035
+ assetFiles.push(rel);
3036
+ assetWritten.add(assetFile);
3037
+ } catch (err) {
3038
+ logWarn(`Failed to emit asset ${asset.source}: ${String(err)}`);
3039
+ }
3040
+ }
3041
+ };
3042
+ const cssOrder = orderCssModules(chunk);
3043
+ let cssFileRel = null;
3044
+ if (cssOrder.length) {
3045
+ const seenCss = /* @__PURE__ */ new Set();
3046
+ const cssPieces = [];
3047
+ for (const cssPath of cssOrder) {
3048
+ let cssSource = moduleOutputs.get(cssPath)?.code;
3049
+ if (!cssSource && fs9.existsSync(cssPath)) {
3050
+ try {
3051
+ cssSource = await fs9.promises.readFile(cssPath, "utf8");
3052
+ } catch (err) {
3053
+ logWarn(`Failed to read CSS source ${cssPath}: ${String(err)}`);
3054
+ }
3055
+ }
3056
+ if (!cssSource) continue;
3057
+ const minified = minifyCss(cssSource);
3058
+ if (!minified.length) continue;
3059
+ const key = getCacheKey(minified);
3060
+ if (seenCss.has(key)) continue;
3061
+ seenCss.add(key);
3062
+ cssPieces.push(minified);
3063
+ }
3064
+ if (cssPieces.length) {
3065
+ const combinedCss = cssPieces.join("\n");
3066
+ const cssHash = getCacheKey(combinedCss).slice(0, 8);
3067
+ const cssFileName = `assets/${chunk.id}.${cssHash}.css`;
3068
+ const cssFilePath = path11.join(outputDir, cssFileName);
3069
+ await fs9.promises.writeFile(cssFilePath, combinedCss, "utf8");
3070
+ cssFileRel = toPosix(path11.relative(outputDir, cssFilePath));
3071
+ buildStats[cssFileRel] = {
3072
+ bytes: Buffer.byteLength(combinedCss),
3073
+ emitter: "native",
3074
+ type: "css"
3075
+ };
3076
+ cssFiles.push(cssFileRel);
3077
+ }
3078
+ }
3079
+ for (const artifact of artifacts) {
3080
+ const nativeFile = path11.join(chunkOutDir, artifact.file_name);
3081
+ let nativeCode = artifact.code;
3082
+ if (cssFileRel) {
3083
+ const absCss = path11.join(outputDir, cssFileRel);
3084
+ const relCss = toPosix(path11.relative(path11.dirname(nativeFile), absCss));
3085
+ const inject = `(()=>{const url=new URL(${JSON.stringify(
3086
+ relCss
3087
+ )},import.meta.url).toString();if(typeof document!=="undefined"&&!document.querySelector('link[data-ionify-css="'+url+'"]')){const l=document.createElement("link");l.rel="stylesheet";l.href=url;l.setAttribute("data-ionify-css",url);document.head.appendChild(l);}})();`;
3088
+ nativeCode = `${inject}
3089
+ ${nativeCode}`;
3090
+ }
3091
+ if (enableSourceMaps && artifact.map) {
3092
+ const mapFile = `${nativeFile}.map`;
3093
+ await fs9.promises.writeFile(mapFile, artifact.map, "utf8");
3094
+ nativeCode = `${nativeCode}
3095
+ //# sourceMappingURL=${path11.basename(mapFile)}`;
3096
+ const relMap = toPosix(path11.relative(outputDir, mapFile));
3097
+ buildStats[relMap] = {
3098
+ bytes: artifact.map_bytes,
3099
+ emitter: "native",
3100
+ type: "map"
3101
+ };
3102
+ jsFiles.push(relMap);
3103
+ }
3104
+ await fs9.promises.writeFile(nativeFile, nativeCode, "utf8");
3105
+ const relNative = toPosix(path11.relative(outputDir, nativeFile));
3106
+ buildStats[relNative] = {
3107
+ bytes: artifact.code_bytes,
3108
+ emitter: "native",
3109
+ type: "js"
3110
+ };
3111
+ jsFiles.push(relNative);
3112
+ await copyAssets(artifact.assets);
3113
+ }
3114
+ if (chunk.css.length) {
3115
+ const seenCss = /* @__PURE__ */ new Set();
3116
+ const cssSources = [];
3117
+ for (const cssPath of chunk.css) {
3118
+ if (!seenCss.add(cssPath)) continue;
3119
+ const output = moduleOutputs.get(cssPath);
3120
+ if (output?.type === "css") {
3121
+ cssSources.push(output.code);
3122
+ } else if (fs9.existsSync(cssPath)) {
3123
+ try {
3124
+ cssSources.push(await fs9.promises.readFile(cssPath, "utf8"));
3125
+ } catch (err) {
3126
+ logWarn(`Failed to read CSS source ${cssPath}: ${String(err)}`);
3127
+ }
3128
+ }
3129
+ }
3130
+ if (cssSources.length) {
3131
+ const combinedCss = cssSources.join("\n\n");
3132
+ const cssHash = crypto3.createHash("sha256").update(combinedCss).digest("hex").slice(0, 8);
3133
+ const cssFileName = `${chunk.id}.${cssHash}.native.css`;
3134
+ const cssFilePath = path11.join(chunkOutDir, cssFileName);
3135
+ await fs9.promises.writeFile(cssFilePath, combinedCss, "utf8");
3136
+ const relCss = path11.relative(outputDir, cssFilePath);
3137
+ buildStats[relCss] = {
3138
+ bytes: Buffer.byteLength(combinedCss),
3139
+ emitter: "native",
3140
+ type: "css"
3141
+ };
3142
+ cssFiles.push(relCss);
3143
+ }
3144
+ }
3145
+ results.push({
3146
+ id: chunk.id,
3147
+ files: {
3148
+ js: jsFiles,
3149
+ css: cssFiles,
3150
+ assets: assetFiles
3151
+ }
3152
+ });
3153
+ }
3154
+ return { artifacts: results, stats: buildStats };
3155
+ }
3156
+ async function writeAssetsManifest(outputDir, artifacts) {
3157
+ const dir = path11.resolve(outputDir);
3158
+ await fs9.promises.mkdir(dir, { recursive: true });
3159
+ const file = path11.join(dir, "manifest.assets.json");
3160
+ const payload = {
3161
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3162
+ chunks: artifacts
3163
+ };
3164
+ await fs9.promises.writeFile(file, JSON.stringify(payload, null, 2), "utf8");
3165
+ }
3166
+
3167
+ // src/core/worker/pool.ts
3168
+ import { Worker } from "worker_threads";
3169
+ import os from "os";
3170
+ import { fileURLToPath as fileURLToPath3 } from "url";
3171
+ var workerPath = fileURLToPath3(new URL("./worker.cjs", import.meta.url));
3172
+ var TransformWorkerPool = class {
3173
+ workers = [];
3174
+ queue = [];
3175
+ active = /* @__PURE__ */ new Map();
3176
+ callbacks = /* @__PURE__ */ new Map();
3177
+ waiters = [];
3178
+ pendingBytes = 0;
3179
+ closed = false;
3180
+ size;
3181
+ maxQueueBytes;
3182
+ constructor(options = {}) {
3183
+ const cpuDefault = Math.max(1, os.cpus().length - 1);
3184
+ this.size = Math.max(1, options.size ?? cpuDefault);
3185
+ this.maxQueueBytes = options.maxQueueBytes;
3186
+ for (let i = 0; i < this.size; i++) {
3187
+ this.spawnWorker();
3188
+ }
3189
+ }
3190
+ spawnWorker() {
3191
+ const worker = new Worker(workerPath, { env: process.env });
3192
+ const id = worker.threadId;
3193
+ worker.on("message", (message) => {
3194
+ const item = this.active.get(id);
3195
+ if (item) {
3196
+ this.active.delete(id);
3197
+ this.pendingBytes -= item.size;
3198
+ this.resolveWaiters();
3199
+ }
3200
+ const cb = message ? this.callbacks.get(message.id) : void 0;
3201
+ if (message && cb) cb(message);
3202
+ if (message) this.callbacks.delete(message.id);
3203
+ this.dequeue(worker);
3204
+ });
3205
+ worker.on("error", (err) => {
3206
+ logWarn(`Transform worker error: ${String(err)}`);
3207
+ const item = this.active.get(id);
3208
+ if (item) {
3209
+ this.active.delete(id);
3210
+ this.queue.unshift(item);
3211
+ }
3212
+ this.spawnWorker();
3213
+ });
3214
+ worker.on("exit", (code) => {
3215
+ const item = this.active.get(id);
3216
+ if (item) {
3217
+ this.active.delete(id);
3218
+ this.queue.unshift(item);
3219
+ }
3220
+ if (!this.closed && code !== 0) {
3221
+ logWarn(`Transform worker exited unexpectedly (${code}), respawning`);
3222
+ this.spawnWorker();
3223
+ }
3224
+ });
3225
+ this.workers.push(worker);
3226
+ }
3227
+ dequeue(worker) {
3228
+ if (this.queue.length === 0) return;
3229
+ const item = this.queue.shift();
3230
+ this.active.set(worker.threadId, item);
3231
+ worker.postMessage(item.job);
3232
+ }
3233
+ resolveWaiters() {
3234
+ if (!this.maxQueueBytes) return;
3235
+ while (this.waiters.length && this.pendingBytes < this.maxQueueBytes) {
3236
+ const resolve = this.waiters.shift();
3237
+ resolve && resolve();
3238
+ }
3239
+ }
3240
+ async run(job) {
3241
+ if (this.closed) {
3242
+ throw new Error("Worker pool already closed");
3243
+ }
3244
+ const size = Buffer.byteLength(job.code, "utf8");
3245
+ if (this.maxQueueBytes) {
3246
+ while (this.pendingBytes + size > this.maxQueueBytes) {
3247
+ await new Promise((resolve) => this.waiters.push(resolve));
3248
+ await new Promise((r) => setTimeout(r, 50 + Math.random() * 100));
3249
+ }
3250
+ }
3251
+ this.pendingBytes += size;
3252
+ return new Promise((resolve) => {
3253
+ this.callbacks.set(job.id, resolve);
3254
+ const idleWorker = this.workers.find((w) => !this.active.has(w.threadId));
3255
+ const item = { job, size };
3256
+ if (idleWorker) {
3257
+ this.active.set(idleWorker.threadId, item);
3258
+ idleWorker.postMessage(job);
3259
+ } else {
3260
+ this.queue.push(item);
3261
+ }
3262
+ });
3263
+ }
3264
+ async runMany(jobs) {
3265
+ const resultMap = /* @__PURE__ */ new Map();
3266
+ await Promise.all(
3267
+ jobs.map(async (job) => {
3268
+ const res = await this.run(job);
3269
+ resultMap.set(job.id, res);
3270
+ })
3271
+ );
3272
+ return jobs.map((job) => resultMap.get(job.id));
3273
+ }
3274
+ async close() {
3275
+ this.closed = true;
3276
+ await Promise.all(this.workers.map((worker) => worker.terminate()));
3277
+ this.workers = [];
3278
+ this.queue = [];
3279
+ this.active.clear();
3280
+ this.callbacks.clear();
3281
+ this.waiters.forEach((resolve) => resolve());
3282
+ this.waiters = [];
3283
+ this.pendingBytes = 0;
3284
+ }
3285
+ async drain() {
3286
+ while (!this.closed && (this.queue.length || this.active.size)) {
3287
+ await new Promise((r) => setTimeout(r, 100));
3288
+ }
3289
+ }
3290
+ };
3291
+
3292
+ // src/cli/commands/build.ts
3293
+ async function runBuildCommand(options = {}) {
3294
+ try {
3295
+ const config = await loadIonifyConfig();
3296
+ const optLevel = resolveOptimizationLevel(config?.optimizationLevel, {
3297
+ cliLevel: options.level,
3298
+ envLevel: process.env.IONIFY_OPTIMIZATION_LEVEL
3299
+ });
3300
+ let minifier;
3301
+ const parserMode = resolveParser(config, { envMode: process.env.IONIFY_PARSER });
3302
+ let treeshake;
3303
+ let scopeHoist;
3304
+ if (optLevel !== null) {
3305
+ const preset = getOptimizationPreset(optLevel);
3306
+ minifier = preset.minifier;
3307
+ treeshake = preset.treeshake;
3308
+ scopeHoist = preset.scopeHoist;
3309
+ logInfo(`Using optimization level ${optLevel} (preset)`);
3310
+ } else {
3311
+ minifier = resolveMinifier(config, { envVar: process.env.IONIFY_MINIFIER });
3312
+ treeshake = resolveTreeshake(config?.treeshake, {
3313
+ envMode: process.env.IONIFY_TREESHAKE,
3314
+ includeEnv: process.env.IONIFY_TREESHAKE_INCLUDE,
3315
+ excludeEnv: process.env.IONIFY_TREESHAKE_EXCLUDE
3316
+ });
3317
+ scopeHoist = resolveScopeHoist(config?.scopeHoist, {
3318
+ envMode: process.env.IONIFY_SCOPE_HOIST,
3319
+ inlineEnv: process.env.IONIFY_SCOPE_HOIST_INLINE,
3320
+ constantEnv: process.env.IONIFY_SCOPE_HOIST_CONST,
3321
+ combineEnv: process.env.IONIFY_SCOPE_HOIST_COMBINE
3322
+ });
3323
+ }
3324
+ applyMinifierEnv(minifier);
3325
+ applyParserEnv(parserMode);
3326
+ applyTreeshakeEnv(treeshake);
3327
+ applyScopeHoistEnv(scopeHoist);
3328
+ const entries = config?.entry ? [config.entry.startsWith("/") ? path12.join(process.cwd(), config.entry) : path12.resolve(process.cwd(), config.entry)] : void 0;
3329
+ if (entries) {
3330
+ logInfo(`Build entries: ${entries.join(", ")}`);
3331
+ } else {
3332
+ logInfo(`No entries in config, planner will infer from graph`);
3333
+ }
3334
+ const pluginNames = Array.isArray(config?.plugins) ? config.plugins.map((p) => typeof p === "string" ? p : p?.name).filter((name) => typeof name === "string" && name.length > 0) : void 0;
3335
+ const rawVersionInputs = {
3336
+ parserMode,
3337
+ minifier,
3338
+ treeshake,
3339
+ scopeHoist,
3340
+ plugins: pluginNames,
3341
+ entry: entries ?? null,
3342
+ cssOptions: config?.css,
3343
+ assetOptions: config?.assets ?? config?.asset
3344
+ };
3345
+ const configHash = computeGraphVersion(rawVersionInputs);
3346
+ logInfo(`[Build] Version hash: ${configHash}`);
3347
+ process.env.IONIFY_CONFIG_HASH = configHash;
3348
+ if (native?.initAstCache) {
3349
+ const versionHash = JSON.stringify(rawVersionInputs);
3350
+ native.initAstCache(versionHash);
3351
+ logInfo(`AST cache initialized with version hash`);
3352
+ }
3353
+ const plan = await generateBuildPlan(entries, rawVersionInputs);
3354
+ const outDir = options.outDir || "dist";
3355
+ const moduleHashes = /* @__PURE__ */ new Map();
3356
+ for (const chunk of plan.chunks) {
3357
+ for (const mod of chunk.modules) {
3358
+ if (mod.hash) {
3359
+ moduleHashes.set(mod.id, mod.hash);
3360
+ }
3361
+ }
3362
+ }
3363
+ const uniqueModules = /* @__PURE__ */ new Set();
3364
+ for (const chunk of plan.chunks) {
3365
+ for (const mod of chunk.modules) uniqueModules.add(mod.id);
3366
+ }
3367
+ const moduleOutputs = /* @__PURE__ */ new Map();
3368
+ const pool = new TransformWorkerPool();
3369
+ try {
3370
+ const jobs = Array.from(uniqueModules).filter((filePath) => fs10.existsSync(filePath)).map((filePath) => {
3371
+ const code = fs10.readFileSync(filePath, "utf8");
3372
+ const sourceHash = getCacheKey(code);
3373
+ const moduleHash = moduleHashes.get(filePath) ?? sourceHash;
3374
+ const cacheKey = getCacheKey(`build-worker:v1:${path12.extname(filePath)}:${moduleHash}:${filePath}`);
3375
+ const cached = readCache(cacheKey);
3376
+ if (cached) {
3377
+ try {
3378
+ const parsed = JSON.parse(cached.toString("utf8"));
3379
+ if (parsed?.code) {
3380
+ const transformedHash = getCacheKey(parsed.code);
3381
+ moduleHashes.set(filePath, transformedHash);
3382
+ const casRoot2 = path12.join(process.cwd(), ".ionify", "cas");
3383
+ const cacheDir = getCasArtifactPath(casRoot2, configHash, transformedHash);
3384
+ if (!fs10.existsSync(path12.join(cacheDir, "transformed.js"))) {
3385
+ fs10.mkdirSync(cacheDir, { recursive: true });
3386
+ fs10.writeFileSync(path12.join(cacheDir, "transformed.js"), parsed.code, "utf8");
3387
+ if (parsed.map) {
3388
+ fs10.writeFileSync(path12.join(cacheDir, "transformed.js.map"), parsed.map, "utf8");
3389
+ }
3390
+ }
3391
+ moduleOutputs.set(filePath, { code: parsed.code, type: parsed.type ?? "js" });
3392
+ return null;
3393
+ }
3394
+ } catch {
3395
+ }
3396
+ }
3397
+ return {
3398
+ id: filePath,
3399
+ filePath,
3400
+ ext: path12.extname(filePath),
3401
+ code,
3402
+ cacheKey
3403
+ };
3404
+ }).filter((job) => !!job);
3405
+ const results = await pool.runMany(
3406
+ jobs.map((job) => ({
3407
+ id: job.id,
3408
+ filePath: job.filePath,
3409
+ ext: job.ext,
3410
+ code: job.code
3411
+ }))
3412
+ );
3413
+ for (let i = 0; i < results.length; i++) {
3414
+ const result = results[i];
3415
+ const job = jobs[i];
3416
+ if (result.error) {
3417
+ throw new Error(`Transform failed for ${result.filePath}: ${result.error}`);
3418
+ }
3419
+ const payload = JSON.stringify({ code: result.code, map: result.map, type: result.type });
3420
+ writeCache(job.cacheKey, Buffer.from(payload));
3421
+ const transformedHash = getCacheKey(result.code);
3422
+ const moduleHash = transformedHash;
3423
+ const casRoot2 = path12.join(process.cwd(), ".ionify", "cas");
3424
+ const versionHash = configHash;
3425
+ const cacheDir = getCasArtifactPath(casRoot2, versionHash, moduleHash);
3426
+ fs10.mkdirSync(cacheDir, { recursive: true });
3427
+ fs10.writeFileSync(path12.join(cacheDir, "transformed.js"), result.code, "utf8");
3428
+ if (result.map) {
3429
+ fs10.writeFileSync(path12.join(cacheDir, "transformed.js.map"), result.map, "utf8");
3430
+ }
3431
+ moduleHashes.set(job.filePath, moduleHash);
3432
+ for (const chunk of plan.chunks) {
3433
+ for (const mod of chunk.modules) {
3434
+ if (mod.id === job.filePath) {
3435
+ mod.hash = moduleHash;
3436
+ }
3437
+ }
3438
+ }
3439
+ moduleOutputs.set(result.filePath, { code: result.code, type: result.type });
3440
+ }
3441
+ } finally {
3442
+ await pool.close();
3443
+ }
3444
+ for (const chunk of plan.chunks) {
3445
+ for (const mod of chunk.modules) {
3446
+ const updatedHash = moduleHashes.get(mod.id);
3447
+ if (updatedHash) {
3448
+ mod.hash = updatedHash;
3449
+ }
3450
+ }
3451
+ }
3452
+ const absOutDir = path12.resolve(outDir);
3453
+ const casRoot = path12.join(process.cwd(), ".ionify", "cas");
3454
+ const { artifacts, stats } = await emitChunks(absOutDir, plan, moduleOutputs, {
3455
+ casRoot,
3456
+ versionHash: configHash
3457
+ });
3458
+ await writeBuildManifest(absOutDir, plan, artifacts);
3459
+ await writeAssetsManifest(absOutDir, artifacts);
3460
+ await fs10.promises.writeFile(
3461
+ path12.join(absOutDir, "build.stats.json"),
3462
+ JSON.stringify(stats, null, 2),
3463
+ "utf8"
3464
+ );
3465
+ logInfo(`Build plan generated \u2192 ${path12.join(absOutDir, "manifest.json")}`);
3466
+ logInfo(`Entries: ${plan.entries.length}, Chunks: ${plan.chunks.length}`);
3467
+ logInfo(`Modules transformed: ${moduleOutputs.size}`);
3468
+ } catch (err) {
3469
+ logError("ionify build failed", err);
3470
+ throw err;
3471
+ }
3472
+ }
3473
+
390
3474
  // src/cli/index.ts
391
3475
  var program = new Command();
392
- program.name("ionify").description("Ionify \u2013 Instant, Intelligent, Unified Build Engine").version("0.1.0");
3476
+ program.name("ionify").description("Ionify \u2013 Instant, Intelligent, Unified Build Engine").version("0.0.1");
393
3477
  program.command("dev").description("Start Ionify development server").option("-p, --port <port>", "Port to run the server on", "5173").action(async (options) => {
394
3478
  try {
395
- await startDevServer(options);
3479
+ const port = parseInt(options.port, 10);
3480
+ await startDevServer({ port });
396
3481
  } catch (err) {
397
- logError(`Dev server failed: ${err.message}`);
3482
+ logError("Failed to start dev server", err);
398
3483
  process.exit(1);
399
3484
  }
400
3485
  });
401
- program.command("build").description("Build for production").option("-o, --outDir <dir>", "Output directory", "dist").option("-l, --level <level>", "Optimization level (0-4)", "3").action(async (options) => {
3486
+ program.command("build").description("Create production build using Ionify bundler").option("-o, --out-dir <dir>", "Output directory", "dist").action(async (options) => {
402
3487
  try {
403
- await runBuildCommand(options);
404
- } catch (err) {
405
- logError(`Build failed: ${err.message}`);
3488
+ await runBuildCommand({ outDir: options.outDir });
3489
+ } catch {
406
3490
  process.exit(1);
407
3491
  }
408
3492
  });
409
- program.command("analyze").description("Analyze bundle and performance").option("-f, --format <format>", "Output format (json|text)", "text").action(async (options) => {
3493
+ program.command("migrate").description("Migrate from Vite/Rollup config (not implemented yet)").action(() => logInfo("Migrate command coming soon..."));
3494
+ program.command("analyze").description("Inspect cached dependency graph stats").option("--json", "Output summary as JSON").option("-l, --limit <count>", "Limit list outputs", "10").action(async (options) => {
410
3495
  try {
411
- await runAnalyzeCommand(options);
3496
+ const limit = parseInt(options.limit ?? "10", 10);
3497
+ await runAnalyzeCommand({ json: !!options.json, limit: Number.isFinite(limit) ? limit : 10 });
412
3498
  } catch (err) {
413
- logError(`Analyzer failed: ${err.message}`);
3499
+ logError("Analyzer failed", err);
414
3500
  process.exit(1);
415
3501
  }
416
3502
  });
417
- program.parse();
3503
+ program.parse(process.argv);