@spotpatch/next 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ pnpm exec spotpatch-next check
40
40
  pnpm dev
41
41
  ```
42
42
 
43
- The supported initializer result has three parts:
43
+ The supported initializer result has three parts. The TypeScript example below enables the in-page Review / Trusted fast selector by discovering the local TypeScript check; a JavaScript-only project remains on `withSpotPatch()` and review mode.
44
44
 
45
45
  ```ts
46
46
  // next.config.ts
@@ -49,7 +49,7 @@ import { withSpotPatch } from "@spotpatch/next";
49
49
 
50
50
  const nextConfig: NextConfig = {};
51
51
 
52
- export default withSpotPatch()(nextConfig);
52
+ export default withSpotPatch({ trustedFastMode: true })(nextConfig);
53
53
  ```
54
54
 
55
55
  ```ts
@@ -141,7 +141,7 @@ pnpm exec spotpatch-next check
141
141
  pnpm dev
142
142
  ```
143
143
 
144
- 受支持的初始化结果包含三部分:
144
+ 受支持的初始化结果包含三部分。下面的 TypeScript 示例会通过自动发现本地 TypeScript 校验来开放页面内“审阅 / 可信快速”选择;纯 JavaScript 项目保持 `withSpotPatch()` 和审阅模式。
145
145
 
146
146
  ```ts
147
147
  // next.config.ts
@@ -150,7 +150,7 @@ import { withSpotPatch } from "@spotpatch/next";
150
150
 
151
151
  const nextConfig: NextConfig = {};
152
152
 
153
- export default withSpotPatch()(nextConfig);
153
+ export default withSpotPatch({ trustedFastMode: true })(nextConfig);
154
154
  ```
155
155
 
156
156
  ```ts
