@vizejs/vite-plugin 0.22.0 → 0.24.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.js +171 -45
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -8,6 +8,13 @@ import { transformWithOxc } from "vite";
8
8
  import { createRequire } from "node:module";
9
9
 
10
10
  //#region src/hmr.ts
11
+ function didHashChange(prevHash, nextHash) {
12
+ return prevHash !== nextHash;
13
+ }
14
+ function hasHmrChanges(prev, next) {
15
+ if (!prev) return true;
16
+ return didHashChange(prev.scriptHash, next.scriptHash) || didHashChange(prev.templateHash, next.templateHash) || didHashChange(prev.styleHash, next.styleHash);
17
+ }
11
18
  /**
12
19
  * Detect the type of HMR update needed based on content hash changes.
13
20
  *
@@ -17,10 +24,10 @@ import { createRequire } from "node:module";
17
24
  */
18
25
  function detectHmrUpdateType(prev, next) {
19
26
  if (!prev) return "full-reload";
20
- const scriptChanged = prev.scriptHash !== next.scriptHash;
27
+ const scriptChanged = didHashChange(prev.scriptHash, next.scriptHash);
21
28
  if (scriptChanged) return "full-reload";
22
- const templateChanged = prev.templateHash !== next.templateHash;
23
- const styleChanged = prev.styleHash !== next.styleHash;
29
+ const templateChanged = didHashChange(prev.templateHash, next.templateHash);
30
+ const styleChanged = didHashChange(prev.styleHash, next.styleHash);
24
31
  if (styleChanged && !templateChanged) return "style-only";
25
32
  if (templateChanged) return "template-only";
26
33
  return "full-reload";
@@ -158,6 +165,9 @@ function hasDelegatedStyles(compiled) {
158
165
  if (!compiled.styles) return false;
159
166
  return compiled.styles.some((s) => needsPreprocessor(s) || isCssModule(s));
160
167
  }
168
+ function supportsTemplateOnlyHmr(output) {
169
+ return /(?:^|\n)(?:_sfc_main|__sfc__)\.render\s*=\s*render\b/m.test(output);
170
+ }
161
171
  function generateScopeId(filename) {
162
172
  const hash = createHash("sha256").update(filename).digest("hex");
163
173
  return hash.slice(0, 8);
@@ -176,6 +186,7 @@ function generateOutput(compiled, options) {
176
186
  let output = compiled.code;
177
187
  const exportDefaultRegex = /^export default /m;
178
188
  const hasExportDefault = exportDefaultRegex.test(output);
189
+ const hasNamedRenderExport = /^export function render\b/m.test(output);
179
190
  const hasSfcMainDefined = /\bconst\s+_sfc_main\s*=/.test(output);
180
191
  if (hasExportDefault && !hasSfcMainDefined) {
181
192
  output = output.replace(exportDefaultRegex, "const _sfc_main = ");
@@ -183,6 +194,11 @@ function generateOutput(compiled, options) {
183
194
  output += "\nexport default _sfc_main;";
184
195
  } else if (hasExportDefault && hasSfcMainDefined) {
185
196
  if (compiled.hasScoped && compiled.scopeId) output = output.replace(/^export default _sfc_main/m, `_sfc_main.__scopeId = "data-v-${compiled.scopeId}";\nexport default _sfc_main`);
197
+ } else if (!hasExportDefault && !hasSfcMainDefined && hasNamedRenderExport) {
198
+ output += "\nconst _sfc_main = {};";
199
+ if (compiled.hasScoped && compiled.scopeId) output += `\n_sfc_main.__scopeId = "data-v-${compiled.scopeId}";`;
200
+ output += "\n_sfc_main.render = render;";
201
+ output += "\nexport default _sfc_main;";
186
202
  }
187
203
  const useDelegatedStyles = hasDelegatedStyles(compiled) && filePath;
188
204
  if (useDelegatedStyles) {
@@ -241,7 +257,10 @@ const __vize_css_id__ = ${cssId};
241
257
  })();
242
258
  ${output}`;
243
259
  }
244
- if (!isProduction && isDev && hasExportDefault) output += generateHmrCode(compiled.scopeId, hmrUpdateType ?? "full-reload");
260
+ if (!isProduction && isDev && hasExportDefault) {
261
+ const effectiveHmrUpdateType = hmrUpdateType === "template-only" && !supportsTemplateOnlyHmr(output) ? "full-reload" : hmrUpdateType ?? "full-reload";
262
+ output += generateHmrCode(compiled.scopeId, effectiveHmrUpdateType);
263
+ }
245
264
  return output;
246
265
  }
247
266
 
@@ -438,6 +457,25 @@ async function loadConfigFile(configPath, env) {
438
457
  */
439
458
  const vizeConfigStore = new Map();
440
459
 
460
+ //#endregion
461
+ //#region src/compile-options.ts
462
+ function buildCompileFileOptions(filePath, source, options) {
463
+ const scopeId = /<style[^>]*\bscoped\b/.test(source) ? `data-v-${generateScopeId(filePath)}` : void 0;
464
+ return {
465
+ filename: filePath,
466
+ sourceMap: options.sourceMap,
467
+ ssr: options.ssr,
468
+ vapor: options.vapor,
469
+ scopeId
470
+ };
471
+ }
472
+ function buildCompileBatchOptions(options) {
473
+ return {
474
+ ssr: options.ssr,
475
+ vapor: options.vapor
476
+ };
477
+ }
478
+
441
479
  //#endregion
442
480
  //#region src/compiler.ts
443
481
  const { compileSfc, compileSfcBatchWithResults } = native;
@@ -472,12 +510,7 @@ function compileFile(filePath, cache, options, source) {
472
510
  const content = source ?? fs.readFileSync(filePath, "utf-8");
473
511
  const scopeId = generateScopeId(filePath);
474
512
  const hasScoped = /<style[^>]*\bscoped\b/.test(content);
475
- const result = compileSfc(content, {
476
- filename: filePath,
477
- sourceMap: options.sourceMap,
478
- ssr: options.ssr,
479
- scopeId: hasScoped ? `data-v-${scopeId}` : void 0
480
- });
513
+ const result = compileSfc(content, buildCompileFileOptions(filePath, content, options));
481
514
  if (result.errors.length > 0) {
482
515
  const errorMsg = result.errors.join("\n");
483
516
  console.error(`[vize] Compilation error in ${filePath}:\n${errorMsg}`);
@@ -491,6 +524,9 @@ function compileFile(filePath, cache, options, source) {
491
524
  css: result.css,
492
525
  scopeId,
493
526
  hasScoped,
527
+ templateHash: result.templateHash,
528
+ styleHash: result.styleHash,
529
+ scriptHash: result.scriptHash,
494
530
  styles
495
531
  };
496
532
  cache.set(filePath, compiled);
@@ -505,7 +541,7 @@ function compileBatch(files, cache, options) {
505
541
  path: f.path,
506
542
  source: f.source
507
543
  }));
508
- const result = compileSfcBatchWithResults(inputs, { ssr: options.ssr });
544
+ const result = compileSfcBatchWithResults(inputs, buildCompileBatchOptions(options));
509
545
  const sourceMap = new Map();
510
546
  for (const f of files) sourceMap.set(f.path, f.source);
511
547
  for (const fileResult of result.results) {
@@ -533,6 +569,23 @@ function compileBatch(files, cache, options) {
533
569
 
534
570
  //#endregion
535
571
  //#region src/plugin/state.ts
572
+ function hasFileMetadataChanged(previous, next) {
573
+ return previous === void 0 || previous.mtimeMs !== next.mtimeMs || previous.size !== next.size;
574
+ }
575
+ function diffPrecompileFiles(files, currentMetadata, previousMetadata) {
576
+ const changedFiles = [];
577
+ const seenFiles = new Set(files);
578
+ for (const file of files) {
579
+ const metadata = currentMetadata.get(file);
580
+ if (!metadata || hasFileMetadataChanged(previousMetadata.get(file), metadata)) changedFiles.push(file);
581
+ }
582
+ const deletedFiles = [];
583
+ for (const file of previousMetadata.keys()) if (!seenFiles.has(file)) deletedFiles.push(file);
584
+ return {
585
+ changedFiles,
586
+ deletedFiles
587
+ };
588
+ }
536
589
  /**
537
590
  * Pre-compile all Vue files matching scan patterns.
538
591
  */
@@ -543,9 +596,32 @@ async function compileAll(state) {
543
596
  ignore: state.ignorePatterns,
544
597
  absolute: true
545
598
  });
546
- state.logger.info(`Pre-compiling ${files.length} Vue files...`);
547
- const fileContents = [];
599
+ const currentMetadata = new Map();
548
600
  for (const file of files) try {
601
+ const stat = fs.statSync(file);
602
+ currentMetadata.set(file, {
603
+ mtimeMs: stat.mtimeMs,
604
+ size: stat.size
605
+ });
606
+ } catch (e) {
607
+ state.logger.error(`Failed to stat ${file}:`, e);
608
+ }
609
+ const { changedFiles, deletedFiles } = diffPrecompileFiles(files, currentMetadata, state.precompileMetadata);
610
+ const cachedFileCount = files.length - changedFiles.length;
611
+ for (const file of deletedFiles) {
612
+ state.cache.delete(file);
613
+ state.collectedCss.delete(file);
614
+ state.precompileMetadata.delete(file);
615
+ state.pendingHmrUpdateTypes.delete(file);
616
+ }
617
+ state.logger.info(`Pre-compiling ${files.length} Vue files... (${changedFiles.length} changed, ${cachedFileCount} cached, ${deletedFiles.length} removed)`);
618
+ if (changedFiles.length === 0) {
619
+ const elapsed$1 = (performance.now() - startTime).toFixed(2);
620
+ state.logger.info(`Pre-compilation complete: cache reused (${elapsed$1}ms)`);
621
+ return;
622
+ }
623
+ const fileContents = [];
624
+ for (const file of changedFiles) try {
549
625
  const source = fs.readFileSync(file, "utf-8");
550
626
  fileContents.push({
551
627
  path: file,
@@ -554,22 +630,30 @@ async function compileAll(state) {
554
630
  } catch (e) {
555
631
  state.logger.error(`Failed to read ${file}:`, e);
556
632
  }
557
- const result = compileBatch(fileContents, state.cache, { ssr: state.mergedOptions.ssr ?? false });
558
- if (state.isProduction) {
559
- for (const fileResult of result.results) if (fileResult.css) {
633
+ const result = compileBatch(fileContents, state.cache, {
634
+ ssr: state.mergedOptions.ssr ?? false,
635
+ vapor: state.mergedOptions.vapor ?? false
636
+ });
637
+ for (const file of changedFiles) {
638
+ state.collectedCss.delete(file);
639
+ state.pendingHmrUpdateTypes.delete(file);
640
+ }
641
+ for (const fileResult of result.results) {
642
+ const metadata = currentMetadata.get(fileResult.path);
643
+ if (fileResult.errors.length > 0) {
644
+ state.cache.delete(fileResult.path);
645
+ state.collectedCss.delete(fileResult.path);
646
+ state.precompileMetadata.delete(fileResult.path);
647
+ continue;
648
+ }
649
+ if (metadata) state.precompileMetadata.set(fileResult.path, metadata);
650
+ if (state.isProduction && fileResult.css) {
560
651
  const cached = state.cache.get(fileResult.path);
561
- const hasDelegated = cached?.styles?.some((s) => s.lang !== null && [
562
- "scss",
563
- "sass",
564
- "less",
565
- "stylus",
566
- "styl"
567
- ].includes(s.lang) || s.module !== false);
568
- if (!hasDelegated) state.collectedCss.set(fileResult.path, resolveCssImports(fileResult.css, fileResult.path, state.cssAliasRules, false));
652
+ if (cached && !hasDelegatedStyles(cached)) state.collectedCss.set(fileResult.path, resolveCssImports(fileResult.css, fileResult.path, state.cssAliasRules, false));
569
653
  }
570
654
  }
571
655
  const elapsed = (performance.now() - startTime).toFixed(2);
572
- state.logger.info(`Pre-compilation complete: ${result.successCount} succeeded, ${result.failedCount} failed (${elapsed}ms, native batch: ${result.timeMs.toFixed(2)}ms)`);
656
+ state.logger.info(`Pre-compilation complete: ${result.successCount} recompiled, ${cachedFileCount} reused, ${result.failedCount} failed (${elapsed}ms, native batch: ${result.timeMs.toFixed(2)}ms)`);
573
657
  }
574
658
 
575
659
  //#endregion
@@ -741,6 +825,24 @@ async function resolveIdHook(ctx, state, id, importer) {
741
825
 
742
826
  //#endregion
743
827
  //#region src/plugin/load.ts
828
+ const SERVER_PLACEHOLDER_CODE = `import { createElementBlock, defineComponent } from "vue";
829
+ export default defineComponent({
830
+ name: "ServerPlaceholder",
831
+ render() {
832
+ return createElementBlock("div");
833
+ }
834
+ });
835
+ `;
836
+ function getBoundaryPlaceholderCode(realPath, ssr) {
837
+ if (ssr && realPath.endsWith(".client.vue")) return SERVER_PLACEHOLDER_CODE;
838
+ if (!ssr && realPath.endsWith(".server.vue")) return SERVER_PLACEHOLDER_CODE;
839
+ return null;
840
+ }
841
+ function getOxcDumpPath(root, realPath) {
842
+ const dumpDir = path.resolve(root || process.cwd(), "__agent_only", "oxc-dumps");
843
+ fs.mkdirSync(dumpDir, { recursive: true });
844
+ return path.join(dumpDir, `vize-oxc-error-${path.basename(realPath)}.ts`);
845
+ }
744
846
  function loadHook(state, id, loadOptions) {
745
847
  const currentBase = loadOptions?.ssr ? state.serverViteBase : state.clientViteBase;
746
848
  if (id === RESOLVED_CSS_MODULE) {
@@ -807,22 +909,26 @@ function loadHook(state, id, loadOptions) {
807
909
  state.logger.log(`load: skipping non-vue virtual module ${realPath}`);
808
910
  return null;
809
911
  }
912
+ const placeholderCode = getBoundaryPlaceholderCode(realPath, !!loadOptions?.ssr);
913
+ if (placeholderCode) {
914
+ state.logger.log(`load: using boundary placeholder for ${realPath}`);
915
+ return {
916
+ code: placeholderCode,
917
+ map: null
918
+ };
919
+ }
810
920
  let compiled = state.cache.get(realPath);
811
921
  if (!compiled && fs.existsSync(realPath)) {
812
922
  state.logger.log(`load: on-demand compiling ${realPath}`);
813
923
  compiled = compileFile(realPath, state.cache, {
814
924
  sourceMap: state.mergedOptions?.sourceMap ?? !(state.isProduction ?? false),
815
- ssr: state.mergedOptions?.ssr ?? false
925
+ ssr: state.mergedOptions?.ssr ?? false,
926
+ vapor: state.mergedOptions?.vapor ?? false
816
927
  });
817
928
  }
818
929
  if (compiled) {
819
- const hasDelegated = compiled.styles?.some((s) => s.lang !== null && [
820
- "scss",
821
- "sass",
822
- "less",
823
- "stylus",
824
- "styl"
825
- ].includes(s.lang) || s.module !== false);
930
+ const hasDelegated = hasDelegatedStyles(compiled);
931
+ const pendingHmrUpdateType = loadOptions?.ssr ? void 0 : state.pendingHmrUpdateTypes.get(realPath);
826
932
  if (compiled.css && !hasDelegated) compiled = {
827
933
  ...compiled,
828
934
  css: resolveCssImports(compiled.css, realPath, state.cssAliasRules, state.server !== null, currentBase)
@@ -830,9 +936,11 @@ function loadHook(state, id, loadOptions) {
830
936
  const output = rewriteStaticAssetUrls(rewriteDynamicTemplateImports(generateOutput(compiled, {
831
937
  isProduction: state.isProduction,
832
938
  isDev: state.server !== null,
939
+ hmrUpdateType: pendingHmrUpdateType,
833
940
  extractCss: state.extractCss,
834
941
  filePath: realPath
835
942
  }), state.dynamicImportAliasRules), state.dynamicImportAliasRules);
943
+ if (!loadOptions?.ssr) state.pendingHmrUpdateTypes.delete(realPath);
836
944
  return {
837
945
  code: output,
838
946
  map: null
@@ -868,7 +976,7 @@ async function transformHook(state, code, id, options) {
868
976
  };
869
977
  } catch (e) {
870
978
  state.logger.error(`transformWithOxc failed for ${realPath}:`, e);
871
- const dumpPath = `/tmp/vize-oxc-error-${path.basename(realPath)}.ts`;
979
+ const dumpPath = getOxcDumpPath(state.root, realPath);
872
980
  fs.writeFileSync(dumpPath, code, "utf-8");
873
981
  state.logger.error(`Dumped failing code to ${dumpPath}`);
874
982
  return {
@@ -889,20 +997,29 @@ async function handleHotUpdateHook(state, ctx) {
889
997
  const prevCompiled = state.cache.get(file);
890
998
  compileFile(file, state.cache, {
891
999
  sourceMap: state.mergedOptions?.sourceMap ?? !state.isProduction,
892
- ssr: state.mergedOptions?.ssr ?? false
1000
+ ssr: state.mergedOptions?.ssr ?? false,
1001
+ vapor: state.mergedOptions?.vapor ?? false
893
1002
  }, source);
894
1003
  const newCompiled = state.cache.get(file);
1004
+ try {
1005
+ const stat = fs.statSync(file);
1006
+ state.precompileMetadata.set(file, {
1007
+ mtimeMs: stat.mtimeMs,
1008
+ size: stat.size
1009
+ });
1010
+ } catch {
1011
+ state.precompileMetadata.delete(file);
1012
+ }
1013
+ if (!hasHmrChanges(prevCompiled, newCompiled)) {
1014
+ state.pendingHmrUpdateTypes.delete(file);
1015
+ state.logger.log(`Re-compiled: ${path.relative(state.root, file)} (no-op)`);
1016
+ return [];
1017
+ }
895
1018
  const updateType = detectHmrUpdateType(prevCompiled, newCompiled);
896
1019
  state.logger.log(`Re-compiled: ${path.relative(state.root, file)} (${updateType})`);
897
1020
  const virtualId = toVirtualId(file);
898
1021
  const modules = server.moduleGraph.getModulesByFile(virtualId) ?? server.moduleGraph.getModulesByFile(file);
899
- const hasDelegated = newCompiled.styles?.some((s) => s.lang !== null && [
900
- "scss",
901
- "sass",
902
- "less",
903
- "stylus",
904
- "styl"
905
- ].includes(s.lang) || s.module !== false);
1022
+ const hasDelegated = hasDelegatedStyles(newCompiled);
906
1023
  if (hasDelegated && updateType === "style-only") {
907
1024
  const affectedModules = new Set();
908
1025
  for (const block of newCompiled.styles ?? []) {
@@ -917,10 +1034,12 @@ async function handleHotUpdateHook(state, ctx) {
917
1034
  const styleMods = server.moduleGraph.getModulesByFile(styleId);
918
1035
  if (styleMods) for (const mod of styleMods) affectedModules.add(mod);
919
1036
  }
920
- if (modules) for (const mod of modules) affectedModules.add(mod);
921
1037
  if (affectedModules.size > 0) return [...affectedModules];
1038
+ if (modules) return [...modules];
1039
+ return [];
922
1040
  }
923
1041
  if (updateType === "style-only" && newCompiled.css && !hasDelegated) {
1042
+ state.pendingHmrUpdateTypes.delete(file);
924
1043
  server.ws.send({
925
1044
  type: "custom",
926
1045
  event: "vize:update",
@@ -932,7 +1051,11 @@ async function handleHotUpdateHook(state, ctx) {
932
1051
  });
933
1052
  return [];
934
1053
  }
935
- if (modules) return [...modules];
1054
+ if (modules) {
1055
+ state.pendingHmrUpdateTypes.set(file, updateType);
1056
+ return [...modules];
1057
+ }
1058
+ state.pendingHmrUpdateTypes.delete(file);
936
1059
  } catch (e) {
937
1060
  state.logger.error(`Re-compilation failed for ${file}:`, e);
938
1061
  }
@@ -988,7 +1111,8 @@ function createPostTransformPlugin(state) {
988
1111
  try {
989
1112
  const compiled = compileFile(id, state.cache, {
990
1113
  sourceMap: state.mergedOptions?.sourceMap ?? !(state.isProduction ?? false),
991
- ssr: state.mergedOptions?.ssr ?? false
1114
+ ssr: state.mergedOptions?.ssr ?? false,
1115
+ vapor: state.mergedOptions?.vapor ?? false
992
1116
  }, code);
993
1117
  const output = generateOutput(compiled, {
994
1118
  isProduction: state.isProduction,
@@ -1019,6 +1143,8 @@ function vize(options = {}) {
1019
1143
  const state = {
1020
1144
  cache: new Map(),
1021
1145
  collectedCss: new Map(),
1146
+ precompileMetadata: new Map(),
1147
+ pendingHmrUpdateTypes: new Map(),
1022
1148
  isProduction: false,
1023
1149
  root: "",
1024
1150
  clientViteBase: "/",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizejs/vite-plugin",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "High-performance native Vite plugin for Vue SFC compilation powered by Vize",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "tinyglobby": "^0.2.0",
46
- "@vizejs/native": "0.22.0"
46
+ "@vizejs/native": "0.24.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsdown",