@ionify/ionify 0.1.0 → 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,74 +21,2424 @@ 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/cli/commands/analyze.ts
17
- import fs2 from "fs";
18
- import path2 from "path";
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
41
+ import fs from "fs";
42
+ import path from "path";
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;
94
+ try {
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
+ }
102
+ } catch {
103
+ }
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 {
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;
129
+ } catch {
130
+ } finally {
131
+ if (this.saveTimer) {
132
+ clearTimeout(this.saveTimer);
133
+ this.saveTimer = null;
134
+ }
135
+ }
136
+ }
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
+ };
279
+
280
+ // src/core/resolver.ts
281
+ import fs2 from "fs";
282
+ import path2 from "path";
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"];
287
+ var swc = null;
288
+ (() => {
289
+ try {
290
+ const require2 = createRequire(import.meta.url);
291
+ swc = require2("@swc/core");
292
+ } catch {
293
+ swc = null;
294
+ }
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;
407
+ var customAliasEntries = [];
408
+ var resolvePathCache = /* @__PURE__ */ new Map();
409
+ function createAliasEntry(pattern, targets) {
410
+ const hasWildcard = pattern.includes("*");
411
+ if (hasWildcard) {
412
+ const escaped = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&");
413
+ const matcher = new RegExp(`^${escaped.replace(/\*/g, "(.*)")}$`);
414
+ return {
415
+ resolveCandidates(specifier) {
416
+ const match = matcher.exec(specifier);
417
+ if (!match) return [];
418
+ const wildcards = match.slice(1);
419
+ return targets.map((target) => {
420
+ if (!target.includes("*")) return target;
421
+ const segments = target.split("*");
422
+ let rebuilt = segments[0] ?? "";
423
+ for (let i = 1; i < segments.length; i++) {
424
+ const replacement = wildcards[i - 1] ?? wildcards[wildcards.length - 1] ?? "";
425
+ rebuilt += replacement + segments[i];
426
+ }
427
+ return rebuilt;
428
+ });
429
+ }
430
+ };
431
+ }
432
+ const normalizedPattern = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
433
+ return {
434
+ resolveCandidates(specifier) {
435
+ if (specifier === normalizedPattern) {
436
+ return targets;
437
+ }
438
+ if (normalizedPattern && specifier.startsWith(normalizedPattern + "/")) {
439
+ const remainder = specifier.slice(normalizedPattern.length + 1);
440
+ return targets.map((target) => path2.join(target, remainder));
441
+ }
442
+ return [];
443
+ }
444
+ };
445
+ }
446
+ function buildAliasEntries(aliases, baseDir) {
447
+ const entries = [];
448
+ for (const [pattern, value] of Object.entries(aliases)) {
449
+ const replacements = Array.isArray(value) ? value : [value];
450
+ const targets = replacements.filter((rep) => typeof rep === "string" && rep.trim().length > 0).map(
451
+ (rep) => path2.isAbsolute(rep) ? rep : path2.resolve(baseDir, rep)
452
+ );
453
+ if (!targets.length) continue;
454
+ entries.push(createAliasEntry(pattern, targets));
455
+ }
456
+ return entries;
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
+ }
498
+ function configureResolverAliases(aliases, baseDir) {
499
+ customAliasEntries = aliases ? buildAliasEntries(aliases, baseDir) : [];
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
+ });
1434
+
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";
1440
+ var CONFIG_BASENAMES = [
1441
+ "ionify.config.ts",
1442
+ "ionify.config.mts",
1443
+ "ionify.config.js",
1444
+ "ionify.config.mjs",
1445
+ "ionify.config.cjs"
1446
+ ];
1447
+ var cachedConfig2 = null;
1448
+ var configLoaded = false;
1449
+ async function bundleConfig(entry) {
1450
+ const absDir = path7.dirname(entry);
1451
+ const inlineIonifyPlugin = {
1452
+ name: "inline-ionify",
1453
+ setup(build2) {
1454
+ build2.onResolve({ filter: /^ionify$/ }, () => ({
1455
+ path: "ionify-virtual",
1456
+ namespace: "ionify-ns"
1457
+ }));
1458
+ build2.onLoad({ filter: /.*/, namespace: "ionify-ns" }, () => ({
1459
+ contents: `
1460
+ export function defineConfig(config) {
1461
+ return typeof config === 'function' ? config : () => config;
1462
+ }
1463
+ `,
1464
+ loader: "js"
1465
+ }));
1466
+ }
1467
+ };
1468
+ const result = await build({
1469
+ entryPoints: [entry],
1470
+ bundle: true,
1471
+ platform: "node",
1472
+ format: "esm",
1473
+ sourcemap: "inline",
1474
+ write: false,
1475
+ target: "node18",
1476
+ logLevel: "silent",
1477
+ absWorkingDir: absDir,
1478
+ plugins: [inlineIonifyPlugin]
1479
+ });
1480
+ const output = result.outputFiles?.[0];
1481
+ if (!output) throw new Error("Failed to bundle ionify config");
1482
+ const dirnameLiteral = JSON.stringify(absDir);
1483
+ const filenameLiteral = JSON.stringify(entry);
1484
+ const importMetaLiteral = JSON.stringify(pathToFileURL2(entry).href);
1485
+ let contents = output.text;
1486
+ if (contents.includes("import.meta.url")) {
1487
+ contents = contents.replace(/import\.meta\.url/g, "__IONIFY_IMPORT_META_URL");
1488
+ contents = `const __IONIFY_IMPORT_META_URL = ${importMetaLiteral};
1489
+ ${contents}`;
1490
+ }
1491
+ const preamble = `const __dirname = ${dirnameLiteral};
1492
+ const __filename = ${filenameLiteral};
1493
+ `;
1494
+ return preamble + contents;
1495
+ }
1496
+ function findConfigFile(cwd) {
1497
+ for (const name of CONFIG_BASENAMES) {
1498
+ const candidate = path7.resolve(cwd, name);
1499
+ if (fs5.existsSync(candidate) && fs5.statSync(candidate).isFile()) {
1500
+ return candidate;
1501
+ }
1502
+ }
1503
+ return null;
1504
+ }
1505
+ async function loadIonifyConfig(cwd = process.cwd()) {
1506
+ if (configLoaded) return cachedConfig2;
1507
+ configLoaded = true;
1508
+ const configPath = findConfigFile(cwd);
1509
+ if (!configPath) {
1510
+ cachedConfig2 = null;
1511
+ configureResolverAliases(void 0, cwd);
1512
+ return cachedConfig2;
1513
+ }
1514
+ try {
1515
+ const bundled = await bundleConfig(configPath);
1516
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(bundled).toString("base64")}`;
1517
+ const imported = await import(dataUrl);
1518
+ let resolved = imported?.default ?? imported?.config ?? imported ?? null;
1519
+ if (resolved && typeof resolved === "function") {
1520
+ resolved = resolved({ mode: process.env.NODE_ENV || "development" });
1521
+ }
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
+ }
19
1765
 
20
- // src/native/index.ts
21
- import fs from "fs";
22
- import path from "path";
23
- import { createRequire } from "module";
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
+ }
24
1785
 
25
- // src/core/version.ts
26
- import { createHash } from "crypto";
27
-
28
- // src/native/index.ts
29
- function resolveCandidates() {
30
- const cwd = process.cwd();
31
- const releaseDir = path.resolve(cwd, "target", "release");
32
- const debugDir = path.resolve(cwd, "target", "debug");
33
- const nativeDir = path.resolve(cwd, "native");
34
- const moduleDir = path.dirname(new URL(import.meta.url).pathname);
35
- const packageNativeDir = path.resolve(moduleDir, "..", "native");
36
- const packageDistDir = path.resolve(moduleDir, "..");
37
- const platformFile = process.platform === "win32" ? "ionify_core.dll" : process.platform === "darwin" ? "libionify_core.dylib" : "libionify_core.so";
38
- const candidates = [
39
- // Installed package locations (checked first)
40
- path.join(packageDistDir, "ionify_core.node"),
41
- path.join(packageNativeDir, "ionify_core.node"),
42
- // Development locations
43
- path.join(nativeDir, "ionify_core.node"),
44
- path.join(releaseDir, "ionify_core.node"),
45
- path.join(releaseDir, platformFile),
46
- path.join(debugDir, "ionify_core.node"),
47
- path.join(debugDir, platformFile)
48
- ];
49
- return candidates.filter((candidate) => {
50
- try {
51
- return fs.existsSync(candidate) && fs.statSync(candidate).isFile();
52
- } catch {
53
- return false;
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
+ }
54
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 || {}
55
1890
  });
56
- }
57
- var nativeBinding = null;
58
- (() => {
59
- const require2 = createRequire(import.meta.url);
60
- for (const candidate of resolveCandidates()) {
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) => {
61
2008
  try {
62
- const mod = require2(candidate);
63
- if (mod) {
64
- nativeBinding = mod;
65
- break;
2009
+ const parsed = url.parse(req.url || "/", true);
2010
+ let reqPath = parsed.pathname || "/";
2011
+ try {
2012
+ reqPath = decodeURIComponent(reqPath);
2013
+ } catch {
66
2014
  }
67
- } catch {
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);
2311
+ }
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
+ }
2327
+ }
2328
+ modules.push({
2329
+ absPath,
2330
+ url: normalizeUrlFromFs(rootDir, absPath),
2331
+ hash,
2332
+ reason
2333
+ });
2334
+ }
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);
68
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 });
69
2417
  }
70
- })();
71
- var native = nativeBinding;
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
+ };
2432
+ }
72
2433
 
73
2434
  // src/cli/commands/analyze.ts
2435
+ import fs8 from "fs";
2436
+ import path10 from "path";
74
2437
  function readGraphFromDisk(root) {
75
- const file = path2.join(root, ".ionify", "graph.json");
76
- if (!fs2.existsSync(file)) return null;
2438
+ const file = path10.join(root, ".ionify", "graph.json");
2439
+ if (!fs8.existsSync(file)) return null;
77
2440
  try {
78
- const raw = fs2.readFileSync(file, "utf8");
2441
+ const raw = fs8.readFileSync(file, "utf8");
79
2442
  const snapshot = JSON.parse(raw);
80
2443
  if (snapshot?.version !== 1 || !snapshot?.nodes) return null;
81
2444
  return Object.entries(snapshot.nodes).map(([id, node]) => ({
@@ -170,23 +2533,971 @@ async function runAnalyzeCommand(options = {}) {
170
2533
  }
171
2534
  }
172
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
+
173
3474
  // src/cli/index.ts
174
3475
  var program = new Command();
175
- program.name("ionify").description("Ionify \u2013 Instant, Intelligent, Unified Build Engine").version("0.1.0");
176
- program.command("dev").description("Start Ionify development server (coming in full release)").option("-p, --port <port>", "Port to run the server on", "5173").action(async () => {
177
- logError("Dev server is not available in this release. Stay tuned!");
178
- process.exit(1);
3476
+ program.name("ionify").description("Ionify \u2013 Instant, Intelligent, Unified Build Engine").version("0.0.1");
3477
+ program.command("dev").description("Start Ionify development server").option("-p, --port <port>", "Port to run the server on", "5173").action(async (options) => {
3478
+ try {
3479
+ const port = parseInt(options.port, 10);
3480
+ await startDevServer({ port });
3481
+ } catch (err) {
3482
+ logError("Failed to start dev server", err);
3483
+ process.exit(1);
3484
+ }
179
3485
  });
180
- program.command("build").description("Build for production (coming in full release)").option("-o, --outDir <dir>", "Output directory", "dist").option("-l, --level <level>", "Optimization level (0-4)", "3").action(async () => {
181
- logError("Build command is not available in this release. Stay tuned!");
182
- process.exit(1);
3486
+ program.command("build").description("Create production build using Ionify bundler").option("-o, --out-dir <dir>", "Output directory", "dist").action(async (options) => {
3487
+ try {
3488
+ await runBuildCommand({ outDir: options.outDir });
3489
+ } catch {
3490
+ process.exit(1);
3491
+ }
183
3492
  });
184
- 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) => {
185
3495
  try {
186
- await runAnalyzeCommand(options);
3496
+ const limit = parseInt(options.limit ?? "10", 10);
3497
+ await runAnalyzeCommand({ json: !!options.json, limit: Number.isFinite(limit) ? limit : 10 });
187
3498
  } catch (err) {
188
- logError(`Analyzer failed: ${err.message}`);
3499
+ logError("Analyzer failed", err);
189
3500
  process.exit(1);
190
3501
  }
191
3502
  });
192
- program.parse();
3503
+ program.parse(process.argv);