iterate-ui-vite 0.1.2 → 0.2.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.d.ts CHANGED
@@ -1,8 +1,24 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  interface IteratePluginOptions {
4
- /** Port for the iterate daemon (default: 4000) */
4
+ /**
5
+ * Port for the iterate daemon. If omitted, resolved in order:
6
+ * 1. ITERATE_DAEMON_PORT env var
7
+ * 2. An already-running daemon's port from .iterate/daemon.lock
8
+ * 3. `daemonPort` from .iterate/config.json
9
+ * 4. Auto-picked starting from the default (47100)
10
+ */
5
11
  daemonPort?: number;
12
+ /**
13
+ * Name of the app this plugin instance wraps. Must match a `name` in
14
+ * `.iterate/config.json`'s `apps[]` array. When set, the overlay forwards
15
+ * this name to the daemon's `/api/command` and `/api/iterations` endpoints
16
+ * when the user creates iterations via the overlay toolbar — so iterations
17
+ * spawn the right dev server for the app the user is currently viewing.
18
+ *
19
+ * Required in multi-app repos. Optional in single-app repos.
20
+ */
21
+ appName?: string;
6
22
  /** Disable the babel plugin that injects component names/source locations (default: false) */
7
23
  disableBabelPlugin?: boolean;
8
24
  }
@@ -26,5 +42,7 @@ interface IteratePluginOptions {
26
42
  * 5. Cleans up daemon when dev server stops
27
43
  */
28
44
  declare function iterate(options?: IteratePluginOptions): Plugin[];
45
+ /** Exported for tests. Resolves the daemon port for a vite plugin invocation. */
46
+ declare function resolveDaemonPort(repoRoot: string, startingFrom: number, override?: number): Promise<number>;
29
47
 
30
- export { type IteratePluginOptions, iterate as default, iterate };
48
+ export { type IteratePluginOptions, iterate as default, iterate, resolveDaemonPort };
package/dist/index.js CHANGED
@@ -2,10 +2,29 @@
2
2
  import { spawn, execSync } from "child_process";
3
3
  import { createRequire } from "module";
4
4
  import { readFileSync } from "fs";
5
+ import { dirname, join } from "path";
5
6
  import http from "http";
7
+ import {
8
+ findFreePort,
9
+ isPortInUse,
10
+ loadConfig,
11
+ readLockfile,
12
+ isDaemonAlive
13
+ } from "iterate-ui-core/node";
14
+ function resolvePackageEntry(packageName, _require) {
15
+ try {
16
+ return _require.resolve(packageName);
17
+ } catch {
18
+ const pkgJsonPath = _require.resolve(`${packageName}/package.json`);
19
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
20
+ const main = pkg.exports?.["."]?.import ?? pkg.main ?? "index.js";
21
+ return join(dirname(pkgJsonPath), main);
22
+ }
23
+ }
6
24
  function iterate(options = {}) {
7
- const daemonPort = options.daemonPort ?? 4e3;
8
25
  let daemon = null;
26
+ let resolvedPort = null;
27
+ let resolvedBase = "";
9
28
  let overlayJS = null;
10
29
  const plugins = [];
11
30
  if (!options.disableBabelPlugin) {
@@ -52,7 +71,7 @@ function iterate(options = {}) {
52
71
  });
53
72
  if (!result?.code) return null;
54
73
  return { code: result.code, map: result.map };
55
- } catch (err) {
74
+ } catch {
56
75
  return null;
57
76
  }
58
77
  }
