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 @@ -14,6 +14,7 @@

### Changed

- 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.

Expand Down
2 changes: 1 addition & 1 deletion docs/deployments.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Entry = `main` from the wrangler config. With `no_bundle: true`, every file unde

## Command UX

**`base44 site deploy [--git-hash <hash>] [--concurrency <n>] [--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 <id> (commit <hash>)`. Under `--json`, stdout is a single `{deploymentId, gitHash}` document.
**`base44 site deploy [--git-hash <hash>] [--concurrency <n>] [--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 <id> (commit <hash>)`. 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.

Expand Down
18 changes: 12 additions & 6 deletions packages/cli/src/cli/commands/project/eject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ 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) {
// 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({
Expand All @@ -170,10 +170,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(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/commands/project/site-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".',
},
],
});
Expand Down
14 changes: 12 additions & 2 deletions packages/cli/src/core/project/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ export const TemplatesConfigSchema = z.object({
});

export type Template = z.infer<typeof TemplateSchema>;
// Defaults are the conventions `base44 create` scaffolds, so a `site` block only
// has to name what this project does differently.
//
// 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(),
buildCommand: z.string().default("npm run build"),
serveCommand: z.string().optional(),
outputDirectory: z.string().optional(),
installCommand: z.string().optional(),
installCommand: z.string().default("npm install"),
});

const PluginMetadataSchema = z.object({
Expand Down
19 changes: 6 additions & 13 deletions packages/cli/tests/cli/build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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"));

Expand Down
12 changes: 12 additions & 0 deletions packages/cli/tests/cli/deploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
34 changes: 34 additions & 0 deletions packages/cli/tests/core/project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@ 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({
buildCommand: "npm run build",
installCommand: "npm install",
});

// 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 () => {
// 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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Site Defaults Project",
// A site block that names nothing: every command comes from the schema defaults.
"site": {}
}
7 changes: 7 additions & 0 deletions packages/cli/tests/fixtures/with-site-defaults/package.json
Original file line number Diff line number Diff line change
@@ -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)\""
}
}
Loading