@superblocksteam/sdk 2.0.115-next.0 → 2.0.115-next.2
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/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/dev-s3-restore.test.d.mts +2 -0
- package/dist/cli-replacement/dev-s3-restore.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs +457 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.d.mts +7 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +142 -3
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.d.mts +26 -0
- package/dist/cli-replacement/package-json-snapshot.d.mts.map +1 -0
- package/dist/cli-replacement/package-json-snapshot.mjs +222 -0
- package/dist/cli-replacement/package-json-snapshot.mjs.map +1 -0
- package/dist/cli-replacement/package-json-snapshot.test.d.mts +2 -0
- package/dist/cli-replacement/package-json-snapshot.test.d.mts.map +1 -0
- package/dist/cli-replacement/package-json-snapshot.test.mjs +207 -0
- package/dist/cli-replacement/package-json-snapshot.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-persist.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-persist.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-persist.test.mjs +77 -0
- package/dist/dev-utils/dev-server-persist.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +1 -0
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +85 -55
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/vite-dev-server-diagnostics.d.mts +61 -0
- package/dist/dev-utils/vite-dev-server-diagnostics.d.mts.map +1 -0
- package/dist/dev-utils/vite-dev-server-diagnostics.mjs +133 -0
- package/dist/dev-utils/vite-dev-server-diagnostics.mjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/telemetry/logging.d.ts.map +1 -1
- package/dist/telemetry/logging.js +22 -10
- package/dist/telemetry/logging.js.map +1 -1
- package/dist/telemetry/logging.test.d.ts +2 -0
- package/dist/telemetry/logging.test.d.ts.map +1 -0
- package/dist/telemetry/logging.test.js +104 -0
- package/dist/telemetry/logging.test.js.map +1 -0
- package/package.json +7 -7
- package/src/cli-replacement/dev-s3-restore.test.mts +599 -0
- package/src/cli-replacement/dev.mts +202 -6
- package/src/cli-replacement/package-json-snapshot.mts +328 -0
- package/src/cli-replacement/package-json-snapshot.test.mts +250 -0
- package/src/dev-utils/dev-server-persist.test.mts +96 -0
- package/src/dev-utils/dev-server.mts +106 -75
- package/src/dev-utils/vite-dev-server-diagnostics.mts +213 -0
- package/src/index.ts +15 -0
- package/src/telemetry/logging.test.ts +142 -0
- package/src/telemetry/logging.ts +30 -10
- package/test/vite-dev-server-diagnostics.test.mts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/turbo.json +1 -0
|
@@ -58,6 +58,13 @@ import {
|
|
|
58
58
|
ensureRemoteHasDefaultBranch,
|
|
59
59
|
getGitErrorFields,
|
|
60
60
|
} from "./git-repo-setup.mjs";
|
|
61
|
+
import {
|
|
62
|
+
didPackageJsonSnapshotChange,
|
|
63
|
+
packageJsonSnapshot,
|
|
64
|
+
packageJsonSnapshotDiagnostic,
|
|
65
|
+
type PackageJsonSnapshot,
|
|
66
|
+
restoreManagedPackageDependencies,
|
|
67
|
+
} from "./package-json-snapshot.mjs";
|
|
61
68
|
import { getCurrentCliVersion } from "./version-detection.js";
|
|
62
69
|
|
|
63
70
|
const exec = promisify(child_process.exec);
|
|
@@ -296,6 +303,15 @@ export async function dev(options: {
|
|
|
296
303
|
|
|
297
304
|
/** Pre-existing HTTP server from warm standby mode (avoids port gap on transition). */
|
|
298
305
|
existingServer?: HttpServer;
|
|
306
|
+
|
|
307
|
+
/** Force package installation when the caller has detected dependency drift. */
|
|
308
|
+
forcePackageInstall?: boolean;
|
|
309
|
+
|
|
310
|
+
/** Package snapshot before S3 workspace restore; used to refresh forced install after DBFS sync. */
|
|
311
|
+
packageJsonSnapshotBeforeRestore?: PackageJsonSnapshot | null;
|
|
312
|
+
|
|
313
|
+
/** Restore managed warm-template package pins after DBFS download in S3 warm restore flow. */
|
|
314
|
+
normalizeManagedPackageDependencies?: boolean;
|
|
299
315
|
}) {
|
|
300
316
|
const {
|
|
301
317
|
cwd,
|
|
@@ -566,6 +582,9 @@ export async function dev(options: {
|
|
|
566
582
|
}
|
|
567
583
|
|
|
568
584
|
let hasPackageChanged = false;
|
|
585
|
+
let packageJsonRequiresInstall = false;
|
|
586
|
+
const hasPackageJsonSnapshotBeforeRestore =
|
|
587
|
+
options.packageJsonSnapshotBeforeRestore !== undefined;
|
|
569
588
|
|
|
570
589
|
const packageJsonBefore = await readPkgJson(cwd);
|
|
571
590
|
|
|
@@ -576,6 +595,17 @@ export async function dev(options: {
|
|
|
576
595
|
);
|
|
577
596
|
|
|
578
597
|
await syncService!.downloadDirectory();
|
|
598
|
+
if (
|
|
599
|
+
options.normalizeManagedPackageDependencies &&
|
|
600
|
+
(await restoreManagedPackageDependencies(
|
|
601
|
+
cwd,
|
|
602
|
+
packageJsonBefore,
|
|
603
|
+
))
|
|
604
|
+
) {
|
|
605
|
+
logger.info(
|
|
606
|
+
"Restored managed package dependencies to the warm template versions after DBFS download",
|
|
607
|
+
);
|
|
608
|
+
}
|
|
579
609
|
span.end();
|
|
580
610
|
});
|
|
581
611
|
}
|
|
@@ -603,6 +633,100 @@ export async function dev(options: {
|
|
|
603
633
|
applicationId: applicationConfig.id,
|
|
604
634
|
workDir: cwd,
|
|
605
635
|
});
|
|
636
|
+
|
|
637
|
+
// In CSB mode, schedule a background retry loop. The app's
|
|
638
|
+
// git config may not be persisted yet at claim time (e.g. git
|
|
639
|
+
// was connected moments before the CSB was assigned). Without
|
|
640
|
+
// this, the CSB runs its entire lifetime without git — meaning
|
|
641
|
+
// changes are only in DBFS and are lost if the CSB recycles.
|
|
642
|
+
if (lockType === LockType.CSB) {
|
|
643
|
+
const GIT_RETRY_DELAY_MS = 30_000;
|
|
644
|
+
const GIT_RETRY_MAX_ATTEMPTS = 10;
|
|
645
|
+
const scheduleRetry = (attempt: number): void => {
|
|
646
|
+
if (attempt > GIT_RETRY_MAX_ATTEMPTS) {
|
|
647
|
+
logger.info(
|
|
648
|
+
"[git] background bootstrap retry exhausted, giving up",
|
|
649
|
+
{
|
|
650
|
+
gitCategory: "setup",
|
|
651
|
+
gitOperation: "background-retry",
|
|
652
|
+
gitOutcome: "exhausted",
|
|
653
|
+
gitAttempt: attempt - 1,
|
|
654
|
+
applicationId: applicationConfig.id,
|
|
655
|
+
},
|
|
656
|
+
);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const timer = setTimeout(() => {
|
|
660
|
+
void (async () => {
|
|
661
|
+
if (gitService) return;
|
|
662
|
+
try {
|
|
663
|
+
const svc = await bootstrapGitService({
|
|
664
|
+
sdk,
|
|
665
|
+
applicationId: applicationConfig.id,
|
|
666
|
+
cwd,
|
|
667
|
+
logger,
|
|
668
|
+
userName: gitUserName,
|
|
669
|
+
userEmail: gitUserEmail,
|
|
670
|
+
superblocksBaseUrl:
|
|
671
|
+
tokenConfig.superblocksBaseUrl,
|
|
672
|
+
});
|
|
673
|
+
if (!svc) {
|
|
674
|
+
scheduleRetry(attempt + 1);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
gitService = svc;
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
await fetchAndEnsureLiveBranch(
|
|
682
|
+
svc,
|
|
683
|
+
"Git background retry fetch failed",
|
|
684
|
+
);
|
|
685
|
+
} catch {
|
|
686
|
+
// non-fatal
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
activeDbfsBranchName =
|
|
690
|
+
await ensureRuntimeDbfsBranchConsistency({
|
|
691
|
+
sdk,
|
|
692
|
+
applicationConfig,
|
|
693
|
+
logger,
|
|
694
|
+
lockService,
|
|
695
|
+
syncService,
|
|
696
|
+
currentBranchName: activeDbfsBranchName,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
logger.info(
|
|
700
|
+
"[git] background bootstrap retry succeeded",
|
|
701
|
+
{
|
|
702
|
+
gitCategory: "setup",
|
|
703
|
+
gitOperation: "background-retry",
|
|
704
|
+
gitOutcome: "success",
|
|
705
|
+
gitAttempt: attempt,
|
|
706
|
+
applicationId: applicationConfig.id,
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
} catch (err) {
|
|
710
|
+
logger.warn(
|
|
711
|
+
"[git] background bootstrap retry failed",
|
|
712
|
+
{
|
|
713
|
+
gitCategory: "setup",
|
|
714
|
+
gitOperation: "background-retry",
|
|
715
|
+
gitOutcome: "failed",
|
|
716
|
+
gitAttempt: attempt,
|
|
717
|
+
applicationId: applicationConfig.id,
|
|
718
|
+
...getGitErrorFields(err),
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
scheduleRetry(attempt + 1);
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
}, GIT_RETRY_DELAY_MS);
|
|
725
|
+
timer.unref();
|
|
726
|
+
};
|
|
727
|
+
scheduleRetry(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
606
730
|
gitSpan.end();
|
|
607
731
|
return;
|
|
608
732
|
}
|
|
@@ -720,16 +844,73 @@ export async function dev(options: {
|
|
|
720
844
|
|
|
721
845
|
const packageJsonAfter = await readPkgJson(cwd);
|
|
722
846
|
|
|
847
|
+
let packageJsonSnapshotBefore: PackageJsonSnapshot | null = null;
|
|
848
|
+
let packageJsonSnapshotAfter: PackageJsonSnapshot | null = null;
|
|
849
|
+
let packageJsonInstallBaselineSnapshot: PackageJsonSnapshot | null =
|
|
850
|
+
null;
|
|
851
|
+
|
|
723
852
|
if (packageJsonBefore && packageJsonAfter) {
|
|
724
853
|
hasPackageChanged =
|
|
725
854
|
JSON.stringify(packageJsonBefore, null, 2) !==
|
|
726
855
|
JSON.stringify(packageJsonAfter, null, 2);
|
|
727
856
|
} else if (packageJsonAfter) {
|
|
728
857
|
hasPackageChanged = true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (packageJsonBefore) {
|
|
861
|
+
packageJsonSnapshotBefore =
|
|
862
|
+
packageJsonSnapshot(packageJsonBefore);
|
|
863
|
+
}
|
|
864
|
+
if (packageJsonAfter) {
|
|
865
|
+
packageJsonSnapshotAfter = packageJsonSnapshot(packageJsonAfter);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
packageJsonInstallBaselineSnapshot =
|
|
869
|
+
hasPackageJsonSnapshotBeforeRestore
|
|
870
|
+
? (options.packageJsonSnapshotBeforeRestore ?? null)
|
|
871
|
+
: packageJsonSnapshotBefore;
|
|
872
|
+
if (packageJsonAfter) {
|
|
873
|
+
packageJsonRequiresInstall = didPackageJsonSnapshotChange(
|
|
874
|
+
packageJsonInstallBaselineSnapshot,
|
|
875
|
+
packageJsonSnapshotAfter,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (!packageJsonBefore && packageJsonRequiresInstall) {
|
|
729
879
|
logger.info("package.json was created, installing packages…");
|
|
730
880
|
}
|
|
881
|
+
const forcePackageInstallRequested = !!options.forcePackageInstall;
|
|
882
|
+
let forcePackageInstall = forcePackageInstallRequested;
|
|
883
|
+
if (
|
|
884
|
+
forcePackageInstallRequested &&
|
|
885
|
+
hasPackageJsonSnapshotBeforeRestore
|
|
886
|
+
) {
|
|
887
|
+
forcePackageInstall = packageJsonRequiresInstall;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
logger.info("Package install decision", {
|
|
891
|
+
packageJsonBeforePresent: !!packageJsonBefore,
|
|
892
|
+
packageJsonAfterPresent: !!packageJsonAfter,
|
|
893
|
+
hasPackageChanged,
|
|
894
|
+
packageJsonRequiresInstall,
|
|
895
|
+
forcePackageInstall,
|
|
896
|
+
forcePackageInstallRequested,
|
|
897
|
+
upgradePromiseCount: upgradePromises.length,
|
|
898
|
+
packageJsonSnapshotBefore: packageJsonSnapshotDiagnostic(
|
|
899
|
+
packageJsonSnapshotBefore,
|
|
900
|
+
),
|
|
901
|
+
packageJsonInstallBaselineSnapshot: packageJsonSnapshotDiagnostic(
|
|
902
|
+
packageJsonInstallBaselineSnapshot,
|
|
903
|
+
),
|
|
904
|
+
packageJsonSnapshotAfter: packageJsonSnapshotDiagnostic(
|
|
905
|
+
packageJsonSnapshotAfter,
|
|
906
|
+
),
|
|
907
|
+
});
|
|
731
908
|
|
|
732
|
-
if (
|
|
909
|
+
if (
|
|
910
|
+
packageJsonRequiresInstall ||
|
|
911
|
+
upgradePromises.length > 0 ||
|
|
912
|
+
forcePackageInstall
|
|
913
|
+
) {
|
|
733
914
|
logger.info("Installing packages…");
|
|
734
915
|
await tracer.startActiveSpan("installPackages", async (span) => {
|
|
735
916
|
try {
|
|
@@ -748,7 +929,9 @@ export async function dev(options: {
|
|
|
748
929
|
);
|
|
749
930
|
}
|
|
750
931
|
|
|
751
|
-
|
|
932
|
+
const shouldUploadPackageState =
|
|
933
|
+
hasPackageChanged || forcePackageInstall;
|
|
934
|
+
if (shouldUploadPackageState || uploadFirst) {
|
|
752
935
|
logger.info(
|
|
753
936
|
`Uploading local files to branch '${activeDbfsBranchName}' on server before starting`,
|
|
754
937
|
);
|
|
@@ -1230,10 +1413,23 @@ async function ensureLiveBranchCheckedOutAfterFetch(
|
|
|
1230
1413
|
// If remote live branch exists, initialize/switch local live branch from it.
|
|
1231
1414
|
// This handles fresh repos where HEAD is unborn before first fetch.
|
|
1232
1415
|
if (await canResolveRef(git, `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`)) {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
await git.checkoutOrCreate(
|
|
1418
|
+
SUPERBLOCKS_LIVE_GIT_BRANCH,
|
|
1419
|
+
`origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
|
|
1420
|
+
);
|
|
1421
|
+
} catch {
|
|
1422
|
+
// Pre-warmed CSBs download DBFS files before git is set up, leaving
|
|
1423
|
+
// untracked working tree files that collide with the branch contents.
|
|
1424
|
+
// Force-checkout is safe here — the files are identical.
|
|
1425
|
+
await git.raw([
|
|
1426
|
+
"checkout",
|
|
1427
|
+
"-f",
|
|
1428
|
+
"-B",
|
|
1429
|
+
SUPERBLOCKS_LIVE_GIT_BRANCH,
|
|
1430
|
+
`origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
|
|
1431
|
+
]);
|
|
1432
|
+
}
|
|
1237
1433
|
return;
|
|
1238
1434
|
}
|
|
1239
1435
|
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const SUPERBLOCKS_LIBRARY_PACKAGE = "@superblocksteam/library";
|
|
6
|
+
export const SUPERBLOCKS_SDK_API_PACKAGE = "@superblocksteam/sdk-api";
|
|
7
|
+
|
|
8
|
+
export const MANAGED_PACKAGE_DEPENDENCIES = [
|
|
9
|
+
SUPERBLOCKS_LIBRARY_PACKAGE,
|
|
10
|
+
SUPERBLOCKS_SDK_API_PACKAGE,
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export const PACKAGE_DEPENDENCY_FIELDS = [
|
|
14
|
+
"dependencies",
|
|
15
|
+
"devDependencies",
|
|
16
|
+
"peerDependencies",
|
|
17
|
+
"optionalDependencies",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const PACKAGE_INSTALL_METADATA_FIELDS = [
|
|
21
|
+
"dependenciesMeta",
|
|
22
|
+
"devEngines",
|
|
23
|
+
"engines",
|
|
24
|
+
"overrides",
|
|
25
|
+
"packageManager",
|
|
26
|
+
"peerDependenciesMeta",
|
|
27
|
+
"pnpm",
|
|
28
|
+
"resolutions",
|
|
29
|
+
"workspaces",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
const PACKAGE_BUNDLE_DEPENDENCIES_FIELDS = [
|
|
33
|
+
"bundleDependencies",
|
|
34
|
+
"bundledDependencies",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export type PackageJsonSnapshot = {
|
|
38
|
+
value: string;
|
|
39
|
+
diagnostic: {
|
|
40
|
+
sha256: string;
|
|
41
|
+
bytes: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type PackageJsonSnapshotReadResult = {
|
|
46
|
+
packageJson: unknown | null;
|
|
47
|
+
snapshot: PackageJsonSnapshot | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function packageJsonSnapshot(packageJson: unknown): PackageJsonSnapshot {
|
|
51
|
+
const packageJsonObject = packageJsonRecord(packageJson);
|
|
52
|
+
const normalized = PACKAGE_DEPENDENCY_FIELDS.reduce<Record<string, unknown>>(
|
|
53
|
+
(acc, field) => {
|
|
54
|
+
const dependencies = packageJsonObject
|
|
55
|
+
? dependencyMap(packageJsonObject, field)
|
|
56
|
+
: undefined;
|
|
57
|
+
if (dependencies) {
|
|
58
|
+
acc[field] = dependencies;
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
},
|
|
62
|
+
{},
|
|
63
|
+
);
|
|
64
|
+
if (packageJsonObject) {
|
|
65
|
+
const bundleDependenciesField = PACKAGE_BUNDLE_DEPENDENCIES_FIELDS.find(
|
|
66
|
+
(field) => Object.prototype.hasOwnProperty.call(packageJsonObject, field),
|
|
67
|
+
);
|
|
68
|
+
if (bundleDependenciesField) {
|
|
69
|
+
normalized.bundleDependencies =
|
|
70
|
+
packageJsonObject[bundleDependenciesField];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const field of PACKAGE_INSTALL_METADATA_FIELDS) {
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(packageJsonObject, field)) {
|
|
75
|
+
normalized[field] = packageJsonObject[field];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const value = stableStringify(normalized);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
value,
|
|
83
|
+
diagnostic: {
|
|
84
|
+
sha256: createHash("sha256").update(value).digest("hex"),
|
|
85
|
+
bytes: Buffer.byteLength(value),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function didPackageJsonSnapshotChange(
|
|
91
|
+
before: PackageJsonSnapshot | null,
|
|
92
|
+
after: PackageJsonSnapshot | null,
|
|
93
|
+
): boolean {
|
|
94
|
+
if (before?.value === after?.value) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If restore removes package.json, there is no manifest to install from.
|
|
99
|
+
// Treat that as no install work instead of forcing an install that cannot
|
|
100
|
+
// converge until DBFS or another source recreates the manifest.
|
|
101
|
+
return after !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function packageJsonSnapshotDiagnostic(
|
|
105
|
+
snapshot: PackageJsonSnapshot | null,
|
|
106
|
+
): { present: boolean; sha256?: string; bytes?: number } {
|
|
107
|
+
if (snapshot === null) {
|
|
108
|
+
return { present: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
present: true,
|
|
113
|
+
...snapshot.diagnostic,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type PackageJsonObject = Record<string, unknown>;
|
|
118
|
+
type PackageDependencySpec = { field: string; value: unknown };
|
|
119
|
+
|
|
120
|
+
function stableStringify(value: unknown): string {
|
|
121
|
+
return stableStringifyValue(value) ?? "null";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function stableStringifyValue(value: unknown): string | undefined {
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
const items = value.map((item) => stableStringifyValue(item) ?? "null");
|
|
131
|
+
return `[${items.join(",")}]`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (value && typeof value === "object") {
|
|
135
|
+
const object = value as Record<string, unknown>;
|
|
136
|
+
const entries = Object.keys(object)
|
|
137
|
+
.sort()
|
|
138
|
+
.flatMap((key) => {
|
|
139
|
+
const serializedValue = stableStringifyValue(object[key]);
|
|
140
|
+
return serializedValue === undefined
|
|
141
|
+
? []
|
|
142
|
+
: [`${JSON.stringify(key)}:${serializedValue}`];
|
|
143
|
+
});
|
|
144
|
+
return `{${entries.join(",")}}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const serialized = JSON.stringify(value);
|
|
148
|
+
return serialized === undefined ? undefined : serialized;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function packageJsonRecord(packageJson: unknown): PackageJsonObject | null {
|
|
152
|
+
if (
|
|
153
|
+
!packageJson ||
|
|
154
|
+
typeof packageJson !== "object" ||
|
|
155
|
+
Array.isArray(packageJson)
|
|
156
|
+
) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return packageJson as PackageJsonObject;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function dependencyMap(
|
|
164
|
+
packageJson: PackageJsonObject,
|
|
165
|
+
field: string,
|
|
166
|
+
): Record<string, unknown> | undefined {
|
|
167
|
+
const dependencies = packageJson[field];
|
|
168
|
+
if (
|
|
169
|
+
dependencies &&
|
|
170
|
+
typeof dependencies === "object" &&
|
|
171
|
+
!Array.isArray(dependencies)
|
|
172
|
+
) {
|
|
173
|
+
return dependencies as Record<string, unknown>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function packageDependencySpecs(
|
|
180
|
+
packageJson: PackageJsonObject,
|
|
181
|
+
packageName: string,
|
|
182
|
+
): PackageDependencySpec[] {
|
|
183
|
+
return PACKAGE_DEPENDENCY_FIELDS.flatMap((field) => {
|
|
184
|
+
const dependencies = dependencyMap(packageJson, field);
|
|
185
|
+
if (
|
|
186
|
+
dependencies &&
|
|
187
|
+
Object.prototype.hasOwnProperty.call(dependencies, packageName)
|
|
188
|
+
) {
|
|
189
|
+
return [{ field, value: dependencies[packageName] }];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function dependencySpecEquals(a: unknown, b: unknown): boolean {
|
|
197
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function readPackageJson(cwd: string): Promise<unknown | null> {
|
|
201
|
+
return (await readPackageJsonFile(cwd))?.packageJson ?? null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readPackageJsonFile(
|
|
205
|
+
cwd: string,
|
|
206
|
+
): Promise<{ packageJson: unknown; source: string } | null> {
|
|
207
|
+
try {
|
|
208
|
+
const source = await fs.readFile(join(cwd, "package.json"), "utf-8");
|
|
209
|
+
return {
|
|
210
|
+
packageJson: JSON.parse(source),
|
|
211
|
+
source,
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function detectJsonIndent(source: string): number | string {
|
|
219
|
+
return source.match(/\n([ \t]+)"/)?.[1] ?? 2;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function hasFinalNewline(source: string): boolean {
|
|
223
|
+
return source.endsWith("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function readPackageJsonSnapshot(
|
|
227
|
+
cwd: string,
|
|
228
|
+
): Promise<PackageJsonSnapshot | null> {
|
|
229
|
+
const packageJson = await readPackageJson(cwd);
|
|
230
|
+
if (!packageJson) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return packageJsonSnapshot(packageJson);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function readPackageJsonSnapshotWithSource(
|
|
237
|
+
cwd: string,
|
|
238
|
+
): Promise<PackageJsonSnapshotReadResult> {
|
|
239
|
+
const packageJson = await readPackageJson(cwd);
|
|
240
|
+
return {
|
|
241
|
+
packageJson,
|
|
242
|
+
snapshot: packageJson ? packageJsonSnapshot(packageJson) : null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function restoreManagedPackageDependencies(
|
|
247
|
+
cwd: string,
|
|
248
|
+
warmPackageJson: unknown | null,
|
|
249
|
+
): Promise<boolean> {
|
|
250
|
+
const warmPackageJsonObject = packageJsonRecord(warmPackageJson);
|
|
251
|
+
if (!warmPackageJsonObject) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const restoredPackageJsonFile = await readPackageJsonFile(cwd);
|
|
256
|
+
if (!restoredPackageJsonFile) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const restoredPackageJson = packageJsonRecord(
|
|
261
|
+
restoredPackageJsonFile.packageJson,
|
|
262
|
+
);
|
|
263
|
+
if (!restoredPackageJson) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
const restoredPackageJsonSource = restoredPackageJsonFile.source;
|
|
267
|
+
|
|
268
|
+
let changed = false;
|
|
269
|
+
for (const packageName of MANAGED_PACKAGE_DEPENDENCIES) {
|
|
270
|
+
const [warmPackageSpec] = packageDependencySpecs(
|
|
271
|
+
warmPackageJsonObject,
|
|
272
|
+
packageName,
|
|
273
|
+
);
|
|
274
|
+
if (!warmPackageSpec) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const restoredPackageSpecs = packageDependencySpecs(
|
|
279
|
+
restoredPackageJson,
|
|
280
|
+
packageName,
|
|
281
|
+
);
|
|
282
|
+
if (
|
|
283
|
+
restoredPackageSpecs.length === 1 &&
|
|
284
|
+
restoredPackageSpecs[0].field === warmPackageSpec.field &&
|
|
285
|
+
dependencySpecEquals(restoredPackageSpecs[0].value, warmPackageSpec.value)
|
|
286
|
+
) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
restoredPackageSpecs.length === 1 &&
|
|
292
|
+
restoredPackageSpecs[0].field === warmPackageSpec.field
|
|
293
|
+
) {
|
|
294
|
+
dependencyMap(restoredPackageJson, warmPackageSpec.field)![packageName] =
|
|
295
|
+
warmPackageSpec.value;
|
|
296
|
+
} else {
|
|
297
|
+
for (const field of PACKAGE_DEPENDENCY_FIELDS) {
|
|
298
|
+
delete dependencyMap(restoredPackageJson, field)?.[packageName];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let dependencies = dependencyMap(
|
|
302
|
+
restoredPackageJson,
|
|
303
|
+
warmPackageSpec.field,
|
|
304
|
+
);
|
|
305
|
+
if (!dependencies) {
|
|
306
|
+
dependencies = {};
|
|
307
|
+
restoredPackageJson[warmPackageSpec.field] = dependencies;
|
|
308
|
+
}
|
|
309
|
+
dependencies[packageName] = warmPackageSpec.value;
|
|
310
|
+
}
|
|
311
|
+
changed = true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!changed) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await fs.writeFile(
|
|
319
|
+
join(cwd, "package.json"),
|
|
320
|
+
JSON.stringify(
|
|
321
|
+
restoredPackageJson,
|
|
322
|
+
null,
|
|
323
|
+
detectJsonIndent(restoredPackageJsonSource),
|
|
324
|
+
) + (hasFinalNewline(restoredPackageJsonSource) ? "\n" : ""),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
}
|