vite-plugin-enter-dev 0.0.10 → 0.0.12

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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # vite-plugin-enter-dev
2
+
3
+ Version 0.0.12 preserves the host React compiler and its Babel options. Use
4
+ `enterDevPlugin({ react: false })` alongside `@vitejs/plugin-react`; Enter adds
5
+ source locations through `api.reactBabel`. Templates without a host compiler
6
+ can keep `enterDevPlugin()`. Duplicate React compilers fail at configuration
7
+ resolution with an actionable error instead of serving broken entry modules.
8
+
9
+ `vite-plugin-enter-dev/configure` exports `configureVite(source)` and
10
+ `configureViteFile(path, check?)`. The Gateway uses these to repair existing
11
+ and imported Vite configurations. Migration preserves host options, handles
12
+ static configurations and aliased imports, and is idempotent. Unsupported
13
+ dynamic configurations fail without changing the file; check mode never writes.
14
+ File migration requires the caller to exclude concurrent file writers for the
15
+ whole operation; the Gateway holds and drains its workspace lock before calling.
16
+
17
+ Version 0.0.11's reload-generation recovery remains enabled: served documents
18
+ compare the server instance and reload revision on initial HMR connection and
19
+ reconnection, reloading once when stale.
20
+
21
+ Run `pnpm test`, `pnpm lint`, and `pnpm typecheck` in this package. Tests compile
22
+ a real TSX entry through Vite and verify source annotations, migration, and
23
+ host Babel options.
24
+
25
+ Publish 0.0.12 before deploying a Gateway requiring it. Existing sandboxes must
26
+ upgrade the package, migrate the configuration, and restart Vite; the Gateway
27
+ checks this even when the old server's readiness endpoint still responds.
@@ -0,0 +1,4 @@
1
+ declare function configureVite(source: string): string;
2
+ declare function configureViteFile(path: string, check?: boolean): boolean;
3
+
4
+ export { configureVite, configureViteFile };
@@ -0,0 +1,123 @@
1
+ import { parseSync, types, traverse, transformFromAstSync } from '@babel/core';
2
+ import { readFileSync, writeFileSync, statSync, renameSync, rmSync } from 'fs';
3
+ import { randomUUID } from 'crypto';
4
+
5
+ // src/configure.ts
6
+ function configureVite(source) {
7
+ const ast = parseSync(source, { babelrc: false, configFile: false, parserOpts: { plugins: ["typescript"] } });
8
+ if (!ast) throw new Error("Cannot parse Vite config");
9
+ const imports = ast.program.body.filter((node) => types.isImportDeclaration(node));
10
+ if (imports.some((node) => node.source.value === "@vitejs/plugin-react" && node.specifiers.some((item) => !types.isImportDefaultSpecifier(item)))) {
11
+ throw new Error("Unsupported React import: expected a default import");
12
+ }
13
+ const defineNames = imports.filter((node) => node.source.value === "vite").flatMap((node) => node.specifiers.filter((item) => types.isImportSpecifier(item) && types.isIdentifier(item.imported, { name: "defineConfig" })).map((item) => item.local.name));
14
+ const reactNames = imports.filter((node) => node.source.value === "@vitejs/plugin-react").flatMap((node) => node.specifiers.filter((item) => types.isImportDefaultSpecifier(item)).map((item) => item.local.name));
15
+ const entry = ast.program.body.find((node) => types.isExportDefaultDeclaration(node));
16
+ let config = entry?.declaration;
17
+ if (types.isIdentifier(config)) {
18
+ const name = config.name;
19
+ traverse(ast, {
20
+ Program(path) {
21
+ const binding2 = path.scope.getBinding(name);
22
+ if (!binding2?.constant || binding2.referencePaths.length !== 1 || !binding2.referencePaths[0]?.parentPath?.isExportDefaultDeclaration()) {
23
+ throw new Error("Unsupported Vite config: exported binding has other references");
24
+ }
25
+ }
26
+ });
27
+ const binding = ast.program.body.filter((node) => types.isVariableDeclaration(node, { kind: "const" })).flatMap((node) => node.declarations).find((node) => types.isIdentifier(node.id, { name }));
28
+ config = binding?.init ?? void 0;
29
+ }
30
+ if (types.isCallExpression(config)) {
31
+ if (!types.isIdentifier(config.callee) || !defineNames.includes(config.callee.name) || config.arguments.length !== 1) throw new Error("Unsupported Vite config wrapper");
32
+ config = config.arguments[0];
33
+ }
34
+ if (types.isArrowFunctionExpression(config) || types.isFunctionExpression(config)) {
35
+ if (types.isBlockStatement(config.body)) {
36
+ if (config.body.body.length !== 1 || !types.isReturnStatement(config.body.body[0])) {
37
+ throw new Error("Unsupported Vite config: expected a single unconditional return");
38
+ }
39
+ config = config.body.body[0].argument ?? void 0;
40
+ } else {
41
+ config = config.body;
42
+ }
43
+ }
44
+ if (!types.isObjectExpression(config)) throw new Error("Unsupported Vite config: expected a static exported configuration object");
45
+ if (config.properties.some((node) => types.isSpreadElement(node) || node.computed || types.isObjectMethod(node) && (types.isIdentifier(node.key, { name: "plugins" }) || types.isStringLiteral(node.key, { value: "plugins" })))) {
46
+ throw new Error("Unsupported Vite config: dynamic configuration may override plugins");
47
+ }
48
+ const pluginProperties = config.properties.filter((node) => types.isObjectProperty(node) && !node.computed && (types.isIdentifier(node.key, { name: "plugins" }) || types.isStringLiteral(node.key, { value: "plugins" })));
49
+ if (pluginProperties.length > 1) throw new Error("Unsupported Vite config: duplicate plugins properties");
50
+ let changed = false;
51
+ let plugins = pluginProperties[0];
52
+ if (!plugins) {
53
+ plugins = types.objectProperty(types.identifier("plugins"), types.arrayExpression([]));
54
+ config.properties.push(plugins);
55
+ changed = true;
56
+ }
57
+ if (!types.isObjectProperty(plugins) || !types.isArrayExpression(plugins.value)) {
58
+ throw new Error("Unsupported Vite config: expected a literal plugins array");
59
+ }
60
+ const calls = plugins.value.elements.map((node) => types.isSpreadElement(node) ? node.argument : node);
61
+ if (calls.some((node) => !types.isCallExpression(node) && node !== null && !types.isBooleanLiteral(node))) {
62
+ throw new Error("Unsupported Vite config: dynamic plugin list");
63
+ }
64
+ const hostReact = calls.some((node) => types.isCallExpression(node) && types.isIdentifier(node.callee) && reactNames.includes(node.callee.name));
65
+ for (const name of ["enterProdPlugin", "enterDevPlugin"]) {
66
+ const specifiers = imports.filter((node) => node.source.value === "vite-plugin-enter-dev").flatMap((node) => node.specifiers).filter((node) => types.isImportSpecifier(node) && types.isIdentifier(node.imported, { name }));
67
+ const aliases = specifiers.map((node) => node.local.name);
68
+ let local = aliases[0];
69
+ if (!local) {
70
+ local = name;
71
+ while (new RegExp(`\\b${local}\\b`).test(source)) local = `_${local}`;
72
+ ast.program.body.unshift(types.importDeclaration([types.importSpecifier(types.identifier(local), types.identifier(name))], types.stringLiteral("vite-plugin-enter-dev")));
73
+ changed = true;
74
+ }
75
+ const matching = calls.filter((node) => types.isCallExpression(node) && types.isIdentifier(node.callee) && (aliases.includes(node.callee.name) || node.callee.name === local));
76
+ if (matching.length > 1) throw new Error(`Duplicate ${name} calls in Vite config`);
77
+ let call = matching[0];
78
+ if (!call) {
79
+ call = types.callExpression(types.identifier(local), []);
80
+ plugins.value.elements.push(types.spreadElement(call));
81
+ changed = true;
82
+ }
83
+ if (name !== "enterDevPlugin" || !hostReact || !types.isCallExpression(call)) continue;
84
+ if (call.arguments.length === 0) call.arguments.push(types.objectExpression([]));
85
+ const options = call.arguments[0];
86
+ if (!types.isObjectExpression(options) || options.properties.some((node) => !types.isObjectProperty(node) || node.computed)) {
87
+ throw new Error("Unsupported enterDevPlugin options: expected a static object");
88
+ }
89
+ const matchingOptions = options.properties.filter((node) => types.isObjectProperty(node) && !node.computed && (types.isIdentifier(node.key, { name: "react" }) || types.isStringLiteral(node.key, { value: "react" })));
90
+ if (matchingOptions.length > 1) throw new Error("Unsupported duplicate react options");
91
+ const option = matchingOptions[0];
92
+ if (types.isObjectProperty(option)) {
93
+ if (!types.isBooleanLiteral(option.value)) throw new Error("Unsupported dynamic enterDevPlugin react option");
94
+ if (!option.value.value) continue;
95
+ option.value = types.booleanLiteral(false);
96
+ } else {
97
+ options.properties.push(types.objectProperty(types.identifier("react"), types.booleanLiteral(false)));
98
+ }
99
+ changed = true;
100
+ }
101
+ if (!changed) return source;
102
+ const result = transformFromAstSync(ast, source, { babelrc: false, configFile: false });
103
+ if (!result?.code) throw new Error("Cannot generate Vite config");
104
+ return `${result.code}
105
+ `;
106
+ }
107
+ function configureViteFile(path, check = false) {
108
+ const source = readFileSync(path, "utf8");
109
+ const output = configureVite(source);
110
+ if (source === output) return false;
111
+ if (check) return true;
112
+ const temporary = `${path}.${randomUUID()}.tmp`;
113
+ try {
114
+ writeFileSync(temporary, output, { flag: "wx", mode: statSync(path).mode });
115
+ if (readFileSync(path, "utf8") !== source) throw new Error("Vite config changed during migration");
116
+ renameSync(temporary, path);
117
+ } finally {
118
+ rmSync(temporary, { force: true });
119
+ }
120
+ return true;
121
+ }
122
+
123
+ export { configureVite, configureViteFile };
package/dist/index.d.ts CHANGED
@@ -11,9 +11,8 @@ interface DevOptions {
11
11
  * 宿主框架自己已经注册了 React 转换时必须传 false:vinext 会在
12
12
  * configResolved 里检测重复注册并直接抛错
13
13
  * ("[vinext] Duplicate @vitejs/plugin-react detected"),
14
- * 整个 dev server 和 build 都起不来。关掉后本插件只保留
15
- * allowedHosts / 端口 / base / 消息监听,代价是失去可视化编辑
16
- * 需要的源码位置注入。
14
+ * 整个 dev server 和 build 都起不来。源码位置通过宿主 React
15
+ * 插件的 Babel 扩展接口注入,无需重复注册编译器。
17
16
  */
18
17
  react?: boolean;
19
18
  }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import react from '@vitejs/plugin-react';
