@solcreek/cli 0.4.9 → 0.4.11
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/dist/commands/deploy.js +35 -365
- package/dist/commands/logs.d.ts +75 -0
- package/dist/commands/logs.js +233 -0
- package/dist/index.js +2 -0
- package/dist/utils/prepare-bundle.d.ts +63 -0
- package/dist/utils/prepare-bundle.js +266 -0
- package/package.json +2 -2
package/dist/commands/deploy.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import consola from "consola";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
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: ${
|
|
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:
|
|
824
|
-
entrypoint:
|
|
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
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
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
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1050
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `creek logs` — read structured tenant logs from R2 archive.
|
|
3
|
+
*
|
|
4
|
+
* Auth: requires `creek login`. Server is responsible for tenant
|
|
5
|
+
* isolation — the CLI simply targets the project slug, the
|
|
6
|
+
* authenticated session decides which team's logs are visible.
|
|
7
|
+
*
|
|
8
|
+
* Project resolution: --project flag wins; otherwise resolve from
|
|
9
|
+
* cwd creek.toml / wrangler.*. If neither, error with hint.
|
|
10
|
+
*
|
|
11
|
+
* `--follow` is reserved for Step 7 (WebSocket subscribe). For now
|
|
12
|
+
* the command is one-shot historical query.
|
|
13
|
+
*
|
|
14
|
+
* Output:
|
|
15
|
+
* default → human-friendly multi-line per entry, colored by
|
|
16
|
+
* outcome (ok=dim, exception=red, etc.)
|
|
17
|
+
* --json → newline-delimited LogEntry JSON, suitable for `| jq`
|
|
18
|
+
*/
|
|
19
|
+
export declare const logsCommand: import("citty").CommandDef<{
|
|
20
|
+
json: {
|
|
21
|
+
type: "boolean";
|
|
22
|
+
description: string;
|
|
23
|
+
default: boolean;
|
|
24
|
+
};
|
|
25
|
+
yes: {
|
|
26
|
+
type: "boolean";
|
|
27
|
+
description: string;
|
|
28
|
+
default: boolean;
|
|
29
|
+
};
|
|
30
|
+
project: {
|
|
31
|
+
type: "string";
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
34
|
+
since: {
|
|
35
|
+
type: "string";
|
|
36
|
+
description: string;
|
|
37
|
+
};
|
|
38
|
+
until: {
|
|
39
|
+
type: "string";
|
|
40
|
+
description: string;
|
|
41
|
+
};
|
|
42
|
+
outcome: {
|
|
43
|
+
type: "string";
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
"script-type": {
|
|
47
|
+
type: "string";
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
deployment: {
|
|
51
|
+
type: "string";
|
|
52
|
+
description: string;
|
|
53
|
+
};
|
|
54
|
+
branch: {
|
|
55
|
+
type: "string";
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
58
|
+
level: {
|
|
59
|
+
type: "string";
|
|
60
|
+
description: string;
|
|
61
|
+
};
|
|
62
|
+
search: {
|
|
63
|
+
type: "string";
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
limit: {
|
|
67
|
+
type: "string";
|
|
68
|
+
description: string;
|
|
69
|
+
};
|
|
70
|
+
follow: {
|
|
71
|
+
type: "boolean";
|
|
72
|
+
description: string;
|
|
73
|
+
};
|
|
74
|
+
}>;
|
|
75
|
+
//# sourceMappingURL=logs.d.ts.map
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { CreekClient, resolveConfig, ConfigNotFoundError, } from "@solcreek/sdk";
|
|
4
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
5
|
+
import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS, } from "../utils/output.js";
|
|
6
|
+
/**
|
|
7
|
+
* `creek logs` — read structured tenant logs from R2 archive.
|
|
8
|
+
*
|
|
9
|
+
* Auth: requires `creek login`. Server is responsible for tenant
|
|
10
|
+
* isolation — the CLI simply targets the project slug, the
|
|
11
|
+
* authenticated session decides which team's logs are visible.
|
|
12
|
+
*
|
|
13
|
+
* Project resolution: --project flag wins; otherwise resolve from
|
|
14
|
+
* cwd creek.toml / wrangler.*. If neither, error with hint.
|
|
15
|
+
*
|
|
16
|
+
* `--follow` is reserved for Step 7 (WebSocket subscribe). For now
|
|
17
|
+
* the command is one-shot historical query.
|
|
18
|
+
*
|
|
19
|
+
* Output:
|
|
20
|
+
* default → human-friendly multi-line per entry, colored by
|
|
21
|
+
* outcome (ok=dim, exception=red, etc.)
|
|
22
|
+
* --json → newline-delimited LogEntry JSON, suitable for `| jq`
|
|
23
|
+
*/
|
|
24
|
+
export const logsCommand = defineCommand({
|
|
25
|
+
meta: {
|
|
26
|
+
name: "logs",
|
|
27
|
+
description: "Read recent log entries for a project",
|
|
28
|
+
},
|
|
29
|
+
args: {
|
|
30
|
+
project: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Project slug. Defaults to creek.toml in cwd.",
|
|
33
|
+
},
|
|
34
|
+
since: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Time window start. Relative (1h, 30m, 2d) or ISO. Default: 1h",
|
|
37
|
+
},
|
|
38
|
+
until: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: 'Time window end. "now" or ISO. Default: now',
|
|
41
|
+
},
|
|
42
|
+
outcome: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Filter by tail outcome. Repeatable via comma (ok,exception).",
|
|
45
|
+
},
|
|
46
|
+
"script-type": {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Filter by production/branch/deployment. Repeatable via comma.",
|
|
49
|
+
},
|
|
50
|
+
deployment: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "8-hex deploy id — scopes to that single deployment preview.",
|
|
53
|
+
},
|
|
54
|
+
branch: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Branch name — scopes to that branch preview.",
|
|
57
|
+
},
|
|
58
|
+
level: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Filter by console level (error,warn,...). Entry needs at least one matching log line.",
|
|
61
|
+
},
|
|
62
|
+
search: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Substring match against console messages, exceptions, and request URLs.",
|
|
65
|
+
},
|
|
66
|
+
limit: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Max entries to print. Default 100, max 1000.",
|
|
69
|
+
},
|
|
70
|
+
follow: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "(Step 7 — not yet implemented) Live tail via WebSocket.",
|
|
73
|
+
},
|
|
74
|
+
...globalArgs,
|
|
75
|
+
},
|
|
76
|
+
async run({ args }) {
|
|
77
|
+
if (args.follow) {
|
|
78
|
+
consola.warn("--follow is not yet implemented (Phase 8 Step 7).");
|
|
79
|
+
consola.info("This command currently returns historical entries only.");
|
|
80
|
+
}
|
|
81
|
+
const jsonMode = resolveJsonMode(args);
|
|
82
|
+
const token = getToken();
|
|
83
|
+
if (!token) {
|
|
84
|
+
if (jsonMode)
|
|
85
|
+
jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
|
|
86
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const projectSlug = await resolveProjectSlug(args.project, jsonMode);
|
|
90
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
91
|
+
const filters = {
|
|
92
|
+
...(args.since ? { since: args.since } : {}),
|
|
93
|
+
...(args.until ? { until: args.until } : {}),
|
|
94
|
+
...(args.deployment ? { deployment: args.deployment } : {}),
|
|
95
|
+
...(args.branch ? { branch: args.branch } : {}),
|
|
96
|
+
...(args.search ? { search: args.search } : {}),
|
|
97
|
+
...(args.limit ? { limit: Number(args.limit) } : {}),
|
|
98
|
+
...(args.outcome
|
|
99
|
+
? { outcomes: parseList(args.outcome) }
|
|
100
|
+
: {}),
|
|
101
|
+
...(args["script-type"]
|
|
102
|
+
? {
|
|
103
|
+
scriptTypes: parseList(args["script-type"]),
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(args.level
|
|
107
|
+
? { levels: parseList(args.level) }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
let response;
|
|
111
|
+
try {
|
|
112
|
+
response = await client.getLogs(projectSlug, filters);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
if (jsonMode)
|
|
117
|
+
jsonOutput({ ok: false, error: "logs_failed", message: msg }, 1, []);
|
|
118
|
+
consola.error(`Failed to read logs: ${msg}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
if (jsonMode) {
|
|
122
|
+
// ndjson — easy to pipe to jq
|
|
123
|
+
for (const entry of response.entries) {
|
|
124
|
+
process.stdout.write(JSON.stringify(entry) + "\n");
|
|
125
|
+
}
|
|
126
|
+
if (response.truncated) {
|
|
127
|
+
process.stderr.write(`# truncated — more entries match. Refine --since/--limit to narrow.\n`);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (response.entries.length === 0) {
|
|
132
|
+
consola.info("No log entries match the query.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Human output: oldest at top so the latest entry is closest to the prompt.
|
|
136
|
+
const ordered = [...response.entries].reverse();
|
|
137
|
+
for (const entry of ordered) {
|
|
138
|
+
printEntry(entry);
|
|
139
|
+
}
|
|
140
|
+
if (response.truncated) {
|
|
141
|
+
consola.warn(`Truncated to ${response.entries.length} entries — refine --since/--limit to see more.`);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
async function resolveProjectSlug(override, jsonMode) {
|
|
146
|
+
if (override)
|
|
147
|
+
return override;
|
|
148
|
+
let resolved;
|
|
149
|
+
try {
|
|
150
|
+
resolved = resolveConfig(process.cwd());
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err instanceof ConfigNotFoundError) {
|
|
154
|
+
if (jsonMode)
|
|
155
|
+
jsonOutput({ ok: false, error: "no_project", message: "No project config in cwd" }, 1, NO_PROJECT_BREADCRUMBS);
|
|
156
|
+
consola.error("No project config in cwd. Pass --project <slug>.");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
return resolved.projectName;
|
|
162
|
+
}
|
|
163
|
+
function parseList(input) {
|
|
164
|
+
return input
|
|
165
|
+
.split(",")
|
|
166
|
+
.map((s) => s.trim())
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
}
|
|
169
|
+
const COLOR = {
|
|
170
|
+
reset: "\x1b[0m",
|
|
171
|
+
dim: "\x1b[2m",
|
|
172
|
+
red: "\x1b[31m",
|
|
173
|
+
yellow: "\x1b[33m",
|
|
174
|
+
green: "\x1b[32m",
|
|
175
|
+
cyan: "\x1b[36m",
|
|
176
|
+
gray: "\x1b[90m",
|
|
177
|
+
};
|
|
178
|
+
function color(s, c) {
|
|
179
|
+
return process.stdout.isTTY ? `${COLOR[c]}${s}${COLOR.reset}` : s;
|
|
180
|
+
}
|
|
181
|
+
function printEntry(entry) {
|
|
182
|
+
const ts = new Date(entry.timestamp).toISOString().replace("T", " ").slice(0, 19);
|
|
183
|
+
const outcomeColor = entry.outcome === "ok"
|
|
184
|
+
? "gray"
|
|
185
|
+
: entry.outcome === "exception"
|
|
186
|
+
? "red"
|
|
187
|
+
: "yellow";
|
|
188
|
+
const status = entry.request?.status;
|
|
189
|
+
const statusStr = status === undefined
|
|
190
|
+
? ""
|
|
191
|
+
: status >= 500
|
|
192
|
+
? color(String(status), "red")
|
|
193
|
+
: status >= 400
|
|
194
|
+
? color(String(status), "yellow")
|
|
195
|
+
: color(String(status), "green");
|
|
196
|
+
const variant = entry.scriptType === "production"
|
|
197
|
+
? ""
|
|
198
|
+
: entry.scriptType === "branch"
|
|
199
|
+
? ` [branch ${entry.branch}]`
|
|
200
|
+
: ` [deploy ${entry.deployId}]`;
|
|
201
|
+
const headline = [
|
|
202
|
+
color(ts, "dim"),
|
|
203
|
+
color(entry.outcome, outcomeColor),
|
|
204
|
+
entry.request?.method ?? "—",
|
|
205
|
+
entry.request?.url
|
|
206
|
+
? new URL(entry.request.url).pathname + new URL(entry.request.url).search
|
|
207
|
+
: "—",
|
|
208
|
+
statusStr,
|
|
209
|
+
color(variant, "dim"),
|
|
210
|
+
]
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join(" ");
|
|
213
|
+
process.stdout.write(headline + "\n");
|
|
214
|
+
for (const log of entry.logs) {
|
|
215
|
+
const levelColor = log.level === "error" ? "red" : log.level === "warn" ? "yellow" : "cyan";
|
|
216
|
+
const msg = log.message
|
|
217
|
+
.map((m) => (typeof m === "string" ? m : safeStringify(m)))
|
|
218
|
+
.join(" ");
|
|
219
|
+
process.stdout.write(` ${color(log.level.padEnd(5), levelColor)} ${msg}\n`);
|
|
220
|
+
}
|
|
221
|
+
for (const ex of entry.exceptions) {
|
|
222
|
+
process.stdout.write(` ${color("exc", "red")} ${color(ex.name, "red")}: ${ex.message}\n`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function safeStringify(v) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.stringify(v);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return String(v);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=logs.js.map
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { devCommand } from "./commands/dev.js";
|
|
|
18
18
|
import { rollbackCommand } from "./commands/rollback.js";
|
|
19
19
|
import { opsCommand } from "./commands/ops.js";
|
|
20
20
|
import { queueCommand } from "./commands/queue.js";
|
|
21
|
+
import { logsCommand } from "./commands/logs.js";
|
|
21
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
23
24
|
// Read version from the "creek" facade package (what users install),
|
|
@@ -43,6 +44,7 @@ const main = defineCommand({
|
|
|
43
44
|
status: statusCommand,
|
|
44
45
|
projects: projectsCommand,
|
|
45
46
|
deployments: deploymentsCommand,
|
|
47
|
+
logs: logsCommand,
|
|
46
48
|
login: loginCommand,
|
|
47
49
|
whoami: whoamiCommand,
|
|
48
50
|
init: initCommand,
|
|
@@ -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.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "CLI for the Creek deployment platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"esbuild": "^0.25.0",
|
|
34
34
|
"smol-toml": "^1.3.1",
|
|
35
35
|
"ws": "^8.20.0",
|
|
36
|
-
"@solcreek/sdk": "0.4.
|
|
36
|
+
"@solcreek/sdk": "0.4.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/dom": "^10.4.1",
|