package/dist/cli.js CHANGED
@@ -297,7 +297,7 @@ import {
297
297
  // package.json
298
298
  var package_default = {
299
299
  name: "@spotpatch/next",
300
- version: "0.2.0",
300
+ version: "0.3.1",
301
301
  description: "Development-only Next.js adapter and CLI for SpotPatch.",
302
302
  license: "MIT",
303
303
  repository: {
@@ -964,18 +964,15 @@ async function runNextDevelopment(arguments_) {
964
964
  }
965
965
 
966
966
  // src/initializer.ts
967
- import { randomBytes as randomBytes2 } from "crypto";
968
- import {
969
- access as access2,
970
- lstat,
971
- mkdir,
972
- readFile as readFile2,
973
- rename,
974
- stat,
975
- unlink,
976
- writeFile
977
- } from "fs/promises";
978
967
  import path3 from "path";
968
+ import {
969
+ applyIntegrationPlan,
970
+ createIntegrationFileChange,
971
+ discoverProjectValidationCheck,
972
+ integrationPathExists,
973
+ readIntegrationFile
974
+ } from "@spotpatch/dev-server";
975
+ import { DEFAULT_AGENT_LIMITS } from "@spotpatch/shared";
979
976
  import { MagicString } from "magic-string";
980
977
  import {
981
978
  parseSync,
@@ -1008,23 +1005,6 @@ function isParserErrorSeverity(value) {
1008
1005
  function isRecord3(value) {
1009
1006
  return typeof value === "object" && value !== null && !Array.isArray(value);
1010
1007
  }
1011
- async function pathExists2(absolutePath) {
1012
- try {
1013
- await access2(absolutePath);
1014
- return true;
1015
- } catch {
1016
- return false;
1017
- }
1018
- }
1019
- async function readRegularFile(absolutePath) {
1020
- const metadata = await lstat(absolutePath);
1021
- if (!metadata.isFile() || metadata.isSymbolicLink()) {
1022
- throw new Error(
1023
- `SpotPatch refuses to modify the non-regular file ${path3.basename(absolutePath)}.`
1024
- );
1025
- }
1026
- return readFile2(absolutePath, "utf8");
1027
- }
1028
1008
  function parseModule(absolutePath, source) {
1029
1009
  const result = parseSync(absolutePath, source, {
1030
1010
  sourceType: "module",
@@ -1129,18 +1109,18 @@ function unwrapParentheses(expression) {
1129
1109
  }
1130
1110
  return current;
1131
1111
  }
1132
- function isWrappedDefaultExport(declaration, wrapperName) {
1112
+ function wrappedFactoryCall(declaration, wrapperName) {
1133
1113
  if (declaration.type !== "CallExpression" || declaration.arguments.length !== 1) {
1134
- return false;
1114
+ return void 0;
1135
1115
  }
1136
1116
  const factoryCall = unwrapParentheses(declaration.callee);
1137
1117
  if (factoryCall.type !== "CallExpression") {
1138
- return false;
1118
+ return void 0;
1139
1119
  }
1140
1120
  const callee = unwrapParentheses(factoryCall.callee);
1141
- return callee.type === "Identifier" && callee.name === wrapperName;
1121
+ return callee.type === "Identifier" && callee.name === wrapperName ? factoryCall : void 0;
1142
1122
  }
1143
- function transformNextConfig(absolutePath, source) {
1123
+ function transformNextConfig(absolutePath, source, trustedFastModeAvailable) {
1144
1124
  if (absolutePath.endsWith(".cjs") || absolutePath.endsWith(".cts")) {
1145
1125
  throw new Error(
1146
1126
  "SpotPatch init does not rewrite CommonJS next.config files; add withSpotPatch manually."
@@ -1149,9 +1129,6 @@ function transformNextConfig(absolutePath, source) {
1149
1129
  const { program } = parseModule(absolutePath, source);
1150
1130
  const defaultExport = findDefaultExport(program);
1151
1131
  const existingWrapperName = importedWrapperName(program);
1152
- if (existingWrapperName !== void 0 && isWrappedDefaultExport(defaultExport.declaration, existingWrapperName)) {
1153
- return source;
1154
- }
1155
1132
  if (defaultExport.declaration.type === "FunctionDeclaration" || defaultExport.declaration.type === "TSDeclareFunction" || defaultExport.declaration.type === "ClassDeclaration" || defaultExport.declaration.type === "TSInterfaceDeclaration") {
1156
1133
  throw new Error(
1157
1134
  "SpotPatch init cannot safely wrap this next.config default declaration; use an exported config expression."
@@ -1159,6 +1136,18 @@ function transformNextConfig(absolutePath, source) {
1159
1136
  }
1160
1137
  const magicString = new MagicString(source);
1161
1138
  const wrapperName = existingWrapperName ?? chooseWrapperName(program);
1139
+ const existingFactory = existingWrapperName === void 0 ? void 0 : wrappedFactoryCall(defaultExport.declaration, existingWrapperName);
1140
+ if (existingFactory !== void 0) {
1141
+ if (trustedFastModeAvailable && existingFactory.arguments.length === 0) {
1142
+ magicString.overwrite(
1143
+ existingFactory.start,
1144
+ existingFactory.end,
1145
+ `${wrapperName}({ trustedFastMode: true })`
1146
+ );
1147
+ return magicString.toString();
1148
+ }
1149
+ return source;
1150
+ }
1162
1151
  if (existingWrapperName === void 0) {
1163
1152
  const specifier = wrapperName === "withSpotPatch" ? "withSpotPatch" : `withSpotPatch as ${wrapperName}`;
1164
1153
  insertStaticImport(
@@ -1174,7 +1163,7 @@ function transformNextConfig(absolutePath, source) {
1174
1163
  magicString.overwrite(
1175
1164
  defaultExport.declaration.start,
1176
1165
  defaultExport.declaration.end,
1177
- `${wrapperName}()(${expression})`
1166
+ `${wrapperName}(${trustedFastModeAvailable ? "{ trustedFastMode: true }" : ""})(${expression})`
1178
1167
  );
1179
1168
  return magicString.toString();
1180
1169
  }
@@ -1264,7 +1253,7 @@ async function findNextConfig(appRoot) {
1264
1253
  const candidates = (await Promise.all(
1265
1254
  CONFIG_FILE_NAMES.map(async (name) => {
1266
1255
  const absolutePath = path3.join(appRoot, name);
1267
- return await pathExists2(absolutePath) ? absolutePath : void 0;
1256
+ return await integrationPathExists(absolutePath) ? absolutePath : void 0;
1268
1257
  })
1269
1258
  )).filter((value) => value !== void 0);
1270
1259
  if (candidates.length !== 1) {
@@ -1278,12 +1267,12 @@ async function findNextConfig(appRoot) {
1278
1267
  }
1279
1268
  async function resolveInstrumentationPath(appRoot, configPath) {
1280
1269
  const rootRouters = await Promise.all([
1281
- pathExists2(path3.join(appRoot, "app")),
1282
- pathExists2(path3.join(appRoot, "pages"))
1270
+ integrationPathExists(path3.join(appRoot, "app")),
1271
+ integrationPathExists(path3.join(appRoot, "pages"))
1283
1272
  ]);
1284
1273
  const sourceRouters = await Promise.all([
1285
- pathExists2(path3.join(appRoot, "src", "app")),
1286
- pathExists2(path3.join(appRoot, "src", "pages"))
1274
+ integrationPathExists(path3.join(appRoot, "src", "app")),
1275
+ integrationPathExists(path3.join(appRoot, "src", "pages"))
1287
1276
  ]);
1288
1277
  const hasRootRouter = rootRouters.some(Boolean);
1289
1278
  const hasSourceRouter = sourceRouters.some(Boolean);
@@ -1299,7 +1288,7 @@ async function resolveInstrumentationPath(appRoot, configPath) {
1299
1288
  const existing = (await Promise.all(
1300
1289
  INSTRUMENTATION_EXTENSIONS.map(async (extension) => {
1301
1290
  const absolutePath = path3.join(directory, `instrumentation-client${extension}`);
1302
- return await pathExists2(absolutePath) ? absolutePath : void 0;
1291
+ return await integrationPathExists(absolutePath) ? absolutePath : void 0;
1303
1292
  })
1304
1293
  )).filter((value) => value !== void 0);
1305
1294
  if (existing.length > 1) {
@@ -1310,99 +1299,54 @@ async function resolveInstrumentationPath(appRoot, configPath) {
1310
1299
  if (existing[0] !== void 0) {
1311
1300
  return existing[0];
1312
1301
  }
1313
- const useTypeScript = configPath.endsWith(".ts") || configPath.endsWith(".mts") || configPath.endsWith(".cts") || await pathExists2(path3.join(appRoot, "tsconfig.json"));
1302
+ const useTypeScript = configPath.endsWith(".ts") || configPath.endsWith(".mts") || configPath.endsWith(".cts") || await integrationPathExists(path3.join(appRoot, "tsconfig.json"));
1314
1303
  return path3.join(directory, `instrumentation-client.${useTypeScript ? "ts" : "js"}`);
1315
1304
  }
1316
- function createChange(appRoot, absolutePath, nextContent, previousContent) {
1317
- if (previousContent === nextContent) {
1318
- return void 0;
1319
- }
1320
- return Object.freeze({
1321
- absolutePath,
1322
- nextContent,
1323
- ...previousContent === void 0 ? {} : { previousContent },
1324
- relativePath: path3.relative(appRoot, absolutePath).split(path3.sep).join("/")
1325
- });
1326
- }
1327
1305
  async function planNextIntegration(directory = process.cwd()) {
1328
1306
  const appRoot = path3.resolve(directory);
1329
1307
  const packagePath = path3.join(appRoot, "package.json");
1330
1308
  const [packageSource, configPath] = await Promise.all([
1331
- readRegularFile(packagePath),
1309
+ readIntegrationFile(packagePath),
1332
1310
  findNextConfig(appRoot)
1333
1311
  ]);
1334
- const configSource = await readRegularFile(configPath);
1312
+ const [configSource, discoveredCheck] = await Promise.all([
1313
+ readIntegrationFile(configPath),
1314
+ discoverProjectValidationCheck({
1315
+ appRoot,
1316
+ timeoutMs: DEFAULT_AGENT_LIMITS.checkTimeoutMs
1317
+ })
1318
+ ]);
1319
+ const trustedFastModeAvailable = discoveredCheck !== void 0;
1335
1320
  const instrumentationPath = await resolveInstrumentationPath(appRoot, configPath);
1336
- const instrumentationSource = await pathExists2(instrumentationPath) ? await readRegularFile(instrumentationPath) : void 0;
1321
+ const instrumentationSource = await integrationPathExists(instrumentationPath) ? await readIntegrationFile(instrumentationPath) : void 0;
1337
1322
  const changes = [
1338
- createChange(
1323
+ createIntegrationFileChange(
1339
1324
  appRoot,
1340
1325
  configPath,
1341
- transformNextConfig(configPath, configSource),
1326
+ transformNextConfig(configPath, configSource, trustedFastModeAvailable),
1342
1327
  configSource
1343
1328
  ),
1344
- createChange(
1329
+ createIntegrationFileChange(
1345
1330
  appRoot,
1346
1331
  instrumentationPath,
1347
1332
  transformInstrumentationClient(instrumentationPath, instrumentationSource ?? ""),
1348
1333
  instrumentationSource
1349
1334
  ),
1350
- createChange(
1335
+ createIntegrationFileChange(
1351
1336
  appRoot,
1352
1337
  packagePath,
1353
1338
  transformPackageJson(packageSource),
1354
1339
  packageSource
1355
1340
  )
1356
1341
  ].filter((change) => change !== void 0);
1357
- return Object.freeze({ appRoot, changes: Object.freeze(changes) });
1358
- }
1359
- function temporaryPath(absolutePath, label) {
1360
- return path3.join(
1361
- path3.dirname(absolutePath),
1362
- `.${path3.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes2(8).toString("hex")}`
1363
- );
1364
- }
1365
- async function writeAtomic(absolutePath, content, mode) {
1366
- await mkdir(path3.dirname(absolutePath), { recursive: true });
1367
- const stagedPath = temporaryPath(absolutePath, "stage");
1368
- try {
1369
- await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
1370
- await rename(stagedPath, absolutePath);
1371
- } catch (error) {
1372
- await unlink(stagedPath).catch(() => void 0);
1373
- throw error;
1374
- }
1375
- }
1376
- async function rollbackChange(change) {
1377
- if (change.previousContent === void 0) {
1378
- await unlink(change.absolutePath).catch(() => void 0);
1379
- return;
1380
- }
1381
- const mode = (await stat(change.absolutePath)).mode;
1382
- await writeAtomic(change.absolutePath, change.previousContent, mode);
1342
+ return Object.freeze({
1343
+ appRoot,
1344
+ changes: Object.freeze(changes),
1345
+ trustedFastModeAvailable
1346
+ });
1383
1347
  }
1384
1348
  async function applyNextIntegrationPlan(plan) {
1385
- const applied = [];
1386
- try {
1387
- for (const change of plan.changes) {
1388
- const mode = change.previousContent === void 0 ? 384 : (await stat(change.absolutePath)).mode;
1389
- await writeAtomic(change.absolutePath, change.nextContent, mode);
1390
- applied.push(change);
1391
- }
1392
- } catch (error) {
1393
- const rollbackResults = await Promise.allSettled(
1394
- applied.reverse().map(rollbackChange)
1395
- );
1396
- if (rollbackResults.some((result) => result.status === "rejected")) {
1397
- throw new Error(
1398
- "SpotPatch init failed and could not completely restore the previous files.",
1399
- { cause: error }
1400
- );
1401
- }
1402
- throw new Error("SpotPatch init failed; all written files were restored.", {
1403
- cause: error
1404
- });
1405
- }
1349
+ await applyIntegrationPlan(plan);
1406
1350
  }
1407
1351
  async function checkNextIntegration(directory = process.cwd()) {
1408
1352
  try {
@@ -1413,7 +1357,8 @@ async function checkNextIntegration(directory = process.cwd()) {
1413
1357
  return Object.freeze({
1414
1358
  appRoot: plan.appRoot,
1415
1359
  issues: Object.freeze(issues),
1416
- ok: issues.length === 0
1360
+ ok: issues.length === 0,
1361
+ trustedFastModeAvailable: plan.trustedFastModeAvailable
1417
1362
  });
1418
1363
  } catch (error) {
1419
1364
  return Object.freeze({
@@ -1421,7 +1366,8 @@ async function checkNextIntegration(directory = process.cwd()) {
1421
1366
  issues: Object.freeze([
1422
1367
  error instanceof Error ? error.message : "SpotPatch integration check failed."
1423
1368
  ]),
1424
- ok: false
1369
+ ok: false,
1370
+ trustedFastModeAvailable: false
1425
1371
  });
1426
1372
  }
1427
1373
  }
@@ -1449,6 +1395,11 @@ function verifyAdapterExports(appRoot) {
1449
1395
  }
1450
1396
  }
1451
1397
  }
1398
+ function writeTrustedModeStatus(available) {
1399
+ process.stdout.write(
1400
+ available ? "[spotpatch:next] trusted fast mode is available in the page selector.\n" : "[spotpatch:next] review mode is ready; trusted fast mode needs a local TypeScript project check.\n"
1401
+ );
1402
+ }
1452
1403
  async function runInit(arguments_) {
1453
1404
  if (arguments_.length !== 0) {
1454
1405
  throw new Error("SpotPatch init does not accept positional arguments.");
@@ -1458,6 +1409,7 @@ async function runInit(arguments_) {
1458
1409
  const plan = await planNextIntegration(project.appRoot);
1459
1410
  if (plan.changes.length === 0) {
1460
1411
  process.stdout.write("[spotpatch:next] integration is already up to date.\n");
1412
+ writeTrustedModeStatus(plan.trustedFastModeAvailable);
1461
1413
  return 0;
1462
1414
  }
1463
1415
  process.stdout.write("[spotpatch:next] integration preview (resulting files):\n");
@@ -1474,6 +1426,7 @@ ${change.nextContent}`);
1474
1426
  `[spotpatch:next] updated ${String(plan.changes.length)} integration file(s).
1475
1427
  `
1476
1428
  );
1429
+ writeTrustedModeStatus(plan.trustedFastModeAvailable);
1477
1430
  return 0;
1478
1431
  }
1479
1432
  async function runCheck(arguments_) {
@@ -1490,8 +1443,9 @@ async function runCheck(arguments_) {
1490
1443
  }
1491
1444
  return 1;
1492
1445
  }
1446
+ const mode = result.trustedFastModeAvailable ? "trusted fast mode available" : "review mode";
1493
1447
  process.stdout.write(
1494
- `[spotpatch:next] integration verified for Next.js ${project.nextVersion}.
1448
+ `[spotpatch:next] integration verified for Next.js ${project.nextVersion} \xB7 ${mode}.
1495
1449
  `
1496
1450
  );
1497
1451
  return 0;