@vizejs/vite-plugin 0.291.0 → 0.303.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +405 -153
  2. package/package.json +4 -3
package/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  import { registerResolvedVizeConfig, unregisterResolvedVizeConfig } from "./internal/config-bridge.mjs";
2
2
  import { createRequire } from "node:module";
3
- import { createHash } from "node:crypto";
3
+ import crypto, { createHash } from "node:crypto";
4
4
  import * as native from "@vizejs/native";
5
5
  import { applyViteDefineReplacements, buildInspectorGraph, chunkVitePrecompileFiles, classifyVitePluginRequest, createViteBareImportBases, createViteBareImportCandidates, createViteVirtualId, detectViteHmrUpdateType, diffVitePrecompileFiles, generateViteHmrCode, hasViteHmrChanges, isViteBareSpecifier, normalizeViteCssModuleFilename, normalizeViteDevMiddlewareUrl, normalizeVitePrecompileBatchSize, normalizeViteRequireBase, normalizeViteResolvedVuePath, resolveViteAliasRequest, resolveViteCssImports, resolveViteRelativeImport, resolveViteVuePath, rewriteViteDynamicTemplateImports, rewriteViteImportMetaGlobBase, rewriteViteStaticAssetUrls, scopeViteCssForPipeline, shouldApplyViteDefineInVirtualModule, splitViteIdQuery, toViteBrowserImportPrefix, transformViteCssVarsForPipeline } from "@vizejs/native";
6
- import * as vite from "vite";
7
- import { parseSync } from "vite";
6
+ import { parseSync } from "oxc-parser";
8
7
  import fs from "node:fs";
9
8
  import { glob } from "tinyglobby";
10
9
  import path from "node:path";
11
10
  import { pathToFileURL } from "node:url";
11
+ import * as vite from "vite";
12
12
  //#region src/hmr.ts
13
13
  function hasHmrChanges(prev, next) {
14
14
  if (!prev) return true;
@@ -526,6 +526,83 @@ function createLogger(debug) {
526
526
  };
527
527
  }
528
528
  //#endregion
529
+ //#region src/plugin/precompile.ts
530
+ const DEFAULT_PRECOMPILE_IGNORE_PATTERNS = [
531
+ "node_modules/**",
532
+ "dist/**",
533
+ ".git/**",
534
+ ".nuxt/**",
535
+ ".output/**",
536
+ ".nitro/**",
537
+ "coverage/**"
538
+ ];
539
+ function isPrecompileSfcPath(path) {
540
+ return path.endsWith(".vue");
541
+ }
542
+ function diffPrecompileFiles(files, currentMetadata, previousMetadata) {
543
+ return diffVitePrecompileFiles([...files], toNativePrecompileMetadataEntries(currentMetadata), toNativePrecompileMetadataEntries(previousMetadata));
544
+ }
545
+ function normalizePrecompileBatchSize(value) {
546
+ return normalizeVitePrecompileBatchSize(value);
547
+ }
548
+ function chunkPrecompileFiles(files, batchSize, options = {}) {
549
+ return chunkVitePrecompileFiles([...files], batchSize, {
550
+ maxBytes: options.maxBytes,
551
+ metadata: options.metadata ? toNativePrecompileMetadataEntries(options.metadata) : void 0
552
+ });
553
+ }
554
+ function toNativePrecompileMetadataEntries(metadata) {
555
+ const entries = [];
556
+ for (const [path, value] of metadata) entries.push({
557
+ path,
558
+ mtimeMs: value.mtimeMs,
559
+ size: value.size
560
+ });
561
+ return entries;
562
+ }
563
+ //#endregion
564
+ //#region src/plugin/state.ts
565
+ function getEnvironmentCache(state, ssr) {
566
+ return ssr ? state.ssrCache : state.cache;
567
+ }
568
+ function getCompileOptionsForRequest(state, ssr) {
569
+ const options = {
570
+ sourceMap: state.mergedOptions?.sourceMap ?? !state.isProduction,
571
+ ssr,
572
+ vapor: !ssr && (state.mergedOptions?.vapor ?? false),
573
+ customRenderer: state.mergedOptions?.customRenderer ?? false,
574
+ templateSyntax: state.mergedOptions?.templateSyntax ?? "standard"
575
+ };
576
+ if (state.mergedOptions?.mode !== void 0) options.mode = state.mergedOptions.mode;
577
+ if (state.mergedOptions?.runtimeModuleName !== void 0) options.runtimeModuleName = state.mergedOptions.runtimeModuleName;
578
+ if (state.mergedOptions?.runtimeGlobalName !== void 0) options.runtimeGlobalName = state.mergedOptions.runtimeGlobalName;
579
+ if (state.mergedOptions?.vueVersion !== void 0) options.vueVersion = state.mergedOptions.vueVersion;
580
+ if (state.mergedOptions?.experimentalInTagComments) options.experimentalInTagComments = true;
581
+ if (state.mergedOptions?.experimentalPatternedTemplate) options.experimentalPatternedTemplate = true;
582
+ if (state.mergedOptions?.experimentalServerScript) options.experimentalServerScript = true;
583
+ return options;
584
+ }
585
+ function syncCollectedCssForFile(state, filePath, compiled) {
586
+ if (!compiled || !state.extractCss) return;
587
+ if (compiled.styles?.length) {
588
+ state.collectedCss.delete(filePath);
589
+ return;
590
+ }
591
+ if (compiled.css && !hasDelegatedStyles(compiled)) state.collectedCss.set(filePath, resolveCssImports(compiled.css, filePath, state.cssAliasRules, false));
592
+ else state.collectedCss.delete(filePath);
593
+ }
594
+ function shouldExtractCssForRequest(state, ssr) {
595
+ return state.isProduction && !ssr;
596
+ }
597
+ function clearBuildCaches(state) {
598
+ state.cache.clear();
599
+ state.ssrCache.clear();
600
+ state.collectedCss.clear();
601
+ state.precompileMetadata.clear();
602
+ state.pendingHmrUpdateTypes.clear();
603
+ state.viteResolveCache?.clear();
604
+ }
605
+ //#endregion
529
606
  //#region src/compile-options.ts
