@slip-stream-kit/vite 0.23.0 → 0.27.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 +104 -0
- package/dist/index.js.map +3 -3
- package/dist/lib/loopback-guard/loopback-guard.d.ts +25 -0
- package/package.json +2 -2
- package/readme.md +8 -0
package/dist/index.js
CHANGED
|
@@ -133,6 +133,84 @@ var mirrorOnIpv6Loopback = (server, handlers) => {
|
|
|
133
133
|
});
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
// src/lib/loopback-guard/loopback-guard.ts
|
|
137
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
138
|
+
var ALLOW_PARAM = "infra-kit-loopback";
|
|
139
|
+
var ALLOW_COOKIE = `${ALLOW_PARAM}=allow`;
|
|
140
|
+
var loopbackHostname = (host) => {
|
|
141
|
+
if (!host) return null;
|
|
142
|
+
try {
|
|
143
|
+
const { hostname } = new URL(`http://${host}`);
|
|
144
|
+
return LOOPBACK_HOSTS.has(hostname) ? hostname : null;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var isLoopbackNavigation = (req) => {
|
|
150
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
151
|
+
if (!req.headers.accept?.includes("text/html")) return false;
|
|
152
|
+
return loopbackHostname(req.headers.host) !== null;
|
|
153
|
+
};
|
|
154
|
+
var hasAllowCookie = (req) => {
|
|
155
|
+
return (req.headers.cookie ?? "").split(";").some((part) => {
|
|
156
|
+
return part.trim() === ALLOW_COOKIE;
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
var redirect = (res, location) => {
|
|
160
|
+
res.statusCode = 302;
|
|
161
|
+
res.setHeader("Location", location);
|
|
162
|
+
res.end();
|
|
163
|
+
};
|
|
164
|
+
var EXPLAIN_PAGE = `<!doctype html>
|
|
165
|
+
<html lang="en">
|
|
166
|
+
<head>
|
|
167
|
+
<meta charset="utf-8">
|
|
168
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
169
|
+
<title>Open this app through infra-kit dev</title>
|
|
170
|
+
<style>
|
|
171
|
+
body { font: 16px/1.5 system-ui, sans-serif; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; color: #1f2328; }
|
|
172
|
+
code { background: #eff1f3; padding: 0.1em 0.35em; border-radius: 4px; }
|
|
173
|
+
a { color: #0969da; }
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<h1>This dev server was started outside <code>infra-kit dev</code></h1>
|
|
178
|
+
<p>It has no portless alias, so it runs on a random loopback port: the proxy does not follow your local
|
|
179
|
+
backends, cookies and CORS behave differently than on <code>https://<app>.localhost</code>, and the
|
|
180
|
+
page is not what e2e tests see.</p>
|
|
181
|
+
<p>Stop it and start the app from the repository root with <code>ik dev <app></code>, then open the
|
|
182
|
+
<code>https://\u2026localhost</code> URL it prints.</p>
|
|
183
|
+
<p><a href="?${ALLOW_PARAM}=allow">Continue on this address anyway</a></p>
|
|
184
|
+
</body>
|
|
185
|
+
</html>
|
|
186
|
+
`;
|
|
187
|
+
var createLoopbackGuard = (mode) => {
|
|
188
|
+
return (req, res, next) => {
|
|
189
|
+
if (!isLoopbackNavigation(req)) {
|
|
190
|
+
next();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const url = new URL(req.url ?? "/", "https://loopback");
|
|
194
|
+
if (mode.kind === "redirect") {
|
|
195
|
+
redirect(res, `https://${mode.alias}${url.pathname}${url.search}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (url.searchParams.get(ALLOW_PARAM) === "allow") {
|
|
199
|
+
url.searchParams.delete(ALLOW_PARAM);
|
|
200
|
+
res.setHeader("Set-Cookie", `${ALLOW_COOKIE}; Path=/; SameSite=Lax`);
|
|
201
|
+
redirect(res, `${url.pathname}${url.search}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (hasAllowCookie(req)) {
|
|
205
|
+
next();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
res.statusCode = 421;
|
|
209
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
210
|
+
res.end(req.method === "HEAD" ? void 0 : EXPLAIN_PAGE);
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
136
214
|
// src/lib/server-config/server-config.ts
|
|
137
215
|
var withoutUserRoutes = (proxy, userProxy) => {
|
|
138
216
|
if (!userProxy) return proxy;
|
|
@@ -181,10 +259,32 @@ var asRestartable = (server) => {
|
|
|
181
259
|
}
|
|
182
260
|
};
|
|
183
261
|
};
|
|
262
|
+
var resolveGuardMode = (resolved) => {
|
|
263
|
+
const alias = (resolved.ws ?? resolved.hmr)?.host;
|
|
264
|
+
if (alias) return { kind: "redirect", alias };
|
|
265
|
+
if (resolved.strictPort !== true) return { kind: "explain" };
|
|
266
|
+
return null;
|
|
267
|
+
};
|
|
268
|
+
var installLoopbackGuard = (server, mode) => {
|
|
269
|
+
server.middlewares.use(createLoopbackGuard(mode));
|
|
270
|
+
const { logger } = server.config;
|
|
271
|
+
const printUrls = server.printUrls.bind(server);
|
|
272
|
+
server.printUrls = () => {
|
|
273
|
+
printUrls();
|
|
274
|
+
if (mode.kind === "redirect") {
|
|
275
|
+
logger.info(` \u279C Alias: https://${mode.alias}/ \u2014 open this one; loopback page loads redirect here`);
|
|
276
|
+
} else {
|
|
277
|
+
logger.warn(
|
|
278
|
+
"[infra-kit] this dev server was started outside `infra-kit dev`: no portless alias, so the proxy, cookies and HMR differ from the real setup. Run `ik dev <app>` from the repo root instead."
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
184
283
|
var infraKit = (options = {}) => {
|
|
185
284
|
const cwd = options.cwd ?? process.cwd();
|
|
186
285
|
let signature = "";
|
|
187
286
|
let pinnedPortConflict = false;
|
|
287
|
+
let guardMode = null;
|
|
188
288
|
let dispose = () => {
|
|
189
289
|
};
|
|
190
290
|
return {
|
|
@@ -195,6 +295,7 @@ var infraKit = (options = {}) => {
|
|
|
195
295
|
const resolved = await infraKitDev2({ ...options, cwd, command: "serve", applyDevEnv: mode !== "test" });
|
|
196
296
|
signature = await resolveRestartSignature(cwd, resolved.proxy);
|
|
197
297
|
pinnedPortConflict = hasPinnedPortConflict(resolved, userConfig.server);
|
|
298
|
+
guardMode = mode === "test" ? null : resolveGuardMode(resolved);
|
|
198
299
|
return { server: mergeServerConfig(resolved, userConfig.server) };
|
|
199
300
|
},
|
|
200
301
|
configureServer: (server) => {
|
|
@@ -203,6 +304,9 @@ var infraKit = (options = {}) => {
|
|
|
203
304
|
"[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."
|
|
204
305
|
);
|
|
205
306
|
}
|
|
307
|
+
if (server.httpServer && guardMode) {
|
|
308
|
+
installLoopbackGuard(server, guardMode);
|
|
309
|
+
}
|
|
206
310
|
if (server.httpServer) {
|
|
207
311
|
const { logger } = server.config;
|
|
208
312
|
void mirrorOnIpv6Loopback(server.httpServer, {
|
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/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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
|
|
3
|
+
"sources": ["../src/lib/plugin/plugin.ts", "../src/lib/dev-context/dev-context.ts", "../src/lib/ipv6-loopback/ipv6-loopback.ts", "../src/lib/loopback-guard/loopback-guard.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 type { LoopbackGuardMode } from '../loopback-guard/loopback-guard'\nimport { createLoopbackGuard } from '../loopback-guard/loopback-guard'\nimport type { ResolvedDevServer } from '../server-config/server-config'\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 * How to treat a page opened on the raw loopback address. The runner's alias wins when it handed one over;\n * no `strictPort` means nothing placed this UI, i.e. a bare `vite` outside `infra-kit dev`. A runner that\n * placed the UI without an alias leaves loopback as the only address, so it gets no guard.\n */\nconst resolveGuardMode = (resolved: ResolvedDevServer): LoopbackGuardMode | null => {\n const alias = (resolved.ws ?? resolved.hmr)?.host\n\n if (alias) return { kind: 'redirect', alias }\n if (resolved.strictPort !== true) return { kind: 'explain' }\n\n return null\n}\n\nconst installLoopbackGuard = (server: ViteDevServer, mode: LoopbackGuardMode): void => {\n server.middlewares.use(createLoopbackGuard(mode))\n\n const { logger } = server.config\n const printUrls = server.printUrls.bind(server)\n\n server.printUrls = () => {\n printUrls()\n\n if (mode.kind === 'redirect') {\n logger.info(` \u279C Alias: https://${mode.alias}/ \u2014 open this one; loopback page loads redirect here`)\n } else {\n logger.warn(\n '[infra-kit] this dev server was started outside `infra-kit dev`: no portless alias, so the proxy, ' +\n 'cookies and HMR differ from the real setup. Run `ik dev <app>` from the repo root instead.',\n )\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 guardMode: LoopbackGuardMode | null = null\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 guardMode = mode === 'test' ? null : resolveGuardMode(resolved)\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 // No `httpServer` is middleware mode (Storybook's vite builder): someone else owns the address.\n if (server.httpServer && guardMode) {\n installLoopbackGuard(server, guardMode)\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 { IncomingMessage, ServerResponse } from 'node:http'\n\n/**\n * What the guard does with a page opened on a loopback address: send it to the portless alias the runner\n * registered, or explain that this dev server was started outside `infra-kit dev` and has no alias at all.\n */\nexport type LoopbackGuardMode = { kind: 'redirect'; alias: string } | { kind: 'explain' }\n\ntype Next = (err?: unknown) => void\n\n// `[::1]` keeps its brackets: it is compared against `URL.hostname`, which brackets an IPv6 literal.\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]'])\nconst ALLOW_PARAM = 'infra-kit-loopback'\nconst ALLOW_COOKIE = `${ALLOW_PARAM}=allow`\n\nconst loopbackHostname = (host: string | undefined): string | null => {\n if (!host) return null\n\n try {\n const { hostname } = new URL(`http://${host}`)\n\n return LOOPBACK_HOSTS.has(hostname) ? hostname : null\n } catch {\n return null\n }\n}\n\n/**\n * A browser opening a page on `127.0.0.1` / `localhost` / `[::1]`. Assets, HMR, and the runner's health\n * probe (`Accept: text/x-vite-ping`) are not navigations and pass untouched. portless keeps the browser's\n * `Host` (the `*.localhost` alias) when it forwards, so a request that came through the alias never matches.\n */\nexport const isLoopbackNavigation = (req: IncomingMessage): boolean => {\n if (req.method !== 'GET' && req.method !== 'HEAD') return false\n if (!req.headers.accept?.includes('text/html')) return false\n\n return loopbackHostname(req.headers.host) !== null\n}\n\nconst hasAllowCookie = (req: IncomingMessage): boolean => {\n return (req.headers.cookie ?? '').split(';').some((part) => {\n return part.trim() === ALLOW_COOKIE\n })\n}\n\nconst redirect = (res: ServerResponse, location: string): void => {\n res.statusCode = 302\n res.setHeader('Location', location)\n res.end()\n}\n\nconst EXPLAIN_PAGE = `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Open this app through infra-kit dev</title>\n<style>\n body { font: 16px/1.5 system-ui, sans-serif; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; color: #1f2328; }\n code { background: #eff1f3; padding: 0.1em 0.35em; border-radius: 4px; }\n a { color: #0969da; }\n</style>\n</head>\n<body>\n<h1>This dev server was started outside <code>infra-kit dev</code></h1>\n<p>It has no portless alias, so it runs on a random loopback port: the proxy does not follow your local\nbackends, cookies and CORS behave differently than on <code>https://<app>.localhost</code>, and the\npage is not what e2e tests see.</p>\n<p>Stop it and start the app from the repository root with <code>ik dev <app></code>, then open the\n<code>https://\u2026localhost</code> URL it prints.</p>\n<p><a href=\"?${ALLOW_PARAM}=allow\">Continue on this address anyway</a></p>\n</body>\n</html>\n`\n\n/**\n * Connect middleware that keeps people off the raw loopback address of a dev server. In `redirect` mode a\n * loopback page load is sent to the same path on the alias. In `explain` mode it gets a page saying how to\n * start the app properly, with a link that sets a cookie and lets this browser through from then on.\n */\nexport const createLoopbackGuard = (mode: LoopbackGuardMode) => {\n return (req: IncomingMessage, res: ServerResponse, next: Next): void => {\n if (!isLoopbackNavigation(req)) {\n next()\n\n return\n }\n\n const url = new URL(req.url ?? '/', 'https://loopback')\n\n if (mode.kind === 'redirect') {\n redirect(res, `https://${mode.alias}${url.pathname}${url.search}`)\n\n return\n }\n\n if (url.searchParams.get(ALLOW_PARAM) === 'allow') {\n url.searchParams.delete(ALLOW_PARAM)\n res.setHeader('Set-Cookie', `${ALLOW_COOKIE}; Path=/; SameSite=Lax`)\n redirect(res, `${url.pathname}${url.search}`)\n\n return\n }\n\n if (hasAllowCookie(req)) {\n next()\n\n return\n }\n\n // 421 Misdirected Request: the server is fine, the address used to reach it is the problem.\n res.statusCode = 421\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(req.method === 'HEAD' ? undefined : EXPLAIN_PAGE)\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
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* What the guard does with a page opened on a loopback address: send it to the portless alias the runner
|
|
4
|
+
* registered, or explain that this dev server was started outside `infra-kit dev` and has no alias at all.
|
|
5
|
+
*/
|
|
6
|
+
export type LoopbackGuardMode = {
|
|
7
|
+
kind: 'redirect';
|
|
8
|
+
alias: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'explain';
|
|
11
|
+
};
|
|
12
|
+
type Next = (err?: unknown) => void;
|
|
13
|
+
/**
|
|
14
|
+
* A browser opening a page on `127.0.0.1` / `localhost` / `[::1]`. Assets, HMR, and the runner's health
|
|
15
|
+
* probe (`Accept: text/x-vite-ping`) are not navigations and pass untouched. portless keeps the browser's
|
|
16
|
+
* `Host` (the `*.localhost` alias) when it forwards, so a request that came through the alias never matches.
|
|
17
|
+
*/
|
|
18
|
+
export declare const isLoopbackNavigation: (req: IncomingMessage) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Connect middleware that keeps people off the raw loopback address of a dev server. In `redirect` mode a
|
|
21
|
+
* loopback page load is sent to the same path on the alias. In `explain` mode it gets a page saying how to
|
|
22
|
+
* start the app properly, with a link that sets a cookie and lets this browser through from then on.
|
|
23
|
+
*/
|
|
24
|
+
export declare const createLoopbackGuard: (mode: LoopbackGuardMode) => (req: IncomingMessage, res: ServerResponse, next: Next) => void;
|
|
25
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slip-stream-kit/vite",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.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.
|
|
40
|
+
"@slip-stream-kit/config": "^0.27.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"vite": ">=8"
|
package/readme.md
CHANGED
|
@@ -36,6 +36,14 @@ its route from `cloud` to `local`; the answer was already baked, and you restart
|
|
|
36
36
|
watches `.infra-kit/dev-context/` and restarts only when the resolved proxy actually changed. A backend
|
|
37
37
|
that dies demotes its route back to `cloud` the same way.
|
|
38
38
|
|
|
39
|
+
**A guard against the raw loopback address.** The alias is the only address an app is meant to be opened on;
|
|
40
|
+
`127.0.0.1:<port>` gives the page a different origin, so cookies, CORS and OAuth redirects behave unlike
|
|
41
|
+
the real setup. Under `infra-kit dev`, a page opened on `127.0.0.1`, `localhost` or `[::1]` is redirected to
|
|
42
|
+
the same path on the alias, and vite's URL list prints the alias under its own `Local:` line. A bare `vite`
|
|
43
|
+
that nothing placed has no alias, so it answers a loopback page load with a page explaining how to start
|
|
44
|
+
the app through `ik dev <app>`, plus a link that lets that browser continue anyway. Only page loads are
|
|
45
|
+
touched: assets, HMR, the runner's health probe, vitest and middleware mode (Storybook) pass through.
|
|
46
|
+
|
|
39
47
|
**No `command` to thread.** The plugin declares `apply: 'serve'`, and vite filters plugins by `apply`
|
|
40
48
|
*before* it runs their `config` hooks — so on `build` this plugin does not exist. Forgetting to pass
|
|
41
49
|
`{ command }` (which made a build fail-fast on a cloud route with no sourced env) is no longer possible,
|