@mandujs/core 0.24.0 → 0.25.1

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.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Content Types Generator (Issue #199)
3
+ *
4
+ * Emits `.mandu/generated/content-types.d.ts` with typed aliases for
5
+ * every collection declared in `content.config.ts`. This gives users
6
+ * full autocomplete on `docs.all()`, `docs.get(slug)`, etc. without
7
+ * having to thread generics through every call site:
8
+ *
9
+ * ```ts
10
+ * // user writes
11
+ * import type { CollectionMap } from '.mandu/generated/content-types';
12
+ * const entry = (await collections.docs.all())[0];
13
+ * entry.data.title; // typed as string when schema has title: z.string()
14
+ * ```
15
+ *
16
+ * # Contract
17
+ *
18
+ * - Input: a map of collection-name → Collection instance (already
19
+ * returned from `defineCollection({ path, schema })`). We DO NOT
20
+ * re-execute `content.config.ts` here — the caller (the CLI
21
+ * generator in #196) is responsible for running the config in
22
+ * a scratch context and passing the live map to this function.
23
+ *
24
+ * - Output: a single `.d.ts` file with:
25
+ * 1. Re-exports of `Collection`, `CollectionEntry` types
26
+ * 2. A `CollectionMap` interface keyed by collection name
27
+ * 3. A `CollectionName` union type
28
+ * 4. Per-collection `Entry{Name}` aliases
29
+ *
30
+ * - Determinism: output order is stable (sorted by collection name)
31
+ * so regenerating without content changes produces a no-op diff.
32
+ *
33
+ * Schema-level type extraction is intentionally shallow for the MVP —
34
+ * we emit `CollectionEntry<Record<string, unknown>>` for collections
35
+ * without a schema, and `CollectionEntry<z.infer<typeof X>>` only
36
+ * when the user exports the schema alongside the collection. A future
37
+ * pass (tracked in a follow-up) will parse the Zod schema at build
38
+ * time to emit concrete interfaces.
39
+ */
40
+
41
+ import * as fs from "fs";
42
+ import * as path from "path";
43
+ import type { Collection } from "./collection";
44
+
45
+ /**
46
+ * Structural view of a Collection accepted by the type emitter. The
47
+ * generator only reads the collection's name — it never calls methods
48
+ * — so the empty interface lets callers pass typed
49
+ * `Collection<{title: string}>` without running into TypeScript's
50
+ * invariance on generic classes.
51
+ */
52
+ // biome-ignore lint/suspicious/noEmptyInterface: marker type for variance
53
+ interface CollectionLike {}
54
+
55
+ /** Input map: `{ docs: docsCollection, blog: blogCollection }`. */
56
+ export type CollectionRegistry = Record<string, CollectionLike>;
57
+
58
+ // Re-export the Collection type so users of @mandujs/core/content still
59
+ // see it alongside the registry.
60
+ export type { Collection };
61
+
62
+ /** Options for the type-emitter. */
63
+ export interface GenerateTypesOptions {
64
+ /**
65
+ * Destination path for the generated `.d.ts` file. Defaults to
66
+ * `.mandu/generated/content-types.d.ts` relative to `root`.
67
+ */
68
+ outFile?: string;
69
+ /**
70
+ * Project root. Used to resolve `outFile` when that option is
71
+ * relative. Defaults to `process.cwd()`.
72
+ */
73
+ root?: string;
74
+ /**
75
+ * Banner comment prepended to the file. Defaults to an
76
+ * auto-generated warning so authors know not to hand-edit.
77
+ */
78
+ banner?: string;
79
+ }
80
+
81
+ const DEFAULT_BANNER = `/**
82
+ * AUTO-GENERATED by @mandujs/core/content — do not edit by hand.
83
+ * Regenerated on every \`mandu build\` / \`mandu dev\` bootstrap.
84
+ */`;
85
+
86
+ /**
87
+ * Generate the `content-types.d.ts` source string. Exposed separately
88
+ * from the filesystem writer so tests can assert on the output without
89
+ * touching disk.
90
+ */
91
+ export function renderContentTypes(
92
+ collections: CollectionRegistry
93
+ ): string {
94
+ const names = Object.keys(collections).sort();
95
+ const lines: string[] = [];
96
+ lines.push(DEFAULT_BANNER);
97
+ lines.push("");
98
+ lines.push(`import type { Collection, CollectionEntry } from "@mandujs/core/content";`);
99
+ lines.push("");
100
+ lines.push("// Per-collection entry aliases");
101
+ for (const name of names) {
102
+ // We can't introspect Zod at runtime without an ICU-sized schema
103
+ // crawler, so we emit `Record<string, unknown>` here. Users who
104
+ // want full field typing can import their schema directly:
105
+ // type DocEntry = CollectionEntry<z.infer<typeof docsSchema>>;
106
+ lines.push(
107
+ `export type Entry${capitalize(name)} = CollectionEntry<Record<string, unknown>>;`
108
+ );
109
+ }
110
+ lines.push("");
111
+ lines.push("export interface CollectionMap {");
112
+ for (const name of names) {
113
+ lines.push(` ${JSON.stringify(name)}: Collection<Record<string, unknown>>;`);
114
+ }
115
+ lines.push("}");
116
+ lines.push("");
117
+ lines.push(
118
+ `export type CollectionName = ${names.length > 0 ? names.map((n) => JSON.stringify(n)).join(" | ") : "never"};`
119
+ );
120
+ lines.push("");
121
+ return lines.join("\n");
122
+ }
123
+
124
+ /**
125
+ * Write the generated types file to disk. Creates the directory
126
+ * tree if necessary and is a no-op if the file already contains the
127
+ * exact same contents (prevents unnecessary filewatcher churn during
128
+ * `mandu dev`).
129
+ */
130
+ export function generateContentTypes(
131
+ collections: CollectionRegistry,
132
+ options: GenerateTypesOptions = {}
133
+ ): { outFile: string; wrote: boolean } {
134
+ const root = options.root ?? process.cwd();
135
+ const outFile = path.isAbsolute(options.outFile ?? "")
136
+ ? (options.outFile as string)
137
+ : path.resolve(
138
+ root,
139
+ options.outFile ?? ".mandu/generated/content-types.d.ts"
140
+ );
141
+ const banner = options.banner ?? DEFAULT_BANNER;
142
+ const body = renderContentTypes(collections);
143
+ const content = banner === DEFAULT_BANNER ? body : banner + "\n\n" + body.slice(body.indexOf("\n\n") + 2);
144
+
145
+ let existing: string | null = null;
146
+ try {
147
+ existing = fs.readFileSync(outFile, "utf8");
148
+ } catch {
149
+ existing = null;
150
+ }
151
+ if (existing === content) {
152
+ return { outFile, wrote: false };
153
+ }
154
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
155
+ fs.writeFileSync(outFile, content, "utf8");
156
+ return { outFile, wrote: true };
157
+ }
158
+
159
+ function capitalize(value: string): string {
160
+ // Collection names might contain dashes/underscores (`my-docs`),
161
+ // which are not valid identifiers. Strip non-word chars and
162
+ // PascalCase the remaining segments so `my-docs` becomes `MyDocs`.
163
+ return value
164
+ .split(/[^A-Za-z0-9]+/)
165
+ .filter((part) => part.length > 0)
166
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
167
+ .join("");
168
+ }
@@ -1,168 +1,206 @@
1
- /**
2
- * Mandu Content Layer
3
- *
4
- * Astro Content Layer에서 영감받은 빌드 타임 콘텐츠 로딩 시스템
5
- *
6
- * @example
7
- * ```ts
8
- * // content.config.ts
9
- * import { defineContentConfig, glob, file, api } from '@mandujs/core/content';
10
- * import { z } from 'zod';
11
- *
12
- * const postSchema = z.object({
13
- * title: z.string(),
14
- * date: z.coerce.date(),
15
- * tags: z.array(z.string()).default([]),
16
- * });
17
- *
18
- * export default defineContentConfig({
19
- * collections: {
20
- * posts: {
21
- * loader: glob({ pattern: 'content/posts/**\/*.md' }),
22
- * schema: postSchema,
23
- * },
24
- * settings: {
25
- * loader: file({ path: 'data/settings.json' }),
26
- * },
27
- * products: {
28
- * loader: api({ url: 'https://api.example.com/products' }),
29
- * },
30
- * },
31
- * });
32
- * ```
33
- *
34
- * ```ts
35
- * // 페이지에서 사용
36
- * import { getCollection, getEntry } from '@mandujs/core/content';
37
- *
38
- * const posts = await getCollection('posts');
39
- * const post = await getEntry('posts', 'hello-world');
40
- * ```
41
- */
42
-
43
- // ============================================================================
44
- // Core exports
45
- // ============================================================================
46
-
47
- export {
48
- ContentLayer,
49
- createContentLayer,
50
- getCollection,
51
- getEntry,
52
- setGlobalContentLayer,
53
- getGlobalContentLayer,
54
- } from "./content-layer";
55
-
56
- export type { ContentLayerOptions } from "./content-layer";
57
-
58
- // ============================================================================
59
- // Loaders
60
- // ============================================================================
61
-
62
- export { file, glob, api } from "./loaders";
63
-
64
- export type {
65
- Loader,
66
- FileLoaderOptions,
67
- GlobLoaderOptions,
68
- ApiLoaderOptions,
69
- PaginationConfig,
70
- ParsedMarkdown,
71
- LoaderEntry,
72
- } from "./loaders";
73
-
74
- // ============================================================================
75
- // Stores
76
- // ============================================================================
77
-
78
- export { ContentDataStore, createDataStore } from "./data-store";
79
- export type { DataStoreOptions } from "./data-store";
80
-
81
- export { ContentMetaStore, createMetaStore } from "./meta-store";
82
- export type { MetaStoreOptions } from "./meta-store";
83
-
84
- // ============================================================================
85
- // Utilities
86
- // ============================================================================
87
-
88
- export {
89
- generateDigest,
90
- generateFileDigest,
91
- combineDigests,
92
- digestsMatch,
93
- hasChanged,
94
- } from "./digest";
95
-
96
- export type { DigestOptions } from "./digest";
97
-
98
- export { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
99
- export type { CreateLoaderContextOptions } from "./loader-context";
100
-
101
- export { createContentWatcher } from "./watcher";
102
- export type { ContentWatcherOptions } from "./watcher";
103
-
104
- // ============================================================================
105
- // Types
106
- // ============================================================================
107
-
108
- export type {
109
- // Core types
110
- DataEntry,
111
- RenderedContent,
112
- ContentHeading,
113
- CollectionConfig,
114
- ContentConfig,
115
-
116
- // Loader types
117
- LoaderContext,
118
- ParseDataOptions,
119
-
120
- // Store interfaces
121
- DataStore,
122
- MetaStore,
123
-
124
- // Logger & Watcher
125
- ContentLogger,
126
- ContentWatcher,
127
-
128
- // Config
129
- ManduContentConfig,
130
-
131
- // Helper types
132
- InferEntryData,
133
- CollectionEntry,
134
- } from "./types";
135
-
136
- // Errors
137
- export {
138
- ContentError,
139
- LoaderError,
140
- ParseError,
141
- ValidationError,
142
- } from "./types";
143
-
144
- // ============================================================================
145
- // Config helper
146
- // ============================================================================
147
-
148
- /**
149
- * Content 설정 정의 헬퍼
150
- *
151
- * @example
152
- * ```ts
153
- * export default defineContentConfig({
154
- * collections: {
155
- * posts: { loader: glob({ pattern: 'content/posts/**\/*.md' }) },
156
- * },
157
- * });
158
- * ```
159
- */
160
- import type { CollectionConfig, ContentConfig as ContentConfigType } from "./types";
161
-
162
- export function defineContentConfig<T extends ContentConfigType>(config: T): T {
163
- return config;
164
- }
165
-
166
- export function defineCollection<T extends CollectionConfig>(config: T): T {
167
- return config;
168
- }
1
+ /**
2
+ * Mandu Content Layer
3
+ *
4
+ * Astro Content Layer에서 영감받은 빌드 타임 콘텐츠 로딩 시스템
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // content.config.ts
9
+ * import { defineContentConfig, glob, file, api } from '@mandujs/core/content';
10
+ * import { z } from 'zod';
11
+ *
12
+ * const postSchema = z.object({
13
+ * title: z.string(),
14
+ * date: z.coerce.date(),
15
+ * tags: z.array(z.string()).default([]),
16
+ * });
17
+ *
18
+ * export default defineContentConfig({
19
+ * collections: {
20
+ * posts: {
21
+ * loader: glob({ pattern: 'content/posts/**\/*.md' }),
22
+ * schema: postSchema,
23
+ * },
24
+ * settings: {
25
+ * loader: file({ path: 'data/settings.json' }),
26
+ * },
27
+ * products: {
28
+ * loader: api({ url: 'https://api.example.com/products' }),
29
+ * },
30
+ * },
31
+ * });
32
+ * ```
33
+ *
34
+ * ```ts
35
+ * // 페이지에서 사용
36
+ * import { getCollection, getEntry } from '@mandujs/core/content';
37
+ *
38
+ * const posts = await getCollection('posts');
39
+ * const post = await getEntry('posts', 'hello-world');
40
+ * ```
41
+ */
42
+
43
+ // ============================================================================
44
+ // Core exports
45
+ // ============================================================================
46
+
47
+ export {
48
+ ContentLayer,
49
+ createContentLayer,
50
+ getCollection,
51
+ getEntry,
52
+ setGlobalContentLayer,
53
+ getGlobalContentLayer,
54
+ } from "./content-layer";
55
+
56
+ export type { ContentLayerOptions } from "./content-layer";
57
+
58
+ // ============================================================================
59
+ // Loaders
60
+ // ============================================================================
61
+
62
+ export { file, glob, api } from "./loaders";
63
+
64
+ export type {
65
+ Loader,
66
+ FileLoaderOptions,
67
+ GlobLoaderOptions,
68
+ ApiLoaderOptions,
69
+ PaginationConfig,
70
+ ParsedMarkdown,
71
+ LoaderEntry,
72
+ } from "./loaders";
73
+
74
+ // ============================================================================
75
+ // Stores
76
+ // ============================================================================
77
+
78
+ export { ContentDataStore, createDataStore } from "./data-store";
79
+ export type { DataStoreOptions } from "./data-store";
80
+
81
+ export { ContentMetaStore, createMetaStore } from "./meta-store";
82
+ export type { MetaStoreOptions } from "./meta-store";
83
+
84
+ // ============================================================================
85
+ // Utilities
86
+ // ============================================================================
87
+
88
+ export {
89
+ generateDigest,
90
+ generateFileDigest,
91
+ combineDigests,
92
+ digestsMatch,
93
+ hasChanged,
94
+ } from "./digest";
95
+
96
+ export type { DigestOptions } from "./digest";
97
+
98
+ export { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
99
+ export type { CreateLoaderContextOptions } from "./loader-context";
100
+
101
+ export { createContentWatcher } from "./watcher";
102
+ export type { ContentWatcherOptions } from "./watcher";
103
+
104
+ // ============================================================================
105
+ // Types
106
+ // ============================================================================
107
+
108
+ export type {
109
+ // Core types
110
+ DataEntry,
111
+ RenderedContent,
112
+ ContentHeading,
113
+ CollectionConfig,
114
+ ContentConfig,
115
+
116
+ // Loader types
117
+ LoaderContext,
118
+ ParseDataOptions,
119
+
120
+ // Store interfaces
121
+ DataStore,
122
+ MetaStore,
123
+
124
+ // Logger & Watcher
125
+ ContentLogger,
126
+ ContentWatcher,
127
+
128
+ // Config
129
+ ManduContentConfig,
130
+
131
+ // Helper types
132
+ InferEntryData,
133
+ // Legacy ContentLayer CollectionEntry — the MVP Collection API
134
+ // (Issue #199) exports a different `CollectionEntry` shape further
135
+ // below ({ slug, data, content, filePath }). The MVP wins the
136
+ // unqualified name since it is the first-class public surface; the
137
+ // legacy ContentLayer alias is preserved here for back-compat.
138
+ CollectionEntry as LegacyCollectionEntry,
139
+ } from "./types";
140
+
141
+ // Errors
142
+ export {
143
+ ContentError,
144
+ LoaderError,
145
+ ParseError,
146
+ ValidationError,
147
+ } from "./types";
148
+
149
+ // ============================================================================
150
+ // Config helper
151
+ // ============================================================================
152
+
153
+ /**
154
+ * Content 설정 정의 헬퍼
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * export default defineContentConfig({
159
+ * collections: {
160
+ * posts: { loader: glob({ pattern: 'content/posts/**\/*.md' }) },
161
+ * },
162
+ * });
163
+ * ```
164
+ */
165
+ import type { ContentConfig as ContentConfigType } from "./types";
166
+
167
+ export function defineContentConfig<T extends ContentConfigType>(config: T): T {
168
+ return config;
169
+ }
170
+
171
+ // ============================================================================
172
+ // MVP Collection API (Issue #199)
173
+ // ============================================================================
174
+ //
175
+ // `defineCollection` now supports BOTH the legacy `{ loader, schema }` shape
176
+ // (for backwards compat with ContentLayer projects) and the new
177
+ // `{ path, schema, ... }` shape that returns a Collection instance with
178
+ // `.load()/.all()/.get()/.getCompiled()`. The overload dispatch lives in
179
+ // `./collection.ts` — it detects the shape at runtime via the presence
180
+ // of the `loader` property.
181
+
182
+ export { defineCollection, Collection } from "./collection";
183
+ export type {
184
+ CollectionEntry,
185
+ CompiledCollectionEntry,
186
+ CollectionSort,
187
+ DefineCollectionOptions,
188
+ } from "./collection";
189
+
190
+ export { z } from "./schema";
191
+ export type { ZodSchema, ZodError, ZodType, ZodTypeAny, Infer } from "./schema";
192
+
193
+ export { parseFrontmatter, parseSimpleYaml, parseScalar } from "./frontmatter";
194
+ export type { ParsedFrontmatter } from "./frontmatter";
195
+
196
+ export { slugFromPath } from "./slug";
197
+ export type { SlugFromPathOptions } from "./slug";
198
+
199
+ export { generateSidebar } from "./sidebar";
200
+ export type { SidebarNode, GenerateSidebarOptions } from "./sidebar";
201
+
202
+ export { generateLLMSTxt } from "./llms-txt";
203
+ export type { LLMSTxtInput, GenerateLLMSTxtOptions } from "./llms-txt";
204
+
205
+ export { generateContentTypes, renderContentTypes } from "./generate-types";
206
+ export type { CollectionRegistry, GenerateTypesOptions } from "./generate-types";