530
607
  function buildCompileFileOptions(filePath, options) {
531
608
  return {
@@ -624,7 +701,23 @@ function inlineStyleSrcBlocks(source, filePath, dependencies) {
624
701
  return `<style${stripSrcAttribute(`${beforeSrc}${afterSrc}`)}>\n${imported.content}\n</style>`;
625
702
  });
626
703
  }
704
+ /**
705
+ * Cheap necessary condition for an SFC block carrying a `src` attribute.
706
+ *
707
+ * `extractSfcSrcInfo` parses a full SFC descriptor, so calling it for every file
708
+ * costs a second whole-source parse per compile on top of the one the compiler
709
+ * itself performs -- and block `src` attributes are rare (roughly 4% of files in
710
+ * a real app). Any `<script src>`/`<template src>`/`<style src>` necessarily
711
+ * contains `src` followed by `=`, so a source without this substring provably
712
+ * has nothing to inline and can skip the descriptor parse entirely. False
713
+ * positives (e.g. `<img src=...>` in a template) simply take the original path.
714
+ */
715
+ const SFC_SRC_ATTRIBUTE_HINT = /\bsrc\s*=/i;
627
716
  function resolveSfcSrcImports(filePath, source) {
717
+ if (!SFC_SRC_ATTRIBUTE_HINT.test(source)) return {
718
+ source,
719
+ dependencies: []
720
+ };
628
721
  const dependencies = [];
629
722
  const srcInfo = native.extractSfcSrcInfo(source, filePath);
630
723
  let resolvedSource = source;
@@ -726,82 +819,272 @@ function compileBatch(files, cache, options) {
726
819
  }
727
820
  return result;
728
821
  }
