@chidchanun/bcp 0.1.8 → 0.1.10
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/CHANGELOG.md +82 -0
- package/README.md +218 -7
- package/docs/application-modules.md +63 -3
- package/docs/releasing.md +34 -19
- package/docs/route-guards.md +240 -0
- package/docs/server-data-loaders.md +240 -0
- package/docs/updating.md +130 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-guards.ts +497 -0
- package/packages/bundler/src/server-production-middleware.ts +33 -8
- package/packages/bundler/src/server-production.ts +25 -0
- package/packages/cli/src/args.ts +58 -0
- package/packages/cli/src/index.ts +41 -13
- package/packages/cli/src/update.ts +860 -0
- package/packages/client/src/index.tsx +5 -0
- package/packages/client/src/loader-data.tsx +229 -0
- package/packages/client/src/router-v2.tsx +254 -29
- package/packages/server/src/dev-navigation-target.ts +188 -0
- package/packages/server/src/index.ts +209 -119
- package/packages/server/src/navigation-payload.ts +24 -1
- package/packages/server/src/navigation-response.ts +284 -0
- package/packages/server/src/page-guard.ts +529 -0
- package/packages/server/src/page-loader.ts +643 -0
- package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
- package/packages/server/src/standalone-production-runtime-v2-navigation.ts +785 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +131 -19
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
- package/packages/server/src/standalone-production-runtime-v4.ts +42 -2
- package/packages/server/src/static-dev-server.ts +21 -17
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
spawn,
|
|
5
|
+
} from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const FRAMEWORK_PACKAGE =
|
|
8
|
+
"@chidchanun/bcp";
|
|
9
|
+
|
|
10
|
+
const LOCKFILES = [
|
|
11
|
+
["package-lock.json", "npm"],
|
|
12
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
13
|
+
["yarn.lock", "yarn"],
|
|
14
|
+
["bun.lock", "bun"],
|
|
15
|
+
["bun.lockb", "bun"],
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
type DependencySectionName =
|
|
19
|
+
| "dependencies"
|
|
20
|
+
| "devDependencies"
|
|
21
|
+
| "optionalDependencies";
|
|
22
|
+
|
|
23
|
+
export type PackageManager =
|
|
24
|
+
| "npm"
|
|
25
|
+
| "pnpm"
|
|
26
|
+
| "yarn"
|
|
27
|
+
| "bun";
|
|
28
|
+
|
|
29
|
+
interface ProjectPackageJson {
|
|
30
|
+
name?: unknown;
|
|
31
|
+
scripts?: Record<string, string>;
|
|
32
|
+
dependencies?: Record<string, string>;
|
|
33
|
+
devDependencies?: Record<string, string>;
|
|
34
|
+
optionalDependencies?: Record<string, string>;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FrameworkDependency {
|
|
39
|
+
section: DependencySectionName;
|
|
40
|
+
key: "bcp" | "@chidchanun/bcp";
|
|
41
|
+
specifier: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UpdatePlan {
|
|
45
|
+
currentSpecifier: string;
|
|
46
|
+
currentVersion: string | null;
|
|
47
|
+
targetVersion: string;
|
|
48
|
+
nextSpecifier: string;
|
|
49
|
+
dependency: FrameworkDependency;
|
|
50
|
+
addUpdateScript: boolean;
|
|
51
|
+
changed: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UpdateCommandOptions {
|
|
55
|
+
rootDirectory: string;
|
|
56
|
+
target?: string;
|
|
57
|
+
check?: boolean;
|
|
58
|
+
dryRun?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runBcpUpdate(
|
|
62
|
+
options: UpdateCommandOptions
|
|
63
|
+
): Promise<UpdatePlan> {
|
|
64
|
+
const rootDirectory =
|
|
65
|
+
path.resolve(
|
|
66
|
+
options.rootDirectory
|
|
67
|
+
);
|
|
68
|
+
const packageFile =
|
|
69
|
+
path.join(
|
|
70
|
+
rootDirectory,
|
|
71
|
+
"package.json"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!fs.existsSync(packageFile)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`BCP Update: package.json was not found in ${rootDirectory}.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const originalPackageText =
|
|
81
|
+
fs.readFileSync(
|
|
82
|
+
packageFile,
|
|
83
|
+
"utf8"
|
|
84
|
+
);
|
|
85
|
+
const packageJson =
|
|
86
|
+
parsePackageJson(
|
|
87
|
+
originalPackageText,
|
|
88
|
+
packageFile
|
|
89
|
+
);
|
|
90
|
+
const target =
|
|
91
|
+
normalizeUpdateTarget(
|
|
92
|
+
options.target
|
|
93
|
+
);
|
|
94
|
+
const targetVersion =
|
|
95
|
+
await resolvePublishedVersion(
|
|
96
|
+
target,
|
|
97
|
+
rootDirectory
|
|
98
|
+
);
|
|
99
|
+
const plan =
|
|
100
|
+
createUpdatePlan(
|
|
101
|
+
packageJson,
|
|
102
|
+
targetVersion
|
|
103
|
+
);
|
|
104
|
+
const installedVersion =
|
|
105
|
+
readInstalledFrameworkVersion(
|
|
106
|
+
rootDirectory
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log(
|
|
111
|
+
`[BCP Update] Project: ${rootDirectory}`
|
|
112
|
+
);
|
|
113
|
+
console.log(
|
|
114
|
+
`[BCP Update] Installed: ${installedVersion ?? plan.currentVersion ?? plan.currentSpecifier}`
|
|
115
|
+
);
|
|
116
|
+
console.log(
|
|
117
|
+
`[BCP Update] Target: ${targetVersion} (${target})`
|
|
118
|
+
);
|
|
119
|
+
console.log(
|
|
120
|
+
`[BCP Update] Dependency: ${plan.dependency.key} ${plan.currentSpecifier} -> ${plan.nextSpecifier}`
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (plan.addUpdateScript) {
|
|
124
|
+
console.log(
|
|
125
|
+
"[BCP Update] Project script: add `update: bcp update`."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
installedVersion === targetVersion &&
|
|
131
|
+
!plan.changed
|
|
132
|
+
) {
|
|
133
|
+
console.log(
|
|
134
|
+
"[BCP Update] Already up to date."
|
|
135
|
+
);
|
|
136
|
+
console.log("");
|
|
137
|
+
return plan;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
options.check ||
|
|
142
|
+
options.dryRun
|
|
143
|
+
) {
|
|
144
|
+
console.log(
|
|
145
|
+
options.check
|
|
146
|
+
? "[BCP Update] Update available; no files were changed (--check)."
|
|
147
|
+
: "[BCP Update] Dry run complete; no files were changed."
|
|
148
|
+
);
|
|
149
|
+
console.log("");
|
|
150
|
+
return plan;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const packageManager =
|
|
154
|
+
detectPackageManager(
|
|
155
|
+
rootDirectory
|
|
156
|
+
);
|
|
157
|
+
const lockfile =
|
|
158
|
+
getPackageManagerLockfile(
|
|
159
|
+
rootDirectory,
|
|
160
|
+
packageManager
|
|
161
|
+
);
|
|
162
|
+
const originalLockfile =
|
|
163
|
+
lockfile &&
|
|
164
|
+
fs.existsSync(lockfile)
|
|
165
|
+
? fs.readFileSync(
|
|
166
|
+
lockfile
|
|
167
|
+
)
|
|
168
|
+
: null;
|
|
169
|
+
|
|
170
|
+
const nextPackageJson =
|
|
171
|
+
applyUpdatePlan(
|
|
172
|
+
packageJson,
|
|
173
|
+
plan
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
writeJsonAtomic(
|
|
177
|
+
packageFile,
|
|
178
|
+
nextPackageJson
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
console.log(
|
|
183
|
+
`[BCP Update] Installing with ${packageManager}...`
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await runPackageInstall(
|
|
187
|
+
packageManager,
|
|
188
|
+
rootDirectory
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
fs.writeFileSync(
|
|
192
|
+
packageFile,
|
|
193
|
+
originalPackageText,
|
|
194
|
+
"utf8"
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (lockfile) {
|
|
198
|
+
if (originalLockfile) {
|
|
199
|
+
fs.writeFileSync(
|
|
200
|
+
lockfile,
|
|
201
|
+
originalLockfile
|
|
202
|
+
);
|
|
203
|
+
} else if (
|
|
204
|
+
fs.existsSync(lockfile)
|
|
205
|
+
) {
|
|
206
|
+
fs.rmSync(
|
|
207
|
+
lockfile,
|
|
208
|
+
{
|
|
209
|
+
force: true,
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new Error(
|
|
216
|
+
`BCP Update: install failed; package.json${lockfile ? " and the detected lockfile" : ""} were restored. ${error instanceof Error ? error.message : String(error)}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const finalVersion =
|
|
221
|
+
readInstalledFrameworkVersion(
|
|
222
|
+
rootDirectory
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
finalVersion &&
|
|
227
|
+
finalVersion !== targetVersion
|
|
228
|
+
) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`BCP Update: install completed but resolved framework version is ${finalVersion}; expected ${targetVersion}.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(
|
|
235
|
+
`[BCP Update] Updated to ${targetVersion}.`
|
|
236
|
+
);
|
|
237
|
+
console.log(
|
|
238
|
+
"[BCP Update] Next time you can run `npm run update` from this project."
|
|
239
|
+
);
|
|
240
|
+
console.log(
|
|
241
|
+
"[BCP Update] Recommended: run your typecheck and test suite before committing package.json and the lockfile."
|
|
242
|
+
);
|
|
243
|
+
console.log("");
|
|
244
|
+
|
|
245
|
+
return plan;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createUpdatePlan(
|
|
249
|
+
packageJson: ProjectPackageJson,
|
|
250
|
+
targetVersion: string
|
|
251
|
+
): UpdatePlan {
|
|
252
|
+
assertVersion(
|
|
253
|
+
targetVersion,
|
|
254
|
+
"resolved target version"
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const dependency =
|
|
258
|
+
findFrameworkDependency(
|
|
259
|
+
packageJson
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
assertRegistryDependency(
|
|
263
|
+
dependency.specifier
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
* Updating is an explicit operation, so pin the resolved registry
|
|
268
|
+
* version. This prevents a later plain package-manager install from
|
|
269
|
+
* silently jumping to a different BCP release than the one the updater
|
|
270
|
+
* verified.
|
|
271
|
+
*/
|
|
272
|
+
const nextSpecifier =
|
|
273
|
+
dependency.key === "bcp"
|
|
274
|
+
? `npm:${FRAMEWORK_PACKAGE}@${targetVersion}`
|
|
275
|
+
: targetVersion;
|
|
276
|
+
const addUpdateScript =
|
|
277
|
+
packageJson.scripts?.update ===
|
|
278
|
+
undefined;
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
currentSpecifier:
|
|
282
|
+
dependency.specifier,
|
|
283
|
+
currentVersion:
|
|
284
|
+
extractVersion(
|
|
285
|
+
dependency.specifier
|
|
286
|
+
),
|
|
287
|
+
targetVersion,
|
|
288
|
+
nextSpecifier,
|
|
289
|
+
dependency,
|
|
290
|
+
addUpdateScript,
|
|
291
|
+
changed:
|
|
292
|
+
dependency.specifier !==
|
|
293
|
+
nextSpecifier ||
|
|
294
|
+
addUpdateScript,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function applyUpdatePlan(
|
|
299
|
+
packageJson: ProjectPackageJson,
|
|
300
|
+
plan: UpdatePlan
|
|
301
|
+
): ProjectPackageJson {
|
|
302
|
+
const copy =
|
|
303
|
+
structuredClone(
|
|
304
|
+
packageJson
|
|
305
|
+
);
|
|
306
|
+
const section =
|
|
307
|
+
copy[
|
|
308
|
+
plan.dependency.section
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
!section ||
|
|
313
|
+
typeof section !== "object"
|
|
314
|
+
) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`BCP Update: dependency section ${plan.dependency.section} disappeared while applying the update.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
(
|
|
321
|
+
section as
|
|
322
|
+
Record<string, string>
|
|
323
|
+
)[
|
|
324
|
+
plan.dependency.key
|
|
325
|
+
] =
|
|
326
|
+
plan.nextSpecifier;
|
|
327
|
+
|
|
328
|
+
if (plan.addUpdateScript) {
|
|
329
|
+
copy.scripts = {
|
|
330
|
+
...copy.scripts,
|
|
331
|
+
update:
|
|
332
|
+
"bcp update",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return copy;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function findFrameworkDependency(
|
|
340
|
+
packageJson: ProjectPackageJson
|
|
341
|
+
): FrameworkDependency {
|
|
342
|
+
const matches:
|
|
343
|
+
FrameworkDependency[] = [];
|
|
344
|
+
|
|
345
|
+
for (
|
|
346
|
+
const sectionName
|
|
347
|
+
of [
|
|
348
|
+
"dependencies",
|
|
349
|
+
"devDependencies",
|
|
350
|
+
"optionalDependencies",
|
|
351
|
+
] as const
|
|
352
|
+
) {
|
|
353
|
+
const section =
|
|
354
|
+
packageJson[
|
|
355
|
+
sectionName
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
if (!section) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (
|
|
363
|
+
const key
|
|
364
|
+
of [
|
|
365
|
+
"bcp",
|
|
366
|
+
FRAMEWORK_PACKAGE,
|
|
367
|
+
] as const
|
|
368
|
+
) {
|
|
369
|
+
const specifier =
|
|
370
|
+
section[key];
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
typeof specifier === "string" &&
|
|
374
|
+
specifier.trim() !== ""
|
|
375
|
+
) {
|
|
376
|
+
matches.push({
|
|
377
|
+
section:
|
|
378
|
+
sectionName,
|
|
379
|
+
key,
|
|
380
|
+
specifier:
|
|
381
|
+
specifier.trim(),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (matches.length === 0) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
"BCP Update: this project does not declare `bcp` or `@chidchanun/bcp` in dependencies."
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (matches.length > 1) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"BCP Update: multiple BCP framework dependencies were found. Keep only one of `bcp` or `@chidchanun/bcp` before updating."
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return matches[0];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function detectPackageManager(
|
|
403
|
+
rootDirectory: string
|
|
404
|
+
): PackageManager {
|
|
405
|
+
const detected =
|
|
406
|
+
LOCKFILES
|
|
407
|
+
.filter(
|
|
408
|
+
([fileName]) =>
|
|
409
|
+
fs.existsSync(
|
|
410
|
+
path.join(
|
|
411
|
+
rootDirectory,
|
|
412
|
+
fileName
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
.map(
|
|
417
|
+
([, manager]) =>
|
|
418
|
+
manager
|
|
419
|
+
);
|
|
420
|
+
const unique =
|
|
421
|
+
Array.from(
|
|
422
|
+
new Set(
|
|
423
|
+
detected
|
|
424
|
+
)
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
if (unique.length > 1) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`BCP Update: multiple package-manager lockfiles were found (${unique.join(", ")}). Keep only the lockfile for the package manager you use.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (unique.length === 1) {
|
|
434
|
+
return unique[0];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const userAgent =
|
|
438
|
+
process.env.npm_config_user_agent ??
|
|
439
|
+
"";
|
|
440
|
+
|
|
441
|
+
if (/^pnpm\//i.test(userAgent)) {
|
|
442
|
+
return "pnpm";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (/^yarn\//i.test(userAgent)) {
|
|
446
|
+
return "yarn";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (/^bun\//i.test(userAgent)) {
|
|
450
|
+
return "bun";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return "npm";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function normalizeUpdateTarget(
|
|
457
|
+
value: string | undefined
|
|
458
|
+
): string {
|
|
459
|
+
const target =
|
|
460
|
+
value?.trim() ||
|
|
461
|
+
"latest";
|
|
462
|
+
|
|
463
|
+
if (
|
|
464
|
+
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(
|
|
465
|
+
target
|
|
466
|
+
) ||
|
|
467
|
+
/^[A-Za-z][A-Za-z0-9._-]*$/.test(
|
|
468
|
+
target
|
|
469
|
+
)
|
|
470
|
+
) {
|
|
471
|
+
return target;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
throw new Error(
|
|
475
|
+
`BCP Update: invalid target ${target}. Use a version such as 0.1.10 or an npm dist-tag such as latest.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function resolvePublishedVersion(
|
|
480
|
+
target: string,
|
|
481
|
+
rootDirectory: string
|
|
482
|
+
): Promise<string> {
|
|
483
|
+
const output =
|
|
484
|
+
await runCommandCapture(
|
|
485
|
+
"npm",
|
|
486
|
+
[
|
|
487
|
+
"view",
|
|
488
|
+
`${FRAMEWORK_PACKAGE}@${target}`,
|
|
489
|
+
"version",
|
|
490
|
+
"--json",
|
|
491
|
+
"--prefer-online",
|
|
492
|
+
],
|
|
493
|
+
rootDirectory
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
let parsed:
|
|
497
|
+
unknown;
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
parsed =
|
|
501
|
+
JSON.parse(
|
|
502
|
+
output
|
|
503
|
+
);
|
|
504
|
+
} catch {
|
|
505
|
+
parsed =
|
|
506
|
+
output
|
|
507
|
+
.trim()
|
|
508
|
+
.replace(
|
|
509
|
+
/^"|"$/g,
|
|
510
|
+
""
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (
|
|
515
|
+
Array.isArray(parsed)
|
|
516
|
+
) {
|
|
517
|
+
parsed =
|
|
518
|
+
parsed.at(-1);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const version =
|
|
522
|
+
String(
|
|
523
|
+
parsed ?? ""
|
|
524
|
+
).trim();
|
|
525
|
+
|
|
526
|
+
assertVersion(
|
|
527
|
+
version,
|
|
528
|
+
`${FRAMEWORK_PACKAGE}@${target}`
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
return version;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function assertRegistryDependency(
|
|
535
|
+
specifier: string
|
|
536
|
+
): void {
|
|
537
|
+
if (
|
|
538
|
+
/^(?:file:|link:|workspace:|git\+|https?:)/i.test(
|
|
539
|
+
specifier
|
|
540
|
+
)
|
|
541
|
+
) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`BCP Update: dependency specifier ${specifier} is not a published npm dependency. Use a registry dependency before running update.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function extractVersion(
|
|
549
|
+
specifier: string
|
|
550
|
+
): string | null {
|
|
551
|
+
const match =
|
|
552
|
+
/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(
|
|
553
|
+
specifier
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
return match?.[1] ??
|
|
557
|
+
null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function readInstalledFrameworkVersion(
|
|
561
|
+
rootDirectory: string
|
|
562
|
+
): string | null {
|
|
563
|
+
for (
|
|
564
|
+
const packageFile
|
|
565
|
+
of [
|
|
566
|
+
path.join(
|
|
567
|
+
rootDirectory,
|
|
568
|
+
"node_modules",
|
|
569
|
+
"bcp",
|
|
570
|
+
"package.json"
|
|
571
|
+
),
|
|
572
|
+
path.join(
|
|
573
|
+
rootDirectory,
|
|
574
|
+
"node_modules",
|
|
575
|
+
"@chidchanun",
|
|
576
|
+
"bcp",
|
|
577
|
+
"package.json"
|
|
578
|
+
),
|
|
579
|
+
]
|
|
580
|
+
) {
|
|
581
|
+
if (!fs.existsSync(packageFile)) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const metadata =
|
|
587
|
+
JSON.parse(
|
|
588
|
+
fs.readFileSync(
|
|
589
|
+
packageFile,
|
|
590
|
+
"utf8"
|
|
591
|
+
)
|
|
592
|
+
) as {
|
|
593
|
+
version?: unknown;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
typeof metadata.version === "string" &&
|
|
598
|
+
/^\d+\.\d+\.\d+/.test(
|
|
599
|
+
metadata.version
|
|
600
|
+
)
|
|
601
|
+
) {
|
|
602
|
+
return metadata.version;
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
// Fall back to the declared dependency version.
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function getPackageManagerLockfile(
|
|
613
|
+
rootDirectory: string,
|
|
614
|
+
manager: PackageManager
|
|
615
|
+
): string | null {
|
|
616
|
+
const entry =
|
|
617
|
+
LOCKFILES.find(
|
|
618
|
+
([fileName, candidate]) =>
|
|
619
|
+
candidate === manager &&
|
|
620
|
+
fs.existsSync(
|
|
621
|
+
path.join(
|
|
622
|
+
rootDirectory,
|
|
623
|
+
fileName
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
return entry
|
|
629
|
+
? path.join(
|
|
630
|
+
rootDirectory,
|
|
631
|
+
entry[0]
|
|
632
|
+
)
|
|
633
|
+
: null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function runPackageInstall(
|
|
637
|
+
packageManager: PackageManager,
|
|
638
|
+
rootDirectory: string
|
|
639
|
+
): Promise<void> {
|
|
640
|
+
return runCommandInherited(
|
|
641
|
+
packageManager,
|
|
642
|
+
[
|
|
643
|
+
"install",
|
|
644
|
+
],
|
|
645
|
+
rootDirectory
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function runCommandCapture(
|
|
650
|
+
command: string,
|
|
651
|
+
args: string[],
|
|
652
|
+
cwd: string
|
|
653
|
+
): Promise<string> {
|
|
654
|
+
const invocation =
|
|
655
|
+
resolveCommandInvocation(
|
|
656
|
+
command,
|
|
657
|
+
args
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
return new Promise<string>(
|
|
661
|
+
(
|
|
662
|
+
resolve,
|
|
663
|
+
reject
|
|
664
|
+
) => {
|
|
665
|
+
const chunks:
|
|
666
|
+
string[] = [];
|
|
667
|
+
const errors:
|
|
668
|
+
string[] = [];
|
|
669
|
+
const child =
|
|
670
|
+
spawn(
|
|
671
|
+
invocation.command,
|
|
672
|
+
invocation.args,
|
|
673
|
+
{
|
|
674
|
+
cwd,
|
|
675
|
+
env:
|
|
676
|
+
process.env,
|
|
677
|
+
stdio: [
|
|
678
|
+
"ignore",
|
|
679
|
+
"pipe",
|
|
680
|
+
"pipe",
|
|
681
|
+
],
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
child.stdout?.on(
|
|
686
|
+
"data",
|
|
687
|
+
(chunk) => {
|
|
688
|
+
chunks.push(
|
|
689
|
+
chunk.toString()
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
child.stderr?.on(
|
|
694
|
+
"data",
|
|
695
|
+
(chunk) => {
|
|
696
|
+
errors.push(
|
|
697
|
+
chunk.toString()
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
child.once(
|
|
702
|
+
"error",
|
|
703
|
+
reject
|
|
704
|
+
);
|
|
705
|
+
child.once(
|
|
706
|
+
"exit",
|
|
707
|
+
(code) => {
|
|
708
|
+
if (code === 0) {
|
|
709
|
+
resolve(
|
|
710
|
+
chunks.join("")
|
|
711
|
+
);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
reject(
|
|
716
|
+
new Error(
|
|
717
|
+
`${command} ${args.join(" ")} failed with exit code ${code}. ${errors.join("").trim()}`
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function runCommandInherited(
|
|
727
|
+
command: string,
|
|
728
|
+
args: string[],
|
|
729
|
+
cwd: string
|
|
730
|
+
): Promise<void> {
|
|
731
|
+
const invocation =
|
|
732
|
+
resolveCommandInvocation(
|
|
733
|
+
command,
|
|
734
|
+
args
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
return new Promise<void>(
|
|
738
|
+
(
|
|
739
|
+
resolve,
|
|
740
|
+
reject
|
|
741
|
+
) => {
|
|
742
|
+
const child =
|
|
743
|
+
spawn(
|
|
744
|
+
invocation.command,
|
|
745
|
+
invocation.args,
|
|
746
|
+
{
|
|
747
|
+
cwd,
|
|
748
|
+
env:
|
|
749
|
+
process.env,
|
|
750
|
+
stdio:
|
|
751
|
+
"inherit",
|
|
752
|
+
}
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
child.once(
|
|
756
|
+
"error",
|
|
757
|
+
reject
|
|
758
|
+
);
|
|
759
|
+
child.once(
|
|
760
|
+
"exit",
|
|
761
|
+
(code) => {
|
|
762
|
+
if (code === 0) {
|
|
763
|
+
resolve();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
reject(
|
|
768
|
+
new Error(
|
|
769
|
+
`${command} install failed with exit code ${code}.`
|
|
770
|
+
)
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function resolveCommandInvocation(
|
|
779
|
+
command: string,
|
|
780
|
+
args: string[]
|
|
781
|
+
): {
|
|
782
|
+
command: string;
|
|
783
|
+
args: string[];
|
|
784
|
+
} {
|
|
785
|
+
if (
|
|
786
|
+
process.platform === "win32"
|
|
787
|
+
) {
|
|
788
|
+
return {
|
|
789
|
+
command:
|
|
790
|
+
process.env.ComSpec ??
|
|
791
|
+
"cmd.exe",
|
|
792
|
+
args: [
|
|
793
|
+
"/d",
|
|
794
|
+
"/s",
|
|
795
|
+
"/c",
|
|
796
|
+
command,
|
|
797
|
+
...args,
|
|
798
|
+
],
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
command,
|
|
804
|
+
args,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function writeJsonAtomic(
|
|
809
|
+
filePath: string,
|
|
810
|
+
value: unknown
|
|
811
|
+
): void {
|
|
812
|
+
const temporary =
|
|
813
|
+
`${filePath}.bcp-update-${process.pid}`;
|
|
814
|
+
const content =
|
|
815
|
+
`${JSON.stringify(
|
|
816
|
+
value,
|
|
817
|
+
null,
|
|
818
|
+
2
|
|
819
|
+
)}\n`;
|
|
820
|
+
|
|
821
|
+
fs.writeFileSync(
|
|
822
|
+
temporary,
|
|
823
|
+
content,
|
|
824
|
+
"utf8"
|
|
825
|
+
);
|
|
826
|
+
fs.renameSync(
|
|
827
|
+
temporary,
|
|
828
|
+
filePath
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function parsePackageJson(
|
|
833
|
+
content: string,
|
|
834
|
+
filePath: string
|
|
835
|
+
): ProjectPackageJson {
|
|
836
|
+
try {
|
|
837
|
+
return JSON.parse(
|
|
838
|
+
content
|
|
839
|
+
) as ProjectPackageJson;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`BCP Update: invalid package.json at ${filePath}. ${error instanceof Error ? error.message : String(error)}`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function assertVersion(
|
|
848
|
+
value: string,
|
|
849
|
+
label: string
|
|
850
|
+
): void {
|
|
851
|
+
if (
|
|
852
|
+
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(
|
|
853
|
+
value
|
|
854
|
+
)
|
|
855
|
+
) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`BCP Update: ${label} resolved to invalid version ${value || "(empty)"}.`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
}
|