@contentrain/query 7.2.3 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +27 -0
  2. package/dist/astro/index.cjs +12 -6
  3. package/dist/astro/index.d.cts +3 -1
  4. package/dist/astro/index.d.cts.map +1 -1
  5. package/dist/astro/index.d.mts +3 -1
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +12 -6
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/cdn/index.d.cts +1 -1
  10. package/dist/cdn/index.d.mts +1 -1
  11. package/dist/cli.cjs +24 -6
  12. package/dist/cli.mjs +24 -6
  13. package/dist/cli.mjs.map +1 -1
  14. package/dist/{frontmatter-DSFLOTwS.cjs → frontmatter-BxZi5QMk.cjs} +68 -1
  15. package/dist/{frontmatter-CVjPyhOq.mjs → frontmatter-D-QXY_WH.mjs} +52 -3
  16. package/dist/frontmatter-D-QXY_WH.mjs.map +1 -0
  17. package/dist/{generate-B7jooE0f.mjs → generate-CT6gqQfR.mjs} +13 -9
  18. package/dist/generate-CT6gqQfR.mjs.map +1 -0
  19. package/dist/{generate-D3C6itur.cjs → generate-D0yAmE56.cjs} +12 -8
  20. package/dist/generator/generate.cjs +1 -1
  21. package/dist/generator/generate.d.cts +3 -1
  22. package/dist/generator/generate.d.cts.map +1 -1
  23. package/dist/generator/generate.d.mts +3 -1
  24. package/dist/generator/generate.d.mts.map +1 -1
  25. package/dist/generator/generate.mjs +1 -1
  26. package/dist/{index-Cjj6FPwV.d.cts → index-D-hSDfue.d.cts} +1 -1
  27. package/dist/{index-Cjj6FPwV.d.cts.map → index-D-hSDfue.d.cts.map} +1 -1
  28. package/dist/{index-hug7w9_n.d.mts → index-DWpmPtUW.d.mts} +1 -1
  29. package/dist/{index-hug7w9_n.d.mts.map → index-DWpmPtUW.d.mts.map} +1 -1
  30. package/dist/index.d.cts +1 -1
  31. package/dist/index.d.mts +1 -1
  32. package/dist/publication-Ba8bLPAO.d.mts +10 -0
  33. package/dist/publication-Ba8bLPAO.d.mts.map +1 -0
  34. package/dist/publication-DW0WaAuC.d.cts +10 -0
  35. package/dist/publication-DW0WaAuC.d.cts.map +1 -0
  36. package/package.json +2 -2
  37. package/dist/frontmatter-CVjPyhOq.mjs.map +0 -1
  38. package/dist/generate-B7jooE0f.mjs.map +0 -1
package/README.md CHANGED
@@ -581,3 +581,30 @@ Full documentation at **[ai.contentrain.io/packages/sdk](https://ai.contentrain.
581
581
  ## 📄 License
582
582
 
583
583
  MIT
584
+
585
+
586
+ ### Public builds and scheduled publication
587
+
588
+ Editorial generation keeps all content by default. For a deployed static site,
589
+ use `contentrain-query generate --published` (or `contentrain generate --published`).
590
+ Only published entries inside their `publish_at`/`expire_at` window are included.
591
+ Legacy entries without metadata remain visible, matching Studio CDN behavior.
592
+ Invalid schedule values are excluded; malformed metadata stops the build.
593
+
594
+ `--at 2026-10-01T12:00:00Z` fixes the clock for a reproducible public build and
595
+ implies `--published`. The programmatic equivalents are
596
+ `generate({ projectRoot, publishedOnly: true })` and
597
+ `generate({ projectRoot, at: '2026-10-01T12:00:00Z' })`.
598
+
599
+ Astro content collections use the same filter:
600
+
601
+ ```ts
602
+ contentrainLoader({ model: 'posts', locale: 'en', publishedOnly: true })
603
+ ```
604
+
605
+ Publication begins exactly at `publish_at` and ends exactly at `expire_at`.
606
+ All four model kinds read canonical `.contentrain/meta` files, including content
607
+ stored at custom paths. Metadata is not copied into public content fields.
608
+ Watch mode reacts to metadata edits. Time passing alone does not redeploy a
609
+ static site: schedule a build and deployment at each deadline, with retries.
610
+ Use a fixed `at` for testing/reproduction, not a permanent deployment clock.
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_frontmatter = require("../frontmatter-DSFLOTwS.cjs");
2
+ const require_frontmatter = require("../frontmatter-BxZi5QMk.cjs");
3
3
  let node_path = require("node:path");
4
4
  let node_url = require("node:url");
5
5
  //#region src/astro/loader.ts
@@ -38,7 +38,8 @@ function contentrainLoader(options) {
38
38
  const refs = manifest.contentFiles.filter((ref) => ref.modelId === model.id && (options.locale === void 0 || ref.locale === options.locale));
39
39
  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
40
  const prefixLocale = model.i18n === true && options.locale === void 0;
41
- const entries = (await Promise.all(refs.map((ref) => entriesOf(ref, model, prefixLocale)))).flat();
41
+ const publication = require_frontmatter.publicationContext(root, manifest.config.locales.default, options);
42
+ const entries = (await Promise.all(refs.map((ref) => entriesOf(ref, model, prefixLocale, publication)))).flat();
42
43
  const siteRoot = toPath(context.config?.root);
43
44
  context.store.clear();
44
45
  for (const entry of entries) {
@@ -62,9 +63,11 @@ function contentrainLoader(options) {
62
63
  watching = true;
63
64
  const paths = new Set(refs.map((ref) => ref.filePath));
64
65
  for (const path of paths) context.watcher.add(path);
66
+ const metaDir = (0, node_path.join)(root, ".contentrain", "meta", model.id);
67
+ context.watcher.add(metaDir);
65
68
  context.watcher.add((0, node_path.join)(root, ".contentrain", "models", `${model.id}.json`));
66
69
  const reload = (path) => {
67
- if (!paths.has(path) && !path.endsWith(`${model.id}.json`)) return;
70
+ if (!paths.has(path) && !path.startsWith(metaDir + "/") && !path.endsWith(`${model.id}.json`)) return;
68
71
  this.load(context);
69
72
  };
70
73
  context.watcher.on("change", reload);
@@ -80,7 +83,10 @@ function toPath(root) {
80
83
  if (typeof root !== "string") return (0, node_url.fileURLToPath)(root);
81
84
  return root.startsWith("file:") ? (0, node_url.fileURLToPath)(root) : root;
82
85
  }
83
- async function entriesOf(ref, model, prefixLocale) {
86
+ async function entriesOf(ref, model, prefixLocale, publication) {
87
+ const meta = publication ? await require_frontmatter.publicationMeta(ref, model, publication) : void 0;
88
+ const visible = (id) => !publication || require_frontmatter.isPublishedAt(id === void 0 ? meta : meta?.[id], publication.at);
89
+ if ((model.kind === "singleton" || model.kind === "document") && !visible()) return [];
84
90
  const prefix = prefixLocale && ref.locale ? `${ref.locale}/` : "";
85
91
  const withLocale = (data) => ref.locale === null ? data : {
86
92
  ...data,
@@ -90,7 +96,7 @@ async function entriesOf(ref, model, prefixLocale) {
90
96
  case "collection": {
91
97
  const raw = await require_frontmatter.readJson(ref.filePath);
92
98
  if (!raw) return [];
93
- return Object.entries(raw).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([id, fields]) => ({
99
+ return Object.entries(raw).filter(([id]) => visible(id)).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([id, fields]) => ({
94
100
  id: `${prefix}${id}`,
95
101
  data: withLocale({
96
102
  id,
@@ -111,7 +117,7 @@ async function entriesOf(ref, model, prefixLocale) {
111
117
  case "dictionary": {
112
118
  const raw = await require_frontmatter.readJson(ref.filePath);
113
119
  if (!raw) return [];
114
- return Object.entries(raw).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([key, value]) => ({
120
+ return Object.entries(raw).filter(([id]) => visible(id)).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([key, value]) => ({
115
121
  id: `${prefix}${key}`,
116
122
  data: withLocale({
117
123
  key,
@@ -1,3 +1,5 @@
1
+ import { t as PublicationOptions } from "../publication-DW0WaAuC.cjs";
2
+
1
3
  //#region src/astro/loader.d.ts
2
4
  /** The slice of Astro's `LoaderContext` this loader uses. */
3
5
  interface ContentrainLoaderContext {
@@ -43,7 +45,7 @@ interface ContentrainLoader {
43
45
  name: string;
44
46
  load: (context: ContentrainLoaderContext) => Promise<void>;
45
47
  }
46
- interface ContentrainLoaderOptions {
48
+ interface ContentrainLoaderOptions extends PublicationOptions {
47
49
  /** Model id — the `id` in `.contentrain/models/<id>.json`. */
48
50
  model: string;
49
51
  /** Project root holding `.contentrain`. Default: `process.cwd()`. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;UAsBiB,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;EAN3C;EAQJ,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;EAXkF;EAajG,KAAA;EAZkB;EAclB,IAAA;EAdgD;;;;;EAoBhD,MAAA;AAAA;;;;;;AAfF;;;;;;;;;;;AAKA;;;;iBAwCgB,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,iBAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;;;UAuBiB,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;AAAA;;;;;;;AAfF;;;;;;;;;;;AAKA;;;iBAwCgB,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,iBAAA"}
@@ -1,3 +1,5 @@
1
+ import { t as PublicationOptions } from "../publication-Ba8bLPAO.mjs";
2
+
1
3
  //#region src/astro/loader.d.ts
2
4
  /** The slice of Astro's `LoaderContext` this loader uses. */
3
5
  interface ContentrainLoaderContext {
@@ -43,7 +45,7 @@ interface ContentrainLoader {
43
45
  name: string;
44
46
  load: (context: ContentrainLoaderContext) => Promise<void>;
45
47
  }
46
- interface ContentrainLoaderOptions {
48
+ interface ContentrainLoaderOptions extends PublicationOptions {
47
49
  /** Model id — the `id` in `.contentrain/models/<id>.json`. */
48
50
  model: string;
49
51
  /** Project root holding `.contentrain`. Default: `process.cwd()`. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;UAsBiB,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;EAN3C;EAQJ,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;EAXkF;EAajG,KAAA;EAZkB;EAclB,IAAA;EAdgD;;;;;EAoBhD,MAAA;AAAA;;;;;;AAfF;;;;;;;;;;;AAKA;;;;iBAwCgB,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,iBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/astro/loader.ts"],"mappings":";;;;UAuBiB,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;AAAA;;;;;;;AAfF;;;;;;;;;;;AAKA;;;iBAwCgB,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,iBAAA"}
@@ -1,4 +1,4 @@
1
- import { a as readJson, n as stringLikeFieldKeys, o as readText, r as readProjectManifest, t as parseFrontmatter } from "../frontmatter-CVjPyhOq.mjs";
1
+ 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-D-QXY_WH.mjs";
2
2
  import { isAbsolute, join, relative } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  //#region src/astro/loader.ts
@@ -37,7 +37,8 @@ function contentrainLoader(options) {
37
37
  const refs = manifest.contentFiles.filter((ref) => ref.modelId === model.id && (options.locale === void 0 || ref.locale === options.locale));
38
38
  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
39
  const prefixLocale = model.i18n === true && options.locale === void 0;
40
- const entries = (await Promise.all(refs.map((ref) => entriesOf(ref, model, prefixLocale)))).flat();
40
+ const publication = publicationContext(root, manifest.config.locales.default, options);
41
+ const entries = (await Promise.all(refs.map((ref) => entriesOf(ref, model, prefixLocale, publication)))).flat();
41
42
  const siteRoot = toPath(context.config?.root);
42
43
  context.store.clear();
43
44
  for (const entry of entries) {
@@ -61,9 +62,11 @@ function contentrainLoader(options) {
61
62
  watching = true;
62
63
  const paths = new Set(refs.map((ref) => ref.filePath));
63
64
  for (const path of paths) context.watcher.add(path);
65
+ const metaDir = join(root, ".contentrain", "meta", model.id);
66
+ context.watcher.add(metaDir);
64
67
  context.watcher.add(join(root, ".contentrain", "models", `${model.id}.json`));
65
68
  const reload = (path) => {
66
- if (!paths.has(path) && !path.endsWith(`${model.id}.json`)) return;
69
+ if (!paths.has(path) && !path.startsWith(metaDir + "/") && !path.endsWith(`${model.id}.json`)) return;
67
70
  this.load(context);
68
71
  };
69
72
  context.watcher.on("change", reload);
@@ -79,7 +82,10 @@ function toPath(root) {
79
82
  if (typeof root !== "string") return fileURLToPath(root);
80
83
  return root.startsWith("file:") ? fileURLToPath(root) : root;
81
84
  }
82
- async function entriesOf(ref, model, prefixLocale) {
85
+ async function entriesOf(ref, model, prefixLocale, publication) {
86
+ const meta = publication ? await publicationMeta(ref, model, publication) : void 0;
87
+ const visible = (id) => !publication || isPublishedAt(id === void 0 ? meta : meta?.[id], publication.at);
88
+ if ((model.kind === "singleton" || model.kind === "document") && !visible()) return [];
83
89
  const prefix = prefixLocale && ref.locale ? `${ref.locale}/` : "";
84
90
  const withLocale = (data) => ref.locale === null ? data : {
85
91
  ...data,
@@ -89,7 +95,7 @@ async function entriesOf(ref, model, prefixLocale) {
89
95
  case "collection": {
90
96
  const raw = await readJson(ref.filePath);
91
97
  if (!raw) return [];
92
- return Object.entries(raw).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([id, fields]) => ({
98
+ return Object.entries(raw).filter(([id]) => visible(id)).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([id, fields]) => ({
93
99
  id: `${prefix}${id}`,
94
100
  data: withLocale({
95
101
  id,
@@ -110,7 +116,7 @@ async function entriesOf(ref, model, prefixLocale) {
110
116
  case "dictionary": {
111
117
  const raw = await readJson(ref.filePath);
112
118
  if (!raw) return [];
113
- return Object.entries(raw).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([key, value]) => ({
119
+ return Object.entries(raw).filter(([id]) => visible(id)).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([key, value]) => ({
114
120
  id: `${prefix}${key}`,
115
121
  data: withLocale({
116
122
  key,
@@ -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 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 {\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 perFile = await Promise.all(refs.map(ref => entriesOf(ref, model, prefixLocale)))\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 context.watcher.add(join(root, '.contentrain', 'models', `${model.id}.json`))\n const reload = (path: string) => {\n if (!paths.has(path) && !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): Promise<Entry[]> {\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 .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 .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 { 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,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-Cjj6FPwV.cjs";
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-D-hSDfue.cjs";
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 };
@@ -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-hug7w9_n.mjs";
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-DWpmPtUW.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
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- const require_generate = require("./generate-D3C6itur.cjs");
2
+ const require_generate = require("./generate-D0yAmE56.cjs");
3
3
  let node_path = require("node:path");
4
4
  let node_fs = require("node:fs");
5
5
  //#region src/cli.ts
6
6
  async function main() {
7
7
  const args = process.argv.slice(2);
8
8
  if (args[0] !== "generate" && args.length === 0) {
9
- console.log("Usage: contentrain-query generate [--root <path>] [--watch]");
9
+ console.log("Usage: contentrain-query generate [--root <path>] [--watch] [--published] [--at <ISO timestamp>]");
10
10
  console.log("");
11
11
  console.log("Commands:");
12
12
  console.log(" generate Generate typed client from .contentrain/ project files");
@@ -14,6 +14,8 @@ async function main() {
14
14
  console.log("Options:");
15
15
  console.log(" --root Project root directory (default: cwd)");
16
16
  console.log(" --watch Watch for changes and regenerate automatically");
17
+ console.log(" --published Include only published content within its publication window");
18
+ console.log(" --at Reproducible publication timestamp (implies --published)");
17
19
  process.exit(0);
18
20
  }
19
21
  const command = args[0] === "generate" ? "generate" : args[0];
@@ -26,20 +28,30 @@ async function main() {
26
28
  const projectRoot = rootArg ? (0, node_path.resolve)(rootArg) : process.cwd();
27
29
  const watchMode = args.includes("--watch");
28
30
  try {
29
- const result = await require_generate.generate({ projectRoot });
31
+ const atIndex = args.indexOf("--at");
32
+ const at = atIndex < 0 ? void 0 : args[atIndex + 1];
33
+ if (atIndex >= 0 && (!at || at.startsWith("--"))) throw new Error("--at requires an ISO timestamp");
34
+ const publication = {
35
+ publishedOnly: args.includes("--published"),
36
+ at
37
+ };
38
+ const result = await require_generate.generate({
39
+ projectRoot,
40
+ ...publication
41
+ });
30
42
  console.log(`@contentrain/query — generated client`);
31
43
  console.log(` Models: ${result.typesCount}`);
32
44
  console.log(` Data modules: ${result.dataModulesCount}`);
33
45
  console.log(` Files: ${result.generatedFiles.length}`);
34
46
  if (result.packageJsonUpdated) console.log(` package.json: #contentrain imports added`);
35
47
  console.log(` Output: .contentrain/client/`);
36
- if (watchMode) startWatch(projectRoot);
48
+ if (watchMode) startWatch(projectRoot, publication);
37
49
  } catch (err) {
38
50
  console.error("Generate failed:", err.message);
39
51
  process.exit(1);
40
52
  }
41
53
  }
42
- function startWatch(projectRoot) {
54
+ function startWatch(projectRoot, publication) {
43
55
  const crDir = (0, node_path.join)(projectRoot, ".contentrain");
44
56
  const modelsDir = (0, node_path.join)(crDir, "models");
45
57
  const contentDir = (0, node_path.join)(crDir, "content");
@@ -47,7 +59,10 @@ function startWatch(projectRoot) {
47
59
  let debounceTimer = null;
48
60
  const regenerate = async () => {
49
61
  try {
50
- const result = await require_generate.generate({ projectRoot });
62
+ const result = await require_generate.generate({
63
+ projectRoot,
64
+ ...publication
65
+ });
51
66
  console.log(` Regenerated: ${result.typesCount} models, ${result.dataModulesCount} data modules`);
52
67
  } catch (err) {
53
68
  console.error(" Regenerate failed:", err.message);
@@ -67,6 +82,9 @@ function startWatch(projectRoot) {
67
82
  try {
68
83
  (0, node_fs.watch)(contentDir, { recursive: true }, onChange);
69
84
  } catch {}
85
+ try {
86
+ (0, node_fs.watch)((0, node_path.join)(crDir, "meta"), { recursive: true }, onChange);
87
+ } catch {}
70
88
  try {
71
89
  (0, node_fs.watch)(configFile, onChange);
72
90
  } catch {}
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { t as generate } from "./generate-B7jooE0f.mjs";
2
+ import { t as generate } from "./generate-CT6gqQfR.mjs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { watch } from "node:fs";
5
5
  //#region src/cli.ts
6
6
  async function main() {
7
7
  const args = process.argv.slice(2);
8
8
  if (args[0] !== "generate" && args.length === 0) {
9
- console.log("Usage: contentrain-query generate [--root <path>] [--watch]");
9
+ console.log("Usage: contentrain-query generate [--root <path>] [--watch] [--published] [--at <ISO timestamp>]");
10
10
  console.log("");
11
11
  console.log("Commands:");
12
12
  console.log(" generate Generate typed client from .contentrain/ project files");
@@ -14,6 +14,8 @@ async function main() {
14
14
  console.log("Options:");
15
15
  console.log(" --root Project root directory (default: cwd)");
16
16
  console.log(" --watch Watch for changes and regenerate automatically");
17
+ console.log(" --published Include only published content within its publication window");
18
+ console.log(" --at Reproducible publication timestamp (implies --published)");
17
19
  process.exit(0);
18
20
  }
19
21
  const command = args[0] === "generate" ? "generate" : args[0];
@@ -26,20 +28,30 @@ async function main() {
26
28
  const projectRoot = rootArg ? resolve(rootArg) : process.cwd();
27
29
  const watchMode = args.includes("--watch");
28
30
  try {
29
- const result = await generate({ projectRoot });
31
+ const atIndex = args.indexOf("--at");
32
+ const at = atIndex < 0 ? void 0 : args[atIndex + 1];
33
+ if (atIndex >= 0 && (!at || at.startsWith("--"))) throw new Error("--at requires an ISO timestamp");
34
+ const publication = {
35
+ publishedOnly: args.includes("--published"),
36
+ at
37
+ };
38
+ const result = await generate({
39
+ projectRoot,
40
+ ...publication
41
+ });
30
42
  console.log(`@contentrain/query — generated client`);
31
43
  console.log(` Models: ${result.typesCount}`);
32
44
  console.log(` Data modules: ${result.dataModulesCount}`);
33
45
  console.log(` Files: ${result.generatedFiles.length}`);
34
46
  if (result.packageJsonUpdated) console.log(` package.json: #contentrain imports added`);
35
47
  console.log(` Output: .contentrain/client/`);
36
- if (watchMode) startWatch(projectRoot);
48
+ if (watchMode) startWatch(projectRoot, publication);
37
49
  } catch (err) {
38
50
  console.error("Generate failed:", err.message);
39
51
  process.exit(1);
40
52
  }
41
53
  }
42
- function startWatch(projectRoot) {
54
+ function startWatch(projectRoot, publication) {
43
55
  const crDir = join(projectRoot, ".contentrain");
44
56
  const modelsDir = join(crDir, "models");
45
57
  const contentDir = join(crDir, "content");
@@ -47,7 +59,10 @@ function startWatch(projectRoot) {
47
59
  let debounceTimer = null;
48
60
  const regenerate = async () => {
49
61
  try {
50
- const result = await generate({ projectRoot });
62
+ const result = await generate({
63
+ projectRoot,
64
+ ...publication
65
+ });
51
66
  console.log(` Regenerated: ${result.typesCount} models, ${result.dataModulesCount} data modules`);
52
67
  } catch (err) {
53
68
  console.error(" Regenerate failed:", err.message);
@@ -67,6 +82,9 @@ function startWatch(projectRoot) {
67
82
  try {
68
83
  watch(contentDir, { recursive: true }, onChange);
69
84
  } catch {}
85
+ try {
86
+ watch(join(crDir, "meta"), { recursive: true }, onChange);
87
+ } catch {}
70
88
  try {
71
89
  watch(configFile, onChange);
72
90
  } catch {}
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { resolve, join } from 'node:path'\nimport { watch } from 'node:fs'\nimport { generate } from './generator/generate.js'\n\nasync function main() {\n const args = process.argv.slice(2)\n\n if (args[0] !== 'generate' && args.length === 0) {\n console.log('Usage: contentrain-query generate [--root <path>] [--watch]')\n console.log('')\n console.log('Commands:')\n console.log(' generate Generate typed client from .contentrain/ project files')\n console.log('')\n console.log('Options:')\n console.log(' --root Project root directory (default: cwd)')\n console.log(' --watch Watch for changes and regenerate automatically')\n process.exit(0)\n }\n\n const command = args[0] === 'generate' ? 'generate' : args[0]\n\n if (command !== 'generate') {\n console.error(`Unknown command: ${command}`)\n process.exit(1)\n }\n\n const rootIdx = args.indexOf('--root')\n const rootArg = rootIdx !== -1 ? args[rootIdx + 1] : undefined\n const projectRoot = rootArg ? resolve(rootArg) : process.cwd()\n const watchMode = args.includes('--watch')\n\n try {\n const result = await generate({ projectRoot })\n\n console.log(`@contentrain/query — generated client`)\n console.log(` Models: ${result.typesCount}`)\n console.log(` Data modules: ${result.dataModulesCount}`)\n console.log(` Files: ${result.generatedFiles.length}`)\n if (result.packageJsonUpdated) {\n console.log(` package.json: #contentrain imports added`)\n }\n console.log(` Output: .contentrain/client/`)\n\n if (watchMode) {\n startWatch(projectRoot)\n }\n } catch (err) {\n console.error('Generate failed:', (err as Error).message)\n process.exit(1)\n }\n}\n\nfunction startWatch(projectRoot: string) {\n const crDir = join(projectRoot, '.contentrain')\n const modelsDir = join(crDir, 'models')\n const contentDir = join(crDir, 'content')\n const configFile = join(crDir, 'config.json')\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n const regenerate = async () => {\n try {\n const result = await generate({ projectRoot })\n console.log(` Regenerated: ${result.typesCount} models, ${result.dataModulesCount} data modules`)\n } catch (err) {\n console.error(' Regenerate failed:', (err as Error).message)\n }\n }\n\n const onChange = (_event: string, filename: string | null) => {\n // Skip changes in the client output directory\n if (filename && filename.startsWith('client')) return\n\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n console.log(` Change detected: ${filename ?? 'unknown'}`)\n regenerate()\n }, 150)\n }\n\n try { watch(modelsDir, { recursive: true }, onChange) } catch { /* dir may not exist */ }\n try { watch(contentDir, { recursive: true }, onChange) } catch { /* dir may not exist */ }\n try { watch(configFile, onChange) } catch { /* file may not exist */ }\n\n console.log('')\n console.log('Watching for changes in .contentrain/ ...')\n console.log('Press Ctrl+C to stop.')\n}\n\nmain()\n"],"mappings":";;;;;AAMA,eAAe,OAAO;CACpB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,KAAI,KAAK,OAAO,cAAc,KAAK,WAAW,GAAG;AAC/C,UAAQ,IAAI,8DAA8D;AAC1E,UAAQ,IAAI,GAAG;AACf,UAAQ,IAAI,YAAY;AACxB,UAAQ,IAAI,uEAAuE;AACnF,UAAQ,IAAI,GAAG;AACf,UAAQ,IAAI,WAAW;AACvB,UAAQ,IAAI,sDAAsD;AAClE,UAAQ,IAAI,+DAA+D;AAC3E,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,OAAO,aAAa,aAAa,KAAK;AAE3D,KAAI,YAAY,YAAY;AAC1B,UAAQ,MAAM,oBAAoB,UAAU;AAC5C,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,QAAQ,SAAS;CACtC,MAAM,UAAU,YAAY,KAAK,KAAK,UAAU,KAAK,KAAA;CACrD,MAAM,cAAc,UAAU,QAAQ,QAAQ,GAAG,QAAQ,KAAK;CAC9D,MAAM,YAAY,KAAK,SAAS,UAAU;AAE1C,KAAI;EACF,MAAM,SAAS,MAAM,SAAS,EAAE,aAAa,CAAC;AAE9C,UAAQ,IAAI,wCAAwC;AACpD,UAAQ,IAAI,mBAAmB,OAAO,aAAa;AACnD,UAAQ,IAAI,mBAAmB,OAAO,mBAAmB;AACzD,UAAQ,IAAI,mBAAmB,OAAO,eAAe,SAAS;AAC9D,MAAI,OAAO,mBACT,SAAQ,IAAI,6CAA6C;AAE3D,UAAQ,IAAI,uCAAuC;AAEnD,MAAI,UACF,YAAW,YAAY;UAElB,KAAK;AACZ,UAAQ,MAAM,oBAAqB,IAAc,QAAQ;AACzD,UAAQ,KAAK,EAAE;;;AAInB,SAAS,WAAW,aAAqB;CACvC,MAAM,QAAQ,KAAK,aAAa,eAAe;CAC/C,MAAM,YAAY,KAAK,OAAO,SAAS;CACvC,MAAM,aAAa,KAAK,OAAO,UAAU;CACzC,MAAM,aAAa,KAAK,OAAO,cAAc;CAE7C,IAAI,gBAAsD;CAE1D,MAAM,aAAa,YAAY;AAC7B,MAAI;GACF,MAAM,SAAS,MAAM,SAAS,EAAE,aAAa,CAAC;AAC9C,WAAQ,IAAI,kBAAkB,OAAO,WAAW,WAAW,OAAO,iBAAiB,eAAe;WAC3F,KAAK;AACZ,WAAQ,MAAM,wBAAyB,IAAc,QAAQ;;;CAIjE,MAAM,YAAY,QAAgB,aAA4B;AAE5D,MAAI,YAAY,SAAS,WAAW,SAAS,CAAE;AAE/C,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,iBAAiB;AAC/B,WAAQ,IAAI,sBAAsB,YAAY,YAAY;AAC1D,eAAY;KACX,IAAI;;AAGT,KAAI;AAAE,QAAM,WAAW,EAAE,WAAW,MAAM,EAAE,SAAS;SAAS;AAC9D,KAAI;AAAE,QAAM,YAAY,EAAE,WAAW,MAAM,EAAE,SAAS;SAAS;AAC/D,KAAI;AAAE,QAAM,YAAY,SAAS;SAAS;AAE1C,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,4CAA4C;AACxD,SAAQ,IAAI,wBAAwB;;AAGtC,MAAM"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { resolve, join } from 'node:path'\nimport { watch } from 'node:fs'\nimport type { PublicationOptions } from './generator/publication.js'\nimport { generate } from './generator/generate.js'\n\nasync function main() {\n const args = process.argv.slice(2)\n\n if (args[0] !== 'generate' && args.length === 0) {\n console.log('Usage: contentrain-query generate [--root <path>] [--watch] [--published] [--at <ISO timestamp>]')\n console.log('')\n console.log('Commands:')\n console.log(' generate Generate typed client from .contentrain/ project files')\n console.log('')\n console.log('Options:')\n console.log(' --root Project root directory (default: cwd)')\n console.log(' --watch Watch for changes and regenerate automatically')\n console.log(' --published Include only published content within its publication window')\n console.log(' --at Reproducible publication timestamp (implies --published)')\n process.exit(0)\n }\n\n const command = args[0] === 'generate' ? 'generate' : args[0]\n\n if (command !== 'generate') {\n console.error(`Unknown command: ${command}`)\n process.exit(1)\n }\n\n const rootIdx = args.indexOf('--root')\n const rootArg = rootIdx !== -1 ? args[rootIdx + 1] : undefined\n const projectRoot = rootArg ? resolve(rootArg) : process.cwd()\n const watchMode = args.includes('--watch')\n\n try {\n const atIndex = args.indexOf('--at')\n const at = atIndex < 0 ? undefined : args[atIndex + 1]\n if (atIndex >= 0 && (!at || at.startsWith('--'))) throw new Error('--at requires an ISO timestamp')\n const publication = { publishedOnly: args.includes('--published'), at }\n const result = await generate({ projectRoot, ...publication })\n\n console.log(`@contentrain/query — generated client`)\n console.log(` Models: ${result.typesCount}`)\n console.log(` Data modules: ${result.dataModulesCount}`)\n console.log(` Files: ${result.generatedFiles.length}`)\n if (result.packageJsonUpdated) {\n console.log(` package.json: #contentrain imports added`)\n }\n console.log(` Output: .contentrain/client/`)\n\n if (watchMode) {\n startWatch(projectRoot, publication)\n }\n } catch (err) {\n console.error('Generate failed:', (err as Error).message)\n process.exit(1)\n }\n}\n\nfunction startWatch(projectRoot: string, publication: PublicationOptions) {\n const crDir = join(projectRoot, '.contentrain')\n const modelsDir = join(crDir, 'models')\n const contentDir = join(crDir, 'content')\n const configFile = join(crDir, 'config.json')\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n const regenerate = async () => {\n try {\n const result = await generate({ projectRoot, ...publication })\n console.log(` Regenerated: ${result.typesCount} models, ${result.dataModulesCount} data modules`)\n } catch (err) {\n console.error(' Regenerate failed:', (err as Error).message)\n }\n }\n\n const onChange = (_event: string, filename: string | null) => {\n // Skip changes in the client output directory\n if (filename && filename.startsWith('client')) return\n\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n console.log(` Change detected: ${filename ?? 'unknown'}`)\n regenerate()\n }, 150)\n }\n\n try { watch(modelsDir, { recursive: true }, onChange) } catch { /* dir may not exist */ }\n try { watch(contentDir, { recursive: true }, onChange) } catch { /* dir may not exist */ }\n try { watch(join(crDir, 'meta'), { recursive: true }, onChange) } catch { /* dir may not exist */ }\n try { watch(configFile, onChange) } catch { /* file may not exist */ }\n\n console.log('')\n console.log('Watching for changes in .contentrain/ ...')\n console.log('Press Ctrl+C to stop.')\n}\n\nmain()\n"],"mappings":";;;;;AAOA,eAAe,OAAO;CACpB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,KAAI,KAAK,OAAO,cAAc,KAAK,WAAW,GAAG;AAC/C,UAAQ,IAAI,mGAAmG;AAC/G,UAAQ,IAAI,GAAG;AACf,UAAQ,IAAI,YAAY;AACxB,UAAQ,IAAI,uEAAuE;AACnF,UAAQ,IAAI,GAAG;AACf,UAAQ,IAAI,WAAW;AACvB,UAAQ,IAAI,sDAAsD;AAClE,UAAQ,IAAI,+DAA+D;AAC3E,UAAQ,IAAI,6EAA6E;AACzF,UAAQ,IAAI,yEAAyE;AACrF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,OAAO,aAAa,aAAa,KAAK;AAE3D,KAAI,YAAY,YAAY;AAC1B,UAAQ,MAAM,oBAAoB,UAAU;AAC5C,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,QAAQ,SAAS;CACtC,MAAM,UAAU,YAAY,KAAK,KAAK,UAAU,KAAK,KAAA;CACrD,MAAM,cAAc,UAAU,QAAQ,QAAQ,GAAG,QAAQ,KAAK;CAC9D,MAAM,YAAY,KAAK,SAAS,UAAU;AAE1C,KAAI;EACF,MAAM,UAAU,KAAK,QAAQ,OAAO;EACpC,MAAM,KAAK,UAAU,IAAI,KAAA,IAAY,KAAK,UAAU;AACpD,MAAI,WAAW,MAAM,CAAC,MAAM,GAAG,WAAW,KAAK,EAAG,OAAM,IAAI,MAAM,iCAAiC;EACnG,MAAM,cAAc;GAAE,eAAe,KAAK,SAAS,cAAc;GAAE;GAAI;EACvE,MAAM,SAAS,MAAM,SAAS;GAAE;GAAa,GAAG;GAAa,CAAC;AAE9D,UAAQ,IAAI,wCAAwC;AACpD,UAAQ,IAAI,mBAAmB,OAAO,aAAa;AACnD,UAAQ,IAAI,mBAAmB,OAAO,mBAAmB;AACzD,UAAQ,IAAI,mBAAmB,OAAO,eAAe,SAAS;AAC9D,MAAI,OAAO,mBACT,SAAQ,IAAI,6CAA6C;AAE3D,UAAQ,IAAI,uCAAuC;AAEnD,MAAI,UACF,YAAW,aAAa,YAAY;UAE/B,KAAK;AACZ,UAAQ,MAAM,oBAAqB,IAAc,QAAQ;AACzD,UAAQ,KAAK,EAAE;;;AAInB,SAAS,WAAW,aAAqB,aAAiC;CACxE,MAAM,QAAQ,KAAK,aAAa,eAAe;CAC/C,MAAM,YAAY,KAAK,OAAO,SAAS;CACvC,MAAM,aAAa,KAAK,OAAO,UAAU;CACzC,MAAM,aAAa,KAAK,OAAO,cAAc;CAE7C,IAAI,gBAAsD;CAE1D,MAAM,aAAa,YAAY;AAC7B,MAAI;GACF,MAAM,SAAS,MAAM,SAAS;IAAE;IAAa,GAAG;IAAa,CAAC;AAC9D,WAAQ,IAAI,kBAAkB,OAAO,WAAW,WAAW,OAAO,iBAAiB,eAAe;WAC3F,KAAK;AACZ,WAAQ,MAAM,wBAAyB,IAAc,QAAQ;;;CAIjE,MAAM,YAAY,QAAgB,aAA4B;AAE5D,MAAI,YAAY,SAAS,WAAW,SAAS,CAAE;AAE/C,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,iBAAiB;AAC/B,WAAQ,IAAI,sBAAsB,YAAY,YAAY;AAC1D,eAAY;KACX,IAAI;;AAGT,KAAI;AAAE,QAAM,WAAW,EAAE,WAAW,MAAM,EAAE,SAAS;SAAS;AAC9D,KAAI;AAAE,QAAM,YAAY,EAAE,WAAW,MAAM,EAAE,SAAS;SAAS;AAC/D,KAAI;AAAE,QAAM,KAAK,OAAO,OAAO,EAAE,EAAE,WAAW,MAAM,EAAE,SAAS;SAAS;AACxE,KAAI;AAAE,QAAM,YAAY,SAAS;SAAS;AAE1C,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,4CAA4C;AACxD,SAAQ,IAAI,wBAAwB;;AAGtC,MAAM"}
@@ -1,5 +1,54 @@
1
- let node_path = require("node:path");
2
1
  let node_fs_promises = require("node:fs/promises");
2
+ let node_path = require("node:path");
3
+ //#region src/generator/publication.ts
4
+ function publicationContext(projectRoot, defaultLocale, options) {
5
+ if (!options.publishedOnly && options.at === void 0) return void 0;
6
+ if (options.at !== void 0 && !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/i.test(options.at)) throw new Error("Invalid publication --at timestamp: include an ISO date, time and timezone");
7
+ const at = options.at === void 0 ? Date.now() : Date.parse(options.at);
8
+ if (!Number.isFinite(at)) throw new Error("Invalid publication --at timestamp");
9
+ return {
10
+ projectRoot,
11
+ defaultLocale,
12
+ at
13
+ };
14
+ }
15
+ /** Canonical meta is independent of custom content paths and locale strategies. */
16
+ async function publicationMeta(ref, model, context) {
17
+ const locale = model.i18n ? ref.locale ?? context.defaultLocale : context.defaultLocale;
18
+ const parts = [
19
+ context.projectRoot,
20
+ ".contentrain",
21
+ "meta",
22
+ model.id
23
+ ];
24
+ if (model.kind === "document") parts.push(ref.slug ?? model.id);
25
+ const path = (0, node_path.join)(...parts, `${locale}.json`);
26
+ let text;
27
+ try {
28
+ text = await (0, node_fs_promises.readFile)(path, "utf8");
29
+ } catch (error) {
30
+ if (error.code === "ENOENT") return void 0;
31
+ throw error;
32
+ }
33
+ const meta = JSON.parse(text);
34
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) throw new Error(`Invalid publication metadata: ${path}`);
35
+ return meta;
36
+ }
37
+ /** Legacy content without meta remains visible, matching Studio's CDN contract. */
38
+ function isPublishedAt(meta, at) {
39
+ if (meta === void 0) return true;
40
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return false;
41
+ const value = meta;
42
+ if (value.status && value.status !== "published") return false;
43
+ for (const key of ["publish_at", "expire_at"]) {
44
+ if (value[key] === void 0 || value[key] === null) continue;
45
+ const time = typeof value[key] === "string" ? Date.parse(value[key]) : NaN;
46
+ if (!Number.isFinite(time)) return false;
47
+ if (key === "publish_at" ? time > at : time <= at) return false;
48
+ }
49
+ return true;
50
+ }
51
+ //#endregion
3
52
  //#region src/generator/utils.ts
4
53
  async function readJson(filePath) {
5
54
  try {
@@ -243,12 +292,30 @@ function parseValue(raw, forceString = false) {
243
292
  return raw;
244
293
  }
245
294
  //#endregion
295
+ Object.defineProperty(exports, "isPublishedAt", {
296
+ enumerable: true,
297
+ get: function() {
298
+ return isPublishedAt;
299
+ }
300
+ });
246
301
  Object.defineProperty(exports, "parseFrontmatter", {
247
302
  enumerable: true,
248
303
  get: function() {
249
304
  return parseFrontmatter;
250
305
  }
251
306
  });
307
+ Object.defineProperty(exports, "publicationContext", {
308
+ enumerable: true,
309
+ get: function() {
310
+ return publicationContext;
311
+ }
312
+ });
313
+ Object.defineProperty(exports, "publicationMeta", {
314
+ enumerable: true,
315
+ get: function() {
316
+ return publicationMeta;
317
+ }
318
+ });
252
319
  Object.defineProperty(exports, "readDir", {
253
320
  enumerable: true,
254
321
  get: function() {
@@ -1,5 +1,54 @@
1
- import { dirname, join } from "node:path";
2
1
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ //#region src/generator/publication.ts
4
+ function publicationContext(projectRoot, defaultLocale, options) {
5
+ if (!options.publishedOnly && options.at === void 0) return void 0;
6
+ if (options.at !== void 0 && !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/i.test(options.at)) throw new Error("Invalid publication --at timestamp: include an ISO date, time and timezone");
7
+ const at = options.at === void 0 ? Date.now() : Date.parse(options.at);
8
+ if (!Number.isFinite(at)) throw new Error("Invalid publication --at timestamp");
9
+ return {
10
+ projectRoot,
11
+ defaultLocale,
12
+ at
13
+ };
14
+ }
15
+ /** Canonical meta is independent of custom content paths and locale strategies. */
16
+ async function publicationMeta(ref, model, context) {
17
+ const locale = model.i18n ? ref.locale ?? context.defaultLocale : context.defaultLocale;
18
+ const parts = [
19
+ context.projectRoot,
20
+ ".contentrain",
21
+ "meta",
22
+ model.id
23
+ ];
24
+ if (model.kind === "document") parts.push(ref.slug ?? model.id);
25
+ const path = join(...parts, `${locale}.json`);
26
+ let text;
27
+ try {
28
+ text = await readFile(path, "utf8");
29
+ } catch (error) {
30
+ if (error.code === "ENOENT") return void 0;
31
+ throw error;
32
+ }
33
+ const meta = JSON.parse(text);
34
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) throw new Error(`Invalid publication metadata: ${path}`);
35
+ return meta;
36
+ }
37
+ /** Legacy content without meta remains visible, matching Studio's CDN contract. */
38
+ function isPublishedAt(meta, at) {
39
+ if (meta === void 0) return true;
40
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return false;
41
+ const value = meta;
42
+ if (value.status && value.status !== "published") return false;
43
+ for (const key of ["publish_at", "expire_at"]) {
44
+ if (value[key] === void 0 || value[key] === null) continue;
45
+ const time = typeof value[key] === "string" ? Date.parse(value[key]) : NaN;
46
+ if (!Number.isFinite(time)) return false;
47
+ if (key === "publish_at" ? time > at : time <= at) return false;
48
+ }
49
+ return true;
50
+ }
51
+ //#endregion
3
52
  //#region src/generator/utils.ts
4
53
  async function readJson(filePath) {
5
54
  try {
@@ -243,6 +292,6 @@ function parseValue(raw, forceString = false) {
243
292
  return raw;
244
293
  }
245
294
  //#endregion
246
- export { readJson as a, readDir as i, stringLikeFieldKeys as n, readText as o, readProjectManifest as r, writeText as s, parseFrontmatter as t };
295
+ 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 };
247
296
 
248
- //# sourceMappingURL=frontmatter-CVjPyhOq.mjs.map
297
+ //# sourceMappingURL=frontmatter-D-QXY_WH.mjs.map