@contentrain/query 7.3.7 → 7.4.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.
- package/README.md +51 -8
- package/dist/astro/index.cjs +30 -2
- package/dist/astro/index.d.cts +9 -0
- package/dist/astro/index.d.cts.map +1 -1
- package/dist/astro/index.d.mts +10 -1
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +30 -2
- package/dist/astro/index.mjs.map +1 -1
- package/dist/cdn/index.d.mts +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{frontmatter-BUaCvV-s.mjs → frontmatter-D9yVqC5y.mjs} +2 -2
- package/dist/{frontmatter-BUaCvV-s.mjs.map → frontmatter-D9yVqC5y.mjs.map} +1 -1
- package/dist/{generate-CdLXEj13.mjs → generate-BOi6o7Z5.mjs} +27 -14
- package/dist/generate-BOi6o7Z5.mjs.map +1 -0
- package/dist/{generate-oSHTaAuy.cjs → generate-D4NDHZ_K.cjs} +26 -13
- package/dist/generator/generate.cjs +1 -1
- package/dist/generator/generate.d.cts +10 -5
- package/dist/generator/generate.d.cts.map +1 -1
- package/dist/generator/generate.d.mts +11 -6
- package/dist/generator/generate.d.mts.map +1 -1
- package/dist/generator/generate.mjs +1 -1
- package/dist/{index-DWpmPtUW.d.mts → index-ClftI9li.d.mts} +1 -1
- package/dist/{index-DWpmPtUW.d.mts.map → index-ClftI9li.d.mts.map} +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +18 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +19 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/media-HEEDdrFS.mjs +27 -0
- package/dist/media-HEEDdrFS.mjs.map +1 -0
- package/dist/media-o6KezVYy.cjs +36 -0
- package/dist/{publication-Ba8bLPAO.d.mts → publication-CMrWLPi7.d.mts} +1 -1
- package/dist/publication-CMrWLPi7.d.mts.map +1 -0
- package/package.json +2 -2
- package/dist/generate-CdLXEj13.mjs.map +0 -1
- package/dist/publication-Ba8bLPAO.d.mts.map +0 -1
- package/dist/{frontmatter-DERIsjeG.cjs → frontmatter-DSpE3so9.cjs} +1 -1
package/README.md
CHANGED
|
@@ -159,18 +159,47 @@ Supported methods:
|
|
|
159
159
|
- `first()`
|
|
160
160
|
- `all()`
|
|
161
161
|
|
|
162
|
-
### `media(value)`
|
|
162
|
+
### `media(value, baseOverride?)` / `mediaBody(markdown, baseOverride?)`
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
Resolve a stored `media/...` path — `media()` for a single field value, `mediaBody()` for every `](media/...)` link and image embed inside a markdown string — to an absolute delivery URL. Emitted **only** when a media base is configured at generate time — set `cdn.url` in `.contentrain/config.json` or run `contentrain generate --mediaBaseUrl <base>` (`--cdnBaseUrl` still works as a deprecated alias). With no base configured, these are not emitted at all — not a pass-through, an absent export — so code that imports them from `#contentrain` without generating with a base will fail to resolve at build time:
|
|
165
165
|
|
|
166
166
|
```ts
|
|
167
|
-
import { media } from '#contentrain'
|
|
167
|
+
import { media, mediaBody } from '#contentrain'
|
|
168
168
|
|
|
169
|
-
media('media/original/hero.webp') // → '{
|
|
169
|
+
media('media/original/hero.webp') // → '{mediaBaseUrl}/media/original/hero.webp'
|
|
170
170
|
media('https://images.unsplash.com/x.jpg') // → unchanged (external pass-through)
|
|
171
|
+
|
|
172
|
+
mediaBody('See  for more.')
|
|
173
|
+
// → 'See  for more.'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Both are idempotent — external URLs (`http(s)://`, `//`, `data:`) and already-absolute delivery URLs pass through untouched — and take an optional second argument that overrides the base baked in at generate time for that one call. That is how a Nuxt app stays on one build across environments: read the base from its own runtime config, not from what `contentrain generate` happened to bake in.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// Nuxt: nuxt.config.ts
|
|
180
|
+
export default defineNuxtConfig({
|
|
181
|
+
runtimeConfig: {
|
|
182
|
+
public: { contentrainMediaBaseUrl: '' }, // filled from NUXT_PUBLIC_CONTENTRAIN_MEDIA_BASE_URL
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Nuxt: a composable or component
|
|
187
|
+
const { public: { contentrainMediaBaseUrl } } = useRuntimeConfig()
|
|
188
|
+
const cover = media(post.cover, contentrainMediaBaseUrl)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This is the local-mode counterpart of CDN mode's `MediaAccessor.url()`. For Studio-CDN content, media fields already carry absolute URLs, so no resolution is needed.
|
|
192
|
+
|
|
193
|
+
For a Nuxt app (or anything else) that never bakes a base into the generated client at all — reading it purely from its own runtime config or env every time, so `media()`/`mediaBody()` are never emitted — import the underlying pure resolvers directly instead of going through the generated client:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { resolveMediaUrl, resolveMediaRefsInBody } from '@contentrain/query'
|
|
197
|
+
|
|
198
|
+
const cover = resolveMediaUrl(post.cover, useRuntimeConfig().public.contentrainMediaBaseUrl)
|
|
199
|
+
const body = resolveMediaRefsInBody(post.body, useRuntimeConfig().public.contentrainMediaBaseUrl)
|
|
171
200
|
```
|
|
172
201
|
|
|
173
|
-
|
|
202
|
+
Both are opt-in: called with no base (`undefined`, `null`, or `''`), they return the value unchanged, so a project that never configures one keeps relative `media/...` paths exactly as stored.
|
|
174
203
|
|
|
175
204
|
## 🔗 Relations
|
|
176
205
|
|
|
@@ -211,6 +240,7 @@ Public root exports:
|
|
|
211
240
|
- `ConversationClient` — Conversation API client
|
|
212
241
|
- `ContentrainError` — HTTP error class for CDN mode
|
|
213
242
|
- `applyWhere` — shared where filter helper
|
|
243
|
+
- `resolveMediaUrl`, `resolveMediaRefsInBody` — the media resolver behind `media()`/`mediaBody()`/`contentrainLoader`'s `mediaBaseUrl`, usable standalone with a base sourced from anywhere (a framework's own runtime config, an env var)
|
|
214
244
|
|
|
215
245
|
## CDN Transport
|
|
216
246
|
|
|
@@ -476,6 +506,18 @@ loads with ids prefixed (`en/my-post`) — Astro ids are unique per collection,
|
|
|
476
506
|
without the prefix one language would overwrite the other. `root` points at the
|
|
477
507
|
project holding `.contentrain` (default: `process.cwd()`).
|
|
478
508
|
|
|
509
|
+
This loader reads `.contentrain` directly, so nothing upstream has rewritten a
|
|
510
|
+
stored `media/...` reference into an absolute URL yet — pass `mediaBaseUrl` to
|
|
511
|
+
resolve it here, the same opt-in resolver `media()`/`mediaBody()` use:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
contentrainLoader({ model: 'blog-post', locale: 'en', mediaBaseUrl: 'https://cdn.example.com' })
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Applied automatically to `image`/`video`/`file` fields, to `](media/...)`
|
|
518
|
+
references inside `markdown`/`richtext` fields, and to a document's markdown
|
|
519
|
+
body. Omit it to keep values exactly as stored.
|
|
520
|
+
|
|
479
521
|
Astro is not a dependency, not even a peer: the loader is structurally what
|
|
480
522
|
`defineCollection` accepts, so any Astro 5 version works and non-Astro consumers
|
|
481
523
|
of this package carry nothing extra.
|
|
@@ -496,9 +538,10 @@ const hero = client.singleton('hero').get()
|
|
|
496
538
|
**Via the `contentrain` CLI (recommended for most users):**
|
|
497
539
|
|
|
498
540
|
```bash
|
|
499
|
-
contentrain generate
|
|
500
|
-
contentrain generate --watch
|
|
501
|
-
contentrain generate --json
|
|
541
|
+
contentrain generate # Generate once
|
|
542
|
+
contentrain generate --watch # Regenerate on model/content changes
|
|
543
|
+
contentrain generate --json # Machine-readable JSON for CI
|
|
544
|
+
contentrain generate --mediaBaseUrl https://cdn.example.com # Bake media()/mediaBody() into the client
|
|
502
545
|
```
|
|
503
546
|
|
|
504
547
|
**Via `contentrain-query` (programmatic / build tool flows):**
|
package/dist/astro/index.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_media = require("../media-o6KezVYy.cjs");
|
|
3
|
+
const require_frontmatter = require("../frontmatter-DSpE3so9.cjs");
|
|
4
|
+
let _contentrain_types = require("@contentrain/types");
|
|
3
5
|
let node_path = require("node:path");
|
|
4
6
|
let node_url = require("node:url");
|
|
5
7
|
//#region src/astro/loader.ts
|
|
@@ -39,7 +41,8 @@ function contentrainLoader(options) {
|
|
|
39
41
|
if (options.locale !== void 0 && refs.length === 0 && model.i18n) context.logger?.warn(`contentrainLoader: model "${model.id}" has no content for locale "${options.locale}" — collection is empty.`);
|
|
40
42
|
const prefixLocale = model.i18n === true && options.locale === void 0;
|
|
41
43
|
const publication = require_frontmatter.publicationContext(root, manifest.config.locales.default, options);
|
|
42
|
-
const
|
|
44
|
+
const perFile = await Promise.all(refs.map((ref) => entriesOf(ref, model, prefixLocale, publication)));
|
|
45
|
+
const entries = options.mediaBaseUrl ? resolveEntriesMedia(perFile.flat(), model, options.mediaBaseUrl) : perFile.flat();
|
|
43
46
|
const siteRoot = toPath(context.config?.root);
|
|
44
47
|
context.store.clear();
|
|
45
48
|
for (const entry of entries) {
|
|
@@ -83,6 +86,31 @@ function toPath(root) {
|
|
|
83
86
|
if (typeof root !== "string") return (0, node_url.fileURLToPath)(root);
|
|
84
87
|
return root.startsWith("file:") ? (0, node_url.fileURLToPath)(root) : root;
|
|
85
88
|
}
|
|
89
|
+
const MEDIA_BODY_TYPES = new Set(["markdown", "richtext"]);
|
|
90
|
+
function fieldNamesOfType(model, types) {
|
|
91
|
+
if (!model.fields) return [];
|
|
92
|
+
return Object.entries(model.fields).filter(([, f]) => types.has(f.type)).map(([name]) => name);
|
|
93
|
+
}
|
|
94
|
+
/** Resolves `media/...` references to absolute URLs — image/video/file field
|
|
95
|
+
* values (including those nested inside object/array fields, via the shared
|
|
96
|
+
* `@contentrain/types` engine), `](media/...)` links and inline HTML
|
|
97
|
+
* src/href inside markdown/richtext fields, and a document's markdown body —
|
|
98
|
+
* across every loaded entry. A model with no fields and no body is returned
|
|
99
|
+
* unchanged, entry by entry. */
|
|
100
|
+
function resolveEntriesMedia(entries, model, mediaBaseUrl) {
|
|
101
|
+
const bodyFields = fieldNamesOfType(model, MEDIA_BODY_TYPES);
|
|
102
|
+
if (!model.fields && model.kind !== "document") return entries;
|
|
103
|
+
return entries.map((entry) => {
|
|
104
|
+
const data = model.fields ? (0, _contentrain_types.rewriteEntryMedia)(entry.data, model.fields, mediaBaseUrl) : entry.data;
|
|
105
|
+
for (const field of bodyFields) if (field in data) data[field] = require_media.resolveMediaRefsInBody(data[field], mediaBaseUrl);
|
|
106
|
+
const body = entry.body === void 0 ? void 0 : require_media.resolveMediaRefsInBody(entry.body, mediaBaseUrl);
|
|
107
|
+
return {
|
|
108
|
+
...entry,
|
|
109
|
+
data,
|
|
110
|
+
...body === void 0 ? {} : { body }
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
86
114
|
async function entriesOf(ref, model, prefixLocale, publication) {
|
|
87
115
|
const meta = publication ? await require_frontmatter.publicationMeta(ref, model, publication) : void 0;
|
|
88
116
|
const visible = (id) => !publication || require_frontmatter.isPublishedAt(id === void 0 ? meta : meta?.[id], publication.at);
|
package/dist/astro/index.d.cts
CHANGED
|
@@ -56,6 +56,15 @@ interface ContentrainLoaderOptions extends PublicationOptions {
|
|
|
56
56
|
* post do not collide — Astro ids are unique per collection.
|
|
57
57
|
*/
|
|
58
58
|
locale?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Public media delivery base for resolving relative `media/...` references
|
|
61
|
+
* to absolute URLs. This loader reads `.contentrain` directly — nothing
|
|
62
|
+
* upstream has rewritten them yet — so, unlike CDN-bundled content, they
|
|
63
|
+
* need a resolver here. Applied automatically to image/video/file fields,
|
|
64
|
+
* to `](media/...)` references inside markdown/richtext fields, and to a
|
|
65
|
+
* document's markdown body. Omit to keep values exactly as stored.
|
|
66
|
+
*/
|
|
67
|
+
mediaBaseUrl?: string;
|
|
59
68
|
}
|
|
60
69
|
/**
|
|
61
70
|
* A loader for one Contentrain model.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;;;UAyBiB,wBAAA;EACf,KAAA;IACE,KAAA;IACA,GAAA,GAAM,KAAA;MACJ,EAAA;MACA,IAAA,EAAM,MAAA;MACN,IAAA;MACA,MAAA;MACA,QAAA;QAAa,IAAA;QAAc,QAAA,GAAW,MAAA;MAAA;MACtC,QAAA;IAAA;EAAA;EAGJ,MAAA;IAAW,IAAA,GAAO,GAAA;IAAsB,IAAA,GAAO,GAAA;EAAA;EAPrC;EASV,MAAA;IAAW,IAAA,GAAO,GAAA;EAAA;EAClB,cAAA,IAAkB,IAAA;EAClB,SAAA,IAAa,KAAA;IAAS,EAAA;IAAY,IAAA,EAAM,MAAA;IAAyB,QAAA;EAAA,MAAwB,OAAA,CAAQ,MAAA;EACjG,cAAA,IAAkB,OAAA,aAAoB,OAAA;IAAU,IAAA;IAAc,QAAA,GAAW,MAAA;EAAA;EACzE,OAAA;IAAY,GAAA,GAAM,IAAA;IAAuB,EAAA,GAAK,KAAA,UAAe,EAAA,GAAK,IAAA;EAAA;AAAA;;UAInD,iBAAA;EACf,IAAA;EACA,IAAA,GAAO,OAAA,EAAS,wBAAA,KAA6B,OAAA;AAAA;AAAA,UAG9B,wBAAA,SAAiC,kBAAA;EAXyC;EAazF,KAAA;EAZA;EAcA,IAAA;EAdsC;;;;;EAoBtC,MAAA;EAnBkB;;;;;;;AAIpB;EAwBE,YAAA;AAAA;;;;;;;;;AAnBF;;;;;;;;;;;AAiDA;iBAAgB,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,iBAAA"}
|
package/dist/astro/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as PublicationOptions } from "../publication-
|
|
1
|
+
import { t as PublicationOptions } from "../publication-CMrWLPi7.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/astro/loader.d.ts
|
|
4
4
|
/** The slice of Astro's `LoaderContext` this loader uses. */
|
|
@@ -56,6 +56,15 @@ interface ContentrainLoaderOptions extends PublicationOptions {
|
|
|
56
56
|
* post do not collide — Astro ids are unique per collection.
|
|
57
57
|
*/
|
|
58
58
|
locale?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Public media delivery base for resolving relative `media/...` references
|
|
61
|
+
* to absolute URLs. This loader reads `.contentrain` directly — nothing
|
|
62
|
+
* upstream has rewritten them yet — so, unlike CDN-bundled content, they
|
|
63
|
+
* need a resolver here. Applied automatically to image/video/file fields,
|
|
64
|
+
* to `](media/...)` references inside markdown/richtext fields, and to a
|
|
65
|
+
* document's markdown body. Omit to keep values exactly as stored.
|
|
66
|
+
*/
|
|
67
|
+
mediaBaseUrl?: string;
|
|
59
68
|
}
|
|
60
69
|
/**
|
|
61
70
|
* A loader for one Contentrain model.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;;;UAyBiB,wBAAA;EACf,KAAA;IACE,KAAA;IACA,GAAA,GAAM,KAAA;MACJ,EAAA;MACA,IAAA,EAAM,MAAA;MACN,IAAA;MACA,MAAA;MACA,QAAA;QAAa,IAAA;QAAc,QAAA,GAAW,MAAA;MAAA;MACtC,QAAA;IAAA;EAAA;EAGJ,MAAA;IAAW,IAAA,GAAO,GAAA;IAAsB,IAAA,GAAO,GAAA;EAAA;EAPrC;EASV,MAAA;IAAW,IAAA,GAAO,GAAA;EAAA;EAClB,cAAA,IAAkB,IAAA;EAClB,SAAA,IAAa,KAAA;IAAS,EAAA;IAAY,IAAA,EAAM,MAAA;IAAyB,QAAA;EAAA,MAAwB,OAAA,CAAQ,MAAA;EACjG,cAAA,IAAkB,OAAA,aAAoB,OAAA;IAAU,IAAA;IAAc,QAAA,GAAW,MAAA;EAAA;EACzE,OAAA;IAAY,GAAA,GAAM,IAAA;IAAuB,EAAA,GAAK,KAAA,UAAe,EAAA,GAAK,IAAA;EAAA;AAAA;;UAInD,iBAAA;EACf,IAAA;EACA,IAAA,GAAO,OAAA,EAAS,wBAAA,KAA6B,OAAA;AAAA;AAAA,UAG9B,wBAAA,SAAiC,kBAAA;EAXyC;EAazF,KAAA;EAZA;EAcA,IAAA;EAdsC;;;;;EAoBtC,MAAA;EAnBkB;;;;;;;AAIpB;EAwBE,YAAA;AAAA;;;;;;;;;AAnBF;;;;;;;;;;;AAiDA;iBAAgB,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,iBAAA"}
|
package/dist/astro/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as resolveMediaRefsInBody } from "../media-HEEDdrFS.mjs";
|
|
2
|
+
import { a as readJson, c as isPublishedAt, l as publicationContext, n as stringLikeFieldKeys, o as readText, r as readProjectManifest, t as parseFrontmatter, u as publicationMeta } from "../frontmatter-D9yVqC5y.mjs";
|
|
3
|
+
import { rewriteEntryMedia } from "@contentrain/types";
|
|
2
4
|
import { isAbsolute, join, relative } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
//#region src/astro/loader.ts
|
|
@@ -38,7 +40,8 @@ function contentrainLoader(options) {
|
|
|
38
40
|
if (options.locale !== void 0 && refs.length === 0 && model.i18n) context.logger?.warn(`contentrainLoader: model "${model.id}" has no content for locale "${options.locale}" — collection is empty.`);
|
|
39
41
|
const prefixLocale = model.i18n === true && options.locale === void 0;
|
|
40
42
|
const publication = publicationContext(root, manifest.config.locales.default, options);
|
|
41
|
-
const
|
|
43
|
+
const perFile = await Promise.all(refs.map((ref) => entriesOf(ref, model, prefixLocale, publication)));
|
|
44
|
+
const entries = options.mediaBaseUrl ? resolveEntriesMedia(perFile.flat(), model, options.mediaBaseUrl) : perFile.flat();
|
|
42
45
|
const siteRoot = toPath(context.config?.root);
|
|
43
46
|
context.store.clear();
|
|
44
47
|
for (const entry of entries) {
|
|
@@ -82,6 +85,31 @@ function toPath(root) {
|
|
|
82
85
|
if (typeof root !== "string") return fileURLToPath(root);
|
|
83
86
|
return root.startsWith("file:") ? fileURLToPath(root) : root;
|
|
84
87
|
}
|
|
88
|
+
const MEDIA_BODY_TYPES = new Set(["markdown", "richtext"]);
|
|
89
|
+
function fieldNamesOfType(model, types) {
|
|
90
|
+
if (!model.fields) return [];
|
|
91
|
+
return Object.entries(model.fields).filter(([, f]) => types.has(f.type)).map(([name]) => name);
|
|
92
|
+
}
|
|
93
|
+
/** Resolves `media/...` references to absolute URLs — image/video/file field
|
|
94
|
+
* values (including those nested inside object/array fields, via the shared
|
|
95
|
+
* `@contentrain/types` engine), `](media/...)` links and inline HTML
|
|
96
|
+
* src/href inside markdown/richtext fields, and a document's markdown body —
|
|
97
|
+
* across every loaded entry. A model with no fields and no body is returned
|
|
98
|
+
* unchanged, entry by entry. */
|
|
99
|
+
function resolveEntriesMedia(entries, model, mediaBaseUrl) {
|
|
100
|
+
const bodyFields = fieldNamesOfType(model, MEDIA_BODY_TYPES);
|
|
101
|
+
if (!model.fields && model.kind !== "document") return entries;
|
|
102
|
+
return entries.map((entry) => {
|
|
103
|
+
const data = model.fields ? rewriteEntryMedia(entry.data, model.fields, mediaBaseUrl) : entry.data;
|
|
104
|
+
for (const field of bodyFields) if (field in data) data[field] = resolveMediaRefsInBody(data[field], mediaBaseUrl);
|
|
105
|
+
const body = entry.body === void 0 ? void 0 : resolveMediaRefsInBody(entry.body, mediaBaseUrl);
|
|
106
|
+
return {
|
|
107
|
+
...entry,
|
|
108
|
+
data,
|
|
109
|
+
...body === void 0 ? {} : { body }
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
85
113
|
async function entriesOf(ref, model, prefixLocale, publication) {
|
|
86
114
|
const meta = publication ? await publicationMeta(ref, model, publication) : void 0;
|
|
87
115
|
const visible = (id) => !publication || isPublishedAt(id === void 0 ? meta : meta?.[id], publication.at);
|
package/dist/astro/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/astro/loader.ts"],"sourcesContent":["// Astro content-layer loader for a `.contentrain` project.\n//\n// `contentrain generate` produces a typed client for application code; Astro\n// wants its content in the content layer instead, so pages can query it with\n// `getCollection()` and render documents with `<Content />`. This loader reads\n// the same `.contentrain` files the generator reads — through the same manifest\n// — so a project never has two answers about what its content is.\n//\n// Astro is NOT a dependency here, not even a peer: the returned object is\n// structurally what `defineCollection({ loader })` accepts, and the context is\n// typed by the small surface this loader actually uses. That keeps\n// @contentrain/query dependency-free and usable from any Astro 5 version.\n\nimport { publicationContext, publicationMeta, isPublishedAt, type PublicationOptions, type PublicationContext } from '../generator/publication.js'\nimport type { ModelDefinition } from '@contentrain/types'\nimport { isAbsolute, join, relative } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { ContentFileRef } from '../generator/config-reader.js'\nimport { readProjectManifest } from '../generator/config-reader.js'\nimport { readJson, readText } from '../generator/utils.js'\nimport { parseFrontmatter, stringLikeFieldKeys } from '../shared/frontmatter.js'\n\n/** The slice of Astro's `LoaderContext` this loader uses. */\nexport interface ContentrainLoaderContext {\n store: {\n clear: () => void\n set: (entry: {\n id: string\n data: Record<string, unknown>\n body?: string\n digest?: string\n rendered?: { html: string, metadata?: Record<string, unknown> }\n filePath?: string\n }) => void\n }\n logger?: { info: (msg: string) => void, warn: (msg: string) => void }\n /** Astro's resolved config; only `root` is read, to make file paths relative. */\n config?: { root?: URL | string }\n generateDigest?: (data: unknown) => string\n parseData?: (entry: { id: string, data: Record<string, unknown>, filePath?: string }) => Promise<Record<string, unknown>>\n renderMarkdown?: (content: string) => Promise<{ html: string, metadata?: Record<string, unknown> }>\n watcher?: { add: (path: string) => void, on: (event: string, cb: (path: string) => void) => void }\n}\n\n/** Structurally Astro's `Loader`. */\nexport interface ContentrainLoader {\n name: string\n load: (context: ContentrainLoaderContext) => Promise<void>\n}\n\nexport interface ContentrainLoaderOptions extends PublicationOptions {\n /** Model id — the `id` in `.contentrain/models/<id>.json`. */\n model: string\n /** Project root holding `.contentrain`. Default: `process.cwd()`. */\n root?: string\n /**\n * Load only this locale. Omitted on an i18n model, every supported locale is\n * loaded and entry ids are prefixed (`en/my-post`) so the two languages of one\n * post do not collide — Astro ids are unique per collection.\n */\n locale?: string\n}\n\ninterface Entry {\n id: string\n data: Record<string, unknown>\n body?: string\n filePath: string\n}\n\n/**\n * A loader for one Contentrain model.\n *\n * ```ts\n * // src/content.config.ts\n * import { defineCollection } from 'astro:content'\n * import { contentrainLoader } from '@contentrain/query/astro'\n *\n * export const collections = {\n * posts: defineCollection({ loader: contentrainLoader({ model: 'blog-post', locale: 'en' }) }),\n * articles: defineCollection({ loader: contentrainLoader({ model: 'blog-article' }) }),\n * }\n * ```\n *\n * Each model kind maps to the shape Astro can actually use:\n * collection → one entry per record · document → one entry per file, markdown\n * in `body` (and rendered, so `<Content />` works) · dictionary → one entry per\n * key, because a single blob would make `getEntry()` useless · singleton → one\n * entry, id `<model>` or the locale.\n */\nexport function contentrainLoader(options: ContentrainLoaderOptions): ContentrainLoader {\n const root = options.root ?? process.cwd()\n let watching = false\n\n return {\n name: `contentrain:${options.model}`,\n async load(context) {\n const manifest = await readProjectManifest(root)\n const model = manifest.models.find(m => m.id === options.model)\n if (!model) {\n const known = manifest.models.map(m => m.id).join(', ') || 'none'\n throw new Error(\n `contentrainLoader: model \"${options.model}\" not found in ${join(root, '.contentrain', 'models')}. Models here: ${known}.`,\n )\n }\n\n const refs = manifest.contentFiles.filter(ref =>\n ref.modelId === model.id && (options.locale === undefined || ref.locale === options.locale),\n )\n if (options.locale !== undefined && refs.length === 0 && model.i18n) {\n context.logger?.warn(\n `contentrainLoader: model \"${model.id}\" has no content for locale \"${options.locale}\" — collection is empty.`,\n )\n }\n // Prefix ids only when one collection really holds several languages;\n // a single-locale load keeps the natural id so routes stay clean.\n const prefixLocale = model.i18n === true && options.locale === undefined\n\n const publication = publicationContext(root, manifest.config.locales.default, options)\n const perFile = await Promise.all(refs.map(ref => entriesOf(ref, model, prefixLocale, publication)))\n const entries = perFile.flat()\n // Astro rejects an absolute filePath (\"must be relative to the site\n // root\"), and a `.contentrain` outside the Astro root has no relative\n // form it accepts — there we send none rather than a path it refuses.\n const siteRoot = toPath(context.config?.root)\n\n context.store.clear()\n for (const entry of entries) {\n const data = context.parseData\n ? await context.parseData({ id: entry.id, data: entry.data, filePath: entry.filePath })\n : entry.data\n const filePath = siteRoot === undefined ? undefined : relative(siteRoot, entry.filePath)\n context.store.set({\n id: entry.id,\n data,\n ...(filePath === undefined || filePath.startsWith('..') || isAbsolute(filePath) ? {} : { filePath }),\n ...(entry.body === undefined ? {} : { body: entry.body }),\n ...(context.generateDigest ? { digest: context.generateDigest(entry.data) } : {}),\n ...(entry.body !== undefined && context.renderMarkdown\n ? { rendered: await context.renderMarkdown(entry.body) }\n : {}),\n })\n }\n context.logger?.info(`Loaded ${entries.length} ${model.kind} ${entries.length === 1 ? 'entry' : 'entries'} from ${model.id}`)\n\n // Dev server: reload when the files behind this collection change. The\n // listener is registered once — `load` runs again on every change, and a\n // listener per run would multiply the work on every save.\n if (context.watcher && !watching) {\n watching = true\n const paths = new Set(refs.map(ref => ref.filePath))\n for (const path of paths) context.watcher.add(path)\n const metaDir = join(root, '.contentrain', 'meta', model.id)\n context.watcher.add(metaDir)\n context.watcher.add(join(root, '.contentrain', 'models', `${model.id}.json`))\n const reload = (path: string) => {\n if (!paths.has(path) && !path.startsWith(metaDir + '/') && !path.endsWith(`${model.id}.json`)) return\n void this.load(context)\n }\n context.watcher.on('change', reload)\n context.watcher.on('add', reload)\n context.watcher.on('unlink', reload)\n }\n },\n }\n}\n\n/** Astro hands `config.root` as a file URL; tests and other hosts may use a path. */\nfunction toPath(root: URL | string | undefined): string | undefined {\n if (root === undefined) return undefined\n if (typeof root !== 'string') return fileURLToPath(root)\n return root.startsWith('file:') ? fileURLToPath(root) : root\n}\n\nasync function entriesOf(ref: ContentFileRef, model: ModelDefinition, prefixLocale: boolean, publication?: PublicationContext): Promise<Entry[]> {\n const meta = publication ? await publicationMeta(ref, model, publication) : undefined\n const visible = (id?: string) => !publication || isPublishedAt(id === undefined ? meta : meta?.[id], publication.at)\n if ((model.kind === 'singleton' || model.kind === 'document') && !visible()) return []\n const prefix = prefixLocale && ref.locale ? `${ref.locale}/` : ''\n const withLocale = (data: Record<string, unknown>): Record<string, unknown> =>\n ref.locale === null ? data : { ...data, locale: ref.locale }\n\n switch (ref.kind) {\n case 'collection': {\n const raw = await readJson<Record<string, Record<string, unknown>>>(ref.filePath)\n if (!raw) return []\n return Object.entries(raw)\n .filter(([id]) => visible(id))\n .toSorted(([a], [b]) => a.localeCompare(b, 'en'))\n .map(([id, fields]) => ({\n id: `${prefix}${id}`,\n data: withLocale({ id, ...fields }),\n filePath: ref.filePath,\n }))\n }\n\n case 'singleton': {\n const raw = await readJson<Record<string, unknown>>(ref.filePath)\n if (!raw) return []\n return [{\n id: prefix ? `${ref.locale}` : model.id,\n data: withLocale(raw),\n filePath: ref.filePath,\n }]\n }\n\n case 'dictionary': {\n // One entry per key: a dictionary key is a semantic address, and a single\n // blob entry would make getEntry() and reference() useless on it.\n const raw = await readJson<Record<string, string>>(ref.filePath)\n if (!raw) return []\n return Object.entries(raw)\n .filter(([id]) => visible(id))\n .toSorted(([a], [b]) => a.localeCompare(b, 'en'))\n .map(([key, value]) => ({\n id: `${prefix}${key}`,\n data: withLocale({ key, value }),\n filePath: ref.filePath,\n }))\n }\n\n case 'document': {\n const text = await readText(ref.filePath)\n if (text === null) return []\n const { frontmatter, body } = parseFrontmatter(text, stringLikeFieldKeys(model))\n const slug = ref.slug ?? model.id\n return [{\n id: `${prefix}${slug}`,\n data: withLocale({ slug, ...frontmatter }),\n body,\n filePath: ref.filePath,\n }]\n }\n\n default:\n return []\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/astro/loader.ts"],"sourcesContent":["// Astro content-layer loader for a `.contentrain` project.\n//\n// `contentrain generate` produces a typed client for application code; Astro\n// wants its content in the content layer instead, so pages can query it with\n// `getCollection()` and render documents with `<Content />`. This loader reads\n// the same `.contentrain` files the generator reads — through the same manifest\n// — so a project never has two answers about what its content is.\n//\n// Astro is NOT a dependency here, not even a peer: the returned object is\n// structurally what `defineCollection({ loader })` accepts, and the context is\n// typed by the small surface this loader actually uses. That keeps\n// @contentrain/query dependency-free and usable from any Astro 5 version.\n\nimport { publicationContext, publicationMeta, isPublishedAt, type PublicationOptions, type PublicationContext } from '../generator/publication.js'\nimport type { ModelDefinition } from '@contentrain/types'\nimport { rewriteEntryMedia } from '@contentrain/types'\nimport { isAbsolute, join, relative } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { ContentFileRef } from '../generator/config-reader.js'\nimport { readProjectManifest } from '../generator/config-reader.js'\nimport { readJson, readText } from '../generator/utils.js'\nimport { parseFrontmatter, stringLikeFieldKeys } from '../shared/frontmatter.js'\nimport { resolveMediaRefsInBody } from '../shared/media.js'\n\n/** The slice of Astro's `LoaderContext` this loader uses. */\nexport interface ContentrainLoaderContext {\n store: {\n clear: () => void\n set: (entry: {\n id: string\n data: Record<string, unknown>\n body?: string\n digest?: string\n rendered?: { html: string, metadata?: Record<string, unknown> }\n filePath?: string\n }) => void\n }\n logger?: { info: (msg: string) => void, warn: (msg: string) => void }\n /** Astro's resolved config; only `root` is read, to make file paths relative. */\n config?: { root?: URL | string }\n generateDigest?: (data: unknown) => string\n parseData?: (entry: { id: string, data: Record<string, unknown>, filePath?: string }) => Promise<Record<string, unknown>>\n renderMarkdown?: (content: string) => Promise<{ html: string, metadata?: Record<string, unknown> }>\n watcher?: { add: (path: string) => void, on: (event: string, cb: (path: string) => void) => void }\n}\n\n/** Structurally Astro's `Loader`. */\nexport interface ContentrainLoader {\n name: string\n load: (context: ContentrainLoaderContext) => Promise<void>\n}\n\nexport interface ContentrainLoaderOptions extends PublicationOptions {\n /** Model id — the `id` in `.contentrain/models/<id>.json`. */\n model: string\n /** Project root holding `.contentrain`. Default: `process.cwd()`. */\n root?: string\n /**\n * Load only this locale. Omitted on an i18n model, every supported locale is\n * loaded and entry ids are prefixed (`en/my-post`) so the two languages of one\n * post do not collide — Astro ids are unique per collection.\n */\n locale?: string\n /**\n * Public media delivery base for resolving relative `media/...` references\n * to absolute URLs. This loader reads `.contentrain` directly — nothing\n * upstream has rewritten them yet — so, unlike CDN-bundled content, they\n * need a resolver here. Applied automatically to image/video/file fields,\n * to `](media/...)` references inside markdown/richtext fields, and to a\n * document's markdown body. Omit to keep values exactly as stored.\n */\n mediaBaseUrl?: string\n}\n\ninterface Entry {\n id: string\n data: Record<string, unknown>\n body?: string\n filePath: string\n}\n\n/**\n * A loader for one Contentrain model.\n *\n * ```ts\n * // src/content.config.ts\n * import { defineCollection } from 'astro:content'\n * import { contentrainLoader } from '@contentrain/query/astro'\n *\n * export const collections = {\n * posts: defineCollection({ loader: contentrainLoader({ model: 'blog-post', locale: 'en' }) }),\n * articles: defineCollection({ loader: contentrainLoader({ model: 'blog-article' }) }),\n * }\n * ```\n *\n * Each model kind maps to the shape Astro can actually use:\n * collection → one entry per record · document → one entry per file, markdown\n * in `body` (and rendered, so `<Content />` works) · dictionary → one entry per\n * key, because a single blob would make `getEntry()` useless · singleton → one\n * entry, id `<model>` or the locale.\n */\nexport function contentrainLoader(options: ContentrainLoaderOptions): ContentrainLoader {\n const root = options.root ?? process.cwd()\n let watching = false\n\n return {\n name: `contentrain:${options.model}`,\n async load(context) {\n const manifest = await readProjectManifest(root)\n const model = manifest.models.find(m => m.id === options.model)\n if (!model) {\n const known = manifest.models.map(m => m.id).join(', ') || 'none'\n throw new Error(\n `contentrainLoader: model \"${options.model}\" not found in ${join(root, '.contentrain', 'models')}. Models here: ${known}.`,\n )\n }\n\n const refs = manifest.contentFiles.filter(ref =>\n ref.modelId === model.id && (options.locale === undefined || ref.locale === options.locale),\n )\n if (options.locale !== undefined && refs.length === 0 && model.i18n) {\n context.logger?.warn(\n `contentrainLoader: model \"${model.id}\" has no content for locale \"${options.locale}\" — collection is empty.`,\n )\n }\n // Prefix ids only when one collection really holds several languages;\n // a single-locale load keeps the natural id so routes stay clean.\n const prefixLocale = model.i18n === true && options.locale === undefined\n\n const publication = publicationContext(root, manifest.config.locales.default, options)\n const perFile = await Promise.all(refs.map(ref => entriesOf(ref, model, prefixLocale, publication)))\n const entries = options.mediaBaseUrl\n ? resolveEntriesMedia(perFile.flat(), model, options.mediaBaseUrl)\n : perFile.flat()\n // Astro rejects an absolute filePath (\"must be relative to the site\n // root\"), and a `.contentrain` outside the Astro root has no relative\n // form it accepts — there we send none rather than a path it refuses.\n const siteRoot = toPath(context.config?.root)\n\n context.store.clear()\n for (const entry of entries) {\n const data = context.parseData\n ? await context.parseData({ id: entry.id, data: entry.data, filePath: entry.filePath })\n : entry.data\n const filePath = siteRoot === undefined ? undefined : relative(siteRoot, entry.filePath)\n context.store.set({\n id: entry.id,\n data,\n ...(filePath === undefined || filePath.startsWith('..') || isAbsolute(filePath) ? {} : { filePath }),\n ...(entry.body === undefined ? {} : { body: entry.body }),\n ...(context.generateDigest ? { digest: context.generateDigest(entry.data) } : {}),\n ...(entry.body !== undefined && context.renderMarkdown\n ? { rendered: await context.renderMarkdown(entry.body) }\n : {}),\n })\n }\n context.logger?.info(`Loaded ${entries.length} ${model.kind} ${entries.length === 1 ? 'entry' : 'entries'} from ${model.id}`)\n\n // Dev server: reload when the files behind this collection change. The\n // listener is registered once — `load` runs again on every change, and a\n // listener per run would multiply the work on every save.\n if (context.watcher && !watching) {\n watching = true\n const paths = new Set(refs.map(ref => ref.filePath))\n for (const path of paths) context.watcher.add(path)\n const metaDir = join(root, '.contentrain', 'meta', model.id)\n context.watcher.add(metaDir)\n context.watcher.add(join(root, '.contentrain', 'models', `${model.id}.json`))\n const reload = (path: string) => {\n if (!paths.has(path) && !path.startsWith(metaDir + '/') && !path.endsWith(`${model.id}.json`)) return\n void this.load(context)\n }\n context.watcher.on('change', reload)\n context.watcher.on('add', reload)\n context.watcher.on('unlink', reload)\n }\n },\n }\n}\n\n/** Astro hands `config.root` as a file URL; tests and other hosts may use a path. */\nfunction toPath(root: URL | string | undefined): string | undefined {\n if (root === undefined) return undefined\n if (typeof root !== 'string') return fileURLToPath(root)\n return root.startsWith('file:') ? fileURLToPath(root) : root\n}\n\nconst MEDIA_BODY_TYPES = new Set(['markdown', 'richtext'])\n\nfunction fieldNamesOfType(model: ModelDefinition, types: Set<string>): string[] {\n if (!model.fields) return []\n return Object.entries(model.fields).filter(([, f]) => types.has(f.type)).map(([name]) => name)\n}\n\n/** Resolves `media/...` references to absolute URLs — image/video/file field\n * values (including those nested inside object/array fields, via the shared\n * `@contentrain/types` engine), `](media/...)` links and inline HTML\n * src/href inside markdown/richtext fields, and a document's markdown body —\n * across every loaded entry. A model with no fields and no body is returned\n * unchanged, entry by entry. */\nfunction resolveEntriesMedia(entries: Entry[], model: ModelDefinition, mediaBaseUrl: string): Entry[] {\n const bodyFields = fieldNamesOfType(model, MEDIA_BODY_TYPES)\n if (!model.fields && model.kind !== 'document') return entries\n\n return entries.map((entry) => {\n const data = model.fields ? rewriteEntryMedia(entry.data, model.fields, mediaBaseUrl) : entry.data\n for (const field of bodyFields) {\n if (field in data) data[field] = resolveMediaRefsInBody(data[field], mediaBaseUrl)\n }\n const body = entry.body === undefined ? undefined : resolveMediaRefsInBody(entry.body, mediaBaseUrl) as string\n return { ...entry, data, ...(body === undefined ? {} : { body }) }\n })\n}\n\nasync function entriesOf(ref: ContentFileRef, model: ModelDefinition, prefixLocale: boolean, publication?: PublicationContext): Promise<Entry[]> {\n const meta = publication ? await publicationMeta(ref, model, publication) : undefined\n const visible = (id?: string) => !publication || isPublishedAt(id === undefined ? meta : meta?.[id], publication.at)\n if ((model.kind === 'singleton' || model.kind === 'document') && !visible()) return []\n const prefix = prefixLocale && ref.locale ? `${ref.locale}/` : ''\n const withLocale = (data: Record<string, unknown>): Record<string, unknown> =>\n ref.locale === null ? data : { ...data, locale: ref.locale }\n\n switch (ref.kind) {\n case 'collection': {\n const raw = await readJson<Record<string, Record<string, unknown>>>(ref.filePath)\n if (!raw) return []\n return Object.entries(raw)\n .filter(([id]) => visible(id))\n .toSorted(([a], [b]) => a.localeCompare(b, 'en'))\n .map(([id, fields]) => ({\n id: `${prefix}${id}`,\n data: withLocale({ id, ...fields }),\n filePath: ref.filePath,\n }))\n }\n\n case 'singleton': {\n const raw = await readJson<Record<string, unknown>>(ref.filePath)\n if (!raw) return []\n return [{\n id: prefix ? `${ref.locale}` : model.id,\n data: withLocale(raw),\n filePath: ref.filePath,\n }]\n }\n\n case 'dictionary': {\n // One entry per key: a dictionary key is a semantic address, and a single\n // blob entry would make getEntry() and reference() useless on it.\n const raw = await readJson<Record<string, string>>(ref.filePath)\n if (!raw) return []\n return Object.entries(raw)\n .filter(([id]) => visible(id))\n .toSorted(([a], [b]) => a.localeCompare(b, 'en'))\n .map(([key, value]) => ({\n id: `${prefix}${key}`,\n data: withLocale({ key, value }),\n filePath: ref.filePath,\n }))\n }\n\n case 'document': {\n const text = await readText(ref.filePath)\n if (text === null) return []\n const { frontmatter, body } = parseFrontmatter(text, stringLikeFieldKeys(model))\n const slug = ref.slug ?? model.id\n return [{\n id: `${prefix}${slug}`,\n data: withLocale({ slug, ...frontmatter }),\n body,\n filePath: ref.filePath,\n }]\n }\n\n default:\n return []\n }\n}\n"],"mappings":"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"}
|
package/dist/cdn/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as FormsClient, C as CommentsClientConfig, D as FormFieldError, E as FormFieldConfig, F as MediaManifest, G as TransportConfig, I as CdnDocumentQuery, J as DocumentDataSource, K as CollectionDataSource, L as CdnDictionaryAccessor, M as MediaAccessor, N as MediaAsset, O as FormSubmitOptions, P as MediaAssetMeta, R as CdnSingletonAccessor, S as CommentsClient, T as FormConfig, W as HttpTransport, X as SingletonDataSource, Y as DocumentIndexEntry, _ as CommentSubmitResult, a as ConversationClient, b as CommentThreadQuery, c as ConversationHistory, d as ConversationSendOptions, f as ConversationToolResult, g as CommentSubmission, h as CommentFieldError, i as ContentrainError, j as FormsClientConfig, k as FormSubmitResult, l as ConversationMessage, m as CommentAuthor, n as ContentrainCDNConfig, o as ConversationClientConfig, p as ConversationUsage, q as DictionaryDataSource, r as createContentrain, s as ConversationContext, t as ContentrainCDNClient, u as ConversationResponse, v as CommentThread, w as PublicComment, x as CommentType, y as CommentThreadConfig, z as CdnCollectionQuery } from "../index-
|
|
1
|
+
import { A as FormsClient, C as CommentsClientConfig, D as FormFieldError, E as FormFieldConfig, F as MediaManifest, G as TransportConfig, I as CdnDocumentQuery, J as DocumentDataSource, K as CollectionDataSource, L as CdnDictionaryAccessor, M as MediaAccessor, N as MediaAsset, O as FormSubmitOptions, P as MediaAssetMeta, R as CdnSingletonAccessor, S as CommentsClient, T as FormConfig, W as HttpTransport, X as SingletonDataSource, Y as DocumentIndexEntry, _ as CommentSubmitResult, a as ConversationClient, b as CommentThreadQuery, c as ConversationHistory, d as ConversationSendOptions, f as ConversationToolResult, g as CommentSubmission, h as CommentFieldError, i as ContentrainError, j as FormsClientConfig, k as FormSubmitResult, l as ConversationMessage, m as CommentAuthor, n as ContentrainCDNConfig, o as ConversationClientConfig, p as ConversationUsage, q as DictionaryDataSource, r as createContentrain, s as ConversationContext, t as ContentrainCDNClient, u as ConversationResponse, v as CommentThread, w as PublicComment, x as CommentType, y as CommentThreadConfig, z as CdnCollectionQuery } from "../index-ClftI9li.mjs";
|
|
2
2
|
export { CdnCollectionQuery, CdnDictionaryAccessor, CdnDocumentQuery, CdnSingletonAccessor, CollectionDataSource, CommentAuthor, CommentFieldError, CommentSubmission, CommentSubmitResult, CommentThread, CommentThreadConfig, CommentThreadQuery, CommentType, CommentsClient, CommentsClientConfig, ContentrainCDNClient, ContentrainCDNConfig, ContentrainError, ConversationClient, ConversationClientConfig, ConversationContext, ConversationHistory, ConversationMessage, ConversationResponse, ConversationSendOptions, ConversationToolResult, ConversationUsage, DictionaryDataSource, DocumentDataSource, DocumentIndexEntry, FormConfig, FormFieldConfig, FormFieldError, FormSubmitOptions, FormSubmitResult, FormsClient, FormsClientConfig, HttpTransport, MediaAccessor, MediaAsset, MediaAssetMeta, MediaManifest, PublicComment, SingletonDataSource, TransportConfig, createContentrain };
|
package/dist/cli.cjs
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { parseFrontmatterScalar, parseFrontmatterScalarString, splitFrontmatterList } from "@contentrain/types";
|
|
1
2
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { parseFrontmatterScalar, parseFrontmatterScalarString, splitFrontmatterList } from "@contentrain/types";
|
|
4
4
|
//#region src/generator/publication.ts
|
|
5
5
|
function publicationContext(projectRoot, defaultLocale, options) {
|
|
6
6
|
if (!options.publishedOnly && options.at === void 0) return void 0;
|
|
@@ -292,4 +292,4 @@ function parseValue(raw, forceString = false) {
|
|
|
292
292
|
//#endregion
|
|
293
293
|
export { readJson as a, isPublishedAt as c, readDir as i, publicationContext as l, stringLikeFieldKeys as n, readText as o, readProjectManifest as r, writeText as s, parseFrontmatter as t, publicationMeta as u };
|
|
294
294
|
|
|
295
|
-
//# sourceMappingURL=frontmatter-
|
|
295
|
+
//# sourceMappingURL=frontmatter-D9yVqC5y.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmatter-BUaCvV-s.mjs","names":[],"sources":["../src/generator/publication.ts","../src/generator/utils.ts","../src/generator/config-reader.ts","../src/shared/frontmatter.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { ModelDefinition } from '@contentrain/types'\nimport type { ContentFileRef } from './config-reader.js'\n\nexport interface PublicationOptions {\n /** Opt in for public builds; the default keeps the existing editorial client. */\n publishedOnly?: boolean\n /** ISO timestamp for a reproducible public build. Implies publishedOnly. */\n at?: string\n}\n\nexport interface PublicationContext {\n projectRoot: string\n defaultLocale: string\n at: number\n}\n\nexport function publicationContext(projectRoot: string, defaultLocale: string, options: PublicationOptions): PublicationContext | undefined {\n if (!options.publishedOnly && options.at === undefined) return undefined\n if (options.at !== undefined && !/^\\d{4}-\\d{2}-\\d{2}T.*(?:Z|[+-]\\d{2}:\\d{2})$/i.test(options.at))\n throw new Error('Invalid publication --at timestamp: include an ISO date, time and timezone')\n const at = options.at === undefined ? Date.now() : Date.parse(options.at)\n if (!Number.isFinite(at)) throw new Error('Invalid publication --at timestamp')\n return { projectRoot, defaultLocale, at }\n}\n\n/** Canonical meta is independent of custom content paths and locale strategies. */\nexport async function publicationMeta(ref: ContentFileRef, model: ModelDefinition, context: PublicationContext): Promise<Record<string, unknown> | undefined> {\n const locale = model.i18n ? ref.locale ?? context.defaultLocale : context.defaultLocale\n const parts = [context.projectRoot, '.contentrain', 'meta', model.id]\n if (model.kind === 'document') parts.push(ref.slug ?? model.id)\n const path = join(...parts, `${locale}.json`)\n let text: string\n try { text = await readFile(path, 'utf8') }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined\n throw error\n }\n const meta: unknown = JSON.parse(text)\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) throw new Error(`Invalid publication metadata: ${path}`)\n return meta as Record<string, unknown>\n}\n\n/** Legacy content without meta remains visible, matching Studio's CDN contract. */\nexport function isPublishedAt(meta: unknown, at: number): boolean {\n if (meta === undefined) return true\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return false\n const value = meta as Record<string, unknown>\n if (value.status && value.status !== 'published') return false\n for (const key of ['publish_at', 'expire_at'] as const) {\n if (value[key] === undefined || value[key] === null) continue\n const time = typeof value[key] === 'string' ? Date.parse(value[key]) : NaN\n if (!Number.isFinite(time)) return false\n if (key === 'publish_at' ? time > at : time <= at) return false\n }\n return true\n}\n","import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'\nimport { join, dirname } from 'node:path'\n\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as T\n } catch {\n return null\n }\n}\n\nexport async function readDir(dirPath: string): Promise<string[]> {\n try {\n return await readdir(dirPath)\n } catch {\n return []\n }\n}\n\nexport async function readText(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n\nexport async function writeText(filePath: string, content: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true })\n await writeFile(filePath, content, 'utf-8')\n}\n\nexport function contentrainDir(projectRoot: string): string {\n return join(projectRoot, '.contentrain')\n}\n","import type { ContentrainConfig, ModelDefinition, LocaleStrategy } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { readJson, readDir, readText, contentrainDir } from './utils.js'\n\nexport interface ContentFileRef {\n modelId: string\n locale: string | null\n filePath: string\n kind: ModelDefinition['kind']\n /** Extracted slug for document kind (from filename, not frontmatter) */\n slug?: string\n}\n\nexport interface ProjectManifest {\n config: ContentrainConfig\n models: ModelDefinition[]\n contentFiles: ContentFileRef[]\n}\n\nexport async function readProjectManifest(projectRoot: string): Promise<ProjectManifest> {\n const crDir = contentrainDir(projectRoot)\n\n // 1. Read config\n const rawConfig = await readJson<Partial<ContentrainConfig>>(join(crDir, 'config.json'))\n const config: ContentrainConfig = {\n version: rawConfig?.version ?? 1,\n stack: (rawConfig?.stack ?? 'other') as ContentrainConfig['stack'],\n workflow: (rawConfig?.workflow ?? 'auto-merge') as ContentrainConfig['workflow'],\n locales: {\n default: rawConfig?.locales?.default ?? 'en',\n supported: rawConfig?.locales?.supported ?? [rawConfig?.locales?.default ?? 'en'],\n },\n domains: rawConfig?.domains ?? [],\n repository: rawConfig?.repository,\n assets_path: rawConfig?.assets_path,\n cdn: rawConfig?.cdn,\n branchRetention: rawConfig?.branchRetention,\n }\n\n // 2. Read all models (parallel)\n const modelsDir = join(crDir, 'models')\n const modelFiles = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const modelResults = await Promise.all(\n modelFiles.map(file => readJson<ModelDefinition>(join(modelsDir, file))),\n )\n const models = modelResults\n .filter((m): m is ModelDefinition => m != null && Boolean(m.id))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n\n // 3. Map content files (parallel per model)\n const allRefs = await Promise.all(\n models.map(model => mapContentFiles(projectRoot, model, config)),\n )\n const contentFiles = allRefs.flat()\n\n return { config, models, contentFiles }\n}\n\nfunction getContentDir(projectRoot: string, model: ModelDefinition): string {\n if (model.content_path) return join(projectRoot, model.content_path)\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nfunction getLocaleStrategy(model: ModelDefinition): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\nfunction jsonFilePath(dir: string, model: ModelDefinition, locale: string): string {\n const strategy = getLocaleStrategy(model)\n switch (strategy) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\nasync function mapContentFiles(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n): Promise<ContentFileRef[]> {\n const dir = getContentDir(projectRoot, model)\n\n if (model.kind === 'document') {\n return mapDocumentFiles(dir, model, config)\n }\n\n // JSON-based models (singleton, collection, dictionary)\n if (model.i18n) {\n const results = await Promise.all(\n config.locales.supported.map(async (locale) => {\n const filePath = jsonFilePath(dir, model, locale)\n const content = await readJson(filePath)\n if (content === null) return null\n return { modelId: model.id, locale, filePath, kind: model.kind } as ContentFileRef\n }),\n )\n return results.filter(Boolean) as ContentFileRef[]\n }\n\n // Non-i18n: always data.json\n const filePath = join(dir, 'data.json')\n const content = await readJson(filePath)\n if (content === null) return []\n return [{ modelId: model.id, locale: null, filePath, kind: model.kind }]\n}\n\nasync function mapDocumentFiles(\n dir: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n): Promise<ContentFileRef[]> {\n const strategy = getLocaleStrategy(model)\n\n if (!model.i18n) {\n // Non-i18n documents: {dir}/{slug}.md\n const files = (await readDir(dir)).filter(f => f.endsWith('.md'))\n return files.map(f => ({\n modelId: model.id,\n locale: null,\n filePath: join(dir, f),\n kind: 'document' as const,\n slug: f.replace('.md', ''),\n }))\n }\n\n // i18n documents — resolve per locale in parallel\n const allLocaleRefs = await Promise.all(\n config.locales.supported.map(locale => mapDocumentLocale(dir, model, locale, strategy)),\n )\n return allLocaleRefs.flat()\n}\n\nasync function mapDocumentLocale(\n dir: string,\n model: ModelDefinition,\n locale: string,\n strategy: LocaleStrategy,\n): Promise<ContentFileRef[]> {\n if (strategy === 'file') {\n // Each slug is a directory: {dir}/{slug}/{locale}.md\n const entries = await readDir(dir)\n const results = await Promise.all(\n entries.map(async (entry) => {\n const filePath = join(dir, entry, `${locale}.md`)\n const content = await readText(filePath)\n if (content === null) return null\n return { modelId: model.id, locale, filePath, kind: 'document', slug: entry } as ContentFileRef\n }),\n )\n return results.filter(Boolean) as ContentFileRef[]\n }\n\n if (strategy === 'directory') {\n // {dir}/{locale}/{slug}.md\n const localeDir = join(dir, locale)\n const files = (await readDir(localeDir)).filter(f => f.endsWith('.md'))\n return files.map(f => ({\n modelId: model.id,\n locale,\n filePath: join(localeDir, f),\n kind: 'document' as const,\n slug: f.replace('.md', ''),\n }))\n }\n\n if (strategy === 'suffix') {\n // {dir}/{slug}.{locale}.md\n const suffix = `.${locale}.md`\n const files = (await readDir(dir)).filter(f => f.endsWith(suffix))\n return files.map(f => ({\n modelId: model.id,\n locale,\n filePath: join(dir, f),\n kind: 'document' as const,\n slug: f.slice(0, -suffix.length),\n }))\n }\n\n return []\n}\n","// Frontmatter parsing shared by the client generator and the Astro loader.\n//\n// Both read the same `.contentrain` documents, so the parser lives here rather\n// than being replicated: a divergence would mean the generated client and the\n// Astro collection disagree about the same file's fields.\n//\n// The same argument reaches one level further out. The content engine reads\n// these documents too, through `@contentrain/types`, and for a while this file\n// carried its own copy of the scalar rules — so the two disagreed about what a\n// quoted string with escapes in it says. The *structure* below (nesting,\n// indentation, the model-aware string coercion) is this package's; the scalar\n// grammar is imported, because how a value is spelled on disk is a contract\n// rather than an implementation detail.\n\nimport type { ModelDefinition } from '@contentrain/types'\nimport {\n parseFrontmatterScalar,\n parseFrontmatterScalarString,\n splitFrontmatterList,\n} from '@contentrain/types'\n\n// Field types that map to `string` in the generated types — their frontmatter\n// values must NOT be numerically coerced (e.g. a string SKU \"007\").\nconst STRING_LIKE_TYPES = new Set([\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext', 'date', 'datetime', 'image', 'video', 'file',\n 'select', 'relation',\n])\n\nexport function stringLikeFieldKeys(model: ModelDefinition): Set<string> {\n const keys = new Set<string>()\n if (model.fields) {\n for (const [name, field] of Object.entries(model.fields)) {\n if (STRING_LIKE_TYPES.has(field.type)) keys.add(name)\n }\n }\n return keys\n}\n\n// Minimal frontmatter parser (replicated from MCP pattern)\nexport function parseFrontmatter(text: string, stringKeys: Set<string> = new Set()): { frontmatter: Record<string, unknown>; body: string } {\n const normalized = text.replace(/\\r\\n/g, '\\n')\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/)\n if (!match) return { frontmatter: {}, body: normalized }\n\n const fmStr = match[1]!\n const body = match[2]!.trim()\n const frontmatter: Record<string, unknown> = {}\n\n // Stack-based parser that handles nested objects and arrays\n const lines = fmStr.split('\\n')\n const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [{ obj: frontmatter, indent: -1 }]\n\n for (const [lineIndex, line] of lines.entries()) {\n // Skip empty lines\n if (line.trim() === '') continue\n\n // Array item\n const arrayMatch = line.match(/^(\\s*)-\\s+(.*)$/)\n if (arrayMatch) {\n const arrIndent = arrayMatch[1]!.length\n const value = arrayMatch[2]!.trim()\n // Find the parent that owns this array\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= arrIndent) {\n stack.pop()\n }\n const parent = stack[stack.length - 1]!.obj\n const lastKey = Object.keys(parent).pop()\n if (lastKey && Array.isArray(parent[lastKey])) {\n (parent[lastKey] as unknown[]).push(parseValue(value))\n }\n continue\n }\n\n // Key-value pair\n const kvMatch = line.match(/^(\\s*)([\\w][\\w.-]*)\\s*:\\s*(.*)$/)\n if (!kvMatch) continue\n\n const kvIndent = kvMatch[1]!.length\n const key = kvMatch[2]!\n const rawValue = kvMatch[3]!.trim()\n\n // Pop stack to find correct parent based on indentation\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= kvIndent) {\n stack.pop()\n }\n const current = stack[stack.length - 1]!.obj\n\n if (rawValue === '') {\n // A bare `key:` is ambiguous: empty array, empty object, or null. It is\n // resolved by what follows — a list item opens an array, a more-indented\n // key opens an object, and anything else (including end of frontmatter)\n // is an empty array, which is what the canonical reader in\n // `@contentrain/types` returns. Documents written today say `key: []`\n // outright; this path is for the ones written before that.\n //\n // The peek used `lines.indexOf(line)`, which finds the FIRST line with\n // this text rather than this one — two identically spelled lines in one\n // document made the second read the first one's successor.\n let nextLine = ''\n for (let i = lineIndex + 1; i < lines.length; i += 1) {\n if (lines[i]!.trim() !== '') {\n nextLine = lines[i]!\n break\n }\n }\n const nextIndent = nextLine.length - nextLine.trimStart().length\n if (nextLine.trim().startsWith('-')) {\n current[key] = []\n } else if (nextLine !== '' && nextIndent > kvIndent) {\n const nested: Record<string, unknown> = {}\n current[key] = nested\n stack.push({ obj: nested, indent: kvIndent })\n } else {\n current[key] = []\n }\n continue\n }\n\n if (rawValue.startsWith('[') && rawValue.endsWith(']')) {\n const inner = rawValue.slice(1, -1).trim()\n current[key] = inner === ''\n ? []\n : splitFrontmatterList(inner).map(item => parseFrontmatterScalarString(item.trim()))\n continue\n }\n\n // Only top-level keys are matched against the model's declared field types.\n const forceString = current === frontmatter && stringKeys.has(key)\n current[key] = parseValue(rawValue, forceString)\n }\n\n return { frontmatter, body }\n}\n\nfunction parseValue(raw: string, forceString = false): unknown {\n return forceString ? parseFrontmatterScalarString(raw) : parseFrontmatterScalar(raw)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"frontmatter-D9yVqC5y.mjs","names":[],"sources":["../src/generator/publication.ts","../src/generator/utils.ts","../src/generator/config-reader.ts","../src/shared/frontmatter.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { ModelDefinition } from '@contentrain/types'\nimport type { ContentFileRef } from './config-reader.js'\n\nexport interface PublicationOptions {\n /** Opt in for public builds; the default keeps the existing editorial client. */\n publishedOnly?: boolean\n /** ISO timestamp for a reproducible public build. Implies publishedOnly. */\n at?: string\n}\n\nexport interface PublicationContext {\n projectRoot: string\n defaultLocale: string\n at: number\n}\n\nexport function publicationContext(projectRoot: string, defaultLocale: string, options: PublicationOptions): PublicationContext | undefined {\n if (!options.publishedOnly && options.at === undefined) return undefined\n if (options.at !== undefined && !/^\\d{4}-\\d{2}-\\d{2}T.*(?:Z|[+-]\\d{2}:\\d{2})$/i.test(options.at))\n throw new Error('Invalid publication --at timestamp: include an ISO date, time and timezone')\n const at = options.at === undefined ? Date.now() : Date.parse(options.at)\n if (!Number.isFinite(at)) throw new Error('Invalid publication --at timestamp')\n return { projectRoot, defaultLocale, at }\n}\n\n/** Canonical meta is independent of custom content paths and locale strategies. */\nexport async function publicationMeta(ref: ContentFileRef, model: ModelDefinition, context: PublicationContext): Promise<Record<string, unknown> | undefined> {\n const locale = model.i18n ? ref.locale ?? context.defaultLocale : context.defaultLocale\n const parts = [context.projectRoot, '.contentrain', 'meta', model.id]\n if (model.kind === 'document') parts.push(ref.slug ?? model.id)\n const path = join(...parts, `${locale}.json`)\n let text: string\n try { text = await readFile(path, 'utf8') }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined\n throw error\n }\n const meta: unknown = JSON.parse(text)\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) throw new Error(`Invalid publication metadata: ${path}`)\n return meta as Record<string, unknown>\n}\n\n/** Legacy content without meta remains visible, matching Studio's CDN contract. */\nexport function isPublishedAt(meta: unknown, at: number): boolean {\n if (meta === undefined) return true\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return false\n const value = meta as Record<string, unknown>\n if (value.status && value.status !== 'published') return false\n for (const key of ['publish_at', 'expire_at'] as const) {\n if (value[key] === undefined || value[key] === null) continue\n const time = typeof value[key] === 'string' ? Date.parse(value[key]) : NaN\n if (!Number.isFinite(time)) return false\n if (key === 'publish_at' ? time > at : time <= at) return false\n }\n return true\n}\n","import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'\nimport { join, dirname } from 'node:path'\n\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as T\n } catch {\n return null\n }\n}\n\nexport async function readDir(dirPath: string): Promise<string[]> {\n try {\n return await readdir(dirPath)\n } catch {\n return []\n }\n}\n\nexport async function readText(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n\nexport async function writeText(filePath: string, content: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true })\n await writeFile(filePath, content, 'utf-8')\n}\n\nexport function contentrainDir(projectRoot: string): string {\n return join(projectRoot, '.contentrain')\n}\n","import type { ContentrainConfig, ModelDefinition, LocaleStrategy } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { readJson, readDir, readText, contentrainDir } from './utils.js'\n\nexport interface ContentFileRef {\n modelId: string\n locale: string | null\n filePath: string\n kind: ModelDefinition['kind']\n /** Extracted slug for document kind (from filename, not frontmatter) */\n slug?: string\n}\n\nexport interface ProjectManifest {\n config: ContentrainConfig\n models: ModelDefinition[]\n contentFiles: ContentFileRef[]\n}\n\nexport async function readProjectManifest(projectRoot: string): Promise<ProjectManifest> {\n const crDir = contentrainDir(projectRoot)\n\n // 1. Read config\n const rawConfig = await readJson<Partial<ContentrainConfig>>(join(crDir, 'config.json'))\n const config: ContentrainConfig = {\n version: rawConfig?.version ?? 1,\n stack: (rawConfig?.stack ?? 'other') as ContentrainConfig['stack'],\n workflow: (rawConfig?.workflow ?? 'auto-merge') as ContentrainConfig['workflow'],\n locales: {\n default: rawConfig?.locales?.default ?? 'en',\n supported: rawConfig?.locales?.supported ?? [rawConfig?.locales?.default ?? 'en'],\n },\n domains: rawConfig?.domains ?? [],\n repository: rawConfig?.repository,\n assets_path: rawConfig?.assets_path,\n cdn: rawConfig?.cdn,\n branchRetention: rawConfig?.branchRetention,\n }\n\n // 2. Read all models (parallel)\n const modelsDir = join(crDir, 'models')\n const modelFiles = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const modelResults = await Promise.all(\n modelFiles.map(file => readJson<ModelDefinition>(join(modelsDir, file))),\n )\n const models = modelResults\n .filter((m): m is ModelDefinition => m != null && Boolean(m.id))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n\n // 3. Map content files (parallel per model)\n const allRefs = await Promise.all(\n models.map(model => mapContentFiles(projectRoot, model, config)),\n )\n const contentFiles = allRefs.flat()\n\n return { config, models, contentFiles }\n}\n\nfunction getContentDir(projectRoot: string, model: ModelDefinition): string {\n if (model.content_path) return join(projectRoot, model.content_path)\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nfunction getLocaleStrategy(model: ModelDefinition): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\nfunction jsonFilePath(dir: string, model: ModelDefinition, locale: string): string {\n const strategy = getLocaleStrategy(model)\n switch (strategy) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\nasync function mapContentFiles(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n): Promise<ContentFileRef[]> {\n const dir = getContentDir(projectRoot, model)\n\n if (model.kind === 'document') {\n return mapDocumentFiles(dir, model, config)\n }\n\n // JSON-based models (singleton, collection, dictionary)\n if (model.i18n) {\n const results = await Promise.all(\n config.locales.supported.map(async (locale) => {\n const filePath = jsonFilePath(dir, model, locale)\n const content = await readJson(filePath)\n if (content === null) return null\n return { modelId: model.id, locale, filePath, kind: model.kind } as ContentFileRef\n }),\n )\n return results.filter(Boolean) as ContentFileRef[]\n }\n\n // Non-i18n: always data.json\n const filePath = join(dir, 'data.json')\n const content = await readJson(filePath)\n if (content === null) return []\n return [{ modelId: model.id, locale: null, filePath, kind: model.kind }]\n}\n\nasync function mapDocumentFiles(\n dir: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n): Promise<ContentFileRef[]> {\n const strategy = getLocaleStrategy(model)\n\n if (!model.i18n) {\n // Non-i18n documents: {dir}/{slug}.md\n const files = (await readDir(dir)).filter(f => f.endsWith('.md'))\n return files.map(f => ({\n modelId: model.id,\n locale: null,\n filePath: join(dir, f),\n kind: 'document' as const,\n slug: f.replace('.md', ''),\n }))\n }\n\n // i18n documents — resolve per locale in parallel\n const allLocaleRefs = await Promise.all(\n config.locales.supported.map(locale => mapDocumentLocale(dir, model, locale, strategy)),\n )\n return allLocaleRefs.flat()\n}\n\nasync function mapDocumentLocale(\n dir: string,\n model: ModelDefinition,\n locale: string,\n strategy: LocaleStrategy,\n): Promise<ContentFileRef[]> {\n if (strategy === 'file') {\n // Each slug is a directory: {dir}/{slug}/{locale}.md\n const entries = await readDir(dir)\n const results = await Promise.all(\n entries.map(async (entry) => {\n const filePath = join(dir, entry, `${locale}.md`)\n const content = await readText(filePath)\n if (content === null) return null\n return { modelId: model.id, locale, filePath, kind: 'document', slug: entry } as ContentFileRef\n }),\n )\n return results.filter(Boolean) as ContentFileRef[]\n }\n\n if (strategy === 'directory') {\n // {dir}/{locale}/{slug}.md\n const localeDir = join(dir, locale)\n const files = (await readDir(localeDir)).filter(f => f.endsWith('.md'))\n return files.map(f => ({\n modelId: model.id,\n locale,\n filePath: join(localeDir, f),\n kind: 'document' as const,\n slug: f.replace('.md', ''),\n }))\n }\n\n if (strategy === 'suffix') {\n // {dir}/{slug}.{locale}.md\n const suffix = `.${locale}.md`\n const files = (await readDir(dir)).filter(f => f.endsWith(suffix))\n return files.map(f => ({\n modelId: model.id,\n locale,\n filePath: join(dir, f),\n kind: 'document' as const,\n slug: f.slice(0, -suffix.length),\n }))\n }\n\n return []\n}\n","// Frontmatter parsing shared by the client generator and the Astro loader.\n//\n// Both read the same `.contentrain` documents, so the parser lives here rather\n// than being replicated: a divergence would mean the generated client and the\n// Astro collection disagree about the same file's fields.\n//\n// The same argument reaches one level further out. The content engine reads\n// these documents too, through `@contentrain/types`, and for a while this file\n// carried its own copy of the scalar rules — so the two disagreed about what a\n// quoted string with escapes in it says. The *structure* below (nesting,\n// indentation, the model-aware string coercion) is this package's; the scalar\n// grammar is imported, because how a value is spelled on disk is a contract\n// rather than an implementation detail.\n\nimport type { ModelDefinition } from '@contentrain/types'\nimport {\n parseFrontmatterScalar,\n parseFrontmatterScalarString,\n splitFrontmatterList,\n} from '@contentrain/types'\n\n// Field types that map to `string` in the generated types — their frontmatter\n// values must NOT be numerically coerced (e.g. a string SKU \"007\").\nconst STRING_LIKE_TYPES = new Set([\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext', 'date', 'datetime', 'image', 'video', 'file',\n 'select', 'relation',\n])\n\nexport function stringLikeFieldKeys(model: ModelDefinition): Set<string> {\n const keys = new Set<string>()\n if (model.fields) {\n for (const [name, field] of Object.entries(model.fields)) {\n if (STRING_LIKE_TYPES.has(field.type)) keys.add(name)\n }\n }\n return keys\n}\n\n// Minimal frontmatter parser (replicated from MCP pattern)\nexport function parseFrontmatter(text: string, stringKeys: Set<string> = new Set()): { frontmatter: Record<string, unknown>; body: string } {\n const normalized = text.replace(/\\r\\n/g, '\\n')\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/)\n if (!match) return { frontmatter: {}, body: normalized }\n\n const fmStr = match[1]!\n const body = match[2]!.trim()\n const frontmatter: Record<string, unknown> = {}\n\n // Stack-based parser that handles nested objects and arrays\n const lines = fmStr.split('\\n')\n const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [{ obj: frontmatter, indent: -1 }]\n\n for (const [lineIndex, line] of lines.entries()) {\n // Skip empty lines\n if (line.trim() === '') continue\n\n // Array item\n const arrayMatch = line.match(/^(\\s*)-\\s+(.*)$/)\n if (arrayMatch) {\n const arrIndent = arrayMatch[1]!.length\n const value = arrayMatch[2]!.trim()\n // Find the parent that owns this array\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= arrIndent) {\n stack.pop()\n }\n const parent = stack[stack.length - 1]!.obj\n const lastKey = Object.keys(parent).pop()\n if (lastKey && Array.isArray(parent[lastKey])) {\n (parent[lastKey] as unknown[]).push(parseValue(value))\n }\n continue\n }\n\n // Key-value pair\n const kvMatch = line.match(/^(\\s*)([\\w][\\w.-]*)\\s*:\\s*(.*)$/)\n if (!kvMatch) continue\n\n const kvIndent = kvMatch[1]!.length\n const key = kvMatch[2]!\n const rawValue = kvMatch[3]!.trim()\n\n // Pop stack to find correct parent based on indentation\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= kvIndent) {\n stack.pop()\n }\n const current = stack[stack.length - 1]!.obj\n\n if (rawValue === '') {\n // A bare `key:` is ambiguous: empty array, empty object, or null. It is\n // resolved by what follows — a list item opens an array, a more-indented\n // key opens an object, and anything else (including end of frontmatter)\n // is an empty array, which is what the canonical reader in\n // `@contentrain/types` returns. Documents written today say `key: []`\n // outright; this path is for the ones written before that.\n //\n // The peek used `lines.indexOf(line)`, which finds the FIRST line with\n // this text rather than this one — two identically spelled lines in one\n // document made the second read the first one's successor.\n let nextLine = ''\n for (let i = lineIndex + 1; i < lines.length; i += 1) {\n if (lines[i]!.trim() !== '') {\n nextLine = lines[i]!\n break\n }\n }\n const nextIndent = nextLine.length - nextLine.trimStart().length\n if (nextLine.trim().startsWith('-')) {\n current[key] = []\n } else if (nextLine !== '' && nextIndent > kvIndent) {\n const nested: Record<string, unknown> = {}\n current[key] = nested\n stack.push({ obj: nested, indent: kvIndent })\n } else {\n current[key] = []\n }\n continue\n }\n\n if (rawValue.startsWith('[') && rawValue.endsWith(']')) {\n const inner = rawValue.slice(1, -1).trim()\n current[key] = inner === ''\n ? []\n : splitFrontmatterList(inner).map(item => parseFrontmatterScalarString(item.trim()))\n continue\n }\n\n // Only top-level keys are matched against the model's declared field types.\n const forceString = current === frontmatter && stringKeys.has(key)\n current[key] = parseValue(rawValue, forceString)\n }\n\n return { frontmatter, body }\n}\n\nfunction parseValue(raw: string, forceString = false): unknown {\n return forceString ? parseFrontmatterScalarString(raw) : parseFrontmatterScalar(raw)\n}\n"],"mappings":";;;;AAkBA,SAAgB,mBAAmB,aAAqB,eAAuB,SAA6D;AAC1I,KAAI,CAAC,QAAQ,iBAAiB,QAAQ,OAAO,KAAA,EAAW,QAAO,KAAA;AAC/D,KAAI,QAAQ,OAAO,KAAA,KAAa,CAAC,+CAA+C,KAAK,QAAQ,GAAG,CAC9F,OAAM,IAAI,MAAM,6EAA6E;CAC/F,MAAM,KAAK,QAAQ,OAAO,KAAA,IAAY,KAAK,KAAK,GAAG,KAAK,MAAM,QAAQ,GAAG;AACzE,KAAI,CAAC,OAAO,SAAS,GAAG,CAAE,OAAM,IAAI,MAAM,qCAAqC;AAC/E,QAAO;EAAE;EAAa;EAAe;EAAI;;;AAI3C,eAAsB,gBAAgB,KAAqB,OAAwB,SAA2E;CAC5J,MAAM,SAAS,MAAM,OAAO,IAAI,UAAU,QAAQ,gBAAgB,QAAQ;CAC1E,MAAM,QAAQ;EAAC,QAAQ;EAAa;EAAgB;EAAQ,MAAM;EAAG;AACrE,KAAI,MAAM,SAAS,WAAY,OAAM,KAAK,IAAI,QAAQ,MAAM,GAAG;CAC/D,MAAM,OAAO,KAAK,GAAG,OAAO,GAAG,OAAO,OAAO;CAC7C,IAAI;AACJ,KAAI;AAAE,SAAO,MAAM,SAAS,MAAM,OAAO;UAClC,OAAO;AACZ,MAAK,MAAgC,SAAS,SAAU,QAAO,KAAA;AAC/D,QAAM;;CAER,MAAM,OAAgB,KAAK,MAAM,KAAK;AACtC,KAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,KAAK,CAAE,OAAM,IAAI,MAAM,iCAAiC,OAAO;AACtH,QAAO;;;AAIT,SAAgB,cAAc,MAAe,IAAqB;AAChE,KAAI,SAAS,KAAA,EAAW,QAAO;AAC/B,KAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,KAAK,CAAE,QAAO;CACrE,MAAM,QAAQ;AACd,KAAI,MAAM,UAAU,MAAM,WAAW,YAAa,QAAO;AACzD,MAAK,MAAM,OAAO,CAAC,cAAc,YAAY,EAAW;AACtD,MAAI,MAAM,SAAS,KAAA,KAAa,MAAM,SAAS,KAAM;EACrD,MAAM,OAAO,OAAO,MAAM,SAAS,WAAW,KAAK,MAAM,MAAM,KAAK,GAAG;AACvE,MAAI,CAAC,OAAO,SAAS,KAAK,CAAE,QAAO;AACnC,MAAI,QAAQ,eAAe,OAAO,KAAK,QAAQ,GAAI,QAAO;;AAE5D,QAAO;;;;ACrDT,eAAsB,SAAY,UAAqC;AACrE,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,UAAU,QAAQ;AAC7C,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;AAIX,eAAsB,QAAQ,SAAoC;AAChE,KAAI;AACF,SAAO,MAAM,QAAQ,QAAQ;SACvB;AACN,SAAO,EAAE;;;AAIb,eAAsB,SAAS,UAA0C;AACvE,KAAI;AACF,SAAO,MAAM,SAAS,UAAU,QAAQ;SAClC;AACN,SAAO;;;AAIX,eAAsB,UAAU,UAAkB,SAAgC;AAChF,OAAM,MAAM,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;AACnD,OAAM,UAAU,UAAU,SAAS,QAAQ;;AAG7C,SAAgB,eAAe,aAA6B;AAC1D,QAAO,KAAK,aAAa,eAAe;;;;ACf1C,eAAsB,oBAAoB,aAA+C;CACvF,MAAM,QAAQ,eAAe,YAAY;CAGzC,MAAM,YAAY,MAAM,SAAqC,KAAK,OAAO,cAAc,CAAC;CACxF,MAAM,SAA4B;EAChC,SAAS,WAAW,WAAW;EAC/B,OAAQ,WAAW,SAAS;EAC5B,UAAW,WAAW,YAAY;EAClC,SAAS;GACP,SAAS,WAAW,SAAS,WAAW;GACxC,WAAW,WAAW,SAAS,aAAa,CAAC,WAAW,SAAS,WAAW,KAAK;GAClF;EACD,SAAS,WAAW,WAAW,EAAE;EACjC,YAAY,WAAW;EACvB,aAAa,WAAW;EACxB,KAAK,WAAW;EAChB,iBAAiB,WAAW;EAC7B;CAGD,MAAM,YAAY,KAAK,OAAO,SAAS;CACvC,MAAM,cAAc,MAAM,QAAQ,UAAU,EAAE,QAAO,MAAK,EAAE,SAAS,QAAQ,CAAC;CAI9E,MAAM,UAHe,MAAM,QAAQ,IACjC,WAAW,KAAI,SAAQ,SAA0B,KAAK,WAAW,KAAK,CAAC,CAAC,CACzE,EAEE,QAAQ,MAA4B,KAAK,QAAQ,QAAQ,EAAE,GAAG,CAAC,CAC/D,UAAU,GAAG,MAAM,EAAE,GAAG,cAAc,EAAE,IAAI,KAAK,CAAC;AAQrD,QAAO;EAAE;EAAQ;EAAQ,eALT,MAAM,QAAQ,IAC5B,OAAO,KAAI,UAAS,gBAAgB,aAAa,OAAO,OAAO,CAAC,CACjE,EAC4B,MAAM;EAEI;;AAGzC,SAAS,cAAc,aAAqB,OAAgC;AAC1E,KAAI,MAAM,aAAc,QAAO,KAAK,aAAa,MAAM,aAAa;AACpE,QAAO,KAAK,eAAe,YAAY,EAAE,WAAW,MAAM,QAAQ,MAAM,GAAG;;AAG7E,SAAS,kBAAkB,OAAwC;AACjE,QAAO,MAAM,mBAAmB;;AAGlC,SAAS,aAAa,KAAa,OAAwB,QAAwB;AAEjF,SADiB,kBAAkB,MAAM,EACzC;EACE,KAAK,SAAU,QAAO,KAAK,KAAK,GAAG,MAAM,GAAG,GAAG,OAAO,OAAO;EAC7D,KAAK,YAAa,QAAO,KAAK,KAAK,QAAQ,GAAG,MAAM,GAAG,OAAO;EAC9D,KAAK,OAAQ,QAAO,KAAK,KAAK,GAAG,MAAM,GAAG,OAAO;EAEjD,QAAS,QAAO,KAAK,KAAK,GAAG,OAAO,OAAO;;;AAI/C,eAAe,gBACb,aACA,OACA,QAC2B;CAC3B,MAAM,MAAM,cAAc,aAAa,MAAM;AAE7C,KAAI,MAAM,SAAS,WACjB,QAAO,iBAAiB,KAAK,OAAO,OAAO;AAI7C,KAAI,MAAM,KASR,SARgB,MAAM,QAAQ,IAC5B,OAAO,QAAQ,UAAU,IAAI,OAAO,WAAW;EAC7C,MAAM,WAAW,aAAa,KAAK,OAAO,OAAO;AAEjD,MADgB,MAAM,SAAS,SAAS,KACxB,KAAM,QAAO;AAC7B,SAAO;GAAE,SAAS,MAAM;GAAI;GAAQ;GAAU,MAAM,MAAM;GAAM;GAChE,CACH,EACc,OAAO,QAAQ;CAIhC,MAAM,WAAW,KAAK,KAAK,YAAY;AAEvC,KADgB,MAAM,SAAS,SAAS,KACxB,KAAM,QAAO,EAAE;AAC/B,QAAO,CAAC;EAAE,SAAS,MAAM;EAAI,QAAQ;EAAM;EAAU,MAAM,MAAM;EAAM,CAAC;;AAG1E,eAAe,iBACb,KACA,OACA,QAC2B;CAC3B,MAAM,WAAW,kBAAkB,MAAM;AAEzC,KAAI,CAAC,MAAM,KAGT,SADe,MAAM,QAAQ,IAAI,EAAE,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CACpD,KAAI,OAAM;EACrB,SAAS,MAAM;EACf,QAAQ;EACR,UAAU,KAAK,KAAK,EAAE;EACtB,MAAM;EACN,MAAM,EAAE,QAAQ,OAAO,GAAG;EAC3B,EAAE;AAOL,SAHsB,MAAM,QAAQ,IAClC,OAAO,QAAQ,UAAU,KAAI,WAAU,kBAAkB,KAAK,OAAO,QAAQ,SAAS,CAAC,CACxF,EACoB,MAAM;;AAG7B,eAAe,kBACb,KACA,OACA,QACA,UAC2B;AAC3B,KAAI,aAAa,QAAQ;EAEvB,MAAM,UAAU,MAAM,QAAQ,IAAI;AASlC,UARgB,MAAM,QAAQ,IAC5B,QAAQ,IAAI,OAAO,UAAU;GAC3B,MAAM,WAAW,KAAK,KAAK,OAAO,GAAG,OAAO,KAAK;AAEjD,OADgB,MAAM,SAAS,SAAS,KACxB,KAAM,QAAO;AAC7B,UAAO;IAAE,SAAS,MAAM;IAAI;IAAQ;IAAU,MAAM;IAAY,MAAM;IAAO;IAC7E,CACH,EACc,OAAO,QAAQ;;AAGhC,KAAI,aAAa,aAAa;EAE5B,MAAM,YAAY,KAAK,KAAK,OAAO;AAEnC,UADe,MAAM,QAAQ,UAAU,EAAE,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CAC1D,KAAI,OAAM;GACrB,SAAS,MAAM;GACf;GACA,UAAU,KAAK,WAAW,EAAE;GAC5B,MAAM;GACN,MAAM,EAAE,QAAQ,OAAO,GAAG;GAC3B,EAAE;;AAGL,KAAI,aAAa,UAAU;EAEzB,MAAM,SAAS,IAAI,OAAO;AAE1B,UADe,MAAM,QAAQ,IAAI,EAAE,QAAO,MAAK,EAAE,SAAS,OAAO,CAAC,CACrD,KAAI,OAAM;GACrB,SAAS,MAAM;GACf;GACA,UAAU,KAAK,KAAK,EAAE;GACtB,MAAM;GACN,MAAM,EAAE,MAAM,GAAG,CAAC,OAAO,OAAO;GACjC,EAAE;;AAGL,QAAO,EAAE;;;;AC9JX,MAAM,oBAAoB,IAAI,IAAI;CAChC;CAAU;CAAQ;CAAS;CAAO;CAAQ;CAAS;CAAS;CAAQ;CACpE;CAAY;CAAY;CAAQ;CAAY;CAAS;CAAS;CAC9D;CAAU;CACX,CAAC;AAEF,SAAgB,oBAAoB,OAAqC;CACvE,MAAM,uBAAO,IAAI,KAAa;AAC9B,KAAI,MAAM;OACH,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,CACtD,KAAI,kBAAkB,IAAI,MAAM,KAAK,CAAE,MAAK,IAAI,KAAK;;AAGzD,QAAO;;AAIT,SAAgB,iBAAiB,MAAc,6BAA0B,IAAI,KAAK,EAA0D;CAC1I,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC9C,MAAM,QAAQ,WAAW,MAAM,qCAAqC;AACpE,KAAI,CAAC,MAAO,QAAO;EAAE,aAAa,EAAE;EAAE,MAAM;EAAY;CAExD,MAAM,QAAQ,MAAM;CACpB,MAAM,OAAO,MAAM,GAAI,MAAM;CAC7B,MAAM,cAAuC,EAAE;CAG/C,MAAM,QAAQ,MAAM,MAAM,KAAK;CAC/B,MAAM,QAAiE,CAAC;EAAE,KAAK;EAAa,QAAQ;EAAI,CAAC;AAEzG,MAAK,MAAM,CAAC,WAAW,SAAS,MAAM,SAAS,EAAE;AAE/C,MAAI,KAAK,MAAM,KAAK,GAAI;EAGxB,MAAM,aAAa,KAAK,MAAM,kBAAkB;AAChD,MAAI,YAAY;GACd,MAAM,YAAY,WAAW,GAAI;GACjC,MAAM,QAAQ,WAAW,GAAI,MAAM;AAEnC,UAAO,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,GAAI,UAAU,UAC5D,OAAM,KAAK;GAEb,MAAM,SAAS,MAAM,MAAM,SAAS,GAAI;GACxC,MAAM,UAAU,OAAO,KAAK,OAAO,CAAC,KAAK;AACzC,OAAI,WAAW,MAAM,QAAQ,OAAO,SAAS,CAC1C,QAAO,SAAuB,KAAK,WAAW,MAAM,CAAC;AAExD;;EAIF,MAAM,UAAU,KAAK,MAAM,kCAAkC;AAC7D,MAAI,CAAC,QAAS;EAEd,MAAM,WAAW,QAAQ,GAAI;EAC7B,MAAM,MAAM,QAAQ;EACpB,MAAM,WAAW,QAAQ,GAAI,MAAM;AAGnC,SAAO,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,GAAI,UAAU,SAC5D,OAAM,KAAK;EAEb,MAAM,UAAU,MAAM,MAAM,SAAS,GAAI;AAEzC,MAAI,aAAa,IAAI;GAWnB,IAAI,WAAW;AACf,QAAK,IAAI,IAAI,YAAY,GAAG,IAAI,MAAM,QAAQ,KAAK,EACjD,KAAI,MAAM,GAAI,MAAM,KAAK,IAAI;AAC3B,eAAW,MAAM;AACjB;;GAGJ,MAAM,aAAa,SAAS,SAAS,SAAS,WAAW,CAAC;AAC1D,OAAI,SAAS,MAAM,CAAC,WAAW,IAAI,CACjC,SAAQ,OAAO,EAAE;YACR,aAAa,MAAM,aAAa,UAAU;IACnD,MAAM,SAAkC,EAAE;AAC1C,YAAQ,OAAO;AACf,UAAM,KAAK;KAAE,KAAK;KAAQ,QAAQ;KAAU,CAAC;SAE7C,SAAQ,OAAO,EAAE;AAEnB;;AAGF,MAAI,SAAS,WAAW,IAAI,IAAI,SAAS,SAAS,IAAI,EAAE;GACtD,MAAM,QAAQ,SAAS,MAAM,GAAG,GAAG,CAAC,MAAM;AAC1C,WAAQ,OAAO,UAAU,KACrB,EAAE,GACF,qBAAqB,MAAM,CAAC,KAAI,SAAQ,6BAA6B,KAAK,MAAM,CAAC,CAAC;AACtF;;AAKF,UAAQ,OAAO,WAAW,UADN,YAAY,eAAe,WAAW,IAAI,IAAI,CAClB;;AAGlD,QAAO;EAAE;EAAa;EAAM;;AAG9B,SAAS,WAAW,KAAa,cAAc,OAAgB;AAC7D,QAAO,cAAc,6BAA6B,IAAI,GAAG,uBAAuB,IAAI"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as readJson, c as isPublishedAt, i as readDir, l as publicationContext, n as stringLikeFieldKeys, o as readText, r as readProjectManifest, s as writeText, t as parseFrontmatter, u as publicationMeta } from "./frontmatter-
|
|
1
|
+
import { a as readJson, c as isPublishedAt, i as readDir, l as publicationContext, n as stringLikeFieldKeys, o as readText, r as readProjectManifest, s as writeText, t as parseFrontmatter, u as publicationMeta } from "./frontmatter-D9yVqC5y.mjs";
|
|
2
2
|
import { rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/generator/type-emitter.ts
|
|
@@ -93,8 +93,10 @@ function emitTypes(models, hasMedia = false) {
|
|
|
93
93
|
lines.push("export declare function document(model: string): DocumentQuery<Record<string, unknown>>");
|
|
94
94
|
lines.push("");
|
|
95
95
|
if (hasMedia) {
|
|
96
|
-
lines.push("/** Resolve a stored `media/...` path to its absolute delivery URL. External URLs and already-absolute values pass through unchanged. */");
|
|
97
|
-
lines.push("export declare function media(value: string): string");
|
|
96
|
+
lines.push("/** Resolve a stored `media/...` path to its absolute delivery URL. External URLs and already-absolute values pass through unchanged. An optional second argument overrides the configured base for this call — e.g. a value read from framework runtime config. */");
|
|
97
|
+
lines.push("export declare function media(value: string, baseOverride?: string): string");
|
|
98
|
+
lines.push("/** Resolve every `](media/...)` reference inside a markdown string to an absolute delivery URL. Same override rules as `media()`. */");
|
|
99
|
+
lines.push("export declare function mediaBody(markdown: string, baseOverride?: string): string");
|
|
98
100
|
lines.push("");
|
|
99
101
|
}
|
|
100
102
|
lines.push("export interface ContentrainClient {");
|
|
@@ -106,6 +108,10 @@ function emitTypes(models, hasMedia = false) {
|
|
|
106
108
|
lines.push(" dictionary(model: string): DictionaryAccessor");
|
|
107
109
|
for (const m of documents) lines.push(` document(model: '${m.id}'): DocumentQuery<${kebabToPascal(m.id)}>`);
|
|
108
110
|
lines.push(" document(model: string): DocumentQuery<Record<string, unknown>>");
|
|
111
|
+
if (hasMedia) {
|
|
112
|
+
lines.push(" media(value: string, baseOverride?: string): string");
|
|
113
|
+
lines.push(" mediaBody(markdown: string, baseOverride?: string): string");
|
|
114
|
+
}
|
|
109
115
|
lines.push("}");
|
|
110
116
|
lines.push("");
|
|
111
117
|
lines.push("export declare function createContentrainClient(): ContentrainClient");
|
|
@@ -263,7 +269,7 @@ async function emitSingleModule(ref, models, publication) {
|
|
|
263
269
|
}
|
|
264
270
|
//#endregion
|
|
265
271
|
//#region src/generator/runtime-emitter.ts
|
|
266
|
-
function emitRuntimeModule(models, dataModules, defaultLocale,
|
|
272
|
+
function emitRuntimeModule(models, dataModules, defaultLocale, mediaBaseUrl) {
|
|
267
273
|
const lines = [
|
|
268
274
|
"/* eslint-disable */",
|
|
269
275
|
"/* oxlint-disable */",
|
|
@@ -274,8 +280,8 @@ function emitRuntimeModule(models, dataModules, defaultLocale, cdnBaseUrl) {
|
|
|
274
280
|
lines.push(`const _defaultLocale = '${defaultLocale}'`);
|
|
275
281
|
lines.push("");
|
|
276
282
|
}
|
|
277
|
-
if (
|
|
278
|
-
lines.push(`const _mediaBase = ${JSON.stringify(
|
|
283
|
+
if (mediaBaseUrl) {
|
|
284
|
+
lines.push(`const _mediaBase = ${JSON.stringify(mediaBaseUrl.replace(/\/+$/, ""))}`);
|
|
279
285
|
lines.push("");
|
|
280
286
|
}
|
|
281
287
|
for (const dm of dataModules) {
|
|
@@ -425,12 +431,19 @@ function emitRuntimeModule(models, dataModules, defaultLocale, cdnBaseUrl) {
|
|
|
425
431
|
lines.push("}");
|
|
426
432
|
lines.push("");
|
|
427
433
|
}
|
|
428
|
-
if (
|
|
434
|
+
if (mediaBaseUrl) {
|
|
429
435
|
lines.push("// ─── Media ───");
|
|
430
436
|
lines.push("");
|
|
431
|
-
lines.push("export function media(value) {");
|
|
437
|
+
lines.push("export function media(value, baseOverride) {");
|
|
432
438
|
lines.push(" if (typeof value !== 'string' || !value.startsWith('media/')) return value");
|
|
433
|
-
lines.push("
|
|
439
|
+
lines.push(" var base = baseOverride ? String(baseOverride).replace(/\\/+$/, '') : _mediaBase");
|
|
440
|
+
lines.push(" return base + '/' + value");
|
|
441
|
+
lines.push("}");
|
|
442
|
+
lines.push("");
|
|
443
|
+
lines.push("export function mediaBody(markdown, baseOverride) {");
|
|
444
|
+
lines.push(" if (typeof markdown !== 'string') return markdown");
|
|
445
|
+
lines.push(" var base = baseOverride ? String(baseOverride).replace(/\\/+$/, '') : _mediaBase");
|
|
446
|
+
lines.push(" return markdown.replace(/\\]\\((media\\/[^)\\s]+)\\)/g, function (_m, p) { return '](' + base + '/' + p + ')' })");
|
|
434
447
|
lines.push("}");
|
|
435
448
|
lines.push("");
|
|
436
449
|
}
|
|
@@ -442,7 +455,7 @@ function emitCjsWrapper(models, hasMedia = false) {
|
|
|
442
455
|
if (models.some((m) => m.kind === "singleton")) exports.push("singleton");
|
|
443
456
|
if (models.some((m) => m.kind === "dictionary")) exports.push("dictionary");
|
|
444
457
|
if (models.some((m) => m.kind === "document")) exports.push("document");
|
|
445
|
-
if (hasMedia) exports.push("media");
|
|
458
|
+
if (hasMedia) exports.push("media", "mediaBody");
|
|
446
459
|
return `/* eslint-disable */
|
|
447
460
|
/* oxlint-disable */
|
|
448
461
|
// Auto-generated CJS proxy — delegates to ESM via dynamic import()
|
|
@@ -674,11 +687,11 @@ async function generate(options) {
|
|
|
674
687
|
const clientDir = join(projectRoot, ".contentrain", "client");
|
|
675
688
|
const dataDir = join(clientDir, "data");
|
|
676
689
|
const manifest = await readProjectManifest(projectRoot);
|
|
677
|
-
const
|
|
678
|
-
const hasMedia = Boolean(
|
|
690
|
+
const mediaBaseUrl = options.mediaBaseUrl ?? options.cdnBaseUrl ?? manifest.config.cdn?.url;
|
|
691
|
+
const hasMedia = Boolean(mediaBaseUrl);
|
|
679
692
|
const dataModules = await emitDataModules(manifest.models, manifest.contentFiles, publicationContext(projectRoot, manifest.config.locales.default, options));
|
|
680
693
|
const typesContent = emitTypes(manifest.models, hasMedia);
|
|
681
|
-
const runtimeContent = emitRuntimeModule(manifest.models, dataModules, manifest.config.locales.default,
|
|
694
|
+
const runtimeContent = emitRuntimeModule(manifest.models, dataModules, manifest.config.locales.default, mediaBaseUrl);
|
|
682
695
|
const cjsContent = emitCjsWrapper(manifest.models, hasMedia);
|
|
683
696
|
const newFileNames = new Set(dataModules.map((dm) => dm.fileName));
|
|
684
697
|
try {
|
|
@@ -708,4 +721,4 @@ async function generate(options) {
|
|
|
708
721
|
//#endregion
|
|
709
722
|
export { generate as t };
|
|
710
723
|
|
|
711
|
-
//# sourceMappingURL=generate-
|
|
724
|
+
//# sourceMappingURL=generate-BOi6o7Z5.mjs.map
|