@farm.js/content 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ import path from "node:path";
2
+ import { definePlugin } from "@farm.js/core/plugin";
3
+ import fg from "fast-glob";
4
+ import { asset } from "./assets.js";
5
+ import { collection, files, isSupportedContentFile } from "./config.js";
6
+ import { loadContentCollections, writeContentServerModule } from "./loader.js";
7
+ const CONTENT_SERVER_ID = "@farm.js/content/server";
8
+ export { asset, collection, files };
9
+ /** Load schema-validated local content as a typed server-only collection registry. */
10
+ export function content(options) {
11
+ validateOptions(options);
12
+ let root = process.cwd();
13
+ let generatedFile = "";
14
+ let sourceFiles = new Set();
15
+ const plugin = definePlugin({
16
+ name: "farm:content",
17
+ enforce: "pre",
18
+ async configure(config) {
19
+ if ((config.plugins ?? []).filter((candidate) => candidate.name === "farm:content").length > 1) {
20
+ throw new Error("[farm:content] Configure all collections in one content() plugin instance");
21
+ }
22
+ root = path.resolve(config.root ?? ".");
23
+ generatedFile = path.join(root, ".farm", "content", "server.mjs");
24
+ await rebuild();
25
+ const vite = config.vite ?? {};
26
+ return {
27
+ ...config,
28
+ vite: {
29
+ ...vite,
30
+ optimizeDeps: {
31
+ ...vite.optimizeDeps,
32
+ exclude: [...new Set([CONTENT_SERVER_ID, ...(vite.optimizeDeps?.exclude ?? [])])],
33
+ },
34
+ plugins: [createContentVitePlugin(), ...(vite.plugins ?? [])],
35
+ },
36
+ };
37
+ },
38
+ });
39
+ async function rebuild() {
40
+ const attemptedSourceFiles = new Set();
41
+ try {
42
+ const loaded = await loadContentCollections(root, options.collections, attemptedSourceFiles);
43
+ await writeContentServerModule(generatedFile, loaded.collections, loaded.assetImports);
44
+ sourceFiles = new Set(loaded.sourceFiles);
45
+ }
46
+ catch (error) {
47
+ sourceFiles = new Set([...sourceFiles, ...attemptedSourceFiles]);
48
+ throw error;
49
+ }
50
+ }
51
+ function createContentVitePlugin() {
52
+ let rebuildQueue = Promise.resolve();
53
+ return {
54
+ name: "farm:content-runtime",
55
+ enforce: "pre",
56
+ resolveId(id, importer, resolveOptions) {
57
+ if (id !== CONTENT_SERVER_ID)
58
+ return undefined;
59
+ const environmentName = this.environment?.name;
60
+ const serverEnvironment = resolveOptions?.ssr || environmentName === "ssr" || environmentName === "rsc";
61
+ if (!serverEnvironment) {
62
+ throw new Error(`[farm:content] ${CONTENT_SERVER_ID} is server-only and cannot be imported into the client environment${importer ? ` by ${importer}` : ""}`);
63
+ }
64
+ return generatedFile;
65
+ },
66
+ configureServer(server) {
67
+ const onFile = (event, filePath) => {
68
+ if (event !== "add" && event !== "change" && event !== "unlink")
69
+ return;
70
+ const absoluteFile = path.resolve(filePath);
71
+ rebuildQueue = rebuildQueue
72
+ .then(async () => {
73
+ if (!(await shouldRebuildForFile(root, absoluteFile, event, sourceFiles, options.collections))) {
74
+ return;
75
+ }
76
+ await rebuild();
77
+ const modules = server.moduleGraph.getModulesByFile?.(generatedFile);
78
+ for (const module of modules ?? [])
79
+ server.moduleGraph.invalidateModule(module);
80
+ server.ws.send({ type: "full-reload" });
81
+ })
82
+ .catch((error) => {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ server.ws.send({
85
+ type: "error",
86
+ err: { message, stack: error instanceof Error ? error.stack : undefined },
87
+ });
88
+ });
89
+ };
90
+ server.watcher.on("all", onFile);
91
+ server.httpServer?.once("close", () => server.watcher.off?.("all", onFile));
92
+ },
93
+ };
94
+ }
95
+ return plugin;
96
+ }
97
+ async function shouldRebuildForFile(root, filePath, event, sourceFiles, collections) {
98
+ if (sourceFiles.has(filePath))
99
+ return true;
100
+ const relative = path.relative(root, filePath);
101
+ if (event !== "add" ||
102
+ relative === "" ||
103
+ relative.startsWith("..") ||
104
+ path.isAbsolute(relative) ||
105
+ !isSupportedContentFile(filePath)) {
106
+ return false;
107
+ }
108
+ for (const definition of Object.values(collections)) {
109
+ const matches = await fg([...definition.source.patterns], {
110
+ cwd: root,
111
+ absolute: true,
112
+ onlyFiles: true,
113
+ unique: true,
114
+ dot: false,
115
+ followSymbolicLinks: false,
116
+ ignore: [...definition.source.ignore],
117
+ });
118
+ if (matches.some((candidate) => path.resolve(candidate) === filePath))
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+ function validateOptions(options) {
124
+ if (!options || typeof options !== "object" || !options.collections) {
125
+ throw new TypeError("Content plugin needs a collections object");
126
+ }
127
+ if (Object.keys(options.collections).length === 0) {
128
+ throw new TypeError("Content plugin needs at least one collection");
129
+ }
130
+ for (const [name, definition] of Object.entries(options.collections)) {
131
+ if (!definition || typeof definition !== "object") {
132
+ throw new TypeError(`Content collection "${name}" must come from collection()`);
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,13 @@
1
+ import { type ContentAssetImport } from "./assets.js";
2
+ import type { ContentCollections, ContentEntry } from "./types.js";
3
+ export interface LoadedContent {
4
+ readonly collections: Readonly<Record<string, readonly ContentEntry<any>[]>>;
5
+ readonly sourceFiles: ReadonlySet<string>;
6
+ readonly assetImports: ReadonlyMap<string, ContentAssetImport>;
7
+ }
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>;
10
+ type EncodedContentValue = null | boolean | number | string | ["undefined"] | ["date", string] | ["bigint", string] | ["array", EncodedContentValue[]] | ["object", Array<[string, EncodedContentValue]>];
11
+ export declare function encodeContentValue(value: unknown, seen?: Set<object>): EncodedContentValue;
12
+ export {};
13
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +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"}
package/dist/loader.js ADDED
@@ -0,0 +1,360 @@
1
+ import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import matter from "gray-matter";
5
+ import { parse as parseYaml } from "yaml";
6
+ import { mergeContentAssetData, resolveContentAssetFields, splitContentAssetInput, } from "./assets.js";
7
+ import { isSupportedContentFile } from "./config.js";
8
+ import { resolveMarkdownAssets } from "./markdown-assets.js";
9
+ export async function loadContentCollections(root, collections, sourceFiles = new Set()) {
10
+ const loaded = Object.create(null);
11
+ const assetImports = new Map();
12
+ for (const [name, definition] of Object.entries(collections)) {
13
+ validateCollectionName(name);
14
+ const entries = await loadCollection(root, name, definition, sourceFiles, assetImports);
15
+ loaded[name] = Object.freeze(entries);
16
+ }
17
+ return {
18
+ collections: Object.freeze(loaded),
19
+ sourceFiles,
20
+ assetImports,
21
+ };
22
+ }
23
+ export async function writeContentServerModule(outputFile, collections, assetImports = new Map()) {
24
+ const outputDirectory = path.dirname(outputFile);
25
+ await mkdir(outputDirectory, { recursive: true });
26
+ const actualOutputDirectory = await realpath(outputDirectory);
27
+ const encoded = encodeContentValue(collections);
28
+ 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");
32
+ const assetUrls = imports.length
33
+ ? `, { ${imports
34
+ .map((asset, index) => `${JSON.stringify(asset.token)}: farmContentAsset${index}`)
35
+ .join(", ")} }`
36
+ : "";
37
+ const source = `// Generated by @farm.js/content. Do not edit.
38
+ import { createContentRuntime, decodeContentValue } from "@farm.js/content/internal/runtime";
39
+ ${importSource ? `\n${importSource}` : ""}
40
+
41
+ const runtime = createContentRuntime(decodeContentValue(${JSON.stringify(encoded)}${assetUrls}));
42
+
43
+ export const getCollection = runtime.getCollection;
44
+ export const getEntry = runtime.getEntry;
45
+ export const getEntryOrThrow = runtime.getEntryOrThrow;
46
+ `;
47
+ await writeFileIfChanged(outputFile, source);
48
+ }
49
+ async function loadCollection(root, name, definition, sourceFiles, assetImports) {
50
+ const source = definition.source;
51
+ const relativeFiles = await fg([...source.patterns], {
52
+ cwd: root,
53
+ absolute: false,
54
+ onlyFiles: true,
55
+ unique: true,
56
+ dot: false,
57
+ followSymbolicLinks: false,
58
+ ignore: [...source.ignore],
59
+ });
60
+ const files = relativeFiles
61
+ .map(normalizePath)
62
+ .filter(isSupportedContentFile)
63
+ .sort((left, right) => left.localeCompare(right));
64
+ const entries = [];
65
+ const ids = new Map();
66
+ for (const relativeFile of files) {
67
+ const absoluteFile = path.resolve(root, relativeFile);
68
+ const id = contentEntryId(relativeFile, source);
69
+ const duplicate = ids.get(id);
70
+ if (duplicate) {
71
+ throw new Error(`[farm:content] Collection "${name}" has duplicate entry ID "${id}" from ${duplicate} and ${relativeFile}`);
72
+ }
73
+ ids.set(id, relativeFile);
74
+ sourceFiles.add(absoluteFile);
75
+ const parsed = await parseContentFile(absoluteFile, relativeFile);
76
+ const assetContext = {
77
+ root,
78
+ collectionName: name,
79
+ sourceFile: relativeFile,
80
+ sourceFiles,
81
+ imports: assetImports,
82
+ };
83
+ const typedAssets = definition.assets && definition.assets !== true ? definition.assets : undefined;
84
+ let schemaInput = parsed.data;
85
+ let assetInput;
86
+ if (typedAssets) {
87
+ try {
88
+ const split = splitContentAssetInput(parsed.data, typedAssets);
89
+ schemaInput = split.schemaInput;
90
+ assetInput = split.assetInput;
91
+ }
92
+ catch (error) {
93
+ throw new Error(`[farm:content] Collection ${JSON.stringify(name)} rejected ${relativeFile}: ${errorMessage(error)}`, { cause: error });
94
+ }
95
+ }
96
+ const validatedData = await validateContentData(definition.schema, schemaInput, name, relativeFile);
97
+ const data = typedAssets
98
+ ? mergeContentAssetData(validatedData, await resolveContentAssetFields(typedAssets, assetInput, assetContext), assetContext)
99
+ : validatedData;
100
+ const resolvedBody = definition.assets && /\.mdx?$/i.test(relativeFile)
101
+ ? await resolveMarkdownAssets(parsed.body, assetContext)
102
+ : { body: parsed.body, assets: Object.freeze([]) };
103
+ const baseEntry = {
104
+ id,
105
+ data,
106
+ body: resolvedBody.body,
107
+ bodyAssets: resolvedBody.assets,
108
+ filePath: relativeFile,
109
+ words: countWords(resolvedBody.body),
110
+ };
111
+ const transformed = definition.transform
112
+ ? await definition.transform(baseEntry)
113
+ : baseEntry.data;
114
+ entries.push(Object.freeze({
115
+ id,
116
+ data: freezeContentValue(transformed),
117
+ body: resolvedBody.body,
118
+ bodyAssets: freezeContentValue(resolvedBody.assets),
119
+ filePath: relativeFile,
120
+ }));
121
+ }
122
+ return entries;
123
+ }
124
+ async function parseContentFile(absoluteFile, relativeFile) {
125
+ const source = await readFile(absoluteFile, "utf8");
126
+ const extension = path.extname(relativeFile).toLowerCase();
127
+ try {
128
+ if (extension === ".md" || extension === ".mdx") {
129
+ const parsed = matter(source);
130
+ return { data: parsed.data, body: parsed.content };
131
+ }
132
+ if (extension === ".json") {
133
+ return { data: JSON.parse(source), body: "" };
134
+ }
135
+ if (extension === ".yaml" || extension === ".yml") {
136
+ return { data: parseYaml(source), body: "" };
137
+ }
138
+ }
139
+ catch (error) {
140
+ throw new Error(`[farm:content] Could not parse ${relativeFile}: ${errorMessage(error)}`, {
141
+ cause: error,
142
+ });
143
+ }
144
+ throw new Error(`[farm:content] Unsupported content file: ${relativeFile}`);
145
+ }
146
+ async function validateContentData(schema, input, collectionName, filePath) {
147
+ try {
148
+ const standard = schema["~standard"];
149
+ if (standard) {
150
+ const result = await standard.validate(input);
151
+ if (result.issues?.length) {
152
+ throw new ContentValidationError(formatIssues(result.issues));
153
+ }
154
+ return result.value;
155
+ }
156
+ if (schema.parseAsync)
157
+ return await schema.parseAsync(input);
158
+ if (schema.parse)
159
+ return schema.parse(input);
160
+ }
161
+ catch (error) {
162
+ const details = error instanceof ContentValidationError ? error.message : formatSchemaError(error);
163
+ throw new Error(`[farm:content] Collection "${collectionName}" rejected ${filePath}: ${details}`, { cause: error });
164
+ }
165
+ throw new TypeError(`[farm:content] Collection "${collectionName}" has an invalid schema`);
166
+ }
167
+ class ContentValidationError extends Error {
168
+ }
169
+ function formatSchemaError(error) {
170
+ if (error && typeof error === "object" && "issues" in error) {
171
+ const issues = error.issues;
172
+ if (issues?.length)
173
+ return formatIssues(issues);
174
+ }
175
+ return errorMessage(error);
176
+ }
177
+ function formatIssues(issues) {
178
+ return issues
179
+ .map((issue) => {
180
+ const pathLabel = issue.path?.length
181
+ ? `${issue.path.map(formatIssuePathSegment).join(".")}: `
182
+ : "";
183
+ return `${pathLabel}${issue.message}`;
184
+ })
185
+ .join("; ");
186
+ }
187
+ function formatIssuePathSegment(segment) {
188
+ if (segment && typeof segment === "object" && "key" in segment) {
189
+ return String(segment.key);
190
+ }
191
+ return String(segment);
192
+ }
193
+ function contentEntryId(filePath, source) {
194
+ const base = source.base ?? inferCommonSourceBase(source.patterns);
195
+ const relative = base && base !== "." ? path.posix.relative(base, filePath) : filePath;
196
+ if (relative === ".." || relative.startsWith("../") || path.posix.isAbsolute(relative)) {
197
+ throw new Error(`[farm:content] Source file ${filePath} is outside the configured content base ${base}`);
198
+ }
199
+ return relative.replace(/\.(?:mdx?|json|ya?ml)$/i, "").replace(/(?:^|\/)index$/, "") || "index";
200
+ }
201
+ function inferCommonSourceBase(patterns) {
202
+ const bases = patterns.map(inferSourceBase).map((base) => base.split("/").filter(Boolean));
203
+ const [first = [], ...rest] = bases;
204
+ const common = [];
205
+ for (let index = 0; index < first.length; index += 1) {
206
+ const segment = first[index];
207
+ if (!rest.every((base) => base[index] === segment))
208
+ break;
209
+ common.push(segment);
210
+ }
211
+ return common.join("/");
212
+ }
213
+ function inferSourceBase(pattern) {
214
+ const segments = normalizePath(pattern).split("/");
215
+ const staticSegments = [];
216
+ for (const segment of segments) {
217
+ if (/[*?{}()[\]!]/.test(segment))
218
+ break;
219
+ staticSegments.push(segment);
220
+ }
221
+ if (staticSegments.length > 0 && path.posix.extname(staticSegments.at(-1) ?? "")) {
222
+ staticSegments.pop();
223
+ }
224
+ return staticSegments.join("/");
225
+ }
226
+ function validateCollectionName(name) {
227
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) {
228
+ throw new TypeError(`[farm:content] Collection name "${name}" must start with a letter and contain only letters, numbers, underscores, or hyphens`);
229
+ }
230
+ }
231
+ function countWords(body) {
232
+ const normalized = body.trim();
233
+ return normalized ? normalized.split(/\s+/).length : 0;
234
+ }
235
+ function normalizePath(value) {
236
+ return value.replace(/\\/g, "/");
237
+ }
238
+ function toViteImportSpecifier(outputDirectory, filePath) {
239
+ const relative = normalizePath(path.relative(outputDirectory, filePath));
240
+ return relative.startsWith(".") ? relative : `./${relative}`;
241
+ }
242
+ export function encodeContentValue(value, seen = new Set()) {
243
+ if (value === null || typeof value === "boolean" || typeof value === "string")
244
+ return value;
245
+ if (typeof value === "number") {
246
+ if (!Number.isFinite(value)) {
247
+ throw new TypeError("[farm:content] Content data cannot contain non-finite numbers");
248
+ }
249
+ return value;
250
+ }
251
+ if (value === undefined)
252
+ return ["undefined"];
253
+ if (typeof value === "bigint")
254
+ return ["bigint", value.toString()];
255
+ if (typeof value === "function" || typeof value === "symbol") {
256
+ throw new TypeError(`[farm:content] Content data cannot contain ${typeof value} values`);
257
+ }
258
+ if (value instanceof Date) {
259
+ if (Number.isNaN(value.getTime())) {
260
+ throw new TypeError("[farm:content] Content data cannot contain an invalid Date");
261
+ }
262
+ return ["date", value.toISOString()];
263
+ }
264
+ if (seen.has(value)) {
265
+ throw new TypeError("[farm:content] Content data cannot contain circular references");
266
+ }
267
+ seen.add(value);
268
+ try {
269
+ if (Array.isArray(value))
270
+ return ["array", encodeArray(value, seen)];
271
+ const prototype = Object.getPrototypeOf(value);
272
+ if (prototype !== Object.prototype && prototype !== null) {
273
+ throw new TypeError(`[farm:content] Content data must use plain objects, arrays, dates, bigints, and primitive values`);
274
+ }
275
+ return ["object", encodeObject(value, seen)];
276
+ }
277
+ finally {
278
+ seen.delete(value);
279
+ }
280
+ }
281
+ function encodeArray(value, seen) {
282
+ const descriptors = Object.getOwnPropertyDescriptors(value);
283
+ const keys = Reflect.ownKeys(value).filter((key) => key !== "length");
284
+ for (const key of keys) {
285
+ if (typeof key === "symbol" || !isArrayIndex(key, value.length)) {
286
+ throw new TypeError("[farm:content] Content arrays cannot contain custom properties");
287
+ }
288
+ }
289
+ return Array.from({ length: value.length }, (_, index) => {
290
+ const descriptor = descriptors[index];
291
+ if (!descriptor) {
292
+ throw new TypeError("[farm:content] Content data cannot contain sparse arrays");
293
+ }
294
+ assertSerializableProperty(descriptor);
295
+ return encodeContentValue(descriptor.value, seen);
296
+ });
297
+ }
298
+ function encodeObject(value, seen) {
299
+ const descriptors = Object.getOwnPropertyDescriptors(value);
300
+ return Reflect.ownKeys(value).map((key) => {
301
+ if (typeof key === "symbol") {
302
+ throw new TypeError("[farm:content] Content objects cannot contain symbol keys");
303
+ }
304
+ const descriptor = descriptors[key];
305
+ assertSerializableProperty(descriptor);
306
+ return [key, encodeContentValue(descriptor.value, seen)];
307
+ });
308
+ }
309
+ function assertSerializableProperty(descriptor) {
310
+ if (!("value" in descriptor)) {
311
+ throw new TypeError("[farm:content] Content data cannot contain getters or setters");
312
+ }
313
+ if (!descriptor.enumerable) {
314
+ throw new TypeError("[farm:content] Content data cannot contain non-enumerable properties");
315
+ }
316
+ }
317
+ function isArrayIndex(key, length) {
318
+ const index = Number(key);
319
+ return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
320
+ }
321
+ function freezeContentValue(value, ancestors = new Set()) {
322
+ if (!value || typeof value !== "object" || Object.isFrozen(value))
323
+ return value;
324
+ if (ancestors.has(value)) {
325
+ throw new TypeError("[farm:content] Content data cannot contain circular references");
326
+ }
327
+ ancestors.add(value);
328
+ try {
329
+ if (Array.isArray(value)) {
330
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
331
+ if ("value" in descriptor)
332
+ freezeContentValue(descriptor.value, ancestors);
333
+ }
334
+ }
335
+ else if (!(value instanceof Date)) {
336
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
337
+ if ("value" in descriptor)
338
+ freezeContentValue(descriptor.value, ancestors);
339
+ }
340
+ }
341
+ return Object.freeze(value);
342
+ }
343
+ finally {
344
+ ancestors.delete(value);
345
+ }
346
+ }
347
+ async function writeFileIfChanged(filePath, source) {
348
+ try {
349
+ if ((await readFile(filePath, "utf8")) === source)
350
+ return;
351
+ }
352
+ catch (error) {
353
+ if (error.code !== "ENOENT")
354
+ throw error;
355
+ }
356
+ await writeFile(filePath, source, "utf8");
357
+ }
358
+ function errorMessage(error) {
359
+ return error instanceof Error ? error.message : String(error);
360
+ }
@@ -0,0 +1,7 @@
1
+ import { type ContentAssetContext } from "./assets.js";
2
+ import type { ContentAssetValue } from "./types.js";
3
+ export declare function resolveMarkdownAssets(body: string, context: ContentAssetContext): Promise<{
4
+ body: string;
5
+ assets: readonly ContentAssetValue[];
6
+ }>;
7
+ //# sourceMappingURL=markdown-assets.d.ts.map
@@ -0,0 +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"}