Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- App visibility: `base44 visibility <public|private|workspace>` 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

Expand Down
32 changes: 32 additions & 0 deletions packages/cli/src/cli/commands/project/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Command } from "commander";
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 } from "@/core/errors.js";
import { readProjectConfig } from "@/core/project/index.js";

async function buildAction(ctx: CLIContext): Promise<RunCommandResult> {
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);
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)}`,
};
}

export function getBuildCommand(): Command {
return new Base44Command("build")
.description("Build the site with the Base44 app id injected")
.action(buildAction);
}
39 changes: 39 additions & 0 deletions packages/cli/src/cli/commands/project/site-build.ts
Original file line number Diff line number Diff line change
@@ -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<CLIContext, "runTask">,
{ root, buildCommand, appId }: SiteBuildTarget,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case of editor app => github clone => base44 build, what is the source of the app id? the user should use the --app-id flag?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The id comes from base44 link, not a flag. An editor-app clone ships base44/config.jsonc but deliberately no .app.jsonc (the platform git-ignores it), so the first base44 build fails pointing at base44 link; link lists editor-created apps since #577 and writes .app.jsonc, and build resolves the id from there like every project command. --app-id/BASE44_APP_ID intentionally don't work here: they carry an id without a project root, and build requires a linked project — injecting app X's id into app Y's checkout is exactly the mistake that guard blocks. If we ever want --app-id to override in unlinked checkouts, that's a product decision to take deliberately, not a gap.

): Promise<void> {
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",
},
);
}
2 changes: 2 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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());
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/tests/cli/build.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Buildable Site Project",
"site": {
"buildCommand": "node -e \"require('fs').writeFileSync('build-env.txt', 'BUILD_APP=' + process.env.VITE_BASE44_APP_ID)\""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Failing Build Project",
"site": {
"buildCommand": "node -e \"process.exit(1)\""
}
}
Loading