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
35 changes: 12 additions & 23 deletions src/commands/publish.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile } from "node:fs/promises";
import { resolveConfig, type Mode } from "../lib/config.js";
import { resolveConfig, type Overrides } from "../lib/config.js";
import { ensureHost } from "../lib/host.js";
import { makeBackend, type StorageBackend } from "../lib/backend.js";
import { generateCode, isValidPath } from "../lib/code.js";
Expand All @@ -11,23 +11,20 @@ import { mapLimit } from "../lib/concurrency.js";

const UPLOAD_CONCURRENCY = 8;

export interface PublishArgs {
/**
* Publish-specific fields plus the shared config Overrides. Intersecting with
* Overrides (rather than re-listing bucket/region/domain/... by hand) keeps a
* single source of truth: a new Overrides field flows straight to resolveConfig
* without a second place to update.
*/
export type PublishArgs = {
path: string;
slug?: string;
title?: string;
force?: boolean;
dryRun?: boolean;
region?: string;
profile?: string;
bucket?: string;
domain?: string;
distribution?: string;
serveRoot?: string;
host?: string;
port?: number;
scheme?: "http" | "https";
mode?: Mode;
}
} & Overrides;

async function uniqueCode(backend: StorageBackend): Promise<string> {
for (let i = 0; i < 5; i++) {
Expand All @@ -44,17 +41,9 @@ async function deriveTitle(uploads: Upload[]): Promise<string | null> {
}

export async function runPublish(args: PublishArgs): Promise<string> {
const cfg = await ensureHost(resolveConfig({
bucket: args.bucket,
region: args.region,
domain: args.domain,
distribution: args.distribution,
serveRoot: args.serveRoot,
host: args.host,
port: args.port,
scheme: args.scheme,
mode: args.mode,
}));
// args is `PublishArgs & Overrides`; resolveConfig reads only the Overrides
// keys and ignores the publish-specific extras (path, slug, profile, ...).
const cfg = await ensureHost(resolveConfig(args));
const backend = makeBackend(cfg, { profile: args.profile });
const uploads = await collectUploads(args.path);

Expand Down
12 changes: 12 additions & 0 deletions test/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ describe("listDocs", () => {
expect(rows[0].url).toBe("http://b.s3-website-us-east-1.amazonaws.com/team/q1/report/");
});

it("threads a region override through to resolveConfig", async () => {
s3mock
.on(ListObjectsV2Command)
.resolves({ Contents: [{ Key: "_meta/a.json" }], IsTruncated: false });
s3mock
.on(GetObjectCommand, { Key: "_meta/a.json" })
.resolves({ Body: { transformToString: async () => JSON.stringify({ code: "a", slug: null, title: "A", createdAt: "2026-01-01T00:00:00Z", files: 1, bytes: 1, sourcePath: "/a" }) } as any });

const rows = await listDocs({ region: "eu-west-1" });
expect(rows[0].url).toBe("http://b.s3-website-eu-west-1.amazonaws.com/a/");
});

it("skips a sidecar missing createdAt without crashing the sort", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
s3mock
Expand Down
6 changes: 6 additions & 0 deletions test/open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ describe("resolveOpenUrl", () => {
"http://b.s3-website-us-east-1.amazonaws.com/team/q1/report/",
);
});

it("threads bucket/region overrides through to resolveConfig", async () => {
expect(
await resolveOpenUrl({ id: "abc", bucket: "override-b", region: "eu-west-1" }),
).toBe("http://override-b.s3-website-eu-west-1.amazonaws.com/abc/");
});
});

describe("describeConfig", () => {
Expand Down
14 changes: 14 additions & 0 deletions test/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ describe("runPublish", () => {
delete process.env.HOSTDOC_DISTRIBUTION;
});

it("threads a bucket override through to resolveConfig", async () => {
// A --bucket flag (highest precedence) must reach resolveConfig and win
// over the ambient env bucket. Guards against publish dropping an Overrides
// field on its way to resolveConfig.
writeFileSync(join(dir, "index.html"), "x");
const url = await runPublish({
path: dir,
slug: "doc1",
dryRun: true,
bucket: "override-b",
});
expect(url).toBe("http://override-b.s3-website-us-east-1.amazonaws.com/doc1/");
});

it("invalidates /<code>/* when overwriting in cloudfront mode", async () => {
process.env.HOSTDOC_DOMAIN = "shared.example.com";
process.env.HOSTDOC_DISTRIBUTION = "DIST1";
Expand Down
18 changes: 18 additions & 0 deletions test/rm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ describe("runRm", () => {
delete process.env.HOSTDOC_DISTRIBUTION;
});

it("threads domain/distribution overrides through to resolveConfig (cloudfront)", async () => {
// ambient env is s3-website (bucket+region). Passing domain+distribution as
// flags must reach resolveConfig and derive cloudfront, which invalidates.
s3mock
.on(ListObjectsV2Command)
.resolves({ Contents: [{ Key: "doc1/index.html" }], IsTruncated: false });
s3mock.on(DeleteObjectsCommand).resolves({});

await runRm({
id: "doc1",
yes: true,
domain: "shared.example.com",
distribution: "DIST9",
});

expect(cfMock.commandCalls(CreateInvalidationCommand)).toHaveLength(1);
});

it.each(["_meta", "../escape", "a b", "x//y", "x/_y", "x?y"])(
"rejects invalid id %j before deleting anything",
async (id) => {
Expand Down
Loading