@slip-stream-kit/vite 0.14.0 → 0.16.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.js CHANGED
@@ -3,7 +3,7 @@ import { infraKitDev as infraKitDev2 } from "@slip-stream-kit/config/vite";
3
3
  import process from "node:process";
4
4
 
5
5
  // src/lib/dev-context/dev-context.ts
6
- import { infraKitDev } from "@slip-stream-kit/config/vite";
6
+ import { infraKitDev, loadPackageConfig } from "@slip-stream-kit/config/vite";
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  var DEV_CONTEXT_DIR = path.join(".infra-kit", "dev-context");
@@ -43,6 +43,16 @@ var proxySignature = (proxy) => {
43
43
  });
44
44
  return JSON.stringify(entries);
45
45
  };
46
+ var restartSignature = (proxy, devEnv) => {
47
+ const unset = [...devEnv?.unset ?? []].sort();
48
+ const set = Object.entries(devEnv?.set ?? {}).sort(([a], [b]) => {
49
+ return a < b ? -1 : 1;
50
+ });
51
+ return JSON.stringify([proxySignature(proxy), unset, set]);
52
+ };
53
+ var resolveRestartSignature = async (cwd, proxy) => {
54
+ return restartSignature(proxy, (await loadPackageConfig(cwd))?.dev?.env);
55
+ };
46
56
  var watchDevContext = (args) => {
47
57
  const { server, cwd, options, current } = args;
48
58
  const { fragmentDir, stateDir, configFile } = resolveWatchTargets(cwd);
@@ -51,14 +61,14 @@ var watchDevContext = (args) => {
51
61
  let signature = current;
52
62
  let timer;
53
63
  const recompute = async () => {
54
- let proxy;
64
+ let next;
55
65
  try {
56
- proxy = (await infraKitDev({ ...options, cwd, command: "serve", port: PROBE_PORT })).proxy;
66
+ const { proxy } = await infraKitDev({ ...options, cwd, command: "serve", port: PROBE_PORT, applyDevEnv: false });
67
+ next = await resolveRestartSignature(cwd, proxy);
57
68
  } catch (error) {
58
69
  server.warn(`[infra-kit] dev-context changed but the proxy could not be re-resolved: ${String(error)}`);
59
70
  return;
60
71
  }
61
- const next = proxySignature(proxy);
62
72
  if (next === signature) return;
63
73
  signature = next;
64
74
  await server.restart();
@@ -86,6 +96,43 @@ var watchDevContext = (args) => {
86
96
  };
87
97
  };
88
98
 
99
+ // src/lib/ipv6-loopback/ipv6-loopback.ts
100
+ import net from "node:net";
101
+ var IPV4_LOOPBACK = "127.0.0.1";
102
+ var IPV6_LOOPBACK = "::1";
103
+ var mirrorOnIpv6Loopback = (server, handlers) => {
104
+ return new Promise((resolve) => {
105
+ const start = () => {
106
+ const address = server.address();
107
+ if (address === null || typeof address === "string" || address.address !== IPV4_LOOPBACK) {
108
+ resolve(void 0);
109
+ return;
110
+ }
111
+ const mirror = net.createServer((socket) => {
112
+ server.emit("connection", socket);
113
+ });
114
+ mirror.on("error", (err) => {
115
+ if (mirror.listening) {
116
+ handlers.onError(err);
117
+ } else {
118
+ handlers.onBindError(err);
119
+ resolve(void 0);
120
+ }
121
+ });
122
+ mirror.listen(address.port, IPV6_LOOPBACK, () => {
123
+ resolve(mirror);
124
+ });
125
+ const close = server.close.bind(server);
126
+ server.close = (callback) => {
127
+ if (mirror.listening) mirror.close();
128
+ return close(callback);
129
+ };
130
+ };
131
+ if (server.listening) start();
132
+ else server.once("listening", start);
133
+ });
134
+ };
135
+
89
136
  // src/lib/server-config/server-config.ts
90
137
  var withoutUserRoutes = (proxy, userProxy) => {
91
138
  if (!userProxy) return proxy;
@@ -136,16 +183,17 @@ var asRestartable = (server) => {
136
183
  };
137
184
  var infraKit = (options = {}) => {
138
185
  const cwd = options.cwd ?? process.cwd();
139
- let signature = proxySignature({});
186
+ let signature = "";
140
187
  let pinnedPortConflict = false;
141
188
  let dispose = () => {
142
189
  };
143
190
  return {
144
191
  name: "infra-kit",
145
192
  apply: "serve",
146
- config: async (userConfig) => {
147
- const resolved = await infraKitDev2({ ...options, cwd, command: "serve" });
148
- signature = proxySignature(resolved.proxy);
193
+ // Vitest runs `serve` config hooks too, under mode `test`; its env is the one the tests were given.
194
+ config: async (userConfig, { mode }) => {
195
+ const resolved = await infraKitDev2({ ...options, cwd, command: "serve", applyDevEnv: mode !== "test" });
196
+ signature = await resolveRestartSignature(cwd, resolved.proxy);
149
197
  pinnedPortConflict = hasPinnedPortConflict(resolved, userConfig.server);
150
198
  return { server: mergeServerConfig(resolved, userConfig.server) };
151
199
  },
@@ -155,6 +203,22 @@ var infraKit = (options = {}) => {
155
203
  "[infra-kit] server.port is pinned in this vite config, but `infra-kit dev` assigned this UI a different port and registered its portless alias against THAT one. The pin wins \u2014 and the hero URL will 502. Remove server.port to let the runner place it."
156
204
  );
157
205
  }
206
+ if (server.httpServer) {
207
+ const { logger } = server.config;
208
+ void mirrorOnIpv6Loopback(server.httpServer, {
209
+ // Only a taken port is worth a warning; a host without IPv6 (EADDRNOTAVAIL/EAFNOSUPPORT) would
210
+ // otherwise warn on every start and restart.
211
+ onBindError: (err) => {
212
+ if (err.code !== "EADDRINUSE") return;
213
+ logger.warn(
214
+ "[infra-kit] [::1] is taken on the dev-server port, so portless\u2019s IPv6 fallback reaches that process instead \u2014 the alias may 502 under heavy parallel load (e.g. e2e workers)."
215
+ );
216
+ },
217
+ onError: (err) => {
218
+ logger.warn(`[infra-kit] [::1] dev-server listener: ${err.code ?? err.message}`);
219
+ }
220
+ });
221
+ }
158
222
  if (options.restartOnDevContextChange === false) return;
159
223
  dispose = watchDevContext({ server: asRestartable(server), cwd, options, current: signature }).dispose;
160
224
  },
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lib/plugin/plugin.ts", "../src/lib/dev-context/dev-context.ts", "../src/lib/server-config/server-config.ts"],
4
- "sourcesContent": ["import type { InfraKitDevOptions } from '@slip-stream-kit/config/vite'\nimport { infraKitDev } from '@slip-stream-kit/config/vite'\nimport process from 'node:process'\nimport type { Plugin, ViteDevServer } from 'vite'\n\nimport type { RestartableServer } from '../dev-context/dev-context'\nimport { proxySignature, watchDevContext } from '../dev-context/dev-context'\nimport { hasPinnedPortConflict, mergeServerConfig } from '../server-config/server-config'\n\n/**\n * Everything `infraKitDev()` accepts, minus `command` \u2014 the plugin knows vite's command without being\n * told (see {@link infraKit}), and accepting it would only let a consumer contradict it.\n */\nexport interface InfraKitPluginOptions extends Omit<InfraKitDevOptions, 'command'> {\n /**\n * Re-resolve the proxy and restart the dev server when the `.infra-kit/dev-context` fragments change,\n * so a backend started AFTER the frontend flips its route from `cloud` to `local` on its own. Default\n * `true`. Set `false` to freeze the proxy at whatever it resolved to when vite booted.\n */\n restartOnDevContextChange?: boolean\n}\n\n/** Adapt a real vite dev server to the narrow surface {@link watchDevContext} drives. */\nconst asRestartable = (server: ViteDevServer): RestartableServer => {\n return {\n watcher: {\n add: (target) => {\n server.watcher.add(target)\n },\n on: (event, listener) => {\n server.watcher.on(event, (file: string) => {\n listener(file)\n })\n },\n },\n restart: async () => {\n await server.restart()\n },\n warn: (message) => {\n server.config.logger.warn(message)\n },\n }\n}\n\n/**\n * The infra-kit vite plugin: a per-worktree dev port (or the one `infra-kit dev` assigned this UI),\n * HMR pointed at the portless HTTPS alias, and the `dev.proxy` map from the package's\n * `infra-kit.config.ts` \u2014 resolved live, and re-resolved whenever the local dev set changes.\n *\n * `apply: 'serve'` is load-bearing, not tidiness. Vite filters plugins by `apply` BEFORE it runs their\n * `config` hooks, so on `build` this plugin does not exist at all. The helper it wraps had to be told\n * (`infraKitDev({ command })`) and would otherwise fail-fast on a cloud route with no sourced env \u2014 a\n * failure whose only cause was a consumer forgetting to thread an argument. That class of bug is gone.\n *\n * @example\n * // vite.config.ts\n * import { infraKit } from '@slip-stream-kit/vite'\n * import { defineConfig } from 'vite'\n *\n * export default defineConfig({ plugins: [infraKit()] })\n */\nexport const infraKit = (options: InfraKitPluginOptions = {}): Plugin => {\n const cwd = options.cwd ?? process.cwd()\n\n let signature = proxySignature({})\n let pinnedPortConflict = false\n let dispose: () => void = () => {}\n\n return {\n name: 'infra-kit',\n apply: 'serve',\n\n config: async (userConfig) => {\n const resolved = await infraKitDev({ ...options, cwd, command: 'serve' })\n\n signature = proxySignature(resolved.proxy)\n pinnedPortConflict = hasPinnedPortConflict(resolved, userConfig.server)\n\n return { server: mergeServerConfig(resolved, userConfig.server) }\n },\n\n configureServer: (server) => {\n if (pinnedPortConflict) {\n server.config.logger.warn(\n '[infra-kit] server.port is pinned in this vite config, but `infra-kit dev` assigned this UI a ' +\n 'different port and registered its portless alias against THAT one. The pin wins \u2014 and the ' +\n 'hero URL will 502. Remove server.port to let the runner place it.',\n )\n }\n\n if (options.restartOnDevContextChange === false) return\n\n dispose = watchDevContext({ server: asRestartable(server), cwd, options, current: signature }).dispose\n },\n\n // The dev server calls this on close (including on the restart the watcher itself triggers), so the\n // pending debounce of a server that is going away never fires against the one replacing it.\n buildEnd: () => {\n dispose()\n dispose = () => {}\n },\n }\n}\n", "import type { InfraKitDevOptions, InfraKitViteProxy } from '@slip-stream-kit/config/vite'\nimport { infraKitDev } from '@slip-stream-kit/config/vite'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\n/** Repo-relative dev-context fragment directory the `infra-kit dev` runner writes into. */\nconst DEV_CONTEXT_DIR = path.join('.infra-kit', 'dev-context')\n\n/** The state directory that holds {@link DEV_CONTEXT_DIR} \u2014 watched too, so a first fragment is seen. */\nconst STATE_DIR = '.infra-kit'\n\n/** The package's own config: editing `dev.proxy` must take effect without a manual restart. */\nconst PACKAGE_CONFIG_FILE = 'infra-kit.config.ts'\n\n/** Coalesce the burst of writes a runner makes when it brings several packages up at once. */\nconst DEBOUNCE_MS = 150\n\n/**\n * A dummy `port` passed to the re-resolve. Only `proxy` is read from the result, and an omitted port\n * makes the helper probe (bind + release) a fresh free one \u2014 a pointless side effect to repeat on every\n * fragment write. Any number suppresses it.\n */\nconst PROBE_PORT = 1\n\n/** Search upward from `start` for `relative`, returning the first hit or undefined. */\nconst findUp = (start: string, relative: string): string | undefined => {\n let dir = path.resolve(start)\n\n for (;;) {\n const candidate = path.join(dir, relative)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (parent === dir) return undefined\n\n dir = parent\n }\n}\n\n/** The paths whose changes can alter this package's proxy map. */\nexport interface WatchTargets {\n /** The dev-context fragment dir, or `undefined` when no repo root could be located. */\n fragmentDir: string | undefined\n /** The dir to watch so the fragment dir is seen even when it does not exist yet. */\n stateDir: string | undefined\n /** This package's own `infra-kit.config.ts` (watched whether or not it exists yet). */\n configFile: string\n}\n\n/**\n * Where to watch, resolved from the package dir.\n *\n * The fragment dir is found by searching upward \u2014 but it may legitimately NOT EXIST yet: a frontend\n * started before any backend has none, and that is exactly the case the live re-resolve exists to fix.\n * So fall back to the `.infra-kit` state dir (also searched upward), then to the git root, and let the\n * watcher pick up the directory when the runner creates it. With no repo root at all there is nothing\n * to watch and the plugin stays inert.\n */\nexport const resolveWatchTargets = (cwd: string): WatchTargets => {\n const configFile = path.join(cwd, PACKAGE_CONFIG_FILE)\n const existing = findUp(cwd, DEV_CONTEXT_DIR)\n\n if (existing) return { fragmentDir: existing, stateDir: path.dirname(existing), configFile }\n\n const state = findUp(cwd, STATE_DIR)\n\n if (state) return { fragmentDir: path.join(state, 'dev-context'), stateDir: state, configFile }\n\n const gitDir = findUp(cwd, '.git')\n\n if (!gitDir) return { fragmentDir: undefined, stateDir: undefined, configFile }\n\n const root = path.dirname(gitDir)\n\n return { fragmentDir: path.join(root, DEV_CONTEXT_DIR), stateDir: path.join(root, STATE_DIR), configFile }\n}\n\n/** Order-independent, field-stable digest of a resolved proxy map. */\nexport const proxySignature = (proxy: InfraKitViteProxy): string => {\n const entries = Object.entries(proxy)\n .map(([routePath, entry]) => {\n const fields = Object.entries(entry as unknown as Record<string, unknown>).sort(([a], [b]) => {\n return a < b ? -1 : 1\n })\n\n return [routePath, fields] as const\n })\n .sort(([a], [b]) => {\n return a < b ? -1 : 1\n })\n\n return JSON.stringify(entries)\n}\n\n/**\n * The slice of `ViteDevServer` this module needs. Narrowed to an interface so the watch loop is\n * driveable by a fake in tests \u2014 booting a real vite dev server to assert \"it restarted once\" would\n * test vite, not this.\n */\nexport interface RestartableServer {\n watcher: {\n add: (target: string) => void\n on: (event: 'add' | 'change' | 'unlink' | 'addDir', listener: (file: string) => void) => void\n }\n restart: () => Promise<void>\n warn: (message: string) => void\n}\n\nexport interface WatchDevContextArgs {\n server: RestartableServer\n /** The package dir whose config + proxy are resolved (the plugin's `cwd`). */\n cwd: string\n /** The options the plugin was constructed with, re-applied on every re-resolve. */\n options: InfraKitDevOptions\n /** The signature of the proxy map vite is currently serving (taken in the `config` hook). */\n current: string\n}\n\n/**\n * Re-resolve the proxy on every dev-context write, and restart vite when \u2014 and ONLY when \u2014 the resolved\n * proxy actually changed.\n *\n * This is the whole reason the plugin exists as a plugin. `infraKitDev()` resolves once, while vite is\n * computing its config, so a backend started AFTER the frontend can never flip its route from `cloud` to\n * `local`: the answer was already baked. Watching the fragments closes that, and a `local \u2192 cloud`\n * demotion (a backend that died) heals the same way.\n *\n * The signature compare is not an optimisation, it is the loop guard. The runner rewrites its fragments\n * on every restart of its own (fresh `pid`, fresh `writtenAt`) and writes a fragment for THIS UI too, so\n * a watcher that restarted on any write would restart on writes caused by its own restart. Comparing the\n * resolved PROXY \u2014 the only thing a restart would change \u2014 makes those writes inert by construction.\n */\nexport const watchDevContext = (args: WatchDevContextArgs): { dispose: () => void } => {\n const { server, cwd, options, current } = args\n const { fragmentDir, stateDir, configFile } = resolveWatchTargets(cwd)\n\n if (!fragmentDir || !stateDir) return { dispose: () => {} }\n\n let signature = current\n let timer: NodeJS.Timeout | undefined\n\n const recompute = async (): Promise<void> => {\n let proxy: InfraKitViteProxy\n\n try {\n proxy = (await infraKitDev({ ...options, cwd, command: 'serve', port: PROBE_PORT })).proxy\n } catch (error) {\n // A half-written fragment, or a `dev.proxy` edit mid-save, resolves to an error. It is transient by\n // nature \u2014 the next write re-runs this \u2014 so warn and keep serving the proxy we already have rather\n // than tearing the dev server down over it.\n server.warn(`[infra-kit] dev-context changed but the proxy could not be re-resolved: ${String(error)}`)\n\n return\n }\n\n const next = proxySignature(proxy)\n\n if (next === signature) return\n\n signature = next\n await server.restart()\n }\n\n const schedule = (): void => {\n clearTimeout(timer)\n timer = setTimeout(() => {\n void recompute()\n }, DEBOUNCE_MS)\n }\n\n const isRelevant = (file: string): boolean => {\n return file === configFile || file === fragmentDir || file.startsWith(fragmentDir + path.sep)\n }\n\n server.watcher.add(stateDir)\n server.watcher.add(configFile)\n\n for (const event of ['add', 'change', 'unlink', 'addDir'] as const) {\n server.watcher.on(event, (file: string) => {\n if (isRelevant(file)) schedule()\n })\n }\n\n return {\n dispose: () => {\n clearTimeout(timer)\n },\n }\n}\n", "import type { InfraKitViteProxy } from '@slip-stream-kit/config/vite'\n\n/**\n * What `infraKitDev()` resolves: the ready-made vite `server` block. Re-declared structurally rather\n * than imported, because the helper types it inline on its return type and there is no named export\n * to reach for.\n */\nexport interface ResolvedDevServer {\n port?: number\n host?: string | boolean\n strictPort?: boolean\n /**\n * The alias websocket override. `@slip-stream-kit/config` <= 0.3.3 spells it `hmr`; later versions\n * spell it `ws`. This package depends on `^0.3.3`, which permits BOTH, so both are declared and\n * {@link mergeServerConfig} accepts either \u2014 reading only `ws` would silently wire HMR to nothing\n * for anyone resolving the older config, with no error to explain it.\n */\n ws?: { protocol: 'wss'; host: string; clientPort: number }\n /** Legacy spelling of {@link ws}, emitted by `@slip-stream-kit/config` <= 0.3.3. */\n hmr?: { protocol: 'wss'; host: string; clientPort: number }\n proxy: InfraKitViteProxy\n}\n\n/**\n * The subset of a consumer's `server` block this plugin can collide with. Structurally compatible with\n * vite's `ServerOptions`, so the plugin passes `config.server` straight in without a cast.\n */\nexport interface UserServerConfig {\n port?: number\n host?: string | boolean\n strictPort?: boolean\n /** Legacy alias of {@link ws}. Still declared because a consumer setting it must still win \u2014 see {@link mergeServerConfig}. */\n hmr?: unknown\n ws?: unknown\n proxy?: Record<string, unknown>\n}\n\n/** The `server` block this plugin contributes \u2014 every key optional, because every key is conditional. */\nexport interface InfraKitServerConfig {\n port?: number\n host?: string | boolean\n strictPort?: boolean\n ws?: ResolvedDevServer['ws']\n proxy?: InfraKitViteProxy\n}\n\n/** Drop every route the consumer declared themselves \u2014 an explicit route in their config outranks ours. */\nconst withoutUserRoutes = (\n proxy: InfraKitViteProxy,\n userProxy: Record<string, unknown> | undefined,\n): InfraKitViteProxy => {\n if (!userProxy) return proxy\n\n const result: InfraKitViteProxy = {}\n\n for (const [routePath, entry] of Object.entries(proxy)) {\n if (routePath in userProxy) continue\n result[routePath] = entry\n }\n\n return result\n}\n\n/**\n * The `server` block to return from the `config` hook, given what `infraKitDev()` resolved and what the\n * consumer wrote themselves.\n *\n * This exists because of ONE fact about vite: a `config` hook's result is merged **over** the user config\n * (`mergeConfig(userConfig, pluginResult)`), so anything emitted here WINS against an explicit setting in\n * the consumer's own `vite.config.ts`. A plugin that just returns everything `infraKitDev()` produced\n * would therefore silently overrule a hand-pinned `server.port` \u2014 the exact opposite of what a plugin\n * should do, and worse than the `server: await infraKitDev()` spread it replaces (there, at least, the\n * consumer could see the assignment).\n *\n * So: emit only what the consumer left unset. `strictPort` rides with `port` \u2014 it is meaningless applied\n * to someone else's port, and forcing it onto a hand-pinned one would turn a soft collision into a hard\n * boot failure.\n */\nexport const mergeServerConfig = (\n resolved: ResolvedDevServer,\n user: UserServerConfig | undefined,\n): InfraKitServerConfig => {\n const server: InfraKitServerConfig = {}\n\n if (user?.port == null) {\n if (resolved.port != null) server.port = resolved.port\n if (user?.strictPort == null && resolved.strictPort != null) server.strictPort = resolved.strictPort\n }\n\n if (user?.host == null && resolved.host != null) server.host = resolved.host\n // Guard on BOTH keys, not just `ws`. Vite 8 back-fills a legacy `server.hmr` onto `server.ws` with\n // `??=`, so ours would already be set by the time the compat shim ran: a consumer who hand-wrote\n // `server.hmr` would silently lose to us, which is the exact failure this whole module exists to\n // prevent. Their explicit setting outranks ours whichever spelling they used.\n // Always CONTRIBUTE as `ws` (vite 8 deprecated `server.hmr.*`), but ACCEPT either spelling from the\n // config package \u2014 see {@link ResolvedDevServer.ws}. Translating here means a consumer still on\n // config <= 0.3.3 stops getting the deprecation warning as soon as this package is published, with\n // no lockstep upgrade required.\n const resolvedWs = resolved.ws ?? resolved.hmr\n\n if (user?.ws == null && user?.hmr == null && resolvedWs != null) server.ws = resolvedWs\n\n const proxy = withoutUserRoutes(resolved.proxy, user?.proxy)\n\n if (Object.keys(proxy).length > 0) server.proxy = proxy\n\n return server\n}\n\n/**\n * True when the consumer pinned `server.port` while `infra-kit dev` had already ASSIGNED this UI a port\n * (`strictPort` on the resolved block is the runner's fingerprint \u2014 only the Layer-B path sets it).\n *\n * Honouring the pin is still right (it is explicit), but it is not free: the runner registered a portless\n * alias pointing at the port it assigned, and the UI is about to bind a different one. The hero URL then\n * 502s, and nothing in that failure names the pinned port as the cause. Worth one warning line.\n */\nexport const hasPinnedPortConflict = (resolved: ResolvedDevServer, user: UserServerConfig | undefined): boolean => {\n return user?.port != null && resolved.strictPort === true && user.port !== resolved.port\n}\n"],
5
- "mappings": "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",
3
+ "sources": ["../src/lib/plugin/plugin.ts", "../src/lib/dev-context/dev-context.ts", "../src/lib/ipv6-loopback/ipv6-loopback.ts", "../src/lib/server-config/server-config.ts"],
4
+ "sourcesContent": ["import type { InfraKitDevOptions } from '@slip-stream-kit/config/vite'\nimport { infraKitDev } from '@slip-stream-kit/config/vite'\nimport process from 'node:process'\nimport type { Plugin, ViteDevServer } from 'vite'\n\nimport type { RestartableServer } from '../dev-context/dev-context'\nimport { resolveRestartSignature, watchDevContext } from '../dev-context/dev-context'\nimport { mirrorOnIpv6Loopback } from '../ipv6-loopback/ipv6-loopback'\nimport { hasPinnedPortConflict, mergeServerConfig } from '../server-config/server-config'\n\n/**\n * Everything `infraKitDev()` accepts, minus `command` and `applyDevEnv` \u2014 the plugin knows vite's command and\n * mode without being told (see {@link infraKit}), and accepting them would only let a consumer contradict it.\n */\nexport interface InfraKitPluginOptions extends Omit<InfraKitDevOptions, 'command' | 'applyDevEnv'> {\n /**\n * Re-resolve the proxy and restart the dev server when the `.infra-kit/dev-context` fragments change,\n * so a backend started AFTER the frontend flips its route from `cloud` to `local` on its own. Default\n * `true`. Set `false` to freeze the proxy at whatever it resolved to when vite booted.\n */\n restartOnDevContextChange?: boolean\n}\n\n/** Adapt a real vite dev server to the narrow surface {@link watchDevContext} drives. */\nconst asRestartable = (server: ViteDevServer): RestartableServer => {\n return {\n watcher: {\n add: (target) => {\n server.watcher.add(target)\n },\n on: (event, listener) => {\n server.watcher.on(event, (file: string) => {\n listener(file)\n })\n },\n },\n restart: async () => {\n await server.restart()\n },\n warn: (message) => {\n server.config.logger.warn(message)\n },\n }\n}\n\n/**\n * The infra-kit vite plugin: a per-worktree dev port (or the one `infra-kit dev` assigned this UI),\n * HMR pointed at the portless HTTPS alias, and the `dev.proxy` map from the package's\n * `infra-kit.config.ts` \u2014 resolved live, and re-resolved whenever the local dev set changes.\n *\n * `apply: 'serve'` is load-bearing, not tidiness. Vite filters plugins by `apply` BEFORE it runs their\n * `config` hooks, so on `build` this plugin does not exist at all. The helper it wraps had to be told\n * (`infraKitDev({ command })`) and would otherwise fail-fast on a cloud route with no sourced env \u2014 a\n * failure whose only cause was a consumer forgetting to thread an argument. That class of bug is gone.\n *\n * @example\n * // vite.config.ts\n * import { infraKit } from '@slip-stream-kit/vite'\n * import { defineConfig } from 'vite'\n *\n * export default defineConfig({ plugins: [infraKit()] })\n */\nexport const infraKit = (options: InfraKitPluginOptions = {}): Plugin => {\n const cwd = options.cwd ?? process.cwd()\n\n let signature = ''\n let pinnedPortConflict = false\n let dispose: () => void = () => {}\n\n return {\n name: 'infra-kit',\n apply: 'serve',\n\n // Vitest runs `serve` config hooks too, under mode `test`; its env is the one the tests were given.\n config: async (userConfig, { mode }) => {\n const resolved = await infraKitDev({ ...options, cwd, command: 'serve', applyDevEnv: mode !== 'test' })\n\n signature = await resolveRestartSignature(cwd, resolved.proxy)\n pinnedPortConflict = hasPinnedPortConflict(resolved, userConfig.server)\n\n return { server: mergeServerConfig(resolved, userConfig.server) }\n },\n\n configureServer: (server) => {\n if (pinnedPortConflict) {\n server.config.logger.warn(\n '[infra-kit] server.port is pinned in this vite config, but `infra-kit dev` assigned this UI a ' +\n 'different port and registered its portless alias against THAT one. The pin wins \u2014 and the ' +\n 'hero URL will 502. Remove server.port to let the runner place it.',\n )\n }\n\n if (server.httpServer) {\n const { logger } = server.config\n\n void mirrorOnIpv6Loopback(server.httpServer, {\n // Only a taken port is worth a warning; a host without IPv6 (EADDRNOTAVAIL/EAFNOSUPPORT) would\n // otherwise warn on every start and restart.\n onBindError: (err) => {\n if (err.code !== 'EADDRINUSE') return\n logger.warn(\n '[infra-kit] [::1] is taken on the dev-server port, so portless\u2019s IPv6 fallback reaches that ' +\n 'process instead \u2014 the alias may 502 under heavy parallel load (e.g. e2e workers).',\n )\n },\n onError: (err) => {\n logger.warn(`[infra-kit] [::1] dev-server listener: ${err.code ?? err.message}`)\n },\n })\n }\n\n if (options.restartOnDevContextChange === false) return\n\n dispose = watchDevContext({ server: asRestartable(server), cwd, options, current: signature }).dispose\n },\n\n // The dev server calls this on close (including on the restart the watcher itself triggers), so the\n // pending debounce of a server that is going away never fires against the one replacing it.\n buildEnd: () => {\n dispose()\n dispose = () => {}\n },\n }\n}\n", "import type { InfraKitDevEnv } from '@slip-stream-kit/config'\nimport type { InfraKitDevOptions, InfraKitViteProxy } from '@slip-stream-kit/config/vite'\nimport { infraKitDev, loadPackageConfig } from '@slip-stream-kit/config/vite'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\n/** Repo-relative dev-context fragment directory the `infra-kit dev` runner writes into. */\nconst DEV_CONTEXT_DIR = path.join('.infra-kit', 'dev-context')\n\n/** The state directory that holds {@link DEV_CONTEXT_DIR} \u2014 watched too, so a first fragment is seen. */\nconst STATE_DIR = '.infra-kit'\n\n/** The package's own config: editing `dev.proxy` must take effect without a manual restart. */\nconst PACKAGE_CONFIG_FILE = 'infra-kit.config.ts'\n\n/** Coalesce the burst of writes a runner makes when it brings several packages up at once. */\nconst DEBOUNCE_MS = 150\n\n/**\n * A dummy `port` passed to the re-resolve. Only `proxy` is read from the result, and an omitted port\n * makes the helper probe (bind + release) a fresh free one \u2014 a pointless side effect to repeat on every\n * fragment write. Any number suppresses it.\n */\nconst PROBE_PORT = 1\n\n/** Search upward from `start` for `relative`, returning the first hit or undefined. */\nconst findUp = (start: string, relative: string): string | undefined => {\n let dir = path.resolve(start)\n\n for (;;) {\n const candidate = path.join(dir, relative)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (parent === dir) return undefined\n\n dir = parent\n }\n}\n\n/** The paths whose changes can alter this package's proxy map. */\nexport interface WatchTargets {\n /** The dev-context fragment dir, or `undefined` when no repo root could be located. */\n fragmentDir: string | undefined\n /** The dir to watch so the fragment dir is seen even when it does not exist yet. */\n stateDir: string | undefined\n /** This package's own `infra-kit.config.ts` (watched whether or not it exists yet). */\n configFile: string\n}\n\n/**\n * Where to watch, resolved from the package dir.\n *\n * The fragment dir is found by searching upward \u2014 but it may legitimately NOT EXIST yet: a frontend\n * started before any backend has none, and that is exactly the case the live re-resolve exists to fix.\n * So fall back to the `.infra-kit` state dir (also searched upward), then to the git root, and let the\n * watcher pick up the directory when the runner creates it. With no repo root at all there is nothing\n * to watch and the plugin stays inert.\n */\nexport const resolveWatchTargets = (cwd: string): WatchTargets => {\n const configFile = path.join(cwd, PACKAGE_CONFIG_FILE)\n const existing = findUp(cwd, DEV_CONTEXT_DIR)\n\n if (existing) return { fragmentDir: existing, stateDir: path.dirname(existing), configFile }\n\n const state = findUp(cwd, STATE_DIR)\n\n if (state) return { fragmentDir: path.join(state, 'dev-context'), stateDir: state, configFile }\n\n const gitDir = findUp(cwd, '.git')\n\n if (!gitDir) return { fragmentDir: undefined, stateDir: undefined, configFile }\n\n const root = path.dirname(gitDir)\n\n return { fragmentDir: path.join(root, DEV_CONTEXT_DIR), stateDir: path.join(root, STATE_DIR), configFile }\n}\n\n/** Order-independent, field-stable digest of a resolved proxy map. */\nexport const proxySignature = (proxy: InfraKitViteProxy): string => {\n const entries = Object.entries(proxy)\n .map(([routePath, entry]) => {\n const fields = Object.entries(entry as unknown as Record<string, unknown>).sort(([a], [b]) => {\n return a < b ? -1 : 1\n })\n\n return [routePath, fields] as const\n })\n .sort(([a], [b]) => {\n return a < b ? -1 : 1\n })\n\n return JSON.stringify(entries)\n}\n\n/**\n * What a restart would change: the proxy map and the package's `dev.env`, which only a fresh config hook\n * applies before vite's `loadEnv`.\n */\nexport const restartSignature = (proxy: InfraKitViteProxy, devEnv: InfraKitDevEnv | undefined): string => {\n const unset = [...(devEnv?.unset ?? [])].sort()\n const set = Object.entries(devEnv?.set ?? {}).sort(([a], [b]) => {\n return a < b ? -1 : 1\n })\n\n return JSON.stringify([proxySignature(proxy), unset, set])\n}\n\n/** The {@link restartSignature} of what the package's config resolves to now. */\nexport const resolveRestartSignature = async (cwd: string, proxy: InfraKitViteProxy): Promise<string> => {\n return restartSignature(proxy, (await loadPackageConfig(cwd))?.dev?.env)\n}\n\n/**\n * The slice of `ViteDevServer` this module needs. Narrowed to an interface so the watch loop is\n * driveable by a fake in tests \u2014 booting a real vite dev server to assert \"it restarted once\" would\n * test vite, not this.\n */\nexport interface RestartableServer {\n watcher: {\n add: (target: string) => void\n on: (event: 'add' | 'change' | 'unlink' | 'addDir', listener: (file: string) => void) => void\n }\n restart: () => Promise<void>\n warn: (message: string) => void\n}\n\nexport interface WatchDevContextArgs {\n server: RestartableServer\n /** The package dir whose config + proxy are resolved (the plugin's `cwd`). */\n cwd: string\n /** The options the plugin was constructed with, re-applied on every re-resolve. */\n options: InfraKitDevOptions\n /** The {@link restartSignature} vite is currently serving (taken in the `config` hook). */\n current: string\n}\n\n/**\n * Re-resolve the proxy on every dev-context write, and restart vite when \u2014 and ONLY when \u2014 the resolved\n * proxy actually changed.\n *\n * This is the whole reason the plugin exists as a plugin. `infraKitDev()` resolves once, while vite is\n * computing its config, so a backend started AFTER the frontend can never flip its route from `cloud` to\n * `local`: the answer was already baked. Watching the fragments closes that, and a `local \u2192 cloud`\n * demotion (a backend that died) heals the same way.\n *\n * The signature compare is not an optimisation, it is the loop guard. The runner rewrites its fragments\n * on every restart of its own (fresh `pid`, fresh `writtenAt`) and writes a fragment for THIS UI too, so\n * a watcher that restarted on any write would restart on writes caused by its own restart. Comparing the\n * resolved PROXY \u2014 the only thing a restart would change \u2014 makes those writes inert by construction.\n */\nexport const watchDevContext = (args: WatchDevContextArgs): { dispose: () => void } => {\n const { server, cwd, options, current } = args\n const { fragmentDir, stateDir, configFile } = resolveWatchTargets(cwd)\n\n if (!fragmentDir || !stateDir) return { dispose: () => {} }\n\n let signature = current\n let timer: NodeJS.Timeout | undefined\n\n const recompute = async (): Promise<void> => {\n let next: string\n\n try {\n // A probe: `dev.env` applied now would land after `loadEnv`; the restart's own config hook applies it.\n const { proxy } = await infraKitDev({ ...options, cwd, command: 'serve', port: PROBE_PORT, applyDevEnv: false })\n\n next = await resolveRestartSignature(cwd, proxy)\n } catch (error) {\n // A half-written fragment, or a `dev.proxy` edit mid-save, resolves to an error. It is transient by\n // nature \u2014 the next write re-runs this \u2014 so warn and keep serving the proxy we already have rather\n // than tearing the dev server down over it.\n server.warn(`[infra-kit] dev-context changed but the proxy could not be re-resolved: ${String(error)}`)\n\n return\n }\n\n if (next === signature) return\n\n signature = next\n await server.restart()\n }\n\n const schedule = (): void => {\n clearTimeout(timer)\n timer = setTimeout(() => {\n void recompute()\n }, DEBOUNCE_MS)\n }\n\n const isRelevant = (file: string): boolean => {\n return file === configFile || file === fragmentDir || file.startsWith(fragmentDir + path.sep)\n }\n\n server.watcher.add(stateDir)\n server.watcher.add(configFile)\n\n for (const event of ['add', 'change', 'unlink', 'addDir'] as const) {\n server.watcher.on(event, (file: string) => {\n if (isRelevant(file)) schedule()\n })\n }\n\n return {\n dispose: () => {\n clearTimeout(timer)\n },\n }\n}\n", "import net from 'node:net'\nimport type { Server } from 'node:net'\n\nconst IPV4_LOOPBACK = '127.0.0.1'\nconst IPV6_LOOPBACK = '::1'\n\nexport interface MirrorHandlers {\n /** The `::1` bind failed (no IPv6, port taken). The IPv4 listener keeps serving either way. */\n onBindError: (err: NodeJS.ErrnoException) => void\n /** An error on the `::1` listener after it bound. Reported, never thrown. */\n onError: (err: NodeJS.ErrnoException) => void\n}\n\n// portless dials a route as `localhost` over 127.0.0.1 then ::1 and opens a fresh TCP connection per\n// request, so parallel e2e load exhausts the IPv4 ephemeral ports (TIME_WAIT) and the refused ::1\n// fallback becomes a 502. A ::1 listener gives that fallback its own port space; still loopback-only.\n/**\n * Once `server` listens on `127.0.0.1`, also accept on `[::1]` at the same port and hand every socket to\n * it; any other bind is left alone. Resolves to the `::1` listener once bound, `undefined` when skipped\n * or the bind failed. Closing `server` closes the listener.\n */\nexport const mirrorOnIpv6Loopback = (server: Server, handlers: MirrorHandlers): Promise<Server | undefined> => {\n return new Promise((resolve) => {\n const start = (): void => {\n const address = server.address()\n\n if (address === null || typeof address === 'string' || address.address !== IPV4_LOOPBACK) {\n resolve(undefined)\n\n return\n }\n\n const mirror = net.createServer((socket) => {\n server.emit('connection', socket)\n })\n\n mirror.on('error', (err: NodeJS.ErrnoException) => {\n if (mirror.listening) {\n handlers.onError(err)\n } else {\n handlers.onBindError(err)\n resolve(undefined)\n }\n })\n mirror.listen(address.port, IPV6_LOOPBACK, () => {\n resolve(mirror)\n })\n\n const close = server.close.bind(server)\n\n server.close = (callback) => {\n if (mirror.listening) mirror.close()\n\n return close(callback)\n }\n }\n\n if (server.listening) start()\n else server.once('listening', start)\n })\n}\n", "import type { InfraKitViteProxy } from '@slip-stream-kit/config/vite'\n\n/**\n * What `infraKitDev()` resolves: the ready-made vite `server` block. Re-declared structurally rather\n * than imported, because the helper types it inline on its return type and there is no named export\n * to reach for.\n */\nexport interface ResolvedDevServer {\n port?: number\n host?: string | boolean\n strictPort?: boolean\n /**\n * The alias websocket override. `@slip-stream-kit/config` <= 0.3.3 spells it `hmr`; later versions\n * spell it `ws`. This package depends on `^0.3.3`, which permits BOTH, so both are declared and\n * {@link mergeServerConfig} accepts either \u2014 reading only `ws` would silently wire HMR to nothing\n * for anyone resolving the older config, with no error to explain it.\n */\n ws?: { protocol: 'wss'; host: string; clientPort: number }\n /** Legacy spelling of {@link ws}, emitted by `@slip-stream-kit/config` <= 0.3.3. */\n hmr?: { protocol: 'wss'; host: string; clientPort: number }\n proxy: InfraKitViteProxy\n}\n\n/**\n * The subset of a consumer's `server` block this plugin can collide with. Structurally compatible with\n * vite's `ServerOptions`, so the plugin passes `config.server` straight in without a cast.\n */\nexport interface UserServerConfig {\n port?: number\n host?: string | boolean\n strictPort?: boolean\n /** Legacy alias of {@link ws}. Still declared because a consumer setting it must still win \u2014 see {@link mergeServerConfig}. */\n hmr?: unknown\n ws?: unknown\n proxy?: Record<string, unknown>\n}\n\n/** The `server` block this plugin contributes \u2014 every key optional, because every key is conditional. */\nexport interface InfraKitServerConfig {\n port?: number\n host?: string | boolean\n strictPort?: boolean\n ws?: ResolvedDevServer['ws']\n proxy?: InfraKitViteProxy\n}\n\n/** Drop every route the consumer declared themselves \u2014 an explicit route in their config outranks ours. */\nconst withoutUserRoutes = (\n proxy: InfraKitViteProxy,\n userProxy: Record<string, unknown> | undefined,\n): InfraKitViteProxy => {\n if (!userProxy) return proxy\n\n const result: InfraKitViteProxy = {}\n\n for (const [routePath, entry] of Object.entries(proxy)) {\n if (routePath in userProxy) continue\n result[routePath] = entry\n }\n\n return result\n}\n\n/**\n * The `server` block to return from the `config` hook, given what `infraKitDev()` resolved and what the\n * consumer wrote themselves.\n *\n * This exists because of ONE fact about vite: a `config` hook's result is merged **over** the user config\n * (`mergeConfig(userConfig, pluginResult)`), so anything emitted here WINS against an explicit setting in\n * the consumer's own `vite.config.ts`. A plugin that just returns everything `infraKitDev()` produced\n * would therefore silently overrule a hand-pinned `server.port` \u2014 the exact opposite of what a plugin\n * should do, and worse than the `server: await infraKitDev()` spread it replaces (there, at least, the\n * consumer could see the assignment).\n *\n * So: emit only what the consumer left unset. `strictPort` rides with `port` \u2014 it is meaningless applied\n * to someone else's port, and forcing it onto a hand-pinned one would turn a soft collision into a hard\n * boot failure.\n */\nexport const mergeServerConfig = (\n resolved: ResolvedDevServer,\n user: UserServerConfig | undefined,\n): InfraKitServerConfig => {\n const server: InfraKitServerConfig = {}\n\n if (user?.port == null) {\n if (resolved.port != null) server.port = resolved.port\n if (user?.strictPort == null && resolved.strictPort != null) server.strictPort = resolved.strictPort\n }\n\n if (user?.host == null && resolved.host != null) server.host = resolved.host\n // Guard on BOTH keys, not just `ws`. Vite 8 back-fills a legacy `server.hmr` onto `server.ws` with\n // `??=`, so ours would already be set by the time the compat shim ran: a consumer who hand-wrote\n // `server.hmr` would silently lose to us, which is the exact failure this whole module exists to\n // prevent. Their explicit setting outranks ours whichever spelling they used.\n // Always CONTRIBUTE as `ws` (vite 8 deprecated `server.hmr.*`), but ACCEPT either spelling from the\n // config package \u2014 see {@link ResolvedDevServer.ws}. Translating here means a consumer still on\n // config <= 0.3.3 stops getting the deprecation warning as soon as this package is published, with\n // no lockstep upgrade required.\n const resolvedWs = resolved.ws ?? resolved.hmr\n\n if (user?.ws == null && user?.hmr == null && resolvedWs != null) server.ws = resolvedWs\n\n const proxy = withoutUserRoutes(resolved.proxy, user?.proxy)\n\n if (Object.keys(proxy).length > 0) server.proxy = proxy\n\n return server\n}\n\n/**\n * True when the consumer pinned `server.port` while `infra-kit dev` had already ASSIGNED this UI a port\n * (`strictPort` on the resolved block is the runner's fingerprint \u2014 only the Layer-B path sets it).\n *\n * Honouring the pin is still right (it is explicit), but it is not free: the runner registered a portless\n * alias pointing at the port it assigned, and the UI is about to bind a different one. The hero URL then\n * 502s, and nothing in that failure names the pinned port as the cause. Worth one warning line.\n */\nexport const hasPinnedPortConflict = (resolved: ResolvedDevServer, user: UserServerConfig | undefined): boolean => {\n return user?.port != null && resolved.strictPort === true && user.port !== resolved.port\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["infraKitDev", "infraKitDev"]
7
7
  }
@@ -1,3 +1,4 @@
1
+ import type { InfraKitDevEnv } from '@slip-stream-kit/config';
1
2
  import type { InfraKitDevOptions, InfraKitViteProxy } from '@slip-stream-kit/config/vite';
2
3
  /** The paths whose changes can alter this package's proxy map. */
3
4
  export interface WatchTargets {
@@ -20,6 +21,13 @@ export interface WatchTargets {
20
21
  export declare const resolveWatchTargets: (cwd: string) => WatchTargets;
21
22
  /** Order-independent, field-stable digest of a resolved proxy map. */
22
23
  export declare const proxySignature: (proxy: InfraKitViteProxy) => string;
24
+ /**
25
+ * What a restart would change: the proxy map and the package's `dev.env`, which only a fresh config hook
26
+ * applies before vite's `loadEnv`.
27
+ */
28
+ export declare const restartSignature: (proxy: InfraKitViteProxy, devEnv: InfraKitDevEnv | undefined) => string;
29
+ /** The {@link restartSignature} of what the package's config resolves to now. */
30
+ export declare const resolveRestartSignature: (cwd: string, proxy: InfraKitViteProxy) => Promise<string>;
23
31
  /**
24
32
  * The slice of `ViteDevServer` this module needs. Narrowed to an interface so the watch loop is
25
33
  * driveable by a fake in tests — booting a real vite dev server to assert "it restarted once" would
@@ -39,7 +47,7 @@ export interface WatchDevContextArgs {
39
47
  cwd: string;
40
48
  /** The options the plugin was constructed with, re-applied on every re-resolve. */
41
49
  options: InfraKitDevOptions;
42
- /** The signature of the proxy map vite is currently serving (taken in the `config` hook). */
50
+ /** The {@link restartSignature} vite is currently serving (taken in the `config` hook). */
43
51
  current: string;
44
52
  }
45
53
  /**
@@ -0,0 +1,13 @@
1
+ import type { Server } from 'node:net';
2
+ export interface MirrorHandlers {
3
+ /** The `::1` bind failed (no IPv6, port taken). The IPv4 listener keeps serving either way. */
4
+ onBindError: (err: NodeJS.ErrnoException) => void;
5
+ /** An error on the `::1` listener after it bound. Reported, never thrown. */
6
+ onError: (err: NodeJS.ErrnoException) => void;
7
+ }
8
+ /**
9
+ * Once `server` listens on `127.0.0.1`, also accept on `[::1]` at the same port and hand every socket to
10
+ * it; any other bind is left alone. Resolves to the `::1` listener once bound, `undefined` when skipped
11
+ * or the bind failed. Closing `server` closes the listener.
12
+ */
13
+ export declare const mirrorOnIpv6Loopback: (server: Server, handlers: MirrorHandlers) => Promise<Server | undefined>;
@@ -1,10 +1,10 @@
1
1
  import type { InfraKitDevOptions } from '@slip-stream-kit/config/vite';
2
2
  import type { Plugin } from 'vite';
3
3
  /**
4
- * Everything `infraKitDev()` accepts, minus `command` — the plugin knows vite's command without being
5
- * told (see {@link infraKit}), and accepting it would only let a consumer contradict it.
4
+ * Everything `infraKitDev()` accepts, minus `command` and `applyDevEnv` — the plugin knows vite's command and
5
+ * mode without being told (see {@link infraKit}), and accepting them would only let a consumer contradict it.
6
6
  */
7
- export interface InfraKitPluginOptions extends Omit<InfraKitDevOptions, 'command'> {
7
+ export interface InfraKitPluginOptions extends Omit<InfraKitDevOptions, 'command' | 'applyDevEnv'> {
8
8
  /**
9
9
  * Re-resolve the proxy and restart the dev server when the `.infra-kit/dev-context` fragments change,
10
10
  * so a backend started AFTER the frontend flips its route from `cloud` to `local` on its own. Default
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slip-stream-kit/vite",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ArthurSaenz/infra-kit.git",
@@ -37,7 +37,7 @@
37
37
  "node": ">=24.x"
38
38
  },
39
39
  "dependencies": {
40
- "@slip-stream-kit/config": "^0.14.0"
40
+ "@slip-stream-kit/config": "^0.16.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "vite": ">=8"
package/readme.md CHANGED
@@ -56,8 +56,8 @@ Everything [`infraKitDev`](https://www.npmjs.com/package/@slip-stream-kit/config
56
56
  | `restartOnDevContextChange` | `true` | Re-resolve the proxy and restart when the local dev set changes. `false` freezes the proxy at boot. |
57
57
  | `cwd` | `process.cwd()` | The package dir whose `infra-kit.config.ts` is loaded. |
58
58
  | `port` | dynamic | Pin the dev-server port. Overrides the runner's assignment (and will 502 the alias). |
59
- | `host` | `127.0.0.1` | Vite's own `localhost` default binds `[::1]` only, which the proxy cannot dial. |
60
- | `basicAuth` | from `E2E__BASIC_AUTH_*` | Credentials injected as an `Authorization` header on every route. |
59
+ | `host` | `127.0.0.1` | Vite's own `localhost` default binds `[::1]` only, which the proxy cannot dial. On `127.0.0.1` the plugin also accepts on `[::1]` at the same port, so portless's IPv6 fallback still connects when parallel e2e load uses up the IPv4 loopback ports. |
60
+ | `basicAuth` | from `E2E__BASIC_AUTH_USERNAME` / `E2E__BASIC_AUTH_PASSWORD` | Credentials injected as an `Authorization` header on every route. The same two names (double underscore) are the only ones an e2e suite may read — `infra-kit audit` fails any other `E2E_*BASIC_AUTH_*_USERNAME` / `_PASSWORD` name. |
61
61
 
62
62
  ## Versioning
63
63