@superblocksteam/sdk 2.0.116-next.0 → 2.0.116-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/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +16 -0
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +78 -0
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.d.mts +2 -0
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +341 -0
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +121 -127
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/normalize-workspace-protocol.d.ts +15 -0
- package/dist/cli-replacement/normalize-workspace-protocol.d.ts.map +1 -0
- package/dist/cli-replacement/normalize-workspace-protocol.js +44 -0
- package/dist/cli-replacement/normalize-workspace-protocol.js.map +1 -0
- package/dist/cli-replacement/normalize-workspace-protocol.test.d.ts +2 -0
- package/dist/cli-replacement/normalize-workspace-protocol.test.d.ts.map +1 -0
- package/dist/cli-replacement/normalize-workspace-protocol.test.js +105 -0
- package/dist/cli-replacement/normalize-workspace-protocol.test.js.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +2 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +6 -2
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/package.json +12 -12
- package/src/cli-replacement/automatic-upgrades.test.ts +128 -0
- package/src/cli-replacement/automatic-upgrades.ts +17 -0
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +407 -0
- package/src/cli-replacement/dev.mts +182 -170
- package/src/cli-replacement/normalize-workspace-protocol.test.ts +122 -0
- package/src/cli-replacement/normalize-workspace-protocol.ts +64 -0
- package/src/dev-utils/dev-server.mts +12 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
ensureRemoteHasDefaultBranch,
|
|
59
59
|
getGitErrorFields,
|
|
60
60
|
} from "./git-repo-setup.mjs";
|
|
61
|
+
import { normalizeWorkspaceProtocolForNpm } from "./normalize-workspace-protocol.js";
|
|
61
62
|
import {
|
|
62
63
|
didPackageJsonSnapshotChange,
|
|
63
64
|
packageJsonSnapshot,
|
|
@@ -205,6 +206,45 @@ async function readPkgJson(cwd: string) {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
async function normalizePackageJsonForNpm(cwd: string, logger: Logger) {
|
|
210
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
211
|
+
let raw: string;
|
|
212
|
+
try {
|
|
213
|
+
raw = await nodeFs.readFile(packageJsonPath, "utf8");
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let parsed: ReturnType<typeof JSON.parse>;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(raw);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
logger.warn(
|
|
225
|
+
`Could not parse package.json at ${packageJsonPath} for workspace-protocol normalization: ${err instanceof Error ? err.message : String(err)}`,
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const result = normalizeWorkspaceProtocolForNpm(parsed);
|
|
231
|
+
if (!result.changed) return;
|
|
232
|
+
|
|
233
|
+
for (const warning of result.warnings) {
|
|
234
|
+
logger.warn(warning);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await nodeFs.writeFile(
|
|
238
|
+
packageJsonPath,
|
|
239
|
+
`${JSON.stringify(result.packageJson, null, 2)}\n`,
|
|
240
|
+
);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Failed to write normalized package.json at ${packageJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
208
248
|
async function installPackages(cwd: string, logger: Logger) {
|
|
209
249
|
try {
|
|
210
250
|
const pm = await detect({
|
|
@@ -224,6 +264,13 @@ async function installPackages(cwd: string, logger: Logger) {
|
|
|
224
264
|
return;
|
|
225
265
|
}
|
|
226
266
|
|
|
267
|
+
if (pm.agent === "npm") {
|
|
268
|
+
// npm rejects pnpm's `workspace:` protocol with EUNSUPPORTEDPROTOCOL,
|
|
269
|
+
// crash-looping the SABS dev-server pod. Strip the prefix so npm can
|
|
270
|
+
// resolve a published version instead.
|
|
271
|
+
await normalizePackageJsonForNpm(cwd, logger);
|
|
272
|
+
}
|
|
273
|
+
|
|
227
274
|
const installCommand = resolveCommand(
|
|
228
275
|
pm.agent,
|
|
229
276
|
"install",
|
|
@@ -370,6 +417,10 @@ export async function dev(options: {
|
|
|
370
417
|
|
|
371
418
|
const fsOperationQueue = new OperationQueue();
|
|
372
419
|
let activeDbfsBranchName = applicationConfig?.branchName ?? "main";
|
|
420
|
+
let devStartupGitOutcome:
|
|
421
|
+
| "skipped_no_remote"
|
|
422
|
+
| "bootstrap_failed"
|
|
423
|
+
| "completed" = "skipped_no_remote";
|
|
373
424
|
if (applicationConfig && !skipSync) {
|
|
374
425
|
const rpcClient = new AutoConnectingRpcClient(
|
|
375
426
|
tokenConfig.superblocksBaseUrl,
|
|
@@ -482,11 +533,11 @@ export async function dev(options: {
|
|
|
482
533
|
);
|
|
483
534
|
}
|
|
484
535
|
|
|
485
|
-
const [
|
|
486
|
-
|
|
536
|
+
const [serverHash, currentUser] = await tracer.startActiveSpan(
|
|
537
|
+
"fetchUserAndServerHash",
|
|
538
|
+
async (span) => {
|
|
487
539
|
try {
|
|
488
|
-
|
|
489
|
-
sdk.hashLocalDirectory(cwd),
|
|
540
|
+
return await Promise.all([
|
|
490
541
|
getDraftOrLiveEditHash(
|
|
491
542
|
sdk,
|
|
492
543
|
applicationConfig.id,
|
|
@@ -495,11 +546,15 @@ export async function dev(options: {
|
|
|
495
546
|
),
|
|
496
547
|
sdk.fetchCurrentUser(),
|
|
497
548
|
]);
|
|
498
|
-
return results;
|
|
499
549
|
} finally {
|
|
500
550
|
span.end();
|
|
501
551
|
}
|
|
502
|
-
}
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
logger.info(
|
|
556
|
+
`[dev-startup] Fetched server hash for branch '${activeDbfsBranchName}'`,
|
|
557
|
+
);
|
|
503
558
|
|
|
504
559
|
gitUserName = currentUser.user.name;
|
|
505
560
|
gitUserEmail = currentUser.user.email;
|
|
@@ -558,60 +613,15 @@ export async function dev(options: {
|
|
|
558
613
|
},
|
|
559
614
|
);
|
|
560
615
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
logger.info(
|
|
565
|
-
`Local files are in sync with the server on branch '${activeDbfsBranchName}', local hash: ${localContents.hash}, server hash: ${serverHash}`,
|
|
566
|
-
);
|
|
567
|
-
} else {
|
|
568
|
-
logger.info(
|
|
569
|
-
`Local files are out of sync with the server on branch '${activeDbfsBranchName}', local hash: ${localContents.hash}, server hash: ${serverHash}`,
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (!(downloadFirst || uploadFirst)) {
|
|
574
|
-
throw new Error(
|
|
575
|
-
"You must choose --download-first or --upload-first to use the dev command",
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
if (downloadFirst && uploadFirst) {
|
|
579
|
-
throw new Error(
|
|
580
|
-
"Choose either --download-first or --upload-first",
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
let hasPackageChanged = false;
|
|
585
|
-
let packageJsonRequiresInstall = false;
|
|
586
|
-
const hasPackageJsonSnapshotBeforeRestore =
|
|
587
|
-
options.packageJsonSnapshotBeforeRestore !== undefined;
|
|
588
|
-
|
|
589
|
-
const packageJsonBefore = await readPkgJson(cwd);
|
|
590
|
-
|
|
591
|
-
if (downloadFirst && !isSynced) {
|
|
592
|
-
await tracer.startActiveSpan("downloadFirst", async (span) => {
|
|
593
|
-
logger.info(
|
|
594
|
-
`Starting download of branch '${activeDbfsBranchName}'`,
|
|
595
|
-
);
|
|
596
|
-
|
|
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
|
-
}
|
|
609
|
-
span.end();
|
|
610
|
-
});
|
|
611
|
-
}
|
|
616
|
+
logger.info(
|
|
617
|
+
`[dev-startup] Starting git sync (${downloadFirst ? "download" : "upload"}-first) on branch '${activeDbfsBranchName}'`,
|
|
618
|
+
);
|
|
612
619
|
|
|
613
|
-
// Git sync step — runs
|
|
614
|
-
//
|
|
620
|
+
// Git sync step — runs before DBFS download so the working tree can
|
|
621
|
+
// be reconciled with remotes/branches first; DBFS download then
|
|
622
|
+
// overwrites app files with server state (source of truth). The
|
|
623
|
+
// .git directory is not managed by DBFS. Best-effort: never blocks
|
|
624
|
+
// startup on failure.
|
|
615
625
|
await tracer.startActiveSpan("gitSync", async (gitSpan) => {
|
|
616
626
|
try {
|
|
617
627
|
const bootstrappedGitService = await bootstrapGitService({
|
|
@@ -625,6 +635,7 @@ export async function dev(options: {
|
|
|
625
635
|
});
|
|
626
636
|
|
|
627
637
|
if (!bootstrappedGitService) {
|
|
638
|
+
devStartupGitOutcome = "skipped_no_remote";
|
|
628
639
|
logger.info("[git] startup git sync skipped", {
|
|
629
640
|
gitCategory: "setup",
|
|
630
641
|
gitOperation: "bootstrap",
|
|
@@ -633,104 +644,11 @@ export async function dev(options: {
|
|
|
633
644
|
applicationId: applicationConfig.id,
|
|
634
645
|
workDir: cwd,
|
|
635
646
|
});
|
|
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
|
-
|
|
730
647
|
gitSpan.end();
|
|
731
648
|
return;
|
|
732
649
|
}
|
|
733
650
|
|
|
651
|
+
devStartupGitOutcome = "completed";
|
|
734
652
|
gitService = bootstrappedGitService;
|
|
735
653
|
|
|
736
654
|
// At this point the local repo is usable for status(),
|
|
@@ -774,6 +692,7 @@ export async function dev(options: {
|
|
|
774
692
|
}
|
|
775
693
|
} catch (gitError) {
|
|
776
694
|
// Init/configure/ensureGitRepo failed — repo is not usable
|
|
695
|
+
devStartupGitOutcome = "bootstrap_failed";
|
|
777
696
|
gitService = undefined;
|
|
778
697
|
logger.warn(
|
|
779
698
|
"[git] startup git setup failed, continuing without git",
|
|
@@ -798,6 +717,93 @@ export async function dev(options: {
|
|
|
798
717
|
}
|
|
799
718
|
});
|
|
800
719
|
|
|
720
|
+
logger.info(
|
|
721
|
+
`[dev-startup] Git sync complete (outcome=${devStartupGitOutcome})`,
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const localContents = await tracer.startActiveSpan(
|
|
725
|
+
"hashLocalDirectory",
|
|
726
|
+
async (span) => {
|
|
727
|
+
try {
|
|
728
|
+
return await sdk.hashLocalDirectory(cwd);
|
|
729
|
+
} finally {
|
|
730
|
+
span.end();
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
const isSynced = localContents.hash === serverHash;
|
|
736
|
+
|
|
737
|
+
logger.info(
|
|
738
|
+
`[dev-startup] Local directory ${isSynced ? "in sync" : "out of sync"} with server`,
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
if (!(downloadFirst || uploadFirst)) {
|
|
742
|
+
throw new Error(
|
|
743
|
+
"You must choose --download-first or --upload-first to use the dev command",
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (downloadFirst && uploadFirst) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
"Choose either --download-first or --upload-first",
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
let hasPackageChanged = false;
|
|
753
|
+
let packageJsonRequiresInstall = false;
|
|
754
|
+
const hasPackageJsonSnapshotBeforeRestore =
|
|
755
|
+
options.packageJsonSnapshotBeforeRestore !== undefined;
|
|
756
|
+
|
|
757
|
+
const packageJsonBefore = await readPkgJson(cwd);
|
|
758
|
+
|
|
759
|
+
if (downloadFirst && !isSynced) {
|
|
760
|
+
await tracer.startActiveSpan("downloadFirst", async (span) => {
|
|
761
|
+
logger.info(
|
|
762
|
+
`Starting download of branch '${activeDbfsBranchName}'`,
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
await syncService!.downloadDirectory();
|
|
766
|
+
logger.info("[dev-startup] DBFS download complete");
|
|
767
|
+
try {
|
|
768
|
+
const postDownloadLocalContents =
|
|
769
|
+
await tracer.startActiveSpan(
|
|
770
|
+
"hashLocalDirectoryAfterDbfsDownload",
|
|
771
|
+
async (hashSpan) => {
|
|
772
|
+
try {
|
|
773
|
+
return await sdk.hashLocalDirectory(cwd);
|
|
774
|
+
} finally {
|
|
775
|
+
hashSpan.end();
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
);
|
|
779
|
+
const hashMatchesServer =
|
|
780
|
+
postDownloadLocalContents.hash === serverHash;
|
|
781
|
+
logger.info(
|
|
782
|
+
`[dev-startup] Post-download hash ${hashMatchesServer ? "matches" : "does not match"} server`,
|
|
783
|
+
);
|
|
784
|
+
} catch (hashError) {
|
|
785
|
+
logger.warn(
|
|
786
|
+
"[dev-startup] Failed to compute post-download hash",
|
|
787
|
+
getErrorMeta(hashError),
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (
|
|
791
|
+
options.normalizeManagedPackageDependencies &&
|
|
792
|
+
(await restoreManagedPackageDependencies(
|
|
793
|
+
cwd,
|
|
794
|
+
packageJsonBefore,
|
|
795
|
+
))
|
|
796
|
+
) {
|
|
797
|
+
logger.info(
|
|
798
|
+
"Restored managed package dependencies to the warm template versions after DBFS download",
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
span.end();
|
|
802
|
+
});
|
|
803
|
+
} else if (downloadFirst && isSynced) {
|
|
804
|
+
logger.info("[dev-startup] Skipping download, already in sync");
|
|
805
|
+
}
|
|
806
|
+
|
|
801
807
|
let hasCliUpdated = false;
|
|
802
808
|
let upgradePromises: Promise<void>[] = [];
|
|
803
809
|
const forceUpgrade =
|
|
@@ -1085,6 +1091,7 @@ export async function dev(options: {
|
|
|
1085
1091
|
lockService: lockService,
|
|
1086
1092
|
aiService: aiService,
|
|
1087
1093
|
gitService: gitService,
|
|
1094
|
+
gitBootstrapFailed: devStartupGitOutcome === "bootstrap_failed",
|
|
1088
1095
|
activateGitService: activateRuntimeGitService,
|
|
1089
1096
|
snapshotManager: snapshotManager,
|
|
1090
1097
|
logger: options.logger,
|
|
@@ -1198,6 +1205,12 @@ async function bootstrapGitService({
|
|
|
1198
1205
|
}
|
|
1199
1206
|
|
|
1200
1207
|
await service.configure({ credentials, userName, userEmail });
|
|
1208
|
+
logger.info("[git] configure complete, proceeding to ensureGitRepo", {
|
|
1209
|
+
gitCategory: "setup",
|
|
1210
|
+
gitOperation: "bootstrap",
|
|
1211
|
+
applicationId,
|
|
1212
|
+
workDir: cwd,
|
|
1213
|
+
});
|
|
1201
1214
|
await ensureGitRepo(service, gitConfig.gitRemoteUrl, superblocksBaseUrl);
|
|
1202
1215
|
|
|
1203
1216
|
return service;
|
|
@@ -1241,6 +1254,18 @@ async function ensureGitRepo(
|
|
|
1241
1254
|
|
|
1242
1255
|
if (!hasGit) {
|
|
1243
1256
|
await git.init();
|
|
1257
|
+
const gitDirAfterInit = await nodeFs
|
|
1258
|
+
.access(path.join(git.workDir, ".git"))
|
|
1259
|
+
.then(
|
|
1260
|
+
() => true,
|
|
1261
|
+
() => false,
|
|
1262
|
+
);
|
|
1263
|
+
getLogger().info("[git] ensureGitRepo init complete", {
|
|
1264
|
+
gitCategory: "setup",
|
|
1265
|
+
gitOperation: "ensure-repo",
|
|
1266
|
+
gitDirCreated: gitDirAfterInit,
|
|
1267
|
+
workDir: git.workDir,
|
|
1268
|
+
});
|
|
1244
1269
|
await git.addRemote("origin", remoteUrl);
|
|
1245
1270
|
|
|
1246
1271
|
const remoteRefs = await git
|
|
@@ -1413,23 +1438,10 @@ async function ensureLiveBranchCheckedOutAfterFetch(
|
|
|
1413
1438
|
// If remote live branch exists, initialize/switch local live branch from it.
|
|
1414
1439
|
// This handles fresh repos where HEAD is unborn before first fetch.
|
|
1415
1440
|
if (await canResolveRef(git, `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`)) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
}
|
|
1441
|
+
await git.checkoutOrCreate(
|
|
1442
|
+
SUPERBLOCKS_LIVE_GIT_BRANCH,
|
|
1443
|
+
`origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
|
|
1444
|
+
);
|
|
1433
1445
|
return;
|
|
1434
1446
|
}
|
|
1435
1447
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { normalizeWorkspaceProtocolForNpm } from "./normalize-workspace-protocol.js";
|
|
4
|
+
|
|
5
|
+
describe("normalizeWorkspaceProtocolForNpm", () => {
|
|
6
|
+
it("returns unchanged when no workspace: deps are present", () => {
|
|
7
|
+
const pkg = {
|
|
8
|
+
dependencies: { react: "^18.0.0", "@superblocksteam/library": "2.5.0" },
|
|
9
|
+
devDependencies: { vitest: "^1.0.0" },
|
|
10
|
+
};
|
|
11
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
12
|
+
|
|
13
|
+
expect(result.changed).toBe(false);
|
|
14
|
+
expect(result.warnings).toHaveLength(0);
|
|
15
|
+
expect(result.packageJson).toEqual(pkg);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("strips workspace: prefix from dependencies and reports a warning per dep", () => {
|
|
19
|
+
const pkg = {
|
|
20
|
+
dependencies: {
|
|
21
|
+
"@superblocksteam/library": "workspace:*",
|
|
22
|
+
"@superblocksteam/sdk-api": "workspace:*",
|
|
23
|
+
react: "^18.0.0",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
27
|
+
|
|
28
|
+
expect(result.changed).toBe(true);
|
|
29
|
+
expect(result.packageJson.dependencies).toEqual({
|
|
30
|
+
"@superblocksteam/library": "*",
|
|
31
|
+
"@superblocksteam/sdk-api": "*",
|
|
32
|
+
react: "^18.0.0",
|
|
33
|
+
});
|
|
34
|
+
expect(result.warnings).toHaveLength(2);
|
|
35
|
+
expect(result.warnings.join(" ")).toContain("@superblocksteam/library");
|
|
36
|
+
expect(result.warnings.join(" ")).toContain("@superblocksteam/sdk-api");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("preserves the version range after the workspace: prefix", () => {
|
|
40
|
+
const pkg = {
|
|
41
|
+
dependencies: {
|
|
42
|
+
a: "workspace:^1.2.3",
|
|
43
|
+
b: "workspace:~2.0.0",
|
|
44
|
+
c: "workspace:1.0.0",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
48
|
+
|
|
49
|
+
expect(result.changed).toBe(true);
|
|
50
|
+
expect(result.packageJson.dependencies).toEqual({
|
|
51
|
+
a: "^1.2.3",
|
|
52
|
+
b: "~2.0.0",
|
|
53
|
+
c: "1.0.0",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("normalizes across all dependency buckets (devDependencies, optionalDependencies, peerDependencies)", () => {
|
|
58
|
+
const pkg = {
|
|
59
|
+
dependencies: { a: "workspace:*" },
|
|
60
|
+
devDependencies: { b: "workspace:*" },
|
|
61
|
+
optionalDependencies: { c: "workspace:*" },
|
|
62
|
+
peerDependencies: { d: "workspace:*" },
|
|
63
|
+
};
|
|
64
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
65
|
+
|
|
66
|
+
expect(result.changed).toBe(true);
|
|
67
|
+
expect(result.packageJson.dependencies?.a).toBe("*");
|
|
68
|
+
expect(result.packageJson.devDependencies?.b).toBe("*");
|
|
69
|
+
expect(result.packageJson.optionalDependencies?.c).toBe("*");
|
|
70
|
+
expect(result.packageJson.peerDependencies?.d).toBe("*");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("does not touch values that merely contain the substring 'workspace' but don't use the protocol", () => {
|
|
74
|
+
const pkg = {
|
|
75
|
+
dependencies: {
|
|
76
|
+
"some-workspace-tool": "1.0.0",
|
|
77
|
+
a: "git+ssh://git@github.com/foo/workspace.git",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
81
|
+
|
|
82
|
+
expect(result.changed).toBe(false);
|
|
83
|
+
expect(result.packageJson).toEqual(pkg);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not mutate the input object", () => {
|
|
87
|
+
const pkg = {
|
|
88
|
+
dependencies: { a: "workspace:*" },
|
|
89
|
+
};
|
|
90
|
+
const original = JSON.parse(JSON.stringify(pkg));
|
|
91
|
+
normalizeWorkspaceProtocolForNpm(pkg);
|
|
92
|
+
expect(pkg).toEqual(original);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("handles a package.json with no dependency buckets", () => {
|
|
96
|
+
const pkg = { name: "test", version: "1.0.0" };
|
|
97
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
98
|
+
|
|
99
|
+
expect(result.changed).toBe(false);
|
|
100
|
+
expect(result.warnings).toHaveLength(0);
|
|
101
|
+
expect(result.packageJson).toEqual(pkg);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("bare workspace: (no version after colon) falls back to *", () => {
|
|
105
|
+
const pkg = { dependencies: { a: "workspace:" } };
|
|
106
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
107
|
+
|
|
108
|
+
expect(result.changed).toBe(true);
|
|
109
|
+
expect(result.packageJson.dependencies?.a).toBe("*");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("operator-only forms (workspace:^ and workspace:~) preserve the operator as-is", () => {
|
|
113
|
+
const pkg = {
|
|
114
|
+
dependencies: { a: "workspace:^", b: "workspace:~" },
|
|
115
|
+
};
|
|
116
|
+
const result = normalizeWorkspaceProtocolForNpm(pkg);
|
|
117
|
+
|
|
118
|
+
expect(result.changed).toBe(true);
|
|
119
|
+
expect(result.packageJson.dependencies?.a).toBe("^");
|
|
120
|
+
expect(result.packageJson.dependencies?.b).toBe("~");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// pnpm's `workspace:` protocol is not understood by npm — `npm install` rejects
|
|
2
|
+
// it with EUNSUPPORTEDPROTOCOL. In the SABS dev-server pod the user app's
|
|
3
|
+
// package.json can arrive with `workspace:*` deps (build-time rewrite was
|
|
4
|
+
// missed, DBFS row predates the rewrite, etc.) and the install crash-loops the
|
|
5
|
+
// pod with no useful signal. This helper strips the `workspace:` prefix so npm
|
|
6
|
+
// can resolve a published version. It runs in the cloud pod only — local dev
|
|
7
|
+
// uses pnpm and needs the protocol intact for workspace linking.
|
|
8
|
+
|
|
9
|
+
type PackageJson = {
|
|
10
|
+
dependencies?: Record<string, string>;
|
|
11
|
+
devDependencies?: Record<string, string>;
|
|
12
|
+
optionalDependencies?: Record<string, string>;
|
|
13
|
+
peerDependencies?: Record<string, string>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEPENDENCY_BUCKETS = [
|
|
18
|
+
"dependencies",
|
|
19
|
+
"devDependencies",
|
|
20
|
+
"optionalDependencies",
|
|
21
|
+
"peerDependencies",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const WORKSPACE_PROTOCOL = "workspace:";
|
|
25
|
+
|
|
26
|
+
export interface NormalizeResult {
|
|
27
|
+
changed: boolean;
|
|
28
|
+
warnings: string[];
|
|
29
|
+
packageJson: PackageJson;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeWorkspaceProtocolForNpm(
|
|
33
|
+
packageJson: PackageJson,
|
|
34
|
+
): NormalizeResult {
|
|
35
|
+
const next: PackageJson = { ...packageJson };
|
|
36
|
+
const warnings: string[] = [];
|
|
37
|
+
let changed = false;
|
|
38
|
+
|
|
39
|
+
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
40
|
+
const deps = packageJson[bucket];
|
|
41
|
+
if (!deps) continue;
|
|
42
|
+
|
|
43
|
+
const nextDeps: Record<string, string> = {};
|
|
44
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
45
|
+
if (typeof value === "string" && value.startsWith(WORKSPACE_PROTOCOL)) {
|
|
46
|
+
const stripped = value.slice(WORKSPACE_PROTOCOL.length) || "*";
|
|
47
|
+
nextDeps[name] = stripped;
|
|
48
|
+
changed = true;
|
|
49
|
+
warnings.push(
|
|
50
|
+
// For @superblocksteam/sdk-api this falls back to the latest published
|
|
51
|
+
// non-ephemeral package. To pin an ephemeral version, fix the upstream
|
|
52
|
+
// rewrite path (scripts/setup-template.sh) so workspace:* never ships
|
|
53
|
+
// to the pod.
|
|
54
|
+
`Stripped pnpm "workspace:" protocol from ${bucket}.${name} (was "${value}", now "${stripped}") so npm install can resolve it.`,
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
nextDeps[name] = String(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
next[bucket] = nextDeps;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { changed, warnings, packageJson: next };
|
|
64
|
+
}
|