@content-collections/core 0.2.0 → 0.3.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.
@@ -0,0 +1,186 @@
1
+ import { z } from 'zod';
2
+ import { ZodObject } from 'zod';
3
+ import { ZodRawShape } from 'zod';
4
+ import { ZodString } from 'zod';
5
+ import { ZodTypeAny } from 'zod';
6
+
7
+ declare type AddContent<TShape extends ZodRawShape> = TShape extends {
8
+ content: ZodTypeAny;
9
+ } ? TShape : TShape & WithContent;
10
+
11
+ export declare type AnyCollection = Collection<any, ZodRawShape, any, any, any>;
12
+
13
+ export declare type AnyConfiguration = Configuration<Array<AnyCollection>>;
14
+
15
+ export declare type Builder = Awaited<ReturnType<typeof createBuilder>>;
16
+
17
+ export declare type BuilderEvents = {
18
+ "builder:start": {
19
+ startedAt: number;
20
+ };
21
+ "builder:end": {
22
+ startedAt: number;
23
+ endedAt: number;
24
+ };
25
+ };
26
+
27
+ export declare class CollectError extends Error {
28
+ type: ErrorType$2;
29
+ constructor(type: ErrorType$2, message: string);
30
+ }
31
+
32
+ export declare type Collection<TName extends string, TShape extends ZodRawShape, TSchema, TTransformResult, TDocument> = Omit<CollectionRequest<TName, TShape, TSchema, TTransformResult, TDocument>, "schema"> & {
33
+ typeName: string;
34
+ schema: TShape;
35
+ };
36
+
37
+ declare type CollectionByName<TConfiguration extends AnyConfiguration> = {
38
+ [TCollection in TConfiguration["collections"][number] as TCollection["name"]]: TCollection;
39
+ };
40
+
41
+ declare type CollectionFile = {
42
+ data: {
43
+ content: string;
44
+ [key: string]: unknown;
45
+ };
46
+ path: string;
47
+ };
48
+
49
+ export declare type CollectionRequest<TName extends string, TShape extends ZodRawShape, TSchema, TTransformResult, TDocument> = {
50
+ /**
51
+ * The name of the collection
52
+ */
53
+ name: TName;
54
+ /**
55
+ * The name of the generated TypeScript type.
56
+ * If the typeName is undefined the pluralized name of the collection will be used.
57
+ */
58
+ typeName?: string;
59
+ schema: (z: Z) => TShape;
60
+ transform?: (context: Context, data: TSchema) => TTransformResult;
61
+ directory: string | string[];
62
+ include: string | string[];
63
+ onSuccess?: (documents: Array<TDocument>) => void | Promise<void>;
64
+ };
65
+
66
+ declare type CollectorEvents = {
67
+ "collector:read-error": {
68
+ filePath: string;
69
+ error: CollectError;
70
+ };
71
+ "collector:parse-error": {
72
+ filePath: string;
73
+ error: CollectError;
74
+ };
75
+ };
76
+
77
+ export declare type Configuration<TCollections extends Array<AnyCollection>> = {
78
+ collections: TCollections;
79
+ };
80
+
81
+ export declare class ConfigurationError extends Error {
82
+ type: ErrorType;
83
+ constructor(type: ErrorType, message: string);
84
+ }
85
+
86
+ export declare type Context = {
87
+ documents<TCollection extends AnyCollection>(collection: TCollection): Array<Schema<TCollection["schema"]>>;
88
+ };
89
+
90
+ export declare function createBuilder(configurationPath: string, options?: Options): Promise<{
91
+ sync: (modification: Modification, filePath: string) => Promise<boolean>;
92
+ build: () => Promise<void>;
93
+ watch: () => Promise<{
94
+ unsubscribe: () => Promise<void>;
95
+ }>;
96
+ on: {
97
+ <TKey extends "builder:start" | "builder:end" | "collector:read-error" | "collector:parse-error" | "transformer:validation-error" | "transformer:error" | "watcher:file-changed">(key: TKey, listener: (event: Events[TKey]) => void): void;
98
+ <TKey_1 extends "_error" | "_all">(key: TKey_1, listener: (event: SystemEvents[TKey_1]) => void): void;
99
+ };
100
+ }>;
101
+
102
+ export declare function defineCollection<TName extends string, TShape extends ZodRawShape, TSchema = Schema<TShape>, TTransformResult = never, TDocument = [TTransformResult] extends [never] ? Schema<TShape> : Awaited<TTransformResult>>(collection: CollectionRequest<TName, TShape, TSchema, TTransformResult, TDocument>): Collection<TName, TShape, TSchema, TTransformResult, TDocument>;
103
+
104
+ export declare function defineConfig<TConfig extends AnyConfiguration>(config: TConfig): TConfig;
105
+
106
+ declare type ErrorEvent = EventWithError & SystemEvent;
107
+
108
+ declare type ErrorType$1 = "Validation" | "Configuration" | "Transform";
109
+
110
+ declare type ErrorType$2 = "Parse" | "Read";
111
+
112
+ declare type ErrorType = "Read" | "Compile";
113
+
114
+ declare type Events = BuilderEvents & CollectorEvents & TransformerEvents & WatcherEvents;
115
+
116
+ declare type EventWithError = {
117
+ error: Error;
118
+ };
119
+
120
+ declare type GetDocument<TCollection extends AnyCollection> = TCollection extends Collection<any, ZodRawShape, any, any, infer TDocument> ? TDocument : never;
121
+
122
+ export declare type GetTypeByName<TConfiguration extends AnyConfiguration, TName extends keyof CollectionByName<TConfiguration>, TCollection = CollectionByName<TConfiguration>[TName]> = TCollection extends AnyCollection ? GetDocument<TCollection> : never;
123
+
124
+ export declare type Meta = {
125
+ filePath: string;
126
+ fileName: string;
127
+ directory: string;
128
+ path: string;
129
+ extension: string;
130
+ };
131
+
132
+ export declare type Modification = "create" | "update" | "delete";
133
+
134
+ declare type Options$1 = {
135
+ configName: string;
136
+ cacheDir?: string;
137
+ };
138
+
139
+ declare type Options = Options$1 & {
140
+ outputDir?: string;
141
+ };
142
+
143
+ export declare type Schema<TShape extends ZodRawShape> = z.infer<ZodObject<AddContent<TShape>>> & {
144
+ _meta: Meta;
145
+ };
146
+
147
+ declare type SystemEvent = {
148
+ _event: string;
149
+ };
150
+
151
+ declare type SystemEvents = {
152
+ _error: ErrorEvent;
153
+ _all: SystemEvent;
154
+ };
155
+
156
+ declare type TransformerEvents = {
157
+ "transformer:validation-error": {
158
+ collection: AnyCollection;
159
+ file: CollectionFile;
160
+ error: TransformError;
161
+ };
162
+ "transformer:error": {
163
+ collection: AnyCollection;
164
+ error: TransformError;
165
+ };
166
+ };
167
+
168
+ export declare class TransformError extends Error {
169
+ type: ErrorType$1;
170
+ constructor(type: ErrorType$1, message: string);
171
+ }
172
+
173
+ declare type WatcherEvents = {
174
+ "watcher:file-changed": {
175
+ filePath: string;
176
+ modification: Modification;
177
+ };
178
+ };
179
+
180
+ declare type WithContent = {
181
+ content: ZodString;
182
+ };
183
+
184
+ declare type Z = typeof z;
185
+
186
+ export { }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,27 @@
1
1
  import { ZodRawShape, z, ZodObject, ZodTypeAny, ZodString } from 'zod';
