@mandujs/core 0.41.2 → 0.43.0

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 (88) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/event-bus.ts +2 -2
  64. package/src/observability/metrics.ts +334 -334
  65. package/src/observability/tracing.ts +694 -694
  66. package/src/openapi/generator.ts +1 -1
  67. package/src/perf/user-marks.ts +553 -553
  68. package/src/plugins/registry.ts +387 -387
  69. package/src/resource/ddl/diff.ts +392 -392
  70. package/src/resource/ddl/snapshot.ts +448 -447
  71. package/src/resource/generator-schema.ts +477 -476
  72. package/src/resource/parser.ts +4 -2
  73. package/src/resource/schema.ts +1 -1
  74. package/src/router/fs-patterns.ts +422 -422
  75. package/src/runtime/fast-refresh-types.ts +126 -128
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/router.test.ts +476 -476
  78. package/src/runtime/security.ts +155 -155
  79. package/src/runtime/server.ts +36 -19
  80. package/src/runtime/session-key.ts +328 -328
  81. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  82. package/src/seo/resolve/index.ts +353 -353
  83. package/src/spec/load.ts +1 -1
  84. package/src/testing/reporter.ts +676 -676
  85. package/src/testing/server.ts +196 -196
  86. package/src/testing/snapshot.ts +444 -444
  87. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  88. package/src/utils/bun.ts +8 -8