2
+ import { randomUUID } from 'crypto';
2
3
 
3
4
  // src/index.ts
4
5
 
@@ -164,7 +165,7 @@ function injectSourcePlugin({ types: t }) {
164
165
  name: "inject-source-location-enhanced",
165
166
  visitor: {
166
167
  Program: {
167
- enter(path, state) {
168
+ enter(_path, state) {
168
169
  state.threeImportedElements = /* @__PURE__ */ new Set();
169
170
  state.threeNamespaces = /* @__PURE__ */ new Set();
170
171
  }
@@ -229,7 +230,7 @@ function injectSourcePlugin({ types: t }) {
229
230
  );
230
231
  const sourceStackAttr = t.jsxAttribute(
231
232
  t.jsxIdentifier("data-source-stack"),
232
- t.stringLiteral(JSON.stringify(elementInfo))
233
+ t.jsxExpressionContainer(t.stringLiteral(JSON.stringify(elementInfo)))
233
234
  );
234
235
  const elementTypeAttr = t.jsxAttribute(
235
236
  t.jsxIdentifier("data-element-type"),
@@ -273,7 +274,7 @@ function getElementType(nameNode) {
273
274
  if (nameNode.type === "JSXIdentifier") {
274
275
  return nameNode.name;
275
276
  } else if (nameNode.type === "JSXMemberExpression") {
276
- return `${nameNode.object.name}.${nameNode.property.name}`;
277
+ return `${getElementType(nameNode.object)}.${nameNode.property.name}`;
277
278
  }
278
279
  return "unknown";
279
280
  }
@@ -283,35 +284,80 @@ function getParentElementType(parentNode) {
283
284
  }
284
285
  return null;
285
286
  }
287
+ function reloadGenerationPlugin() {
288
+ const instance = randomUUID();
289
+ let revision = 0;
290
+ let base = "/";
291
+ const generation = () => `${instance}:${revision}`;
292
+ return {
293
+ name: "enter-dev-reload-generation",
294
+ apply: "serve",
295
+ configResolved(config) {
296
+ base = config.base;
297
+ },
298
+ configureServer(server) {
299
+ const originalSend = server.ws.send.bind(server.ws);
300
+ const send = (payload, data) => {
301
+ if (typeof payload === "string") {
302
+ originalSend(payload, data);
303
+ return;
304
+ }
305
+ if (payload.type === "full-reload") revision += 1;
306
+ originalSend(payload);
307
+ };
308
+ server.ws.send = send;
309
+ server.ws.on("enter:check-generation", (data, client) => {
310
+ if (data?.generation !== generation()) {
311
+ client.send("enter:reload-required", { generation: generation() });
312
+ }
313
+ });
314
+ server.httpServer?.once("close", () => {
315
+ if (server.ws.send === send) server.ws.send = originalSend;
316
+ });
317
+ },
318
+ transformIndexHtml() {
319
+ return [
320
+ {
321
+ tag: "script",
322
+ attrs: { type: "module" },
323
+ injectTo: "head-prepend",
324
+ children: `import { createHotContext } from ${JSON.stringify(`${base}@vite/client`)};
325
+ const generation = ${JSON.stringify(generation())};
326
+ const hot = createHotContext('/@enter/reload-generation');
327
+ let reloading = false;
328
+ hot.on('enter:reload-required', () => {
329
+ if (reloading) return;
330
+ reloading = true;
331
+ window.location.reload();
332
+ });
333
+ const check = () => hot.send('enter:check-generation', { generation });
334
+ hot.on('vite:ws:connect', check);
335
+ check();`
336
+ }
337
+ ];
338
+ }
339
+ };
340
+ }
286
341
 
287
342
  // src/index.ts
288
343
  function enterDevPlugin(options = {}) {
289
- const {
290
- injectMessageListener = true,
291
- react: enableReact = true
292
- } = options;
293
- const reactPlugin = enableReact ? react({
294
- // 确保在开发模式下启用JSX源码映射
295
- jsxImportSource: void 0,
296
- jsxRuntime: "automatic",
297
- // 启用构建时注入源码位置信息
298
- babel: {
299
- plugins: [
300
- // 我们的自定义插件:在构建时注入源码位置
301
- [injectSourcePlugin],
302
- // 标准的React JSX转换
303
- [
304
- "@babel/plugin-transform-react-jsx",
305
- {
306
- runtime: "automatic",
307
- importSource: "react",
308
- development: true
309
- // 保持开发模式以获得更好的调试体验
310
- }
311
- ]
312
- ]
344
+ const { injectMessageListener = true, react: enableReact = true } = options;
345
+ const reactPlugin = enableReact ? react() : [];
346
+ const sourcePlugin = {
347
+ name: "enter-dev-source-location",
348
+ api: {
349
+ reactBabel(config) {
350
+ if (!config.plugins.some((plugin) => (Array.isArray(plugin) ? plugin[0] : plugin) === injectSourcePlugin)) {
351
+ config.plugins.push(injectSourcePlugin);
352
+ }
353
+ }
354
+ },
355
+ configResolved(config) {
356
+ if (config.plugins.filter((plugin) => plugin.name === "vite:react-babel").length > 1) {
357
+ throw new Error("Enter preview: duplicate React plugins. Use enterDevPlugin({ react: false }) when the project provides React.");
358
+ }
313
359
  }
314
- }) : [];
360
+ };
315
361
  const htmlInjectPlugin = {
316
362
  name: "enter-dev-message-listener",
317
363
  enforce: "pre",
@@ -330,7 +376,7 @@ function enterDevPlugin(options = {}) {
330
376
  if (target && (target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
331
377
  const isStylesheet =
332
378
  target.tagName === 'LINK' &&
333
- String(target.rel || '').toLowerCase().split(/s+/).includes('stylesheet');
379
+ String(target.rel || '').toLowerCase().split(/\\s+/).includes('stylesheet');
334
380
  if (target.tagName === 'SCRIPT' || isStylesheet) {
335
381
  window.__earlyErrors__.push({
336
382
  type: 'resource',
@@ -502,7 +548,15 @@ function enterDevPlugin(options = {}) {
502
548
  }
503
549
  }
504
550
  };
505
- return [...reactPlugin, htmlInjectPlugin, sandboxServerPlugin, routerBasePlugin, publicAssetReloadPlugin];
551
+ return [
552
+ ...reactPlugin,
553
+ sourcePlugin,
554
+ htmlInjectPlugin,
555
+ sandboxServerPlugin,
556
+ routerBasePlugin,
557
+ publicAssetReloadPlugin,
558
+ reloadGenerationPlugin()
559
+ ];
506
560
  }
507
561
  function enterProdPlugin(options = {}) {
508
562
  const {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-enter-dev",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A Vite plugin for development enhancements",
@@ -11,17 +11,15 @@
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js"
14
+ },
15
+ "./configure": {
16
+ "types": "./dist/configure.d.ts",
17
+ "import": "./dist/configure.js"
14
18
  }
15
19
  },
16
20
  "files": [
17
21
  "dist"
18
22
  ],
19
- "scripts": {
20
- "build": "tsup",
21
- "dev": "tsup --watch",
22
- "lint": "eslint . --max-warnings 0",
23
- "typecheck": "tsc --noEmit"
24
- },
25
23
  "publishConfig": {
26
24
  "registry": "https://registry.npmjs.org/"
27
25
  },
@@ -34,19 +32,31 @@
34
32
  ],
35
33
  "author": "",
36
34
  "license": "MIT",
37
- "packageManager": "pnpm@10.15.0",
38
35
  "devDependencies": {
39
36
  "@types/node": "^20.19.9",
40
- "@workspace/eslint-config": "workspace:*",
41
- "@workspace/typescript-config": "workspace:*",
42
37
  "tsup": "^8.5.1",
43
- "typescript": "^5.9.2",
44
- "vite": "^6.0.0"
38
+ "@typescript/native": "npm:typescript@7.0.2",
39
+ "typescript": "npm:@typescript/typescript6@6.0.2",
40
+ "vite": "^6.0.0",
41
+ "react": "^19.2.3",
42
+ "react-dom": "^19.2.3",
43
+ "@types/babel__core": "^7.20.5",
44
+ "eslint": "^9.32.0",
45
+ "@workspace/eslint-config": "0.0.0",
46
+ "@workspace/typescript-config": "0.0.0"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
48
50
  },
49
51
  "dependencies": {
50
- "@vitejs/plugin-react": "^5.1.1"
52
+ "@vitejs/plugin-react": "^5.1.1",
53
+ "@babel/core": "^7.28.3"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsup --watch",
58
+ "lint": "eslint . --max-warnings 0",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "pnpm build && node --test --test-timeout=15000 *.test.mjs"
51
61
  }
52
- }
62
+ }