Skip to content
Open
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 packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The CLI will guide you through project setup. For step-by-step tutorials, see th
| [`sandbox checkpoint`](https://docs.base44.com/developers/references/cli/commands/sandbox-checkpoint) | Save a restore point in the builder's version history; run it after each unit of work, since sandbox writes are committed but not checkpointed and a Restore or Revert in the builder discards everything after the last checkpoint |
| [`site deploy`](https://docs.base44.com/developers/references/cli/commands/site-deploy) | Deploy built site files to Base44 hosting |
| [`site open`](https://docs.base44.com/developers/references/cli/commands/site-open) | Open the published site in your browser |
| `drop [dir]` | Host a folder of static files (index.html at its root) on Base44 as a new app |
| [`types generate`](https://docs.base44.com/developers/references/cli/commands/types-generate) | Generate TypeScript types from project resources |

## Global flags
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/src/cli/commands/drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { resolve } from "node:path";
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, theme } from "@/cli/utils/index.js";
import { dropSite } from "@/core/site/index.js";

async function dropAction(
{ runTask, jsonMode }: CLIContext,
dir: string,
): Promise<RunCommandResult> {
const { appId, appUrl } = await runTask(
"Creating app and deploying site...",
async () => await dropSite(resolve(dir)),
{
successMessage: theme.colors.base44Orange("Site deployed successfully"),
errorMessage: "Drop failed",
},
);

return {
outroMessage: `Visit your site at: ${theme.colors.links(appUrl)}`,
stdout: jsonMode
? `${JSON.stringify({ appId, appUrl }, null, 2)}\n`
: undefined,
};
}

export function getDropCommand(): Command {
return new Base44Command("drop", { requireAppContext: false })
.description(
"Host a folder of static files (index.html at its root) on Base44 as a new app",
)
.argument("[dir]", "Folder to deploy", ".")
.action(dropAction);
}
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 { getWhoamiCommand } from "@/cli/commands/auth/whoami.js";
import { getBranchesCommand } from "@/cli/commands/branches/index.js";
import { getConnectorsCommand } from "@/cli/commands/connectors/index.js";
import { getDashboardCommand } from "@/cli/commands/dashboard/index.js";
import { getDropCommand } from "@/cli/commands/drop.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";
Expand Down Expand Up @@ -82,6 +83,7 @@ export function createProgram(context: CLIContext): Command {
program.addCommand(getDashboardCommand());
program.addCommand(getBuildCommand());
program.addCommand(getDeployCommand());
program.addCommand(getDropCommand());
program.addCommand(getVisibilityCommand());
program.addCommand(getLinkCommand());
program.addCommand(getEjectCommand());
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/src/core/site/drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { basename, join } from "node:path";
import { InvalidInputError } from "@/core/errors.js";
import { createProject } from "@/core/project/api.js";
import { setAppContext } from "@/core/project/app-config.js";
import { deploySite } from "@/core/site/deploy.js";
import { pathExists } from "@/core/utils/fs.js";

async function requireIndexHtml(siteDir: string) {
if (!(await pathExists(join(siteDir, "index.html")))) {
throw new InvalidInputError(
`No index.html found in "${siteDir}" — a static site needs one at the folder root.`,
);
}
}

export async function dropSite(siteDir: string) {
await requireIndexHtml(siteDir);
const name = basename(siteDir);
const { projectId } = await createProject(name, `Static site '${name}'`);
setAppContext({ id: projectId });
const { appUrl } = await deploySite(siteDir);
return { appId: projectId, appUrl };
}
1 change: 1 addition & 0 deletions packages/cli/src/core/site/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./api.js";
export * from "./config.js";
export * from "./deploy.js";
export * from "./deployment.js";
export * from "./drop.js";
export * from "./full-stack.js";
export * from "./git-hash.js";
export * from "./manifest.js";
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/tests/cli/drop.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import { fixture, setupCLITests } from "./testkit/index.js";

const APP_URL = "https://my-site.base44.app";

describe("drop command", () => {
const t = setupCLITests();

it("creates an app named after the folder and deploys it", async () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });
await t.givenUnlinkedProject(fixture("static-drop"));
t.api.mockCreateApp({ id: t.api.appId, name: "my-site" });
t.api.mockSiteDeploy({ app_url: APP_URL });

const result = await t.run("drop", "my-site");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("Site deployed successfully");
t.expectResult(result).toContain(APP_URL);
expect(t.api.createAppRequests).toEqual([
expect.objectContaining({
name: "my-site",
is_managed_source_code: false,
}),
]);
});

it("prints the app id and URL as JSON under --json", async () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });
await t.givenUnlinkedProject(fixture("static-drop"));
t.api.mockCreateApp({ id: t.api.appId, name: "my-site" });
t.api.mockSiteDeploy({ app_url: APP_URL });

const result = await t.run("drop", "my-site", "--json");

t.expectResult(result).toSucceed();
expect(JSON.parse(result.stdout)).toEqual({
appId: t.api.appId,
appUrl: APP_URL,
});
});

it("fails before creating an app when the folder has no index.html", async () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });
await t.givenUnlinkedProject(fixture("static-drop"));
t.api.mockCreateApp({ id: t.api.appId, name: "no-index" });

const result = await t.run("drop", "no-index");

t.expectResult(result).toFail();
t.expectResult(result).toContain("No index.html found");
expect(t.api.createAppRequests).toEqual([]);
});

it("reports the server error when the deploy fails", async () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });
await t.givenUnlinkedProject(fixture("static-drop"));
t.api.mockCreateApp({ id: t.api.appId, name: "my-site" });
t.api.mockSiteDeployError({ status: 413, body: { error: "Too large" } });

const result = await t.run("drop", "my-site");

t.expectResult(result).toFail();
t.expectResult(result).toContain("Too large");
});
});
13 changes: 12 additions & 1 deletion packages/cli/tests/cli/testkit/TestAPIServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,8 +986,19 @@ export class TestAPIServer {

// ─── GENERAL ENDPOINTS ───────────────────────────────────

/** Captured JSON bodies of POST /api/apps requests. */
readonly createAppRequests: unknown[] = [];

mockCreateApp(response: CreateAppResponse): this {
return this.addRoute("POST", "/api/apps", response);
this.pendingRoutes.push({
method: "POST",
path: "/api/apps",
handler: (req, res) => {
this.createAppRequests.push(req.body);
res.status(200).json(response);
},
});
return this;
}

mockListProjects(response: ListProjectsResponse[]): this {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
h1 { color: tomato; }
2 changes: 2 additions & 0 deletions packages/cli/tests/fixtures/static-drop/my-site/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html><head><link rel="stylesheet" href="assets/style.css"></head><body><h1>Dropped</h1></body></html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>not the entry</h1>
Loading