@spotpatch/next 0.1.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/cli.js ADDED
@@ -0,0 +1,1527 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createRequire as createRequire2 } from "module";
5
+ import path4 from "path";
6
+
7
+ // src/cli-owner.ts
8
+ import { spawn } from "child_process";
9
+ import { randomBytes } from "crypto";
10
+ import { serializeResolvedSpotPatchOptions } from "@spotpatch/dev-server";
11
+
12
+ // src/cli-args.ts
13
+ import { isLoopbackHostname } from "@spotpatch/dev-server";
14
+ function parsePort(value) {
15
+ const port = Number(value);
16
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(port) || port < 1 || port > 65535) {
17
+ throw new RangeError("SpotPatch Next requires a valid development port.");
18
+ }
19
+ return port;
20
+ }
21
+ function parseOptionValue(arguments_, index, longName, shortName) {
22
+ const argument = arguments_[index];
23
+ if (argument === longName || argument === shortName) {
24
+ const value = arguments_[index + 1];
25
+ if (value === void 0 || value.startsWith("-")) {
26
+ throw new Error(`SpotPatch Next requires a value after ${argument}.`);
27
+ }
28
+ return Object.freeze({ consumed: 1, value });
29
+ }
30
+ const longPrefix = `${longName}=`;
31
+ const shortPrefix = `${shortName}=`;
32
+ if (argument?.startsWith(longPrefix)) {
33
+ return Object.freeze({ consumed: 0, value: argument.slice(longPrefix.length) });
34
+ }
35
+ if (argument?.startsWith(shortPrefix)) {
36
+ return Object.freeze({ consumed: 0, value: argument.slice(shortPrefix.length) });
37
+ }
38
+ return Object.freeze({ consumed: 0 });
39
+ }
40
+ function parseNextDevArguments(arguments_, environment = process.env, defaultBundler2 = "turbopack") {
41
+ let hostname;
42
+ let port;
43
+ let webpack = false;
44
+ let turbopack = false;
45
+ for (let index = 0; index < arguments_.length; index += 1) {
46
+ const argument = arguments_[index];
47
+ const hostOption = parseOptionValue(arguments_, index, "--hostname", "-H");
48
+ if (hostOption.value !== void 0) {
49
+ if (hostname !== void 0) {
50
+ throw new Error("SpotPatch Next received duplicate hostname options.");
51
+ }
52
+ hostname = hostOption.value;
53
+ index += hostOption.consumed;
54
+ continue;
55
+ }
56
+ const portOption = parseOptionValue(arguments_, index, "--port", "-p");
57
+ if (portOption.value !== void 0) {
58
+ if (port !== void 0) {
59
+ throw new Error("SpotPatch Next received duplicate port options.");
60
+ }
61
+ port = parsePort(portOption.value);
62
+ index += portOption.consumed;
63
+ continue;
64
+ }
65
+ if (argument === "--webpack") {
66
+ webpack = true;
67
+ } else if (argument === "--turbopack" || argument === "--turbo") {
68
+ turbopack = true;
69
+ }
70
+ }
71
+ const resolvedHostname = hostname ?? "localhost";
72
+ if (!isLoopbackHostname(resolvedHostname)) {
73
+ throw new Error(
74
+ "SpotPatch Next only supports a loopback --hostname in its first release."
75
+ );
76
+ }
77
+ if (webpack && turbopack) {
78
+ throw new Error("SpotPatch Next cannot enable webpack and Turbopack together.");
79
+ }
80
+ const resolvedPort = port ?? (environment.PORT === void 0 ? 3e3 : parsePort(environment.PORT));
81
+ const hostForUrl = resolvedHostname.includes(":") ? `[${resolvedHostname}]` : resolvedHostname;
82
+ const nextArguments = [...arguments_];
83
+ if (hostname === void 0) {
84
+ nextArguments.push("--hostname", resolvedHostname);
85
+ }
86
+ if (port === void 0) {
87
+ nextArguments.push("--port", String(resolvedPort));
88
+ }
89
+ return Object.freeze({
90
+ bundler: webpack ? "webpack" : turbopack ? "turbopack" : defaultBundler2,
91
+ hostname: resolvedHostname,
92
+ nextArguments: Object.freeze(nextArguments),
93
+ port: resolvedPort,
94
+ publicOrigin: `http://${hostForUrl}:${String(resolvedPort)}`
95
+ });
96
+ }
97
+
98
+ // src/internal/constants.ts
99
+ var NEXT_IPC_PROTOCOL_VERSION = 1;
100
+ var NEXT_IPC_MESSAGE_LIMIT_BYTES = 256 * 1024;
101
+ var NEXT_INTERNAL_REGISTRATION_PATH = "/__spotpatch-internal/register";
102
+ var NEXT_INTERNAL_CONFIGURATION_PATH = "/__spotpatch-internal/configure";
103
+ var NEXT_ENVIRONMENT_KEYS = Object.freeze({
104
+ appRoot: "SPOTPATCH_NEXT_APP_ROOT",
105
+ bundler: "SPOTPATCH_NEXT_BUNDLER",
106
+ configurationSecret: "SPOTPATCH_NEXT_CONFIGURATION_SECRET",
107
+ internalOrigin: "SPOTPATCH_NEXT_INTERNAL_ORIGIN",
108
+ internalSecret: "SPOTPATCH_NEXT_INTERNAL_SECRET",
109
+ launchNonce: "SPOTPATCH_NEXT_LAUNCH_NONCE",
110
+ registryEpoch: "SPOTPATCH_NEXT_REGISTRY_EPOCH",
111
+ sidecarOrigin: "SPOTPATCH_NEXT_SIDECAR_ORIGIN"
112
+ });
113
+ var NEXT_SOURCE_RULE_KEYS = Object.freeze(["*.jsx", "*.tsx"]);
114
+ var NEXT_DEFAULT_INCLUDE = Object.freeze([/\.(?:jsx|tsx)$/u]);
115
+
116
+ // src/internal/ipc.ts
117
+ import path from "path";
118
+ import {
119
+ parseSerializedSpotPatchOptions
120
+ } from "@spotpatch/dev-server";
121
+ var ID_PATTERN = /^[A-Za-z0-9_-]{16,128}$/u;
122
+ var ENVIRONMENT_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/u;
123
+ function isRecord(value) {
124
+ return typeof value === "object" && value !== null && !Array.isArray(value);
125
+ }
126
+ function hasExactKeys(value, expectedKeys) {
127
+ const actual = Object.keys(value).sort();
128
+ const expected = [...expectedKeys].sort();
129
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
130
+ }
131
+ function assertIpcMessageSize(value) {
132
+ let serialized;
133
+ try {
134
+ serialized = JSON.stringify(value);
135
+ } catch (error) {
136
+ throw new TypeError("The SpotPatch IPC message is not serializable.", {
137
+ cause: error
138
+ });
139
+ }
140
+ if (Buffer.byteLength(serialized, "utf8") > NEXT_IPC_MESSAGE_LIMIT_BYTES) {
141
+ throw new RangeError("The SpotPatch IPC message exceeds the size limit.");
142
+ }
143
+ }
144
+ function parseCredentials(value) {
145
+ if (!isRecord(value) || Object.keys(value).length > 32) {
146
+ throw new TypeError("The SpotPatch IPC credentials are invalid.");
147
+ }
148
+ const entries = Object.entries(value);
149
+ const credentials = {};
150
+ for (const [name, credential] of entries) {
151
+ if (!ENVIRONMENT_NAME_PATTERN.test(name) || typeof credential !== "string" || credential.length === 0 || credential.length > 16384 || credential.includes("\0")) {
152
+ throw new TypeError("The SpotPatch IPC credentials are invalid.");
153
+ }
154
+ credentials[name] = credential;
155
+ }
156
+ return Object.freeze(credentials);
157
+ }
158
+ function parseNextConfigureMessage(value) {
159
+ assertIpcMessageSize(value);
160
+ if (!isRecord(value) || !hasExactKeys(value, [
161
+ "appRoot",
162
+ "credentials",
163
+ "nonce",
164
+ "options",
165
+ "protocolVersion",
166
+ "requestId",
167
+ "type"
168
+ ]) || value.type !== "spotpatch:next:configure" || value.protocolVersion !== NEXT_IPC_PROTOCOL_VERSION || typeof value.nonce !== "string" || !ID_PATTERN.test(value.nonce) || typeof value.requestId !== "string" || !ID_PATTERN.test(value.requestId) || typeof value.appRoot !== "string" || !path.isAbsolute(value.appRoot) || value.appRoot.length > 4096 || value.appRoot.includes("\0")) {
169
+ throw new TypeError("The SpotPatch configure IPC message is invalid.");
170
+ }
171
+ return Object.freeze({
172
+ appRoot: value.appRoot,
173
+ credentials: parseCredentials(value.credentials),
174
+ nonce: value.nonce,
175
+ options: parseSerializedSpotPatchOptions(value.options),
176
+ protocolVersion: NEXT_IPC_PROTOCOL_VERSION,
177
+ requestId: value.requestId,
178
+ type: "spotpatch:next:configure"
179
+ });
180
+ }
181
+
182
+ // src/project.ts
183
+ import { execFile } from "child_process";
184
+ import { access, readFile, realpath } from "fs/promises";
185
+ import { createRequire } from "module";
186
+ import path2 from "path";
187
+ import { promisify } from "util";
188
+ var execFileAsync = promisify(execFile);
189
+ var VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/u;
190
+ async function pathExists(absolutePath) {
191
+ try {
192
+ await access(absolutePath);
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+ function assertSupportedNextVersion(version) {
199
+ const match = VERSION_PATTERN.exec(version);
200
+ const major = Number(match?.[1]);
201
+ const minor = Number(match?.[2]);
202
+ if (match === null || !Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || major < 15 || major >= 17 || major === 15 && minor < 3) {
203
+ throw new Error(
204
+ `SpotPatch Next requires Next.js >=15.3.0 <17.0.0; found ${version}.`
205
+ );
206
+ }
207
+ }
208
+ async function detectRouterKind(appRoot) {
209
+ const [appCandidates, pagesCandidates] = await Promise.all([
210
+ Promise.all([
211
+ pathExists(path2.join(appRoot, "app")),
212
+ pathExists(path2.join(appRoot, "src", "app"))
213
+ ]),
214
+ Promise.all([
215
+ pathExists(path2.join(appRoot, "pages")),
216
+ pathExists(path2.join(appRoot, "src", "pages"))
217
+ ])
218
+ ]);
219
+ const hasApp = appCandidates.some(Boolean);
220
+ const hasPages = pagesCandidates.some(Boolean);
221
+ if (hasApp && hasPages) {
222
+ return "hybrid";
223
+ }
224
+ if (hasApp) {
225
+ return "app";
226
+ }
227
+ if (hasPages) {
228
+ return "pages";
229
+ }
230
+ throw new Error("SpotPatch could not find an App or Pages Router directory.");
231
+ }
232
+ async function findProjectRoot(appRoot) {
233
+ try {
234
+ const result = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
235
+ cwd: appRoot,
236
+ encoding: "utf8",
237
+ timeout: 5e3
238
+ });
239
+ const candidate = await realpath(result.stdout.trim());
240
+ const relative = path2.relative(candidate, appRoot);
241
+ if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
242
+ return candidate;
243
+ }
244
+ } catch {
245
+ }
246
+ return appRoot;
247
+ }
248
+ async function inspectNextProject(directory = process.cwd()) {
249
+ const appRoot = await realpath(directory);
250
+ const resolveFromApplication = createRequire(path2.join(appRoot, "package.json"));
251
+ let nextEntry;
252
+ let manifestPath;
253
+ try {
254
+ nextEntry = resolveFromApplication.resolve("next/dist/bin/next");
255
+ manifestPath = resolveFromApplication.resolve("next/package.json");
256
+ } catch (error) {
257
+ throw new Error("SpotPatch could not resolve the application's local Next.js.", {
258
+ cause: error
259
+ });
260
+ }
261
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
262
+ if (typeof manifest !== "object" || manifest === null || !("version" in manifest) || typeof manifest.version !== "string") {
263
+ throw new Error("SpotPatch could not read the local Next.js version.");
264
+ }
265
+ assertSupportedNextVersion(manifest.version);
266
+ const [projectRoot, routerKind] = await Promise.all([
267
+ findProjectRoot(appRoot),
268
+ detectRouterKind(appRoot)
269
+ ]);
270
+ return Object.freeze({
271
+ appRoot,
272
+ nextEntry,
273
+ nextVersion: manifest.version,
274
+ projectRoot,
275
+ routerKind
276
+ });
277
+ }
278
+
279
+ // src/sidecar.ts
280
+ import {
281
+ createServer
282
+ } from "http";
283
+ import {
284
+ createAgentJobManager,
285
+ createRuntimeAiConfig,
286
+ createSession,
287
+ createSourceRegistrationService,
288
+ createSourceRegistry,
289
+ createSpotPatchMiddleware
290
+ } from "@spotpatch/dev-server";
291
+ import {
292
+ SPOTPATCH_API_BASE,
293
+ SPOTPATCH_ENDPOINTS,
294
+ runtimeConfigSchema
295
+ } from "@spotpatch/shared";
296
+
297
+ // package.json
298
+ var package_default = {
299
+ name: "@spotpatch/next",
300
+ version: "0.1.0",
301
+ description: "Development-only Next.js adapter and CLI for SpotPatch.",
302
+ license: "MIT",
303
+ repository: {
304
+ type: "git",
305
+ url: "git+https://github.com/huanglvjing/spotpatch.git",
306
+ directory: "packages/next"
307
+ },
308
+ homepage: "https://github.com/huanglvjing/spotpatch#readme",
309
+ bugs: {
310
+ url: "https://github.com/huanglvjing/spotpatch/issues"
311
+ },
312
+ keywords: [
313
+ "spotpatch",
314
+ "nextjs",
315
+ "react",
316
+ "developer-tools"
317
+ ],
318
+ type: "module",
319
+ sideEffects: [
320
+ "./dist/client.cjs",
321
+ "./dist/client.js"
322
+ ],
323
+ engines: {
324
+ node: ">=20.19.0"
325
+ },
326
+ files: [
327
+ "dist",
328
+ "loader.cjs",
329
+ "loader.d.cts"
330
+ ],
331
+ main: "./dist/index.cjs",
332
+ module: "./dist/index.js",
333
+ types: "./dist/index.d.ts",
334
+ bin: {
335
+ "spotpatch-next": "./dist/cli.js"
336
+ },
337
+ exports: {
338
+ ".": {
339
+ import: {
340
+ types: "./dist/index.d.ts",
341
+ default: "./dist/index.js"
342
+ },
343
+ require: {
344
+ types: "./dist/index.d.cts",
345
+ default: "./dist/index.cjs"
346
+ }
347
+ },
348
+ "./client": {
349
+ import: {
350
+ types: "./dist/client.d.ts",
351
+ default: "./dist/client.js"
352
+ },
353
+ require: {
354
+ types: "./dist/client.d.cts",
355
+ default: "./dist/client.cjs"
356
+ }
357
+ },
358
+ "./loader": {
359
+ types: "./loader.d.cts",
360
+ default: "./loader.cjs"
361
+ },
362
+ "./noop": {
363
+ import: {
364
+ types: "./dist/noop.d.ts",
365
+ default: "./dist/noop.js"
366
+ },
367
+ require: {
368
+ types: "./dist/noop.d.cts",
369
+ default: "./dist/noop.cjs"
370
+ }
371
+ }
372
+ },
373
+ typesVersions: {
374
+ "*": {
375
+ client: [
376
+ "dist/client.d.ts"
377
+ ],
378
+ loader: [
379
+ "loader.d.cts"
380
+ ],
381
+ noop: [
382
+ "dist/noop.d.ts"
383
+ ]
384
+ }
385
+ },
386
+ scripts: {
387
+ build: "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean && tsup --config tsup.cli.config.ts && tsup --config tsup.loader.config.ts && tsup --config tsup.client.config.ts && pnpm verify:loader",
388
+ clean: `node --input-type=module -e "import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })"`,
389
+ typecheck: "tsc --noEmit -p tsconfig.json",
390
+ "verify:loader": "node scripts/verify-loader.cjs"
391
+ },
392
+ dependencies: {
393
+ "@spotpatch/compiler": "workspace:^",
394
+ "@spotpatch/dev-server": "workspace:^",
395
+ "@spotpatch/runtime": "workspace:^",
396
+ "@spotpatch/shared": "workspace:^",
397
+ bippy: "0.6.1",
398
+ "magic-string": "1.1.0",
399
+ "oxc-parser": "0.143.0"
400
+ },
401
+ peerDependencies: {
402
+ next: ">=15.3.0 <17.0.0",
403
+ react: "^18.2.0 || ^19.0.0",
404
+ "react-dom": "^18.2.0 || ^19.0.0"
405
+ },
406
+ devDependencies: {
407
+ next: "16.3.0",
408
+ react: "19.2.8",
409
+ "react-dom": "19.2.8"
410
+ },
411
+ publishConfig: {
412
+ access: "public",
413
+ registry: "https://registry.npmjs.org/"
414
+ }
415
+ };
416
+
417
+ // src/internal/configuration-server.ts
418
+ import { timingSafeEqual } from "crypto";
419
+ import { isLoopbackHostname as isLoopbackHostname2, readJsonRequestBody } from "@spotpatch/dev-server";
420
+ var CONFIGURATION_SECRET_HEADER = "x-spotpatch-configuration";
421
+ var CONFIGURATION_SECRET_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u;
422
+ function getSingleHeader(request, name) {
423
+ const value = request.headers[name.toLowerCase()];
424
+ return Array.isArray(value) ? value[0] : value;
425
+ }
426
+ function secretsMatch(actual, expected) {
427
+ if (actual === void 0) {
428
+ return false;
429
+ }
430
+ const actualBytes = Buffer.from(actual);
431
+ const expectedBytes = Buffer.from(expected);
432
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
433
+ }
434
+ function hasLoopbackHost(request) {
435
+ const host = getSingleHeader(request, "host");
436
+ if (host === void 0) {
437
+ return false;
438
+ }
439
+ try {
440
+ return isLoopbackHostname2(new URL(`http://${host}`).hostname);
441
+ } catch {
442
+ return false;
443
+ }
444
+ }
445
+ function writeJson(response, statusCode, payload) {
446
+ const body = JSON.stringify(payload);
447
+ response.statusCode = statusCode;
448
+ response.setHeader("Cache-Control", "no-store");
449
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
450
+ response.setHeader("Content-Length", Buffer.byteLength(body));
451
+ response.end(body);
452
+ }
453
+ function createConfigurationRequestHandler(options) {
454
+ if (!CONFIGURATION_SECRET_PATTERN.test(options.configurationSecret)) {
455
+ throw new TypeError("The SpotPatch configuration secret is invalid.");
456
+ }
457
+ return (request, response) => {
458
+ const handle = async () => {
459
+ const contentType = getSingleHeader(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
460
+ if (request.method !== "POST" || contentType !== "application/json" || !hasLoopbackHost(request) || getSingleHeader(request, "origin") !== void 0 || !secretsMatch(
461
+ getSingleHeader(request, CONFIGURATION_SECRET_HEADER),
462
+ options.configurationSecret
463
+ )) {
464
+ writeJson(response, 403, { ok: false });
465
+ return;
466
+ }
467
+ const value = await readJsonRequestBody(request, NEXT_IPC_MESSAGE_LIMIT_BYTES);
468
+ const acknowledgement = await options.onConfiguration(value);
469
+ if (acknowledgement === void 0) {
470
+ writeJson(response, 400, { ok: false });
471
+ return;
472
+ }
473
+ assertIpcMessageSize(acknowledgement);
474
+ writeJson(response, 200, acknowledgement);
475
+ };
476
+ void handle().catch(() => {
477
+ if (response.headersSent) {
478
+ response.destroy();
479
+ return;
480
+ }
481
+ writeJson(response, 400, { ok: false });
482
+ });
483
+ };
484
+ }
485
+
486
+ // src/sidecar.ts
487
+ var SIDECAR_SELF_CHECK_LIMIT_BYTES = 16384;
488
+ var SIDECAR_SELF_CHECK_TIMEOUT_MS = 3e3;
489
+ function requestPath(url) {
490
+ try {
491
+ return new URL(url ?? "/", "http://spotpatch.invalid").pathname;
492
+ } catch {
493
+ return "";
494
+ }
495
+ }
496
+ function writeUnavailable(response, statusCode) {
497
+ const body = JSON.stringify({ ok: false });
498
+ response.statusCode = statusCode;
499
+ response.setHeader("Cache-Control", "no-store");
500
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
501
+ response.setHeader("Content-Length", Buffer.byteLength(body));
502
+ response.end(body);
503
+ }
504
+ function validateCredentialEnvironment(options, credentials) {
505
+ const expected = options.ai === false ? [] : [
506
+ ...new Set(
507
+ Object.values(options.ai.providers).map((provider) => provider.apiKeyEnv)
508
+ )
509
+ ].sort();
510
+ const actual = Object.keys(credentials).sort();
511
+ if (actual.length !== expected.length || actual.some((name, index) => name !== expected[index])) {
512
+ throw new TypeError("The SpotPatch credential environment is inconsistent.");
513
+ }
514
+ }
515
+ async function selfCheckSidecar(sidecarOrigin, publicOrigin, expectedConfig) {
516
+ const response = await fetch(new URL(SPOTPATCH_ENDPOINTS.bootstrap, sidecarOrigin), {
517
+ method: "POST",
518
+ headers: {
519
+ "Content-Type": "application/json",
520
+ Origin: publicOrigin,
521
+ "Sec-Fetch-Site": "same-origin"
522
+ },
523
+ body: "{}",
524
+ signal: AbortSignal.timeout(SIDECAR_SELF_CHECK_TIMEOUT_MS)
525
+ });
526
+ const declaredLength = Number(response.headers.get("content-length"));
527
+ if (Number.isFinite(declaredLength) && declaredLength > SIDECAR_SELF_CHECK_LIMIT_BYTES) {
528
+ await response.body?.cancel();
529
+ throw new Error("SpotPatch Sidecar self-check failed.");
530
+ }
531
+ const text = await response.text();
532
+ if (!response.ok || !response.headers.get("cache-control")?.toLowerCase().includes("no-store") || Buffer.byteLength(text, "utf8") > SIDECAR_SELF_CHECK_LIMIT_BYTES) {
533
+ throw new Error("SpotPatch Sidecar self-check failed.");
534
+ }
535
+ let value;
536
+ try {
537
+ value = JSON.parse(text);
538
+ } catch (error) {
539
+ throw new Error("SpotPatch Sidecar self-check failed.", { cause: error });
540
+ }
541
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).length !== 2 || !("ok" in value) || value.ok !== true || !("data" in value)) {
542
+ throw new Error("SpotPatch Sidecar self-check failed.");
543
+ }
544
+ const parsed = runtimeConfigSchema.safeParse(value.data);
545
+ const parsedExpected = runtimeConfigSchema.safeParse(expectedConfig);
546
+ if (!parsed.success || !parsedExpected.success || JSON.stringify(parsed.data) !== JSON.stringify(parsedExpected.data)) {
547
+ throw new Error("SpotPatch Sidecar self-check failed.");
548
+ }
549
+ }
550
+ function closeServer(server) {
551
+ return new Promise((resolve, reject) => {
552
+ server.close((error) => {
553
+ if (error === void 0 || "code" in error && error.code === "ERR_SERVER_NOT_RUNNING") {
554
+ resolve();
555
+ return;
556
+ }
557
+ reject(error);
558
+ });
559
+ });
560
+ }
561
+ async function createNextSidecar(sidecarOptions) {
562
+ let active = false;
563
+ let closed = false;
564
+ let registry;
565
+ let agentManager;
566
+ let handler = (_request, response) => {
567
+ writeUnavailable(response, 503);
568
+ };
569
+ const configurationHandler = createConfigurationRequestHandler(
570
+ sidecarOptions.configuration
571
+ );
572
+ const server = createServer((request, response) => {
573
+ try {
574
+ if (requestPath(request.url) === NEXT_INTERNAL_CONFIGURATION_PATH) {
575
+ configurationHandler(request, response);
576
+ return;
577
+ }
578
+ handler(request, response);
579
+ } catch {
580
+ writeUnavailable(response, 500);
581
+ }
582
+ });
583
+ await new Promise((resolve, reject) => {
584
+ const onError = (error) => {
585
+ reject(error);
586
+ };
587
+ server.once("error", onError);
588
+ server.listen(0, "127.0.0.1", () => {
589
+ server.off("error", onError);
590
+ resolve();
591
+ });
592
+ });
593
+ server.on("error", () => {
594
+ if (!closed) {
595
+ sidecarOptions.onFatalError?.();
596
+ }
597
+ });
598
+ const address = server.address();
599
+ if (address === null || typeof address === "string") {
600
+ await closeServer(server);
601
+ throw new Error("SpotPatch Sidecar did not bind a loopback address.");
602
+ }
603
+ const sidecarOrigin = `http://127.0.0.1:${String(address.port)}`;
604
+ return Object.freeze({
605
+ origin: sidecarOrigin,
606
+ async activate(input) {
607
+ if (closed || active) {
608
+ throw new Error("SpotPatch Sidecar activation is not available.");
609
+ }
610
+ validateCredentialEnvironment(input.options, input.credentials);
611
+ if (!input.options.enabled) {
612
+ handler = (_request, response) => {
613
+ writeUnavailable(response, 404);
614
+ };
615
+ active = true;
616
+ return;
617
+ }
618
+ const sourceRegistry = createSourceRegistry();
619
+ const session = createSession();
620
+ const runtimeConfig = Object.freeze({
621
+ apiBase: SPOTPATCH_API_BASE,
622
+ ai: createRuntimeAiConfig(input.options.ai),
623
+ budget: input.options.budget,
624
+ bundler: input.bundler,
625
+ debug: input.options.debug,
626
+ editor: input.options.editor,
627
+ framework: "next",
628
+ frameworkVersion: input.nextVersion,
629
+ locale: input.options.locale,
630
+ maxTargets: input.options.maxTargets,
631
+ redact: input.options.redact,
632
+ routerKind: input.routerKind,
633
+ sessionToken: session.token,
634
+ shortcut: input.options.shortcut,
635
+ spotPatchVersion: package_default.version
636
+ });
637
+ const manager = input.options.ai === false ? void 0 : createAgentJobManager({
638
+ ai: input.options.ai,
639
+ environment: input.credentials,
640
+ root: input.projectRoot
641
+ });
642
+ const middleware = createSpotPatchMiddleware({
643
+ ...manager === void 0 ? {} : { agentManager: manager },
644
+ bootstrap: {
645
+ expectedOrigin: input.publicOrigin,
646
+ runtimeConfig
647
+ },
648
+ logger: {
649
+ warn(message) {
650
+ process.stderr.write(`${message}
651
+ `);
652
+ }
653
+ },
654
+ options: input.options,
655
+ registry: sourceRegistry,
656
+ root: input.appRoot,
657
+ session
658
+ });
659
+ const registration = await createSourceRegistrationService({
660
+ internalSecret: input.internalSecret,
661
+ options: input.options,
662
+ registry: sourceRegistry,
663
+ registryEpoch: input.registryEpoch,
664
+ root: input.appRoot
665
+ });
666
+ registry = sourceRegistry;
667
+ agentManager = manager;
668
+ handler = (request, response) => {
669
+ if (requestPath(request.url) === NEXT_INTERNAL_REGISTRATION_PATH) {
670
+ registration.handler(request, response);
671
+ return;
672
+ }
673
+ middleware(request, response, () => {
674
+ writeUnavailable(response, 404);
675
+ });
676
+ };
677
+ await selfCheckSidecar(sidecarOrigin, input.publicOrigin, runtimeConfig);
678
+ active = true;
679
+ },
680
+ async close() {
681
+ if (closed) {
682
+ return;
683
+ }
684
+ closed = true;
685
+ handler = (_request, response) => {
686
+ writeUnavailable(response, 503);
687
+ };
688
+ registry?.clear();
689
+ await agentManager?.close();
690
+ server.closeIdleConnections();
691
+ const closing = closeServer(server);
692
+ server.closeAllConnections();
693
+ await closing;
694
+ }
695
+ });
696
+ }
697
+
698
+ // src/cli-owner.ts
699
+ var CONFIGURATION_STARTUP_TIMEOUT_MS = 6e4;
700
+ var FORCED_TERMINATION_TIMEOUT_MS = 5e3;
701
+ var MAX_CONFIGURATION_REQUESTS = 32;
702
+ var ID_PATTERN2 = /^[A-Za-z0-9_-]{16,128}$/u;
703
+ function isRecord2(value) {
704
+ return typeof value === "object" && value !== null && !Array.isArray(value);
705
+ }
706
+ function defaultBundler(nextVersion) {
707
+ return Number(nextVersion.split(".", 1)[0]) >= 16 ? "turbopack" : "webpack";
708
+ }
709
+ function signalExitCode(signal) {
710
+ return signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
711
+ }
712
+ function sortedRecord(value) {
713
+ return Object.freeze(
714
+ Object.fromEntries(
715
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
716
+ )
717
+ );
718
+ }
719
+ function configurationSignature(message) {
720
+ return JSON.stringify({
721
+ appRoot: message.appRoot,
722
+ credentials: sortedRecord(message.credentials),
723
+ options: serializeResolvedSpotPatchOptions(message.options)
724
+ });
725
+ }
726
+ function readCorrelation(value, nonce) {
727
+ if (!isRecord2(value) || typeof value.nonce !== "string" || value.nonce !== nonce || !ID_PATTERN2.test(value.nonce) || typeof value.requestId !== "string" || !ID_PATTERN2.test(value.requestId)) {
728
+ return void 0;
729
+ }
730
+ return Object.freeze({ nonce, requestId: value.requestId });
731
+ }
732
+ function createAck(correlation, result) {
733
+ return Object.freeze({
734
+ ...result,
735
+ nonce: correlation.nonce,
736
+ protocolVersion: NEXT_IPC_PROTOCOL_VERSION,
737
+ requestId: correlation.requestId,
738
+ type: "spotpatch:next:configure-ack"
739
+ });
740
+ }
741
+ function waitForChild(child) {
742
+ return new Promise((resolve) => {
743
+ let settled = false;
744
+ const finish = (result) => {
745
+ if (settled) {
746
+ return;
747
+ }
748
+ settled = true;
749
+ resolve(Object.freeze(result));
750
+ };
751
+ child.once("error", () => {
752
+ finish({ code: null, error: true, signal: null });
753
+ });
754
+ child.once("exit", (code, signal) => {
755
+ finish({ code, error: false, signal });
756
+ });
757
+ });
758
+ }
759
+ async function closeSidecar(sidecar) {
760
+ let timer;
761
+ try {
762
+ await Promise.race([
763
+ sidecar.close(),
764
+ new Promise((_resolve, reject) => {
765
+ timer = setTimeout(() => {
766
+ reject(new Error("SpotPatch Sidecar shutdown timed out."));
767
+ }, FORCED_TERMINATION_TIMEOUT_MS);
768
+ timer.unref();
769
+ })
770
+ ]);
771
+ } finally {
772
+ if (timer !== void 0) {
773
+ clearTimeout(timer);
774
+ }
775
+ }
776
+ }
777
+ async function runNextDevelopment(arguments_) {
778
+ const project = await inspectNextProject();
779
+ const dev = parseNextDevArguments(
780
+ arguments_,
781
+ process.env,
782
+ defaultBundler(project.nextVersion)
783
+ );
784
+ const launchNonce = randomBytes(24).toString("base64url");
785
+ const configurationSecret = randomBytes(32).toString("base64url");
786
+ const internalSecret = randomBytes(32).toString("base64url");
787
+ const registryEpoch = randomBytes(24).toString("base64url");
788
+ const lifecycle = {
789
+ sidecarFailed: false
790
+ };
791
+ const seenRequestIds = /* @__PURE__ */ new Set();
792
+ let acceptedSignature;
793
+ let activationFailed = false;
794
+ let configured = false;
795
+ let failureCode;
796
+ let configurationQueue = Promise.resolve();
797
+ const handleConfiguration = async (value) => {
798
+ const correlation = readCorrelation(value, launchNonce);
799
+ let message;
800
+ try {
801
+ message = parseNextConfigureMessage(value);
802
+ } catch {
803
+ failureCode = "INVALID_IPC";
804
+ if (correlation !== void 0) {
805
+ lifecycle.child?.kill("SIGTERM");
806
+ return createAck(correlation, { code: "INVALID_IPC", ok: false });
807
+ }
808
+ lifecycle.child?.kill("SIGTERM");
809
+ return void 0;
810
+ }
811
+ if (message.nonce !== launchNonce || message.appRoot !== project.appRoot || message.options.allowLan || seenRequestIds.has(message.requestId) || seenRequestIds.size >= MAX_CONFIGURATION_REQUESTS) {
812
+ failureCode = "INVALID_IPC";
813
+ lifecycle.child?.kill("SIGTERM");
814
+ return createAck(message, { code: "INVALID_IPC", ok: false });
815
+ }
816
+ seenRequestIds.add(message.requestId);
817
+ const signature = configurationSignature(message);
818
+ if (acceptedSignature !== void 0 && acceptedSignature !== signature) {
819
+ failureCode = "CONFIGURATION_CONFLICT";
820
+ return createAck(message, {
821
+ code: "CONFIGURATION_CONFLICT",
822
+ ok: false
823
+ });
824
+ }
825
+ if (activationFailed) {
826
+ return createAck(message, { code: "CONFIGURATION_FAILED", ok: false });
827
+ }
828
+ if (acceptedSignature === void 0) {
829
+ acceptedSignature = signature;
830
+ const sidecar2 = lifecycle.sidecar;
831
+ if (sidecar2 === void 0) {
832
+ activationFailed = true;
833
+ failureCode = "CONFIGURATION_FAILED";
834
+ return createAck(message, { code: "CONFIGURATION_FAILED", ok: false });
835
+ }
836
+ try {
837
+ await sidecar2.activate({
838
+ appRoot: project.appRoot,
839
+ bundler: dev.bundler,
840
+ credentials: message.credentials,
841
+ internalSecret,
842
+ nextVersion: project.nextVersion,
843
+ options: message.options,
844
+ projectRoot: project.projectRoot,
845
+ publicOrigin: dev.publicOrigin,
846
+ registryEpoch,
847
+ routerKind: project.routerKind
848
+ });
849
+ } catch {
850
+ activationFailed = true;
851
+ failureCode = "CONFIGURATION_FAILED";
852
+ return createAck(message, { code: "CONFIGURATION_FAILED", ok: false });
853
+ }
854
+ }
855
+ if (!configured) {
856
+ configured = true;
857
+ clearTimeout(startupTimer);
858
+ process.stdout.write(
859
+ `[spotpatch:next] ready for Next.js ${project.nextVersion} (${dev.bundler}) at ${dev.publicOrigin}
860
+ `
861
+ );
862
+ }
863
+ return createAck(message, { ok: true });
864
+ };
865
+ const enqueueConfiguration = (value) => {
866
+ const result2 = configurationQueue.then(() => handleConfiguration(value));
867
+ configurationQueue = result2.then(
868
+ () => void 0,
869
+ () => void 0
870
+ );
871
+ return result2.catch(() => {
872
+ failureCode = "CONFIGURATION_FAILURE";
873
+ lifecycle.child?.kill("SIGTERM");
874
+ return void 0;
875
+ });
876
+ };
877
+ const sidecar = await createNextSidecar({
878
+ configuration: {
879
+ configurationSecret,
880
+ onConfiguration: enqueueConfiguration
881
+ },
882
+ onFatalError() {
883
+ lifecycle.sidecarFailed = true;
884
+ lifecycle.child?.kill("SIGTERM");
885
+ }
886
+ });
887
+ lifecycle.sidecar = sidecar;
888
+ const childEnvironment = {
889
+ ...process.env,
890
+ [NEXT_ENVIRONMENT_KEYS.appRoot]: project.appRoot,
891
+ [NEXT_ENVIRONMENT_KEYS.bundler]: dev.bundler,
892
+ [NEXT_ENVIRONMENT_KEYS.configurationSecret]: configurationSecret,
893
+ [NEXT_ENVIRONMENT_KEYS.internalOrigin]: sidecar.origin,
894
+ [NEXT_ENVIRONMENT_KEYS.internalSecret]: internalSecret,
895
+ [NEXT_ENVIRONMENT_KEYS.launchNonce]: launchNonce,
896
+ [NEXT_ENVIRONMENT_KEYS.registryEpoch]: registryEpoch,
897
+ [NEXT_ENVIRONMENT_KEYS.sidecarOrigin]: sidecar.origin
898
+ };
899
+ const child = spawn(
900
+ process.execPath,
901
+ [project.nextEntry, "dev", ...dev.nextArguments],
902
+ {
903
+ cwd: project.appRoot,
904
+ env: childEnvironment,
905
+ shell: false,
906
+ stdio: "inherit",
907
+ windowsHide: true
908
+ }
909
+ );
910
+ lifecycle.child = child;
911
+ const startupTimer = setTimeout(() => {
912
+ failureCode = "CONFIGURATION_TIMEOUT";
913
+ child.kill("SIGTERM");
914
+ }, CONFIGURATION_STARTUP_TIMEOUT_MS);
915
+ startupTimer.unref();
916
+ let requestedSignal;
917
+ let forceTimer;
918
+ const forwardSignal = (signal) => {
919
+ if (requestedSignal !== void 0) {
920
+ return;
921
+ }
922
+ requestedSignal = signal;
923
+ child.kill(signal);
924
+ forceTimer = setTimeout(() => {
925
+ child.kill("SIGKILL");
926
+ }, FORCED_TERMINATION_TIMEOUT_MS);
927
+ forceTimer.unref();
928
+ };
929
+ const onSigint = () => {
930
+ forwardSignal("SIGINT");
931
+ };
932
+ const onSigterm = () => {
933
+ forwardSignal("SIGTERM");
934
+ };
935
+ process.once("SIGINT", onSigint);
936
+ process.once("SIGTERM", onSigterm);
937
+ const result = await waitForChild(child);
938
+ clearTimeout(startupTimer);
939
+ process.off("SIGINT", onSigint);
940
+ process.off("SIGTERM", onSigterm);
941
+ if (forceTimer !== void 0) {
942
+ clearTimeout(forceTimer);
943
+ }
944
+ await configurationQueue;
945
+ try {
946
+ await closeSidecar(sidecar);
947
+ } catch {
948
+ failureCode ??= "SHUTDOWN_FAILED";
949
+ }
950
+ if (failureCode !== void 0) {
951
+ process.stderr.write(`[spotpatch:next] stopped (${failureCode}).
952
+ `);
953
+ return 1;
954
+ }
955
+ if (lifecycle.sidecarFailed || result.error) {
956
+ process.stderr.write("[spotpatch:next] stopped (PROCESS_FAILURE).\n");
957
+ return 1;
958
+ }
959
+ if (result.signal !== null || requestedSignal !== void 0) {
960
+ return signalExitCode(result.signal ?? requestedSignal ?? null);
961
+ }
962
+ return result.code ?? 1;
963
+ }
964
+
965
+ // src/initializer.ts
966
+ import { randomBytes as randomBytes2 } from "crypto";
967
+ import {
968
+ access as access2,
969
+ lstat,
970
+ mkdir,
971
+ readFile as readFile2,
972
+ rename,
973
+ stat,
974
+ unlink,
975
+ writeFile
976
+ } from "fs/promises";
977
+ import path3 from "path";
978
+ import { MagicString } from "magic-string";
979
+ import {
980
+ parseSync,
981
+ Visitor
982
+ } from "oxc-parser";
983
+ var ADAPTER_PACKAGE_NAME = "@spotpatch/next";
984
+ var CLIENT_MODULE_ID = "@spotpatch/next/client";
985
+ var CONFIG_FILE_NAMES = Object.freeze([
986
+ "next.config.ts",
987
+ "next.config.mts",
988
+ "next.config.js",
989
+ "next.config.mjs",
990
+ "next.config.cts",
991
+ "next.config.cjs"
992
+ ]);
993
+ var INSTRUMENTATION_EXTENSIONS = Object.freeze([
994
+ ".ts",
995
+ ".tsx",
996
+ ".js",
997
+ ".jsx",
998
+ ".mts",
999
+ ".mjs",
1000
+ ".cts",
1001
+ ".cjs"
1002
+ ]);
1003
+ var SIMPLE_SCRIPT_ARGUMENT_PATTERN = /^[A-Za-z0-9._:/=@%+,-]+$/u;
1004
+ function isParserErrorSeverity(value) {
1005
+ return value === "Error";
1006
+ }
1007
+ function isRecord3(value) {
1008
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1009
+ }
1010
+ async function pathExists2(absolutePath) {
1011
+ try {
1012
+ await access2(absolutePath);
1013
+ return true;
1014
+ } catch {
1015
+ return false;
1016
+ }
1017
+ }
1018
+ async function readRegularFile(absolutePath) {
1019
+ const metadata = await lstat(absolutePath);
1020
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
1021
+ throw new Error(
1022
+ `SpotPatch refuses to modify the non-regular file ${path3.basename(absolutePath)}.`
1023
+ );
1024
+ }
1025
+ return readFile2(absolutePath, "utf8");
1026
+ }
1027
+ function parseModule(absolutePath, source) {
1028
+ const result = parseSync(absolutePath, source, {
1029
+ sourceType: "module",
1030
+ showSemanticErrors: true
1031
+ });
1032
+ const error = result.errors.find((entry) => isParserErrorSeverity(entry.severity));
1033
+ if (error !== void 0) {
1034
+ throw new SyntaxError(
1035
+ `SpotPatch could not safely parse ${path3.basename(absolutePath)} (${error.message}).`
1036
+ );
1037
+ }
1038
+ return Object.freeze({ program: result.program, source });
1039
+ }
1040
+ function importsOf(program) {
1041
+ return program.body.filter(
1042
+ (statement) => statement.type === "ImportDeclaration"
1043
+ );
1044
+ }
1045
+ function importInsertionOffset(program) {
1046
+ const imports = importsOf(program);
1047
+ const lastImport = imports.at(-1);
1048
+ if (lastImport !== void 0) {
1049
+ return lastImport.end;
1050
+ }
1051
+ const directives = program.body.filter(
1052
+ (statement) => statement.type === "ExpressionStatement" && "directive" in statement && typeof statement.directive === "string"
1053
+ );
1054
+ return directives.at(-1)?.end ?? program.hashbang?.end ?? 0;
1055
+ }
1056
+ function insertStaticImport(magicString, program, statement) {
1057
+ const offset = importInsertionOffset(program);
1058
+ if (offset === 0) {
1059
+ magicString.prepend(`${statement}
1060
+
1061
+ `);
1062
+ return;
1063
+ }
1064
+ magicString.appendRight(offset, `
1065
+ ${statement}`);
1066
+ }
1067
+ function findDefaultExport(program) {
1068
+ const exports = program.body.filter(
1069
+ (statement) => statement.type === "ExportDefaultDeclaration"
1070
+ );
1071
+ if (exports.length !== 1) {
1072
+ throw new Error(
1073
+ "SpotPatch init requires exactly one ESM default export in next.config."
1074
+ );
1075
+ }
1076
+ const defaultExport = exports[0];
1077
+ if (defaultExport === void 0) {
1078
+ throw new Error("SpotPatch init could not read the next.config default export.");
1079
+ }
1080
+ return defaultExport;
1081
+ }
1082
+ function importedWrapperName(program) {
1083
+ const adapterImports = importsOf(program).filter(
1084
+ (statement) => statement.source.value === ADAPTER_PACKAGE_NAME
1085
+ );
1086
+ if (adapterImports.length > 1) {
1087
+ throw new Error(
1088
+ "SpotPatch init found multiple @spotpatch/next imports in next.config."
1089
+ );
1090
+ }
1091
+ const adapterImport = adapterImports[0];
1092
+ if (adapterImport === void 0) {
1093
+ return void 0;
1094
+ }
1095
+ const wrapper = adapterImport.specifiers.find(
1096
+ (specifier) => specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "withSpotPatch" && specifier.importKind !== "type"
1097
+ );
1098
+ if (wrapper === void 0) {
1099
+ throw new Error(
1100
+ "SpotPatch init cannot safely merge the existing @spotpatch/next import."
1101
+ );
1102
+ }
1103
+ return wrapper.local.name;
1104
+ }
1105
+ function collectIdentifierNames(program) {
1106
+ const names = /* @__PURE__ */ new Set();
1107
+ new Visitor({
1108
+ Identifier(node) {
1109
+ names.add(node.name);
1110
+ }
1111
+ }).visit(program);
1112
+ return names;
1113
+ }
1114
+ function chooseWrapperName(program) {
1115
+ const names = collectIdentifierNames(program);
1116
+ let suffix = 0;
1117
+ let candidate = "withSpotPatch";
1118
+ while (names.has(candidate)) {
1119
+ suffix += 1;
1120
+ candidate = `withSpotPatch${String(suffix)}`;
1121
+ }
1122
+ return candidate;
1123
+ }
1124
+ function unwrapParentheses(expression) {
1125
+ let current = expression;
1126
+ while (current.type === "ParenthesizedExpression") {
1127
+ current = current.expression;
1128
+ }
1129
+ return current;
1130
+ }
1131
+ function isWrappedDefaultExport(declaration, wrapperName) {
1132
+ if (declaration.type !== "CallExpression" || declaration.arguments.length !== 1) {
1133
+ return false;
1134
+ }
1135
+ const factoryCall = unwrapParentheses(declaration.callee);
1136
+ if (factoryCall.type !== "CallExpression") {
1137
+ return false;
1138
+ }
1139
+ const callee = unwrapParentheses(factoryCall.callee);
1140
+ return callee.type === "Identifier" && callee.name === wrapperName;
1141
+ }
1142
+ function transformNextConfig(absolutePath, source) {
1143
+ if (absolutePath.endsWith(".cjs") || absolutePath.endsWith(".cts")) {
1144
+ throw new Error(
1145
+ "SpotPatch init does not rewrite CommonJS next.config files; add withSpotPatch manually."
1146
+ );
1147
+ }
1148
+ const { program } = parseModule(absolutePath, source);
1149
+ const defaultExport = findDefaultExport(program);
1150
+ const existingWrapperName = importedWrapperName(program);
1151
+ if (existingWrapperName !== void 0 && isWrappedDefaultExport(defaultExport.declaration, existingWrapperName)) {
1152
+ return source;
1153
+ }
1154
+ if (defaultExport.declaration.type === "FunctionDeclaration" || defaultExport.declaration.type === "TSDeclareFunction" || defaultExport.declaration.type === "ClassDeclaration" || defaultExport.declaration.type === "TSInterfaceDeclaration") {
1155
+ throw new Error(
1156
+ "SpotPatch init cannot safely wrap this next.config default declaration; use an exported config expression."
1157
+ );
1158
+ }
1159
+ const magicString = new MagicString(source);
1160
+ const wrapperName = existingWrapperName ?? chooseWrapperName(program);
1161
+ if (existingWrapperName === void 0) {
1162
+ const specifier = wrapperName === "withSpotPatch" ? "withSpotPatch" : `withSpotPatch as ${wrapperName}`;
1163
+ insertStaticImport(
1164
+ magicString,
1165
+ program,
1166
+ `import { ${specifier} } from ${JSON.stringify(ADAPTER_PACKAGE_NAME)};`
1167
+ );
1168
+ }
1169
+ const expression = source.slice(
1170
+ defaultExport.declaration.start,
1171
+ defaultExport.declaration.end
1172
+ );
1173
+ magicString.overwrite(
1174
+ defaultExport.declaration.start,
1175
+ defaultExport.declaration.end,
1176
+ `${wrapperName}()(${expression})`
1177
+ );
1178
+ return magicString.toString();
1179
+ }
1180
+ function transformInstrumentationClient(absolutePath, source) {
1181
+ const { program } = parseModule(absolutePath, source);
1182
+ const clientImports = importsOf(program).filter(
1183
+ (statement) => statement.source.value === CLIENT_MODULE_ID && statement.importKind !== "type" && (statement.specifiers.length === 0 || statement.specifiers.some(
1184
+ (specifier) => specifier.type !== "ImportSpecifier" || specifier.importKind !== "type"
1185
+ ))
1186
+ );
1187
+ if (clientImports.length > 1) {
1188
+ throw new Error("SpotPatch init found duplicate @spotpatch/next/client imports.");
1189
+ }
1190
+ if (clientImports.length === 1) {
1191
+ return source;
1192
+ }
1193
+ const magicString = new MagicString(source);
1194
+ insertStaticImport(
1195
+ magicString,
1196
+ program,
1197
+ `import ${JSON.stringify(CLIENT_MODULE_ID)};`
1198
+ );
1199
+ return magicString.toString();
1200
+ }
1201
+ function parsePackageManifest(source) {
1202
+ let value;
1203
+ try {
1204
+ value = JSON.parse(source);
1205
+ } catch (error) {
1206
+ throw new SyntaxError("SpotPatch could not parse package.json.", {
1207
+ cause: error
1208
+ });
1209
+ }
1210
+ if (!isRecord3(value)) {
1211
+ throw new TypeError("SpotPatch requires package.json to contain an object.");
1212
+ }
1213
+ return value;
1214
+ }
1215
+ function hasAdapterDependency(manifest) {
1216
+ return [
1217
+ manifest.dependencies,
1218
+ manifest.devDependencies,
1219
+ manifest.optionalDependencies
1220
+ ].some(
1221
+ (dependencies) => isRecord3(dependencies) && typeof dependencies[ADAPTER_PACKAGE_NAME] === "string"
1222
+ );
1223
+ }
1224
+ function transformDevScript(script) {
1225
+ const tokens = script.trim().split(/\s+/u);
1226
+ if (tokens[0] === "spotpatch-next" && tokens[1] === "dev") {
1227
+ if (tokens.every((token) => SIMPLE_SCRIPT_ARGUMENT_PATTERN.test(token))) {
1228
+ return script;
1229
+ }
1230
+ throw new Error("SpotPatch init cannot verify the existing dev script safely.");
1231
+ }
1232
+ if (tokens[0] !== "next" || tokens[1] !== "dev" || !tokens.every((token) => SIMPLE_SCRIPT_ARGUMENT_PATTERN.test(token))) {
1233
+ throw new Error("SpotPatch init only rewrites a simple `next dev` package script.");
1234
+ }
1235
+ return ["spotpatch-next", ...tokens.slice(1)].join(" ");
1236
+ }
1237
+ function detectIndent(source) {
1238
+ return /^([\t ]+)"/mu.exec(source)?.[1] ?? " ";
1239
+ }
1240
+ function transformPackageJson(source) {
1241
+ const manifest = parsePackageManifest(source);
1242
+ if (!hasAdapterDependency(manifest)) {
1243
+ throw new Error(
1244
+ "SpotPatch init requires @spotpatch/next in package dependencies first."
1245
+ );
1246
+ }
1247
+ if (!isRecord3(manifest.scripts) || typeof manifest.scripts.dev !== "string") {
1248
+ throw new Error("SpotPatch init requires a string package script named dev.");
1249
+ }
1250
+ const dev = transformDevScript(manifest.scripts.dev);
1251
+ if (dev === manifest.scripts.dev) {
1252
+ return source;
1253
+ }
1254
+ const nextManifest = {
1255
+ ...manifest,
1256
+ scripts: { ...manifest.scripts, dev }
1257
+ };
1258
+ const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
1259
+ const serialized = JSON.stringify(nextManifest, void 0, detectIndent(source));
1260
+ return `${serialized.replaceAll("\n", lineEnding)}${lineEnding}`;
1261
+ }
1262
+ async function findNextConfig(appRoot) {
1263
+ const candidates = (await Promise.all(
1264
+ CONFIG_FILE_NAMES.map(async (name) => {
1265
+ const absolutePath = path3.join(appRoot, name);
1266
+ return await pathExists2(absolutePath) ? absolutePath : void 0;
1267
+ })
1268
+ )).filter((value) => value !== void 0);
1269
+ if (candidates.length !== 1) {
1270
+ throw new Error("SpotPatch init requires exactly one supported next.config file.");
1271
+ }
1272
+ const configPath = candidates[0];
1273
+ if (configPath === void 0) {
1274
+ throw new Error("SpotPatch init could not read the next.config path.");
1275
+ }
1276
+ return configPath;
1277
+ }
1278
+ async function resolveInstrumentationPath(appRoot, configPath) {
1279
+ const rootRouters = await Promise.all([
1280
+ pathExists2(path3.join(appRoot, "app")),
1281
+ pathExists2(path3.join(appRoot, "pages"))
1282
+ ]);
1283
+ const sourceRouters = await Promise.all([
1284
+ pathExists2(path3.join(appRoot, "src", "app")),
1285
+ pathExists2(path3.join(appRoot, "src", "pages"))
1286
+ ]);
1287
+ const hasRootRouter = rootRouters.some(Boolean);
1288
+ const hasSourceRouter = sourceRouters.some(Boolean);
1289
+ if (hasRootRouter && hasSourceRouter) {
1290
+ throw new Error(
1291
+ "SpotPatch init cannot choose an instrumentation location for mixed root/src routers."
1292
+ );
1293
+ }
1294
+ if (!hasRootRouter && !hasSourceRouter) {
1295
+ throw new Error("SpotPatch init could not find an App or Pages Router.");
1296
+ }
1297
+ const directory = hasSourceRouter ? path3.join(appRoot, "src") : appRoot;
1298
+ const existing = (await Promise.all(
1299
+ INSTRUMENTATION_EXTENSIONS.map(async (extension) => {
1300
+ const absolutePath = path3.join(directory, `instrumentation-client${extension}`);
1301
+ return await pathExists2(absolutePath) ? absolutePath : void 0;
1302
+ })
1303
+ )).filter((value) => value !== void 0);
1304
+ if (existing.length > 1) {
1305
+ throw new Error(
1306
+ "SpotPatch init found multiple instrumentation-client entry files."
1307
+ );
1308
+ }
1309
+ if (existing[0] !== void 0) {
1310
+ return existing[0];
1311
+ }
1312
+ const useTypeScript = configPath.endsWith(".ts") || configPath.endsWith(".mts") || configPath.endsWith(".cts") || await pathExists2(path3.join(appRoot, "tsconfig.json"));
1313
+ return path3.join(directory, `instrumentation-client.${useTypeScript ? "ts" : "js"}`);
1314
+ }
1315
+ function createChange(appRoot, absolutePath, nextContent, previousContent) {
1316
+ if (previousContent === nextContent) {
1317
+ return void 0;
1318
+ }
1319
+ return Object.freeze({
1320
+ absolutePath,
1321
+ nextContent,
1322
+ ...previousContent === void 0 ? {} : { previousContent },
1323
+ relativePath: path3.relative(appRoot, absolutePath).split(path3.sep).join("/")
1324
+ });
1325
+ }
1326
+ async function planNextIntegration(directory = process.cwd()) {
1327
+ const appRoot = path3.resolve(directory);
1328
+ const packagePath = path3.join(appRoot, "package.json");
1329
+ const [packageSource, configPath] = await Promise.all([
1330
+ readRegularFile(packagePath),
1331
+ findNextConfig(appRoot)
1332
+ ]);
1333
+ const configSource = await readRegularFile(configPath);
1334
+ const instrumentationPath = await resolveInstrumentationPath(appRoot, configPath);
1335
+ const instrumentationSource = await pathExists2(instrumentationPath) ? await readRegularFile(instrumentationPath) : void 0;
1336
+ const changes = [
1337
+ createChange(
1338
+ appRoot,
1339
+ configPath,
1340
+ transformNextConfig(configPath, configSource),
1341
+ configSource
1342
+ ),
1343
+ createChange(
1344
+ appRoot,
1345
+ instrumentationPath,
1346
+ transformInstrumentationClient(instrumentationPath, instrumentationSource ?? ""),
1347
+ instrumentationSource
1348
+ ),
1349
+ createChange(
1350
+ appRoot,
1351
+ packagePath,
1352
+ transformPackageJson(packageSource),
1353
+ packageSource
1354
+ )
1355
+ ].filter((change) => change !== void 0);
1356
+ return Object.freeze({ appRoot, changes: Object.freeze(changes) });
1357
+ }
1358
+ function temporaryPath(absolutePath, label) {
1359
+ return path3.join(
1360
+ path3.dirname(absolutePath),
1361
+ `.${path3.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes2(8).toString("hex")}`
1362
+ );
1363
+ }
1364
+ async function writeAtomic(absolutePath, content, mode) {
1365
+ await mkdir(path3.dirname(absolutePath), { recursive: true });
1366
+ const stagedPath = temporaryPath(absolutePath, "stage");
1367
+ try {
1368
+ await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
1369
+ await rename(stagedPath, absolutePath);
1370
+ } catch (error) {
1371
+ await unlink(stagedPath).catch(() => void 0);
1372
+ throw error;
1373
+ }
1374
+ }
1375
+ async function rollbackChange(change) {
1376
+ if (change.previousContent === void 0) {
1377
+ await unlink(change.absolutePath).catch(() => void 0);
1378
+ return;
1379
+ }
1380
+ const mode = (await stat(change.absolutePath)).mode;
1381
+ await writeAtomic(change.absolutePath, change.previousContent, mode);
1382
+ }
1383
+ async function applyNextIntegrationPlan(plan) {
1384
+ const applied = [];
1385
+ try {
1386
+ for (const change of plan.changes) {
1387
+ const mode = change.previousContent === void 0 ? 384 : (await stat(change.absolutePath)).mode;
1388
+ await writeAtomic(change.absolutePath, change.nextContent, mode);
1389
+ applied.push(change);
1390
+ }
1391
+ } catch (error) {
1392
+ const rollbackResults = await Promise.allSettled(
1393
+ applied.reverse().map(rollbackChange)
1394
+ );
1395
+ if (rollbackResults.some((result) => result.status === "rejected")) {
1396
+ throw new Error(
1397
+ "SpotPatch init failed and could not completely restore the previous files.",
1398
+ { cause: error }
1399
+ );
1400
+ }
1401
+ throw new Error("SpotPatch init failed; all written files were restored.", {
1402
+ cause: error
1403
+ });
1404
+ }
1405
+ }
1406
+ async function checkNextIntegration(directory = process.cwd()) {
1407
+ try {
1408
+ const plan = await planNextIntegration(directory);
1409
+ const issues = plan.changes.map(
1410
+ (change) => `INTEGRATION_REQUIRED:${change.relativePath}`
1411
+ );
1412
+ return Object.freeze({
1413
+ appRoot: plan.appRoot,
1414
+ issues: Object.freeze(issues),
1415
+ ok: issues.length === 0
1416
+ });
1417
+ } catch (error) {
1418
+ return Object.freeze({
1419
+ appRoot: path3.resolve(directory),
1420
+ issues: Object.freeze([
1421
+ error instanceof Error ? error.message : "SpotPatch integration check failed."
1422
+ ]),
1423
+ ok: false
1424
+ });
1425
+ }
1426
+ }
1427
+
1428
+ // src/cli.ts
1429
+ function writeUsage() {
1430
+ process.stderr.write(
1431
+ "Usage: spotpatch-next <dev|init|check>\n dev [next dev options] Start the local Next.js development server.\n init Preview and apply safe integration changes.\n check Verify the integration without writing files.\n"
1432
+ );
1433
+ }
1434
+ function verifyAdapterExports(appRoot) {
1435
+ const resolveFromApplication = createRequire2(path4.join(appRoot, "package.json"));
1436
+ for (const moduleId of [
1437
+ "@spotpatch/next",
1438
+ "@spotpatch/next/client",
1439
+ "@spotpatch/next/loader",
1440
+ "@spotpatch/next/noop"
1441
+ ]) {
1442
+ try {
1443
+ resolveFromApplication.resolve(moduleId);
1444
+ } catch (error) {
1445
+ throw new Error(`SpotPatch could not resolve the required export ${moduleId}.`, {
1446
+ cause: error
1447
+ });
1448
+ }
1449
+ }
1450
+ }
1451
+ async function runInit(arguments_) {
1452
+ if (arguments_.length !== 0) {
1453
+ throw new Error("SpotPatch init does not accept positional arguments.");
1454
+ }
1455
+ const project = await inspectNextProject();
1456
+ verifyAdapterExports(project.appRoot);
1457
+ const plan = await planNextIntegration(project.appRoot);
1458
+ if (plan.changes.length === 0) {
1459
+ process.stdout.write("[spotpatch:next] integration is already up to date.\n");
1460
+ return 0;
1461
+ }
1462
+ process.stdout.write("[spotpatch:next] integration preview (resulting files):\n");
1463
+ for (const change of plan.changes) {
1464
+ process.stdout.write(`
1465
+ --- ${change.relativePath}
1466
+ ${change.nextContent}`);
1467
+ if (!change.nextContent.endsWith("\n")) {
1468
+ process.stdout.write("\n");
1469
+ }
1470
+ }
1471
+ await applyNextIntegrationPlan(plan);
1472
+ process.stdout.write(
1473
+ `[spotpatch:next] updated ${String(plan.changes.length)} integration file(s).
1474
+ `
1475
+ );
1476
+ return 0;
1477
+ }
1478
+ async function runCheck(arguments_) {
1479
+ if (arguments_.length !== 0) {
1480
+ throw new Error("SpotPatch check does not accept positional arguments.");
1481
+ }
1482
+ const project = await inspectNextProject();
1483
+ verifyAdapterExports(project.appRoot);
1484
+ const result = await checkNextIntegration(project.appRoot);
1485
+ if (!result.ok) {
1486
+ for (const issue of result.issues) {
1487
+ process.stderr.write(`[spotpatch:next] ${issue}
1488
+ `);
1489
+ }
1490
+ return 1;
1491
+ }
1492
+ process.stdout.write(
1493
+ `[spotpatch:next] integration verified for Next.js ${project.nextVersion}.
1494
+ `
1495
+ );
1496
+ return 0;
1497
+ }
1498
+ async function main(arguments_) {
1499
+ const [command, ...rest] = arguments_;
1500
+ if (command === "dev") {
1501
+ const integration = await checkNextIntegration();
1502
+ if (!integration.ok) {
1503
+ throw new Error(
1504
+ "SpotPatch Next integration is incomplete; run `spotpatch-next init` first."
1505
+ );
1506
+ }
1507
+ return runNextDevelopment(rest);
1508
+ }
1509
+ if (command === "init") {
1510
+ return runInit(rest);
1511
+ }
1512
+ if (command === "check") {
1513
+ return runCheck(rest);
1514
+ }
1515
+ writeUsage();
1516
+ return 1;
1517
+ }
1518
+ try {
1519
+ process.exitCode = await main(process.argv.slice(2));
1520
+ } catch (error) {
1521
+ process.stderr.write(
1522
+ `[spotpatch:next] ${error instanceof Error ? error.message : "The command failed."}
1523
+ `
1524
+ );
1525
+ process.exitCode = 1;
1526
+ }
1527
+ //# sourceMappingURL=cli.js.map