@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.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/dev-s3-restore.test.d.mts +2 -0
  3. package/dist/cli-replacement/dev-s3-restore.test.d.mts.map +1 -0
  4. package/dist/cli-replacement/dev-s3-restore.test.mjs +457 -0
  5. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -0
  6. package/dist/cli-replacement/dev.d.mts +7 -0
  7. package/dist/cli-replacement/dev.d.mts.map +1 -1
  8. package/dist/cli-replacement/dev.mjs +142 -3
  9. package/dist/cli-replacement/dev.mjs.map +1 -1
  10. package/dist/cli-replacement/package-json-snapshot.d.mts +26 -0
  11. package/dist/cli-replacement/package-json-snapshot.d.mts.map +1 -0
  12. package/dist/cli-replacement/package-json-snapshot.mjs +222 -0
  13. package/dist/cli-replacement/package-json-snapshot.mjs.map +1 -0
  14. package/dist/cli-replacement/package-json-snapshot.test.d.mts +2 -0
  15. package/dist/cli-replacement/package-json-snapshot.test.d.mts.map +1 -0
  16. package/dist/cli-replacement/package-json-snapshot.test.mjs +207 -0
  17. package/dist/cli-replacement/package-json-snapshot.test.mjs.map +1 -0
  18. package/dist/dev-utils/dev-server-persist.test.d.mts +2 -0
  19. package/dist/dev-utils/dev-server-persist.test.d.mts.map +1 -0
  20. package/dist/dev-utils/dev-server-persist.test.mjs +77 -0
  21. package/dist/dev-utils/dev-server-persist.test.mjs.map +1 -0
  22. package/dist/dev-utils/dev-server.d.mts +1 -0
  23. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  24. package/dist/dev-utils/dev-server.mjs +85 -55
  25. package/dist/dev-utils/dev-server.mjs.map +1 -1
  26. package/dist/dev-utils/vite-dev-server-diagnostics.d.mts +61 -0
  27. package/dist/dev-utils/vite-dev-server-diagnostics.d.mts.map +1 -0
  28. package/dist/dev-utils/vite-dev-server-diagnostics.mjs +133 -0
  29. package/dist/dev-utils/vite-dev-server-diagnostics.mjs.map +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/telemetry/logging.d.ts.map +1 -1
  35. package/dist/telemetry/logging.js +22 -10
  36. package/dist/telemetry/logging.js.map +1 -1
  37. package/dist/telemetry/logging.test.d.ts +2 -0
  38. package/dist/telemetry/logging.test.d.ts.map +1 -0
  39. package/dist/telemetry/logging.test.js +104 -0
  40. package/dist/telemetry/logging.test.js.map +1 -0
  41. package/package.json +7 -7
  42. package/src/cli-replacement/dev-s3-restore.test.mts +599 -0
  43. package/src/cli-replacement/dev.mts +202 -6
  44. package/src/cli-replacement/package-json-snapshot.mts +328 -0
  45. package/src/cli-replacement/package-json-snapshot.test.mts +250 -0
  46. package/src/dev-utils/dev-server-persist.test.mts +96 -0
  47. package/src/dev-utils/dev-server.mts +106 -75
  48. package/src/dev-utils/vite-dev-server-diagnostics.mts +213 -0
  49. package/src/index.ts +15 -0
  50. package/src/telemetry/logging.test.ts +142 -0
  51. package/src/telemetry/logging.ts +30 -10
  52. package/test/vite-dev-server-diagnostics.test.mts +336 -0
  53. package/tsconfig.tsbuildinfo +1 -1
  54. 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 (hasPackageChanged || upgradePromises.length > 0) {
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
- if (hasPackageChanged || uploadFirst) {
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
- await git.checkoutOrCreate(
1234
- SUPERBLOCKS_LIVE_GIT_BRANCH,
1235
- `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
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
+ }