2
+ import { parse } from 'yaml';
3
+
4
+ type Parsers = typeof parsers;
5
+ type Parser = keyof typeof parsers;
6
+ declare function frontmatterParser(fileContent: string): {
7
+ content: string;
8
+ };
9
+ declare const parsers: {
10
+ readonly frontmatter: {
11
+ readonly hasContent: true;
12
+ readonly parse: typeof frontmatterParser;
13
+ };
14
+ readonly json: {
15
+ readonly hasContent: false;
16
+ readonly parse: (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined) => any;
17
+ };
18
+ readonly yaml: {
19
+ readonly hasContent: false;
20
+ readonly parse: typeof parse;
21
+ };
22
+ };
23
+
24
+ type CacheFn = <TInput, TOutput>(input: TInput, compute: (input: TInput) => Promise<TOutput> | TOutput) => Promise<TOutput>;
2
25
 
3
26
  type Meta = {
4
27
  filePath: string;
@@ -13,30 +36,37 @@ type WithContent = {
13
36
  type AddContent<TShape extends ZodRawShape> = TShape extends {
14
37
  content: ZodTypeAny;
15
38
  } ? TShape : TShape & WithContent;
16
- type Schema<TShape extends ZodRawShape> = z.infer<ZodObject<AddContent<TShape>>> & {
39
+ type GetParsedShape<TParser extends Parser, TShape extends ZodRawShape> = Parsers[TParser]["hasContent"] extends true ? AddContent<TShape> : TShape;
40
+ type GetShape<TParser extends Parser | undefined, TShape extends ZodRawShape> = TParser extends Parser ? GetParsedShape<TParser, TShape> : AddContent<TShape>;
41
+ type Schema<TParser extends Parser | undefined, TShape extends ZodRawShape> = z.infer<ZodObject<GetShape<TParser, TShape>>> & {
17
42
  _meta: Meta;
18
43
  };
19
44
  type Context = {
20
- documents<TCollection extends AnyCollection>(collection: TCollection): Array<Schema<TCollection["schema"]>>;
45
+ documents<TCollection extends AnyCollection>(collection: TCollection): Array<Schema<TCollection["parser"], TCollection["schema"]>>;
46
+ cache: CacheFn;
21
47
  };
22
48
  type Z = typeof z;
23
- type CollectionRequest<TName extends string, TShape extends ZodRawShape, TSchema, TTransformResult, TDocument> = {
49
+ type CollectionRequest<TName extends string, TShape extends ZodRawShape, TParser, TSchema, TTransformResult, TDocument> = {
24
50
  name: TName;
51
+ parser?: TParser;
25
52
  typeName?: string;
26
53
  schema: (z: Z) => TShape;
27
- transform?: (context: Context, data: TSchema) => TTransformResult;
28
- directory: string | string[];
54
+ transform?: (data: TSchema, context: Context) => TTransformResult;
55
+ directory: string;
29
56
  include: string | string[];
30
57
  onSuccess?: (documents: Array<TDocument>) => void | Promise<void>;
31
58
  };
32
- type Collection<TName extends string, TShape extends ZodRawShape, TSchema, TTransformResult, TDocument> = Omit<CollectionRequest<TName, TShape, TSchema, TTransformResult, TDocument>, "schema"> & {
59
+ type Collection<TName extends string, TShape extends ZodRawShape, TParser extends Parser, TSchema, TTransformResult, TDocument> = Omit<CollectionRequest<TName, TShape, TParser, TSchema, TTransformResult, TDocument>, "schema"> & {
33
60
  typeName: string;
34
61
  schema: TShape;
62
+ parser: TParser;
35
63
  };
36
- type AnyCollection = Collection<any, ZodRawShape, any, any, any>;
37
- declare function defineCollection<TName extends string, TShape extends ZodRawShape, TSchema = Schema<TShape>, TTransformResult = never, TDocument = [TTransformResult] extends [never] ? Schema<TShape> : Awaited<TTransformResult>>(collection: CollectionRequest<TName, TShape, TSchema, TTransformResult, TDocument>): Collection<TName, TShape, TSchema, TTransformResult, TDocument>;
64
+ type AnyCollection = Collection<any, ZodRawShape, Parser, any, any, any>;
65
+ declare function defineCollection<TName extends string, TShape extends ZodRawShape, TParser extends Parser = "frontmatter", TSchema = Schema<TParser, TShape>, TTransformResult = never, TDocument = [TTransformResult] extends [never] ? Schema<TParser, TShape> : Awaited<TTransformResult>>(collection: CollectionRequest<TName, TShape, TParser, TSchema, TTransformResult, TDocument>): Collection<TName, TShape, TParser, TSchema, TTransformResult, TDocument>;
66
+ type Cache = "memory" | "file" | "none";
38
67
  type Configuration<TCollections extends Array<AnyCollection>> = {
39
68
  collections: TCollections;
69
+ cache?: Cache;
40
70
  };
41
71
  type AnyConfiguration = Configuration<Array<AnyCollection>>;
42
72
  declare function defineConfig<TConfig extends AnyConfiguration>(config: TConfig): TConfig;
@@ -44,7 +74,7 @@ declare function defineConfig<TConfig extends AnyConfiguration>(config: TConfig)
44
74
  type Modification = "create" | "update" | "delete";
45
75
  type CollectionFile = {
46
76
  data: {
47
- content: string;
77
+ content?: string;
48
78
  [key: string]: unknown;
49
79
  };
50
80
  path: string;
@@ -52,7 +82,7 @@ type CollectionFile = {
52
82
  type CollectionByName<TConfiguration extends AnyConfiguration> = {
53
83
  [TCollection in TConfiguration["collections"][number] as TCollection["name"]]: TCollection;
54
84
  };
55
- type GetDocument<TCollection extends AnyCollection> = TCollection extends Collection<any, ZodRawShape, any, any, infer TDocument> ? TDocument : never;
85
+ type GetDocument<TCollection extends AnyCollection> = TCollection extends Collection<any, ZodRawShape, any, any, any, infer TDocument> ? TDocument : never;
56
86
  type GetTypeByName<TConfiguration extends AnyConfiguration, TName extends keyof CollectionByName<TConfiguration>, TCollection = CollectionByName<TConfiguration>[TName]> = TCollection extends AnyCollection ? GetDocument<TCollection> : never;
57
87
 
58
88
  type CollectorEvents = {
package/dist/index.js CHANGED
@@ -21,9 +21,14 @@ function defineCollection(collection) {
21
21
  if (!typeName) {
22
22
  typeName = generateTypeName(collection.name);
23
23
  }
24
+ let parser = collection.parser;
25
+ if (!parser) {
26
+ parser = "frontmatter";
27
+ }
24
28
  return {
25
29
  ...collection,
26
30
  typeName,
31
+ parser,
27
32
  schema: collection.schema(z)
28
33
  };
29
34
  }
@@ -39,7 +44,7 @@ import path from "path";
39
44
  // package.json
40
45
  var package_default = {
41
46
  name: "@content-collections/core",
42
- version: "0.1.2",
47
+ version: "0.3.0",
43
48
  type: "module",
44
49
  main: "dist/index.cjs",
45
50
  types: "./dist/index.d.ts",
@@ -79,12 +84,14 @@ var package_default = {
79
84
  "gray-matter": "^4.0.3",
80
85
  micromatch: "^4.0.5",
81
86
  pluralize: "^8.0.0",
87
+ yaml: "^2.3.4",
82
88
  zod: "^3.22.4"
83
89
  }
84
90
  };
85
91
 
86
92
  // src/configurationReader.ts
87
93
  import { existsSync } from "fs";
94
+ import { createHash } from "crypto";
88
95
  var ConfigurationError = class extends Error {
89
96
  type;
90
97
  constructor(type, message) {
@@ -114,7 +121,10 @@ async function compile(configurationPath, outfile) {
114
121
  }
115
122
  await esbuild.build({
116
123
  entryPoints: [configurationPath],
117
- external: [...Object.keys(package_default.dependencies), "@content-collections/*"],
124
+ external: [
125
+ ...Object.keys(package_default.dependencies),
126
+ "@content-collections/*"
127
+ ],
118
128
  bundle: true,
119
129
  platform: "node",
120
130
  format: "esm",
@@ -144,19 +154,56 @@ function createConfigurationReader() {
144
154
  );
145
155
  }
146
156
  const module = await import(`file://${path.resolve(outfile)}?x=${Date.now()}`);
157
+ const hash = createHash("sha256");
158
+ hash.update(await fs.readFile(outfile, "utf-8"));
159
+ const checksum = hash.digest("hex");
147
160
  return {
148
161
  ...module.default,
149
162
  path: configurationPath,
150
- generateTypes: true
163
+ generateTypes: true,
164
+ checksum
151
165
  };
152
166
  };
153
167
  }
154
168
 
155
169
  // src/collector.ts
156
- import matter from "gray-matter";
157
170
  import fg from "fast-glob";
158
171
  import { readFile } from "fs/promises";
159
172
  import path2 from "path";
173
+
174
+ // src/parser.ts
175
+ import matter from "gray-matter";
176
+ import { parse, stringify } from "yaml";
177
+ function frontmatterParser(fileContent) {
178
+ const { data, content } = matter(fileContent, {
179
+ engines: {
180
+ yaml: {
181
+ parse,
182
+ stringify
183
+ }
184
+ }
185
+ });
186
+ return {
187
+ ...data,
188
+ content: content.trim()
189
+ };
190
+ }
191
+ var parsers = {
192
+ frontmatter: {
193
+ hasContent: true,
194
+ parse: frontmatterParser
195
+ },
196
+ json: {
197
+ hasContent: false,
198
+ parse: JSON.parse
199
+ },
200
+ yaml: {
201
+ hasContent: false,
202
+ parse
203
+ }
204
+ };
205
+
206
+ // src/collector.ts
160
207
  var CollectError = class extends Error {
161
208
  type;
162
209
  constructor(type, message) {
@@ -176,55 +223,37 @@ function createCollector(emitter, baseDirectory = ".") {
176
223
  return null;
177
224
  }
178
225
  }
179
- function parse(file) {
180
- const { data, content } = matter(file);
181
- return {
182
- ...data,
183
- content
184
- };
185
- }
186
- async function collectFile(directory, filePath) {
187
- const file = await read(path2.join(directory, filePath));
226
+ async function collectFile(collection, filePath) {
227
+ const absolutePath = path2.join(baseDirectory, collection.directory, filePath);
228
+ const file = await read(absolutePath);
188
229
  if (!file) {
189
230
  return null;
190
231
  }
191
232
  try {
192
- const data = parse(file);
233
+ const data = parsers[collection.parser].parse(file);
193
234
  return {
194
235
  data,
195
236
  path: filePath
196
237
  };
197
238
  } catch (error) {
198
239
  emitter.emit("collector:parse-error", {
199
- filePath: path2.join(directory, filePath),
240
+ filePath: path2.join(collection.directory, filePath),
200
241
  error: new CollectError("Parse", String(error))
201
242
  });
202
243
  return null;
203
244
  }
204
245
  }
205
- async function resolveDirectory(collection, directory) {
206
- const collectionDirectory = path2.join(baseDirectory, directory);
246
+ async function resolveCollection(collection) {
247
+ const collectionDirectory = path2.join(baseDirectory, collection.directory);
207
248
  const filePaths = await fg(collection.include, {
208
249
  cwd: collectionDirectory,
209
250
  onlyFiles: true,
210
251
  absolute: false
211
252
  });
212
253
  const promises = filePaths.map(
213
- (filePath) => collectFile(collectionDirectory, filePath)
254
+ (filePath) => collectFile(collection, filePath)
214
255
  );
215
- return Promise.all(promises);
216
- }
217
- async function resolveCollection(collection) {
218
- let files = [];
219
- if (typeof collection.directory === "string") {
220
- files = await resolveDirectory(collection, collection.directory);
221
- } else {
222
- const promises = collection.directory.map(
223
- (directory) => resolveDirectory(collection, directory)
224
- );
225
- const resolved = await Promise.all(promises);
226
- files = resolved.flat();
227
- }
256
+ const files = await Promise.all(promises);
228
257
  return {
229
258
  ...collection,
230
259
  files: files.filter(isDefined).sort(orderByPath)
@@ -326,23 +355,27 @@ var TransformError = class extends Error {
326
355
  this.type = type;
327
356
  }
328
357
  };
329
- function createPath(path6, ext) {
330
- let p = path6.slice(0, -ext.length);
358
+ function createPath(path7, ext) {
359
+ let p = path7.slice(0, -ext.length);
331
360
  if (p.endsWith("/index")) {
332
361
  p = p.slice(0, -6);
333
362
  }
334
363
  return p;
335
364
  }
336
- function createTransformer(emitter) {
337
- function createSchema(schema) {
365
+ function createTransformer(emitter, cacheManager) {
366
+ function createSchema(parserName, schema) {
367
+ const parser = parsers[parserName];
368
+ if (!parser.hasContent) {
369
+ return z2.object(schema);
370
+ }
338
371
  return z2.object({
339
372
  content: z2.string(),
340
373
  ...schema
341
374
  });
342
375
  }
343
376
  async function parseFile(collection, file) {
344
- const { data, path: path6 } = file;
345
- const schema = createSchema(collection.schema);
377
+ const { data, path: path7 } = file;
378
+ const schema = createSchema(collection.parser, collection.schema);
346
379
  let parsedData = await schema.safeParseAsync(data);
347
380
  if (!parsedData.success) {
348
381
  emitter.emit("transformer:validation-error", {
@@ -352,7 +385,7 @@ function createTransformer(emitter) {
352
385
  });
353
386
  return null;
354
387
  }
355
- const ext = extname(path6);
388
+ const ext = extname(path7);
356
389
  let extension = ext;
357
390
  if (extension.startsWith(".")) {
358
391
  extension = extension.slice(1);
@@ -360,11 +393,11 @@ function createTransformer(emitter) {
360
393
  const document = {
361
394
  ...parsedData.data,
362
395
  _meta: {
363
- filePath: path6,
364
- fileName: basename(path6),
365
- directory: dirname(path6),
396
+ filePath: path7,
397
+ fileName: basename(path7),
398
+ directory: dirname(path7),
366
399
  extension,
367
- path: createPath(path6, ext)
400
+ path: createPath(path7, ext)
368
401
  }
369
402
  };
370
403
  return {
@@ -380,7 +413,7 @@ function createTransformer(emitter) {
380
413
  documents: (await Promise.all(promises)).filter(isDefined)
381
414
  };
382
415
  }
383
- function createContext(collections) {
416
+ function createContext(collections, cache) {
384
417
  return {
385
418
  documents: (collection) => {
386
419
  const resolved = collections.find((c) => c.name === collection.name);
@@ -391,19 +424,22 @@ function createTransformer(emitter) {
391
424
  );
392
425
  }
393
426
  return resolved.documents.map((doc) => doc.document);
394
- }
427
+ },
428
+ cache: cache.cacheFn
395
429
  };
396
430
  }
397
431
  async function transformCollection(collections, collection) {
398
432
  if (collection.transform) {
399
433
  const docs = [];
400
- const context = createContext(collections);
401
434
  for (const doc of collection.documents) {
435
+ const cache = cacheManager.cache(collection.name, doc.document._meta.path);
436
+ const context = createContext(collections, cache);
402
437
  try {
403
438
  docs.push({
404
439
  ...doc,
405
- document: await collection.transform(context, doc.document)
440
+ document: await collection.transform(doc.document, context)
406
441
  });
442
+ await cache.tidyUp();
407
443
  } catch (error) {
408
444
  if (error instanceof TransformError) {
409
445
  emitter.emit("transformer:error", {
@@ -418,6 +454,7 @@ function createTransformer(emitter) {
418
454
  }
419
455
  }
420
456
  }
457
+ await cacheManager.flush();
421
458
  return docs;
422
459
  }
423
460
  return collection.documents;
@@ -438,26 +475,13 @@ function createTransformer(emitter) {
438
475
  import micromatch from "micromatch";
439
476
  import path4 from "path";
440
477
  function createSynchronizer(readCollectionFile, collections, baseDirectory = ".") {
441
- function findCollectionAndDirectory(filePath) {
478
+ function findCollection(filePath) {
442
479
  const resolvedFilePath = path4.resolve(filePath);
443
- for (const collection of collections) {
444
- const directories = [];
445
- if (typeof collection.directory === "string") {
446
- directories.push(collection.directory);
447
- } else {
448
- directories.push(...collection.directory);
449
- }
450
- for (const directory of directories) {
451
- const resolvedDirectory = path4.resolve(baseDirectory, directory);
452
- if (resolvedFilePath.startsWith(resolvedDirectory)) {
453
- return {
454
- collection,
455
- directory
456
- };
457
- }
458
- }
459
- }
460
- return null;
480
+ return collections.find((collection) => {
481
+ return resolvedFilePath.startsWith(
482
+ path4.resolve(baseDirectory, collection.directory)
483
+ );
484
+ });
461
485
  }
462
486
  function createRelativePath(collectionPath, filePath) {
463
487
  const resolvedCollectionPath = path4.resolve(baseDirectory, collectionPath);
@@ -469,12 +493,11 @@ function createSynchronizer(readCollectionFile, collections, baseDirectory = "."
469
493
  return relativePath;
470
494
  }
471
495
  function resolve(filePath) {
472
- const collectionAndDirectory = findCollectionAndDirectory(filePath);
473
- if (!collectionAndDirectory) {
496
+ const collection = findCollection(filePath);
497
+ if (!collection) {
474
498
  return null;
475
499
  }
476
- const { collection, directory } = collectionAndDirectory;
477
- const relativePath = createRelativePath(directory, filePath);
500
+ const relativePath = createRelativePath(collection.directory, filePath);
478
501
  if (!micromatch.isMatch(relativePath, collection.include)) {
479
502
  return null;
480
503
  }
@@ -504,22 +527,7 @@ function createSynchronizer(readCollectionFile, collections, baseDirectory = "."
504
527
  const index = collection.files.findIndex(
505
528
  (file2) => file2.path === relativePath
506
529
  );
507
- const directories = [];
508
- if (typeof collection.directory === "string") {
509
- directories.push(collection.directory);
510
- } else {
511
- directories.push(...collection.directory);
512
- }
513
- let file = null;
514
- for (const directory of directories) {
515
- file = await readCollectionFile(
516
- path4.join(baseDirectory, directory),
517
- relativePath
518
- );
519
- if (file) {
520
- break;
521
- }
522
- }
530
+ const file = await readCollectionFile(collection, relativePath);
523
531
  if (!file) {
524
532
  return false;
525
533
  }
@@ -538,7 +546,7 @@ function createSynchronizer(readCollectionFile, collections, baseDirectory = "."
538
546
  }
539
547
 
540
548
  // src/builder.ts
541
- import path5 from "path";
549
+ import path6 from "path";
542
550
 
543
551
  // src/watcher.ts
544
552
  import * as watcher from "@parcel/watcher";
@@ -563,7 +571,7 @@ async function createWatcher(emitter, paths, sync, build2) {
563
571
  }
564
572
  };
565
573
  const subscriptions = await Promise.all(
566
- paths.map((path6) => watcher.subscribe(path6, onChange))
574
+ paths.map((path7) => watcher.subscribe(path7, onChange))
567
575
  );
568
576
  return {
569
577
  unsubscribe: async () => {
@@ -604,12 +612,96 @@ function createEmitter() {
604
612
  };
605
613
  }
606
614
 
615
+ // src/cache.ts
616
+ import path5, { join } from "path";
617
+ import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
618
+ import { existsSync as existsSync2 } from "fs";
619
+ import { createHash as createHash2 } from "crypto";
620
+ function createKey(config, input) {
621
+ return createHash2("sha256").update(config).update(JSON.stringify(input)).digest("hex");
622
+ }
623
+ async function createCacheDirectory(directory) {
624
+ const cacheDirectory = path5.join(directory, ".content-collections", "cache");
625
+ if (!existsSync2(cacheDirectory)) {
626
+ await mkdir(cacheDirectory, { recursive: true });
627
+ }
628
+ return cacheDirectory;
629
+ }
630
+ function fileName(input) {
631
+ return input.replace(/[^a-z0-9]/gi, "_").toLowerCase();
632
+ }
633
+ async function createCacheManager(baseDirectory, configChecksum) {
634
+ const cacheDirectory = await createCacheDirectory(baseDirectory);
635
+ let mapping = {};
636
+ const mappingPath = join(cacheDirectory, "mapping.json");
637
+ if (existsSync2(mappingPath)) {
638
+ mapping = JSON.parse(await readFile2(mappingPath, "utf-8"));
639
+ }
640
+ async function flush() {
641
+ await writeFile(mappingPath, JSON.stringify(mapping));
642
+ }
643
+ function cache(collection, file) {
644
+ const directory = join(
645
+ cacheDirectory,
646
+ fileName(collection),
647
+ fileName(file)
648
+ );
649
+ let collectionMapping = mapping[collection];
650
+ if (!collectionMapping) {
651
+ collectionMapping = {};
652
+ mapping[collection] = collectionMapping;
653
+ }
654
+ let fileMapping = collectionMapping[file];
655
+ if (!fileMapping) {
656
+ fileMapping = [];
657
+ collectionMapping[file] = fileMapping;
658
+ }
659
+ let newFileMapping = [];
660
+ const cacheFn = async (input, fn) => {
661
+ const key = createKey(configChecksum, input);
662
+ newFileMapping.push(key);
663
+ const filePath = join(directory, `${key}.cache`);
664
+ if (fileMapping?.includes(key) || newFileMapping.includes(key)) {
665
+ if (existsSync2(filePath)) {
666
+ return JSON.parse(await readFile2(filePath, "utf-8"));
667
+ }
668
+ }
669
+ const output = await fn(input);
670
+ if (!existsSync2(directory)) {
671
+ await mkdir(directory, { recursive: true });
672
+ }
673
+ await writeFile(filePath, JSON.stringify(output));
674
+ return output;
675
+ };
676
+ const tidyUp = async () => {
677
+ const filesToDelete = fileMapping?.filter((key) => !newFileMapping.includes(key)) || [];
678
+ for (const key of filesToDelete) {
679
+ const filePath = join(directory, `${key}.cache`);
680
+ if (existsSync2(filePath)) {
681
+ await unlink(filePath);
682
+ }
683
+ }
684
+ if (collectionMapping) {
685
+ collectionMapping[file] = newFileMapping;
686
+ }
687
+ };
688
+ return {
689
+ cacheFn,
690
+ tidyUp
691
+ };
692
+ }
693
+ return {
694
+ cache,
695
+ flush
696
+ };
697
+ }
698
+
607
699
  // src/builder.ts
608
700
  function resolveOutputDir(baseDirectory, options) {
609
701
  if (options.outputDir) {
610
702
  return options.outputDir;
611
703
  }
612
- return path5.join(baseDirectory, ".content-collections", "generated");
704
+ return path6.join(baseDirectory, ".content-collections", "generated");
613
705
  }
614
706
  async function createBuilder(configurationPath, options = {
615
707
  configName: defaultConfigName
@@ -617,7 +709,7 @@ async function createBuilder(configurationPath, options = {
617
709
  const emitter = createEmitter();
618
710
  const readConfiguration = createConfigurationReader();
619
711
  const configuration = await readConfiguration(configurationPath, options);
620
- const baseDirectory = path5.dirname(configurationPath);
712
+ const baseDirectory = path6.dirname(configurationPath);
621
713
  const directory = resolveOutputDir(baseDirectory, options);
622
714
  const collector = createCollector(emitter, baseDirectory);
623
715
  const writer = await createWriter(directory);
@@ -631,7 +723,8 @@ async function createBuilder(configurationPath, options = {
631
723
  resolved,
632
724
  baseDirectory
633
725
  );
634
- const transform = createTransformer(emitter);
726
+ const cacheManager = await createCacheManager(baseDirectory, configuration.checksum);
727
+ const transform = createTransformer(emitter, cacheManager);
635
728
  async function sync(modification, filePath) {
636
729
  if (modification === "delete") {
637
730
  return synchronizer.deleted(filePath);
@@ -655,16 +748,9 @@ async function createBuilder(configurationPath, options = {
655
748
  });
656
749
  }
657
750
  async function watch() {
658
- const paths = [];
659
- for (const collection of resolved) {
660
- if (typeof collection.directory === "string") {
661
- paths.push(path5.join(baseDirectory, collection.directory));
662
- } else {
663
- paths.push(
664
- ...collection.directory.map((dir) => path5.join(baseDirectory, dir))
665
- );
666
- }
667
- }
751
+ const paths = resolved.map(
752
+ (collection) => path6.join(baseDirectory, collection.directory)
753
+ );
668
754
  const watcher2 = await createWatcher(emitter, paths, sync, build2);
669
755
  return watcher2;
670
756
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@content-collections/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "./dist/index.d.ts",
@@ -33,6 +33,7 @@
33
33
  "gray-matter": "^4.0.3",
34
34
  "micromatch": "^4.0.5",
35
35
  "pluralize": "^8.0.0",
36
+ "yaml": "^2.3.4",
36
37
  "zod": "^3.22.4"
37
38
  },
38
39
  "scripts": {