@farm.js/content 0.1.0-beta.0 → 0.1.0-beta.2

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/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ContentAssetsInput, ContentCollection, ContentCollectionInput, ContentDataWithAssets, ContentFileSource, ContentSchema, InferContentSchema } from "./types.js";
1
+ import type { ContentAssetsInput, ContentCollection, ContentCollectionInput, ContentDataWithAssets, ContentFileSource, ContentRemoteDocument, ContentRemoteSource, ContentSchema, InferContentSchema } from "./types.js";
2
2
  export interface ContentFilesOptions {
3
3
  /** Ignore globs, evaluated relative to the Farm project root. */
4
4
  ignore?: string | readonly string[];
@@ -6,6 +6,18 @@ export interface ContentFilesOptions {
6
6
  base?: string;
7
7
  }
8
8
  export declare function files(patterns: string | readonly string[], options?: ContentFilesOptions): ContentFileSource;
9
+ export interface ContentRemoteOptions {
10
+ /** Names the source in error messages, e.g. "sanity:posts". */
11
+ name: string;
12
+ fetch(): Promise<readonly ContentRemoteDocument[]>;
13
+ /** Development re-fetch cadence in milliseconds. */
14
+ refreshInterval?: number;
15
+ create?: ContentRemoteSource["create"];
16
+ update?: ContentRemoteSource["update"];
17
+ delete?: ContentRemoteSource["delete"];
18
+ }
19
+ /** A content source that fetches documents instead of reading local files. */
20
+ export declare function remote(options: ContentRemoteOptions): ContentRemoteSource;
9
21
  export declare function collection<TSchema extends ContentSchema<any>, const TAssets extends ContentAssetsInput = undefined, TTransformed = ContentDataWithAssets<InferContentSchema<TSchema>, TAssets>>(input: ContentCollectionInput<TSchema, TTransformed, TAssets>): ContentCollection<TTransformed, InferContentSchema<TSchema>>;
10
22
  export declare function isSupportedContentFile(filePath: string): boolean;
11
23
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACpC,gGAAgG;IAChG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,KAAK,CACnB,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACpC,OAAO,GAAE,mBAAwB,GAChC,iBAAiB,CAWnB;AAED,wBAAgB,UAAU,CACxB,OAAO,SAAS,aAAa,CAAC,GAAG,CAAC,EAClC,KAAK,CAAC,OAAO,SAAS,kBAAkB,GAAG,SAAS,EACpD,YAAY,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAE1E,KAAK,EAAE,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,GAC5D,iBAAiB,CAAC,YAAY,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAgC9D;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEhE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACpC,gGAAgG;IAChG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,KAAK,CACnB,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACpC,OAAO,GAAE,mBAAwB,GAChC,iBAAiB,CAWnB;AAED,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,SAAS,qBAAqB,EAAE,CAAC,CAAC;IACnD,oDAAoD;IACpD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC;CACxC;AAED,8EAA8E;AAC9E,wBAAgB,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAmCzE;AAED,wBAAgB,UAAU,CACxB,OAAO,SAAS,aAAa,CAAC,GAAG,CAAC,EAClC,KAAK,CAAC,OAAO,SAAS,kBAAkB,GAAG,SAAS,EACpD,YAAY,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAE1E,KAAK,EAAE,sBAAsB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,GAC5D,iBAAiB,CAAC,YAAY,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAuC9D;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEhE"}
package/dist/config.js CHANGED
@@ -12,12 +12,48 @@ export function files(patterns, options = {}) {
12
12
  ...(base ? { base } : {}),
13
13
  });
14
14
  }
