Always apply default config 320873db
Resolves #39
Heath Stewart · 2026-05-26 23:19 6 file(s) · +110 −16
packages/cli/src/commands/init.ts +10 −5
12 12
	group,
13 13
} from "@clack/prompts";
14 14
import * as path from "node:path";
15 -
import { findConfig, generateConfigTemplate } from "../lib/config";
15 +
import {
16 +
	findConfig,
17 +
	generateConfigTemplate,
18 +
	DEFAULT_PUBLISHER_CONFIG,
19 +
} from "../lib/config";
16 20
import { loadCredentials, listAllCredentials } from "../lib/credentials";
17 21
import { createAgent, createPublication } from "../lib/atproto";
18 22
import { selectCredential } from "../lib/credential-select";
340 344
			siteUrl: siteConfig.siteUrl,
341 345
			contentDir: siteConfig.contentDir || "./content",
342 346
			imagesDir: siteConfig.imagesDir || undefined,
343 -
			publicDir: siteConfig.publicDir || "./public",
344 -
			outputDir: siteConfig.outputDir || "./dist",
345 -
			pathPrefix: siteConfig.pathPrefix ?? "/posts",
347 +
			publicDir: siteConfig.publicDir || DEFAULT_PUBLISHER_CONFIG.publicDir,
348 +
			outputDir: siteConfig.outputDir || DEFAULT_PUBLISHER_CONFIG.outputDir,
349 +
			pathPrefix: siteConfig.pathPrefix ?? DEFAULT_PUBLISHER_CONFIG.pathPrefix,
346 350
			publicationUri,
347 351
			pdsUrl,
348 352
			frontmatter: frontmatterMapping,
356 360
		log.success(`Configuration saved to ${configPath}`);
357 361
358 362
		// Create .well-known/site.standard.publication file
359 -
		const publicDir = siteConfig.publicDir || "./public";
363 +
		const publicDir =
364 +
			siteConfig.publicDir || DEFAULT_PUBLISHER_CONFIG.publicDir;
360 365
		const resolvedPublicDir = path.isAbsolute(publicDir)
361 366
			? publicDir
362 367
			: path.join(process.cwd(), publicDir);
