@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 +4 -4
- package/dist/cli.js +67 -113
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +6 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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.
|
|
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
|
|
1112
|
+
function wrappedFactoryCall(declaration, wrapperName) {
|
|
1133
1113
|
if (declaration.type !== "CallExpression" || declaration.arguments.length !== 1) {
|
|
1134
|
-
return
|
|
1114
|
+
return void 0;
|
|
1135
1115
|
}
|
|
1136
1116
|
const factoryCall = unwrapParentheses(declaration.callee);
|
|
1137
1117
|
if (factoryCall.type !== "CallExpression") {
|
|
1138
|
-
return
|
|
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
|
|
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
|
-
|
|
1282
|
-
|
|
1270
|
+
integrationPathExists(path3.join(appRoot, "app")),
|
|
1271
|
+
integrationPathExists(path3.join(appRoot, "pages"))
|
|
1283
1272
|
]);
|
|
1284
1273
|
const sourceRouters = await Promise.all([
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1309
|
+
readIntegrationFile(packagePath),
|
|
1332
1310
|
findNextConfig(appRoot)
|
|
1333
1311
|
]);
|
|
1334
|
-
const configSource = await
|
|
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
|
|
1321
|
+
const instrumentationSource = await integrationPathExists(instrumentationPath) ? await readIntegrationFile(instrumentationPath) : void 0;
|
|
1337
1322
|
const changes = [
|
|
1338
|
-
|
|
1323
|
+
createIntegrationFileChange(
|
|
1339
1324
|
appRoot,
|
|
1340
1325
|
configPath,
|
|
1341
|
-
transformNextConfig(configPath, configSource),
|
|
1326
|
+
transformNextConfig(configPath, configSource, trustedFastModeAvailable),
|
|
1342
1327
|
configSource
|
|
1343
1328
|
),
|
|
1344
|
-
|
|
1329
|
+
createIntegrationFileChange(
|
|
1345
1330
|
appRoot,
|
|
1346
1331
|
instrumentationPath,
|
|
1347
1332
|
transformInstrumentationClient(instrumentationPath, instrumentationSource ?? ""),
|
|
1348
1333
|
instrumentationSource
|
|
1349
1334
|
),
|
|
1350
|
-
|
|
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({
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
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;
|