@vizejs/vite-plugin 0.302.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.
- package/dist/index.mjs +714 -110
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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
6
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
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");
|
|
@@ -526,6 +610,83 @@ function createLogger(debug) {
|
|
|
526
610
|
};
|
|
527
611
|
}
|
|
528
612
|
//#endregion
|
|
613
|
+
//#region src/plugin/precompile.ts
|
|
614
|
+
const DEFAULT_PRECOMPILE_IGNORE_PATTERNS = [
|
|
615
|
+
"node_modules/**",
|
|
616
|
+
"dist/**",
|
|
617
|
+
".git/**",
|
|
618
|
+
".nuxt/**",
|
|
619
|
+
".output/**",
|
|
620
|
+
".nitro/**",
|
|
621
|
+
"coverage/**"
|
|
622
|
+
];
|
|
623
|
+
function isPrecompileSfcPath(path) {
|
|
624
|
+
return path.endsWith(".vue");
|
|
625
|
+
}
|
|
626
|
+
function diffPrecompileFiles(files, currentMetadata, previousMetadata) {
|
|
627
|
+
return diffVitePrecompileFiles([...files], toNativePrecompileMetadataEntries(currentMetadata), toNativePrecompileMetadataEntries(previousMetadata));
|
|
628
|
+
}
|
|
629
|
+
function normalizePrecompileBatchSize(value) {
|
|
630
|
+
return normalizeVitePrecompileBatchSize(value);
|
|
631
|
+
}
|
|
632
|
+
function chunkPrecompileFiles(files, batchSize, options = {}) {
|
|
633
|
+
return chunkVitePrecompileFiles([...files], batchSize, {
|
|
634
|
+
maxBytes: options.maxBytes,
|
|
635
|
+
metadata: options.metadata ? toNativePrecompileMetadataEntries(options.metadata) : void 0
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function toNativePrecompileMetadataEntries(metadata) {
|
|
639
|
+
const entries = [];
|
|
640
|
+
for (const [path, value] of metadata) entries.push({
|
|
641
|
+
path,
|
|
642
|
+
mtimeMs: value.mtimeMs,
|
|
643
|
+
size: value.size
|
|
644
|
+
});
|
|
645
|
+
return entries;
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/plugin/state.ts
|
|
649
|
+
function getEnvironmentCache(state, ssr) {
|
|
650
|
+
return ssr ? state.ssrCache : state.cache;
|
|
651
|
+
}
|
|
652
|
+
function getCompileOptionsForRequest(state, ssr) {
|
|
653
|
+
const options = {
|
|
654
|
+
sourceMap: state.mergedOptions?.sourceMap ?? !state.isProduction,
|
|
655
|
+
ssr,
|
|
656
|
+
vapor: !ssr && (state.mergedOptions?.vapor ?? false),
|
|
657
|
+
customRenderer: state.mergedOptions?.customRenderer ?? false,
|
|
658
|
+
templateSyntax: state.mergedOptions?.templateSyntax ?? "standard"
|
|
659
|
+
};
|
|
660
|
+
if (state.mergedOptions?.mode !== void 0) options.mode = state.mergedOptions.mode;
|
|
661
|
+
if (state.mergedOptions?.runtimeModuleName !== void 0) options.runtimeModuleName = state.mergedOptions.runtimeModuleName;
|
|
662
|
+
if (state.mergedOptions?.runtimeGlobalName !== void 0) options.runtimeGlobalName = state.mergedOptions.runtimeGlobalName;
|
|
663
|
+
if (state.mergedOptions?.vueVersion !== void 0) options.vueVersion = state.mergedOptions.vueVersion;
|
|
664
|
+
if (state.mergedOptions?.experimentalInTagComments) options.experimentalInTagComments = true;
|
|
665
|
+
if (state.mergedOptions?.experimentalPatternedTemplate) options.experimentalPatternedTemplate = true;
|
|
666
|
+
if (state.mergedOptions?.experimentalServerScript) options.experimentalServerScript = true;
|
|
667
|
+
return options;
|
|
668
|
+
}
|
|
669
|
+
function syncCollectedCssForFile(state, filePath, compiled) {
|
|
670
|
+
if (!compiled || !state.extractCss) return;
|
|
671
|
+
if (compiled.styles?.length) {
|
|
672
|
+
state.collectedCss.delete(filePath);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (compiled.css && !hasDelegatedStyles(compiled)) state.collectedCss.set(filePath, resolveCssImports(compiled.css, filePath, state.cssAliasRules, false));
|
|
676
|
+
else state.collectedCss.delete(filePath);
|
|
677
|
+
}
|
|
678
|
+
function shouldExtractCssForRequest(state, ssr) {
|
|
679
|
+
return state.isProduction && !ssr;
|
|
680
|
+
}
|
|
681
|
+
function clearBuildCaches(state) {
|
|
682
|
+
state.cache.clear();
|
|
683
|
+
state.ssrCache.clear();
|
|
684
|
+
state.collectedCss.clear();
|
|
685
|
+
state.precompileMetadata.clear();
|
|
686
|
+
state.pendingHmrUpdateTypes.clear();
|
|
687
|
+
state.viteResolveCache?.clear();
|
|
688
|
+
}
|
|
689
|
+
//#endregion
|
|
529
690
|
//#region src/compile-options.ts
|
|
530
691
|
function buildCompileFileOptions(filePath, options) {
|
|
531
692
|
return {
|
|
@@ -742,82 +903,495 @@ function compileBatch(files, cache, options) {
|
|
|
742
903
|
}
|
|
743
904
|
return result;
|
|
744
905
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
".
|
|
753
|
-
".nitro/**",
|
|
754
|
-
"coverage/**"
|
|
755
|
-
];
|
|
756
|
-
function isPrecompileSfcPath(path) {
|
|
757
|
-
return path.endsWith(".vue");
|
|
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
|
+
*/
|
|
912
|
+
function hashPrecompileSource(source) {
|
|
913
|
+
return crypto.createHash("sha256").update(source, "utf8").digest("base64url");
|
|
758
914
|
}
|
|
759
|
-
|
|
760
|
-
|
|
915
|
+
/** Key-independent JSON: object key order must not change the hash. */
|
|
916
|
+
function stableStringify(value) {
|
|
917
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
918
|
+
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(",")}}`;
|
|
919
|
+
return JSON.stringify(value) ?? "null";
|
|
761
920
|
}
|
|
762
|
-
function
|
|
763
|
-
|
|
921
|
+
function describeBinary(binary) {
|
|
922
|
+
try {
|
|
923
|
+
const stat = fs.statSync(binary);
|
|
924
|
+
return `${path.basename(binary)}:${stat.size}:${stat.mtimeMs}`;
|
|
925
|
+
} catch {
|
|
926
|
+
return `${path.basename(binary)}:unresolved`;
|
|
927
|
+
}
|
|
764
928
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
929
|
+
/**
|
|
930
|
+
* Identity of the native compiler that produced (or would produce) the output.
|
|
931
|
+
*
|
|
932
|
+
* The package version covers releases; the binary's size and mtime cover local
|
|
933
|
+
* `pnpm --dir npm/native build:debug` rebuilds, which change codegen without
|
|
934
|
+
* changing any version. Hashing the binary itself would be airtight but costs
|
|
935
|
+
* hundreds of milliseconds for a >30 MB artifact, so a rebuild is treated as a
|
|
936
|
+
* new identity — a miss, never a stale hit.
|
|
937
|
+
*/
|
|
938
|
+
function resolveCompilerIdentity() {
|
|
939
|
+
const configured = process.env.NAPI_RS_NATIVE_LIBRARY_PATH;
|
|
940
|
+
try {
|
|
941
|
+
const manifestPath = createRequire(import.meta.url).resolve("@vizejs/native/package.json");
|
|
942
|
+
const packageDir = path.dirname(manifestPath);
|
|
943
|
+
const version = JSON.parse(fs.readFileSync(manifestPath, "utf-8")).version;
|
|
944
|
+
const binaries = configured ? [configured] : fs.readdirSync(packageDir).filter((name) => name.endsWith(".node")).sort().map((name) => path.join(packageDir, name));
|
|
945
|
+
return {
|
|
946
|
+
version: typeof version === "string" ? version : "unknown",
|
|
947
|
+
binaries: binaries.map(describeBinary)
|
|
948
|
+
};
|
|
949
|
+
} catch {
|
|
950
|
+
return {
|
|
951
|
+
version: "unresolved",
|
|
952
|
+
binaries: [configured ?? "unresolved"]
|
|
953
|
+
};
|
|
954
|
+
}
|
|
770
955
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
956
|
+
/**
|
|
957
|
+
* Hash of everything outside the source text that changes compiled output.
|
|
958
|
+
*
|
|
959
|
+
* `compileOptions` must be the object actually handed to the native batch
|
|
960
|
+
* compiler, so a newly added compile option cannot be forgotten here.
|
|
961
|
+
*/
|
|
962
|
+
function computePrecompileCacheKey(compileOptions) {
|
|
963
|
+
const material = stableStringify({
|
|
964
|
+
format: 2,
|
|
965
|
+
compiler: resolveCompilerIdentity(),
|
|
966
|
+
options: compileOptions
|
|
777
967
|
});
|
|
778
|
-
return
|
|
968
|
+
return crypto.createHash("sha256").update(material, "utf8").digest("hex").slice(0, 32);
|
|
779
969
|
}
|
|
780
970
|
//#endregion
|
|
781
|
-
//#region src/plugin/
|
|
782
|
-
|
|
783
|
-
|
|
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);
|
|
784
1027
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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)
|
|
792
1126
|
};
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
if (state.mergedOptions?.runtimeGlobalName !== void 0) options.runtimeGlobalName = state.mergedOptions.runtimeGlobalName;
|
|
796
|
-
if (state.mergedOptions?.vueVersion !== void 0) options.vueVersion = state.mergedOptions.vueVersion;
|
|
797
|
-
if (state.mergedOptions?.experimentalInTagComments) options.experimentalInTagComments = true;
|
|
798
|
-
if (state.mergedOptions?.experimentalPatternedTemplate) options.experimentalPatternedTemplate = true;
|
|
799
|
-
if (state.mergedOptions?.experimentalServerScript) options.experimentalServerScript = true;
|
|
800
|
-
return options;
|
|
1127
|
+
if (cssLength !== -1) module.css = record.toString("utf8", cssStart, cssStart + cssLength);
|
|
1128
|
+
return isCompiledModule(module) ? module : null;
|
|
801
1129
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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");
|
|
807
1171
|
}
|
|
808
|
-
if (
|
|
809
|
-
|
|
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;
|
|
810
1197
|
}
|
|
811
|
-
|
|
812
|
-
|
|
1198
|
+
//#endregion
|
|
1199
|
+
//#region src/plugin/precompile-cache.ts
|
|
1200
|
+
/**
|
|
1201
|
+
* Persistent (on-disk) pre-compile cache.
|
|
1202
|
+
*
|
|
1203
|
+
* `state.precompileMetadata` plus `state.cache` already skip recompilation
|
|
1204
|
+
* inside one process, but both are empty on every process start, so `vite
|
|
1205
|
+
* build`, CI, and each dev-server start recompile the whole scan from scratch.
|
|
1206
|
+
* This module backs them with a manifest under
|
|
1207
|
+
* `node_modules/.vize/vite-precompile/` so a cold process can restore the
|
|
1208
|
+
* previous run's output.
|
|
1209
|
+
*
|
|
1210
|
+
* The two invalidation gates -- manifest identity and per-entry source hash --
|
|
1211
|
+
* live in `./precompile-cache-key.ts`, which documents why each one is safe.
|
|
1212
|
+
* Two more gates live in `./precompile-cache-store.ts`, which owns the on-disk
|
|
1213
|
+
* container:
|
|
1214
|
+
*
|
|
1215
|
+
* - **Shape.** Entries are validated before use and dropped individually if
|
|
1216
|
+
* they do not describe a complete `CompiledModule`. The container's own
|
|
1217
|
+
* `format`/`key` are re-checked after parsing, so a manifest reached by any
|
|
1218
|
+
* route other than its key still gets rejected.
|
|
1219
|
+
* - **`src` imports.** SFCs that pull blocks in through `<script src>` /
|
|
1220
|
+
* `<template src>` / `<style src>` are never persisted: their output depends
|
|
1221
|
+
* on files whose content this cache does not hash.
|
|
1222
|
+
*
|
|
1223
|
+
* A missing, truncated, or corrupt manifest degrades to a full recompile: every
|
|
1224
|
+
* read is guarded and any failure yields an empty cache. Writes go through a
|
|
1225
|
+
* sibling temp file and a rename, so an interrupted write cannot be read back.
|
|
1226
|
+
*
|
|
1227
|
+
* Set `VIZE_PRECOMPILE_CACHE=0` to force a full recompile without editing any
|
|
1228
|
+
* config.
|
|
1229
|
+
*/
|
|
1230
|
+
/** Manifest location, relative to the Vite root. */
|
|
1231
|
+
const PRECOMPILE_CACHE_DIR = path.join("node_modules", ".vize", "vite-precompile");
|
|
1232
|
+
/** Set to `0`/`false` to force a full recompile without editing the config. */
|
|
1233
|
+
const PRECOMPILE_CACHE_ENV = "VIZE_PRECOMPILE_CACHE";
|
|
1234
|
+
/** Whether the environment forces the cache off. */
|
|
1235
|
+
function isPrecompileCacheDisabledByEnv(env = process.env) {
|
|
1236
|
+
const value = env[PRECOMPILE_CACHE_ENV];
|
|
1237
|
+
return value === "0" || value === "false";
|
|
1238
|
+
}
|
|
1239
|
+
const disabledCache = {
|
|
1240
|
+
file: null,
|
|
1241
|
+
get: () => void 0,
|
|
1242
|
+
set: () => {},
|
|
1243
|
+
delete: () => {},
|
|
1244
|
+
retain: () => {},
|
|
1245
|
+
flush: () => false
|
|
1246
|
+
};
|
|
1247
|
+
/** A cache that never hits and never writes. */
|
|
1248
|
+
function createDisabledPrecompileCache() {
|
|
1249
|
+
return disabledCache;
|
|
1250
|
+
}
|
|
1251
|
+
function openPrecompileCache(options) {
|
|
1252
|
+
const { root, compileOptions, onDiagnostic, env = process.env } = options;
|
|
1253
|
+
if (!root || isPrecompileCacheDisabledByEnv(env)) return createDisabledPrecompileCache();
|
|
1254
|
+
const key = computePrecompileCacheKey(compileOptions);
|
|
1255
|
+
const file = path.join(root, PRECOMPILE_CACHE_DIR, `${key}${PRECOMPILE_CACHE_EXTENSION}`);
|
|
1256
|
+
const entries = readManifestEntries(file, key, root, onDiagnostic);
|
|
1257
|
+
let dirty = false;
|
|
1258
|
+
return {
|
|
1259
|
+
file,
|
|
1260
|
+
get(filePath, sourceHash) {
|
|
1261
|
+
const entry = entries.get(filePath);
|
|
1262
|
+
return entry && entry.hash === sourceHash ? entry.module : void 0;
|
|
1263
|
+
},
|
|
1264
|
+
set(filePath, sourceHash, module) {
|
|
1265
|
+
if (!isPersistablePrecompileModule(module)) {
|
|
1266
|
+
if (entries.delete(filePath)) dirty = true;
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const existing = entries.get(filePath);
|
|
1270
|
+
if (existing?.hash === sourceHash && existing.module === module) return;
|
|
1271
|
+
entries.set(filePath, {
|
|
1272
|
+
hash: sourceHash,
|
|
1273
|
+
module
|
|
1274
|
+
});
|
|
1275
|
+
dirty = true;
|
|
1276
|
+
},
|
|
1277
|
+
delete(filePath) {
|
|
1278
|
+
if (entries.delete(filePath)) dirty = true;
|
|
1279
|
+
},
|
|
1280
|
+
retain(files) {
|
|
1281
|
+
const keep = files instanceof Set ? files : new Set(files);
|
|
1282
|
+
for (const filePath of entries.keys()) if (!keep.has(filePath)) {
|
|
1283
|
+
entries.delete(filePath);
|
|
1284
|
+
dirty = true;
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
flush() {
|
|
1288
|
+
if (!dirty) 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));
|
|
1302
|
+
dirty = false;
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
813
1306
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1307
|
+
/**
|
|
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.
|
|
1324
|
+
*
|
|
1325
|
+
* A missing, truncated, corrupt, or foreign manifest is indistinguishable from
|
|
1326
|
+
* no cache at all, which is exactly the safe outcome: recompile everything.
|
|
1327
|
+
*/
|
|
1328
|
+
function readManifestEntries(file, key, root, onDiagnostic) {
|
|
1329
|
+
let bytes;
|
|
1330
|
+
try {
|
|
1331
|
+
bytes = fs.readFileSync(file);
|
|
1332
|
+
} catch {
|
|
1333
|
+
return /* @__PURE__ */ new Map();
|
|
1334
|
+
}
|
|
1335
|
+
return decodePrecompileManifest(bytes, {
|
|
1336
|
+
key,
|
|
1337
|
+
root,
|
|
1338
|
+
onReject: (reason) => onDiagnostic?.(`Ignoring pre-compile cache ${file}: ${reason}`)
|
|
1339
|
+
}) ?? /* @__PURE__ */ new Map();
|
|
1340
|
+
}
|
|
1341
|
+
/** Write through a sibling temp file so a crash cannot leave a partial manifest. */
|
|
1342
|
+
function writeManifest(file, container, onDiagnostic) {
|
|
1343
|
+
const temp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
1344
|
+
try {
|
|
1345
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1346
|
+
fs.writeFileSync(temp, container);
|
|
1347
|
+
fs.renameSync(temp, file);
|
|
1348
|
+
return true;
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
try {
|
|
1351
|
+
fs.rmSync(temp, { force: true });
|
|
1352
|
+
} catch {}
|
|
1353
|
+
onDiagnostic?.(`Failed to write pre-compile cache ${file}:`, error);
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
//#endregion
|
|
1358
|
+
//#region src/plugin/precompile-run.ts
|
|
1359
|
+
/**
|
|
1360
|
+
* The pre-compilation pass.
|
|
1361
|
+
*
|
|
1362
|
+
* Scans the configured patterns, diffs them against the previous in-process
|
|
1363
|
+
* run, restores whatever the persistent cache can prove is still valid, and
|
|
1364
|
+
* batch-compiles the rest. Extracted from `state.ts` so the state module stays
|
|
1365
|
+
* a state module.
|
|
1366
|
+
*/
|
|
1367
|
+
/**
|
|
1368
|
+
* The options the pre-compile batch actually compiles with.
|
|
1369
|
+
*
|
|
1370
|
+
* Also the input to the cache key, so the two can never drift: a new option
|
|
1371
|
+
* that reaches the native compiler reaches the key with it.
|
|
1372
|
+
*/
|
|
1373
|
+
function resolvePrecompileBatchOptions(state) {
|
|
1374
|
+
return {
|
|
1375
|
+
ssr: false,
|
|
1376
|
+
vapor: state.mergedOptions.vapor ?? false,
|
|
1377
|
+
mode: state.mergedOptions.mode,
|
|
1378
|
+
customRenderer: state.mergedOptions.customRenderer ?? false,
|
|
1379
|
+
templateSyntax: state.mergedOptions.templateSyntax ?? "standard",
|
|
1380
|
+
experimentalInTagComments: state.mergedOptions.experimentalInTagComments ?? false,
|
|
1381
|
+
experimentalPatternedTemplate: state.mergedOptions.experimentalPatternedTemplate ?? false,
|
|
1382
|
+
experimentalServerScript: state.mergedOptions.experimentalServerScript ?? false,
|
|
1383
|
+
runtimeModuleName: state.mergedOptions.runtimeModuleName,
|
|
1384
|
+
runtimeGlobalName: state.mergedOptions.runtimeGlobalName,
|
|
1385
|
+
vueVersion: state.mergedOptions.vueVersion
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function openCacheForRun(state, batchOptions) {
|
|
1389
|
+
if (!state.root) return createDisabledPrecompileCache();
|
|
1390
|
+
return openPrecompileCache({
|
|
1391
|
+
root: state.root,
|
|
1392
|
+
compileOptions: buildCompileBatchOptions(batchOptions),
|
|
1393
|
+
onDiagnostic: (message, error) => error === void 0 ? state.logger.warn(message) : state.logger.warn(message, error)
|
|
1394
|
+
});
|
|
821
1395
|
}
|
|
822
1396
|
/**
|
|
823
1397
|
* Pre-compile all Vue files matching scan patterns.
|
|
@@ -858,41 +1432,50 @@ async function compileAll(state) {
|
|
|
858
1432
|
if (state.extractCss) state.collectedCss.delete(file);
|
|
859
1433
|
state.pendingHmrUpdateTypes.delete(file);
|
|
860
1434
|
}
|
|
1435
|
+
const batchOptions = resolvePrecompileBatchOptions(state);
|
|
1436
|
+
const cache = openCacheForRun(state, batchOptions);
|
|
1437
|
+
const cacheEnabled = cache.file !== null;
|
|
861
1438
|
let successCount = 0;
|
|
1439
|
+
let restoredCount = 0;
|
|
862
1440
|
let failedCount = 0;
|
|
863
1441
|
let nativeTimeMs = 0;
|
|
864
1442
|
const precompileFailures = [];
|
|
865
1443
|
const chunks = chunkPrecompileFiles(changedFiles, state.precompileBatchSize, { metadata: currentMetadata });
|
|
866
1444
|
for (const chunk of chunks) {
|
|
867
1445
|
const fileContents = [];
|
|
868
|
-
|
|
869
|
-
|
|
1446
|
+
const sourceHashes = /* @__PURE__ */ new Map();
|
|
1447
|
+
for (const file of chunk) {
|
|
1448
|
+
let source;
|
|
1449
|
+
try {
|
|
1450
|
+
source = fs.readFileSync(file, "utf-8");
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
failedCount++;
|
|
1453
|
+
state.cache.delete(file);
|
|
1454
|
+
if (state.extractCss) state.collectedCss.delete(file);
|
|
1455
|
+
state.precompileMetadata.delete(file);
|
|
1456
|
+
cache.delete(file);
|
|
1457
|
+
precompileFailures.push(`[vize] Failed to read ${file}: ${formatUnknownError$2(e)}`);
|
|
1458
|
+
state.logger.error(`Failed to read ${file}:`, e);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const sourceHash = cacheEnabled ? hashPrecompileSource(source) : void 0;
|
|
1462
|
+
const restored = sourceHash === void 0 ? void 0 : cache.get(file, sourceHash);
|
|
1463
|
+
if (restored) {
|
|
1464
|
+
state.cache.set(file, restored);
|
|
1465
|
+
const metadata = currentMetadata.get(file);
|
|
1466
|
+
if (metadata) state.precompileMetadata.set(file, metadata);
|
|
1467
|
+
syncCollectedCssForFile(state, file, restored);
|
|
1468
|
+
restoredCount++;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (sourceHash !== void 0) sourceHashes.set(file, sourceHash);
|
|
870
1472
|
fileContents.push({
|
|
871
1473
|
path: file,
|
|
872
1474
|
source
|
|
873
1475
|
});
|
|
874
|
-
} catch (e) {
|
|
875
|
-
failedCount++;
|
|
876
|
-
state.cache.delete(file);
|
|
877
|
-
if (state.extractCss) state.collectedCss.delete(file);
|
|
878
|
-
state.precompileMetadata.delete(file);
|
|
879
|
-
precompileFailures.push(`[vize] Failed to read ${file}: ${formatUnknownError$2(e)}`);
|
|
880
|
-
state.logger.error(`Failed to read ${file}:`, e);
|
|
881
1476
|
}
|
|
882
1477
|
if (fileContents.length === 0) continue;
|
|
883
|
-
const result = compileBatch(fileContents, state.cache,
|
|
884
|
-
ssr: false,
|
|
885
|
-
vapor: state.mergedOptions.vapor ?? false,
|
|
886
|
-
mode: state.mergedOptions.mode,
|
|
887
|
-
customRenderer: state.mergedOptions.customRenderer ?? false,
|
|
888
|
-
templateSyntax: state.mergedOptions.templateSyntax ?? "standard",
|
|
889
|
-
experimentalInTagComments: state.mergedOptions.experimentalInTagComments ?? false,
|
|
890
|
-
experimentalPatternedTemplate: state.mergedOptions.experimentalPatternedTemplate ?? false,
|
|
891
|
-
experimentalServerScript: state.mergedOptions.experimentalServerScript ?? false,
|
|
892
|
-
runtimeModuleName: state.mergedOptions.runtimeModuleName,
|
|
893
|
-
runtimeGlobalName: state.mergedOptions.runtimeGlobalName,
|
|
894
|
-
vueVersion: state.mergedOptions.vueVersion
|
|
895
|
-
});
|
|
1478
|
+
const result = compileBatch(fileContents, state.cache, batchOptions);
|
|
896
1479
|
const chunkFailedCount = result.results.filter((fileResult) => fileResult.errors.length > 0).length;
|
|
897
1480
|
failedCount += chunkFailedCount;
|
|
898
1481
|
successCount += result.results.length - chunkFailedCount;
|
|
@@ -903,16 +1486,22 @@ async function compileAll(state) {
|
|
|
903
1486
|
state.cache.delete(fileResult.path);
|
|
904
1487
|
if (state.extractCss) state.collectedCss.delete(fileResult.path);
|
|
905
1488
|
state.precompileMetadata.delete(fileResult.path);
|
|
1489
|
+
cache.delete(fileResult.path);
|
|
906
1490
|
precompileFailures.push(formatCompileErrorMessage(fileResult.path, fileResult.errors));
|
|
907
1491
|
continue;
|
|
908
1492
|
}
|
|
909
1493
|
if (metadata) state.precompileMetadata.set(fileResult.path, metadata);
|
|
910
|
-
|
|
1494
|
+
const compiled = state.cache.get(fileResult.path);
|
|
1495
|
+
const sourceHash = sourceHashes.get(fileResult.path);
|
|
1496
|
+
if (compiled && sourceHash !== void 0) cache.set(fileResult.path, sourceHash, compiled);
|
|
1497
|
+
syncCollectedCssForFile(state, fileResult.path, compiled);
|
|
911
1498
|
}
|
|
912
1499
|
}
|
|
1500
|
+
cache.retain(new Set(sfcFiles));
|
|
1501
|
+
cache.flush();
|
|
913
1502
|
const elapsed = (performance.now() - startTime).toFixed(2);
|
|
914
1503
|
const batchLabel = chunks.length === 1 ? "batch" : "batches";
|
|
915
|
-
state.logger.info(`Pre-compilation complete: ${successCount} recompiled, ${cachedFileCount} reused, ${failedCount} failed (${elapsed}ms, native ${batchLabel}: ${nativeTimeMs.toFixed(2)}ms)`);
|
|
1504
|
+
state.logger.info(`Pre-compilation complete: ${successCount} recompiled, ${restoredCount} restored from disk, ${cachedFileCount} reused, ${failedCount} failed (${elapsed}ms, native ${batchLabel}: ${nativeTimeMs.toFixed(2)}ms)`);
|
|
916
1505
|
if (failedCount > 0) {
|
|
917
1506
|
const details = precompileFailures.length > 0 ? `\n\n${precompileFailures.join("\n\n")}` : "";
|
|
918
1507
|
throw new Error(`[vize] Pre-compilation failed for ${failedCount} file(s).${details}`);
|
|
@@ -1317,10 +1906,10 @@ function isPotentialVizeResolveId(id) {
|
|
|
1317
1906
|
function classifyImporterRequest(importer) {
|
|
1318
1907
|
return importer ? classifyVitePluginRequest(importer) : null;
|
|
1319
1908
|
}
|
|
1320
|
-
function isPotentialVizeImporter(importer
|
|
1909
|
+
function isPotentialVizeImporter(importer) {
|
|
1321
1910
|
if (importer === void 0) return false;
|
|
1322
1911
|
if (importer.startsWith("\0") || importer.startsWith("vize:")) return true;
|
|
1323
|
-
return
|
|
1912
|
+
return importer.includes(".vue");
|
|
1324
1913
|
}
|
|
1325
1914
|
function shouldCompileVueSfcRequest(request) {
|
|
1326
1915
|
if (!request.isVueSfcPath || request.isVueStyleQuery || request.hasMacroQuery || request.hasDefinePageQuery) return false;
|
|
@@ -1354,8 +1943,8 @@ async function resolveAliasedVueImport(ctx, state, id, importer, isSsrRequest, h
|
|
|
1354
1943
|
return null;
|
|
1355
1944
|
}
|
|
1356
1945
|
async function resolveIdHook(ctx, state, id, importer, options) {
|
|
1946
|
+
if (!isPotentialVizeResolveId(id) && !isPotentialVizeImporter(importer)) return null;
|
|
1357
1947
|
const importerRequest = classifyImporterRequest(importer);
|
|
1358
|
-
if (!isPotentialVizeResolveId(id) && !isPotentialVizeImporter(importer, importerRequest)) return null;
|
|
1359
1948
|
const isBuild = state.server === null;
|
|
1360
1949
|
const isDependencyScan = !!options?.scan;
|
|
1361
1950
|
const isSsrRequest = !!options?.ssr || (importerRequest?.isVizeSsrVirtual ?? false) || (importer ? isPluginVisibleSsrVirtualId(importer) : false);
|
|
@@ -1785,19 +2374,19 @@ function loadCompiledSfcModule(state, realPath, isSsr, currentBase, loadOptions)
|
|
|
1785
2374
|
if (!compiled) return null;
|
|
1786
2375
|
for (const watchFile of new Set([realPath, ...compiled.dependencies ?? []])) loadOptions?.addWatchFile?.(watchFile);
|
|
1787
2376
|
const hasDelegated = hasDelegatedStyles(compiled);
|
|
1788
|
-
const
|
|
1789
|
-
if (compiled.css && !hasDelegated) compiled = {
|
|
1790
|
-
...compiled,
|
|
1791
|
-
css: resolveCssImports(compiled.css, realPath, state.cssAliasRules, state.server !== null, currentBase)
|
|
1792
|
-
};
|
|
1793
|
-
const generatedOutput = generateOutput(compiled, {
|
|
2377
|
+
const outputOptions = {
|
|
1794
2378
|
isProduction: state.isProduction,
|
|
1795
2379
|
isDev: state.server !== null && !isSsr,
|
|
1796
2380
|
ssr: isSsr,
|
|
1797
|
-
hmrUpdateType:
|
|
2381
|
+
hmrUpdateType: loadOptions?.ssr ? void 0 : state.pendingHmrUpdateTypes.get(realPath),
|
|
1798
2382
|
extractCss,
|
|
1799
2383
|
filePath: realPath
|
|
1800
|
-
}
|
|
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);
|
|
1801
2390
|
const normalizedOutput = rewriteImportMetaGlobBase(rewriteStaticAssetUrls(rewriteDynamicTemplateImports(isSsr ? normalizeVueServerRendererImport(generatedOutput) : generatedOutput, state.dynamicImportAliasRules), state.dynamicImportAliasRules), realPath, state.root);
|
|
1802
2391
|
if (!loadOptions?.ssr) state.pendingHmrUpdateTypes.delete(realPath);
|
|
1803
2392
|
return {
|
|
@@ -2138,7 +2727,22 @@ function normalizeVirtualStyleId(id) {
|
|
|
2138
2727
|
if (!withoutPrefix.includes("?vue")) return id;
|
|
2139
2728
|
return withoutPrefix.replace(/\.module\.\w+$/, "").replace(/\.\w+$/, "");
|
|
2140
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
|
+
}
|
|
2141
2744
|
function transformScopedPreprocessorCss(code, id) {
|
|
2745
|
+
if (!mayBeVueStyleQuery(id)) return null;
|
|
2142
2746
|
const request = classifyVitePluginRequest(normalizeVirtualStyleId(id));
|
|
2143
2747
|
if (!request.isVueStyleQuery || !request.styleScoped || !request.styleLang || request.styleLang === "css") return null;
|
|
2144
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.
|
|
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.
|
|
48
|
+
"@vizejs/native": "0.306.0",
|
|
49
49
|
"oxc-parser": "0.133.0",
|
|
50
50
|
"tinyglobby": "0.2.16",
|
|
51
|
-
"vize": "0.
|
|
51
|
+
"vize": "0.306.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "25.9.2",
|