@vizejs/vite-plugin 0.21.0 → 0.23.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 +140 -34
  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
 
@@ -491,6 +510,9 @@ function compileFile(filePath, cache, options, source) {
491
510
  css: result.css,
492
511
  scopeId,
493
512
  hasScoped,
513
+ templateHash: result.templateHash,
514
+ styleHash: result.styleHash,
515
+ scriptHash: result.scriptHash,
494
516
  styles
495
517
  };
496
518
  cache.set(filePath, compiled);
@@ -533,6 +555,23 @@ function compileBatch(files, cache, options) {
533
555
 
534
556
  //#endregion
535
557
  //#region src/plugin/state.ts
558
+ function hasFileMetadataChanged(previous, next) {
559
+ return previous === void 0 || previous.mtimeMs !== next.mtimeMs || previous.size !== next.size;
560
+ }
561
+ function diffPrecompileFiles(files, currentMetadata, previousMetadata) {
562
+ const changedFiles = [];
563
+ const seenFiles = new Set(files);
564
+ for (const file of files) {
565
+ const metadata = currentMetadata.get(file);
566
+ if (!metadata || hasFileMetadataChanged(previousMetadata.get(file), metadata)) changedFiles.push(file);
567
+ }
568
+ const deletedFiles = [];
569
+ for (const file of previousMetadata.keys()) if (!seenFiles.has(file)) deletedFiles.push(file);
570
+ return {
571
+ changedFiles,
572
+ deletedFiles
573
+ };
574
+ }
536
575
  /**
537
576
  * Pre-compile all Vue files matching scan patterns.
538
577
  */
@@ -543,9 +582,32 @@ async function compileAll(state) {
543
582
  ignore: state.ignorePatterns,
544
583
  absolute: true
545
584
  });
