@content-collections/core 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ Content Collections offers a variety of adapters that seamlessly integrate with
28
28
  - [Qwik](https://www.content-collections.dev/docs/quickstart/qwik)
29
29
  - [Remix (Vite)](https://www.content-collections.dev/docs/quickstart/remix-vite)
30
30
  - [Solid Start](https://www.content-collections.dev/docs/quickstart/solid)
31
- - [Svelte Kit](https://www.content-collections.dev/docs/quickstart/svelte-kit)
31
+ - [SvelteKit](https://www.content-collections.dev/docs/quickstart/svelte-kit)
32
32
  - [Vite](https://www.content-collections.dev/docs/quickstart/vite)
33
33
 
34
34
  If your framework is not listed, you can still use Content Collections by using the [CLI](https://www.content-collections.dev/docs/quickstart/cli). Please open a ticket if you want to see your framework listed.
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ declare const parsers: {
14
14
  };
15
15
  readonly json: {
16
16
  readonly hasContent: false;
17
- readonly parse: (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined) => any;
17
+ readonly parse: (text: string, reviver?: (this: any, key: string, value: any) => any) => any;
18
18
  };
19
19
  readonly yaml: {
20
20
  readonly hasContent: false;
@@ -54,12 +54,13 @@ type GetShape<TParser extends Parser | undefined, TShape extends ZodRawShape> =
54
54
  type Schema<TParser extends Parser | undefined, TShape extends ZodRawShape> = z$1.infer<ZodObject<GetShape<TParser, TShape>>> & {
55
55
  _meta: Meta;
56
56
  };
57
- type Context = {
57
+ type Context<TSchema = unknown> = {
58
58
  documents<TCollection extends AnyCollection>(collection: TCollection): Array<Schema<TCollection["parser"], TCollection["schema"]>>;
59
59
  cache: CacheFn;
60
60
  collection: {
61
61
  name: string;
62
62
  directory: string;
63
+ documents: () => Promise<Array<TSchema>>;
63
64
  };
64
65
  };
65
66
  type Z = typeof z$1;
@@ -68,7 +69,7 @@ type CollectionRequest<TName extends string, TShape extends ZodRawShape, TParser
68
69
  parser?: TParser;
69
70
  typeName?: string;
70
71
  schema: (z: Z) => TShape;
71
- transform?: (data: TSchema, context: Context) => TTransformResult;
72
+ transform?: (data: TSchema, context: Context<TSchema>) => TTransformResult;
72
73
  directory: string;
73
74
  include: string | string[];
74
75
  exclude?: string | string[];
@@ -150,12 +151,31 @@ declare class TransformError extends Error {
150
151
  }
151
152
 
152
153
  type WatcherEvents = {
153
- "watcher:file-changed": {
154
- filePath: string;
155
- modification: Modification;
154
+ "watcher:subscribe-error": {
155
+ paths: Array<string>;
156
+ error: Error;
157
+ };
158
+ "watcher:subscribed": {
159
+ paths: Array<string>;
156
160
  };
161
+ "watcher:unsubscribed": {
162
+ paths: Array<string>;
163
+ };
164
+ };
165
+ type SyncFn = (modification: Modification, path: string) => Promise<unknown>;
166
+ type WatchableCollection = {
167
+ directory: string;
157
168
  };
169
+ type WatcherConfiguration = {
170
+ inputPaths: Array<string>;
171
+ collections: Array<WatchableCollection>;
172
+ };
173
+ declare function createWatcher(emitter: Emitter, baseDirectory: string, configuration: WatcherConfiguration, sync: SyncFn): Promise<{
174
+ unsubscribe: () => Promise<void>;
175
+ }>;
176
+ type Watcher = Awaited<ReturnType<typeof createWatcher>>;
158
177
 
178
+ type EventMap = Record<string, object>;
159
179
  type EventWithError = {
160
180
  error: Error;
161
181
  };
@@ -168,6 +188,16 @@ type SystemEvents = {
168
188
  _error: ErrorEvent;
169
189
  _all: SystemEvent;
170
190
  };
191
+ type Keys<TEvents extends EventMap> = keyof TEvents & string;
192
+ type Listener<TEvent> = (event: TEvent) => void;
193
+ declare function createEmitter<TEvents extends EventMap>(): {
194
+ on: {
195
+ <TKey extends Keys<TEvents>>(key: TKey, listener: Listener<TEvents[TKey]>): void;
196
+ <TKey extends Keys<SystemEvents>>(key: TKey, listener: Listener<SystemEvents[TKey]>): void;
197
+ };
198
+ emit: <TKey extends Keys<TEvents>>(key: TKey, event: TEvents[TKey]) => void;
199
+ };
200
+ type Emitter = ReturnType<typeof createEmitter<Events>>;
171
201
 
172
202
  type ErrorType = "Read" | "Compile";
173
203
  declare class ConfigurationError extends Error {
@@ -179,29 +209,56 @@ type Options$1 = {
179
209
  cacheDir?: string;
180
210
  };
181
211
 
182
- type BuilderEvents = {
212
+ type BuildEvents = {
183
213
  "builder:start": {
184
214
  startedAt: number;
185
215
  };
186
216
  "builder:end": {
187
217
  startedAt: number;
188
218
  endedAt: number;
219
+ stats: {
220
+ collections: number;
221
+ documents: number;
222
+ };
223
+ };
224
+ };
225
+
226
+ type BuilderEvents = BuildEvents & {
227
+ "builder:created": {
228
+ createdAt: number;
229
+ configurationPath: string;
230
+ outputDirectory: string;
231
+ };
232
+ "watcher:file-changed": {
233
+ filePath: string;
234
+ modification: Modification;
235
+ };
236
+ "watcher:config-changed": {
237
+ filePath: string;
238
+ modification: Modification;
239
+ };
240
+ "watcher:config-reload-error": {
241
+ error: Error;
242
+ configurationPath: string;
189
243
  };
190
244
  };
191
245
  type Options = Options$1 & {
192
246
  outputDir?: string;
193
247
  };
194
- declare function createBuilder(configurationPath: string, options?: Options): Promise<{
195
- sync: (modification: Modification, filePath: string) => Promise<boolean>;
248
+ declare class ConfigurationReloadError extends Error {
249
+ constructor(message: string);
250
+ }
251
+ declare function createBuilder(configurationPath: string, options?: Options, emitter?: Emitter): Promise<{
196
252
  build: () => Promise<void>;
253
+ sync: (modification: Modification, filePath: string) => Promise<boolean>;
197
254
  watch: () => Promise<{
198
255
  unsubscribe: () => Promise<void>;
199
256
  }>;
200
257
  on: {
201
- <TKey extends "builder:start" | "builder:end" | "collector:read-error" | "collector:parse-error" | "transformer:validation-error" | "transformer:result-error" | "transformer:error" | "watcher:file-changed">(key: TKey, listener: (event: Events[TKey]) => void): void;
202
- <TKey_1 extends "_error" | "_all">(key: TKey_1, listener: (event: SystemEvents[TKey_1]) => void): void;
258
+ <TKey extends "builder:start" | "builder:end" | "builder:created" | "watcher:file-changed" | "watcher:config-changed" | "watcher:config-reload-error" | "collector:read-error" | "collector:parse-error" | "transformer:validation-error" | "transformer:result-error" | "transformer:error" | "watcher:subscribe-error" | "watcher:subscribed" | "watcher:unsubscribed">(key: TKey, listener: (event: Events[TKey]) => void): void;
259
+ <TKey extends "_error" | "_all">(key: TKey, listener: (event: SystemEvents[TKey]) => void): void;
203
260
  };
204
261
  }>;
205
262
  type Builder = Awaited<ReturnType<typeof createBuilder>>;
206
263
 
207
- export { type AnyCollection, type AnyConfiguration, type Builder, type BuilderEvents, CollectError, type Collection, type CollectionRequest, type Configuration, ConfigurationError, type Context, type Document, type GetTypeByName, type Meta, type Modification, type Schema, TransformError, createBuilder, defineCollection, defineConfig };
264
+ export { type AnyCollection, type AnyConfiguration, type Builder, type BuilderEvents, CollectError, type Collection, type CollectionRequest, type Configuration, ConfigurationError, ConfigurationReloadError, type Context, type Document, type GetTypeByName, type Meta, type Modification, type Schema, TransformError, type Watcher, createBuilder, defineCollection, defineConfig };
package/dist/index.js CHANGED
@@ -14,6 +14,20 @@ function isDefined(value) {
14
14
  function orderByPath(a, b) {
15
15
  return a.path.localeCompare(b.path);
16
16
  }
17
+ function removeChildPaths(paths) {
18
+ return Array.from(
19
+ new Set(
20
+ paths.filter((path8) => {
21
+ return !paths.some((otherPath) => {
22
+ if (path8 === otherPath) {
23
+ return false;
24
+ }
25
+ return path8.startsWith(otherPath);
26
+ });
27
+ })
28
+ )
29
+ );
30
+ }
17
31
 
18
32
  // src/config.ts
19
33
  var InvalidReturnTypeSymbol = Symbol(`InvalidReturnType`);
@@ -38,59 +52,81 @@ function defineConfig(config) {
38
52
  }
39
53
 
40
54
  // src/configurationReader.ts
41
- import * as esbuild from "esbuild";
42
55
  import fs from "fs/promises";
43
56
  import path from "path";
44
57
  import { existsSync } from "fs";
45
58
  import { createHash } from "crypto";
46
- var ConfigurationError = class extends Error {
47
- type;
48
- constructor(type, message) {
49
- super(message);
50
- this.type = type;
51
- }
52
- };
59
+
60
+ // src/esbuild.ts
61
+ import { build } from "esbuild";
62
+ import { match, loadTsConfig, tsconfigPathsToRegExp } from "bundle-require";
63
+ import { dirname, join } from "path";
64
+ function tsconfigResolvePaths(configPath) {
65
+ let tsconfig = loadTsConfig(dirname(configPath));
66
+ if (!tsconfig) {
67
+ tsconfig = loadTsConfig();
68
+ }
69
+ return tsconfig?.data?.compilerOptions?.paths || {};
70
+ }
71
+ function createExternalsPlugin(configPath) {
72
+ const resolvedPaths = tsconfigResolvePaths(configPath);
73
+ const resolvePatterns = tsconfigPathsToRegExp(resolvedPaths);
74
+ return {
75
+ name: "external-packages",
76
+ setup: (build3) => {
77
+ const filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/;
78
+ build3.onResolve({ filter }, ({ path: path8, kind }) => {
79
+ if (match(path8, resolvePatterns)) {
80
+ if (kind === "dynamic-import") {
81
+ return { path: path8, external: true };
82
+ }
83
+ return;
84
+ }
85
+ return { path: path8, external: true };
86
+ });
87
+ }
88
+ };
89
+ }
53
90
  var importPathPlugin = {
54
91
  name: "import-path",
55
- setup(build2) {
56
- build2.onResolve({ filter: /^\@content-collections\/core$/ }, () => {
57
- return { path: path.join(__dirname, "index.ts"), external: true };
92
+ setup(build3) {
93
+ build3.onResolve({ filter: /^\@content-collections\/core$/ }, () => {
94
+ return { path: join(__dirname, "index.ts"), external: true };
58
95
  });
59
96
  }
60
97
  };
61
- var defaultConfigName = "content-collection-config.mjs";
62
- function resolveCacheDir(config, options) {
63
- if (options.cacheDir) {
64
- return options.cacheDir;
65
- }
66
- return path.join(path.dirname(config), ".content-collections", "cache");
67
- }
68
- var externalPackagesPlugin = (configPath) => ({
69
- name: "external-packages",
70
- setup: (build2) => {
71
- const filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/;
72
- build2.onResolve({ filter }, ({ path: path7 }) => {
73
- const external = !path7.includes(configPath);
74
- return { path: path7, external };
75
- });
76
- }
77
- });
78
98
  async function compile(configurationPath, outfile) {
79
- const plugins = [
80
- externalPackagesPlugin(configurationPath)
81
- ];
99
+ const plugins = [createExternalsPlugin(configurationPath)];
82
100
  if (process.env.NODE_ENV === "test") {
83
101
  plugins.push(importPathPlugin);
84
102
  }
85
- await esbuild.build({
103
+ const result = await build({
86
104
  entryPoints: [configurationPath],
87
105
  packages: "external",
88
106
  bundle: true,
89
107
  platform: "node",
90
108
  format: "esm",
91
109
  plugins,
92
- outfile
110
+ outfile,
111
+ metafile: true
93
112
  });
113
+ return Object.keys(result.metafile.inputs);
114
+ }
115
+
116
+ // src/configurationReader.ts
117
+ var ConfigurationError = class extends Error {
118
+ type;
119
+ constructor(type, message) {
120
+ super(message);
121
+ this.type = type;
122
+ }
123
+ };
124
+ var defaultConfigName = "content-collection-config.mjs";
125
+ function resolveCacheDir(config, options) {
126
+ if (options.cacheDir) {
127
+ return options.cacheDir;
128
+ }
129
+ return path.join(path.dirname(config), ".content-collections", "cache");
94
130
  }
95
131
  function createConfigurationReader() {
96
132
  return async (configurationPath, options = {
@@ -106,30 +142,103 @@ function createConfigurationReader() {
106
142
  await fs.mkdir(cacheDir, { recursive: true });
107
143
  const outfile = path.join(cacheDir, options.configName);
108
144
  try {
109
- await compile(configurationPath, outfile);
145
+ const configurationPaths = await compile(configurationPath, outfile);
146
+ const module = await import(`file://${path.resolve(outfile)}?x=${Date.now()}`);
147
+ const hash = createHash("sha256");
148
+ hash.update(await fs.readFile(outfile, "utf-8"));
149
+ const checksum = hash.digest("hex");
150
+ return {
151
+ ...module.default,
152
+ path: configurationPath,
153
+ inputPaths: configurationPaths.map((p) => path.resolve(p)),
154
+ generateTypes: true,
155
+ checksum
156
+ };
110
157
  } catch (error) {
111
158
  throw new ConfigurationError(
112
159
  "Compile",
113
160
  `configuration file ${configurationPath} is invalid: ${error}`
114
161
  );
115
162
  }
116
- const module = await import(`file://${path.resolve(outfile)}?x=${Date.now()}`);
117
- const hash = createHash("sha256");
118
- hash.update(await fs.readFile(outfile, "utf-8"));
119
- const checksum = hash.digest("hex");
120
- return {
121
- ...module.default,
122
- path: configurationPath,
123
- generateTypes: true,
124
- checksum
125
- };
163
+ };
164
+ }
165
+
166
+ // src/builder.ts
167
+ import path7 from "path";
168
+
169
+ // src/watcher.ts
170
+ import * as watcher from "@parcel/watcher";
171
+ import path2, { dirname as dirname2, resolve } from "path";
172
+ async function createWatcher(emitter, baseDirectory, configuration, sync) {
173
+ const onChange = async (error, events) => {
174
+ if (error) {
175
+ emitter.emit("watcher:subscribe-error", {
176
+ paths,
177
+ error
178
+ });
179
+ return;
180
+ }
181
+ for (const event of events) {
182
+ await sync(event.type, event.path);
183
+ }
184
+ };
185
+ const paths = removeChildPaths([
186
+ ...configuration.collections.map((collection) => path2.join(baseDirectory, collection.directory)).map((p) => resolve(p)),
187
+ ...configuration.inputPaths.map((p) => dirname2(p))
188
+ ]);
189
+ const subscriptions = (await Promise.all(paths.map((path8) => watcher.subscribe(path8, onChange)))).filter(isDefined);
190
+ emitter.emit("watcher:subscribed", {
191
+ paths
192
+ });
193
+ return {
194
+ unsubscribe: async () => {
195
+ if (!subscriptions || subscriptions.length === 0) {
196
+ return;
197
+ }
198
+ await Promise.all(
199
+ subscriptions.map((subscription) => subscription.unsubscribe())
200
+ );
201
+ emitter.emit("watcher:unsubscribed", {
202
+ paths
203
+ });
204
+ return;
205
+ }
206
+ };
207
+ }
208
+
209
+ // src/events.ts
210
+ import { EventEmitter } from "events";
211
+ function isEventWithError(event) {
212
+ return typeof event === "object" && event !== null && "error" in event;
213
+ }
214
+ function createEmitter() {
215
+ const emitter = new EventEmitter();
216
+ function on(key, listener) {
217
+ emitter.on(key, listener);
218
+ }
219
+ function emit(key, event) {
220
+ emitter.emit(key, event);
221
+ if (isEventWithError(event)) {
222
+ emitter.emit("_error", {
223
+ ...event,
224
+ _event: key
225
+ });
226
+ }
227
+ emitter.emit("_all", {
228
+ ...event,
229
+ _event: key
230
+ });
231
+ }
232
+ return {
233
+ on,
234
+ emit
126
235
  };
127
236
  }
128
237
 
129
238
  // src/collector.ts
130
- import fg from "fast-glob";
239
+ import { glob } from "tinyglobby";
131
240
  import { readFile } from "fs/promises";
132
- import path2 from "path";
241
+ import path3 from "path";
133
242
 
134
243
  // src/parser.ts
135
244
  import matter from "gray-matter";
@@ -187,7 +296,7 @@ function createCollector(emitter, baseDirectory = ".") {
187
296
  }
188
297
  }
189
298
  async function collectFile(collection, filePath) {
190
- const absolutePath = path2.join(baseDirectory, collection.directory, filePath);
299
+ const absolutePath = path3.join(baseDirectory, collection.directory, filePath);
191
300
  const file = await read(absolutePath);
192
301
  if (!file) {
193
302
  return null;
@@ -200,7 +309,7 @@ function createCollector(emitter, baseDirectory = ".") {
200
309
  };
201
310
  } catch (error) {
202
311
  emitter.emit("collector:parse-error", {
203
- filePath: path2.join(collection.directory, filePath),
312
+ filePath: path3.join(collection.directory, filePath),
204
313
  error: new CollectError("Parse", String(error))
205
314
  });
206
315
  return null;
@@ -217,8 +326,9 @@ function createCollector(emitter, baseDirectory = ".") {
217
326
  return void 0;
218
327
  }
219
328
  async function resolveCollection(collection) {
220
- const collectionDirectory = path2.join(baseDirectory, collection.directory);
221
- const filePaths = await fg(collection.include, {
329
+ const collectionDirectory = path3.join(baseDirectory, collection.directory);
330
+ const include = Array.isArray(collection.include) ? collection.include : [collection.include];
331
+ const filePaths = await glob(include, {
222
332
  cwd: collectionDirectory,
223
333
  onlyFiles: true,
224
334
  absolute: false,
@@ -247,7 +357,7 @@ function createCollector(emitter, baseDirectory = ".") {
247
357
 
248
358
  // src/writer.ts
249
359
  import fs2 from "fs/promises";
250
- import path3 from "path";
360
+ import path4 from "path";
251
361
  import pluralize2 from "pluralize";
252
362
 
253
363
  // src/serializer.ts
@@ -286,7 +396,7 @@ function createArrayConstName(name) {
286
396
  return "all" + pluralize2(suffix);
287
397
  }
288
398
  async function createDataFile(directory, collection) {
289
- const dataPath = path3.join(
399
+ const dataPath = path4.join(
290
400
  directory,
291
401
  `${createArrayConstName(collection.name)}.${extension}`
292
402
  );
@@ -313,11 +423,11 @@ async function createJavaScriptFile(directory, configuration) {
313
423
  }
314
424
  content += "\n";
315
425
  content += "export { " + collections.join(", ") + " };\n";
316
- await fs2.writeFile(path3.join(directory, "index.js"), content, "utf-8");
426
+ await fs2.writeFile(path4.join(directory, "index.js"), content, "utf-8");
317
427
  }
318
428
  function createImportPath(directory, target) {
319
- let importPath = path3.posix.join(
320
- ...path3.relative(directory, target).split(path3.sep)
429
+ let importPath = path4.posix.join(
430
+ ...path4.relative(directory, target).split(path4.sep)
321
431
  );
322
432
  if (!importPath.startsWith(".")) {
323
433
  importPath = "./" + importPath;
@@ -345,7 +455,7 @@ import { GetTypeByName } from "@content-collections/core";
345
455
  }
346
456
  content += "\n";
347
457
  content += "export {};\n";
348
- await fs2.writeFile(path3.join(directory, "index.d.ts"), content, "utf-8");
458
+ await fs2.writeFile(path4.join(directory, "index.d.ts"), content, "utf-8");
349
459
  }
350
460
  async function createWriter(directory) {
351
461
  await fs2.mkdir(directory, { recursive: true });
@@ -356,179 +466,28 @@ async function createWriter(directory) {
356
466
  };
357
467
  }
358
468
 
359
- // src/transformer.ts
360
- import { basename, dirname, extname } from "path";
361
- import { z as z3 } from "zod";
362
- var TransformError = class extends Error {
363
- type;
364
- constructor(type, message) {
365
- super(message);
366
- this.type = type;
367
- }
368
- };
369
- function createPath(path7, ext) {
370
- let p = path7.slice(0, -ext.length);
371
- if (p.endsWith("/index")) {
372
- p = p.slice(0, -6);
373
- }
374
- return p;
375
- }
376
- function createTransformer(emitter, cacheManager) {
377
- function createSchema(parserName, schema2) {
378
- const parser = parsers[parserName];
379
- if (!parser.hasContent) {
380
- return z3.object(schema2);
381
- }
382
- return z3.object({
383
- content: z3.string(),
384
- ...schema2
385
- });
386
- }
387
- async function parseFile(collection, file) {
388
- const { data, path: path7 } = file;
389
- const schema2 = createSchema(collection.parser, collection.schema);
390
- let parsedData = await schema2.safeParseAsync(data);
391
- if (!parsedData.success) {
392
- emitter.emit("transformer:validation-error", {
393
- collection,
394
- file,
395
- error: new TransformError("Validation", parsedData.error.message)
396
- });
397
- return null;
398
- }
399
- const ext = extname(path7);
400
- let extension2 = ext;
401
- if (extension2.startsWith(".")) {
402
- extension2 = extension2.slice(1);
403
- }
404
- const document = {
405
- ...parsedData.data,
406
- _meta: {
407
- filePath: path7,
408
- fileName: basename(path7),
409
- directory: dirname(path7),
410
- extension: extension2,
411
- path: createPath(path7, ext)
412
- }
413
- };
414
- return {
415
- document
416
- };
417
- }
418
- async function parseCollection(collection) {
419
- const promises = collection.files.map(
420
- (file) => parseFile(collection, file)
421
- );
422
- return {
423
- ...collection,
424
- documents: (await Promise.all(promises)).filter(isDefined)
425
- };
426
- }
427
- function createContext(collections, collection, cache) {
428
- return {
429
- documents: (collection2) => {
430
- const resolved = collections.find((c) => c.name === collection2.name);
431
- if (!resolved) {
432
- throw new TransformError(
433
- "Configuration",
434
- `Collection ${collection2.name} not found, do you have registered it in your configuration?`
435
- );
436
- }
437
- return resolved.documents.map((doc) => doc.document);
438
- },
439
- collection: {
440
- name: collection.name,
441
- directory: collection.directory
442
- },
443
- cache: cache.cacheFn
444
- };
445
- }
446
- async function transformCollection(collections, collection) {
447
- if (collection.transform) {
448
- const docs = [];
449
- for (const doc of collection.documents) {
450
- const cache = cacheManager.cache(
451
- collection.name,
452
- doc.document._meta.path
453
- );
454
- const context = createContext(collections, collection, cache);
455
- try {
456
- const document = await collection.transform(doc.document, context);
457
- docs.push({
458
- ...doc,
459
- document
460
- });
461
- await cache.tidyUp();
462
- } catch (error) {
463
- if (error instanceof TransformError) {
464
- emitter.emit("transformer:error", {
465
- collection,
466
- error
467
- });
468
- } else {
469
- emitter.emit("transformer:error", {
470
- collection,
471
- error: new TransformError("Transform", String(error))
472
- });
473
- }
474
- }
475
- }
476
- await cacheManager.flush();
477
- return docs;
478
- }
479
- return collection.documents;
480
- }
481
- async function validateDocuments(collection, documents) {
482
- const docs = [];
483
- for (const doc of documents) {
484
- let parsedData = await serializableSchema.safeParseAsync(doc.document);
485
- if (parsedData.success) {
486
- docs.push(doc);
487
- } else {
488
- emitter.emit("transformer:result-error", {
489
- collection,
490
- document: doc.document,
491
- error: new TransformError("Result", parsedData.error.message)
492
- });
493
- }
494
- }
495
- return docs;
496
- }
497
- return async (untransformedCollections) => {
498
- const promises = untransformedCollections.map(
499
- (collection) => parseCollection(collection)
500
- );
501
- const collections = await Promise.all(promises);
502
- for (const collection of collections) {
503
- const documents = await transformCollection(collections, collection);
504
- collection.documents = await validateDocuments(collection, documents);
505
- }
506
- return collections;
507
- };
508
- }
509
-
510
469
  // src/synchronizer.ts
511
- import micromatch from "micromatch";
512
- import path4 from "path";
470
+ import picomatch from "picomatch";
471
+ import path5 from "path";
513
472
  function createSynchronizer(readCollectionFile, collections, baseDirectory = ".") {
514
473
  function findCollections(filePath) {
515
- const resolvedFilePath = path4.resolve(filePath);
474
+ const resolvedFilePath = path5.resolve(filePath);
516
475
  return collections.filter((collection) => {
517
476
  return resolvedFilePath.startsWith(
518
- path4.resolve(baseDirectory, collection.directory)
477
+ path5.resolve(baseDirectory, collection.directory)
519
478
  );
520
479
  });
521
480
  }
522
481
  function createRelativePath(collectionPath, filePath) {
523
- const resolvedCollectionPath = path4.resolve(baseDirectory, collectionPath);
524
- const resolvedFilePath = path4.resolve(filePath);
482
+ const resolvedCollectionPath = path5.resolve(baseDirectory, collectionPath);
483
+ const resolvedFilePath = path5.resolve(filePath);
525
484
  let relativePath = resolvedFilePath.slice(resolvedCollectionPath.length);
526
- if (relativePath.startsWith("/")) {
527
- relativePath = relativePath.slice(1);
485
+ if (relativePath.startsWith(path5.sep)) {
486
+ relativePath = relativePath.slice(path5.sep.length);
528
487
  }
529
488
  return relativePath;
530
489
  }
531
- function resolve(filePath) {
490
+ function resolve2(filePath) {
532
491
  const collections2 = findCollections(filePath);
533
492
  return collections2.map((collection) => {
534
493
  const relativePath = createRelativePath(collection.directory, filePath);
@@ -537,13 +496,13 @@ function createSynchronizer(readCollectionFile, collections, baseDirectory = "."
537
496
  relativePath
538
497
  };
539
498
  }).filter(({ collection, relativePath }) => {
540
- return micromatch.isMatch(relativePath, collection.include, {
499
+ return picomatch.isMatch(relativePath, collection.include, {
541
500
  ignore: collection.exclude
542
501
  });
543
502
  });
544
503
  }
545
504
  function deleted(filePath) {
546
- const resolvedCollections = resolve(filePath);
505
+ const resolvedCollections = resolve2(filePath);
547
506
  if (resolvedCollections.length === 0) {
548
507
  return false;
549
508
  }
@@ -560,7 +519,7 @@ function createSynchronizer(readCollectionFile, collections, baseDirectory = "."
560
519
  return changed2;
561
520
  }
562
521
  async function changed(filePath) {
563
- const resolvedCollections = resolve(filePath);
522
+ const resolvedCollections = resolve2(filePath);
564
523
  if (resolvedCollections.length === 0) {
565
524
  return false;
566
525
  }
@@ -588,75 +547,8 @@ function createSynchronizer(readCollectionFile, collections, baseDirectory = "."
588
547
  };
589
548
  }
590
549
 
591
- // src/builder.ts
592
- import path6 from "path";
593
-
594
- // src/watcher.ts
595
- import * as watcher from "@parcel/watcher";
596
- async function createWatcher(emitter, paths, sync, build2) {
597
- const onChange = async (error, events) => {
598
- if (error) {
599
- console.error(error);
600
- return;
601
- }
602
- let rebuild = false;
603
- for (const event of events) {
604
- if (await sync(event.type, event.path)) {
605
- emitter.emit("watcher:file-changed", {
606
- filePath: event.path,
607
- modification: event.type
608
- });
609
- rebuild = true;
610
- }
611
- }
612
- if (rebuild) {
613
- await build2();
614
- }
615
- };
616
- const subscriptions = await Promise.all(
617
- paths.map((path7) => watcher.subscribe(path7, onChange))
618
- );
619
- return {
620
- unsubscribe: async () => {
621
- await Promise.all(
622
- subscriptions.map((subscription) => subscription.unsubscribe())
623
- );
624
- return;
625
- }
626
- };
627
- }
628
-
629
- // src/events.ts
630
- import { EventEmitter } from "events";
631
- function isEventWithError(event) {
632
- return typeof event === "object" && event !== null && "error" in event;
633
- }
634
- function createEmitter() {
635
- const emitter = new EventEmitter();
636
- function on(key, listener) {
637
- emitter.on(key, listener);
638
- }
639
- function emit(key, event) {
640
- emitter.emit(key, event);
641
- if (isEventWithError(event)) {
642
- emitter.emit("_error", {
643
- ...event,
644
- _event: key
645
- });
646
- }
647
- emitter.emit("_all", {
648
- ...event,
649
- _event: key
650
- });
651
- }
652
- return {
653
- on,
654
- emit
655
- };
656
- }
657
-
658
550
  // src/cache.ts
659
- import path5, { join } from "path";
551
+ import path6, { join as join2 } from "path";
660
552
  import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
661
553
  import { existsSync as existsSync2 } from "fs";
662
554
  import { createHash as createHash2 } from "crypto";
@@ -664,7 +556,7 @@ function createKey(config, input) {
664
556
  return createHash2("sha256").update(config).update(JSON.stringify(input)).digest("hex");
665
557
  }
666
558
  async function createCacheDirectory(directory) {
667
- const cacheDirectory = path5.join(directory, ".content-collections", "cache");
559
+ const cacheDirectory = path6.join(directory, ".content-collections", "cache");
668
560
  if (!existsSync2(cacheDirectory)) {
669
561
  await mkdir(cacheDirectory, { recursive: true });
670
562
  }
@@ -687,13 +579,13 @@ async function readMapping(mappingPath) {
687
579
  }
688
580
  async function createCacheManager(baseDirectory, configChecksum) {
689
581
  const cacheDirectory = await createCacheDirectory(baseDirectory);
690
- const mappingPath = join(cacheDirectory, "mapping.json");
582
+ const mappingPath = join2(cacheDirectory, "mapping.json");
691
583
  const mapping = await readMapping(mappingPath);
692
584
  async function flush() {
693
585
  await writeFile(mappingPath, JSON.stringify(mapping));
694
586
  }
695
587
  function cache(collection, file) {
696
- const directory = join(
588
+ const directory = join2(
697
589
  cacheDirectory,
698
590
  fileName(collection),
699
591
  fileName(file)
@@ -712,7 +604,7 @@ async function createCacheManager(baseDirectory, configChecksum) {
712
604
  const cacheFn = async (input, fn) => {
713
605
  const key = createKey(configChecksum, input);
714
606
  newFileMapping.push(key);
715
- const filePath = join(directory, `${key}.cache`);
607
+ const filePath = join2(directory, `${key}.cache`);
716
608
  if (fileMapping?.includes(key) || newFileMapping.includes(key)) {
717
609
  if (existsSync2(filePath)) {
718
610
  try {
@@ -734,7 +626,7 @@ async function createCacheManager(baseDirectory, configChecksum) {
734
626
  const tidyUp = async () => {
735
627
  const filesToDelete = fileMapping?.filter((key) => !newFileMapping.includes(key)) || [];
736
628
  for (const key of filesToDelete) {
737
- const filePath = join(directory, `${key}.cache`);
629
+ const filePath = join2(directory, `${key}.cache`);
738
630
  if (existsSync2(filePath)) {
739
631
  await unlink(filePath);
740
632
  }
@@ -754,67 +646,337 @@ async function createCacheManager(baseDirectory, configChecksum) {
754
646
  };
755
647
  }
756
648
 
757
- // src/builder.ts
758
- function resolveOutputDir(baseDirectory, options) {
759
- if (options.outputDir) {
760
- return options.outputDir;
649
+ // src/transformer.ts
650
+ import { basename, dirname as dirname3, extname } from "path";
651
+ import { z as z3 } from "zod";
652
+ import os from "os";
653
+ import pLimit from "p-limit";
654
+ var TransformError = class extends Error {
655
+ type;
656
+ constructor(type, message) {
657
+ super(message);
658
+ this.type = type;
761
659
  }
762
- return path6.join(baseDirectory, ".content-collections", "generated");
660
+ };
661
+ function createPath(path8, ext) {
662
+ let p = path8.slice(0, -ext.length);
663
+ if (p.endsWith("/index")) {
664
+ p = p.slice(0, -6);
665
+ }
666
+ return p;
763
667
  }
764
- async function createBuilder(configurationPath, options = {
765
- configName: defaultConfigName
668
+ function createTransformer(emitter, cacheManager) {
669
+ function createSchema(parserName, schema2) {
670
+ const parser = parsers[parserName];
671
+ if (!parser.hasContent) {
672
+ return z3.object(schema2);
673
+ }
674
+ return z3.object({
675
+ content: z3.string(),
676
+ ...schema2
677
+ });
678
+ }
679
+ async function parseFile(collection, file) {
680
+ const { data, path: path8 } = file;
681
+ const schema2 = createSchema(collection.parser, collection.schema);
682
+ let parsedData = await schema2.safeParseAsync(data);
683
+ if (!parsedData.success) {
684
+ emitter.emit("transformer:validation-error", {
685
+ collection,
686
+ file,
687
+ error: new TransformError("Validation", parsedData.error.message)
688
+ });
689
+ return null;
690
+ }
691
+ const ext = extname(path8);
692
+ let extension2 = ext;
693
+ if (extension2.startsWith(".")) {
694
+ extension2 = extension2.slice(1);
695
+ }
696
+ const document = {
697
+ ...parsedData.data,
698
+ _meta: {
699
+ filePath: path8,
700
+ fileName: basename(path8),
701
+ directory: dirname3(path8),
702
+ extension: extension2,
703
+ path: createPath(path8, ext)
704
+ }
705
+ };
706
+ return {
707
+ document
708
+ };
709
+ }
710
+ async function parseCollection(collection) {
711
+ const promises = collection.files.map(
712
+ (file) => parseFile(collection, file)
713
+ );
714
+ return {
715
+ ...collection,
716
+ documents: (await Promise.all(promises)).filter(isDefined)
717
+ };
718
+ }
719
+ function createContext(collections, collection, cache) {
720
+ return {
721
+ documents: (collection2) => {
722
+ const resolved = collections.find((c) => c.name === collection2.name);
723
+ if (!resolved) {
724
+ throw new TransformError(
725
+ "Configuration",
726
+ `Collection ${collection2.name} not found, do you have registered it in your configuration?`
727
+ );
728
+ }
729
+ return resolved.documents.map((doc) => doc.document);
730
+ },
731
+ collection: {
732
+ name: collection.name,
733
+ directory: collection.directory,
734
+ documents: async () => {
735
+ return collection.documents.map((doc) => doc.document);
736
+ }
737
+ },
738
+ cache: cache.cacheFn
739
+ };
740
+ }
741
+ async function transformDocument(collections, collection, transform, doc) {
742
+ const cache = cacheManager.cache(collection.name, doc.document._meta.path);
743
+ const context = createContext(collections, collection, cache);
744
+ try {
745
+ const document = await transform(doc.document, context);
746
+ await cache.tidyUp();
747
+ return {
748
+ ...doc,
749
+ document
750
+ };
751
+ } catch (error) {
752
+ if (error instanceof TransformError) {
753
+ emitter.emit("transformer:error", {
754
+ collection,
755
+ error
756
+ });
757
+ } else {
758
+ emitter.emit("transformer:error", {
759
+ collection,
760
+ error: new TransformError("Transform", String(error))
761
+ });
762
+ }
763
+ }
764
+ }
765
+ async function transformCollection(collections, collection) {
766
+ const transform = collection.transform;
767
+ if (transform) {
768
+ const limit = pLimit(os.cpus().length);
769
+ const docs = collection.documents.map(
770
+ (doc) => limit(() => transformDocument(collections, collection, transform, doc))
771
+ );
772
+ const transformed = await Promise.all(docs);
773
+ await cacheManager.flush();
774
+ return transformed.filter(isDefined);
775
+ }
776
+ return collection.documents;
777
+ }
778
+ async function validateDocuments(collection, documents) {
779
+ const docs = [];
780
+ for (const doc of documents) {
781
+ let parsedData = await serializableSchema.safeParseAsync(doc.document);
782
+ if (parsedData.success) {
783
+ docs.push(doc);
784
+ } else {
785
+ emitter.emit("transformer:result-error", {
786
+ collection,
787
+ document: doc.document,
788
+ error: new TransformError("Result", parsedData.error.message)
789
+ });
790
+ }
791
+ }
792
+ return docs;
793
+ }
794
+ return async (untransformedCollections) => {
795
+ const promises = untransformedCollections.map(
796
+ (collection) => parseCollection(collection)
797
+ );
798
+ const collections = await Promise.all(promises);
799
+ for (const collection of collections) {
800
+ const documents = await transformCollection(collections, collection);
801
+ collection.documents = await validateDocuments(collection, documents);
802
+ }
803
+ return collections;
804
+ };
805
+ }
806
+
807
+ // src/build.ts
808
+ async function createBuildContext({
809
+ emitter,
810
+ outputDirectory,
811
+ baseDirectory,
812
+ configuration
766
813
  }) {
767
- const emitter = createEmitter();
768
- const readConfiguration = createConfigurationReader();
769
- const configuration = await readConfiguration(configurationPath, options);
770
- const baseDirectory = path6.dirname(configurationPath);
771
- const directory = resolveOutputDir(baseDirectory, options);
772
814
  const collector = createCollector(emitter, baseDirectory);
773
- const writer = await createWriter(directory);
774
- const [resolved] = await Promise.all([
815
+ const [writer, resolved, cacheManager] = await Promise.all([
816
+ createWriter(outputDirectory),
775
817
  collector.collect(configuration.collections),
776
- writer.createJavaScriptFile(configuration),
777
- writer.createTypeDefinitionFile(configuration)
818
+ createCacheManager(baseDirectory, configuration.checksum)
778
819
  ]);
779
820
  const synchronizer = createSynchronizer(
780
821
  collector.collectFile,
781
822
  resolved,
782
823
  baseDirectory
783
824
  );
784
- const cacheManager = await createCacheManager(baseDirectory, configuration.checksum);
785
825
  const transform = createTransformer(emitter, cacheManager);
826
+ return {
827
+ resolved,
828
+ writer,
829
+ synchronizer,
830
+ transform,
831
+ emitter,
832
+ cacheManager,
833
+ configuration
834
+ };
835
+ }
836
+ async function build2({
837
+ emitter,
838
+ transform,
839
+ resolved,
840
+ writer,
841
+ configuration
842
+ }) {
843
+ const startedAt = Date.now();
844
+ emitter.emit("builder:start", {
845
+ startedAt
846
+ });
847
+ const collections = await transform(resolved);
848
+ await Promise.all([
849
+ writer.createDataFiles(collections),
850
+ writer.createTypeDefinitionFile(configuration),
851
+ writer.createJavaScriptFile(configuration)
852
+ ]);
853
+ const pendingOnSuccess = collections.filter((collection) => Boolean(collection.onSuccess)).map(
854
+ (collection) => collection.onSuccess?.(collection.documents.map((doc) => doc.document))
855
+ );
856
+ await Promise.all(pendingOnSuccess.filter(isDefined));
857
+ const stats = collections.reduce(
858
+ (acc, collection) => {
859
+ acc.collections++;
860
+ acc.documents += collection.documents.length;
861
+ return acc;
862
+ },
863
+ {
864
+ collections: 0,
865
+ documents: 0
866
+ }
867
+ );
868
+ emitter.emit("builder:end", {
869
+ startedAt,
870
+ endedAt: Date.now(),
871
+ stats
872
+ });
873
+ }
874
+
875
+ // src/builder.ts
876
+ function resolveOutputDir(baseDirectory, options) {
877
+ if (options.outputDir) {
878
+ return options.outputDir;
879
+ }
880
+ return path7.join(baseDirectory, ".content-collections", "generated");
881
+ }
882
+ var ConfigurationReloadError = class extends Error {
883
+ constructor(message) {
884
+ super(message);
885
+ }
886
+ };
887
+ async function createBuilder(configurationPath, options = {
888
+ configName: defaultConfigName
889
+ }, emitter = createEmitter()) {
890
+ const readConfiguration = createConfigurationReader();
891
+ const baseDirectory = path7.dirname(configurationPath);
892
+ const outputDirectory = resolveOutputDir(baseDirectory, options);
893
+ emitter.emit("builder:created", {
894
+ createdAt: Date.now(),
895
+ configurationPath,
896
+ outputDirectory
897
+ });
898
+ let configuration = await readConfiguration(configurationPath, options);
899
+ let watcher2 = null;
900
+ let context = await createBuildContext({
901
+ emitter,
902
+ baseDirectory,
903
+ outputDirectory,
904
+ configuration
905
+ });
786
906
  async function sync(modification, filePath) {
787
- if (modification === "delete") {
788
- return synchronizer.deleted(filePath);
907
+ if (configuration.inputPaths.includes(filePath)) {
908
+ if (await onConfigurationChange()) {
909
+ emitter.emit("watcher:config-changed", {
910
+ filePath,
911
+ modification
912
+ });
913
+ await build2(context);
914
+ return true;
915
+ }
916
+ } else {
917
+ if (await onFileChange(modification, filePath)) {
918
+ emitter.emit("watcher:file-changed", {
919
+ filePath,
920
+ modification
921
+ });
922
+ await build2(context);
923
+ return true;
924
+ }
789
925
  }
790
- return synchronizer.changed(filePath);
926
+ return false;
791
927
  }
792
- async function build2() {
793
- const startedAt = Date.now();
794
- emitter.emit("builder:start", {
795
- startedAt
796
- });
797
- const collections = await transform(resolved);
798
- await writer.createDataFiles(collections);
799
- const pendingOnSuccess = collections.filter((collection) => Boolean(collection.onSuccess)).map(
800
- (collection) => collection.onSuccess?.(collection.documents.map((doc) => doc.document))
801
- );
802
- await Promise.all(pendingOnSuccess.filter(isDefined));
803
- emitter.emit("builder:end", {
804
- startedAt,
805
- endedAt: Date.now()
928
+ async function onConfigurationChange() {
929
+ try {
930
+ configuration = await readConfiguration(configurationPath, options);
931
+ } catch (error) {
932
+ emitter.emit("watcher:config-reload-error", {
933
+ error: new ConfigurationReloadError(
934
+ `Failed to reload configuration: ${error}`
935
+ ),
936
+ configurationPath
937
+ });
938
+ return false;
939
+ }
940
+ if (watcher2) {
941
+ await watcher2.unsubscribe();
942
+ }
943
+ context = await createBuildContext({
944
+ emitter,
945
+ baseDirectory,
946
+ outputDirectory,
947
+ configuration
806
948
  });
949
+ if (watcher2) {
950
+ watcher2 = await createWatcher(
951
+ emitter,
952
+ baseDirectory,
953
+ configuration,
954
+ sync
955
+ );
956
+ }
957
+ return true;
958
+ }
959
+ async function onFileChange(modification, filePath) {
960
+ const { synchronizer } = context;
961
+ if (modification === "delete") {
962
+ return synchronizer.deleted(filePath);
963
+ } else {
964
+ return synchronizer.changed(filePath);
965
+ }
807
966
  }
808
967
  async function watch() {
809
- const paths = resolved.map(
810
- (collection) => path6.join(baseDirectory, collection.directory)
811
- );
812
- const watcher2 = await createWatcher(emitter, paths, sync, build2);
813
- return watcher2;
968
+ watcher2 = await createWatcher(emitter, baseDirectory, configuration, sync);
969
+ return {
970
+ unsubscribe: async () => {
971
+ if (watcher2) {
972
+ await watcher2.unsubscribe();
973
+ }
974
+ }
975
+ };
814
976
  }
815
977
  return {
978
+ build: () => build2(context),
816
979
  sync,
817
- build: build2,
818
980
  watch,
819
981
  on: emitter.on
820
982
  };
@@ -822,6 +984,7 @@ async function createBuilder(configurationPath, options = {
822
984
  export {
823
985
  CollectError,
824
986
  ConfigurationError,
987
+ ConfigurationReloadError,
825
988
  TransformError,
826
989
  createBuilder,
827
990
  defineCollection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@content-collections/core",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -19,27 +19,29 @@
19
19
  "typescript": "^5.0.2"
20
20
  },
21
21
  "devDependencies": {
22
- "@types/micromatch": "^4.0.7",
23
22
  "@types/node": "^20.14.9",
23
+ "@types/picomatch": "^3.0.1",
24
24
  "@types/pluralize": "^0.0.33",
25
25
  "@types/serialize-javascript": "^5.0.4",
26
- "@vitest/coverage-v8": "^1.5.0",
26
+ "@vitest/coverage-v8": "^2.0.5",
27
27
  "tsup": "^8.0.2",
28
28
  "tsx": "^4.1.1",
29
- "typescript": "^5.4.5",
30
- "vitest": "^1.5.0"
29
+ "typescript": "^5.5.4",
30
+ "vitest": "^2.0.5"
31
31
  },
32
32
  "dependencies": {
33
33
  "@parcel/watcher": "^2.4.1",
34
+ "bundle-require": "^5.0.0",
34
35
  "camelcase": "^8.0.0",
35
36
  "esbuild": "^0.21.4",
36
- "fast-glob": "^3.3.2",
37
37
  "gray-matter": "^4.0.3",
38
- "micromatch": "^4.0.7",
38
+ "p-limit": "^6.1.0",
39
+ "picomatch": "^4.0.2",
39
40
  "pluralize": "^8.0.0",
40
41
  "serialize-javascript": "^6.0.2",
42
+ "tinyglobby": "^0.2.5",
41
43
  "yaml": "^2.4.5",
42
- "zod": "^3.22.5"
44
+ "zod": "^3.23.8"
43
45
  },
44
46
  "scripts": {
45
47
  "build": "tsup src/index.ts --format esm --dts -d dist",