@macroforge/vite-plugin 0.1.76 → 0.1.77
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/package.json +3 -3
- package/src/index.js +328 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@macroforge/shared": "^0.1.
|
|
4
|
-
"macroforge": "^0.1.
|
|
3
|
+
"@macroforge/shared": "^0.1.77",
|
|
4
|
+
"macroforge": "^0.1.77"
|
|
5
5
|
},
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@types/node": "^22.0.0"
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
},
|
|
31
31
|
"type": "module",
|
|
32
32
|
"types": "src/index.d.ts",
|
|
33
|
-
"version": "0.1.
|
|
33
|
+
"version": "0.1.77"
|
|
34
34
|
}
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* typesOutputDir: ".macroforge/types", // Types output dir (default: ".macroforge/types")
|
|
32
32
|
* emitMetadata: true, // Emit metadata JSON (default: true)
|
|
33
33
|
* metadataOutputDir: ".macroforge/meta", // Metadata output dir (default: ".macroforge/meta")
|
|
34
|
+
* devCache: true, // Disk cache for dev mode (default: true)
|
|
34
35
|
* },
|
|
35
36
|
* };
|
|
36
37
|
* ```
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
*/
|
|
40
41
|
|
|
41
42
|
import { createRequire } from "node:module";
|
|
43
|
+
import { createHash } from "node:crypto";
|
|
42
44
|
import * as fs from "node:fs";
|
|
43
45
|
import * as path from "node:path";
|
|
44
46
|
import {
|
|
@@ -329,10 +331,17 @@ function emitDeclarationsFromCode(code, fileName, projectRoot) {
|
|
|
329
331
|
export async function macroforge() {
|
|
330
332
|
/**
|
|
331
333
|
* Reference to the loaded Macroforge Rust binary module.
|
|
332
|
-
* @type {{ expandSync: Function, loadConfig?: (content: string, filepath: string) => any } | undefined}
|
|
334
|
+
* @type {{ expandSync: Function, loadConfig?: (content: string, filepath: string) => any, scanProjectSync?: Function } | undefined}
|
|
333
335
|
*/
|
|
334
336
|
let rustTransformer;
|
|
335
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Cached type registry JSON from project scanning.
|
|
340
|
+
* Built during `buildStart` and passed to every `expandSync` call.
|
|
341
|
+
* @type {string | undefined}
|
|
342
|
+
*/
|
|
343
|
+
let typeRegistryJson;
|
|
344
|
+
|
|
336
345
|
// Load the Rust binary first
|
|
337
346
|
try {
|
|
338
347
|
const projectRequire = createRequire(process.cwd() + "/");
|
|
@@ -366,6 +375,8 @@ export async function macroforge() {
|
|
|
366
375
|
let emitMetadata = true;
|
|
367
376
|
/** @type {string} */
|
|
368
377
|
let metadataOutputDir = ".macroforge/meta";
|
|
378
|
+
/** @type {boolean} */
|
|
379
|
+
let devCacheEnabled = true;
|
|
369
380
|
|
|
370
381
|
// Load vite-specific options from the config file
|
|
371
382
|
if (macroConfig.configPath) {
|
|
@@ -387,6 +398,9 @@ export async function macroforge() {
|
|
|
387
398
|
if (viteConfig.metadataOutputDir !== undefined) {
|
|
388
399
|
metadataOutputDir = viteConfig.metadataOutputDir;
|
|
389
400
|
}
|
|
401
|
+
if (viteConfig.devCache !== undefined) {
|
|
402
|
+
devCacheEnabled = viteConfig.devCache;
|
|
403
|
+
}
|
|
390
404
|
}
|
|
391
405
|
} catch (error) {
|
|
392
406
|
throw new Error(
|
|
@@ -398,6 +412,20 @@ export async function macroforge() {
|
|
|
398
412
|
/** @type {string} */
|
|
399
413
|
let projectRoot;
|
|
400
414
|
|
|
415
|
+
// --- Dev cache state ---
|
|
416
|
+
/** @type {boolean} */
|
|
417
|
+
let isDevMode = false;
|
|
418
|
+
/** @type {string | undefined} */
|
|
419
|
+
let cacheDir;
|
|
420
|
+
/** @type {{ version: string, configHash: string, entries: Record<string, { sourceHash: string, hasMacros: boolean }> } | null} */
|
|
421
|
+
let cacheManifest = null;
|
|
422
|
+
/** @type {string} */
|
|
423
|
+
let macroforgeVersion = "unknown";
|
|
424
|
+
/** @type {boolean} */
|
|
425
|
+
let cacheManifestDirty = false;
|
|
426
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
427
|
+
let manifestFlushTimer;
|
|
428
|
+
|
|
401
429
|
/**
|
|
402
430
|
* Ensures a directory exists, creating it recursively if necessary.
|
|
403
431
|
* @param {string} dir
|
|
@@ -408,6 +436,185 @@ export async function macroforge() {
|
|
|
408
436
|
}
|
|
409
437
|
}
|
|
410
438
|
|
|
439
|
+
// --- Dev cache helpers ---
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Computes SHA-256 hash of a string, returned as hex.
|
|
443
|
+
* @param {string} content
|
|
444
|
+
* @returns {string}
|
|
445
|
+
*/
|
|
446
|
+
function contentHash(content) {
|
|
447
|
+
return createHash("sha256").update(content).digest("hex");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Reads the installed macroforge NAPI package version.
|
|
452
|
+
* Resolves the module's main entry point, then reads package.json
|
|
453
|
+
* from the same directory (avoids exports-map restrictions).
|
|
454
|
+
* @returns {string}
|
|
455
|
+
*/
|
|
456
|
+
function getMacroforgeVersion() {
|
|
457
|
+
try {
|
|
458
|
+
const req = createRequire(process.cwd() + "/");
|
|
459
|
+
const mainPath = req.resolve("macroforge");
|
|
460
|
+
const pkgDir = path.dirname(mainPath);
|
|
461
|
+
const pkgJson = JSON.parse(
|
|
462
|
+
fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8"),
|
|
463
|
+
);
|
|
464
|
+
return pkgJson.version;
|
|
465
|
+
} catch {
|
|
466
|
+
return "unknown";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Computes a hash of the macroforge config file for cache invalidation.
|
|
472
|
+
* @returns {string}
|
|
473
|
+
*/
|
|
474
|
+
function getConfigHash() {
|
|
475
|
+
if (macroConfig.configPath) {
|
|
476
|
+
try {
|
|
477
|
+
return contentHash(fs.readFileSync(macroConfig.configPath, "utf-8"));
|
|
478
|
+
} catch {
|
|
479
|
+
// config file disappeared
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return "none";
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Loads and validates the cache manifest from disk.
|
|
487
|
+
* Returns null if the cache is stale (version or config mismatch).
|
|
488
|
+
* @returns {{ version: string, configHash: string, entries: Record<string, { sourceHash: string, hasMacros: boolean }> } | null}
|
|
489
|
+
*/
|
|
490
|
+
function loadCacheManifest() {
|
|
491
|
+
const manifestPath = path.join(cacheDir, "manifest.json");
|
|
492
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
496
|
+
|
|
497
|
+
if (manifest.version !== macroforgeVersion) {
|
|
498
|
+
console.log(
|
|
499
|
+
"[@macroforge/vite-plugin] Cache invalidated: macroforge version changed",
|
|
500
|
+
);
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const currentConfigHash = getConfigHash();
|
|
505
|
+
if (manifest.configHash !== currentConfigHash) {
|
|
506
|
+
console.log(
|
|
507
|
+
"[@macroforge/vite-plugin] Cache invalidated: config changed",
|
|
508
|
+
);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Reject caches built with --builtin-only since they may lack external macro expansions
|
|
513
|
+
if (manifest.builtinOnly) {
|
|
514
|
+
console.log(
|
|
515
|
+
"[@macroforge/vite-plugin] Cache invalidated: built with --builtin-only (run without --builtin-only for full expansion)",
|
|
516
|
+
);
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return manifest;
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Reads a cached expansion result for a source file.
|
|
528
|
+
* @param {string} id - Absolute file path
|
|
529
|
+
* @param {string} code - Current source code content
|
|
530
|
+
* @returns {{ code: string } | null}
|
|
531
|
+
*/
|
|
532
|
+
function readCacheEntry(id, code) {
|
|
533
|
+
if (!cacheManifest || !cacheDir) return null;
|
|
534
|
+
|
|
535
|
+
const relPath = path.relative(projectRoot, id);
|
|
536
|
+
const entry = cacheManifest.entries[relPath];
|
|
537
|
+
if (!entry || !entry.hasMacros) return null;
|
|
538
|
+
|
|
539
|
+
const currentHash = contentHash(code);
|
|
540
|
+
if (entry.sourceHash !== currentHash) return null;
|
|
541
|
+
|
|
542
|
+
const cachePath = path.join(cacheDir, relPath + ".cache");
|
|
543
|
+
try {
|
|
544
|
+
const expandedCode = fs.readFileSync(cachePath, "utf-8");
|
|
545
|
+
return { code: expandedCode };
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Writes a cache entry after macro expansion.
|
|
553
|
+
* Only caches files that actually had macros expanded.
|
|
554
|
+
* @param {string} id - Absolute file path
|
|
555
|
+
* @param {string} sourceCode - Original source code
|
|
556
|
+
* @param {string} expandedCode - Expanded code from rustTransformer
|
|
557
|
+
* @param {boolean} hasMacros - Whether the file actually had macros expanded
|
|
558
|
+
*/
|
|
559
|
+
function writeCacheEntry(id, sourceCode, expandedCode, hasMacros) {
|
|
560
|
+
if (!cacheDir) return;
|
|
561
|
+
|
|
562
|
+
const relPath = path.relative(projectRoot, id);
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
// Only write .cache files for files that actually have macros
|
|
566
|
+
if (hasMacros) {
|
|
567
|
+
const cachePath = path.join(cacheDir, relPath + ".cache");
|
|
568
|
+
ensureDir(path.dirname(cachePath));
|
|
569
|
+
fs.writeFileSync(cachePath, expandedCode, "utf-8");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!cacheManifest) {
|
|
573
|
+
cacheManifest = {
|
|
574
|
+
version: macroforgeVersion,
|
|
575
|
+
configHash: getConfigHash(),
|
|
576
|
+
entries: {},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
cacheManifest.entries[relPath] = {
|
|
581
|
+
sourceHash: contentHash(sourceCode),
|
|
582
|
+
hasMacros,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// Debounce manifest writes — don't write 59KB JSON on every file
|
|
586
|
+
cacheManifestDirty = true;
|
|
587
|
+
if (manifestFlushTimer) clearTimeout(manifestFlushTimer);
|
|
588
|
+
manifestFlushTimer = setTimeout(flushCacheManifest, 500);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.warn(
|
|
591
|
+
`[@macroforge/vite-plugin] Failed to write cache for ${relPath}:`,
|
|
592
|
+
error.message,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Flushes the dirty cache manifest to disk.
|
|
599
|
+
*/
|
|
600
|
+
function flushCacheManifest() {
|
|
601
|
+
if (!cacheManifestDirty || !cacheManifest || !cacheDir) return;
|
|
602
|
+
try {
|
|
603
|
+
ensureDir(cacheDir);
|
|
604
|
+
fs.writeFileSync(
|
|
605
|
+
path.join(cacheDir, "manifest.json"),
|
|
606
|
+
JSON.stringify(cacheManifest, null, 2),
|
|
607
|
+
"utf-8",
|
|
608
|
+
);
|
|
609
|
+
cacheManifestDirty = false;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.warn(
|
|
612
|
+
`[@macroforge/vite-plugin] Failed to write cache manifest:`,
|
|
613
|
+
error.message,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
411
618
|
/**
|
|
412
619
|
* Writes generated TypeScript declaration files to the configured output directory.
|
|
413
620
|
* @param {string} id - The absolute path of the source file
|
|
@@ -498,10 +705,63 @@ export async function macroforge() {
|
|
|
498
705
|
enforce: "pre",
|
|
499
706
|
|
|
500
707
|
/**
|
|
501
|
-
* @param {{ root: string }} config
|
|
708
|
+
* @param {{ root: string, command: string }} config
|
|
502
709
|
*/
|
|
503
710
|
configResolved(config) {
|
|
504
711
|
projectRoot = config.root;
|
|
712
|
+
isDevMode = config.command === "serve";
|
|
713
|
+
|
|
714
|
+
if (isDevMode && devCacheEnabled) {
|
|
715
|
+
cacheDir = path.join(projectRoot, ".macroforge", "cache");
|
|
716
|
+
macroforgeVersion = getMacroforgeVersion();
|
|
717
|
+
cacheManifest = loadCacheManifest();
|
|
718
|
+
|
|
719
|
+
if (cacheManifest) {
|
|
720
|
+
const entryCount = Object.keys(cacheManifest.entries).length;
|
|
721
|
+
console.log(
|
|
722
|
+
`[@macroforge/vite-plugin] Dev cache loaded: ${entryCount} entries`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Pre-scan the project to build a type registry for compile-time type awareness.
|
|
730
|
+
* The registry is passed to every expandSync call so macros can introspect
|
|
731
|
+
* any type in the project (Zig-style type awareness).
|
|
732
|
+
*/
|
|
733
|
+
buildStart() {
|
|
734
|
+
if (!rustTransformer || !rustTransformer.scanProjectSync) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const scanStart = performance.now();
|
|
740
|
+
const scanResult = rustTransformer.scanProjectSync(projectRoot, {
|
|
741
|
+
exportedOnly: false,
|
|
742
|
+
});
|
|
743
|
+
const scanTime = (performance.now() - scanStart).toFixed(0);
|
|
744
|
+
|
|
745
|
+
typeRegistryJson = scanResult.registryJson;
|
|
746
|
+
|
|
747
|
+
console.log(
|
|
748
|
+
`[@macroforge/vite-plugin] Type scan: ${scanResult.typesFound} types from ${scanResult.filesScanned} files (${scanTime}ms)`,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
for (const diag of scanResult.diagnostics) {
|
|
752
|
+
if (diag.level === "error") {
|
|
753
|
+
console.error(
|
|
754
|
+
`[@macroforge/vite-plugin] Scan error: ${diag.message}`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch (error) {
|
|
759
|
+
console.warn(
|
|
760
|
+
`[@macroforge/vite-plugin] Type scan failed, macros will run without type awareness:`,
|
|
761
|
+
error.message || error,
|
|
762
|
+
);
|
|
763
|
+
typeRegistryJson = undefined;
|
|
764
|
+
}
|
|
505
765
|
},
|
|
506
766
|
|
|
507
767
|
/**
|
|
@@ -509,10 +769,6 @@ export async function macroforge() {
|
|
|
509
769
|
* @param {string} id
|
|
510
770
|
*/
|
|
511
771
|
async transform(code, id) {
|
|
512
|
-
// Ensure require() is available for native module loading
|
|
513
|
-
// Use the project's CWD-based require for resolving external macro packages
|
|
514
|
-
const projectRequire = await ensureRequire();
|
|
515
|
-
|
|
516
772
|
// Only transform TypeScript files
|
|
517
773
|
if (!id.endsWith(".ts") && !id.endsWith(".tsx")) {
|
|
518
774
|
return null;
|
|
@@ -533,7 +789,53 @@ export async function macroforge() {
|
|
|
533
789
|
return null;
|
|
534
790
|
}
|
|
535
791
|
|
|
792
|
+
// Quick check: files without @derive can't have macros — skip entirely
|
|
793
|
+
if (!code.includes("@derive")) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
|
|
536
797
|
try {
|
|
798
|
+
// --- Dev cache read ---
|
|
799
|
+
if (isDevMode && devCacheEnabled && cacheManifest) {
|
|
800
|
+
const cached = readCacheEntry(id, code);
|
|
801
|
+
if (cached) {
|
|
802
|
+
let cachedCode = cached.code;
|
|
803
|
+
|
|
804
|
+
// Apply same post-processing as the normal path
|
|
805
|
+
cachedCode = cachedCode.replace(
|
|
806
|
+
/\/\*\*\s*import\s+macro[\s\S]*?\*\/\s*/gi,
|
|
807
|
+
"",
|
|
808
|
+
);
|
|
809
|
+
if (id.endsWith(".svelte.ts") || id.endsWith(".svelte.js")) {
|
|
810
|
+
cachedCode = cachedCode.replace(
|
|
811
|
+
/\/\*\*\s*@derive\b[^*]*\*\//g,
|
|
812
|
+
"",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Generate type definitions from cached expanded code
|
|
817
|
+
if (generateTypes) {
|
|
818
|
+
const emitted = emitDeclarationsFromCode(
|
|
819
|
+
cachedCode,
|
|
820
|
+
id,
|
|
821
|
+
projectRoot,
|
|
822
|
+
);
|
|
823
|
+
if (emitted) {
|
|
824
|
+
writeTypeDefinitions(id, emitted);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
code: cachedCode,
|
|
830
|
+
map: null,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Ensure require() is available for native module loading
|
|
836
|
+
// Use the project's CWD-based require for resolving external macro packages
|
|
837
|
+
const projectRequire = await ensureRequire();
|
|
838
|
+
|
|
537
839
|
// Collect external decorator modules from macro imports
|
|
538
840
|
// Use projectRequire to resolve packages from the project's CWD, not the plugin's location
|
|
539
841
|
const externalDecoratorModules = collectExternalDecoratorModules(
|
|
@@ -546,6 +848,7 @@ export async function macroforge() {
|
|
|
546
848
|
keepDecorators: macroConfig.keepDecorators,
|
|
547
849
|
externalDecoratorModules,
|
|
548
850
|
configPath: macroConfig.configPath,
|
|
851
|
+
typeRegistryJson,
|
|
549
852
|
});
|
|
550
853
|
|
|
551
854
|
// Report diagnostics from macro expansion
|
|
@@ -563,6 +866,14 @@ export async function macroforge() {
|
|
|
563
866
|
}
|
|
564
867
|
|
|
565
868
|
if (result && result.code) {
|
|
869
|
+
// Check if macros were actually expanded
|
|
870
|
+
const hasMacros = result.sourceMapping?.generatedRegions?.length > 0;
|
|
871
|
+
|
|
872
|
+
// --- Dev cache write (self-populating) ---
|
|
873
|
+
if (isDevMode && devCacheEnabled) {
|
|
874
|
+
writeCacheEntry(id, code, result.code, hasMacros);
|
|
875
|
+
}
|
|
876
|
+
|
|
566
877
|
// Remove macro-only imports so SSR output doesn't load native bindings
|
|
567
878
|
result.code = result.code.replace(
|
|
568
879
|
/\/\*\*\s*import\s+macro[\s\S]*?\*\/\s*/gi,
|
|
@@ -612,6 +923,17 @@ export async function macroforge() {
|
|
|
612
923
|
|
|
613
924
|
return null;
|
|
614
925
|
},
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Flush the cache manifest on server close.
|
|
929
|
+
*/
|
|
930
|
+
buildEnd() {
|
|
931
|
+
if (manifestFlushTimer) {
|
|
932
|
+
clearTimeout(manifestFlushTimer);
|
|
933
|
+
manifestFlushTimer = undefined;
|
|
934
|
+
}
|
|
935
|
+
flushCacheManifest();
|
|
936
|
+
},
|
|
615
937
|
};
|
|
616
938
|
|
|
617
939
|
return plugin;
|