-
Notifications
You must be signed in to change notification settings - Fork 34
feat(cli): install and serve a site through the CLI #635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
93d2ceb
feat(project): default the site block's build commands
ronnyrin de2f19d
feat(cli): install and serve a site through the CLI
ronnyrin 8d9279d
fix(project): do not default outputDirectory, its absence is the signal
ronnyrin a58a92a
Merge remote-tracking branch 'origin/feat/site-config-defaults' into …
ronnyrin 9c57bb8
fix(project): do not default outputDirectory, its absence is the signal
ronnyrin 2642749
fix(cli): refuse an address site dev cannot deliver, and validate --port
ronnyrin 1327012
Merge remote-tracking branch 'origin/feat/site-config-defaults' into …
ronnyrin de28d3f
fix(project): gate eject's build on the site block, not outputDirectory
ronnyrin 9337161
Merge remote-tracking branch 'origin/feat/site-config-defaults' into …
ronnyrin 14994d1
Merge remote-tracking branch 'origin/main' into feat/site-install-and…
ronnyrin db3e4a7
feat(site dev): default the bind address so a caller can pass nothing
ronnyrin ba6df59
refactor(schema): default the dev address in the config, not the command
ronnyrin 88da65a
refactor(site dev): drop devHost and devPort, they were never project…
ronnyrin 0d94269
refactor(site dev): the config is the only channel for the bind address
ronnyrin 60b5706
style(tests): wrap a long assertion biome would reformat
ronnyrin fab0fe0
refactor(site dev): drop --backend-url, it had no caller
ronnyrin aea744f
refactor(site dev): run serveCommand as written, drop the address fields
ronnyrin f4434ef
refactor(cli): move `install` under `base44 site`
ronnyrin 73e7fd3
Merge origin/main into feat/site-install-and-serve
ronnyrin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import type { Command } from "commander"; | ||
| import { createServeCommandRunner } from "@/cli/dev/serve-command-runner.js"; | ||
| import { stopRunnerOnProcessSignals } from "@/cli/dev/stop-runner-on-signals.js"; | ||
| import type { CLIContext, RunCommandResult } from "@/cli/types.js"; | ||
| import { Base44Command } from "@/cli/utils/index.js"; | ||
| import { ConfigInvalidError, InvalidInputError } from "@/core/errors.js"; | ||
| import { readProjectConfig } from "@/core/project/index.js"; | ||
|
|
||
| /** | ||
| * What to run when a project has a site but names no dev server. Not a schema | ||
| * default: `base44 dev` reads an absent `serveCommand` as "no frontend to run | ||
| * here", so only a command that exists purely to serve one may assume this. | ||
| */ | ||
| const DEFAULT_SERVE_COMMAND = "npm run dev"; | ||
|
|
||
| async function siteDevAction(ctx: CLIContext): Promise<RunCommandResult> { | ||
| const { app } = ctx; | ||
| // Same shape as `base44 build`: the framework's own app-context step has | ||
| // already refused with actionable hints, so this is the type's guard. | ||
| if (!app?.projectRoot) { | ||
| throw new ConfigInvalidError( | ||
| "base44 site dev requires a linked local project. Run it from a project with base44/.app.jsonc.", | ||
| ); | ||
| } | ||
|
|
||
| const { project } = await readProjectConfig(app.projectRoot); | ||
| const site = project.site; | ||
| if (!site) { | ||
| throw new InvalidInputError( | ||
| "This project has no 'site' block in base44/config.jsonc, so there is no frontend to serve. Add one naming its serveCommand; site dev falls back to \"npm run dev\".", | ||
| ); | ||
| } | ||
|
|
||
| // Run as written: where the dev server binds is the command's own business. | ||
| // In a sandbox @base44/vite-plugin binds 0.0.0.0:5173 for Base44 apps; any | ||
| // other serveCommand must bind the address the sandbox exposes itself. | ||
| const command = site.serveCommand ?? DEFAULT_SERVE_COMMAND; | ||
|
|
||
| const runner = createServeCommandRunner({ | ||
| serveCommand: command, | ||
| projectRoot: project.root, | ||
| appId: app.id, | ||
| }); | ||
| stopRunnerOnProcessSignals(runner); | ||
| runner.onExit((code) => process.exit(code ?? 1)); | ||
| runner.start(); | ||
|
|
||
| return { outroMessage: `Frontend dev server running '${command}'` }; | ||
| } | ||
|
|
||
| export function getSiteDevCommand(): Command { | ||
| // The frontend alone, reaching its backend same-origin — what a hosted sandbox | ||
| // needs. `base44 dev` is the developer-machine command: it also runs the | ||
| // backend, locally or (with --remote) the app's published one. | ||
| return new Base44Command("dev", { requireAuth: false }) | ||
| .description("Run the site's dev server, with no local backend") | ||
| .action(siteDevAction); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,14 @@ | ||
| import { Command } from "commander"; | ||
| import { getSiteDeployCommand } from "./deploy.js"; | ||
| import { getSiteDevCommand } from "./dev.js"; | ||
| import { getSiteInstallCommand } from "./install.js"; | ||
| import { getSiteOpenCommand } from "./open.js"; | ||
|
|
||
| export function getSiteCommand(): Command { | ||
| return new Command("site") | ||
| .description("Manage app site (frontend app)") | ||
| .addCommand(getSiteDeployCommand()) | ||
| .addCommand(getSiteDevCommand()) | ||
| .addCommand(getSiteInstallCommand()) | ||
| .addCommand(getSiteOpenCommand()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| 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 { ConfigNotFoundError } from "@/core/errors.js"; | ||
| import { readProjectConfig } from "@/core/project/index.js"; | ||
|
|
||
| async function installAction({ | ||
| runTask, | ||
| }: CLIContext): Promise<RunCommandResult> { | ||
| const { project } = await readProjectConfig(); | ||
| const installCommand = project.site?.installCommand; | ||
| if (!installCommand) { | ||
| throw new ConfigNotFoundError("No site install command found.", { | ||
| hints: [ | ||
| { | ||
| message: | ||
| 'Add a \'site\' block to your config.jsonc (e.g., "site": { "installCommand": "npm ci" }). Inside one, installCommand defaults to "npm install".', | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
|
|
||
| await runTask( | ||
| "Installing site dependencies...", | ||
| () => execa({ cwd: project.root, shell: true })`${installCommand}`, | ||
| { | ||
| successMessage: "Dependencies installed", | ||
| errorMessage: "Install failed", | ||
| }, | ||
| ); | ||
|
|
||
| return { | ||
| outroMessage: `Installed with ${theme.styles.bold(installCommand)}`, | ||
| }; | ||
| } | ||
|
|
||
| export function getSiteInstallCommand(): Command { | ||
| // Local only: no app to resolve and no API to call, so a machine that has | ||
| // never logged in (a build sandbox) can still install a project. | ||
| return new Base44Command("install", { | ||
| requireAuth: false, | ||
| requireAppContext: false, | ||
| }) | ||
| .description("Install the site's dependencies with its configured command") | ||
| .action(installAction); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import process from "node:process"; | ||
| import type { ServeRunner } from "@/cli/dev/dev-server/serve-runner.js"; | ||
|
|
||
| /** Tear the dev server down on Ctrl-C and on a terminating signal. */ | ||
| export function stopRunnerOnProcessSignals(runner: ServeRunner): void { | ||
| const stop = () => void runner.stop(); | ||
| process.on("SIGINT", stop); | ||
| process.on("SIGTERM", stop); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { fixture, setupCLITests } from "./testkit/index.js"; | ||
|
|
||
| describe("site dev command", () => { | ||
| const t = setupCLITests(); | ||
|
|
||
| it("runs the serveCommand as written, appending nothing", async () => { | ||
| // Where the dev server binds is the command's own business: in a sandbox the | ||
| // vite plugin binds it, so nothing is added to the line. | ||
| await t.givenLoggedInWithProject(fixture("with-npm-serve-command")); | ||
|
|
||
| const handle = await t.runLive("site", "dev"); | ||
| await handle.waitForOutput(/ARGS=/); | ||
| await handle.stop(); | ||
|
|
||
| expect(handle.stdout.join("")).toContain("ARGS= APP="); | ||
| }); | ||
|
|
||
| it("runs a serveCommand that takes no forwarded arguments", async () => { | ||
| // A bare binary used to be refused because the address could not be | ||
| // appended to it. With nothing appended there is nothing to refuse. | ||
| await t.givenLoggedInWithProject(fixture("with-serve-command")); | ||
|
|
||
| const handle = await t.runLive("site", "dev"); | ||
| // Wait on the child's own line: the startup message echoes the command, | ||
| // which also contains "SERVE_APP=". | ||
| await handle.waitForOutput(new RegExp(`SERVE_APP=${t.api.appId}`)); | ||
| await handle.stop(); | ||
|
|
||
| expect(handle.stdout.join("")).toContain(`SERVE_APP=${t.api.appId}`); | ||
| }); | ||
|
|
||
| it("takes no arguments", async () => { | ||
| await t.givenLoggedInWithProject(fixture("with-npm-serve-command")); | ||
|
|
||
| const result = await t.run("site", "dev", "--port", "5999"); | ||
|
|
||
| t.expectResult(result).toFail(); | ||
| }); | ||
|
|
||
| it("serves without a login", async () => { | ||
| // The whole point of the command: a build sandbox that has never logged in. | ||
| await t.givenProject(fixture("with-npm-serve-command")); | ||
|
|
||
| const handle = await t.runLive("site", "dev"); | ||
| await handle.waitForOutput(/ARGS=/); | ||
| await handle.stop(); | ||
|
|
||
| expect(handle.stdout.join("")).toContain("ARGS="); | ||
| }); | ||
|
|
||
| it("serves the frontend same-origin, with no backend url injected", async () => { | ||
| // A sandbox frontend reaches its backend through the vite plugin's /api | ||
| // proxy, so it must not be pointed anywhere else. | ||
| await t.givenLoggedInWithProject(fixture("with-npm-serve-command")); | ||
|
|
||
| const handle = await t.runLive("site", "dev"); | ||
| await handle.waitForOutput(/ARGS=/); | ||
| await handle.stop(); | ||
|
|
||
| const output = handle.stdout.join(""); | ||
| expect(output).toContain(`APP=${t.api.appId}`); | ||
| expect(output).toContain("URL=undefined"); | ||
| }); | ||
|
|
||
| it("fails when the project has no site block", async () => { | ||
| await t.givenLoggedInWithProject(fixture("basic")); | ||
|
|
||
| const result = await t.run("site", "dev"); | ||
|
|
||
| t.expectResult(result).toFail(); | ||
| t.expectResult(result).toContain("no 'site' block"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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("site install command", () => { | ||
| const t = setupCLITests(); | ||
|
|
||
| it("runs the site's configured installCommand", async () => { | ||
| await t.givenLoggedInWithProject(fixture("with-installable-site")); | ||
|
|
||
| const result = await t.run("site", "install"); | ||
|
|
||
| t.expectResult(result).toSucceed(); | ||
| expect(await t.readProjectFile("install-marker.txt")).toBe("installed"); | ||
| }); | ||
|
|
||
| it("installs without a login", async () => { | ||
| // A build sandbox that has never logged in must still be able to install. | ||
| await t.givenProject(fixture("with-installable-site")); | ||
|
|
||
| const result = await t.run("site", "install"); | ||
|
|
||
| t.expectResult(result).toSucceed(); | ||
| expect(await t.readProjectFile("install-marker.txt")).toBe("installed"); | ||
| }); | ||
|
|
||
| it("fails when the installCommand fails", async () => { | ||
| await t.givenLoggedInWithProject(fixture("with-failing-install")); | ||
|
|
||
| const result = await t.run("site", "install"); | ||
|
|
||
| t.expectResult(result).toFail(); | ||
| t.expectResult(result).toContain("Install failed"); | ||
| }); | ||
|
|
||
| it("fails when the project has no site block", async () => { | ||
| await t.givenLoggedInWithProject(fixture("basic")); | ||
|
|
||
| const result = await t.run("site", "install"); | ||
|
|
||
| t.expectResult(result).toFail(); | ||
| t.expectResult(result).toContain("No site install command found"); | ||
| }); | ||
| }); |
6 changes: 6 additions & 0 deletions
6
packages/cli/tests/fixtures/with-failing-install/base44/config.jsonc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "name": "Failing Install Project", | ||
| "site": { | ||
| "installCommand": "node -e \"process.exit(1)\"" | ||
| } | ||
| } |
5 changes: 5 additions & 0 deletions
5
packages/cli/tests/fixtures/with-failing-install/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "failing-install-project", | ||
| "private": true, | ||
| "version": "0.0.0" | ||
| } |
6 changes: 6 additions & 0 deletions
6
packages/cli/tests/fixtures/with-installable-site/base44/config.jsonc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "name": "Installable Site Project", | ||
| "site": { | ||
| "installCommand": "node -e \"require('fs').writeFileSync('install-marker.txt', 'installed')\"" | ||
| } | ||
| } |
7 changes: 7 additions & 0 deletions
7
packages/cli/tests/fixtures/with-npm-serve-command/base44/config.jsonc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "Npm Serve Command Project", | ||
| // Empty on purpose: `site dev` falls back to `npm run dev`, which is what | ||
| // forwards an address through `--`. The schema does not default it — an | ||
| // absent serveCommand is how `base44 dev` knows to run the backend alone. | ||
| "site": {} | ||
| } |
7 changes: 7 additions & 0 deletions
7
packages/cli/tests/fixtures/with-npm-serve-command/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "npm-serve-command-fixture", | ||
| "private": true, | ||
| "scripts": { | ||
| "dev": "node serve.js" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| // Stands in for a real dev server: reports the arguments it was handed and the | ||
| // env it was given, then stays up until the runner stops it. | ||
| const args = process.argv.slice(2).join(" "); | ||
| console.log( | ||
| `ARGS=${args} APP=${process.env.VITE_BASE44_APP_ID} URL=${process.env.VITE_BASE44_APP_BASE_URL}`, | ||
| ); | ||
| setInterval(() => {}, 1000); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.