546
- state.logger.info(`Pre-compiling ${files.length} Vue files...`);
547
- const fileContents = [];
585
+ const currentMetadata = new Map();
548
586
  for (const file of files) try {
587
+ const stat = fs.statSync(file);
588
+ currentMetadata.set(file, {
589
+ mtimeMs: stat.mtimeMs,
590
+ size: stat.size
591
+ });
592
+ } catch (e) {
593
+ state.logger.error(`Failed to stat ${file}:`, e);
594
+ }
595
+ const { changedFiles, deletedFiles } = diffPrecompileFiles(files, currentMetadata, state.precompileMetadata);
596
+ const cachedFileCount = files.length - changedFiles.length;
597
+ for (const file of deletedFiles) {
598
+ state.cache.delete(file);
599
+ state.collectedCss.delete(file);
600
+ state.precompileMetadata.delete(file);
601
+ state.pendingHmrUpdateTypes.delete(file);
602
+ }
603
+ state.logger.info(`Pre-compiling ${files.length} Vue files... (${changedFiles.length} changed, ${cachedFileCount} cached, ${deletedFiles.length} removed)`);
604
+ if (changedFiles.length === 0) {
605
+ const elapsed$1 = (performance.now() - startTime).toFixed(2);
606
+ state.logger.info(`Pre-compilation complete: cache reused (${elapsed$1}ms)`);
607
+ return;
608
+ }
609
+ const fileContents = [];
610
+ for (const file of changedFiles) try {
549
611
  const source = fs.readFileSync(file, "utf-8");
550
612
  fileContents.push({
551
613
  path: file,
@@ -555,21 +617,26 @@ async function compileAll(state) {
555
617
  state.logger.error(`Failed to read ${file}:`, e);
556
618
  }
557
619
  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) {
620
+ for (const file of changedFiles) {
621
+ state.collectedCss.delete(file);
622
+ state.pendingHmrUpdateTypes.delete(file);
623
+ }
624
+ for (const fileResult of result.results) {
625
+ const metadata = currentMetadata.get(fileResult.path);
626
+ if (fileResult.errors.length > 0) {
627
+ state.cache.delete(fileResult.path);
628
+ state.collectedCss.delete(fileResult.path);
629
+ state.precompileMetadata.delete(fileResult.path);
630
+ continue;
631
+ }
632
+ if (metadata) state.precompileMetadata.set(fileResult.path, metadata);
633
+ if (state.isProduction && fileResult.css) {
560
634
  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));
635
+ if (cached && !hasDelegatedStyles(cached)) state.collectedCss.set(fileResult.path, resolveCssImports(fileResult.css, fileResult.path, state.cssAliasRules, false));
569
636
  }
570
637
  }
571
638
  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)`);
639
+ state.logger.info(`Pre-compilation complete: ${result.successCount} recompiled, ${cachedFileCount} reused, ${result.failedCount} failed (${elapsed}ms, native batch: ${result.timeMs.toFixed(2)}ms)`);
573
640
  }
574
641
 
575
642
  //#endregion
@@ -741,6 +808,24 @@ async function resolveIdHook(ctx, state, id, importer) {
741
808
 
742
809
  //#endregion
743
810
  //#region src/plugin/load.ts
811
+ const SERVER_PLACEHOLDER_CODE = `import { createElementBlock, defineComponent } from "vue";
812
+ export default defineComponent({
813
+ name: "ServerPlaceholder",
814
+ render() {
815
+ return createElementBlock("div");
816
+ }
817
+ });
818
+ `;
819
+ function getBoundaryPlaceholderCode(realPath, ssr) {
820
+ if (ssr && realPath.endsWith(".client.vue")) return SERVER_PLACEHOLDER_CODE;
821
+ if (!ssr && realPath.endsWith(".server.vue")) return SERVER_PLACEHOLDER_CODE;
822
+ return null;
823
+ }
824
+ function getOxcDumpPath(root, realPath) {
825
+ const dumpDir = path.resolve(root || process.cwd(), "__agent_only", "oxc-dumps");
826
+ fs.mkdirSync(dumpDir, { recursive: true });
827
+ return path.join(dumpDir, `vize-oxc-error-${path.basename(realPath)}.ts`);
828
+ }
744
829
  function loadHook(state, id, loadOptions) {
745
830
  const currentBase = loadOptions?.ssr ? state.serverViteBase : state.clientViteBase;
746
831
  if (id === RESOLVED_CSS_MODULE) {
@@ -807,6 +892,14 @@ function loadHook(state, id, loadOptions) {
807
892
  state.logger.log(`load: skipping non-vue virtual module ${realPath}`);
808
893
  return null;
809
894
  }
895
+ const placeholderCode = getBoundaryPlaceholderCode(realPath, !!loadOptions?.ssr);
896
+ if (placeholderCode) {
897
+ state.logger.log(`load: using boundary placeholder for ${realPath}`);
898
+ return {
899
+ code: placeholderCode,
900
+ map: null
901
+ };
902
+ }
810
903
  let compiled = state.cache.get(realPath);
811
904
  if (!compiled && fs.existsSync(realPath)) {
812
905
  state.logger.log(`load: on-demand compiling ${realPath}`);
@@ -816,13 +909,8 @@ function loadHook(state, id, loadOptions) {
816
909
  });
817
910
  }
818
911
  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);
912
+ const hasDelegated = hasDelegatedStyles(compiled);
913
+ const pendingHmrUpdateType = loadOptions?.ssr ? void 0 : state.pendingHmrUpdateTypes.get(realPath);
826
914
  if (compiled.css && !hasDelegated) compiled = {
827
915
  ...compiled,
828
916
  css: resolveCssImports(compiled.css, realPath, state.cssAliasRules, state.server !== null, currentBase)
@@ -830,9 +918,11 @@ function loadHook(state, id, loadOptions) {
830
918
  const output = rewriteStaticAssetUrls(rewriteDynamicTemplateImports(generateOutput(compiled, {
831
919
  isProduction: state.isProduction,
832
920
  isDev: state.server !== null,
921
+ hmrUpdateType: pendingHmrUpdateType,
833
922
  extractCss: state.extractCss,
834
923
  filePath: realPath
835
924
  }), state.dynamicImportAliasRules), state.dynamicImportAliasRules);
925
+ if (!loadOptions?.ssr) state.pendingHmrUpdateTypes.delete(realPath);
836
926
  return {
837
927
  code: output,
838
928
  map: null
@@ -868,7 +958,7 @@ async function transformHook(state, code, id, options) {
868
958
  };
869
959
  } catch (e) {
870
960
  state.logger.error(`transformWithOxc failed for ${realPath}:`, e);
871
- const dumpPath = `/tmp/vize-oxc-error-${path.basename(realPath)}.ts`;
961
+ const dumpPath = getOxcDumpPath(state.root, realPath);
872
962
  fs.writeFileSync(dumpPath, code, "utf-8");
873
963
  state.logger.error(`Dumped failing code to ${dumpPath}`);
874
964
  return {
@@ -892,17 +982,25 @@ async function handleHotUpdateHook(state, ctx) {
892
982
  ssr: state.mergedOptions?.ssr ?? false
893
983
  }, source);
894
984
  const newCompiled = state.cache.get(file);
985
+ try {
986
+ const stat = fs.statSync(file);
987
+ state.precompileMetadata.set(file, {
988
+ mtimeMs: stat.mtimeMs,
989
+ size: stat.size
990
+ });
991
+ } catch {
992
+ state.precompileMetadata.delete(file);
993
+ }
994
+ if (!hasHmrChanges(prevCompiled, newCompiled)) {
995
+ state.pendingHmrUpdateTypes.delete(file);
996
+ state.logger.log(`Re-compiled: ${path.relative(state.root, file)} (no-op)`);
997
+ return [];
998
+ }
895
999
  const updateType = detectHmrUpdateType(prevCompiled, newCompiled);
896
1000
  state.logger.log(`Re-compiled: ${path.relative(state.root, file)} (${updateType})`);
897
1001
  const virtualId = toVirtualId(file);
898
1002
  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);
1003
+ const hasDelegated = hasDelegatedStyles(newCompiled);
906
1004
  if (hasDelegated && updateType === "style-only") {
907
1005
  const affectedModules = new Set();
908
1006
  for (const block of newCompiled.styles ?? []) {
@@ -917,10 +1015,12 @@ async function handleHotUpdateHook(state, ctx) {
917
1015
  const styleMods = server.moduleGraph.getModulesByFile(styleId);
918
1016
  if (styleMods) for (const mod of styleMods) affectedModules.add(mod);
919
1017
  }
920
- if (modules) for (const mod of modules) affectedModules.add(mod);
921
1018
  if (affectedModules.size > 0) return [...affectedModules];
1019
+ if (modules) return [...modules];
1020
+ return [];
922
1021
  }
923
1022
  if (updateType === "style-only" && newCompiled.css && !hasDelegated) {
1023
+ state.pendingHmrUpdateTypes.delete(file);
924
1024
  server.ws.send({
925
1025
  type: "custom",
926
1026
  event: "vize:update",
@@ -932,7 +1032,11 @@ async function handleHotUpdateHook(state, ctx) {
932
1032
  });
933
1033
  return [];
934
1034
  }
935
- if (modules) return [...modules];
1035
+ if (modules) {
1036
+ state.pendingHmrUpdateTypes.set(file, updateType);
1037
+ return [...modules];
1038
+ }
1039
+ state.pendingHmrUpdateTypes.delete(file);
936
1040
  } catch (e) {
937
1041
  state.logger.error(`Re-compilation failed for ${file}:`, e);
938
1042
  }
@@ -1019,6 +1123,8 @@ function vize(options = {}) {
1019
1123
  const state = {
1020
1124
  cache: new Map(),
1021
1125
  collectedCss: new Map(),
1126
+ precompileMetadata: new Map(),
1127
+ pendingHmrUpdateTypes: new Map(),
1022
1128
  isProduction: false,
1023
1129
  root: "",
1024
1130
  clientViteBase: "/",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizejs/vite-plugin",
3
- "version": "0.21.0",
3
+ "version": "0.23.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.21.0"
46
+ "@vizejs/native": "0.23.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsdown",