@solcreek/cli 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,17 @@
1
1
  import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
- import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, rmSync } from "node:fs";
3
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
4
4
  // ajv is lazy-imported only when --template --data is used (avoid top-level crash if deps missing)
5
5
  import { join, resolve } from "node:path";
6
6
  import { execSync, execFileSync } from "node:child_process";
7
- import { CreekClient, CreekAuthError, isSSRFramework, getSSRServerEntry, getClientAssetsDir, getDefaultBuildOutput, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToResources, resolvedConfigToBindingRequirements, ConfigNotFoundError, getSSRServerDir, collectServerFiles, isPreBundledFramework, detectAstroCloudflareBuild, detectNextjsMode, detectMonorepo, planDeploy, } from "@solcreek/sdk";
7
+ import { CreekClient, CreekAuthError, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToResources, resolvedConfigToBindingRequirements, ConfigNotFoundError, detectNextjsMode, detectMonorepo, } from "@solcreek/sdk";
8
8
  import { getToken, getApiUrl } from "../utils/config.js";
9
9
  import { collectAssets } from "../utils/bundle.js";
10
- import { bundleSSRServer } from "../utils/ssr-bundle.js";
11
- import { bundleWorker } from "../utils/worker-bundle.js";
12
10
  import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
11
+ import { prepareDeployBundle } from "../utils/prepare-bundle.js";
13
12
  import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
14
13
  import { ensureTosAccepted } from "../utils/tos.js";
15
- import { buildNextjs, patchBundledWorker, hasAdapterOutput } from "../utils/nextjs.js";
14
+ import { hasAdapterOutput } from "../utils/nextjs.js";
16
15
  import { isRepoUrl, parseRepoUrl, validateRepoUrl, validateSubpath, RepoUrlError } from "../utils/repo-url.js";
17
16
  import { checkGitInstalled, cloneRepo, detectPackageManager, installDependencies, cleanupDir as cleanupCloneDir, GitCloneError } from "../utils/git-clone.js";
18
17
  function section(name) {
@@ -676,7 +675,6 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
676
675
  }
677
676
  const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
678
677
  const framework = resolved?.framework ?? detectFramework(pkg);
679
- // Detect Next.js mode (static vs opennext SSR)
680
678
  const nextjsMode = framework === "nextjs" ? detectNextjsMode(pkg, cwd) : null;
681
679
  const monorepo = detectMonorepo(cwd);
682
680
  section("Detect");
@@ -684,133 +682,22 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
684
682
  if (nextjsMode)
685
683
  consola.info(` Next.js mode: ${nextjsMode}${monorepo.isMonorepo ? " (monorepo)" : ""}`);
686
684
  consola.info(" Mode: sandbox (60 min preview)");
687
- // Build
688
- if (!skipBuild) {
685
+ // Pre-build header so the section banner ("Build") still appears
686
+ // before build output streams. Then prepareDeployBundle owns the
687
+ // actual build + plan + collect + bundle pipeline.
688
+ if (!skipBuild)
689
689
  section("Build");
690
- if (nextjsMode === "opennext") {
691
- // Next.js SSR on CF Workers: adapter (>= 16.2) or legacy opennext
692
- try {
693
- buildNextjs(cwd, monorepo.isMonorepo);
694
- }
695
- catch {
696
- consola.error("Next.js build failed");
697
- process.exit(1);
698
- }
699
- consola.success(" Build complete");
700
- }
701
- else {
702
- const buildCommand = resolved?.buildCommand || "npm run build";
703
- if (!buildCommand) {
704
- consola.error("No build script found in package.json.");
705
- consola.info("Add a 'build' script or use --skip-build if already built.");
706
- process.exit(1);
707
- }
708
- consola.start(` ${buildCommand}`);
709
- try {
710
- execSync(buildCommand, { cwd, stdio: "inherit" });
711
- }
712
- catch {
713
- consola.error("Build failed");
714
- consola.info("");
715
- consola.info(" Common fixes:");
716
- consola.info(" npm install (missing dependencies?)");
717
- consola.info(" Check for TypeScript errors");
718
- process.exit(1);
719
- }
720
- consola.success(" Build complete");
721
- }
722
- }
723
- const useAdapterOutput = framework === "nextjs" && hasAdapterOutput(cwd);
724
- const outputDir = useAdapterOutput
725
- ? resolve(cwd, ".creek/adapter-output")
726
- : resolve(cwd, resolved?.buildOutput ?? getDefaultBuildOutput(framework));
727
- // Whether buildOutput exists is now an INPUT to planDeploy, not a
728
- // hard precondition — pure-Worker projects legitimately have no
729
- // build output directory, and planDeploy returns an explicit error
730
- // for the cases that genuinely need one.
731
- // Resolve deploy shape via the SDK's pure planner. All branching
732
- // (SPA vs SSR vs Worker, with-or-without coexisting static assets,
733
- // bundled vs source worker entry) lives in deploy-plan.ts and is
734
- // table-tested. Don't re-derive these conditions inline.
735
- const planResult = planDeploy({
736
- framework,
737
- workerEntry: resolved?.workerEntry ?? null,
738
- workerEntryExists: !!resolved?.workerEntry &&
739
- existsSync(resolve(cwd, resolved.workerEntry)),
740
- buildOutput: resolved?.buildOutput ?? "dist",
741
- buildOutputExists: existsSync(outputDir),
742
- astroCF: null, // sandbox path doesn't run post-build adapter detection (yet)
690
+ const prepared = await prepareDeployBundle({
691
+ cwd,
692
+ resolved: resolved ?? {}, // sandbox path can be called without resolved when delegating; guarded above
693
+ skipBuild,
743
694
  });
744
- if (!planResult.ok) {
745
- consola.error(planResult.reason);
746
- process.exit(1);
747
- }
748
- const plan = planResult.plan;
749
- const renderMode = plan.renderMode;
750
- let clientAssets = {};
751
- let fileList = [];
752
- if (plan.assets.enabled && plan.assets.dir) {
753
- let clientAssetsDir = useAdapterOutput
754
- ? resolve(outputDir, "assets")
755
- : resolve(cwd, plan.assets.dir);
756
- if (!useAdapterOutput && isSSRFramework(framework) && framework) {
757
- const subdir = getClientAssetsDir(framework);
758
- if (subdir)
759
- clientAssetsDir = resolve(clientAssetsDir, subdir);
760
- }
761
- const collected = collectAssets(clientAssetsDir);
762
- clientAssets = collected.assets;
763
- fileList = collected.fileList;
764
- if (plan.assets.excludeFile) {
765
- delete clientAssets["/" + plan.assets.excludeFile];
766
- delete clientAssets[plan.assets.excludeFile];
767
- fileList = fileList.filter((p) => p !== plan.assets.excludeFile && p !== "/" + plan.assets.excludeFile);
768
- }
769
- }
695
+ const { plan, fileList, assets: clientAssets, serverFiles, effectiveRenderMode } = prepared;
770
696
  section("Upload");
771
- consola.info(` Mode: ${renderMode}${plan.worker.entry ? ` (worker: ${plan.worker.entry})` : ""}`);
697
+ consola.info(` Mode: ${effectiveRenderMode}${plan.worker.entry ? ` (worker: ${plan.worker.entry})` : ""}`);
772
698
  if (plan.assets.enabled) {
773
699
  consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
774
700
  }
775
- let serverFiles;
776
- switch (plan.worker.strategy) {
777
- case "none":
778
- break;
779
- case "ssr-framework": {
780
- // Sandbox path doesn't currently support pre-bundled SSR
781
- // frameworks (Nuxt/SvelteKit/Astro CF); planDeploy only returns
782
- // this strategy for SSR frameworks, which we still bundle via
783
- // the legacy single-file path below to keep diff small.
784
- const serverEntry = getSSRServerEntry(framework);
785
- if (serverEntry) {
786
- const serverEntryPath = resolve(outputDir, serverEntry);
787
- if (existsSync(serverEntryPath)) {
788
- consola.start(" Bundling SSR server...");
789
- const bundled = await bundleSSRServer(serverEntryPath);
790
- serverFiles = { "server.js": Buffer.from(bundled).toString("base64") };
791
- consola.success(` SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
792
- }
793
- }
794
- break;
795
- }
796
- case "esbuild-bundle": {
797
- const workerEntryPath = resolve(cwd, plan.worker.entry);
798
- consola.start(" Bundling worker...");
799
- const bundled = await bundleWorker(workerEntryPath, cwd, {
800
- hasClientAssets: plan.assets.enabled,
801
- });
802
- serverFiles = { "worker.js": Buffer.from(bundled).toString("base64") };
803
- consola.success(` Worker bundled (${Math.round(bundled.length / 1024)}KB)`);
804
- break;
805
- }
806
- case "upload-asis": {
807
- const workerEntryPath = resolve(cwd, plan.worker.entry);
808
- const bytes = readFileSync(workerEntryPath);
809
- serverFiles = { "worker.js": bytes.toString("base64") };
810
- consola.success(` Worker (pre-bundled, ${Math.round(bytes.length / 1024)}KB)`);
811
- break;
812
- }
813
- }
814
701
  // Deploy to sandbox
815
702
  if (!jsonMode) {
816
703
  section("Deploy");
@@ -820,13 +707,13 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
820
707
  const result = await sandboxDeploy({
821
708
  manifest: {
822
709
  assets: fileList,
823
- hasWorker: plan.worker.strategy !== "none",
824
- entrypoint: resolved?.workerEntry ?? null,
825
- renderMode,
710
+ hasWorker: prepared.serverFiles !== undefined,
711
+ entrypoint: prepared.effectiveEntrypoint,
712
+ renderMode: effectiveRenderMode,
826
713
  },
827
714
  assets: clientAssets,
828
715
  serverFiles,
829
- framework: framework ?? undefined,
716
+ framework: prepared.framework ?? undefined,
830
717
  source: "cli",
831
718
  ...(resolved
832
719
  ? { bindings: resolvedConfigToBindingRequirements(resolved) }
@@ -1000,43 +887,19 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1000
887
  if (!jsonMode)
1001
888
  consola.success(` Created project: ${project.slug}`);
1002
889
  }
1003
- // Determine deploy mode via the SDK's pure planner. The downstream
1004
- // framework-specific branches (Astro CF, Next.js adapter, Nuxt, etc.)
1005
- // are kept as-is — planDeploy decides the SHAPE (spa / ssr / worker
1006
- // and whether the worker is source vs prebundled), and the legacy
1007
- // collection logic decides WHERE to read the bytes from for each
1008
- // framework. astroCF detection happens post-build below; for the
1009
- // initial plan we conservatively pass null and the framework SSR
1010
- // branch handles the adapter case.
890
+ // Framework is resolved upstream; everything else (SSR / worker /
891
+ // assets / bundling) is decided inside prepareDeployBundle via the
892
+ // SDK's planDeploy resolver.
1011
893
  const framework = resolved.framework;
1012
- const isSSR = isSSRFramework(framework);
1013
- const initialPlan = planDeploy({
1014
- framework,
1015
- workerEntry: resolved.workerEntry ?? null,
1016
- workerEntryExists: !!resolved.workerEntry &&
1017
- existsSync(resolve(cwd, resolved.workerEntry)),
1018
- buildOutput: resolved.buildOutput,
1019
- buildOutputExists: existsSync(resolve(cwd, resolved.buildOutput)),
1020
- astroCF: null,
1021
- });
1022
- if (!initialPlan.ok) {
1023
- consola.error(initialPlan.reason);
1024
- process.exit(1);
1025
- }
1026
- const isWorker = initialPlan.plan.worker.strategy === "esbuild-bundle" ||
1027
- initialPlan.plan.worker.strategy === "upload-asis";
1028
- const workerIsPrebundled = initialPlan.plan.worker.strategy === "upload-asis";
1029
- const workerExcludeFromAssets = initialPlan.plan.assets.excludeFile;
1030
- const renderMode = initialPlan.plan.renderMode;
1031
- // Detect Next.js mode for special build handling
894
+ // Detect Next.js mode for the info banner only — actual handling is
895
+ // inside prepareDeployBundle.
1032
896
  const pkg = existsSync(join(cwd, "package.json"))
1033
897
  ? JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"))
1034
898
  : {};
1035
899
  const nextjsMode = framework === "nextjs" ? detectNextjsMode(pkg, cwd) : null;
1036
900
  const monorepo = framework === "nextjs" ? detectMonorepo(cwd) : { isMonorepo: false, root: null };
1037
- if (nextjsMode) {
1038
- if (!jsonMode)
1039
- consola.info(` Next.js mode: ${nextjsMode}${monorepo.isMonorepo ? " (monorepo)" : ""}`);
901
+ if (nextjsMode && !jsonMode) {
902
+ consola.info(` Next.js mode: ${nextjsMode}${monorepo.isMonorepo ? " (monorepo)" : ""}`);
1040
903
  }
1041
904
  // --- ⚡ Turbo deploy: check if server has a cached build for this commit ---
1042
905
  // Read the local git HEAD SHA. If the working tree is clean and the
@@ -1046,214 +909,21 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1046
909
  if (turboResult) {
1047
910
  return; // ⚡ done — server deployed from cache
1048
911
  }
1049
- // Build (skip for pure Workers with no build command)
1050
- if (!skipBuild && resolved.buildCommand) {
912
+ // Single source of truth for build plan collect → bundle. Both
913
+ // sandbox and authenticated paths call the same function; they
914
+ // diverge only in where the bundle gets POSTed.
915
+ if (!skipBuild && resolved.buildCommand)
1051
916
  section("Build");
1052
- if (nextjsMode === "opennext") {
1053
- try {
1054
- buildNextjs(cwd, monorepo.isMonorepo);
1055
- }
1056
- catch {
1057
- consola.error("Next.js build failed");
1058
- process.exit(1);
1059
- }
1060
- consola.success(" Build complete");
1061
- }
1062
- else {
1063
- const buildCmd = resolved.buildCommand;
1064
- if (buildCmd.length > 500) {
1065
- consola.error("Invalid build command (too long)");
1066
- process.exit(1);
1067
- }
1068
- consola.start(` ${buildCmd}`);
1069
- try {
1070
- execSync(buildCmd, { cwd, stdio: "inherit" });
1071
- }
1072
- catch {
1073
- consola.error("Build failed");
1074
- process.exit(1);
1075
- }
1076
- consola.success(" Build complete");
1077
- }
1078
- }
1079
- // Post-build detection: Astro + @astrojs/cloudflare produces a
1080
- // pre-bundled Worker (dist/server/entry.mjs) + split client assets
1081
- // (dist/client/). We can only detect this after build — before
1082
- // build `framework === "astro"` could mean SSG or CF-adapter-SSR.
1083
- const astroCF = framework === "astro" ? detectAstroCloudflareBuild(cwd) : null;
1084
- // Collect client assets
1085
- // Worker + SPA hybrid: if a Worker project has buildOutput with built files, collect them too
1086
- let clientAssets = {};
1087
- let fileList = [];
1088
- const workerHasClientAssets = isWorker && resolved.buildOutput && resolved.buildOutput !== "." &&
1089
- existsSync(resolve(cwd, resolved.buildOutput));
1090
- if (!isWorker || workerHasClientAssets) {
1091
- let clientAssetsDir;
1092
- // Adapter output takes precedence for Next.js
1093
- if (framework === "nextjs" && hasAdapterOutput(cwd)) {
1094
- clientAssetsDir = resolve(cwd, ".creek/adapter-output/assets");
1095
- }
1096
- else if (astroCF) {
1097
- // Astro CF adapter splits its output: client assets live in
1098
- // dist/client/ (not dist/), so redirect the collector there.
1099
- clientAssetsDir = resolve(cwd, astroCF.assetsDir);
1100
- }
1101
- else {
1102
- const outputDir = resolve(cwd, resolved.buildOutput);
1103
- if (!existsSync(outputDir)) {
1104
- consola.error(`Build output directory not found: ${resolved.buildOutput}`);
1105
- process.exit(1);
1106
- }
1107
- clientAssetsDir = outputDir;
1108
- if (isSSR && framework) {
1109
- const clientSubdir = getClientAssetsDir(framework);
1110
- if (clientSubdir) {
1111
- clientAssetsDir = resolve(outputDir, clientSubdir);
1112
- }
1113
- }
1114
- }
1115
- section("Upload");
1116
- ({ assets: clientAssets, fileList } = collectAssets(clientAssetsDir));
1117
- consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
1118
- }
1119
- // Bundle server/worker files
1120
- let serverFiles;
1121
- if (astroCF) {
1122
- // Astro CF adapter: upload dist/server/ as worker modules
1123
- // (same shape Nuxt/SolidStart use — pre-bundled, do not re-bundle).
1124
- const serverDir = resolve(cwd, astroCF.serverDir);
1125
- if (existsSync(serverDir)) {
1126
- consola.start(" Collecting Astro CF server files...");
1127
- const collected = collectServerFiles(serverDir);
1128
- const fileCount = Object.keys(collected).length;
1129
- serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
1130
- consola.success(` Astro CF worker: ${fileCount} files`);
1131
- }
1132
- }
1133
- else if (isSSR && framework) {
1134
- if (framework === "nextjs" && hasAdapterOutput(cwd)) {
1135
- // Adapter path: read pre-bundled output from .creek/adapter-output/
1136
- const adapterServerDir = resolve(cwd, ".creek/adapter-output/server");
1137
- consola.start(" Collecting adapter output...");
1138
- const collected = {};
1139
- if (existsSync(adapterServerDir)) {
1140
- for (const f of readdirSync(adapterServerDir)) {
1141
- const fp = join(adapterServerDir, f);
1142
- if (!statSync(fp).isFile())
1143
- continue;
1144
- if (f.endsWith(".map"))
1145
- continue;
1146
- let content = readFileSync(fp);
1147
- // Patch bare Node.js module imports → node: prefix (workerd requires it)
1148
- // Note: nodejs_compat_v2 handles most modules, but bare specifiers
1149
- // (without node: prefix) still need patching.
1150
- if (f.endsWith(".js") || f.endsWith(".mjs")) {
1151
- content = Buffer.from(patchBareNodeImports(content.toString("utf-8")));
1152
- }
1153
- collected[f] = content;
1154
- }
1155
- }
1156
- const fileCount = Object.keys(collected).length;
1157
- serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
1158
- consola.success(` Worker bundled: ${fileCount} files (${Math.round(Object.values(collected).reduce((s, b) => s + b.length, 0) / 1024)}KB)`);
1159
- }
1160
- else if (framework === "nextjs") {
1161
- // Legacy path: use wrangler to produce a single bundled worker
1162
- const bundleDir = resolve(cwd, ".creek/bundled");
1163
- consola.start(" Bundling Next.js worker (legacy)...");
1164
- execSync(`npx wrangler deploy --dry-run --outdir "${bundleDir}"`, { cwd, stdio: "pipe" });
1165
- patchBundledWorker(bundleDir, resolve(cwd, ".open-next"));
1166
- const collected = {};
1167
- if (existsSync(bundleDir)) {
1168
- for (const f of readdirSync(bundleDir)) {
1169
- const fp = join(bundleDir, f);
1170
- if (!statSync(fp).isFile())
1171
- continue;
1172
- if (f.endsWith(".map") || f === "README.md")
1173
- continue;
1174
- collected[f] = readFileSync(fp);
1175
- }
1176
- }
1177
- const fileCount = Object.keys(collected).length;
1178
- serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
1179
- consola.success(` Worker bundled: ${fileCount} files (${Math.round(Object.values(collected).reduce((s, b) => s + b.length, 0) / 1024)}KB)`);
1180
- }
1181
- else if (isPreBundledFramework(framework)) {
1182
- // Other pre-bundled SSR frameworks (Nuxt, SvelteKit, etc.)
1183
- const serverDirRel = getSSRServerDir(framework);
1184
- if (serverDirRel) {
1185
- const serverDir = resolve(cwd, serverDirRel);
1186
- if (existsSync(serverDir)) {
1187
- consola.start(" Collecting SSR server files...");
1188
- const collected = collectServerFiles(serverDir);
1189
- const fileCount = Object.keys(collected).length;
1190
- serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
1191
- consola.success(` SSR server: ${fileCount} files`);
1192
- }
1193
- }
1194
- }
1195
- else {
1196
- // Non-pre-bundled SSR: esbuild single-file bundle (fallback)
1197
- const outputDir = resolve(cwd, resolved.buildOutput);
1198
- const serverEntry = getSSRServerEntry(framework);
1199
- if (serverEntry) {
1200
- const serverEntryPath = resolve(outputDir, serverEntry);
1201
- if (existsSync(serverEntryPath)) {
1202
- consola.start(" Bundling SSR server...");
1203
- const bundled = await bundleSSRServer(serverEntryPath);
1204
- serverFiles = {
1205
- "server.js": Buffer.from(bundled).toString("base64"),
1206
- };
1207
- consola.success(` SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
1208
- }
1209
- }
1210
- }
1211
- }
1212
- else if (isWorker && resolved.workerEntry) {
1213
- const workerEntryPath = resolve(cwd, resolved.workerEntry);
1214
- if (!existsSync(workerEntryPath)) {
1215
- consola.error(`Worker entry not found: ${resolved.workerEntry}`);
1216
- process.exit(1);
1217
- }
1218
- section("Bundle");
1219
- if (workerIsPrebundled) {
1220
- // User's build script already produced a fully self-contained
1221
- // bundle inside buildOutput; ship the bytes verbatim. No wrapper,
1222
- // no Creek runtime injection — keeps the deployed Worker free of
1223
- // any @solcreek/* dependency.
1224
- const bytes = readFileSync(workerEntryPath);
1225
- serverFiles = { "worker.js": bytes.toString("base64") };
1226
- consola.success(` Worker (pre-bundled, ${Math.round(bytes.length / 1024)}KB)`);
1227
- }
1228
- else {
1229
- consola.start(" Bundling worker...");
1230
- const bundled = await bundleWorker(workerEntryPath, cwd, {
1231
- hasClientAssets: !!workerHasClientAssets,
1232
- });
1233
- serverFiles = {
1234
- "worker.js": Buffer.from(bundled).toString("base64"),
1235
- };
1236
- consola.success(` Worker bundled (${Math.round(bundled.length / 1024)}KB)`);
1237
- }
1238
- }
1239
- // When the worker bundle lives INSIDE the static asset dir
1240
- // (e.g. dist/_worker.mjs), drop it from clientAssets so it isn't
1241
- // double-uploaded as a publicly accessible file.
1242
- if (workerExcludeFromAssets) {
1243
- delete clientAssets[workerExcludeFromAssets];
1244
- delete clientAssets["/" + workerExcludeFromAssets];
1245
- fileList = fileList.filter((p) => p !== workerExcludeFromAssets && p !== "/" + workerExcludeFromAssets);
1246
- }
917
+ const prepared = await prepareDeployBundle({ cwd, resolved, skipBuild });
918
+ const { plan, framework: detectedFramework, effectiveRenderMode, effectiveEntrypoint, fileList, assets: clientAssets, serverFiles, } = prepared;
919
+ void detectedFramework; // framework var above is the source of truth here
920
+ section("Upload");
921
+ consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
1247
922
  section("Deploy");
1248
923
  consola.start(" Creating deployment...");
1249
924
  const { deployment } = await client.createDeployment(project.id);
1250
925
  consola.start(" Uploading bundle...");
1251
- // If the Astro CF adapter fired post-build, the project is actually
1252
- // SSR (not SPA): overwrite the pre-build-computed renderMode and
1253
- // point the entrypoint at the adapter-emitted entry.mjs.
1254
- const effectiveRenderMode = astroCF ? "ssr" : renderMode;
1255
- const effectiveHasWorker = astroCF ? true : (isSSR || isWorker);
1256
- const effectiveEntrypoint = astroCF ? "entry.mjs" : resolved.workerEntry;
926
+ const effectiveHasWorker = serverFiles !== undefined;
1257
927
  const bundle = {
1258
928
  manifest: {
1259
929
  assets: fileList,
@@ -0,0 +1,63 @@
1
+ /**
2
+ * prepareDeployBundle — single source of truth for "given a project,
3
+ * produce the bundle that gets shipped to either sandbox-api or
4
+ * control-plane". Both `deploySandbox` and `deployAuthenticated` call
5
+ * this; they only differ in (a) whether they auth/look-up a project
6
+ * and (b) which API receives the bundle.
7
+ *
8
+ * Historically these two paths each rolled their own copy of the
9
+ * detect → build → collect → bundle pipeline. They drifted: the
10
+ * sandbox path was missing the worker branch for ~2 weeks (cli@0.4.6
11
+ * regression) and again missed the workers+assets coexist pattern.
12
+ * That divergence is the architectural smell — this file kills it.
13
+ *
14
+ * The function:
15
+ * 1. Reads framework from package.json (or accepts pre-resolved one)
16
+ * 2. Runs the build script (skip with skipBuild: true)
17
+ * 3. Calls SDK's planDeploy() to decide spa/ssr/worker shape
18
+ * 4. Detects post-build framework adapters (Astro CF) and refines
19
+ * 5. Collects static assets per the plan
20
+ * 6. Bundles the worker per the plan (5 framework-aware strategies)
21
+ * 7. Filters worker file out of clientAssets when it lives inside
22
+ * 8. Returns the canonical bundle envelope
23
+ *
24
+ * IO is not abstracted — the function calls execSync, readFileSync,
25
+ * esbuild. Callers needing test isolation should use fixture dirs.
26
+ */
27
+ import { type DeployPlan, type Framework, type ResolvedConfig } from "@solcreek/sdk";
28
+ export interface PrepareDeployBundleInput {
29
+ /** Absolute project directory. */
30
+ cwd: string;
31
+ /** Pre-resolved config (creek.toml or wrangler.* parse). Required. */
32
+ resolved: ResolvedConfig;
33
+ /** When true, skip the build script. Caller is asserting dist/ is current. */
34
+ skipBuild: boolean;
35
+ }
36
+ export interface PreparedDeployBundle {
37
+ /** The plan that drove preparation — passed back so callers can
38
+ * inspect renderMode / worker strategy without re-deriving. */
39
+ plan: DeployPlan;
40
+ /** Framework detected (or pre-resolved). null = static / vanilla worker. */
41
+ framework: Framework | null;
42
+ /** Whether Astro `@astrojs/cloudflare` adapter fired post-build. */
43
+ astroAdapter: {
44
+ serverDir: string;
45
+ assetsDir: string;
46
+ } | null;
47
+ /**
48
+ * Render mode after post-build refinement. Equal to plan.renderMode
49
+ * unless an adapter (e.g. Astro CF) upgraded an SPA-classified
50
+ * project to true SSR.
51
+ */
52
+ effectiveRenderMode: "spa" | "ssr" | "worker";
53
+ /** Main module name the deploy API should treat as the worker entry. */
54
+ effectiveEntrypoint: string | null;
55
+ /** Asset list (paths relative to root, with leading `/`). */
56
+ fileList: string[];
57
+ /** Asset bytes, base64-encoded, keyed by path. */
58
+ assets: Record<string, string>;
59
+ /** Server / worker files, base64-encoded. Undefined for pure SPA. */
60
+ serverFiles?: Record<string, string>;
61
+ }
62
+ export declare function prepareDeployBundle(input: PrepareDeployBundleInput): Promise<PreparedDeployBundle>;
63
+ //# sourceMappingURL=prepare-bundle.d.ts.map
@@ -0,0 +1,266 @@
1
+ /**
2
+ * prepareDeployBundle — single source of truth for "given a project,
3
+ * produce the bundle that gets shipped to either sandbox-api or
4
+ * control-plane". Both `deploySandbox` and `deployAuthenticated` call
5
+ * this; they only differ in (a) whether they auth/look-up a project
6
+ * and (b) which API receives the bundle.
7
+ *
8
+ * Historically these two paths each rolled their own copy of the
9
+ * detect → build → collect → bundle pipeline. They drifted: the
10
+ * sandbox path was missing the worker branch for ~2 weeks (cli@0.4.6
11
+ * regression) and again missed the workers+assets coexist pattern.
12
+ * That divergence is the architectural smell — this file kills it.
13
+ *
14
+ * The function:
15
+ * 1. Reads framework from package.json (or accepts pre-resolved one)
16
+ * 2. Runs the build script (skip with skipBuild: true)
17
+ * 3. Calls SDK's planDeploy() to decide spa/ssr/worker shape
18
+ * 4. Detects post-build framework adapters (Astro CF) and refines
19
+ * 5. Collects static assets per the plan
20
+ * 6. Bundles the worker per the plan (5 framework-aware strategies)
21
+ * 7. Filters worker file out of clientAssets when it lives inside
22
+ * 8. Returns the canonical bundle envelope
23
+ *
24
+ * IO is not abstracted — the function calls execSync, readFileSync,
25
+ * esbuild. Callers needing test isolation should use fixture dirs.
26
+ */
27
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
28
+ import { join, resolve } from "node:path";
29
+ import { execSync } from "node:child_process";
30
+ import consola from "consola";
31
+ import { detectFramework, detectAstroCloudflareBuild, detectMonorepo, detectNextjsMode, getSSRServerDir, getSSRServerEntry, getClientAssetsDir, getDefaultBuildOutput, isSSRFramework, isPreBundledFramework, collectServerFiles, planDeploy, } from "@solcreek/sdk";
32
+ import { collectAssets } from "./bundle.js";
33
+ import { bundleSSRServer } from "./ssr-bundle.js";
34
+ import { bundleWorker } from "./worker-bundle.js";
35
+ import { hasAdapterOutput, buildNextjs, patchBundledWorker } from "./nextjs.js";
36
+ import { patchBareNodeImports } from "../commands/deploy.js";
37
+ export async function prepareDeployBundle(input) {
38
+ const { cwd, resolved, skipBuild } = input;
39
+ // 1. Framework detection. Trust the resolved config if it carries
40
+ // one (creek.toml can pin it); otherwise re-read package.json. We
41
+ // re-read here rather than rely solely on resolved so that a project
42
+ // without creek.toml still gets the auto-detected framework when
43
+ // running against a pre-existing wrangler.* config.
44
+ const pkgJsonPath = join(cwd, "package.json");
45
+ const pkg = existsSync(pkgJsonPath)
46
+ ? JSON.parse(readFileSync(pkgJsonPath, "utf-8"))
47
+ : null;
48
+ const framework = resolved.framework ?? (pkg ? detectFramework(pkg) : null);
49
+ const nextjsMode = framework === "nextjs" && pkg ? detectNextjsMode(pkg, cwd) : null;
50
+ const monorepo = framework === "nextjs" ? detectMonorepo(cwd) : { isMonorepo: false, root: null };
51
+ // 2. Build (when not skipped). Framework-specific build for Next.js
52
+ // adapter; otherwise the user's build script.
53
+ if (!skipBuild && resolved.buildCommand) {
54
+ if (nextjsMode === "opennext") {
55
+ try {
56
+ buildNextjs(cwd, monorepo.isMonorepo);
57
+ }
58
+ catch {
59
+ consola.error("Next.js build failed");
60
+ process.exit(1);
61
+ }
62
+ }
63
+ else {
64
+ const buildCmd = resolved.buildCommand;
65
+ if (buildCmd.length > 500) {
66
+ consola.error("Invalid build command (too long)");
67
+ process.exit(1);
68
+ }
69
+ consola.start(` ${buildCmd}`);
70
+ try {
71
+ execSync(buildCmd, { cwd, stdio: "inherit" });
72
+ }
73
+ catch {
74
+ consola.error("Build failed");
75
+ process.exit(1);
76
+ }
77
+ consola.success(" Build complete");
78
+ }
79
+ }
80
+ // 3. Compute output dir. Next.js adapter writes to .creek/adapter-output;
81
+ // everything else honours [build].output.
82
+ const useAdapterOutput = framework === "nextjs" && hasAdapterOutput(cwd);
83
+ const outputDir = useAdapterOutput
84
+ ? resolve(cwd, ".creek/adapter-output")
85
+ : resolve(cwd, resolved.buildOutput || getDefaultBuildOutput(framework));
86
+ // 4. Post-build framework adapter detection. Astro can be either SSG
87
+ // or CF-adapter-SSR; we only know which after build.
88
+ const astroAdapter = framework === "astro" ? detectAstroCloudflareBuild(cwd) : null;
89
+ // 5. Plan the deploy shape. planDeploy is a pure function — pass in
90
+ // detection results, get out a structured plan with explicit error
91
+ // cases. All branching that used to be inline in deploy.ts lives in
92
+ // deploy-plan.ts now (table-tested).
93
+ const planResult = planDeploy({
94
+ framework,
95
+ workerEntry: resolved.workerEntry ?? null,
96
+ workerEntryExists: !!resolved.workerEntry &&
97
+ existsSync(resolve(cwd, resolved.workerEntry)),
98
+ buildOutput: resolved.buildOutput || "dist",
99
+ buildOutputExists: existsSync(outputDir),
100
+ astroCF: astroAdapter,
101
+ });
102
+ if (!planResult.ok) {
103
+ consola.error(planResult.reason);
104
+ process.exit(1);
105
+ }
106
+ const plan = planResult.plan;
107
+ // 6. Collect client assets. The dir depends on framework (Next.js
108
+ // adapter, Astro CF split output) but the inclusion decision is the
109
+ // plan's call.
110
+ let clientAssets = {};
111
+ let fileList = [];
112
+ if (plan.assets.enabled && plan.assets.dir) {
113
+ let clientAssetsDir;
114
+ if (useAdapterOutput) {
115
+ clientAssetsDir = resolve(outputDir, "assets");
116
+ }
117
+ else if (astroAdapter) {
118
+ clientAssetsDir = resolve(cwd, astroAdapter.assetsDir);
119
+ }
120
+ else {
121
+ clientAssetsDir = resolve(cwd, plan.assets.dir);
122
+ // SSR frameworks split client assets into a sub-dir of buildOutput.
123
+ if (isSSRFramework(framework) && framework) {
124
+ const subdir = getClientAssetsDir(framework);
125
+ if (subdir)
126
+ clientAssetsDir = resolve(clientAssetsDir, subdir);
127
+ }
128
+ }
129
+ const collected = collectAssets(clientAssetsDir);
130
+ clientAssets = collected.assets;
131
+ fileList = collected.fileList;
132
+ }
133
+ // 7. Bundle server / worker. Five strategies, dispatched by either
134
+ // (a) the framework when it's pre-bundled SSR, or (b) the plan's
135
+ // worker.strategy for user-declared workers.
136
+ let serverFiles;
137
+ if (astroAdapter) {
138
+ // Astro CF adapter writes a pre-bundled worker we just upload.
139
+ const serverDir = resolve(cwd, astroAdapter.serverDir);
140
+ if (existsSync(serverDir)) {
141
+ consola.start(" Collecting Astro CF server files...");
142
+ const collected = collectServerFiles(serverDir);
143
+ serverFiles = base64ServerFiles(collected);
144
+ consola.success(` Astro CF worker: ${Object.keys(collected).length} files`);
145
+ }
146
+ }
147
+ else if (isSSRFramework(framework) && framework) {
148
+ if (framework === "nextjs" && hasAdapterOutput(cwd)) {
149
+ // Next.js adapter output → patch bare imports, upload as-is.
150
+ const adapterServerDir = resolve(cwd, ".creek/adapter-output/server");
151
+ consola.start(" Collecting adapter output...");
152
+ const collected = {};
153
+ if (existsSync(adapterServerDir)) {
154
+ for (const f of readdirSync(adapterServerDir)) {
155
+ const fp = join(adapterServerDir, f);
156
+ if (!statSync(fp).isFile() || f.endsWith(".map"))
157
+ continue;
158
+ let content = readFileSync(fp);
159
+ if (f.endsWith(".js") || f.endsWith(".mjs")) {
160
+ content = Buffer.from(patchBareNodeImports(content.toString("utf-8")));
161
+ }
162
+ collected[f] = content;
163
+ }
164
+ }
165
+ serverFiles = base64ServerFiles(collected);
166
+ consola.success(` Worker bundled: ${Object.keys(collected).length} files (${kb(collected)}KB)`);
167
+ }
168
+ else if (framework === "nextjs") {
169
+ // Legacy Next.js: wrangler dry-run produces the bundle.
170
+ const bundleDir = resolve(cwd, ".creek/bundled");
171
+ consola.start(" Bundling Next.js worker (legacy)...");
172
+ execSync(`npx wrangler deploy --dry-run --outdir "${bundleDir}"`, { cwd, stdio: "pipe" });
173
+ patchBundledWorker(bundleDir, resolve(cwd, ".open-next"));
174
+ const collected = {};
175
+ if (existsSync(bundleDir)) {
176
+ for (const f of readdirSync(bundleDir)) {
177
+ const fp = join(bundleDir, f);
178
+ if (!statSync(fp).isFile() || f.endsWith(".map") || f === "README.md")
179
+ continue;
180
+ collected[f] = readFileSync(fp);
181
+ }
182
+ }
183
+ serverFiles = base64ServerFiles(collected);
184
+ consola.success(` Worker bundled: ${Object.keys(collected).length} files (${kb(collected)}KB)`);
185
+ }
186
+ else if (isPreBundledFramework(framework)) {
187
+ // Nuxt / SvelteKit / SolidStart — the framework already produced
188
+ // a bundled server; we just collect.
189
+ const serverDirRel = getSSRServerDir(framework);
190
+ if (serverDirRel) {
191
+ const serverDir = resolve(cwd, serverDirRel);
192
+ if (existsSync(serverDir)) {
193
+ consola.start(" Collecting SSR server files...");
194
+ const collected = collectServerFiles(serverDir);
195
+ serverFiles = base64ServerFiles(collected);
196
+ consola.success(` SSR server: ${Object.keys(collected).length} files`);
197
+ }
198
+ }
199
+ }
200
+ else {
201
+ // Fallback for SSR frameworks that emit a single entry file.
202
+ const serverEntry = getSSRServerEntry(framework);
203
+ if (serverEntry) {
204
+ const serverEntryPath = resolve(outputDir, serverEntry);
205
+ if (existsSync(serverEntryPath)) {
206
+ consola.start(" Bundling SSR server...");
207
+ const bundled = await bundleSSRServer(serverEntryPath);
208
+ serverFiles = { "server.js": Buffer.from(bundled).toString("base64") };
209
+ consola.success(` SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ else if (plan.worker.strategy === "esbuild-bundle" && plan.worker.entry) {
215
+ const workerEntryPath = resolve(cwd, plan.worker.entry);
216
+ consola.start(" Bundling worker...");
217
+ const bundled = await bundleWorker(workerEntryPath, cwd, {
218
+ hasClientAssets: plan.assets.enabled,
219
+ });
220
+ serverFiles = { "worker.js": Buffer.from(bundled).toString("base64") };
221
+ consola.success(` Worker bundled (${Math.round(bundled.length / 1024)}KB)`);
222
+ }
223
+ else if (plan.worker.strategy === "upload-asis" && plan.worker.entry) {
224
+ // Pre-bundled worker (e.g. dist/_worker.mjs from the user's own
225
+ // esbuild step). Ship verbatim — no Creek runtime wrapper, no
226
+ // re-bundle. This is the path that keeps deployed bundles free of
227
+ // any @solcreek/* dependency.
228
+ const bytes = readFileSync(resolve(cwd, plan.worker.entry));
229
+ serverFiles = { "worker.js": bytes.toString("base64") };
230
+ consola.success(` Worker (pre-bundled, ${Math.round(bytes.length / 1024)}KB)`);
231
+ }
232
+ // 8. When the worker bundle lives INSIDE the asset dir
233
+ // (dist/_worker.mjs), drop it from clientAssets so it isn't also
234
+ // served as a publicly accessible static file.
235
+ if (plan.assets.excludeFile) {
236
+ delete clientAssets[plan.assets.excludeFile];
237
+ delete clientAssets["/" + plan.assets.excludeFile];
238
+ fileList = fileList.filter((p) => p !== plan.assets.excludeFile && p !== "/" + plan.assets.excludeFile);
239
+ }
240
+ // 9. Resolve effective render mode + entrypoint. Astro CF post-build
241
+ // detection upgrades a pre-build "spa" classification to true SSR.
242
+ const effectiveRenderMode = astroAdapter
243
+ ? "ssr"
244
+ : plan.renderMode;
245
+ const effectiveEntrypoint = astroAdapter
246
+ ? "entry.mjs"
247
+ : (plan.worker.entry ?? null);
248
+ return {
249
+ plan,
250
+ framework,
251
+ astroAdapter,
252
+ effectiveRenderMode,
253
+ effectiveEntrypoint,
254
+ fileList,
255
+ assets: clientAssets,
256
+ serverFiles,
257
+ };
258
+ }
259
+ // --- helpers ---
260
+ function base64ServerFiles(collected) {
261
+ return Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
262
+ }
263
+ function kb(collected) {
264
+ return Math.round(Object.values(collected).reduce((s, b) => s + b.length, 0) / 1024);
265
+ }
266
+ //# sourceMappingURL=prepare-bundle.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",