@karaoke-cms/contracts 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +25 -0
  2. package/src/index.ts +348 -0
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@karaoke-cms/contracts",
3
+ "type": "module",
4
+ "version": "0.10.3",
5
+ "description": "Shared type contracts for karaoke-cms — interfaces for modules, themes, and configuration",
6
+ "main": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src/"
12
+ ],
13
+ "keywords": [
14
+ "astro",
15
+ "cms",
16
+ "karaoke-cms",
17
+ "types"
18
+ ],
19
+ "peerDependencies": {
20
+ "astro": ">=6.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "astro": "^6.0.8"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,348 @@
1
+ /**
2
+ * @karaoke-cms/contracts
3
+ *
4
+ * Shared type contracts for karaoke-cms.
5
+ *
6
+ * Every interface that crosses a package boundary lives here. All other packages
7
+ * (`@karaoke-cms/astro`, `@karaoke-cms/module-*`, `@karaoke-cms/theme-*`) import
8
+ * from this package rather than from each other.
9
+ *
10
+ * This package also exports `defineModule()` and `defineTheme()` — the factory
11
+ * functions module and theme authors use to declare their package. They have no
12
+ * Astro-specific runtime dependencies, only type-level references to `astro`.
13
+ */
14
+
15
+ import type { AstroIntegration } from 'astro';
16
+
17
+ // ── CSS contract ──────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Metadata for a single CSS class slot in a module's contract.
21
+ *
22
+ * - `required: true` — the theme MUST implement this class for the module to render correctly.
23
+ * - `required: false` — the class is optional / used for progressive enhancement.
24
+ */
25
+ export interface CssContractSlot {
26
+ description: string;
27
+ required: boolean;
28
+ }
29
+
30
+ /**
31
+ * A record mapping CSS class names to their slot metadata.
32
+ *
33
+ * Module authors declare a `CssContract` in `src/css-contract.ts`; themes implement
34
+ * every class listed here. `required: true` slots must be styled.
35
+ *
36
+ * @example
37
+ * export const cssContract: CssContract = {
38
+ * 'blog-list': { description: 'Post list page wrapper', required: true },
39
+ * 'blog-card': { description: 'Individual post card', required: true },
40
+ * 'blog-tag': { description: 'Tag chip on card/post', required: false },
41
+ * };
42
+ */
43
+ export type CssContract = Record<string, CssContractSlot>;
44
+
45
+ // ── Primitive named types ─────────────────────────────────────────────────────
46
+
47
+ /** A single route injected by a module into the Astro site. */
48
+ export interface RouteDefinition {
49
+ pattern: string;
50
+ entrypoint: string;
51
+ }
52
+
53
+ /**
54
+ * Metadata for a docs-style collection owned by a module instance.
55
+ * When present on a `ModuleInstance`, `karaoke()` code-generates per-instance
56
+ * page files instead of using static entrypoints.
57
+ */
58
+ export interface CollectionSpec {
59
+ name: string;
60
+ folder: string;
61
+ label: string;
62
+ layout: string;
63
+ sidebarStyle: string;
64
+ }
65
+
66
+ /** A page file to copy into the user's `src/pages/` on first dev run. */
67
+ export interface ScaffoldPage {
68
+ /** Absolute path to the source `.astro` file inside the npm package. */
69
+ src: string;
70
+ /** Destination path relative to `src/pages/` (e.g. `"blog/index.astro"`). */
71
+ dest: string;
72
+ }
73
+
74
+ // ── Module system ─────────────────────────────────────────────────────────────
75
+
76
+ /** A menu entry registered by a module instance. */
77
+ export interface ModuleMenuEntry {
78
+ id: string;
79
+ name: string;
80
+ path: string;
81
+ section: string;
82
+ weight: number;
83
+ }
84
+
85
+ /**
86
+ * A resolved module instance — returned by a `defineModule()` factory call.
87
+ *
88
+ * Pass instances to `defineConfig({ modules: [...] })` in `karaoke.config.ts`.
89
+ */
90
+ export interface ModuleInstance {
91
+ _type: 'module-instance';
92
+ id: string;
93
+ mount: string;
94
+ /** When false, `karaoke()` skips route injection and menu entries for this module. Defaults to true. */
95
+ enabled: boolean;
96
+ routes: RouteDefinition[];
97
+ menuEntries: ModuleMenuEntry[];
98
+ /** CSS class names this module will use in its markup. Themes implement these classes. */
99
+ cssContract: CssContract;
100
+ hasDefaultCss: boolean;
101
+ /**
102
+ * When present, this module owns a docs-style collection.
103
+ * `karaoke()` code-generates per-instance `.astro` page files instead of
104
+ * using the static entrypoints in `routes`.
105
+ */
106
+ collection?: CollectionSpec;
107
+ scaffoldPages?: ScaffoldPage[];
108
+ /**
109
+ * Absolute path to the module's default CSS file.
110
+ * On first dev run, copied to `src/styles/{id}.css` and imported into `global.css`.
111
+ */
112
+ defaultCssPath?: string;
113
+ /**
114
+ * Optional Astro integration provided by this module.
115
+ * `karaoke()` collects these and registers them alongside the theme integration.
116
+ * Use for Vite plugin registration, build hooks, etc.
117
+ */
118
+ integration?: AstroIntegration;
119
+ }
120
+
121
+ // ── Module factory ────────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Definition object passed to `defineModule()`.
125
+ * Describes a module's static shape; the factory call fills in runtime values.
126
+ */
127
+ export interface ModuleDefinition {
128
+ id: string;
129
+ cssContract: CssContract;
130
+ /** Absolute path to a default CSS file. On first dev run, copied to `src/styles/{id}.css`. */
131
+ defaultCssPath?: string;
132
+ routes: (mount: string) => RouteDefinition[];
133
+ menuEntries: (mount: string, id: string) => ModuleMenuEntry[];
134
+ collection?: () => unknown;
135
+ scaffoldPages?: (mount: string) => ScaffoldPage[];
136
+ /** Optional Astro integration for this module (Vite plugins, build hooks, etc.). */
137
+ integration?: AstroIntegration;
138
+ }
139
+
140
+ /**
141
+ * Define a karaoke-cms module.
142
+ *
143
+ * Returns a factory function. Call the factory with `{ mount }` (and optionally
144
+ * `{ id }` for multi-instance support) to get a `ModuleInstance` that can be
145
+ * passed to `defineConfig({ modules: [...] })`.
146
+ *
147
+ * @example
148
+ * export const blog = defineModule({ id: 'blog', routes: (mount) => [...], ... })
149
+ * // In karaoke.config.ts:
150
+ * modules: [blog({ mount: '/blog' })]
151
+ */
152
+ export function defineModule(def: ModuleDefinition) {
153
+ return function moduleFactory(config: { id?: string; mount?: string; enabled?: boolean } = {}): ModuleInstance {
154
+ const id = config.id ?? def.id;
155
+ const mount = (config.mount ?? '').replace(/\/$/, '');
156
+ const enabled = config.enabled ?? true;
157
+ return {
158
+ _type: 'module-instance',
159
+ id,
160
+ mount,
161
+ enabled,
162
+ routes: def.routes(mount),
163
+ menuEntries: def.menuEntries(mount, id),
164
+ cssContract: def.cssContract,
165
+ hasDefaultCss: !!def.defaultCssPath,
166
+ scaffoldPages: def.scaffoldPages?.(mount),
167
+ defaultCssPath: def.defaultCssPath,
168
+ integration: def.integration,
169
+ };
170
+ };
171
+ }
172
+
173
+ // ── Theme system ──────────────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * A resolved theme instance — returned by a `defineTheme()` factory call.
177
+ *
178
+ * Pass to `defineConfig({ theme: ... })` in `karaoke.config.ts`.
179
+ */
180
+ export interface ThemeInstance {
181
+ _type: 'theme-instance';
182
+ id: string;
183
+ /** Module instances bundled or accepted by this theme (used for deduplication). */
184
+ implementedModules: ModuleInstance[];
185
+ toAstroIntegration: (activeModules?: ModuleInstance[]) => AstroIntegration;
186
+ }
187
+
188
+ // ── Theme factory ─────────────────────────────────────────────────────────────
189
+
190
+ /**
191
+ * User-facing config for a theme factory. Themes may extend this interface
192
+ * to declare their own configuration fields.
193
+ */
194
+ export interface ThemeFactoryConfig {}
195
+
196
+ /**
197
+ * Definition object passed to `defineTheme()`.
198
+ */
199
+ export interface ThemeDefinition {
200
+ id: string;
201
+ /**
202
+ * Module instances statically bundled with this theme.
203
+ * These are merged with `config.modules` by `karaoke()`, deduplicating by id
204
+ * (config wins). Themes that accept modules via factory config should set
205
+ * `implementedModules` dynamically inside `toAstroIntegration` instead.
206
+ */
207
+ implementedModules?: ModuleInstance[];
208
+ toAstroIntegration: (config: ThemeFactoryConfig, modules: ModuleInstance[]) => AstroIntegration;
209
+ }
210
+
211
+ /**
212
+ * Define a karaoke-cms theme.
213
+ *
214
+ * Returns a factory function. Call the factory (optionally with config) to get a
215
+ * `ThemeInstance` that can be passed to `defineConfig({ theme: ... })`. Modules are
216
+ * passed by the `karaoke()` integration at build time via `toAstroIntegration(modules)`.
217
+ *
218
+ * @example
219
+ * export const myTheme = defineTheme({
220
+ * id: 'my-theme',
221
+ * toAstroIntegration: (config, modules) => ({ name: '...', hooks: { ... } }),
222
+ * })
223
+ * // In karaoke.config.ts:
224
+ * theme: myTheme()
225
+ */
226
+ export function defineTheme(def: ThemeDefinition) {
227
+ return function themeFactory(config: ThemeFactoryConfig = {}): ThemeInstance {
228
+ return {
229
+ _type: 'theme-instance',
230
+ id: def.id,
231
+ implementedModules: def.implementedModules ?? [],
232
+ toAstroIntegration: (modules: ModuleInstance[] = []) => def.toAstroIntegration(config, modules),
233
+ };
234
+ };
235
+ }
236
+
237
+ // ── Configuration types ───────────────────────────────────────────────────────
238
+
239
+ export type RegionComponent = 'header' | 'main-menu' | 'search' | 'recent-posts' | 'footer';
240
+
241
+ export type CollectionMode = 'dev' | 'prod';
242
+
243
+ export interface CollectionConfig {
244
+ modes?: CollectionMode[];
245
+ label?: string;
246
+ }
247
+
248
+ export interface ResolvedCollection {
249
+ modes: CollectionMode[];
250
+ label: string;
251
+ enabled: boolean;
252
+ }
253
+
254
+ export type ResolvedCollections = Record<string, ResolvedCollection>;
255
+
256
+ export interface CommentsConfig {
257
+ enabled?: boolean;
258
+ /** GitHub repo in "owner/repo" format */
259
+ repo?: string;
260
+ repoId?: string;
261
+ category?: string;
262
+ categoryId?: string;
263
+ }
264
+
265
+ export interface KaraokeConfig {
266
+ /**
267
+ * Path to the Obsidian vault root (where content/ lives).
268
+ * Absolute, or relative to the Astro project root.
269
+ * Defaults to the project root (vault and project are the same directory).
270
+ * Typically set via KARAOKE_VAULT in .env (gitignored) with .env.default as fallback.
271
+ */
272
+ vault?: string;
273
+ /** Per-collection mode overrides. Merges with collections.yaml; this field takes precedence. */
274
+ collections?: Record<string, CollectionConfig>;
275
+ /** Site title — displayed in the browser tab and nav bar. Defaults to 'Karaoke'. */
276
+ title?: string;
277
+ /** Site description — used in RSS feed and OG meta tags. */
278
+ description?: string;
279
+ /** Theme — package name string (legacy) or a ThemeInstance from defineTheme(). */
280
+ theme?: string | ThemeInstance;
281
+ /** Modules to activate. Each is a ModuleInstance from defineModule(). */
282
+ modules?: ModuleInstance[];
283
+ /** Giscus comments configuration. */
284
+ comments?: CommentsConfig;
285
+ layout?: {
286
+ regions?: {
287
+ top?: { components?: RegionComponent[] };
288
+ left?: { components?: RegionComponent[] };
289
+ right?: { components?: RegionComponent[] };
290
+ bottom?: { components?: RegionComponent[] };
291
+ };
292
+ };
293
+ }
294
+
295
+ /** Resolved (defaults filled in) modules config — available at build time via virtual module. */
296
+ export interface ResolvedModules {
297
+ comments: {
298
+ enabled: boolean;
299
+ repo: string;
300
+ repoId: string;
301
+ category: string;
302
+ categoryId: string;
303
+ };
304
+ }
305
+
306
+ /** Resolved (defaults filled in) layout config — available at build time via virtual module. */
307
+ export interface ResolvedLayout {
308
+ regions: {
309
+ top: { components: RegionComponent[] };
310
+ left: { components: RegionComponent[] };
311
+ right: { components: RegionComponent[] };
312
+ bottom: { components: RegionComponent[] };
313
+ };
314
+ }
315
+
316
+ // ── Menu types ────────────────────────────────────────────────────────────────
317
+
318
+ /** Raw shape of one entry in menus.yaml (used to type the parsed YAML). */
319
+ export interface MenuEntryConfig {
320
+ text: string;
321
+ href?: string;
322
+ weight?: number;
323
+ /** Visibility condition — only 'collection:name' supported in v1. */
324
+ when?: string;
325
+ entries?: MenuEntryConfig[];
326
+ }
327
+
328
+ /** Raw shape of one menu block in menus.yaml. */
329
+ export interface MenuConfig {
330
+ orientation?: 'horizontal' | 'vertical';
331
+ entries?: MenuEntryConfig[];
332
+ }
333
+
334
+ export interface ResolvedMenuEntry {
335
+ text: string;
336
+ href?: string;
337
+ weight: number;
338
+ when?: string;
339
+ entries: ResolvedMenuEntry[];
340
+ }
341
+
342
+ export interface ResolvedMenu {
343
+ name: string;
344
+ orientation: 'horizontal' | 'vertical';
345
+ entries: ResolvedMenuEntry[];
346
+ }
347
+
348
+ export type ResolvedMenus = Record<string, ResolvedMenu>;