15
+ /** A content source that fetches documents instead of reading local files. */
16
+ export function remote(options) {
17
+ if (!options || typeof options !== "object") {
18
+ throw new TypeError("remote() requires an options object");
19
+ }
20
+ const name = typeof options.name === "string" ? options.name.trim() : "";
21
+ if (!name) {
22
+ throw new TypeError("remote() requires a non-empty name for error messages");
23
+ }
24
+ if (typeof options.fetch !== "function") {
25
+ throw new TypeError(`remote(${JSON.stringify(name)}) requires a fetch() function`);
26
+ }
27
+ if (options.refreshInterval !== undefined &&
28
+ (!Number.isFinite(options.refreshInterval) || options.refreshInterval < 100)) {
29
+ throw new TypeError(`remote(${JSON.stringify(name)}) refreshInterval must be at least 100 milliseconds`);
30
+ }
31
+ for (const verb of ["create", "update", "delete"]) {
32
+ if (options[verb] !== undefined && typeof options[verb] !== "function") {
33
+ throw new TypeError(`remote(${JSON.stringify(name)}) ${verb} must be a function`);
34
+ }
35
+ }
36
+ return Object.freeze({
37
+ kind: "remote",
38
+ name,
39
+ fetch: options.fetch,
40
+ ...(options.refreshInterval !== undefined ? { refreshInterval: options.refreshInterval } : {}),
41
+ ...(options.create ? { create: options.create } : {}),
42
+ ...(options.update ? { update: options.update } : {}),
43
+ ...(options.delete ? { delete: options.delete } : {}),
44
+ });
45
+ }
15
46
  export function collection(input) {
16
47
  if (!input || typeof input !== "object") {
17
48
  throw new TypeError("Content collection must be an object");
18
49
  }
19
- if (input.source?.kind !== "files") {
20
- throw new TypeError("Content collection source must come from files()");
50
+ if (input.source?.kind !== "files" && input.source?.kind !== "remote") {
51
+ throw new TypeError("Content collection source must come from files() or remote()");
52
+ }
53
+ if (input.source.kind === "remote" && input.assets && input.assets !== true) {
54
+ // Typed asset fields resolve files next to the entry on disk; a remote
55
+ // document has no directory for them to live in.
56
+ throw new TypeError(`Content collection with remote source ${JSON.stringify(input.source.name)} cannot declare asset fields`);
21
57
  }
22
58
  if (!isContentSchema(input.schema)) {
23
59
  throw new TypeError("Content collection schema must implement Standard Schema, parseAsync, or parse");
package/dist/index.d.ts CHANGED
@@ -2,8 +2,10 @@ import { asset } from "./assets.js";
2
2
  import { collection, files } from "./config.js";
3
3
  import type { ContentCollections, ContentOptions, ContentPlugin } from "./types.js";
4
4
  export { asset, collection, files };
5
+ export { remote } from "./config.js";
6
+ export type { ContentRemoteOptions } from "./config.js";
5
7
  export type { ContentFilesOptions } from "./config.js";
6
- export type { AppContentEntry, AppContentRegistry, ContentAssetField, ContentAssetFields, ContentAssetsInput, ContentAssetValue, ContentCollection, ContentCollectionInput, ContentCollectionName, ContentCollections, ContentEntry, ContentFileAsset, ContentFileSource, ContentImageAsset, ContentOptions, ContentPlugin, ContentSchema, ContentTransformContext, InferContentCollectionEntry, InferContentAssetFields, InferContentRegistry, InferContentSchema, } from "./types.js";
8
+ export type { AppContentEntry, AppContentRegistry, ContentAssetField, ContentAssetFields, ContentAssetsInput, ContentAssetValue, ContentCollection, ContentCollectionInput, ContentCollectionName, ContentCollections, ContentEntry, ContentFileAsset, ContentFileSource, ContentRemoteDocument, ContentRemoteSource, ContentSource, ContentImageAsset, ContentOptions, ContentPlugin, ContentSchema, ContentTransformContext, InferContentCollectionEntry, InferContentAssetFields, InferContentRegistry, InferContentSchema, } from "./types.js";
7
9
  /** Load schema-validated local content as a typed server-only collection registry. */
8
10
  export declare function content<const TCollections extends ContentCollections>(options: ContentOptions<TCollections>): ContentPlugin<TCollections>;
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,KAAK,EAA0B,MAAM,aAAa,CAAC;AAExE,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIpF,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AACpC,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,2BAA2B,EAC3B,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,KAAK,CAAC,YAAY,SAAS,kBAAkB,EACnE,OAAO,EAAE,cAAc,CAAC,YAAY,CAAC,GACpC,aAAa,CAAC,YAAY,CAAC,CA4G7B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,KAAK,EAA0B,MAAM,aAAa,CAAC;AAGxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIpF,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,2BAA2B,EAC3B,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,KAAK,CAAC,YAAY,SAAS,kBAAkB,EACnE,OAAO,EAAE,cAAc,CAAC,YAAY,CAAC,GACpC,aAAa,CAAC,YAAY,CAAC,CAoM7B"}
package/dist/index.js CHANGED
@@ -1,14 +1,27 @@
1
1
  import path from "node:path";
2
+ import { lstat, realpath } from "node:fs/promises";
2
3
  import { definePlugin } from "@farm.js/core/plugin";
3
4
  import fg from "fast-glob";
4
5
  import { asset } from "./assets.js";
5
6
  import { collection, files, isSupportedContentFile } from "./config.js";
6
7
  import { loadContentCollections, writeContentServerModule } from "./loader.js";
8
+ import { registerContentWriteRuntime, setContentAfterWrite } from "./runtime.js";
7
9
  const CONTENT_SERVER_ID = "@farm.js/content/server";
8
10
  export { asset, collection, files };
11
+ export { remote } from "./config.js";
9
12
  /** Load schema-validated local content as a typed server-only collection registry. */
10
13
  export function content(options) {
11
14
  validateOptions(options);
15
+ // Writes need the live sources and schemas; the snapshot module carries
16
+ // only data. Registered here so both dev and the production runtime (where
17
+ // the config module is evaluated too) can reach them.
18
+ const unregisterWriteRuntime = registerContentWriteRuntime(Object.fromEntries(Object.entries(options.collections).map(([name, definition]) => [
19
+ name,
20
+ {
21
+ ...(definition.source.kind === "remote" ? { source: definition.source } : {}),
22
+ schema: definition.schema,
23
+ },
24
+ ])));
12
25
  let root = process.cwd();
13
26
  let generatedFile = "";
14
27
  let sourceFiles = new Set();
@@ -39,9 +52,11 @@ export function content(options) {
39
52
  async function rebuild() {
40
53
  const attemptedSourceFiles = new Set();
41
54
  try {
55
+ await assertGeneratedOutputInsideRoot(root, generatedFile);
42
56
  const loaded = await loadContentCollections(root, options.collections, attemptedSourceFiles);
43
- await writeContentServerModule(generatedFile, loaded.collections, loaded.assetImports);
57
+ const changed = await writeContentServerModule(generatedFile, loaded.collections, loaded.assetImports);
44
58
  sourceFiles = new Set(loaded.sourceFiles);
59
+ return changed;
45
60
  }
46
61
  catch (error) {
47
62
  sourceFiles = new Set([...sourceFiles, ...attemptedSourceFiles]);
@@ -89,11 +104,116 @@ export function content(options) {
89
104
  };
90
105
  server.watcher.on("all", onFile);
91
106
  server.httpServer?.once("close", () => server.watcher.off?.("all", onFile));
107
+ // A successful write refreshes the snapshot immediately in development.
108
+ // The stored chain must always settle: a rejected tail would swallow
109
+ // the next watcher rebuild entirely, so the error report lives inside
110
+ // the chain and the queue continues past it.
111
+ const disposeAfterWrite = setContentAfterWrite(async () => {
112
+ const settled = (rebuildQueue = rebuildQueue
113
+ .then(async () => {
114
+ if (!(await rebuild()))
115
+ return;
116
+ const modules = server.moduleGraph.getModulesByFile?.(generatedFile);
117
+ for (const module of modules ?? [])
118
+ server.moduleGraph.invalidateModule(module);
119
+ server.ws.send({ type: "full-reload" });
120
+ })
121
+ .catch((error) => {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ server.ws.send({
124
+ type: "error",
125
+ err: { message, stack: error instanceof Error ? error.stack : undefined },
126
+ });
127
+ }));
128
+ await settled;
129
+ });
130
+ // Vite restart creates the replacement server before closing this one;
131
+ // the scoped disposers only clear registrations this server owns.
132
+ server.httpServer?.once("close", () => {
133
+ disposeAfterWrite();
134
+ unregisterWriteRuntime();
135
+ });
136
+ // Remote sources have no file events; poll the ones that asked for it.
137
+ // One timer at the smallest requested cadence keeps ordering simple,
138
+ // and a reload only goes out when the generated module actually changed.
139
+ const intervals = Object.values(options.collections)
140
+ .map((definition) => definition.source)
141
+ .filter((candidate) => candidate.kind === "remote")
142
+ .map((candidate) => candidate.refreshInterval)
143
+ .filter((value) => typeof value === "number");
144
+ if (intervals.length > 0) {
145
+ // A persistently failing remote fetch reports once per distinct
146
+ // message, not once per tick - the overlay is a signal, not a log.
147
+ let lastPollError;
148
+ const timer = setInterval(() => {
149
+ rebuildQueue = rebuildQueue
150
+ .then(async () => {
151
+ if (!(await rebuild()))
152
+ return;
153
+ lastPollError = undefined;
154
+ const modules = server.moduleGraph.getModulesByFile?.(generatedFile);
155
+ for (const module of modules ?? [])
156
+ server.moduleGraph.invalidateModule(module);
157
+ server.ws.send({ type: "full-reload" });
158
+ })
159
+ .catch((error) => {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ if (message === lastPollError)
162
+ return;
163
+ lastPollError = message;
164
+ server.ws.send({
165
+ type: "error",
166
+ err: { message, stack: error instanceof Error ? error.stack : undefined },
167
+ });
168
+ });
169
+ }, Math.min(...intervals));
170
+ timer.unref?.();
171
+ server.httpServer?.once("close", () => clearInterval(timer));
172
+ }
92
173
  },
93
174
  };
94
175
  }
95
176
  return plugin;
96
177
  }
178
+ async function assertGeneratedOutputInsideRoot(root, outputFile) {
179
+ const actualRoot = await realpath(root);
180
+ const missingSegments = [];
181
+ let existingAncestor = outputFile;
182
+ const unsafeOutput = () => new Error("[farm:content] Generated content output must stay inside the Farm project root, including through symlinks");
183
+ // Check the prospective destination before mkdir or writeFile can follow a link.
184
+ while (true) {
185
+ let actualAncestor;
186
+ try {
187
+ actualAncestor = await realpath(existingAncestor);
188
+ }
189
+ catch (error) {
190
+ if (error.code !== "ENOENT")
191
+ throw error;
192
+ const entry = await lstat(existingAncestor).catch((statError) => {
193
+ if (statError.code !== "ENOENT")
194
+ throw statError;
195
+ return undefined;
196
+ });
197
+ if (entry)
198
+ throw unsafeOutput(); // A dangling symlink is not a missing directory.
199
+ const parent = path.dirname(existingAncestor);
200
+ if (parent === existingAncestor)
201
+ throw error;
202
+ missingSegments.unshift(path.basename(existingAncestor));
203
+ existingAncestor = parent;
204
+ continue;
205
+ }
206
+ const destination = path.join(actualAncestor, ...missingSegments);
207
+ const relative = path.relative(actualRoot, destination);
208
+ if (!relative ||
209
+ relative === ".." ||
210
+ relative.startsWith(`..${path.sep}`) ||
211
+ path.isAbsolute(relative)) {
212
+ throw unsafeOutput();
213
+ }
214
+ return;
215
+ }
216
+ }
97
217
  async function shouldRebuildForFile(root, filePath, event, sourceFiles, collections) {
98
218
  if (sourceFiles.has(filePath))
99
219
  return true;
@@ -106,6 +226,8 @@ async function shouldRebuildForFile(root, filePath, event, sourceFiles, collecti
106
226
  return false;
107
227
  }
108
228
  for (const definition of Object.values(collections)) {
229
+ if (definition.source.kind !== "files")
230
+ continue;
109
231
  const matches = await fg([...definition.source.patterns], {
110
232
  cwd: root,
111
233
  absolute: true,
package/dist/loader.d.ts CHANGED
@@ -6,7 +6,7 @@ export interface LoadedContent {
6
6
  readonly assetImports: ReadonlyMap<string, ContentAssetImport>;
7
7
  }
8
8
  export declare function loadContentCollections(root: string, collections: ContentCollections, sourceFiles?: Set<string>): Promise<LoadedContent>;
9
- export declare function writeContentServerModule(outputFile: string, collections: Readonly<Record<string, readonly ContentEntry<any>[]>>, assetImports?: ReadonlyMap<string, ContentAssetImport>): Promise<void>;
9
+ export declare function writeContentServerModule(outputFile: string, collections: Readonly<Record<string, readonly ContentEntry<any>[]>>, assetImports?: ReadonlyMap<string, ContentAssetImport>): Promise<boolean>;
10
10
  type EncodedContentValue = null | boolean | number | string | ["undefined"] | ["date", string] | ["bigint", string] | ["array", EncodedContentValue[]] | ["object", Array<[string, EncodedContentValue]>];
11
11
  export declare function encodeContentValue(value: unknown, seen?: Set<object>): EncodedContentValue;
12
12
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAEV,kBAAkB,EAClB,YAAY,EAIb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAChE;AAED,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,kBAAkB,EAC/B,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,OAAO,CAAC,aAAa,CAAC,CAexB;AAED,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EACnE,YAAY,GAAE,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAa,GAChE,OAAO,CAAC,IAAI,CAAC,CAgCf;AAoPD,KAAK,mBAAmB,GACpB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,CAAC,WAAW,CAAC,GACb,CAAC,MAAM,EAAE,MAAM,CAAC,GAChB,CAAC,QAAQ,EAAE,MAAM,CAAC,GAClB,CAAC,OAAO,EAAE,mBAAmB,EAAE,CAAC,GAChC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAErD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,cAAoB,GAAG,mBAAmB,CAmChG"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAKV,kBAAkB,EAClB,YAAY,EAIb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAChE;AAED,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,kBAAkB,EAC/B,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,OAAO,CAAC,aAAa,CAAC,CAuBxB;AAED,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EACnE,YAAY,GAAE,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAa,GAChE,OAAO,CAAC,OAAO,CAAC,CAsClB;AA+XD,KAAK,mBAAmB,GACpB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,CAAC,WAAW,CAAC,GACb,CAAC,MAAM,EAAE,MAAM,CAAC,GAChB,CAAC,QAAQ,EAAE,MAAM,CAAC,GAClB,CAAC,OAAO,EAAE,mBAAmB,EAAE,CAAC,GAChC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAErD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,cAAoB,GAAG,mBAAmB,CAmChG"}
package/dist/loader.js CHANGED
@@ -1,4 +1,5 @@
1
- import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, readFile, realpath, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import fg from "fast-glob";
4
5
  import matter from "gray-matter";
@@ -9,9 +10,10 @@ import { resolveMarkdownAssets } from "./markdown-assets.js";
9
10
  export async function loadContentCollections(root, collections, sourceFiles = new Set()) {
10
11
  const loaded = Object.create(null);
11
12
  const assetImports = new Map();
13
+ const actualRoot = await realpath(root);
12
14
  for (const [name, definition] of Object.entries(collections)) {
13
15
  validateCollectionName(name);
14
- const entries = await loadCollection(root, name, definition, sourceFiles, assetImports);
16
+ const entries = await loadCollection(root, actualRoot, name, definition, sourceFiles, assetImports);
15
17
  loaded[name] = Object.freeze(entries);
16
18
  }
17
19
  return {
@@ -26,9 +28,17 @@ export async function writeContentServerModule(outputFile, collections, assetImp
26
28
  const actualOutputDirectory = await realpath(outputDirectory);
27
29
  const encoded = encodeContentValue(collections);
28
30
  const imports = [...assetImports.values()].sort((left, right) => left.token.localeCompare(right.token));
29
- const importSource = imports
30
- .map((asset, index) => `import farmContentAsset${index} from ${JSON.stringify(`${toViteImportSpecifier(actualOutputDirectory, asset.filePath)}?url`)};`)
31
- .join("\n");
31
+ const importLines = [];
32
+ for (const [index, asset] of imports.entries()) {
33
+ const relativePath = toViteImportSpecifier(actualOutputDirectory, asset.filePath);
34
+ // Vite treats these characters as URL syntax, even in resolved filesystem IDs.
35
+ // Only stage affected assets; ordinary files keep their direct ?url imports.
36
+ const importPath = /[?#%]/.test(relativePath)
37
+ ? await stageContentAsset(actualOutputDirectory, asset.filePath)
38
+ : relativePath;
39
+ importLines.push(`import farmContentAsset${index} from ${JSON.stringify(`${importPath}?url`)};`);
40
+ }
41
+ const importSource = importLines.join("\n");
32
42
  const assetUrls = imports.length
33
43
  ? `, { ${imports
34
44
  .map((asset, index) => `${JSON.stringify(asset.token)}: farmContentAsset${index}`)
@@ -43,11 +53,40 @@ const runtime = createContentRuntime(decodeContentValue(${JSON.stringify(encoded
43
53
  export const getCollection = runtime.getCollection;
44
54
  export const getEntry = runtime.getEntry;
45
55
  export const getEntryOrThrow = runtime.getEntryOrThrow;
56
+ export const collections = runtime.collections;
46
57
  `;
47
- await writeFileIfChanged(outputFile, source);
58
+ return writeFileIfChanged(outputFile, source);
59
+ }
60
+ async function stageContentAsset(outputDirectory, filePath) {
61
+ const bytes = await readFile(filePath);
62
+ const digest = createHash("sha256").update(bytes).digest("hex");
63
+ const extension = path.extname(filePath);
64
+ const filename = `${digest}${/^\.[a-z\d]+$/i.test(extension) ? extension : ".bin"}`;
65
+ const directory = path.join(outputDirectory, "assets");
66
+ await mkdir(directory, { recursive: true });
67
+ if ((await realpath(directory)) !== directory) {
68
+ throw new Error("[farm:content] Generated asset directory cannot be a symlink");
69
+ }
70
+ const target = path.join(directory, filename);
71
+ try {
72
+ // Never overwrite an existing file or follow a pre-existing destination symlink.
73
+ await writeFile(target, bytes, { flag: "wx" });
74
+ }
75
+ catch (error) {
76
+ if (error.code !== "EEXIST")
77
+ throw error;
78
+ if (!(await lstat(target)).isFile() || !(await readFile(target)).equals(bytes)) {
79
+ throw new Error(`[farm:content] Generated asset ${filename} is not a matching regular file`);
80
+ }
81
+ }
82
+ // Content-addressing changes the import on edits, avoiding stale Vite module URLs.
83
+ return `./assets/${filename}`;
48
84
  }
49
- async function loadCollection(root, name, definition, sourceFiles, assetImports) {
85
+ async function loadCollection(root, actualRoot, name, definition, sourceFiles, assetImports) {
50
86
  const source = definition.source;
87
+ if (source.kind === "remote") {
88
+ return loadRemoteCollection(name, definition, source);
89
+ }
51
90
  const relativeFiles = await fg([...source.patterns], {
52
91
  cwd: root,
53
92
  absolute: false,
@@ -72,7 +111,15 @@ async function loadCollection(root, name, definition, sourceFiles, assetImports)
72
111
  }
73
112
  ids.set(id, relativeFile);
74
113
  sourceFiles.add(absoluteFile);
75
- const parsed = await parseContentFile(absoluteFile, relativeFile);
114
+ const actualFile = await realpath(absoluteFile);
115
+ const relativeToRoot = path.relative(actualRoot, actualFile);
116
+ if (relativeToRoot === ".." ||
117
+ relativeToRoot.startsWith(`..${path.sep}`) ||
118
+ path.isAbsolute(relativeToRoot)) {
119
+ throw new Error(`[farm:content] Collection "${name}" source ${relativeFile} cannot leave the Farm project root`);
120
+ }
121
+ sourceFiles.add(actualFile);
122
+ const parsed = await parseContentFile(actualFile, relativeFile);
76
123
  const assetContext = {
77
124
  root,
78
125
  collectionName: name,
@@ -121,12 +168,79 @@ async function loadCollection(root, name, definition, sourceFiles, assetImports)
121
168
  }
122
169
  return entries;
123
170
  }
171
+ /**
172
+ * A remote collection goes through the same validation and transform pipeline
173
+ * as files; only acquisition differs. Documents are sorted by ID so the
174
+ * generated module is stable across fetches that return in a different order.
175
+ */
176
+ async function loadRemoteCollection(name, definition, source) {
177
+ let documents;
178
+ try {
179
+ documents = await source.fetch();
180
+ }
181
+ catch (error) {
182
+ throw new Error(`[farm:content] Collection ${JSON.stringify(name)} failed to fetch from ${JSON.stringify(source.name)}: ${errorMessage(error)}`, { cause: error });
183
+ }
184
+ if (!Array.isArray(documents)) {
185
+ throw new Error(`[farm:content] Collection ${JSON.stringify(name)}: source ${JSON.stringify(source.name)} must resolve to an array of documents`);
186
+ }
187
+ const ids = new Set();
188
+ const sorted = [...documents].sort((left, right) => String(left?.id ?? "").localeCompare(String(right?.id ?? "")));
189
+ const entries = [];
190
+ for (const document of sorted) {
191
+ if (!document ||
192
+ typeof document !== "object" ||
193
+ typeof document.id !== "string" ||
194
+ !document.id) {
195
+ throw new Error(`[farm:content] Collection ${JSON.stringify(name)}: every document from ${JSON.stringify(source.name)} needs a non-empty string id`);
196
+ }
197
+ if (ids.has(document.id)) {
198
+ throw new Error(`[farm:content] Collection ${JSON.stringify(name)} has duplicate entry ID ${JSON.stringify(document.id)} from ${JSON.stringify(source.name)}`);
199
+ }
200
+ ids.add(document.id);
201
+ if (!document.data || typeof document.data !== "object" || Array.isArray(document.data)) {
202
+ throw new Error(`[farm:content] Collection ${JSON.stringify(name)}: document ${JSON.stringify(document.id)} from ${JSON.stringify(source.name)} needs an object \`data\``);
203
+ }
204
+ const reference = `${source.name}/${document.id}`;
205
+ const validatedData = await validateContentData(definition.schema, document.data, name, reference);
206
+ const body = typeof document.body === "string" ? document.body : "";
207
+ const baseEntry = {
208
+ id: document.id,
209
+ data: validatedData,
210
+ body,
211
+ bodyAssets: Object.freeze([]),
212
+ filePath: reference,
213
+ words: countWords(body),
214
+ };
215
+ const transformed = definition.transform
216
+ ? await definition.transform(baseEntry)
217
+ : baseEntry.data;
218
+ entries.push(Object.freeze({
219
+ id: document.id,
220
+ data: freezeContentValue(transformed),
221
+ body,
222
+ bodyAssets: freezeContentValue([]),
223
+ filePath: reference,
224
+ }));
225
+ }
226
+ return entries;
227
+ }
124
228
  async function parseContentFile(absoluteFile, relativeFile) {
125
229
  const source = await readFile(absoluteFile, "utf8");
126
230
  const extension = path.extname(relativeFile).toLowerCase();
127
231
  try {
128
232
  if (extension === ".md" || extension === ".mdx") {
129
- const parsed = matter(source);
233
+ // Check the same delimiter/language that gray-matter will parse, before
234
+ // its default engines can evaluate executable frontmatter.
235
+ const normalizedSource = source.replace(/^\uFEFF/, "");
236
+ if (normalizedSource.startsWith("---") && normalizedSource[3] !== "-") {
237
+ const language = matter.language(normalizedSource).name.toLowerCase();
238
+ if (language && language !== "yaml" && language !== "yml" && language !== "json") {
239
+ throw new Error("Only YAML and JSON frontmatter are supported");
240
+ }
241
+ }
242
+ // Explicit options also avoid gray-matter's process-global parsed cache.
243
+ const parsed = matter(source, {});
130
244
  return { data: parsed.data, body: parsed.content };
131
245
  }
132
246
  if (extension === ".json") {
@@ -347,13 +461,14 @@ function freezeContentValue(value, ancestors = new Set()) {
347
461
  async function writeFileIfChanged(filePath, source) {
348
462
  try {
349
463
  if ((await readFile(filePath, "utf8")) === source)
350
- return;
464
+ return false;
351
465
  }
352
466
  catch (error) {
353
467
  if (error.code !== "ENOENT")
354
468
  throw error;
355
469
  }
356
470
  await writeFile(filePath, source, "utf8");
471
+ return true;
357
472
  }
358
473
  function errorMessage(error) {
359
474
  return error instanceof Error ? error.message : String(error);
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-assets.d.ts","sourceRoot":"","sources":["../src/markdown-assets.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA8BpD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAAE,CAAC,CAsCjE"}
1
+ {"version":3,"file":"markdown-assets.d.ts","sourceRoot":"","sources":["../src/markdown-assets.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA8BpD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAAE,CAAC,CAkDjE"}
@@ -6,7 +6,20 @@ export async function resolveMarkdownAssets(body, context) {
6
6
  return { body, assets: Object.freeze([]) };
7
7
  let tree;
8
8
  try {
9
- tree = fromMarkdown(body);
9
+ if (context.sourceFile.toLowerCase().endsWith(".mdx")) {
10
+ // Parse MDX without evaluating it. Only Markdown children own assets.
11
+ const [{ mdxFromMarkdown }, { mdxjs }] = await Promise.all([
12
+ import("mdast-util-mdx"),
13
+ import("micromark-extension-mdxjs"),
14
+ ]);
15
+ tree = fromMarkdown(body, {
16
+ extensions: [mdxjs()],
17
+ mdastExtensions: [mdxFromMarkdown()],
18
+ });
19
+ }
20
+ else {
21
+ tree = fromMarkdown(body);
22
+ }
10
23
  }
11
24
  catch (error) {
12
25
  throw new Error(`[farm:content] Could not inspect Markdown assets in ${context.sourceFile}: ${errorMessage(error)}`, { cause: error });
@@ -39,7 +52,9 @@ function collectCandidates(tree) {
39
52
  const references = new Map();
40
53
  visit(tree, (node) => {
41
54
  if (node.type === "definition" && node.identifier) {
42
- definitions.set(node.identifier, node);
55
+ // CommonMark resolves duplicate reference labels to their first definition.
56
+ if (!definitions.has(node.identifier))
57
+ definitions.set(node.identifier, node);
43
58
  return;
44
59
  }
45
60
  if (node.type === "imageReference" && node.identifier) {
package/dist/runtime.d.ts CHANGED
@@ -1,11 +1,24 @@
1
- import type { ContentEntry } from "./types.js";
1
+ import type { ContentEntry, ContentRemoteSource, ContentSchema } from "./types.js";
2
2
  type RuntimeCollections = Readonly<Record<string, readonly ContentEntry<any>[]>>;
3
3
  type EncodedContentValue = null | boolean | number | string | ["undefined"] | ["date", string] | ["bigint", string] | ["array", EncodedContentValue[]] | ["object", Array<[string, EncodedContentValue]>];
4
4
  export declare function decodeContentValue(value: EncodedContentValue, assetUrls?: Readonly<Record<string, string>>): any;
5
+ /**
6
+ * Live write runtime, shared between the config-time plugin and the generated
7
+ * server module through globalThis so bundle-copy duplication cannot split it.
8
+ * The plugin registers each collection's source and schema when the factory
9
+ * runs; the generated module's `collections` handle reads them for writes.
10
+ */
11
+ interface ContentWriteRegistration {
12
+ source?: ContentRemoteSource;
13
+ schema?: ContentSchema<any>;
14
+ }
15
+ export declare function registerContentWriteRuntime(registrations: Record<string, ContentWriteRegistration>): () => void;
16
+ export declare function setContentAfterWrite(afterWrite: (() => Promise<void>) | undefined): () => void;
5
17
  export declare function createContentRuntime(collections: RuntimeCollections): Readonly<{
6
18
  getCollection: (name: string) => Promise<readonly ContentEntry<any>[]>;
7
19
  getEntry: (name: string, id: string) => Promise<ContentEntry<any> | undefined>;
8
20
  getEntryOrThrow: (name: string, id: string) => Promise<ContentEntry<any>>;
21
+ collections: Record<string, unknown>;
9
22
  }>;
10
23
  export {};
11
24
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,KAAK,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAEjF,KAAK,mBAAmB,GACpB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,CAAC,WAAW,CAAC,GACb,CAAC,MAAM,EAAE,MAAM,CAAC,GAChB,CAAC,QAAQ,EAAE,MAAM,CAAC,GAClB,CAAC,OAAO,EAAE,mBAAmB,EAAE,CAAC,GAChC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAErD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,mBAAmB,EAC1B,SAAS,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAC/C,GAAG,CAwBL;AAUD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,kBAAkB;0BAkB/B,MAAM;qBACX,MAAM,MAAM,MAAM;4BAQX,MAAM,MAAM,MAAM;GASxD"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEnF,KAAK,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAEjF,KAAK,mBAAmB,GACpB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,CAAC,WAAW,CAAC,GACb,CAAC,MAAM,EAAE,MAAM,CAAC,GAChB,CAAC,QAAQ,EAAE,MAAM,CAAC,GAClB,CAAC,OAAO,EAAE,mBAAmB,EAAE,CAAC,GAChC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAErD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,mBAAmB,EAC1B,SAAS,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAC/C,GAAG,CAwBL;AAUD;;;;;GAKG;AACH,UAAU,wBAAwB;IAChC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,MAAM,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;CAC7B;AA2BD,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,GACtD,MAAM,IAAI,CAiBZ;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,GAAG,MAAM,IAAI,CAS9F;AA6DD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,kBAAkB;0BAkB/B,MAAM;qBACX,MAAM,MAAM,MAAM;4BAQX,MAAM,MAAM,MAAM;;GAwExD"}
package/dist/runtime.js CHANGED
@@ -34,6 +34,100 @@ function replaceAssetUrls(value, assetUrls) {
34
34
  }
35
35
  return result;
36
36
  }
37
+ const WRITE_RUNTIME_KEY = "__FARM_CONTENT_WRITE_RUNTIME__";
38
+ function writeRuntime() {
39
+ const scope = globalThis;
40
+ return (scope[WRITE_RUNTIME_KEY] ??= { registrations: Object.create(null) });
41
+ }
42
+ const RESERVED_REGISTRATION_NAMES = new Set(["__proto__", "constructor", "prototype"]);
43
+ const PROTOCOL_PROBE_KEYS = new Set([
44
+ "then",
45
+ "toJSON",
46
+ "constructor",
47
+ "$$typeof",
48
+ "nodeType",
49
+ "asymmetricMatch",
50
+ "@@__IMMUTABLE_ITERABLE__@@",
51
+ ]);
52
+ export function registerContentWriteRuntime(registrations) {
53
+ const target = writeRuntime().registrations;
54
+ const names = [];
55
+ // Own keys assigned one by one onto a null-prototype record: a collection
56
+ // literally named __proto__ must become data, never the record's prototype.
57
+ for (const name of Object.keys(registrations)) {
58
+ if (RESERVED_REGISTRATION_NAMES.has(name)) {
59
+ throw new Error(`[farm:content] Collection name ${JSON.stringify(name)} is reserved`);
60
+ }
61
+ target[name] = registrations[name];
62
+ names.push(name);
63
+ }
64
+ return () => {
65
+ for (const name of names) {
66
+ if (target[name] === registrations[name])
67
+ delete target[name];
68
+ }
69
+ };
70
+ }
71
+ export function setContentAfterWrite(afterWrite) {
72
+ const state = writeRuntime();
73
+ state.afterWrite = afterWrite;
74
+ // Vite's restart creates the new server (registering its hook) BEFORE it
75
+ // closes the old one, so an unconditional clear in the old server's close
76
+ // handler would disarm the replacement. Only the current owner may clear.
77
+ return () => {
78
+ if (state.afterWrite === afterWrite)
79
+ state.afterWrite = undefined;
80
+ };
81
+ }
82
+ function requireWritableSource(name, verb) {
83
+ const registration = writeRuntime().registrations[name];
84
+ if (!registration?.source) {
85
+ throw new Error(`[farm:content] Collection "${name}" is read-only: only a remote() source can write. ` +
86
+ "Local file collections are edited on disk.");
87
+ }
88
+ const callback = registration.source[verb];
89
+ if (!callback) {
90
+ throw new Error(`[farm:content] Collection "${name}": source ${JSON.stringify(registration.source.name)} ` +
91
+ `does not implement ${verb}(). Add the callback to the source, or perform the write ` +
92
+ "with the provider's own SDK in a server function.");
93
+ }
94
+ return { callback: callback.bind(registration.source), registration };
95
+ }
96
+ async function validateAgainstSchema(schema, data) {
97
+ if (!schema)
98
+ return;
99
+ if (typeof schema.parseAsync === "function") {
100
+ await schema.parseAsync(data);
101
+ return;
102
+ }
103
+ if (typeof schema.parse === "function") {
104
+ schema.parse(data);
105
+ return;
106
+ }
107
+ const standard = schema["~standard"];
108
+ if (standard) {
109
+ const result = await standard.validate(data);
110
+ if (result && "issues" in result && result.issues?.length) {
111
+ throw new Error(`[farm:content] The written document failed validation: ${result.issues
112
+ .map((issue) => issue.message)
113
+ .join("; ")}`);
114
+ }
115
+ }
116
+ }
117
+ async function settleWrite(result) {
118
+ // The CMS accepted the write; refresh the local snapshot where a dev server
119
+ // is present. In production the snapshot is bundled, so the change becomes
120
+ // visible on the next build - pair writes with a deploy hook. A failing
121
+ // refresh must not report the persisted write as failed: the dev pipeline
122
+ // surfaces its own errors.
123
+ try {
124
+ await writeRuntime().afterWrite?.();
125
+ }
126
+ catch {
127
+ // reported by the dev pipeline's own channel
128
+ }
129
+ return result;
130
+ }
37
131
  export function createContentRuntime(collections) {
38
132
  const indexes = new Map();
39
133
  function collection(name) {
@@ -62,5 +156,67 @@ export function createContentRuntime(collections) {
62
156
  }
63
157
  return entry;
64
158
  };
65
- return Object.freeze({ getCollection, getEntry, getEntryOrThrow });
159
+ const handles = new Map();
160
+ const collectionHandle = (name) => {
161
+ let handle = handles.get(name);
162
+ if (!handle) {
163
+ collection(name); // unknown names fail here with the available list
164
+ handle = Object.freeze({
165
+ all: () => getCollection(name),
166
+ get: (id) => getEntry(name, id),
167
+ getOrThrow: (id) => getEntryOrThrow(name, id),
168
+ create: async (input) => {
169
+ const { callback, registration } = requireWritableSource(name, "create");
170
+ const document = await callback(input);
171
+ // The source of truth answered; make sure what it stored still
172
+ // satisfies the collection's schema before anyone trusts it.
173
+ // The CMS changed either way, so the snapshot refreshes even when
174
+ // validation rejects - readers should see the CMS's real state.
175
+ try {
176
+ await validateAgainstSchema(registration.schema, document.data);
177
+ }
178
+ catch (error) {
179
+ await settleWrite(undefined);
180
+ throw error;
181
+ }
182
+ return settleWrite(document);
183
+ },
184
+ update: async (id, patch) => {
185
+ const { callback, registration } = requireWritableSource(name, "update");
186
+ const document = await callback(id, patch);
187
+ try {
188
+ await validateAgainstSchema(registration.schema, document.data);
189
+ }
190
+ catch (error) {
191
+ await settleWrite(undefined);
192
+ throw error;
193
+ }
194
+ return settleWrite(document);
195
+ },
196
+ delete: async (id) => {
197
+ const { callback } = requireWritableSource(name, "delete");
198
+ await callback(id);
199
+ await settleWrite(undefined);
200
+ },
201
+ });
202
+ handles.set(name, handle);
203
+ }
204
+ return handle;
205
+ };
206
+ // Runtimes probe objects with protocol keys: await checks `then`,
207
+ // JSON.stringify checks `toJSON`, test matchers poke a few more. Those
208
+ // probes must see undefined; only a real unknown collection name throws.
209
+ const collectionsProxy = new Proxy(Object.create(null), {
210
+ get(_target, property) {
211
+ if (typeof property !== "string")
212
+ return undefined;
213
+ if (!(property in collections) && PROTOCOL_PROBE_KEYS.has(property))
214
+ return undefined;
215
+ return collectionHandle(property);
216
+ },
217
+ has: (_target, property) => typeof property === "string" && property in collections,
218
+ ownKeys: () => Object.keys(collections),
219
+ getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true }),
220
+ });
221
+ return Object.freeze({ getCollection, getEntry, getEntryOrThrow, collections: collectionsProxy });
66
222
  }
package/dist/server.d.ts CHANGED
@@ -1,9 +1,18 @@
1
- import type { AppContentEntry, ContentCollectionName } from "./types.js";
1
+ import type { AppContentEntry, ContentCollectionHandle, ContentCollectionName } from "./types.js";
2
2
  /** Return every entry in a configured collection, ordered by entry ID. */
3
3
  export declare function getCollection<TName extends ContentCollectionName>(_name: TName): Promise<readonly AppContentEntry<TName>[]>;
4
4
  /** Find one entry by its path-derived ID. */
5
5
  export declare function getEntry<TName extends ContentCollectionName>(_name: TName, _id: string): Promise<AppContentEntry<TName> | undefined>;
6
6
  /** Find one entry by ID or throw an actionable error. */
7
7
  export declare function getEntryOrThrow<TName extends ContentCollectionName>(_name: TName, _id: string): Promise<AppContentEntry<TName>>;
8
- export type { AppContentEntry, AppContentRegistry, ContentCollectionName, ContentEntry, } from "./types.js";
8
+ /**
9
+ * The per-collection handle: reads from the validated snapshot, writes
10
+ * through the source's create/update/delete callbacks. Development rebuilds
11
+ * the snapshot after a write; production serves the bundled snapshot until
12
+ * the next build, so pair writes with a deploy hook.
13
+ */
14
+ export declare const collections: {
15
+ readonly [TName in ContentCollectionName]: ContentCollectionHandle<AppContentEntry<TName>["data"]>;
16
+ };
17
+ export type { AppContentEntry, AppContentRegistry, ContentCollectionHandle, ContentCollectionName, ContentEntry, } from "./types.js";
9
18
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAQzE,0EAA0E;AAC1E,wBAAsB,aAAa,CAAC,KAAK,SAAS,qBAAqB,EACrE,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAE5C;AAED,6CAA6C;AAC7C,wBAAsB,QAAQ,CAAC,KAAK,SAAS,qBAAqB,EAChE,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,CAE7C;AAED,yDAAyD;AACzD,wBAAsB,eAAe,CAAC,KAAK,SAAS,qBAAqB,EACvE,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAEjC;AAED,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,GACb,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAQlG,0EAA0E;AAC1E,wBAAsB,aAAa,CAAC,KAAK,SAAS,qBAAqB,EACrE,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAE5C;AAED,6CAA6C;AAC7C,wBAAsB,QAAQ,CAAC,KAAK,SAAS,qBAAqB,EAChE,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,CAE7C;AAED,yDAAyD;AACzD,wBAAsB,eAAe,CAAC,KAAK,SAAS,qBAAqB,EACvE,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAEjC;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE;IACxB,QAAQ,EAAE,KAAK,IAAI,qBAAqB,GAAG,uBAAuB,CAChE,eAAe,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAC/B;CAC+D,CAAC;AAEnE,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,GACb,MAAM,YAAY,CAAC"}
package/dist/server.js CHANGED
@@ -13,3 +13,10 @@ export async function getEntry(_name, _id) {
13
13
  export async function getEntryOrThrow(_name, _id) {
14
14
  return missingPlugin();
15
15
  }
16
+ /**
17
+ * The per-collection handle: reads from the validated snapshot, writes
18
+ * through the source's create/update/delete callbacks. Development rebuilds
19
+ * the snapshot after a write; production serves the bundled snapshot until
20
+ * the next build, so pair writes with a deploy hook.
21
+ */
22
+ export const collections = new Proxy(Object.create(null), { get: () => missingPlugin() });
package/dist/types.d.ts CHANGED
@@ -6,6 +6,48 @@ export interface ContentFileSource {
6
6
  readonly ignore: readonly string[];
7
7
  readonly base?: string;
8
8
  }
9
+ /** One document delivered by a remote source, before validation. */
10
+ export interface ContentRemoteDocument {
11
+ /** Stable route-friendly identifier, unique within the collection. */
12
+ readonly id: string;
13
+ /** Structured data; goes through the collection schema like frontmatter. */
14
+ readonly data: Record<string, unknown>;
15
+ /** Optional Markdown body. Defaults to an empty string. */
16
+ readonly body?: string;
17
+ }
18
+ /**
19
+ * A source that fetches documents instead of reading local files - a CMS, an
20
+ * API, a database. Fetched while the configuration loads, validated through
21
+ * the same schema pipeline as files, and bundled into production output as
22
+ * the same build-time snapshot. The shape is structural on purpose: an
23
+ * integration can return it without importing this package at runtime.
24
+ */
25
+ /** The document fields a write sends to a remote source. */
26
+ export interface ContentRemoteWrite {
27
+ readonly data: Record<string, unknown>;
28
+ readonly body?: string;
29
+ }
30
+ export interface ContentRemoteSource {
31
+ readonly kind: "remote";
32
+ /** Names the source in error messages, e.g. "sanity:posts". */
33
+ readonly name: string;
34
+ fetch(): Promise<readonly ContentRemoteDocument[]>;
35
+ /**
36
+ * Development only: re-fetch this often (milliseconds) and reload when the
37
+ * documents changed. Leave unset for fetch-on-rebuild only.
38
+ */
39
+ readonly refreshInterval?: number;
40
+ /**
41
+ * Optional write callbacks, mirroring fetch: the callback is the entire
42
+ * provider-specific surface. A source that omits them is read-only, and
43
+ * `collections.<name>.create/update/delete` reports which verb is missing.
44
+ * The CMS stays the source of truth: each returns the confirmed document.
45
+ */
46
+ create?(input: ContentRemoteWrite): Promise<ContentRemoteDocument>;
47
+ update?(id: string, patch: Partial<ContentRemoteWrite>): Promise<ContentRemoteDocument>;
48
+ delete?(id: string): Promise<void>;
49
+ }
50
+ export type ContentSource = ContentFileSource | ContentRemoteSource;
9
51
  export interface ContentSchemaIssue {
10
52
  readonly message: string;
11
53
  readonly path?: readonly unknown[];
@@ -98,12 +140,33 @@ export interface ContentEntry<TData = Record<string, unknown>> {
98
140
  /** Project-relative source path with POSIX separators. */
99
141
  readonly filePath: string;
100
142
  }
143
+ /**
144
+ * The server-side handle for one collection: reads from the validated
145
+ * snapshot, writes through the source's callbacks. In development a write
146
+ * rebuilds the snapshot immediately; in production the write lands in the
147
+ * CMS and the running deployment serves the bundled snapshot until the
148
+ * next build, so pair writes with a deploy hook.
149
+ */
150
+ export interface ContentCollectionHandle<TData = Record<string, unknown>> {
151
+ all(): Promise<readonly ContentEntry<TData>[]>;
152
+ get(id: string): Promise<ContentEntry<TData> | undefined>;
153
+ getOrThrow(id: string): Promise<ContentEntry<TData>>;
154
+ create(input: {
155
+ data: Record<string, unknown>;
156
+ body?: string;
157
+ }): Promise<ContentRemoteDocument>;
158
+ update(id: string, patch: {
159
+ data?: Partial<TData> & Record<string, unknown>;
160
+ body?: string;
161
+ }): Promise<ContentRemoteDocument>;
162
+ delete(id: string): Promise<void>;
163
+ }
101
164
  export interface ContentTransformContext<TData> extends ContentEntry<TData> {
102
165
  /** Number of whitespace-delimited words in body. */
103
166
  readonly words: number;
104
167
  }
105
168
  export interface ContentCollectionInput<TSchema extends ContentSchema<any>, TTransformed = InferContentSchema<TSchema>, TAssets extends ContentAssetsInput = undefined> {
106
- source: ContentFileSource;
169
+ source: ContentSource;
107
170
  schema: TSchema;
108
171
  /**
109
172
  * Process relative assets. `true` manages Markdown/MDX body references. An object also declares
@@ -114,7 +177,7 @@ export interface ContentCollectionInput<TSchema extends ContentSchema<any>, TTra
114
177
  transform?: (entry: ContentTransformContext<ContentDataWithAssets<InferContentSchema<TSchema>, TAssets>>) => TTransformed | Promise<TTransformed>;
115
178
  }
116
179
  export interface ContentCollection<TData = unknown, TSchemaData = TData> {
117
- readonly source: ContentFileSource;
180
+ readonly source: ContentSource;
118
181
  readonly schema: ContentSchema<TSchemaData>;
119
182
  readonly assets?: true | ContentAssetFields;
120
183
  readonly transform?: (entry: ContentTransformContext<any>) => unknown | Promise<unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/content",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Typed local content collections for Farm.js applications",
5
5
  "keywords": [
6
6
  "content",
@@ -42,14 +42,17 @@
42
42
  "dependencies": {
43
43
  "fast-glob": "^3.3.2",
44
44
  "gray-matter": "^4.0.3",
45
- "image-size": "^2.0.2",
45
+ "image-size": "^2.0.4",
46
46
  "mdast-util-from-markdown": "^2.0.2",
47
+ "mdast-util-mdx": "^3.0.0",
48
+ "micromark-extension-mdxjs": "^3.0.0",
47
49
  "yaml": "^2.8.0",
48
- "@farm.js/core": "0.1.0-beta.92"
50
+ "@farm.js/core": "0.1.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@types/node": "^20.10.5",
52
54
  "typescript": "^5.3.3",
55
+ "vite": "5.4.20",
53
56
  "vitest": "^3.2.7"
54
57
  },
55
58
  "engines": {