Always apply default config
320873db
Resolves #39
6 file(s) · +110 −16
Resolves #39
| 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); |
|
| 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); |
|
| 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 | ||
| 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 | ||
| 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; |
| 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 | + | }); |