diff --git a/CHANGELOG.md b/CHANGELOG.md index 314365c3..a5510080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. 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/packages/cli/src/cli/commands/project/eject.ts b/packages/cli/src/cli/commands/project/eject.ts index ca87fd43..873de128 100644 --- a/packages/cli/src/cli/commands/project/eject.ts +++ b/packages/cli/src/cli/commands/project/eject.ts @@ -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({ @@ -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( 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/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index 42041acf..76080d5d 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. +// +// 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({ 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/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 9e9e0def..9df7ed73 100644 --- a/packages/cli/tests/core/project.spec.ts +++ b/packages/cli/tests/core/project.spec.ts @@ -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"), 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)\"" + } +}