@vizejs/vite-plugin 0.303.0 → 0.306.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 +402 -80
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -7,6 +7,7 @@ import { parseSync } from "oxc-parser";
7
7
  import fs from "node:fs";
8
8
  import { glob } from "tinyglobby";
9
9
  import path from "node:path";
10
+ import zlib from "node:zlib";
10
11
  import { pathToFileURL } from "node:url";
11
12
  import * as vite from "vite";
12
13
  //#region src/hmr.ts
@@ -86,9 +87,15 @@ function isNode(value) {
86
87
  function getNodeStart(node) {
87
88
  return typeof node?.start === "number" ? node.start : null;
88
89
  }
90
+ function getNodeEnd(node) {
91
+ return typeof node?.end === "number" ? node.end : null;
92
+ }
89
93
  function getNodeName(node) {
90
94
  return isNode(node) && typeof node.name === "string" ? node.name : null;
91
95
  }
96
+ function isSfcMainDefaultExport(defaultExport) {
97
+ return isIdentifierNamed(isNode(defaultExport?.declaration) ? defaultExport.declaration : null, SFC_MAIN_NAME);
98
+ }
92
99
  function parseProgram(code) {
93
100
  try {
94
101
  const result = parseSync(OUTPUT_PARSE_ID, code);
@@ -146,7 +153,9 @@ function analyzeFastDefaultOutput(code) {
146
153
  hasNamedRenderExport: false,
147
154
  hasNamedSsrRenderExport: false,
148
155
  defaultExportStart,
149
- defaultExportKeywordEnd: defaultExportStart + 14
156
+ defaultExportKeywordEnd: defaultExportStart + 14,
157
+ defaultExportEnd: null,
158
+ defaultExportIsSfcMain: false
150
159
  };
151
160
  }
152
161
  function getExportDefaultKeywordEnd(code, defaultExport) {
@@ -172,7 +181,9 @@ function analyzeModuleOutput(code) {
172
181
  hasNamedRenderExport: exportedNames.includes("render"),
173
182
  hasNamedSsrRenderExport: exportedNames.includes("ssrRender"),
174
183
  defaultExportKeywordEnd,
175
- defaultExportStart
184
+ defaultExportStart,
185
+ defaultExportEnd: getNodeEnd(defaultExport),
186
+ defaultExportIsSfcMain: isSfcMainDefaultExport(defaultExport)
176
187
  };
177
188
  }
178
189
  function rewriteDefaultExportToSfcMain(code, moduleInfo = {
@@ -189,12 +200,29 @@ function rewriteDefaultExportToSfcMain(code, moduleInfo = {
189
200
  if (exportStart == null || keywordEnd == null) return code;
190
201
  return `${code.slice(0, exportStart)}const ${SFC_MAIN_NAME} =${code.slice(keywordEnd)}`;
191
202
  }
192
- function insertBeforeSfcMainDefaultExport(code, insertion, options = {}) {
203
+ /**
204
+ * Locate `export default _sfc_main` when the caller has no analysis to hand.
205
+ *
206
+ * The string test is a sound pre-filter: the AST check below only succeeds when
207
+ * the default export's declaration *is* the `_sfc_main` identifier, which cannot
208
+ * happen unless that name occurs in the module.
209
+ */
210
+ function findSfcMainDefaultExport(code) {
211
+ if (!code.includes(SFC_MAIN_NAME)) return {
212
+ defaultExportStart: null,
213
+ defaultExportEnd: null,
214
+ defaultExportIsSfcMain: false
215
+ };
193
216
  const defaultExport = findDefaultExport(parseProgram(code));
194
- const declaration = isNode(defaultExport?.declaration) ? defaultExport.declaration : null;
195
- const exportStart = getNodeStart(defaultExport);
196
- const exportEnd = typeof defaultExport?.end === "number" ? defaultExport.end : null;
197
- if (!isIdentifierNamed(declaration, SFC_MAIN_NAME) || exportStart == null) return code;
217
+ return {
218
+ defaultExportStart: getNodeStart(defaultExport),
219
+ defaultExportEnd: getNodeEnd(defaultExport),
220
+ defaultExportIsSfcMain: isSfcMainDefaultExport(defaultExport)
221
+ };
222
+ }
223
+ function insertBeforeSfcMainDefaultExport(code, insertion, options = {}) {
224
+ const { defaultExportStart: exportStart, defaultExportEnd: exportEnd, defaultExportIsSfcMain } = options.moduleInfo ?? findSfcMainDefaultExport(code);
225
+ if (!defaultExportIsSfcMain || exportStart == null) return code;
198
226
  if (options.normalizeSemicolon && exportEnd != null) {
199
227
  const suffixStart = code[exportEnd] === ";" ? exportEnd + 1 : exportEnd;
200
228
  return `${code.slice(0, exportStart)}${insertion}\nexport default ${SFC_MAIN_NAME};${code.slice(suffixStart)}`;
@@ -374,6 +402,36 @@ function insertAfterStaticImports(output, imports) {
374
402
  function generateScopeId(filename) {
375
403
  return createHash("sha256").update(filename).digest("hex").slice(0, 8);
376
404
  }
405
+ /**
406
+ * Whether the SFC's `<style>` blocks are handed to Vite as virtual imports.
407
+ *
408
+ * Some blocks require Vite's CSS pipeline (preprocessor or CSS Modules), and a
409
+ * production client build routes plain CSS through it too so nesting,
410
+ * minification, and chunk ownership still apply. In both cases the blocks are
411
+ * emitted as imports and `compiled.css` is not used.
412
+ */
413
+ function usesStyleImports(compiled, options) {
414
+ return !!options.filePath && !!compiled.styles?.length && (hasDelegatedStyles(compiled) || !options.ssr && options.isProduction && !!options.extractCss);
415
+ }
416
+ /**
417
+ * Whether `generateOutput` will embed `compiled.css` in the module.
418
+ *
419
+ * The only consumer of `compiled.css` is the inline `<style>` injection, so this
420
+ * is also the only case in which resolving the CSS's `@import`s affects the
421
+ * output. Callers that would otherwise resolve `compiled.css` eagerly consult
422
+ * this first, which keeps the condition in one place instead of duplicating
423
+ * `generateOutput`'s branch structure at the call site.
424
+ *
425
+ * That guard matters because resolving `@import`s reads and inlines files and
426
+ * crosses the native boundary with the whole stylesheet. A production client
427
+ * build hands plain `<style>` blocks to Vite as virtual imports and an SSR build
428
+ * emits no CSS at all, so in both cases the resolved text was previously built
429
+ * and discarded, once per styled SFC on every build (270 discarded calls on the
430
+ * 300-file bench corpus).
431
+ */
432
+ function embedsInlineCss(compiled, options) {
433
+ return !usesStyleImports(compiled, options) && !options.ssr && !!compiled.css && !(options.isProduction && !!options.extractCss);
434
+ }
377
435
  function generateOutput(compiled, options) {
378
436
  const { isProduction, isDev, ssr, hmrUpdateType, extractCss, filePath } = options;
379
437
  let output = compiled.code;
@@ -387,7 +445,7 @@ function generateOutput(compiled, options) {
387
445
  if (compiled.hasScoped && compiled.scopeId) output += `\n_sfc_main.__scopeId = "data-v-${compiled.scopeId}";`;
388
446
  output += "\nexport default _sfc_main;";
389
447
  } else if (hasExportDefault && hasSfcMainDefined) {
390
- if (compiled.hasScoped && compiled.scopeId) output = insertBeforeSfcMainDefaultExport(output, `_sfc_main.__scopeId = "data-v-${compiled.scopeId}";`);
448
+ if (compiled.hasScoped && compiled.scopeId) output = insertBeforeSfcMainDefaultExport(output, `_sfc_main.__scopeId = "data-v-${compiled.scopeId}";`, { moduleInfo });
391
449
  } else if (!hasExportDefault && !hasSfcMainDefined && hasNamedRenderExport) {
392
450
  output += "\nconst _sfc_main = {};";
393
451
  if (compiled.hasScoped && compiled.scopeId) output += `\n_sfc_main.__scopeId = "data-v-${compiled.scopeId}";`;
@@ -399,7 +457,7 @@ function generateOutput(compiled, options) {
399
457
  output += "\n_sfc_main.ssrRender = ssrRender;";
400
458
  output += "\nexport default _sfc_main;";
401
459
  }
402
- if (!!filePath && !!compiled.styles?.length && (hasDelegatedStyles(compiled) || !ssr && isProduction && extractCss)) {
460
+ if (usesStyleImports(compiled, options)) {
403
461
  const styleImports = [];
404
462
  const cssModuleImports = [];
405
463
  for (const block of compiled.styles) {
@@ -454,13 +512,39 @@ function toPluginVisibleVirtualId(realPath, ssr = false, querySuffix = "") {
454
512
  const rest = params.toString();
455
513
  return `${realPath}.ts?vue&${ssr ? "vize-ssr" : "vize"}${rest ? `&${rest}` : ""}`;
456
514
  }
515
+ /**
516
+ * String pre-gate for {@link fromPluginVisibleVirtualId}, so ordinary module IDs
517
+ * never cross the native boundary (#3427).
518
+ *
519
+ * A non-null result requires `request.path` to end with `.vue.ts` or `.vue.tsx`
520
+ * and `request.querySuffix` to be non-empty. `request.path` is `id` up to the
521
+ * first `?` and `querySuffix` is non-empty exactly when that `?` exists, so both
522
+ * conditions imply these two substring tests. The tests are strictly weaker, so
523
+ * everything the old code accepted still reaches the classifier.
524
+ */
525
+ function mayBePluginVisibleVirtualId(id) {
526
+ return !id.startsWith("\0") && id.includes(".vue.ts") && id.includes("?");
527
+ }
457
528
  function fromPluginVisibleVirtualId(id) {
458
- if (id.startsWith("\0")) return null;
529
+ if (!mayBePluginVisibleVirtualId(id)) return null;
459
530
  const request = classifyVitePluginRequest(id);
460
531
  if (!isPluginVisibleVueVirtualPath(request.path) || !request.querySuffix) return null;
461
532
  const params = new URLSearchParams(request.querySuffix.slice(1));
462
533
  if (!params.has("vue") || !params.has("vize") && !params.has("vize-ssr")) return null;
463
- return stripPluginVisibleVueVirtualSuffix(classifyVitePluginRequest(request.normalizedFsId ?? id).path);
534
+ return stripPluginVisibleVueVirtualSuffix(stripFsPrefix(request.path));
535
+ }
536
+ /**
537
+ * The `path` a second `classifyVitePluginRequest(request.normalizedFsId ?? id)`
538
+ * used to recompute (#3427).
539
+ *
540
+ * `normalizedFsId` is `Some` exactly when the pre-`?` path starts with `/@fs`,
541
+ * and its value is that path with the four-byte prefix removed plus the original
542
+ * query suffix. Re-splitting that at the first `?` therefore yields the path
543
+ * without the prefix — and when `normalizedFsId` is `undefined` the second call
544
+ * classified `id` itself and yielded `request.path` unchanged.
545
+ */
546
+ function stripFsPrefix(path) {
547
+ return path.startsWith("/@fs") ? path.slice(4) : path;
464
548
  }
465
549
  function isPluginVisibleVueVirtualPath(path) {
466
550
  return path.endsWith(".vue.ts") || path.endsWith(".vue.tsx");
@@ -819,9 +903,14 @@ function compileBatch(files, cache, options) {
819
903
  }
820
904
  return result;
821
905
  }
822
- /** SHA-256 of the exact source text handed to the compiler. */
906
+ /**
907
+ * SHA-256 of the exact source text handed to the compiler.
908
+ *
909
+ * `base64url` rather than hex: the same 256 bits in 43 characters instead of 64,
910
+ * and the index carries one of these per entry.
911
+ */
823
912
  function hashPrecompileSource(source) {
824
- return crypto.createHash("sha256").update(source, "utf8").digest("hex");
913
+ return crypto.createHash("sha256").update(source, "utf8").digest("base64url");
825
914
  }
826
915
  /** Key-independent JSON: object key order must not change the hash. */
827
916
  function stableStringify(value) {
@@ -872,13 +961,241 @@ function resolveCompilerIdentity() {
872
961
  */
873
962
  function computePrecompileCacheKey(compileOptions) {
874
963
  const material = stableStringify({
875
- format: 1,
964
+ format: 2,
876
965
  compiler: resolveCompilerIdentity(),
877
966
  options: compileOptions
878
967
  });
879
968
  return crypto.createHash("sha256").update(material, "utf8").digest("hex").slice(0, 32);
880
969
  }
881
970
  //#endregion
971
+ //#region src/plugin/precompile-cache-store.ts
972
+ /**
973
+ * On-disk container for the persistent pre-compile cache.
974
+ *
975
+ * The first format stored the whole manifest as one JSON document, which meant
976
+ * every compiled module's `code` went to disk as a JSON-escaped string: ~87% of
977
+ * the bytes were that one field, and a cold build paid `JSON.stringify` over all
978
+ * of it while a warm build paid `JSON.parse` back. This container splits the
979
+ * two concerns instead:
980
+ *
981
+ * ```text
982
+ * <header JSON>\n<compressed index><compressed payload>
983
+ * ```
984
+ *
985
+ * - The **header** is one line of plain JSON naming the format, the cache key,
986
+ * the codec, and the exact byte length of the two bodies.
987
+ * - The **index** is a compressed JSON array of `[relativePath, sourceHash,
988
+ * recordLength]`. Record offsets are *not* stored: they are the running sum of
989
+ * the lengths, so there is no offset that can point somewhere else.
990
+ * - The **payload** is the records back to back, each one
991
+ * `<meta JSON>\n<code utf8><css utf8>\n`. `code` and `css` are raw UTF-8, so
992
+ * the 90% of the manifest that is compiled output is never escaped, parsed, or
993
+ * re-quoted -- only sliced out.
994
+ *
995
+ * `meta` is `[codeLength, cssLength, everythingElse]`, where `everythingElse` is
996
+ * the compiled module minus `code`/`css` **by rest destructuring**, not by a
997
+ * hand-copied field list. A field added to `CompiledModule` later is therefore
998
+ * carried through automatically instead of being silently dropped.
999
+ *
1000
+ * Every structural expectation above is re-checked on read and any failure
1001
+ * returns `null`, which the caller treats as "no cache" -- a full recompile.
1002
+ * See `decodePrecompileManifest`.
1003
+ */
1004
+ /** File extension of the container. Not `.json`: it is a header plus two blobs. */
1005
+ const PRECOMPILE_CACHE_EXTENSION = ".vpc";
1006
+ const LF = 10;
1007
+ const NEWLINE = Buffer.from("\n");
1008
+ const EMPTY = Buffer.alloc(0);
1009
+ /**
1010
+ * Whether `module` may be persisted.
1011
+ *
1012
+ * Modules assembled from `src` imports depend on sibling files that this cache
1013
+ * does not hash, so they are recompiled on every cold start instead.
1014
+ */
1015
+ function isPersistablePrecompileModule(module) {
1016
+ return !module.dependencies || module.dependencies.length === 0;
1017
+ }
1018
+ function isCompiledModule(value) {
1019
+ if (value === null || typeof value !== "object") return false;
1020
+ const module = value;
1021
+ if (typeof module.code !== "string" || typeof module.scopeId !== "string") return false;
1022
+ if (typeof module.hasScoped !== "boolean") return false;
1023
+ if (module.css !== void 0 && typeof module.css !== "string") return false;
1024
+ if (module.styles !== void 0 && !Array.isArray(module.styles)) return false;
1025
+ if (module.macroArtifacts !== void 0 && !Array.isArray(module.macroArtifacts)) return false;
1026
+ return isPersistablePrecompileModule(module);
1027
+ }
1028
+ /**
1029
+ * `zstd` when this Node has it, `gzip` otherwise.
1030
+ *
1031
+ * The sync zstd bindings arrived in Node 22.15 / 23.8 and this package supports
1032
+ * Node >= 22, so the codec is detected rather than assumed. Both codecs carry
1033
+ * an integrity check of their own -- zstd with `checksumFlag`, gzip with its
1034
+ * trailing CRC32 -- so bit rot inside either body fails decompression instead of
1035
+ * being decoded into a plausible-looking module.
1036
+ */
1037
+ const hasZstd = typeof zlib.zstdCompressSync === "function" && typeof zlib.zstdDecompressSync === "function";
1038
+ const ZSTD_PARAMS = hasZstd ? { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } } : void 0;
1039
+ function compressBody(body) {
1040
+ return hasZstd ? zlib.zstdCompressSync(body, ZSTD_PARAMS) : zlib.gzipSync(body, { level: 1 });
1041
+ }
1042
+ /** `null` for an unknown codec, or one this Node cannot read. */
1043
+ function decompressBody(codec, body) {
1044
+ if (codec === "zstd") return hasZstd ? zlib.zstdDecompressSync(body) : null;
1045
+ if (codec === "gzip") return zlib.gunzipSync(body);
1046
+ return null;
1047
+ }
1048
+ /** `<meta JSON>\n<code><css>\n` -- the trailing LF marks the record boundary. */
1049
+ function encodeRecord(module) {
1050
+ const { code, css, ...rest } = module;
1051
+ const codeBytes = Buffer.from(code, "utf8");
1052
+ const cssBytes = css === void 0 ? null : Buffer.from(css, "utf8");
1053
+ const meta = Buffer.from(JSON.stringify([
1054
+ codeBytes.length,
1055
+ cssBytes === null ? -1 : cssBytes.length,
1056
+ rest
1057
+ ]), "utf8");
1058
+ return Buffer.concat([
1059
+ meta,
1060
+ NEWLINE,
1061
+ codeBytes,
1062
+ cssBytes ?? EMPTY,
1063
+ NEWLINE
1064
+ ]);
1065
+ }
1066
+ /** Serialize the container. Never throws for well-typed entries. */
1067
+ function encodePrecompileManifest(options) {
1068
+ const { key, root, entries } = options;
1069
+ const index = [];
1070
+ const records = [];
1071
+ for (const [file, entry] of entries) {
1072
+ const record = encodeRecord(entry.module);
1073
+ index.push([
1074
+ path.relative(root, file),
1075
+ entry.hash,
1076
+ record.length
1077
+ ]);
1078
+ records.push(record);
1079
+ }
1080
+ const indexBody = compressBody(Buffer.from(JSON.stringify(index), "utf8"));
1081
+ const payloadBody = compressBody(Buffer.concat(records));
1082
+ const header = Buffer.from(JSON.stringify({
1083
+ format: 2,
1084
+ key,
1085
+ codec: hasZstd ? "zstd" : "gzip",
1086
+ index: indexBody.length,
1087
+ payload: payloadBody.length
1088
+ }), "utf8");
1089
+ return Buffer.concat([
1090
+ header,
1091
+ NEWLINE,
1092
+ indexBody,
1093
+ payloadBody
1094
+ ]);
1095
+ }
1096
+ function isByteLength(value) {
1097
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1098
+ }
1099
+ function isIndexRow(value) {
1100
+ return Array.isArray(value) && value.length === 3 && typeof value[0] === "string" && value[0].length > 0 && typeof value[1] === "string" && value[1].length > 0 && isByteLength(value[2]) && value[2] > 0;
1101
+ }
1102
+ /** Rebuild one module from its payload record, or `null` if the record is not one. */
1103
+ function decodeRecord(record) {
1104
+ if (record.at(-1) !== LF) return null;
1105
+ const metaEnd = record.indexOf(LF);
1106
+ if (metaEnd < 0 || metaEnd >= record.length - 1) return null;
1107
+ let meta;
1108
+ try {
1109
+ meta = JSON.parse(record.toString("utf8", 0, metaEnd));
1110
+ } catch {
1111
+ return null;
1112
+ }
1113
+ if (!Array.isArray(meta) || meta.length !== 3) return null;
1114
+ const [codeLength, cssLength, rest] = meta;
1115
+ if (!isByteLength(codeLength) || typeof cssLength !== "number") return null;
1116
+ if (!isByteLength(cssLength) && cssLength !== -1) return null;
1117
+ const cssBytes = cssLength === -1 ? 0 : cssLength;
1118
+ if (metaEnd + 1 + codeLength + cssBytes + 1 !== record.length) return null;
1119
+ if (typeof rest !== "object" || rest === null || Array.isArray(rest)) return null;
1120
+ if ("code" in rest || "css" in rest) return null;
1121
+ const codeStart = metaEnd + 1;
1122
+ const cssStart = codeStart + codeLength;
1123
+ const module = {
1124
+ ...rest,
1125
+ code: record.toString("utf8", codeStart, cssStart)
1126
+ };
1127
+ if (cssLength !== -1) module.css = record.toString("utf8", cssStart, cssStart + cssLength);
1128
+ return isCompiledModule(module) ? module : null;
1129
+ }
1130
+ /**
1131
+ * Parse the container, or return `null`.
1132
+ *
1133
+ * `null` is indistinguishable from having no cache at all, which is exactly the
1134
+ * safe outcome: recompile everything. Every gate below fails that way --
1135
+ * a missing or unparsable header, a foreign `format` or `key`, a codec this Node
1136
+ * cannot read, body lengths that do not account for the file exactly (a
1137
+ * truncated or partially written container), a body that fails its own
1138
+ * decompression checksum, an index that is not the expected shape, and record
1139
+ * lengths that do not sum to the payload. An individual entry whose record does
1140
+ * not decode into a valid `CompiledModule` is dropped on its own, as in format 1;
1141
+ * because offsets come from the index alone, one bad record cannot shift the
1142
+ * others.
1143
+ */
1144
+ function decodePrecompileManifest(bytes, options) {
1145
+ const { key, root, onReject } = options;
1146
+ const reject = (reason) => {
1147
+ onReject?.(reason);
1148
+ return null;
1149
+ };
1150
+ const headerEnd = bytes.indexOf(LF);
1151
+ if (headerEnd < 0) return reject("no header");
1152
+ let header;
1153
+ try {
1154
+ header = JSON.parse(bytes.toString("utf8", 0, headerEnd));
1155
+ } catch {
1156
+ return reject("unparsable header");
1157
+ }
1158
+ if (typeof header !== "object" || header === null || Array.isArray(header)) return reject("unrecognized header");
1159
+ if (header.format !== 2 || header.key !== key) return reject("foreign format or key");
1160
+ if (!isByteLength(header.index) || !isByteLength(header.payload)) return reject("unrecognized body lengths");
1161
+ const indexStart = headerEnd + 1;
1162
+ const payloadStart = indexStart + header.index;
1163
+ if (payloadStart + header.payload !== bytes.length) return reject("truncated container");
1164
+ let bodies;
1165
+ try {
1166
+ const indexText = decompressBody(header.codec, bytes.subarray(indexStart, payloadStart));
1167
+ const payloadText = decompressBody(header.codec, bytes.subarray(payloadStart));
1168
+ bodies = indexText === null || payloadText === null ? null : [indexText, payloadText];
1169
+ } catch {
1170
+ return reject("corrupt body");
1171
+ }
1172
+ if (bodies === null) return reject(`unsupported codec ${JSON.stringify(header.codec)}`);
1173
+ const [indexText, payload] = bodies;
1174
+ let index;
1175
+ try {
1176
+ index = JSON.parse(indexText.toString("utf8"));
1177
+ } catch {
1178
+ return reject("unparsable index");
1179
+ }
1180
+ if (!Array.isArray(index)) return reject("unrecognized index");
1181
+ const entries = /* @__PURE__ */ new Map();
1182
+ let offset = 0;
1183
+ for (const row of index) {
1184
+ if (!isIndexRow(row)) return reject("unrecognized index row");
1185
+ const [relative, hash, length] = row;
1186
+ const end = offset + length;
1187
+ if (end > payload.length) return reject("index overruns payload");
1188
+ const module = decodeRecord(payload.subarray(offset, end));
1189
+ offset = end;
1190
+ if (module !== null) entries.set(path.resolve(root, relative), {
1191
+ hash,
1192
+ module
1193
+ });
1194
+ }
1195
+ if (offset !== payload.length) return reject("payload not fully described by the index");
1196
+ return entries;
1197
+ }
1198
+ //#endregion
882
1199
  //#region src/plugin/precompile-cache.ts
883
1200
  /**
884
1201
  * Persistent (on-disk) pre-compile cache.
@@ -892,10 +1209,11 @@ function computePrecompileCacheKey(compileOptions) {
892
1209
  *
893
1210
  * The two invalidation gates -- manifest identity and per-entry source hash --
894
1211
  * live in `./precompile-cache-key.ts`, which documents why each one is safe.
895
- * Two more gates live here:
1212
+ * Two more gates live in `./precompile-cache-store.ts`, which owns the on-disk
1213
+ * container:
896
1214
  *
897
1215
  * - **Shape.** Entries are validated before use and dropped individually if
898
- * they do not describe a complete `CompiledModule`. The manifest's own
1216
+ * they do not describe a complete `CompiledModule`. The container's own
899
1217
  * `format`/`key` are re-checked after parsing, so a manifest reached by any
900
1218
  * route other than its key still gets rejected.
901
1219
  * - **`src` imports.** SFCs that pull blocks in through `<script src>` /
@@ -913,30 +1231,6 @@ function computePrecompileCacheKey(compileOptions) {
913
1231
  const PRECOMPILE_CACHE_DIR = path.join("node_modules", ".vize", "vite-precompile");
914
1232
  /** Set to `0`/`false` to force a full recompile without editing the config. */
915
1233
  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
1234
  /** Whether the environment forces the cache off. */
941
1235
  function isPrecompileCacheDisabledByEnv(env = process.env) {
942
1236
  const value = env[PRECOMPILE_CACHE_ENV];
@@ -958,8 +1252,8 @@ function openPrecompileCache(options) {
958
1252
  const { root, compileOptions, onDiagnostic, env = process.env } = options;
959
1253
  if (!root || isPrecompileCacheDisabledByEnv(env)) return createDisabledPrecompileCache();
960
1254
  const key = computePrecompileCacheKey(compileOptions);
961
- const file = path.join(root, PRECOMPILE_CACHE_DIR, `${key}.json`);
962
- const entries = readManifestEntries(file, key, onDiagnostic);
1255
+ const file = path.join(root, PRECOMPILE_CACHE_DIR, `${key}${PRECOMPILE_CACHE_EXTENSION}`);
1256
+ const entries = readManifestEntries(file, key, root, onDiagnostic);
963
1257
  let dirty = false;
964
1258
  return {
965
1259
  file,
@@ -992,51 +1286,64 @@ function openPrecompileCache(options) {
992
1286
  },
993
1287
  flush() {
994
1288
  if (!dirty) return false;
995
- if (!writeManifest(file, {
996
- format: 1,
997
- key,
998
- entries: Object.fromEntries(entries)
999
- }, onDiagnostic)) return false;
1289
+ let container;
1290
+ try {
1291
+ container = encodePrecompileManifest({
1292
+ key,
1293
+ root,
1294
+ entries
1295
+ });
1296
+ } catch (error) {
1297
+ onDiagnostic?.(`Failed to encode pre-compile cache ${file}:`, error);
1298
+ return false;
1299
+ }
1300
+ if (!writeManifest(file, container, onDiagnostic)) return false;
1301
+ removeFormat1Manifests(path.dirname(file));
1000
1302
  dirty = false;
1001
1303
  return true;
1002
1304
  }
1003
1305
  };
1004
1306
  }
1005
1307
  /**
1006
- * Parse the manifest, or return an empty map.
1308
+ * Drop the `.json` manifests format 1 left behind.
1309
+ *
1310
+ * Nothing can read them any more, and they are the reason this change exists:
1311
+ * ~9 KB per SFC, so ~27 MB for a 3000-SFC project sitting in `node_modules`
1312
+ * forever after an upgrade. Only `.json` is removed, which is exactly the set
1313
+ * format 1 wrote; every live manifest is a `.vpc`, and a project legitimately
1314
+ * keeps one per compile-option set. Best effort -- a failure here is not worth
1315
+ * a diagnostic, let alone a failed build.
1316
+ */
1317
+ function removeFormat1Manifests(dir) {
1318
+ try {
1319
+ for (const name of fs.readdirSync(dir)) if (name.endsWith(".json")) fs.rmSync(path.join(dir, name), { force: true });
1320
+ } catch {}
1321
+ }
1322
+ /**
1323
+ * Read the container, or return an empty map.
1007
1324
  *
1008
1325
  * A missing, truncated, corrupt, or foreign manifest is indistinguishable from
1009
1326
  * no cache at all, which is exactly the safe outcome: recompile everything.
1010
1327
  */
1011
- function readManifestEntries(file, key, onDiagnostic) {
1012
- const entries = /* @__PURE__ */ new Map();
1013
- let raw;
1328
+ function readManifestEntries(file, key, root, onDiagnostic) {
1329
+ let bytes;
1014
1330
  try {
1015
- raw = fs.readFileSync(file, "utf-8");
1331
+ bytes = fs.readFileSync(file);
1016
1332
  } catch {
1017
- return entries;
1333
+ return /* @__PURE__ */ new Map();
1018
1334
  }
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;
1335
+ return decodePrecompileManifest(bytes, {
1336
+ key,
1337
+ root,
1338
+ onReject: (reason) => onDiagnostic?.(`Ignoring pre-compile cache ${file}: ${reason}`)
1339
+ }) ?? /* @__PURE__ */ new Map();
1033
1340
  }
1034
1341
  /** Write through a sibling temp file so a crash cannot leave a partial manifest. */
1035
- function writeManifest(file, manifest, onDiagnostic) {
1342
+ function writeManifest(file, container, onDiagnostic) {
1036
1343
  const temp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
1037
1344
  try {
1038
1345
  fs.mkdirSync(path.dirname(file), { recursive: true });
1039
- fs.writeFileSync(temp, JSON.stringify(manifest));
1346
+ fs.writeFileSync(temp, container);
1040
1347
  fs.renameSync(temp, file);
1041
1348
  return true;
1042
1349
  } catch (error) {
@@ -1599,10 +1906,10 @@ function isPotentialVizeResolveId(id) {
1599
1906
  function classifyImporterRequest(importer) {
1600
1907
  return importer ? classifyVitePluginRequest(importer) : null;
1601
1908
  }
1602
- function isPotentialVizeImporter(importer, importerRequest) {
1909
+ function isPotentialVizeImporter(importer) {
1603
1910
  if (importer === void 0) return false;
1604
1911
  if (importer.startsWith("\0") || importer.startsWith("vize:")) return true;
1605
- return importerRequest?.isVueSfcPath ?? false;
1912
+ return importer.includes(".vue");
1606
1913
  }
1607
1914
  function shouldCompileVueSfcRequest(request) {
1608
1915
  if (!request.isVueSfcPath || request.isVueStyleQuery || request.hasMacroQuery || request.hasDefinePageQuery) return false;
@@ -1636,8 +1943,8 @@ async function resolveAliasedVueImport(ctx, state, id, importer, isSsrRequest, h
1636
1943
  return null;
1637
1944
  }
1638
1945
  async function resolveIdHook(ctx, state, id, importer, options) {
1946
+ if (!isPotentialVizeResolveId(id) && !isPotentialVizeImporter(importer)) return null;
1639
1947
  const importerRequest = classifyImporterRequest(importer);
1640
- if (!isPotentialVizeResolveId(id) && !isPotentialVizeImporter(importer, importerRequest)) return null;
1641
1948
  const isBuild = state.server === null;
1642
1949
  const isDependencyScan = !!options?.scan;
1643
1950
  const isSsrRequest = !!options?.ssr || (importerRequest?.isVizeSsrVirtual ?? false) || (importer ? isPluginVisibleSsrVirtualId(importer) : false);
@@ -2067,19 +2374,19 @@ function loadCompiledSfcModule(state, realPath, isSsr, currentBase, loadOptions)
2067
2374
  if (!compiled) return null;
2068
2375
  for (const watchFile of new Set([realPath, ...compiled.dependencies ?? []])) loadOptions?.addWatchFile?.(watchFile);
2069
2376
  const hasDelegated = hasDelegatedStyles(compiled);
2070
- const pendingHmrUpdateType = loadOptions?.ssr ? void 0 : state.pendingHmrUpdateTypes.get(realPath);
2071
- if (compiled.css && !hasDelegated) compiled = {
2072
- ...compiled,
2073
- css: resolveCssImports(compiled.css, realPath, state.cssAliasRules, state.server !== null, currentBase)
2074
- };
2075
- const generatedOutput = generateOutput(compiled, {
2377
+ const outputOptions = {
2076
2378
  isProduction: state.isProduction,
2077
2379
  isDev: state.server !== null && !isSsr,
2078
2380
  ssr: isSsr,
2079
- hmrUpdateType: pendingHmrUpdateType,
2381
+ hmrUpdateType: loadOptions?.ssr ? void 0 : state.pendingHmrUpdateTypes.get(realPath),
2080
2382
  extractCss,
2081
2383
  filePath: realPath
2082
- });
2384
+ };
2385
+ if (compiled.css && !hasDelegated && embedsInlineCss(compiled, outputOptions)) compiled = {
2386
+ ...compiled,
2387
+ css: resolveCssImports(compiled.css, realPath, state.cssAliasRules, state.server !== null, currentBase)
2388
+ };
2389
+ const generatedOutput = generateOutput(compiled, outputOptions);
2083
2390
  const normalizedOutput = rewriteImportMetaGlobBase(rewriteStaticAssetUrls(rewriteDynamicTemplateImports(isSsr ? normalizeVueServerRendererImport(generatedOutput) : generatedOutput, state.dynamicImportAliasRules), state.dynamicImportAliasRules), realPath, state.root);
2084
2391
  if (!loadOptions?.ssr) state.pendingHmrUpdateTypes.delete(realPath);
2085
2392
  return {
@@ -2420,7 +2727,22 @@ function normalizeVirtualStyleId(id) {
2420
2727
  if (!withoutPrefix.includes("?vue")) return id;
2421
2728
  return withoutPrefix.replace(/\.module\.\w+$/, "").replace(/\.\w+$/, "");
2422
2729
  }
2730
+ /**
2731
+ * String pre-gate for {@link transformScopedPreprocessorCss} (#3427).
2732
+ *
2733
+ * The post-transform plugin's `transform` hook sees every module in the graph,
2734
+ * and without this every one of them crossed the NAPI boundary to be told it is
2735
+ * not a style query. The native `isVueStyleQuery` is
2736
+ * `query.contains("vue&type=style") || query.contains("vue=&type=style")`, both
2737
+ * of which contain `type=style`; the query is a substring of the id, and
2738
+ * `normalizeVirtualStyleId` only ever deletes characters, so a normalized id
2739
+ * that classifies as a style query implies `type=style` in the raw id.
2740
+ */
2741
+ function mayBeVueStyleQuery(id) {
2742
+ return id.includes("type=style");
2743
+ }
2423
2744
  function transformScopedPreprocessorCss(code, id) {
2745
+ if (!mayBeVueStyleQuery(id)) return null;
2424
2746
  const request = classifyVitePluginRequest(normalizeVirtualStyleId(id));
2425
2747
  if (!request.isVueStyleQuery || !request.styleScoped || !request.styleLang || request.styleLang === "css") return null;
2426
2748
  return scopeCssForPipeline(code, request.styleScoped);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizejs/vite-plugin",
3
- "version": "0.303.0",
3
+ "version": "0.306.0",
4
4
  "description": "High-performance native Vite plugin for Vue SFC compilation powered by Vize",
5
5
  "keywords": [
6
6
  "compiler",
@@ -45,10 +45,10 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
- "@vizejs/native": "0.303.0",
48
+ "@vizejs/native": "0.306.0",
49
49
  "oxc-parser": "0.133.0",
50
50
  "tinyglobby": "0.2.16",
51
- "vize": "0.303.0"
51
+ "vize": "0.306.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "25.9.2",