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