From 93d2ceb50b94d496807cfcb89629492ff0bb49d7 Mon Sep 17 00:00:00 2001 From: ronnyr Date: Tue, 22 Sep 2026 12:18:05 +0300 Subject: [PATCH 1/3] feat(project): default the site block's build commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `site` block had to spell out every command, and the platform that runs these projects kept its own copy of the same values to fill the gaps. Defaults live here now, matching what `base44 create` scaffolds: install `npm install`, build `npm run build`, output `./dist`. A block only names what its project does differently. `serveCommand` is deliberately left undefaulted: `base44 dev` reads its absence as "no frontend to run here" and runs the backend alone, and a default would spawn a dev server for every site block — one that fails immediately takes the backend down with it. Commands that exist only to serve a frontend default it themselves. `site` itself stays optional, so "is there a site?" still keys on the block: a backend-only project omits it and has no site. Inside a block the build fields always resolve, which moves two refusals from "this field is missing" to "there is no site block" — `base44 build` and `site deploy` reword their hints, and `deploy` reads the block rather than one field. Projects that declared a partial block change behaviour: they now build and deploy on the defaults instead of being refused. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL --- CHANGELOG.md | 1 + docs/resources.md | 2 +- .../src/cli/commands/project/site-build.ts | 2 +- packages/cli/src/cli/commands/site/deploy.ts | 2 +- packages/cli/src/core/project/deploy.ts | 4 ++- packages/cli/src/core/project/schema.ts | 16 +++++++-- packages/cli/tests/cli/build.spec.ts | 19 ++++------- packages/cli/tests/core/project.spec.ts | 33 +++++++++++++++++++ .../with-site-defaults/base44/config.jsonc | 5 +++ .../fixtures/with-site-defaults/package.json | 7 ++++ 10 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 packages/cli/tests/fixtures/with-site-defaults/base44/config.jsonc create mode 100644 packages/cli/tests/fixtures/with-site-defaults/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 314365c3..95ffbb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Changed +- The `site` block's build commands now default to the conventions `base44 create` scaffolds, so a block only has to name what a project does differently: `installCommand` `npm install`, `buildCommand` `npm run build`, `outputDirectory` `./dist`. `site` itself stays optional — a backend-only project omits it and still has no site — and `serveCommand` is not defaulted, so `base44 dev` still runs the backend alone for a project that names no dev server. Projects that declared a partial block change behaviour: `base44 build` and `deploy --build` now build them instead of refusing, and `base44 deploy` deploys a site. The "add `site.`" errors now fire only when there is no `site` block at all. - `base44 sandbox` help now explains that writes are committed but not checkpointed: a Restore or Revert in the builder rolls the app back to the last checkpoint and discards everything after it, so run `base44 sandbox checkpoint` after each unit of work and before stopping. The note appears on `sandbox`, `sandbox write`, `sandbox edit`, `sandbox run`, and `sandbox checkpoint`. - The `backend-and-client` template now scaffolds the same client convention editor-created apps use: `@base44/vite-plugin` + `src/lib/app-params.js`, with the SDK client on same-origin `/api` (`serverUrl: ''`). Under `base44 dev` the plugin proxies `/api` to the local dev backend, so scaffolded apps get local entities and functions; the app id is injected via `VITE_BASE44_APP_ID` by `base44 dev`, `base44 dev --remote`, and the build/deploy commands instead of being baked into source. diff --git a/docs/resources.md b/docs/resources.md index 91a83a01..b413c4f4 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -122,7 +122,7 @@ const viaDeployments = deploymentsApiEnabled(); ``` - Gate on → the deployments API, see [deployments.md](deployments.md). Whether the build carries a worker changes what that flow sends, never which flow runs, and a worker brings its own assets directory — so the command may pass a null `outputDir`. -- Gate off → the legacy tar.gz path: tar.gz `site.outputDirectory` and upload via `POST /api/apps/{app_id}/deploy-dist`. This is the flow that requires the config field, and the one that raises "No site configuration found." +- Gate off → the legacy tar.gz path: tar.gz `site.outputDirectory` and upload via `POST /api/apps/{app_id}/deploy-dist`. This is the flow that requires the config field — it defaults to `./dist` inside a `site` block, so "No site configuration found." is raised only for a project with no block at all. Each flow validates its own inputs, so the decision itself is a boolean and needs nothing from the tree. diff --git a/packages/cli/src/cli/commands/project/site-build.ts b/packages/cli/src/cli/commands/project/site-build.ts index ef96e0b4..82e20876 100644 --- a/packages/cli/src/cli/commands/project/site-build.ts +++ b/packages/cli/src/cli/commands/project/site-build.ts @@ -19,7 +19,7 @@ export async function runSiteBuild( hints: [ { message: - 'Add \'site.buildCommand\' to your config.jsonc (e.g., "site": { "buildCommand": "npm run build" })', + 'Add a \'site\' block to your config.jsonc (e.g., "site": { "buildCommand": "npm run build" }). Inside one, buildCommand defaults to "npm run build".', }, ], }); diff --git a/packages/cli/src/cli/commands/site/deploy.ts b/packages/cli/src/cli/commands/site/deploy.ts index c5d2c267..eaf3af9f 100644 --- a/packages/cli/src/cli/commands/site/deploy.ts +++ b/packages/cli/src/cli/commands/site/deploy.ts @@ -126,7 +126,7 @@ async function deployTarball( hints: [ { message: - 'Add \'site.outputDirectory\' to your config.jsonc (e.g., "site": { "outputDirectory": "dist" })', + 'Add a \'site\' block to your config.jsonc (e.g., "site": { "outputDirectory": "dist" }). Inside one, outputDirectory defaults to "./dist".', }, ], }); diff --git a/packages/cli/src/core/project/deploy.ts b/packages/cli/src/core/project/deploy.ts index 4a671197..ddfc03f6 100644 --- a/packages/cli/src/core/project/deploy.ts +++ b/packages/cli/src/core/project/deploy.ts @@ -40,7 +40,9 @@ export function hasResourcesToDeploy(projectData: ProjectData): boolean { connectors, authConfig, } = projectData; - const hasSite = Boolean(project.site?.outputDirectory); + // The block, not the field: `outputDirectory` always resolves now (it defaults), + // so a project declares it has a site by having the block at all. + const hasSite = project.site !== undefined; const hasEntities = entities.length > 0; const hasFunctions = functions.length > 0; const hasActors = actors.length > 0; diff --git a/packages/cli/src/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index 42041acf..849fa89a 100644 --- a/packages/cli/src/core/project/schema.ts +++ b/packages/cli/src/core/project/schema.ts @@ -12,11 +12,21 @@ export const TemplatesConfigSchema = z.object({ }); export type Template = z.infer; +// Defaults are the conventions `base44 create` scaffolds, so a `site` block only +// has to name what this project does differently. `site` itself stays optional: +// a backend-only project omits it and has no site at all, which is what every +// "is there a site?" check keys on. +// +// `serveCommand` is deliberately NOT defaulted. `base44 dev` reads its absence as +// "this project has no frontend to run here" and runs the backend alone; with a +// default it would spawn one for every site block, and a dev server that fails +// immediately takes the backend down with it. Commands that exist only to serve a +// frontend default it themselves, where the intent is unambiguous. const SiteConfigSchema = z.object({ - buildCommand: z.string().optional(), + buildCommand: z.string().optional().default("npm run build"), serveCommand: z.string().optional(), - outputDirectory: z.string().optional(), - installCommand: z.string().optional(), + outputDirectory: z.string().optional().default("./dist"), + installCommand: z.string().optional().default("npm install"), }); const PluginMetadataSchema = z.object({ diff --git a/packages/cli/tests/cli/build.spec.ts b/packages/cli/tests/cli/build.spec.ts index a7d758a1..bb05dc6c 100644 --- a/packages/cli/tests/cli/build.spec.ts +++ b/packages/cli/tests/cli/build.spec.ts @@ -15,13 +15,15 @@ describe("build command", () => { ); }); - it("fails when the project has no site.buildCommand", async () => { - await t.givenLoggedInWithProject(fixture("with-site")); + it("falls back to the default buildCommand when the site block sets none", async () => { + await t.givenLoggedInWithProject(fixture("with-site-defaults")); const result = await t.run("build"); - t.expectResult(result).toFail(); - t.expectResult(result).toContain("No site build command found"); + t.expectResult(result).toSucceed(); + expect(await t.readProjectFile("build-env.txt")).toBe( + `BUILD_APP=${t.api.appId}`, + ); }); it("fails when the buildCommand fails", async () => { @@ -104,15 +106,6 @@ describe("deploy --build", () => { t.expectResult(result).toContain("Build failed"); }); - it("--build fails when the project has no site.buildCommand", async () => { - await t.givenLoggedInWithProject(fixture("with-site")); - - const result = await t.run("deploy", "--yes", "--build"); - - t.expectResult(result).toFail(); - t.expectResult(result).toContain("No site build command found"); - }); - it("--build fails when the project has no site configuration", async () => { await t.givenLoggedInWithProject(fixture("with-entities")); diff --git a/packages/cli/tests/core/project.spec.ts b/packages/cli/tests/core/project.spec.ts index 9e9e0def..8bfeb474 100644 --- a/packages/cli/tests/core/project.spec.ts +++ b/packages/cli/tests/core/project.spec.ts @@ -23,6 +23,39 @@ describe("readProjectConfig", () => { expect(result.agents).toEqual([]); }); + it("defaults the site commands a block does not name", async () => { + const result = await readProjectConfig(resolve(FIXTURES_DIR, "with-site")); + + // The one field the fixture names survives; the rest come from the schema. + expect(result.project.site).toEqual({ + outputDirectory: "site-output", + buildCommand: "npm run build", + installCommand: "npm install", + }); + }); + + it("defaults every site command for an empty block", async () => { + const result = await readProjectConfig( + resolve(FIXTURES_DIR, "with-site-defaults"), + ); + + expect(result.project.site).toEqual({ + outputDirectory: "./dist", + buildCommand: "npm run build", + installCommand: "npm install", + }); + + // Not defaulted: `base44 dev` runs the backend alone without one. + expect(result.project.site?.serveCommand).toBeUndefined(); + }); + + it("leaves a project with no site block without a site", async () => { + // What every "is there a site?" check keys on — a backend-only project. + const result = await readProjectConfig(resolve(FIXTURES_DIR, "basic")); + + expect(result.project.site).toBeUndefined(); + }); + it("reads project with entities", async () => { const result = await readProjectConfig( resolve(FIXTURES_DIR, "with-entities"), diff --git a/packages/cli/tests/fixtures/with-site-defaults/base44/config.jsonc b/packages/cli/tests/fixtures/with-site-defaults/base44/config.jsonc new file mode 100644 index 00000000..0f911f76 --- /dev/null +++ b/packages/cli/tests/fixtures/with-site-defaults/base44/config.jsonc @@ -0,0 +1,5 @@ +{ + "name": "Site Defaults Project", + // A site block that names nothing: every command comes from the schema defaults. + "site": {} +} diff --git a/packages/cli/tests/fixtures/with-site-defaults/package.json b/packages/cli/tests/fixtures/with-site-defaults/package.json new file mode 100644 index 00000000..761301ac --- /dev/null +++ b/packages/cli/tests/fixtures/with-site-defaults/package.json @@ -0,0 +1,7 @@ +{ + "name": "site-defaults-fixture", + "private": true, + "scripts": { + "build": "node -e \"require('fs').writeFileSync('build-env.txt', 'BUILD_APP=' + process.env.VITE_BASE44_APP_ID)\"" + } +} From 9c57bb802ae3d94be01c62898032e2ea2c3ae9a6 Mon Sep 17 00:00:00 2001 From: ronnyr Date: Tue, 22 Sep 2026 13:08:25 +0300 Subject: [PATCH 2/3] fix(project): do not default outputDirectory, its absence is the signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found that defaulting `outputDirectory` turns a field whose absence means "nothing built to upload" into one that always resolves, and two callers act on that: `deployAll` still keyed the upload on the field, so `base44 deploy` on a `"site": {"serveCommand": ...}` project (the shape of this repo's own with-serve-command fixture) would resolve `./dist` and either fail a previously-green deploy after pushing every other resource, or publish whatever happened to sit in `./dist` — a backend bundle, typically — as the app's site. `eject`'s guard `installCommand && buildCommand` became dead, so `eject --yes` would run a network install, a build and a deploy for any ejected app with a site block, unprompted. So `outputDirectory` joins `serveCommand` as deliberately undefaulted, which is the same rule applied consistently: only the two commands whose absence means nothing default. `hasResourcesToDeploy` needs no change and drops out of the diff; eject now keys on `outputDirectory` like every other caller. Added the deploy test that would have caught the first one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL --- CHANGELOG.md | 2 +- docs/deployments.md | 2 +- docs/resources.md | 2 +- .../cli/src/cli/commands/project/eject.ts | 19 +++++++++++----- packages/cli/src/cli/commands/site/deploy.ts | 2 +- packages/cli/src/core/project/deploy.ts | 4 +--- packages/cli/src/core/project/schema.ts | 22 +++++++++---------- packages/cli/tests/cli/deploy.spec.ts | 12 ++++++++++ packages/cli/tests/core/project.spec.ts | 5 +++-- 9 files changed, 44 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ffbb38..a5510080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ ### Changed -- The `site` block's build commands now default to the conventions `base44 create` scaffolds, so a block only has to name what a project does differently: `installCommand` `npm install`, `buildCommand` `npm run build`, `outputDirectory` `./dist`. `site` itself stays optional — a backend-only project omits it and still has no site — and `serveCommand` is not defaulted, so `base44 dev` still runs the backend alone for a project that names no dev server. Projects that declared a partial block change behaviour: `base44 build` and `deploy --build` now build them instead of refusing, and `base44 deploy` deploys a site. The "add `site.`" errors now fire only when there is no `site` block at all. +- The `site` block's two commands now default to the conventions `base44 create` scaffolds, so a block only has to name what a project does differently: `installCommand` `npm install` and `buildCommand` `npm run build`. `site` itself stays optional, and `serveCommand` and `outputDirectory` are deliberately not defaulted — their absence says "no frontend to run here" and "nothing built to upload", so `base44 dev` still runs the backend alone and `base44 deploy` still skips the site step. For a project that declared a partial block, `base44 build` and `deploy --build` now build it instead of refusing, and the deploy flow may offer to build first. - `base44 sandbox` help now explains that writes are committed but not checkpointed: a Restore or Revert in the builder rolls the app back to the last checkpoint and discards everything after it, so run `base44 sandbox checkpoint` after each unit of work and before stopping. The note appears on `sandbox`, `sandbox write`, `sandbox edit`, `sandbox run`, and `sandbox checkpoint`. - The `backend-and-client` template now scaffolds the same client convention editor-created apps use: `@base44/vite-plugin` + `src/lib/app-params.js`, with the SDK client on same-origin `/api` (`serverUrl: ''`). Under `base44 dev` the plugin proxies `/api` to the local dev backend, so scaffolded apps get local entities and functions; the app id is injected via `VITE_BASE44_APP_ID` by `base44 dev`, `base44 dev --remote`, and the build/deploy commands instead of being baked into source. diff --git a/docs/deployments.md b/docs/deployments.md index f01ab6f1..026742c4 100644 --- a/docs/deployments.md +++ b/docs/deployments.md @@ -55,7 +55,7 @@ Entry = `main` from the wrangler config. With `no_bundle: true`, every file unde ## Command UX -**`base44 site deploy [--git-hash ] [--concurrency ] [--build|--no-build]`** — the optional build step is `maybeBuildBeforeDeploy` (`--build` forces it, `--no-build` skips it, otherwise an interactive ask whenever `site.buildCommand` exists). It runs **before** the deploy confirmation, so the prompt is the last gate before anything leaves the machine. Progress: "Found N static assets (M new)" → "Uploaded X of Y assets" → "Deploying worker (K modules)…" (only when there is one) → outro `Deployment (commit )`. Under `--json`, stdout is a single `{deploymentId, gitHash}` document. +**`base44 site deploy [--git-hash ] [--concurrency ] [--build|--no-build]`** — the optional build step is `maybeBuildBeforeDeploy` (`--build` forces it, `--no-build` skips it, otherwise an interactive ask whenever the project has a `site` block, since `buildCommand` defaults inside one). It runs **before** the deploy confirmation, so the prompt is the last gate before anything leaves the machine. Progress: "Found N static assets (M new)" → "Uploaded X of Y assets" → "Deploying worker (K modules)…" (only when there is one) → outro `Deployment (commit )`. Under `--json`, stdout is a single `{deploymentId, gitHash}` document. **"Site" is the only word for it in user-facing copy.** A site is whatever we deploy, worker or no worker, so the prompt, spinner, success line and errors say "site" and never distinguish the two — the distinction is ours, not the user's, and a deploy that reports itself differently depending on the build reads as two products. Internally the code says "worker" for the thing that may or may not be there. diff --git a/docs/resources.md b/docs/resources.md index b413c4f4..91a83a01 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -122,7 +122,7 @@ const viaDeployments = deploymentsApiEnabled(); ``` - Gate on → the deployments API, see [deployments.md](deployments.md). Whether the build carries a worker changes what that flow sends, never which flow runs, and a worker brings its own assets directory — so the command may pass a null `outputDir`. -- Gate off → the legacy tar.gz path: tar.gz `site.outputDirectory` and upload via `POST /api/apps/{app_id}/deploy-dist`. This is the flow that requires the config field — it defaults to `./dist` inside a `site` block, so "No site configuration found." is raised only for a project with no block at all. +- Gate off → the legacy tar.gz path: tar.gz `site.outputDirectory` and upload via `POST /api/apps/{app_id}/deploy-dist`. This is the flow that requires the config field, and the one that raises "No site configuration found." Each flow validates its own inputs, so the decision itself is a boolean and needs nothing from the tree. diff --git a/packages/cli/src/cli/commands/project/eject.ts b/packages/cli/src/cli/commands/project/eject.ts index ca87fd43..5a3d9d2b 100644 --- a/packages/cli/src/cli/commands/project/eject.ts +++ b/packages/cli/src/cli/commands/project/eject.ts @@ -155,11 +155,12 @@ async function eject( ); const { project } = await readProjectConfig(resolvedPath); - const installCommand = project.site?.installCommand; - const buildCommand = project.site?.buildCommand; + const site = project.site; - // Only offer deploy if the project has build commands configured - if (installCommand && buildCommand) { + // The commands default inside a `site` block, so `outputDirectory` is what says + // there is built output to upload. Without it `--yes` would install and build + // for a project with nothing to deploy. + if (site?.outputDirectory) { const shouldDeploy = options.yes ? true : await confirm({ @@ -170,10 +171,16 @@ async function eject( await runTask( "Installing dependencies...", async (updateMessage) => { - await execa({ cwd: resolvedPath, shell: true })`${installCommand}`; + await execa({ + cwd: resolvedPath, + shell: true, + })`${site.installCommand}`; updateMessage("Building project..."); - await execa({ cwd: resolvedPath, shell: true })`${buildCommand}`; + await execa({ + cwd: resolvedPath, + shell: true, + })`${site.buildCommand}`; }, { successMessage: theme.colors.base44Orange( diff --git a/packages/cli/src/cli/commands/site/deploy.ts b/packages/cli/src/cli/commands/site/deploy.ts index eaf3af9f..c5d2c267 100644 --- a/packages/cli/src/cli/commands/site/deploy.ts +++ b/packages/cli/src/cli/commands/site/deploy.ts @@ -126,7 +126,7 @@ async function deployTarball( hints: [ { message: - 'Add a \'site\' block to your config.jsonc (e.g., "site": { "outputDirectory": "dist" }). Inside one, outputDirectory defaults to "./dist".', + 'Add \'site.outputDirectory\' to your config.jsonc (e.g., "site": { "outputDirectory": "dist" })', }, ], }); diff --git a/packages/cli/src/core/project/deploy.ts b/packages/cli/src/core/project/deploy.ts index ddfc03f6..4a671197 100644 --- a/packages/cli/src/core/project/deploy.ts +++ b/packages/cli/src/core/project/deploy.ts @@ -40,9 +40,7 @@ export function hasResourcesToDeploy(projectData: ProjectData): boolean { connectors, authConfig, } = projectData; - // The block, not the field: `outputDirectory` always resolves now (it defaults), - // so a project declares it has a site by having the block at all. - const hasSite = project.site !== undefined; + const hasSite = Boolean(project.site?.outputDirectory); const hasEntities = entities.length > 0; const hasFunctions = functions.length > 0; const hasActors = actors.length > 0; diff --git a/packages/cli/src/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index 849fa89a..76080d5d 100644 --- a/packages/cli/src/core/project/schema.ts +++ b/packages/cli/src/core/project/schema.ts @@ -13,20 +13,20 @@ export const TemplatesConfigSchema = z.object({ export type Template = z.infer; // Defaults are the conventions `base44 create` scaffolds, so a `site` block only -// has to name what this project does differently. `site` itself stays optional: -// a backend-only project omits it and has no site at all, which is what every -// "is there a site?" check keys on. +// has to name what this project does differently. // -// `serveCommand` is deliberately NOT defaulted. `base44 dev` reads its absence as -// "this project has no frontend to run here" and runs the backend alone; with a -// default it would spawn one for every site block, and a dev server that fails -// immediately takes the backend down with it. Commands that exist only to serve a -// frontend default it themselves, where the intent is unambiguous. +// Only the two commands default. `serveCommand` and `outputDirectory` carry a +// meaning in their absence that a default would erase: no frontend to run here, +// and nothing built to upload. `base44 dev` runs the backend alone without the +// first, and `base44 deploy` skips the site step without the second — a default +// would spawn a dev server for every site block and upload whatever happened to +// sit in ./dist. Commands that exist only to serve or build a site supply their +// own fallback, where the intent is unambiguous. const SiteConfigSchema = z.object({ - buildCommand: z.string().optional().default("npm run build"), + buildCommand: z.string().default("npm run build"), serveCommand: z.string().optional(), - outputDirectory: z.string().optional().default("./dist"), - installCommand: z.string().optional().default("npm install"), + outputDirectory: z.string().optional(), + installCommand: z.string().default("npm install"), }); const PluginMetadataSchema = z.object({ diff --git a/packages/cli/tests/cli/deploy.spec.ts b/packages/cli/tests/cli/deploy.spec.ts index 1c4208bb..ab3b3313 100644 --- a/packages/cli/tests/cli/deploy.spec.ts +++ b/packages/cli/tests/cli/deploy.spec.ts @@ -70,6 +70,18 @@ describe("deploy command (unified)", () => { t.expectResult(help).toNotContain("--concurrency"); }); + it("does not deploy a site for a block that never named an output directory", async () => { + // A defaulted command must never turn into an upload: `./dist` here would + // publish whatever happened to be built, or fail a deploy that used to pass. + await t.givenLoggedInWithProject(fixture("with-serve-command")); + + const result = await t.run("deploy", "-y"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("No resources found to deploy"); + t.expectResult(result).toNotContain("Site from"); + }); + it("reports no resources when project is empty", async () => { await t.givenLoggedInWithProject(fixture("basic")); diff --git a/packages/cli/tests/core/project.spec.ts b/packages/cli/tests/core/project.spec.ts index 8bfeb474..9df7ed73 100644 --- a/packages/cli/tests/core/project.spec.ts +++ b/packages/cli/tests/core/project.spec.ts @@ -40,13 +40,14 @@ describe("readProjectConfig", () => { ); expect(result.project.site).toEqual({ - outputDirectory: "./dist", buildCommand: "npm run build", installCommand: "npm install", }); - // Not defaulted: `base44 dev` runs the backend alone without one. + // Neither defaults: their absence says "no frontend to run here" and + // "nothing built to upload", which a default would erase. expect(result.project.site?.serveCommand).toBeUndefined(); + expect(result.project.site?.outputDirectory).toBeUndefined(); }); it("leaves a project with no site block without a site", async () => { From de28d3f16c7b75df88bbffb0af8024e46aa606ba Mon Sep 17 00:00:00 2001 From: ronnyr Date: Tue, 22 Sep 2026 13:49:48 +0300 Subject: [PATCH 3/3] fix(project): gate eject's build on the site block, not outputDirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both commands default inside a `site` block, so install and build always work there now — the earlier `outputDirectory` check was guarding a config neither writer produces. `ensure_cli_configs` and `eject_service` both emit the full block including `outputDirectory: "./dist"`, and the eject ZIP comes from the latter, so the check could never be the condition that differed. Having a site block is the whole question: a backend-only project has nothing to build. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL --- packages/cli/src/cli/commands/project/eject.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/cli/commands/project/eject.ts b/packages/cli/src/cli/commands/project/eject.ts index 5a3d9d2b..873de128 100644 --- a/packages/cli/src/cli/commands/project/eject.ts +++ b/packages/cli/src/cli/commands/project/eject.ts @@ -157,10 +157,9 @@ async function eject( const { project } = await readProjectConfig(resolvedPath); const site = project.site; - // The commands default inside a `site` block, so `outputDirectory` is what says - // there is built output to upload. Without it `--yes` would install and build - // for a project with nothing to deploy. - if (site?.outputDirectory) { + // Both commands default inside a `site` block, so having one is the whole + // condition: a backend-only project has nothing to build. + if (site) { const shouldDeploy = options.yes ? true : await confirm({