@@ -62,15 +81,31 @@ function iterate(options = {}) {
62
81
  name: "iterate",
63
82
  apply: "serve",
64
83
  // Only active during dev
65
- configureServer(server) {
84
+ async configResolved(config) {
85
+ resolvedBase = (config.base ?? "/").replace(/\/+$/, "");
86
+ const repoRoot = getGitRoot() ?? config.root;
87
+ const fileConfig = (() => {
88
+ try {
89
+ return loadConfig(repoRoot);
90
+ } catch {
91
+ return null;
92
+ }
93
+ })();
94
+ const envPort = process.env.ITERATE_DAEMON_PORT ? parseInt(process.env.ITERATE_DAEMON_PORT, 10) : void 0;
95
+ const startingPort = options.daemonPort ?? envPort ?? fileConfig?.daemonPort ?? 47100;
96
+ resolvedPort = await resolveDaemonPort(repoRoot, startingPort, options.daemonPort ?? envPort);
97
+ },
98
+ async configureServer(server) {
66
99
  const repoRoot = getGitRoot() ?? server.config.root;
67
- daemon = startDaemon(daemonPort, repoRoot);
68
- server.middlewares.use("/__iterate__/overlay.js", (_req, res) => {
100
+ const port = resolvedPort ?? 47100;
101
+ daemon = await startDaemonIfNeeded(port, repoRoot);
102
+ const overlayPath = `${resolvedBase}/__iterate__/overlay.js`;
103
+ server.middlewares.use(overlayPath, (_req, res) => {
69
104
  if (!overlayJS) {
70
105
  try {
71
106
  const require2 = createRequire(import.meta.url);
72
- const overlayPath = require2.resolve("iterate-ui-overlay/standalone");
73
- overlayJS = readFileSync(overlayPath, "utf-8");
107
+ const path = require2.resolve("iterate-ui-overlay/standalone");
108
+ overlayJS = readFileSync(path, "utf-8");
74
109
  } catch {
75
110
  res.statusCode = 404;
76
111
  res.end("Overlay bundle not found");
@@ -81,10 +116,15 @@ function iterate(options = {}) {
81
116
  res.setHeader("Cache-Control", "no-cache");
82
117
  res.end(overlayJS);
83
118
  });
119
+ const basePrefix = resolvedBase ? resolvedBase : "";
84
120
  server.middlewares.use((req, res, next) => {
85
121
  const url = req.url ?? "";
86
- if (url.startsWith("/api/") || url.startsWith("/__iterate__/")) {
87
- proxyRequest(req, res, daemonPort);
122
+ const matches = (prefix) => url.startsWith(prefix) || !!basePrefix && url.startsWith(`${basePrefix}${prefix}`);
123
+ if (matches("/api/") || matches("/__iterate__/")) {
124
+ if (basePrefix && url.startsWith(basePrefix)) {
125
+ req.url = url.slice(basePrefix.length) || "/";
126
+ }
127
+ proxyRequest(req, res, port);
88
128
  return;
89
129
  }
90
130
  next();
@@ -94,12 +134,12 @@ function iterate(options = {}) {
94
134
  const proxy = http.request(
95
135
  {
96
136
  hostname: "127.0.0.1",
97
- port: daemonPort,
137
+ port,
98
138
  path: "/ws",
99
139
  method: "GET",
100
140
  headers: {
101
141
  ...req.headers,
102
- host: `127.0.0.1:${daemonPort}`
142
+ host: `127.0.0.1:${port}`
103
143
  }
104
144
  },
105
145
  () => {
@@ -127,25 +167,37 @@ Sec-WebSocket-Accept: ${_proxyRes.headers["sec-websocket-accept"]}\r
127
167
  }
128
168
  });
129
169
  server.httpServer?.on("close", () => {
130
- stopDaemon(daemon, daemonPort);
131
- daemon = null;
170
+ if (daemon) {
171
+ stopDaemon(daemon, port);
172
+ daemon = null;
173
+ }
132
174
  });
133
175
  },
134
176
  // Inject the overlay script into HTML
135
177
  transformIndexHtml(html) {
136
178
  const iterationName = JSON.stringify(process.env.ITERATE_ITERATION_NAME ?? "__original__");
179
+ const port = resolvedPort ?? 47100;
180
+ const overlaySrc = `${resolvedBase}/__iterate__/overlay.js`;
181
+ const appNameKey = options.appName ? `, appName: ${JSON.stringify(options.appName)}` : "";
137
182
  return html.replace(
138
183
  "</body>",
139
184
  `<script>
140
- window.__iterate_shell__ = { activeTool: 'select', activeIteration: ${iterationName}, daemonPort: ${daemonPort} };
185
+ window.__iterate_shell__ = { activeTool: 'select', activeIteration: ${iterationName}, daemonPort: ${port}, basePath: ${JSON.stringify(resolvedBase)}${appNameKey} };
141
186
  </script>
142
- <script src="/__iterate__/overlay.js" defer></script>
187
+ <script src=${JSON.stringify(overlaySrc)} defer></script>
143
188
  </body>`
144
189
  );
145
190
  }
146
191
  });
147
192
  return plugins;
148
193
  }
194
+ async function resolveDaemonPort(repoRoot, startingFrom, override) {
195
+ if (override && Number.isFinite(override)) return override;
196
+ const lock = readLockfile(repoRoot);
197
+ if (lock && isDaemonAlive(lock)) return lock.port;
198
+ if (await isPortInUse(startingFrom)) return startingFrom;
199
+ return await findFreePort(startingFrom);
200
+ }
149
201
  function getGitRoot() {
150
202
  try {
151
203
  return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
@@ -153,10 +205,24 @@ function getGitRoot() {
153
205
  return null;
154
206
  }
155
207
  }
208
+ async function startDaemonIfNeeded(port, cwd) {
209
+ if (await isPortInUse(port)) {
210
+ console.log(`[iterate] daemon already running on port ${port}`);
211
+ return null;
212
+ }
213
+ return startDaemon(port, cwd);
214
+ }
156
215
  function startDaemon(port, cwd) {
216
+ const _req = createRequire(import.meta.url);
217
+ const daemonEntryPath = resolvePackageEntry("iterate-ui-daemon", _req);
218
+ const daemonPath = `file://${daemonEntryPath}`;
157
219
  const child = spawn(
158
220
  process.execPath,
159
- ["--input-type=module", "-e", `import { startDaemon } from "iterate-ui-daemon"; startDaemon({ port: ${port}, cwd: ${JSON.stringify(cwd)} });`],
221
+ [
222
+ "--input-type=module",
223
+ "-e",
224
+ `import { startDaemon } from ${JSON.stringify(daemonPath)}; startDaemon({ port: ${port}, cwd: ${JSON.stringify(cwd)} });`
225
+ ],
160
226
  {
161
227
  cwd,
162
228
  stdio: ["ignore", "pipe", "pipe"],
@@ -218,5 +284,6 @@ function proxyRequest(req, res, port) {
218
284
  var index_default = iterate;
219
285
  export {
220
286
  index_default as default,
221
- iterate
287
+ iterate,
288
+ resolveDaemonPort
222
289
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-ui-vite",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "iterate Vite plugin — auto-starts daemon and injects overlay",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,9 +30,10 @@
30
30
  }
31
31
  },
32
32
  "dependencies": {
33
- "iterate-ui-daemon": "0.1.1",
34
- "iterate-ui-babel-plugin": "0.1.1",
35
- "iterate-ui-overlay": "0.1.1"
33
+ "iterate-ui-core": "0.2.0",
34
+ "iterate-ui-daemon": "0.2.0",
35
+ "iterate-ui-babel-plugin": "0.1.2",
36
+ "iterate-ui-overlay": "0.2.0"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "vite": "^5.0.0 || ^6.0.0"
@@ -43,11 +44,13 @@
43
44
  "@types/node": "^22.0.0",
44
45
  "tsup": "^8.3.0",
45
46
  "typescript": "^5.7.0",
46
- "vite": "^6.0.0"
47
+ "vite": "^6.0.0",
48
+ "vitest": "^4.0.18"
47
49
  },
48
50
  "scripts": {
49
51
  "build": "tsup src/index.ts --format esm --dts --external @babel/core",
50
52
  "dev": "tsup src/index.ts --format esm --dts --watch --external @babel/core",
51
- "clean": "rm -rf dist"
53
+ "clean": "rm -rf dist",
54
+ "test": "vitest run"
52
55
  }
53
56
  }