729
- //#endregion
730
- //#region src/plugin/precompile.ts
731
- const DEFAULT_PRECOMPILE_IGNORE_PATTERNS = [
732
- "node_modules/**",
733
- "dist/**",
734
- ".git/**",
735
- ".nuxt/**",
736
- ".output/**",
737
- ".nitro/**",
738
- "coverage/**"
739
- ];
740
- function isPrecompileSfcPath(path) {
741
- return path.endsWith(".vue");
822
+ /** SHA-256 of the exact source text handed to the compiler. */
823
+ function hashPrecompileSource(source) {
824
+ return crypto.createHash("sha256").update(source, "utf8").digest("hex");
742
825
  }
743
- function diffPrecompileFiles(files, currentMetadata, previousMetadata) {
744
- return diffVitePrecompileFiles([...files], toNativePrecompileMetadataEntries(currentMetadata), toNativePrecompileMetadataEntries(previousMetadata));
826
+ /** Key-independent JSON: object key order must not change the hash. */
827
+ function stableStringify(value) {
828
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
829
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
830
+ return JSON.stringify(value) ?? "null";
745
831
  }
746
- function normalizePrecompileBatchSize(value) {
747
- return normalizeVitePrecompileBatchSize(value);
832
+ function describeBinary(binary) {
833
+ try {
834
+ const stat = fs.statSync(binary);
835
+ return `${path.basename(binary)}:${stat.size}:${stat.mtimeMs}`;
836
+ } catch {
837
+ return `${path.basename(binary)}:unresolved`;
838
+ }
748
839
  }
749
- function chunkPrecompileFiles(files, batchSize, options = {}) {
750
- return chunkVitePrecompileFiles([...files], batchSize, {
751
- maxBytes: options.maxBytes,
752
- metadata: options.metadata ? toNativePrecompileMetadataEntries(options.metadata) : void 0
753
- });
840
+ /**
841
+ * Identity of the native compiler that produced (or would produce) the output.
842
+ *
843
+ * The package version covers releases; the binary's size and mtime cover local
844
+ * `pnpm --dir npm/native build:debug` rebuilds, which change codegen without
845
+ * changing any version. Hashing the binary itself would be airtight but costs
846
+ * hundreds of milliseconds for a >30 MB artifact, so a rebuild is treated as a
847
+ * new identity — a miss, never a stale hit.
848
+ */
849
+ function resolveCompilerIdentity() {
850
+ const configured = process.env.NAPI_RS_NATIVE_LIBRARY_PATH;
851
+ try {
852
+ const manifestPath = createRequire(import.meta.url).resolve("@vizejs/native/package.json");
853
+ const packageDir = path.dirname(manifestPath);
854
+ const version = JSON.parse(fs.readFileSync(manifestPath, "utf-8")).version;
855
+ const binaries = configured ? [configured] : fs.readdirSync(packageDir).filter((name) => name.endsWith(".node")).sort().map((name) => path.join(packageDir, name));
856
+ return {
857
+ version: typeof version === "string" ? version : "unknown",
858
+ binaries: binaries.map(describeBinary)
859
+ };
860
+ } catch {
861
+ return {
862
+ version: "unresolved",
863
+ binaries: [configured ?? "unresolved"]
864
+ };
865
+ }
754
866
  }
755
- function toNativePrecompileMetadataEntries(metadata) {
756
- const entries = [];
757
- for (const [path, value] of metadata) entries.push({
758
- path,
759
- mtimeMs: value.mtimeMs,
760
- size: value.size
867
+ /**
868
+ * Hash of everything outside the source text that changes compiled output.
869
+ *
870
+ * `compileOptions` must be the object actually handed to the native batch
871
+ * compiler, so a newly added compile option cannot be forgotten here.
872
+ */
873
+ function computePrecompileCacheKey(compileOptions) {
874
+ const material = stableStringify({
875
+ format: 1,
876
+ compiler: resolveCompilerIdentity(),
877
+ options: compileOptions
761
878
  });
762
- return entries;
879
+ return crypto.createHash("sha256").update(material, "utf8").digest("hex").slice(0, 32);
763
880
  }
764
881
  //#endregion
765
- //#region src/plugin/state.ts
766
- function getEnvironmentCache(state, ssr) {
767
- return ssr ? state.ssrCache : state.cache;
768
- }
769
- function getCompileOptionsForRequest(state, ssr) {
770
- const options = {
771
- sourceMap: state.mergedOptions?.sourceMap ?? !state.isProduction,
772
- ssr,
773
- vapor: !ssr && (state.mergedOptions?.vapor ?? false),
774
- customRenderer: state.mergedOptions?.customRenderer ?? false,
775
- templateSyntax: state.mergedOptions?.templateSyntax ?? "standard"
882
+ //#region src/plugin/precompile-cache.ts
883
+ /**
884
+ * Persistent (on-disk) pre-compile cache.
885
+ *
886
+ * `state.precompileMetadata` plus `state.cache` already skip recompilation
887
+ * inside one process, but both are empty on every process start, so `vite
888
+ * build`, CI, and each dev-server start recompile the whole scan from scratch.
889
+ * This module backs them with a manifest under
890
+ * `node_modules/.vize/vite-precompile/` so a cold process can restore the
891
+ * previous run's output.
892
+ *
893
+ * The two invalidation gates -- manifest identity and per-entry source hash --
894
+ * live in `./precompile-cache-key.ts`, which documents why each one is safe.
895
+ * Two more gates live here:
896
+ *
897
+ * - **Shape.** Entries are validated before use and dropped individually if
898
+ * they do not describe a complete `CompiledModule`. The manifest's own
899
+ * `format`/`key` are re-checked after parsing, so a manifest reached by any
900
+ * route other than its key still gets rejected.
901
+ * - **`src` imports.** SFCs that pull blocks in through `<script src>` /
902
+ * `<template src>` / `<style src>` are never persisted: their output depends
903
+ * on files whose content this cache does not hash.
904
+ *
905
+ * A missing, truncated, or corrupt manifest degrades to a full recompile: every
906
+ * read is guarded and any failure yields an empty cache. Writes go through a
907
+ * sibling temp file and a rename, so an interrupted write cannot be read back.
908
+ *
909
+ * Set `VIZE_PRECOMPILE_CACHE=0` to force a full recompile without editing any
910
+ * config.
911
+ */
912
+ /** Manifest location, relative to the Vite root. */
913
+ const PRECOMPILE_CACHE_DIR = path.join("node_modules", ".vize", "vite-precompile");
914
+ /** Set to `0`/`false` to force a full recompile without editing the config. */
915
+ const PRECOMPILE_CACHE_ENV = "VIZE_PRECOMPILE_CACHE";
916
+ /**
917
+ * Whether `module` may be persisted.
918
+ *
919
+ * Modules assembled from `src` imports depend on sibling files that this cache
920
+ * does not hash, so they are recompiled on every cold start instead.
921
+ */
922
+ function isPersistablePrecompileModule(module) {
923
+ return !module.dependencies || module.dependencies.length === 0;
924
+ }
925
+ function isCompiledModule(value) {
926
+ if (typeof value !== "object" || value === null) return false;
927
+ const module = value;
928
+ if (typeof module.code !== "string" || typeof module.scopeId !== "string") return false;
929
+ if (typeof module.hasScoped !== "boolean") return false;
930
+ if (module.css !== void 0 && typeof module.css !== "string") return false;
931
+ if (module.styles !== void 0 && !Array.isArray(module.styles)) return false;
932
+ if (module.macroArtifacts !== void 0 && !Array.isArray(module.macroArtifacts)) return false;
933
+ return isPersistablePrecompileModule(module);
934
+ }
935
+ function isCacheEntry(value) {
936
+ if (typeof value !== "object" || value === null) return false;
937
+ const entry = value;
938
+ return typeof entry.hash === "string" && entry.hash.length > 0 && isCompiledModule(entry.module);
939
+ }
940
+ /** Whether the environment forces the cache off. */
941
+ function isPrecompileCacheDisabledByEnv(env = process.env) {
942
+ const value = env[PRECOMPILE_CACHE_ENV];
943
+ return value === "0" || value === "false";
944
+ }
945
+ const disabledCache = {
946
+ file: null,
947
+ get: () => void 0,
948
+ set: () => {},
949
+ delete: () => {},
950
+ retain: () => {},
951
+ flush: () => false
952
+ };
953
+ /** A cache that never hits and never writes. */
954
+ function createDisabledPrecompileCache() {
955
+ return disabledCache;
956
+ }
957
+ function openPrecompileCache(options) {
958
+ const { root, compileOptions, onDiagnostic, env = process.env } = options;
959
+ if (!root || isPrecompileCacheDisabledByEnv(env)) return createDisabledPrecompileCache();
960
+ const key = computePrecompileCacheKey(compileOptions);
961
+ const file = path.join(root, PRECOMPILE_CACHE_DIR, `${key}.json`);
962
+ const entries = readManifestEntries(file, key, onDiagnostic);
963
+ let dirty = false;
964
+ return {
965
+ file,
966
+ get(filePath, sourceHash) {
967
+ const entry = entries.get(filePath);
968
+ return entry && entry.hash === sourceHash ? entry.module : void 0;
969
+ },
970
+ set(filePath, sourceHash, module) {
971
+ if (!isPersistablePrecompileModule(module)) {
972
+ if (entries.delete(filePath)) dirty = true;
973
+ return;
974
+ }
975
+ const existing = entries.get(filePath);
976
+ if (existing?.hash === sourceHash && existing.module === module) return;
977
+ entries.set(filePath, {
978
+ hash: sourceHash,
979
+ module
980
+ });
981
+ dirty = true;
982
+ },
983
+ delete(filePath) {
984
+ if (entries.delete(filePath)) dirty = true;
985
+ },
986
+ retain(files) {
987
+ const keep = files instanceof Set ? files : new Set(files);
988
+ for (const filePath of entries.keys()) if (!keep.has(filePath)) {
989
+ entries.delete(filePath);
990
+ dirty = true;
991
+ }
992
+ },
993
+ flush() {
994
+ if (!dirty) return false;
995
+ if (!writeManifest(file, {
996
+ format: 1,
997
+ key,
998
+ entries: Object.fromEntries(entries)
999
+ }, onDiagnostic)) return false;
1000
+ dirty = false;
1001
+ return true;
1002
+ }
776
1003
  };
777
- if (state.mergedOptions?.mode !== void 0) options.mode = state.mergedOptions.mode;
778
- if (state.mergedOptions?.runtimeModuleName !== void 0) options.runtimeModuleName = state.mergedOptions.runtimeModuleName;
779
- if (state.mergedOptions?.runtimeGlobalName !== void 0) options.runtimeGlobalName = state.mergedOptions.runtimeGlobalName;
780
- if (state.mergedOptions?.vueVersion !== void 0) options.vueVersion = state.mergedOptions.vueVersion;
781
- if (state.mergedOptions?.experimentalInTagComments) options.experimentalInTagComments = true;
782
- if (state.mergedOptions?.experimentalPatternedTemplate) options.experimentalPatternedTemplate = true;
783
- if (state.mergedOptions?.experimentalServerScript) options.experimentalServerScript = true;
784
- return options;
785
1004
  }
786
- function syncCollectedCssForFile(state, filePath, compiled) {
787
- if (!compiled || !state.extractCss) return;
788
- if (compiled.styles?.length) {
789
- state.collectedCss.delete(filePath);
790
- return;
1005
+ /**
1006
+ * Parse the manifest, or return an empty map.
1007
+ *
1008
+ * A missing, truncated, corrupt, or foreign manifest is indistinguishable from
1009
+ * no cache at all, which is exactly the safe outcome: recompile everything.
1010
+ */
1011
+ function readManifestEntries(file, key, onDiagnostic) {
1012
+ const entries = /* @__PURE__ */ new Map();
1013
+ let raw;
1014
+ try {
1015
+ raw = fs.readFileSync(file, "utf-8");
1016
+ } catch {
1017
+ return entries;
791
1018
  }
792
- if (compiled.css && !hasDelegatedStyles(compiled)) state.collectedCss.set(filePath, resolveCssImports(compiled.css, filePath, state.cssAliasRules, false));
793
- else state.collectedCss.delete(filePath);
1019
+ let parsed;
1020
+ try {
1021
+ parsed = JSON.parse(raw);
1022
+ } catch (error) {
1023
+ onDiagnostic?.(`Discarding corrupt pre-compile cache ${file}:`, error);
1024
+ return entries;
1025
+ }
1026
+ const manifest = parsed;
1027
+ if (typeof manifest !== "object" || manifest === null || manifest.format !== 1 || manifest.key !== key || typeof manifest.entries !== "object" || manifest.entries === null) {
1028
+ onDiagnostic?.(`Ignoring pre-compile cache ${file}: unrecognized manifest`);
1029
+ return entries;
1030
+ }
1031
+ for (const [filePath, entry] of Object.entries(manifest.entries)) if (isCacheEntry(entry)) entries.set(filePath, entry);
1032
+ return entries;
794
1033
  }
795
- function shouldExtractCssForRequest(state, ssr) {
796
- return state.isProduction && !ssr;
1034
+ /** Write through a sibling temp file so a crash cannot leave a partial manifest. */
1035
+ function writeManifest(file, manifest, onDiagnostic) {
1036
+ const temp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
1037
+ try {
1038
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1039
+ fs.writeFileSync(temp, JSON.stringify(manifest));
1040
+ fs.renameSync(temp, file);
1041
+ return true;
1042
+ } catch (error) {
1043
+ try {
1044
+ fs.rmSync(temp, { force: true });
1045
+ } catch {}
1046
+ onDiagnostic?.(`Failed to write pre-compile cache ${file}:`, error);
1047
+ return false;
1048
+ }
797
1049
  }
798
- function clearBuildCaches(state) {
799
- state.cache.clear();
800
- state.ssrCache.clear();
801
- state.collectedCss.clear();
802
- state.precompileMetadata.clear();
803
- state.pendingHmrUpdateTypes.clear();
804
- state.viteResolveCache?.clear();
1050
+ //#endregion
1051
+ //#region src/plugin/precompile-run.ts
1052
+ /**
1053
+ * The pre-compilation pass.
1054
+ *
1055
+ * Scans the configured patterns, diffs them against the previous in-process
1056
+ * run, restores whatever the persistent cache can prove is still valid, and
1057
+ * batch-compiles the rest. Extracted from `state.ts` so the state module stays
1058
+ * a state module.
1059
+ */
1060
+ /**
1061
+ * The options the pre-compile batch actually compiles with.
1062
+ *
1063
+ * Also the input to the cache key, so the two can never drift: a new option
1064
+ * that reaches the native compiler reaches the key with it.
1065
+ */
1066
+ function resolvePrecompileBatchOptions(state) {
1067
+ return {
1068
+ ssr: false,
1069
+ vapor: state.mergedOptions.vapor ?? false,
1070
+ mode: state.mergedOptions.mode,
1071
+ customRenderer: state.mergedOptions.customRenderer ?? false,
1072
+ templateSyntax: state.mergedOptions.templateSyntax ?? "standard",
1073
+ experimentalInTagComments: state.mergedOptions.experimentalInTagComments ?? false,
1074
+ experimentalPatternedTemplate: state.mergedOptions.experimentalPatternedTemplate ?? false,
1075
+ experimentalServerScript: state.mergedOptions.experimentalServerScript ?? false,
1076
+ runtimeModuleName: state.mergedOptions.runtimeModuleName,
1077
+ runtimeGlobalName: state.mergedOptions.runtimeGlobalName,
1078
+ vueVersion: state.mergedOptions.vueVersion
1079
+ };
1080
+ }
1081
+ function openCacheForRun(state, batchOptions) {
1082
+ if (!state.root) return createDisabledPrecompileCache();
1083
+ return openPrecompileCache({
1084
+ root: state.root,
1085
+ compileOptions: buildCompileBatchOptions(batchOptions),
1086
+ onDiagnostic: (message, error) => error === void 0 ? state.logger.warn(message) : state.logger.warn(message, error)
1087
+ });
805
1088
  }
806
1089
  /**
807
1090
  * Pre-compile all Vue files matching scan patterns.
@@ -842,41 +1125,50 @@ async function compileAll(state) {
842
1125
  if (state.extractCss) state.collectedCss.delete(file);
843
1126
  state.pendingHmrUpdateTypes.delete(file);
844
1127
  }
1128
+ const batchOptions = resolvePrecompileBatchOptions(state);
1129
+ const cache = openCacheForRun(state, batchOptions);
1130
+ const cacheEnabled = cache.file !== null;
845
1131
  let successCount = 0;
1132
+ let restoredCount = 0;
846
1133
  let failedCount = 0;
847
1134
  let nativeTimeMs = 0;
848
1135
  const precompileFailures = [];
849
1136
  const chunks = chunkPrecompileFiles(changedFiles, state.precompileBatchSize, { metadata: currentMetadata });
850
1137
  for (const chunk of chunks) {
851
1138
  const fileContents = [];
852
- for (const file of chunk) try {
853
- const source = fs.readFileSync(file, "utf-8");
1139
+ const sourceHashes = /* @__PURE__ */ new Map();
1140
+ for (const file of chunk) {
1141
+ let source;
1142
+ try {
1143
+ source = fs.readFileSync(file, "utf-8");
1144
+ } catch (e) {
1145
+ failedCount++;
1146
+ state.cache.delete(file);
1147
+ if (state.extractCss) state.collectedCss.delete(file);
1148
+ state.precompileMetadata.delete(file);
1149
+ cache.delete(file);
1150
+ precompileFailures.push(`[vize] Failed to read ${file}: ${formatUnknownError$2(e)}`);
1151
+ state.logger.error(`Failed to read ${file}:`, e);
1152
+ continue;
1153
+ }
1154
+ const sourceHash = cacheEnabled ? hashPrecompileSource(source) : void 0;
1155
+ const restored = sourceHash === void 0 ? void 0 : cache.get(file, sourceHash);
1156
+ if (restored) {
1157
+ state.cache.set(file, restored);
1158
+ const metadata = currentMetadata.get(file);
1159
+ if (metadata) state.precompileMetadata.set(file, metadata);
1160
+ syncCollectedCssForFile(state, file, restored);
1161
+ restoredCount++;
1162
+ continue;
1163
+ }
1164
+ if (sourceHash !== void 0) sourceHashes.set(file, sourceHash);
854
1165
  fileContents.push({
855
1166
  path: file,
856
1167
  source
857
1168
  });
858
- } catch (e) {
859
- failedCount++;
860
- state.cache.delete(file);
861
- if (state.extractCss) state.collectedCss.delete(file);
862
- state.precompileMetadata.delete(file);
863
- precompileFailures.push(`[vize] Failed to read ${file}: ${formatUnknownError$2(e)}`);
864
- state.logger.error(`Failed to read ${file}:`, e);
865
1169
  }
866
1170
  if (fileContents.length === 0) continue;
867
- const result = compileBatch(fileContents, state.cache, {
868
- ssr: false,
869
- vapor: state.mergedOptions.vapor ?? false,
870
- mode: state.mergedOptions.mode,
871
- customRenderer: state.mergedOptions.customRenderer ?? false,
872
- templateSyntax: state.mergedOptions.templateSyntax ?? "standard",
873
- experimentalInTagComments: state.mergedOptions.experimentalInTagComments ?? false,
874
- experimentalPatternedTemplate: state.mergedOptions.experimentalPatternedTemplate ?? false,
875
- experimentalServerScript: state.mergedOptions.experimentalServerScript ?? false,
876
- runtimeModuleName: state.mergedOptions.runtimeModuleName,
877
- runtimeGlobalName: state.mergedOptions.runtimeGlobalName,
878
- vueVersion: state.mergedOptions.vueVersion
879
- });
1171
+ const result = compileBatch(fileContents, state.cache, batchOptions);
880
1172
  const chunkFailedCount = result.results.filter((fileResult) => fileResult.errors.length > 0).length;
881
1173
  failedCount += chunkFailedCount;
882
1174
  successCount += result.results.length - chunkFailedCount;
@@ -887,16 +1179,22 @@ async function compileAll(state) {
887
1179
  state.cache.delete(fileResult.path);
888
1180
  if (state.extractCss) state.collectedCss.delete(fileResult.path);
889
1181
  state.precompileMetadata.delete(fileResult.path);
1182
+ cache.delete(fileResult.path);
890
1183
  precompileFailures.push(formatCompileErrorMessage(fileResult.path, fileResult.errors));
891
1184
  continue;
892
1185
  }
893
1186
  if (metadata) state.precompileMetadata.set(fileResult.path, metadata);
894
- syncCollectedCssForFile(state, fileResult.path, state.cache.get(fileResult.path));
1187
+ const compiled = state.cache.get(fileResult.path);
1188
+ const sourceHash = sourceHashes.get(fileResult.path);
1189
+ if (compiled && sourceHash !== void 0) cache.set(fileResult.path, sourceHash, compiled);
1190
+ syncCollectedCssForFile(state, fileResult.path, compiled);
895
1191
  }
896
1192
  }
1193
+ cache.retain(new Set(sfcFiles));
1194
+ cache.flush();
897
1195
  const elapsed = (performance.now() - startTime).toFixed(2);
898
1196
  const batchLabel = chunks.length === 1 ? "batch" : "batches";
899
- state.logger.info(`Pre-compilation complete: ${successCount} recompiled, ${cachedFileCount} reused, ${failedCount} failed (${elapsed}ms, native ${batchLabel}: ${nativeTimeMs.toFixed(2)}ms)`);
1197
+ state.logger.info(`Pre-compilation complete: ${successCount} recompiled, ${restoredCount} restored from disk, ${cachedFileCount} reused, ${failedCount} failed (${elapsed}ms, native ${batchLabel}: ${nativeTimeMs.toFixed(2)}ms)`);
900
1198
  if (failedCount > 0) {
901
1199
  const details = precompileFailures.length > 0 ? `\n\n${precompileFailures.join("\n\n")}` : "";
902
1200
  throw new Error(`[vize] Pre-compilation failed for ${failedCount} file(s).${details}`);
@@ -1649,67 +1947,21 @@ function createVirtualTypeScriptTransformer(viteApi) {
1649
1947
  throw new Error("Installed Vite does not expose transformWithOxc or transformWithEsbuild");
1650
1948
  };
1651
1949
  }
1652
- const TYPE_DECLARATION_RE = /\b(?:interface|type|enum|namespace|declare)\s+[A-Za-z_$]/;
1653
- const TYPE_ASSERTION_RE = /\bas\s+(?:const|unknown|never|any|string|number|boolean|readonly\b|[A-Z][A-Za-z0-9_$]*(?:\s*[<[{&|),;=]|$))/;
1654
- const TYPED_BINDING_RE = /\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*:/;
1655
- const GENERIC_FUNCTION_RE = /\bfunction\s+[A-Za-z_$][\w$]*\s*<[^>{}]*>\s*\(/;
1656
- const TYPED_PARAMETER_RE = /[(,]\s*(?:\.\.\.)?[A-Za-z_$][\w$]*\??\s*:\s*[^,)=]+/;
1657
- const RETURN_TYPE_RE = /\)\s*:\s*[^=<{;]+[{=>]/;
1658
- const ACCESS_MODIFIER_RE = /\b(?:public|private|protected|readonly|abstract|implements)\b/;
1659
- const SATISFIES_RE = /\bsatisfies\s+[A-Za-z_$]/;
1660
- function hasUnbalancedDelimiters(code) {
1661
- const stack = [];
1662
- let quote = null;
1663
- let escaped = false;
1664
- let lineComment = false;
1665
- let blockComment = false;
1666
- for (let index = 0; index < code.length; index += 1) {
1667
- const char = code[index];
1668
- const next = code[index + 1];
1669
- if (lineComment) {
1670
- if (char === "\n" || char === "\r") lineComment = false;
1671
- continue;
1672
- }
1673
- if (blockComment) {
1674
- if (char === "*" && next === "/") {
1675
- blockComment = false;
1676
- index += 1;
1677
- }
1678
- continue;
1679
- }
1680
- if (quote) {
1681
- if (escaped) escaped = false;
1682
- else if (char === "\\") escaped = true;
1683
- else if (char === quote) quote = null;
1684
- continue;
1685
- }
1686
- if (char === "/" && next === "/") {
1687
- lineComment = true;
1688
- index += 1;
1689
- continue;
1690
- }
1691
- if (char === "/" && next === "*") {
1692
- blockComment = true;
1693
- index += 1;
1694
- continue;
1695
- }
1696
- if (char === "'" || char === "\"" || char === "`") {
1697
- quote = char;
1698
- continue;
1699
- }
1700
- if (char === "{" || char === "(" || char === "[") {
1701
- stack.push(char);
1702
- continue;
1703
- }
1704
- if (char === "}" || char === ")" || char === "]") {
1705
- const open = stack.pop();
1706
- if (char === "}" && open !== "{" || char === ")" && open !== "(" || char === "]" && open !== "[") return true;
1707
- }
1708
- }
1709
- return quote !== null || blockComment || stack.length > 0;
1710
- }
1711
- function needsVirtualTypeScriptTransform(code) {
1712
- return TYPE_DECLARATION_RE.test(code) || TYPE_ASSERTION_RE.test(code) || TYPED_BINDING_RE.test(code) || GENERIC_FUNCTION_RE.test(code) || TYPED_PARAMETER_RE.test(code) || RETURN_TYPE_RE.test(code) || ACCESS_MODIFIER_RE.test(code) || SATISFIES_RE.test(code) || hasUnbalancedDelimiters(code);
1950
+ /**
1951
+ * Report whether `realPath` names a module Vize's own compiler emitted.
1952
+ *
1953
+ * Vize's Rust emitter guarantees plain JavaScript for every module it produces
1954
+ * (`ensure_javascript_output` at the napi boundary), so re-running Vite's
1955
+ * TypeScript strip over emitter output is a pure re-print. Every emitted module
1956
+ * is recorded in one of the two environment caches, so cache membership is the
1957
+ * cheap, allocation-free proof that the code came from the emitter.
1958
+ *
1959
+ * The probe fails safe: a module the caches do not know about still gets the
1960
+ * strip, which keeps hand-written and malformed virtual modules behaving
1961
+ * exactly as they did before.
1962
+ */
1963
+ function isVizeEmitterOutput(state, realPath) {
1964
+ return state.cache.has(realPath) || state.ssrCache.has(realPath);
1713
1965
  }
1714
1966
  const transformVirtualTypeScript = createVirtualTypeScriptTransformer(vite);
1715
1967
  function getOxcDumpPath(root, realPath) {
@@ -1731,7 +1983,7 @@ function formatUnknownError$1(error) {
1731
1983
  return error instanceof Error ? error.message : String(error);
1732
1984
  }
1733
1985
  async function transformVizeVirtualModule(state, code, realPath, ssr, forceTypeScriptTransform = false) {
1734
- const needsTsTransform = forceTypeScriptTransform || needsVirtualTypeScriptTransform(code);
1986
+ const needsTsTransform = forceTypeScriptTransform || !isVizeEmitterOutput(state, realPath);
1735
1987
  try {
1736
1988
  let transformed = (needsTsTransform ? await transformVirtualTypeScript(code, realPath) : { code }).code;
1737
1989
  if (transformed.includes("import.meta.")) transformed = applyDefineReplacements(transformed, getVirtualModuleDefines(state, ssr));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizejs/vite-plugin",
3
- "version": "0.291.0",
3
+ "version": "0.303.0",
4
4
  "description": "High-performance native Vite plugin for Vue SFC compilation powered by Vize",
5
5
  "keywords": [
6
6
  "compiler",
@@ -45,9 +45,10 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
- "@vizejs/native": "0.291.0",
48
+ "@vizejs/native": "0.303.0",
49
+ "oxc-parser": "0.133.0",
49
50
  "tinyglobby": "0.2.16",
50
- "vize": "0.291.0"
51
+ "vize": "0.303.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "25.9.2",