packages/cli/src/commands/inject.ts +8 −2
3 3
import { glob } from "glob";
4 4
import * as fs from "node:fs/promises";
5 5
import * as path from "node:path";
6 -
import { findConfig, loadConfig, loadState } from "../lib/config";
6 +
import {
7 +
	findConfig,
8 +
	loadConfig,
9 +
	loadState,
10 +
	DEFAULT_PUBLISHER_CONFIG,
11 +
} from "../lib/config";
7 12
8 13
export const injectCommand = command({
9 14
	name: "inject",
33 38
		const configDir = path.dirname(configPath);
34 39
35 40
		// Determine output directory
36 -
		const outputDir = outputDirArg || config.outputDir || "./dist";
41 +
		const outputDir =
42 +
			outputDirArg || config.outputDir || DEFAULT_PUBLISHER_CONFIG.outputDir;
37 43
		const resolvedOutputDir = path.isAbsolute(outputDir)
38 44
			? outputDir
39 45
			: path.join(configDir, outputDir);
packages/cli/src/commands/update.ts +12 −7
10 10
	spinner,
11 11
	log,
12 12
} from "@clack/prompts";
13 -
import { findConfig, loadConfig } from "../lib/config";
13 +
import {
14 +
	findConfig,
15 +
	loadConfig,
16 +
	DEFAULT_PUBLISHER_CONFIG,
17 +
} from "../lib/config";
14 18
import {
15 19
	loadCredentials,
16 20
	listAllCredentials,
70 74
	const configSummary = [
71 75
		`Site URL: ${config.siteUrl}`,
72 76
		`Content Dir: ${config.contentDir}`,
73 -
		`Path Prefix: ${config.pathPrefix ?? "/posts"}`,
77 +
		`Path Prefix: ${config.pathPrefix ?? DEFAULT_PUBLISHER_CONFIG.pathPrefix}`,
74 78
		`Publication URI: ${config.publicationUri}`,
75 79
		config.imagesDir ? `Images Dir: ${config.imagesDir}` : null,
76 80
		config.outputDir ? `Output Dir: ${config.outputDir}` : null,
177 181
	const pathPrefix = exitOnCancel(
178 182
		await text({
179 183
			message: "URL path prefix for posts:",
180 -
			initialValue: config.pathPrefix ?? "/posts",
184 +
			initialValue: config.pathPrefix ?? DEFAULT_PUBLISHER_CONFIG.pathPrefix,
181 185
		}),
182 186
	);
183 187
211 215
	const publicDir = exitOnCancel(
212 216
		await text({
213 217
			message: "Public/static directory:",
214 -
			initialValue: config.publicDir || "./public",
218 +
			initialValue: config.publicDir || DEFAULT_PUBLISHER_CONFIG.publicDir,
215 219
		}),
216 220
	);
217 221
218 222
	const outputDir = exitOnCancel(
219 223
		await text({
220 224
			message: "Build output directory:",
221 -
			initialValue: config.outputDir || "./dist",
225 +
			initialValue: config.outputDir || DEFAULT_PUBLISHER_CONFIG.outputDir,
222 226
		}),
223 227
	);
224 228
360 364
	const publishContent = exitOnCancel(
361 365
		await confirm({
362 366
			message: "Publish the post content on the standard.site document?",
363 -
			initialValue: config.publishContent ?? true,
367 +
			initialValue:
368 +
				config.publishContent ?? DEFAULT_PUBLISHER_CONFIG.publishContent,
364 369
		}),
365 370
	);
366 371
388 393
		removeIndexFromSlug: removeIndexFromSlug || undefined,
389 394
		stripDatePrefix: stripDatePrefix || undefined,
390 395
		textContentField: textContentField || undefined,
391 -
		publishContent: publishContent ?? true,
396 +
		publishContent: publishContent ?? DEFAULT_PUBLISHER_CONFIG.publishContent,
392 397
	};
393 398
}
394 399
packages/cli/src/lib/config.ts +16 −2
10 10
export const CONFIG_FILENAME = "sequoia.json";
11 11
const STATE_FILENAME = ".sequoia-state.json";
12 12
13 +
/**
14 +
 * Default values for optional `PublisherConfig` fields that have documented defaults.
15 +
 * Must be kept in sync with the field comments on `PublisherConfig` in `types.ts`.
16 +
 */
17 +
export const DEFAULT_PUBLISHER_CONFIG = {
18 +
	publicDir: "./public",
19 +
	outputDir: "./dist",
20 +
	pathPrefix: "/posts",
21 +
	publishContent: true,
22 +
	removeIndexFromSlug: false,
23 +
	stripDatePrefix: false,
24 +
	autoSync: true,
25 +
} satisfies Partial<PublisherConfig>;
26 +
13 27
async function fileExists(filePath: string): Promise<boolean> {
14 28
	try {
15 29
		await fs.access(filePath);
61 75
		if (!config.publicationUri)
62 76
			throw new Error("publicationUri is required in config");
63 77
64 -
		return config;
78 +
		return { ...DEFAULT_PUBLISHER_CONFIG, ...config };
65 79
	} catch (error) {
66 80
		if (error instanceof Error && error.message.includes("required")) {
67 81
			throw error;
141 155
		config.textContentField = options.textContentField;
142 156
	}
143 157
144 -
	if (options.publishContent) {
158 +
	if (options.publishContent !== undefined) {
145 159
		config.publishContent = options.publishContent;
146 160
	}
147 161
packages/cli/src/lib/types.ts +6 −0
26 26
	components: string; // Directory to install UI components (default: src/components)
27 27
}
28 28
29 +
/**
30 +
 * Publisher configuration.
31 +
 *
32 +
 * Any field with a documented default must be kept in sync with
33 +
 * `DEFAULT_PUBLISHER_CONFIG` in `config.ts`, and vice versa.
34 +
 */
29 35
export interface PublisherConfig {
30 36
	siteUrl: string;
31 37
	contentDir: string;
packages/cli/test/config.test.ts (added) +58 −0
1 +
import { describe, expect, it, spyOn } from "bun:test";
2 +
import * as fs from "node:fs/promises";
3 +
import { DEFAULT_PUBLISHER_CONFIG, loadConfig } from "../src/lib/config";
4 +
5 +
const REQUIRED_FIELDS = {
6 +
	siteUrl: "https://example.com",
7 +
	contentDir: "./content",
8 +
	publicationUri: "at://did:plc:abc123/site.standard.publication/main",
9 +
};
10 +
11 +
describe("loadConfig", () => {
12 +
	it("applies DEFAULT_PUBLISHER_CONFIG when optional fields are absent", async () => {
13 +
		spyOn(fs, "readFile").mockResolvedValue(
14 +
			JSON.stringify(REQUIRED_FIELDS) as unknown as Buffer,
15 +
		);
16 +
17 +
		const config = await loadConfig("/fake/sequoia.json");
18 +
19 +
		expect(config.publicDir).toBe(DEFAULT_PUBLISHER_CONFIG.publicDir);
20 +
		expect(config.outputDir).toBe(DEFAULT_PUBLISHER_CONFIG.outputDir);
21 +
		expect(config.pathPrefix).toBe(DEFAULT_PUBLISHER_CONFIG.pathPrefix);
22 +
		expect(config.publishContent).toBe(DEFAULT_PUBLISHER_CONFIG.publishContent);
23 +
		expect(config.removeIndexFromSlug).toBe(
24 +
			DEFAULT_PUBLISHER_CONFIG.removeIndexFromSlug,
25 +
		);
26 +
		expect(config.stripDatePrefix).toBe(
27 +
			DEFAULT_PUBLISHER_CONFIG.stripDatePrefix,
28 +
		);
29 +
		expect(config.autoSync).toBe(DEFAULT_PUBLISHER_CONFIG.autoSync);
30 +
	});
31 +
32 +
	it("uses explicit values when all defaultable fields are set", async () => {
33 +
		const custom = {
34 +
			...REQUIRED_FIELDS,
35 +
			publicDir: "./static",
36 +
			outputDir: "./build",
37 +
			pathPrefix: "/articles",
38 +
			publishContent: false,
39 +
			removeIndexFromSlug: true,
40 +
			stripDatePrefix: true,
41 +
			autoSync: false,
42 +
		};
43 +
44 +
		spyOn(fs, "readFile").mockResolvedValue(
45 +
			JSON.stringify(custom) as unknown as Buffer,
46 +
		);
47 +
48 +
		const config = await loadConfig("/fake/sequoia.json");
49 +
50 +
		expect(config.publicDir).toBe("./static");
51 +
		expect(config.outputDir).toBe("./build");
52 +
		expect(config.pathPrefix).toBe("/articles");
53 +
		expect(config.publishContent).toBe(false);
54 +
		expect(config.removeIndexFromSlug).toBe(true);
55 +
		expect(config.stripDatePrefix).toBe(true);
56 +
		expect(config.autoSync).toBe(false);
57 +
	});
58 +
});