vite-plugin-enter-dev 0.0.11 → 0.0.13

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # vite-plugin-enter-dev
2
2
 
3
- Version 0.0.11 adds recovery for full reloads missed before the first HMR connection or while disconnected. Each served HTML document includes the server instance and reload revision. The client compares that generation on initial connection and reconnection; a stale document reloads once, while a current document remains in place.
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.
4
8
 
5
- The plugin uses Vite's public WebSocket and HTML transform hooks. It does not depend on optimizer internals or an arbitrary sleep. Existing workspaces must install this version and restart Vite to activate it. Publish 0.0.11 before deploying a Gateway that requires that version.
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,162 @@
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
+ let setup = [];
35
+ if (types.isArrowFunctionExpression(config) || types.isFunctionExpression(config)) {
36
+ if (types.isBlockStatement(config.body)) {
37
+ const last = config.body.body.at(-1);
38
+ if (!types.isReturnStatement(last)) {
39
+ throw new Error("Unsupported Vite config: expected a single unconditional return");
40
+ }
41
+ setup = config.body.body.slice(0, -1);
42
+ config = last.argument ?? void 0;
43
+ } else {
44
+ config = config.body;
45
+ }
46
+ }
47
+ if (!types.isObjectExpression(config)) throw new Error("Unsupported Vite config: expected a static exported configuration object");
48
+ 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" })))) {
49
+ throw new Error("Unsupported Vite config: dynamic configuration may override plugins");
50
+ }
51
+ const pluginProperties = config.properties.filter((node) => types.isObjectProperty(node) && !node.computed && (types.isIdentifier(node.key, { name: "plugins" }) || types.isStringLiteral(node.key, { value: "plugins" })));
52
+ if (pluginProperties.length > 1) throw new Error("Unsupported Vite config: duplicate plugins properties");
53
+ let changed = false;
54
+ let plugins = pluginProperties[0];
55
+ if (!plugins) {
56
+ plugins = types.objectProperty(types.identifier("plugins"), types.arrayExpression([]));
57
+ config.properties.push(plugins);
58
+ changed = true;
59
+ }
60
+ if (!types.isObjectProperty(plugins)) throw new Error("Unsupported plugins property");
61
+ const pluginArrays = resolvePluginArrays(plugins.value, setup);
62
+ if (!pluginArrays.length) {
63
+ throw new Error("Unsupported Vite config: expected a literal plugins array");
64
+ }
65
+ const calls = pluginArrays.flatMap((array) => array.elements).map((node) => types.isSpreadElement(node) ? node.argument : node);
66
+ if (calls.some((node) => !types.isCallExpression(node) && node !== null && !types.isBooleanLiteral(node))) {
67
+ throw new Error("Unsupported Vite config: dynamic plugin list");
68
+ }
69
+ const hostReact = calls.some((node) => types.isCallExpression(node) && types.isIdentifier(node.callee) && reactNames.includes(node.callee.name));
70
+ for (const name of ["enterProdPlugin", "enterDevPlugin"]) {
71
+ 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 }));
72
+ const aliases = specifiers.map((node) => node.local.name);
73
+ let local = aliases[0];
74
+ if (!local) {
75
+ local = name;
76
+ while (new RegExp(`\\b${local}\\b`).test(source)) local = `_${local}`;
77
+ ast.program.body.unshift(types.importDeclaration([types.importSpecifier(types.identifier(local), types.identifier(name))], types.stringLiteral("vite-plugin-enter-dev")));
78
+ changed = true;
79
+ }
80
+ const matching = calls.filter((node) => types.isCallExpression(node) && types.isIdentifier(node.callee) && (aliases.includes(node.callee.name) || node.callee.name === local));
81
+ if (matching.length > 1) throw new Error(`Duplicate ${name} calls in Vite config`);
82
+ let call = matching[0];
83
+ if (!call) {
84
+ call = types.callExpression(types.identifier(local), []);
85
+ pluginArrays[0].elements.push(types.spreadElement(call));
86
+ changed = true;
87
+ }
88
+ if (name !== "enterDevPlugin" || !hostReact || !types.isCallExpression(call)) continue;
89
+ if (call.arguments.length === 0) call.arguments.push(types.objectExpression([]));
90
+ const options = call.arguments[0];
91
+ if (!types.isObjectExpression(options) || options.properties.some((node) => !types.isObjectProperty(node) || node.computed)) {
92
+ throw new Error("Unsupported enterDevPlugin options: expected a static object");
93
+ }
94
+ const matchingOptions = options.properties.filter((node) => types.isObjectProperty(node) && !node.computed && (types.isIdentifier(node.key, { name: "react" }) || types.isStringLiteral(node.key, { value: "react" })));
95
+ if (matchingOptions.length > 1) throw new Error("Unsupported duplicate react options");
96
+ const option = matchingOptions[0];
97
+ if (types.isObjectProperty(option)) {
98
+ if (!types.isBooleanLiteral(option.value)) throw new Error("Unsupported dynamic enterDevPlugin react option");
99
+ if (!option.value.value) continue;
100
+ option.value = types.booleanLiteral(false);
101
+ } else {
102
+ options.properties.push(types.objectProperty(types.identifier("react"), types.booleanLiteral(false)));
103
+ }
104
+ changed = true;
105
+ }
106
+ if (!changed) return source;
107
+ const result = transformFromAstSync(ast, source, { babelrc: false, configFile: false });
108
+ if (!result?.code) throw new Error("Cannot generate Vite config");
109
+ return `${result.code}
110
+ `;
111
+ }
112
+ function configureViteFile(path, check = false) {
113
+ const source = readFileSync(path, "utf8");
114
+ const output = configureVite(source);
115
+ if (source === output) return false;
116
+ if (check) return true;
117
+ const temporary = `${path}.${randomUUID()}.tmp`;
118
+ try {
119
+ writeFileSync(temporary, output, { flag: "wx", mode: statSync(path).mode });
120
+ if (readFileSync(path, "utf8") !== source) throw new Error("Vite config changed during migration");
121
+ renameSync(temporary, path);
122
+ } finally {
123
+ rmSync(temporary, { force: true });
124
+ }
125
+ return true;
126
+ }
127
+ function resolvePluginArrays(value, setup) {
128
+ if (setup.length === 0 && types.isArrayExpression(value)) return [value];
129
+ if (types.isTSAsExpression(value) || types.isTSSatisfiesExpression(value)) value = value.expression;
130
+ if (types.isCallExpression(value) && types.isMemberExpression(value.callee) && !value.callee.computed && types.isIdentifier(value.callee.property, { name: "filter" }) && value.arguments.length === 1 && types.isIdentifier(value.arguments[0], { name: "Boolean" })) value = value.callee.object;
131
+ if (!types.isIdentifier(value)) throw new Error("Unsupported dynamic plugins reference");
132
+ const name = value.name;
133
+ const first = setup[0];
134
+ if (!types.isVariableDeclaration(first, { kind: "const" }) || first.declarations.length !== 1) {
135
+ throw new Error("Unsupported plugins initializer");
136
+ }
137
+ const declaration = first.declarations[0];
138
+ if (!types.isIdentifier(declaration.id, { name }) || !types.isArrayExpression(declaration.init)) {
139
+ throw new Error("Unsupported plugins initializer");
140
+ }
141
+ const arrays = [declaration.init];
142
+ const visit = (statement) => {
143
+ if (types.isIfStatement(statement)) {
144
+ visit(statement.consequent);
145
+ if (statement.alternate) visit(statement.alternate);
146
+ return;
147
+ }
148
+ if (types.isBlockStatement(statement)) {
149
+ statement.body.forEach(visit);
150
+ return;
151
+ }
152
+ const call = types.isExpressionStatement(statement) ? statement.expression : void 0;
153
+ if (!types.isCallExpression(call) || !types.isMemberExpression(call.callee) || call.callee.computed || !types.isIdentifier(call.callee.object, { name }) || !types.isIdentifier(call.callee.property, { name: "push" }) || call.arguments.some((argument) => types.isJSXNamespacedName(argument) || types.isArgumentPlaceholder(argument))) {
154
+ throw new Error("Unsupported plugins mutation");
155
+ }
156
+ arrays.push(types.arrayExpression(call.arguments));
157
+ };
158
+ setup.slice(1).forEach(visit);
159
+ return arrays;
160
+ }
161
+
162
+ 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
@@ -165,7 +165,7 @@ function injectSourcePlugin({ types: t }) {
165
165
  name: "inject-source-location-enhanced",
166
166
  visitor: {
167
167
  Program: {
168
- enter(path, state) {
168
+ enter(_path, state) {
169
169
  state.threeImportedElements = /* @__PURE__ */ new Set();
170
170
  state.threeNamespaces = /* @__PURE__ */ new Set();
171
171
  }
@@ -230,7 +230,7 @@ function injectSourcePlugin({ types: t }) {
230
230
  );
231
231
  const sourceStackAttr = t.jsxAttribute(
232
232
  t.jsxIdentifier("data-source-stack"),
233
- t.stringLiteral(JSON.stringify(elementInfo))
233
+ t.jsxExpressionContainer(t.stringLiteral(JSON.stringify(elementInfo)))
234
234
  );
235
235
  const elementTypeAttr = t.jsxAttribute(
236
236
  t.jsxIdentifier("data-element-type"),
@@ -274,7 +274,7 @@ function getElementType(nameNode) {
274
274
  if (nameNode.type === "JSXIdentifier") {
275
275
  return nameNode.name;
276
276
  } else if (nameNode.type === "JSXMemberExpression") {
277
- return `${nameNode.object.name}.${nameNode.property.name}`;
277
+ return `${getElementType(nameNode.object)}.${nameNode.property.name}`;
278
278
  }
279
279
  return "unknown";
280
280
  }
@@ -342,28 +342,22 @@ check();`
342
342
  // src/index.ts
343
343
  function enterDevPlugin(options = {}) {
344
344
  const { injectMessageListener = true, react: enableReact = true } = options;
345
- const reactPlugin = enableReact ? react({
346
- // 确保在开发模式下启用JSX源码映射
347
- jsxImportSource: void 0,
348
- jsxRuntime: "automatic",
349
- // 启用构建时注入源码位置信息
350
- babel: {
351
- plugins: [
352
- // 我们的自定义插件:在构建时注入源码位置
353
- [injectSourcePlugin],
354
- // 标准的React JSX转换
355
- [
356
- "@babel/plugin-transform-react-jsx",
357
- {
358
- runtime: "automatic",
359
- importSource: "react",
360
- development: true
361
- // 保持开发模式以获得更好的调试体验
362
- }
363
- ]
364
- ]
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
+ }
365
359
  }
366
- }) : [];
360
+ };
367
361
  const htmlInjectPlugin = {
368
362
  name: "enter-dev-message-listener",
369
363
  enforce: "pre",
@@ -382,7 +376,7 @@ function enterDevPlugin(options = {}) {
382
376
  if (target && (target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
383
377
  const isStylesheet =
384
378
  target.tagName === 'LINK' &&
385
- String(target.rel || '').toLowerCase().split(/s+/).includes('stylesheet');
379
+ String(target.rel || '').toLowerCase().split(/\\s+/).includes('stylesheet');
386
380
  if (target.tagName === 'SCRIPT' || isStylesheet) {
387
381
  window.__earlyErrors__.push({
388
382
  type: 'resource',
@@ -556,6 +550,7 @@ function enterDevPlugin(options = {}) {
556
550
  };
557
551
  return [
558
552
  ...reactPlugin,
553
+ sourcePlugin,
559
554
  htmlInjectPlugin,
560
555
  sandboxServerPlugin,
561
556
  routerBasePlugin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-enter-dev",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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
+ }