@@ -1,133 +1,133 @@
1
- /**
2
- * Content Digest - 변경 감지용 해시 생성
3
- *
4
- * 콘텐츠의 고유 다이제스트를 생성하여 변경 감지에 사용
5
- * xxHash 스타일의 빠른 해시 또는 crypto 기반 SHA-256 사용
6
- */
7
-
8
- import { createHash } from "crypto";
9
-
10
- /**
11
- * 다이제스트 옵션
12
- */
13
- export interface DigestOptions {
14
- /** 해시 알고리즘 (기본: sha256) */
15
- algorithm?: "sha256" | "md5" | "sha1";
16
- /** 출력 길이 (기본: 16) */
17
- length?: number;
18
- }
19
-
20
- const DEFAULT_OPTIONS: DigestOptions = {
21
- algorithm: "sha256",
22
- length: 16,
23
- };
24
-
25
- /**
26
- * 데이터에서 다이제스트 생성
27
- *
28
- * @example
29
- * ```ts
30
- * const digest = generateDigest({ title: "Hello", content: "World" });
31
- * // → "a1b2c3d4e5f6g7h8"
32
- * ```
33
- */
34
- export function generateDigest(data: unknown, options?: DigestOptions): string {
35
- const { algorithm, length } = { ...DEFAULT_OPTIONS, ...options };
36
-
37
- const serialized = stableStringify(data);
38
- const hash = createHash(algorithm!);
39
- hash.update(serialized);
40
-
41
- return hash.digest("hex").slice(0, length);
42
- }
43
-
44
- /**
45
- * 파일 내용에서 다이제스트 생성
46
- */
47
- export function generateFileDigest(content: string | Buffer, options?: DigestOptions): string {
48
- const { algorithm, length } = { ...DEFAULT_OPTIONS, ...options };
49
-
50
- const hash = createHash(algorithm!);
51
- hash.update(content);
52
-
53
- return hash.digest("hex").slice(0, length);
54
- }
55
-
56
- /**
57
- * 여러 소스를 결합한 다이제스트 생성
58
- *
59
- * @example
60
- * ```ts
61
- * const digest = combineDigests([
62
- * generateDigest(frontmatter),
63
- * generateFileDigest(body),
64
- * ]);
65
- * ```
66
- */
67
- export function combineDigests(digests: string[], options?: DigestOptions): string {
68
- const { algorithm, length } = { ...DEFAULT_OPTIONS, ...options };
69
-
70
- const combined = digests.join(":");
71
- const hash = createHash(algorithm!);
72
- hash.update(combined);
73
-
74
- return hash.digest("hex").slice(0, length);
75
- }
76
-
77
- /**
78
- * 안정적인 JSON 문자열화 (키 순서 일관성 보장)
79
- */
80
- function stableStringify(obj: unknown): string {
81
- if (obj === null || obj === undefined) {
82
- return String(obj);
83
- }
84
-
85
- if (typeof obj !== "object") {
86
- return JSON.stringify(obj);
87
- }
88
-
89
- if (Array.isArray(obj)) {
90
- return "[" + obj.map(stableStringify).join(",") + "]";
91
- }
92
-
93
- if (obj instanceof Date) {
94
- return JSON.stringify(obj.toISOString());
95
- }
96
-
97
- if (obj instanceof Map) {
98
- const entries = Array.from(obj.entries()).sort(([a], [b]) =>
99
- String(a).localeCompare(String(b))
100
- );
101
- return stableStringify(Object.fromEntries(entries));
102
- }
103
-
104
- if (obj instanceof Set) {
105
- return stableStringify(Array.from(obj).sort());
106
- }
107
-
108
- // 일반 객체: 키를 정렬하여 직렬화
109
- const keys = Object.keys(obj as Record<string, unknown>).sort();
110
- const pairs = keys.map(
111
- (key) => `${JSON.stringify(key)}:${stableStringify((obj as Record<string, unknown>)[key])}`
112
- );
113
-
114
- return "{" + pairs.join(",") + "}";
115
- }
116
-
117
- /**
118
- * 두 다이제스트 비교
119
- */
120
- export function digestsMatch(a: string | undefined, b: string | undefined): boolean {
121
- if (!a || !b) return false;
122
- return a === b;
123
- }
124
-
125
- /**
126
- * 다이제스트 기반 변경 감지
127
- */
128
- export function hasChanged(
129
- newDigest: string,
130
- oldDigest: string | undefined
131
- ): boolean {
132
- return !digestsMatch(newDigest, oldDigest);
133
- }
1
+ /**
2
+ * Content Digest - 변경 감지용 해시 생성
3
+ *
4
+ * 콘텐츠의 고유 다이제스트를 생성하여 변경 감지에 사용
5
+ * xxHash 스타일의 빠른 해시 또는 crypto 기반 SHA-256 사용
6
+ */
7
+
8
+ import { createHash } from "crypto";
9
+
10
+ /**
11
+ * 다이제스트 옵션
12
+ */
13
+ export interface DigestOptions {
14
+ /** 해시 알고리즘 (기본: sha256) */
15
+ algorithm?: "sha256" | "md5" | "sha1";
16
+ /** 출력 길이 (기본: 16) */
17
+ length?: number;
18
+ }
19
+
20
+ const DEFAULT_OPTIONS: DigestOptions = {
21
+ algorithm: "sha256",
22
+ length: 16,
23
+ };
24
+
25
+ /**
26
+ * 데이터에서 다이제스트 생성
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const digest = generateDigest({ title: "Hello", content: "World" });
31
+ * // → "a1b2c3d4e5f6g7h8"
32
+ * ```
33
+ */
34
+ export function generateDigest(data: unknown, options?: DigestOptions): string {
35
+ const { algorithm, length } = { ...DEFAULT_OPTIONS, ...options };
36
+
37
+ const serialized = stableStringify(data);
38
+ const hash = createHash(algorithm!);
39
+ hash.update(serialized);
40
+
41
+ return hash.digest("hex").slice(0, length);
42
+ }
43
+
44
+ /**
45
+ * 파일 내용에서 다이제스트 생성
46
+ */
47
+ export function generateFileDigest(content: string | Buffer, options?: DigestOptions): string {
48
+ const { algorithm, length } = { ...DEFAULT_OPTIONS, ...options };
49
+
50
+ const hash = createHash(algorithm!);
51
+ hash.update(content);
52
+
53
+ return hash.digest("hex").slice(0, length);
54
+ }
55
+
56
+ /**
57
+ * 여러 소스를 결합한 다이제스트 생성
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const digest = combineDigests([
62
+ * generateDigest(frontmatter),
63
+ * generateFileDigest(body),
64
+ * ]);
65
+ * ```
66
+ */
67
+ export function combineDigests(digests: string[], options?: DigestOptions): string {
68
+ const { algorithm, length } = { ...DEFAULT_OPTIONS, ...options };
69
+
70
+ const combined = digests.join(":");
71
+ const hash = createHash(algorithm!);
72
+ hash.update(combined);
73
+
74
+ return hash.digest("hex").slice(0, length);
75
+ }
76
+
77
+ /**
78
+ * 안정적인 JSON 문자열화 (키 순서 일관성 보장)
79
+ */
80
+ function stableStringify(obj: unknown): string {
81
+ if (obj === null || obj === undefined) {
82
+ return String(obj);
83
+ }
84
+
85
+ if (typeof obj !== "object") {
86
+ return JSON.stringify(obj);
87
+ }
88
+
89
+ if (Array.isArray(obj)) {
90
+ return "[" + obj.map(stableStringify).join(",") + "]";
91
+ }
92
+
93
+ if (obj instanceof Date) {
94
+ return JSON.stringify(obj.toISOString());
95
+ }
96
+
97
+ if (obj instanceof Map) {
98
+ const entries = Array.from(obj.entries()).sort(([a], [b]) =>
99
+ String(a).localeCompare(String(b))
100
+ );
101
+ return stableStringify(Object.fromEntries(entries));
102
+ }
103
+
104
+ if (obj instanceof Set) {
105
+ return stableStringify(Array.from(obj).sort());
106
+ }
107
+
108
+ // 일반 객체: 키를 정렬하여 직렬화
109
+ const keys = Object.keys(obj as Record<string, unknown>).sort();
110
+ const pairs = keys.map(
111
+ (key) => `${JSON.stringify(key)}:${stableStringify((obj as Record<string, unknown>)[key])}`
112
+ );
113
+
114
+ return "{" + pairs.join(",") + "}";
115
+ }
116
+
117
+ /**
118
+ * 두 다이제스트 비교
119
+ */
120
+ export function digestsMatch(a: string | undefined, b: string | undefined): boolean {
121
+ if (!a || !b) return false;
122
+ return a === b;
123
+ }
124
+
125
+ /**
126
+ * 다이제스트 기반 변경 감지
127
+ */
128
+ export function hasChanged(
129
+ newDigest: string,
130
+ oldDigest: string | undefined
131
+ ): boolean {
132
+ return !digestsMatch(newDigest, oldDigest);
133
+ }
@@ -1,168 +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
+ /**
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
+ }
@@ -179,7 +179,12 @@ export function defineContentConfig<T extends ContentConfigType>(config: T): T {
179
179
  // `./collection.ts` — it detects the shape at runtime via the presence
180
180
  // of the `loader` property.
181
181
 
182
- export { defineCollection, Collection } from "./collection";
182
+ export {
183
+ defineCollection,
184
+ Collection,
185
+ getRegisteredCollections,
186
+ invalidateAllCollections,
187
+ } from "./collection";
183
188
  export type {
184
189
  CollectionEntry,
185
190
  CompiledCollectionEntry,