@superblocksteam/sdk 2.0.115 → 2.0.116-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +105 -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.map +1 -1
- package/dist/dev-utils/dev-server.mjs +2 -0
- 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 +163 -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 +6 -0
- 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",
|
|
@@ -482,11 +529,11 @@ export async function dev(options: {
|
|
|
482
529
|
);
|
|
483
530
|
}
|
|
484
531
|
|
|
485
|
-
const [
|
|
486
|
-
|
|
532
|
+
const [serverHash, currentUser] = await tracer.startActiveSpan(
|
|
533
|
+
"fetchUserAndServerHash",
|
|
534
|
+
async (span) => {
|
|
487
535
|
try {
|
|
488
|
-
|
|
489
|
-
sdk.hashLocalDirectory(cwd),
|
|
536
|
+
return await Promise.all([
|
|
490
537
|
getDraftOrLiveEditHash(
|
|
491
538
|
sdk,
|
|
492
539
|
applicationConfig.id,
|
|
@@ -495,11 +542,15 @@ export async function dev(options: {
|
|
|
495
542
|
),
|
|
496
543
|
sdk.fetchCurrentUser(),
|
|
497
544
|
]);
|
|
498
|
-
return results;
|
|
499
545
|
} finally {
|
|
500
546
|
span.end();
|
|
501
547
|
}
|
|
502
|
-
}
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
logger.info(
|
|
552
|
+
`[dev-startup] Fetched server hash for branch '${activeDbfsBranchName}'`,
|
|
553
|
+
);
|
|
503
554
|
|
|
504
555
|
gitUserName = currentUser.user.name;
|
|
505
556
|
gitUserEmail = currentUser.user.email;
|
|
@@ -558,60 +609,19 @@ export async function dev(options: {
|
|
|
558
609
|
},
|
|
559
610
|
);
|
|
560
611
|
|
|
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
|
-
}
|
|
612
|
+
logger.info(
|
|
613
|
+
`[dev-startup] Starting git sync (${downloadFirst ? "download" : "upload"}-first) on branch '${activeDbfsBranchName}'`,
|
|
614
|
+
);
|
|
612
615
|
|
|
613
|
-
// Git sync step — runs
|
|
614
|
-
//
|
|
616
|
+
// Git sync step — runs before DBFS download so the working tree can
|
|
617
|
+
// be reconciled with remotes/branches first; DBFS download then
|
|
618
|
+
// overwrites app files with server state (source of truth). The
|
|
619
|
+
// .git directory is not managed by DBFS. Best-effort: never blocks
|
|
620
|
+
// startup on failure.
|
|
621
|
+
let devStartupGitOutcome:
|
|
622
|
+
| "skipped_no_remote"
|
|
623
|
+
| "bootstrap_failed"
|
|
624
|
+
| "completed" = "skipped_no_remote";
|
|
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 =
|
|
@@ -1413,23 +1419,10 @@ async function ensureLiveBranchCheckedOutAfterFetch(
|
|
|
1413
1419
|
// If remote live branch exists, initialize/switch local live branch from it.
|
|
1414
1420
|
// This handles fresh repos where HEAD is unborn before first fetch.
|
|
1415
1421
|
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
|
-
}
|
|
1422
|
+
await git.checkoutOrCreate(
|
|
1423
|
+
SUPERBLOCKS_LIVE_GIT_BRANCH,
|
|
1424
|
+
`origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
|
|
1425
|
+
);
|
|
1433
1426
|
return;
|
|
1434
1427
|
}
|
|
1435
1428
|
|
|
@@ -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
|
+
}
|
|
@@ -532,6 +532,9 @@ export async function createDevServer({
|
|
|
532
532
|
logger.error(
|
|
533
533
|
`Application ID header mismatch, expected ${resourceConfig!.id}, received ${req.headers["x-superblocks-application-id"]}`,
|
|
534
534
|
);
|
|
535
|
+
logger.info(
|
|
536
|
+
`[dev-server-runtime] _sb_connect application mismatch: expected ${resourceConfig!.id}, got ${req.headers["x-superblocks-application-id"]}`,
|
|
537
|
+
);
|
|
535
538
|
res.status(401).send(
|
|
536
539
|
JSON.stringify({
|
|
537
540
|
error: `Application ID header mismatch. Expected ${resourceConfig!.id}, received ${req.headers["x-superblocks-application-id"]}`,
|
|
@@ -551,6 +554,9 @@ export async function createDevServer({
|
|
|
551
554
|
logger.error(
|
|
552
555
|
`Dev server expects branch ${currentBranch}, but received ${incomingBranch}. Check your current branch with 'git branch' and try again.`,
|
|
553
556
|
);
|
|
557
|
+
logger.info(
|
|
558
|
+
`[dev-server-runtime] _sb_connect branch mismatch: expected ${currentBranch}, got ${incomingBranch}`,
|
|
559
|
+
);
|
|
554
560
|
res.status(401).send(
|
|
555
561
|
JSON.stringify({
|
|
556
562
|
error: `Dev server expects branch ${currentBranch}, but received ${incomingBranch}. Check your current branch with 'git branch' and try again.`,
|