From 5332942f7db6e4eb20248d0f1b4fe5d19d081c65 Mon Sep 17 00:00:00 2001 From: David Susskind Date: Mon, 3 Aug 2026 09:37:54 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feat(build):=20add=20base44=20build=20?= =?UTF-8?q?=E2=80=94=20run=20the=20site=20buildCommand=20with=20the=20app?= =?UTF-8?q?=20id=20injected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deployed bundles built with a bare `npm run build` carry no app id (the template no longer bakes it into source), so their API calls fail once deployed. `base44 build` runs the configured site.buildCommand from the project root with VITE_BASE44_APP_ID set to the linked app's id. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 1 + .../cli/src/cli/commands/project/build.ts | 52 +++++++++++++++++++ packages/cli/src/cli/program.ts | 2 + packages/cli/tests/cli/build.spec.ts | 43 +++++++++++++++ .../with-buildable-site/base44/config.jsonc | 7 +++ .../site-output/index.html | 1 + .../with-failing-build/base44/config.jsonc | 7 +++ .../with-failing-build/site-output/index.html | 1 + 8 files changed, 114 insertions(+) create mode 100644 packages/cli/src/cli/commands/project/build.ts create mode 100644 packages/cli/tests/cli/build.spec.ts create mode 100644 packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc create mode 100644 packages/cli/tests/fixtures/with-buildable-site/site-output/index.html create mode 100644 packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc create mode 100644 packages/cli/tests/fixtures/with-failing-build/site-output/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f871aff..e0eaada1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - App visibility: `base44 visibility ` sets it on the server directly (accepts `--app-id` to target any app). Also configurable via `"visibility"` in `config.jsonc`, which `base44 deploy` applies. New projects scaffold `"visibility": "public"`. +- `base44 build` runs the site's `buildCommand` with `VITE_BASE44_APP_ID` injected, so built bundles always carry the linked app's id. ### Fixed diff --git a/packages/cli/src/cli/commands/project/build.ts b/packages/cli/src/cli/commands/project/build.ts new file mode 100644 index 00000000..87ff65a2 --- /dev/null +++ b/packages/cli/src/cli/commands/project/build.ts @@ -0,0 +1,52 @@ +import type { Command } from "commander"; +import { execa } from "execa"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { Base44Command, theme } from "@/cli/utils/index.js"; +import { ConfigInvalidError, ConfigNotFoundError } from "@/core/errors.js"; +import { readProjectConfig } from "@/core/project/index.js"; + +async function buildAction(ctx: CLIContext): Promise { + const { app } = ctx; + if (!app?.projectRoot) { + throw new ConfigInvalidError( + "base44 build requires a linked local project. Run it from a project with base44/.app.jsonc.", + ); + } + + const { project } = await readProjectConfig(app.projectRoot); + const buildCommand = project.site?.buildCommand; + if (!buildCommand) { + throw new ConfigNotFoundError("No site build command found.", { + hints: [ + { + message: + 'Add \'site.buildCommand\' to your config.jsonc (e.g., "site": { "buildCommand": "npm run build" })', + }, + ], + }); + } + + await ctx.runTask( + "Building site...", + () => + execa({ + cwd: project.root, + shell: true, + env: { VITE_BASE44_APP_ID: app.id }, + })`${buildCommand}`, + { + successMessage: "Site built successfully", + errorMessage: "Build failed", + }, + ); + + return { + outroMessage: `Site built with app id ${theme.styles.bold(app.id)}`, + }; +} + +export function getBuildCommand(): Command { + return new Base44Command("build") + .description("Build the site with the Base44 app id injected") + .action(buildAction); +} diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index b3a7a87f..04e1c33e 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -9,6 +9,7 @@ import { getConnectorsCommand } from "@/cli/commands/connectors/index.js"; import { getDashboardCommand } from "@/cli/commands/dashboard/index.js"; import { getEntitiesPushCommand } from "@/cli/commands/entities/push.js"; import { getFunctionsCommand } from "@/cli/commands/functions/index.js"; +import { getBuildCommand } from "@/cli/commands/project/build.js"; import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; @@ -69,6 +70,7 @@ export function createProgram(context: CLIContext): Command { program.addCommand(getCreateCommand()); program.addCommand(getScaffoldCommand()); program.addCommand(getDashboardCommand()); + program.addCommand(getBuildCommand()); program.addCommand(getDeployCommand()); program.addCommand(getVisibilityCommand()); program.addCommand(getLinkCommand()); diff --git a/packages/cli/tests/cli/build.spec.ts b/packages/cli/tests/cli/build.spec.ts new file mode 100644 index 00000000..d8d390db --- /dev/null +++ b/packages/cli/tests/cli/build.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("build command", () => { + const t = setupCLITests(); + + it("runs the site buildCommand with the app id injected", async () => { + await t.givenLoggedInWithProject(fixture("with-buildable-site")); + + const result = await t.run("build"); + + t.expectResult(result).toSucceed(); + expect(await t.readProjectFile("build-env.txt")).toBe( + `BUILD_APP=${t.api.appId}`, + ); + }); + + it("fails when the project has no site.buildCommand", async () => { + await t.givenLoggedInWithProject(fixture("with-site")); + + const result = await t.run("build"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("No site build command found"); + }); + + it("fails when the buildCommand fails", async () => { + await t.givenLoggedInWithProject(fixture("with-failing-build")); + + const result = await t.run("build"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("Build failed"); + }); + + it("fails when not in a project directory", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + + const result = await t.run("build"); + + t.expectResult(result).toFail(); + }); +}); diff --git a/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc b/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc new file mode 100644 index 00000000..077b2755 --- /dev/null +++ b/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc @@ -0,0 +1,7 @@ +{ + "name": "Buildable Site Project", + "site": { + "buildCommand": "node -e \"require('fs').writeFileSync('build-env.txt', 'BUILD_APP=' + process.env.VITE_BASE44_APP_ID)\"", + "outputDirectory": "site-output" + } +} diff --git a/packages/cli/tests/fixtures/with-buildable-site/site-output/index.html b/packages/cli/tests/fixtures/with-buildable-site/site-output/index.html new file mode 100644 index 00000000..cea6f157 --- /dev/null +++ b/packages/cli/tests/fixtures/with-buildable-site/site-output/index.html @@ -0,0 +1 @@ +buildable site fixture diff --git a/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc b/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc new file mode 100644 index 00000000..58a5347b --- /dev/null +++ b/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc @@ -0,0 +1,7 @@ +{ + "name": "Failing Build Project", + "site": { + "buildCommand": "node -e \"process.exit(1)\"", + "outputDirectory": "site-output" + } +} diff --git a/packages/cli/tests/fixtures/with-failing-build/site-output/index.html b/packages/cli/tests/fixtures/with-failing-build/site-output/index.html new file mode 100644 index 00000000..18ecdcb7 --- /dev/null +++ b/packages/cli/tests/fixtures/with-failing-build/site-output/index.html @@ -0,0 +1 @@ + From 40070bbc58745fb4f69fc19b8395673ba5fb6a93 Mon Sep 17 00:00:00 2001 From: David Susskind Date: Mon, 3 Aug 2026 09:53:03 +0300 Subject: [PATCH 2/3] test: trim the build fixtures to what the build tests use Co-Authored-By: Claude Fable 5 --- .../cli/tests/fixtures/with-buildable-site/base44/config.jsonc | 3 +-- .../tests/fixtures/with-buildable-site/site-output/index.html | 1 - .../cli/tests/fixtures/with-failing-build/base44/config.jsonc | 3 +-- .../tests/fixtures/with-failing-build/site-output/index.html | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 packages/cli/tests/fixtures/with-buildable-site/site-output/index.html delete mode 100644 packages/cli/tests/fixtures/with-failing-build/site-output/index.html diff --git a/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc b/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc index 077b2755..05abc950 100644 --- a/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc +++ b/packages/cli/tests/fixtures/with-buildable-site/base44/config.jsonc @@ -1,7 +1,6 @@ { "name": "Buildable Site Project", "site": { - "buildCommand": "node -e \"require('fs').writeFileSync('build-env.txt', 'BUILD_APP=' + process.env.VITE_BASE44_APP_ID)\"", - "outputDirectory": "site-output" + "buildCommand": "node -e \"require('fs').writeFileSync('build-env.txt', 'BUILD_APP=' + process.env.VITE_BASE44_APP_ID)\"" } } diff --git a/packages/cli/tests/fixtures/with-buildable-site/site-output/index.html b/packages/cli/tests/fixtures/with-buildable-site/site-output/index.html deleted file mode 100644 index cea6f157..00000000 --- a/packages/cli/tests/fixtures/with-buildable-site/site-output/index.html +++ /dev/null @@ -1 +0,0 @@ -buildable site fixture diff --git a/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc b/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc index 58a5347b..fb3844ea 100644 --- a/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc +++ b/packages/cli/tests/fixtures/with-failing-build/base44/config.jsonc @@ -1,7 +1,6 @@ { "name": "Failing Build Project", "site": { - "buildCommand": "node -e \"process.exit(1)\"", - "outputDirectory": "site-output" + "buildCommand": "node -e \"process.exit(1)\"" } } diff --git a/packages/cli/tests/fixtures/with-failing-build/site-output/index.html b/packages/cli/tests/fixtures/with-failing-build/site-output/index.html deleted file mode 100644 index 18ecdcb7..00000000 --- a/packages/cli/tests/fixtures/with-failing-build/site-output/index.html +++ /dev/null @@ -1 +0,0 @@ - From a4c4c7bfea4c2cc283559be5aa2324c5fdbd0b8b Mon Sep 17 00:00:00 2001 From: David Susskind Date: Mon, 3 Aug 2026 10:42:08 +0300 Subject: [PATCH 3/3] refactor(build): house the build runner in site-build.ts runSiteBuild is about to gain a second consumer (the deploy build step); giving it its own module now keeps that PR from touching this command. Co-Authored-By: Claude Fable 5 --- .../cli/src/cli/commands/project/build.ts | 34 ++++------------ .../src/cli/commands/project/site-build.ts | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 packages/cli/src/cli/commands/project/site-build.ts diff --git a/packages/cli/src/cli/commands/project/build.ts b/packages/cli/src/cli/commands/project/build.ts index 87ff65a2..747329ce 100644 --- a/packages/cli/src/cli/commands/project/build.ts +++ b/packages/cli/src/cli/commands/project/build.ts @@ -1,8 +1,8 @@ import type { Command } from "commander"; -import { execa } from "execa"; +import { runSiteBuild } from "@/cli/commands/project/site-build.js"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { Base44Command, theme } from "@/cli/utils/index.js"; -import { ConfigInvalidError, ConfigNotFoundError } from "@/core/errors.js"; +import { ConfigInvalidError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/project/index.js"; async function buildAction(ctx: CLIContext): Promise { @@ -14,31 +14,11 @@ async function buildAction(ctx: CLIContext): Promise { } const { project } = await readProjectConfig(app.projectRoot); - const buildCommand = project.site?.buildCommand; - if (!buildCommand) { - throw new ConfigNotFoundError("No site build command found.", { - hints: [ - { - message: - 'Add \'site.buildCommand\' to your config.jsonc (e.g., "site": { "buildCommand": "npm run build" })', - }, - ], - }); - } - - await ctx.runTask( - "Building site...", - () => - execa({ - cwd: project.root, - shell: true, - env: { VITE_BASE44_APP_ID: app.id }, - })`${buildCommand}`, - { - successMessage: "Site built successfully", - errorMessage: "Build failed", - }, - ); + await runSiteBuild(ctx, { + root: project.root, + buildCommand: project.site?.buildCommand, + appId: app.id, + }); return { outroMessage: `Site built with app id ${theme.styles.bold(app.id)}`, diff --git a/packages/cli/src/cli/commands/project/site-build.ts b/packages/cli/src/cli/commands/project/site-build.ts new file mode 100644 index 00000000..9a51a6cc --- /dev/null +++ b/packages/cli/src/cli/commands/project/site-build.ts @@ -0,0 +1,39 @@ +import { execa } from "execa"; +import type { CLIContext } from "@/cli/types.js"; +import { ConfigNotFoundError } from "@/core/errors.js"; + +interface SiteBuildTarget { + root: string; + buildCommand?: string; + appId: string; +} + +export async function runSiteBuild( + { runTask }: Pick, + { root, buildCommand, appId }: SiteBuildTarget, +): Promise { + if (!buildCommand) { + throw new ConfigNotFoundError("No site build command found.", { + hints: [ + { + message: + 'Add \'site.buildCommand\' to your config.jsonc (e.g., "site": { "buildCommand": "npm run build" })', + }, + ], + }); + } + + await runTask( + "Building site...", + () => + execa({ + cwd: root, + shell: true, + env: { VITE_BASE44_APP_ID: appId }, + })`${buildCommand}`, + { + successMessage: "Site built successfully", + errorMessage: "Build failed", + }, + ); +}