@content-collections/core 0.1.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/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-test.log +47 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/coverage/.tmp/coverage-0.json +1 -0
- package/coverage/.tmp/coverage-1.json +1 -0
- package/coverage/.tmp/coverage-2.json +1 -0
- package/coverage/.tmp/coverage-3.json +1 -0
- package/coverage/.tmp/coverage-4.json +1 -0
- package/coverage/.tmp/coverage-5.json +1 -0
- package/coverage/.tmp/coverage-6.json +1 -0
- package/coverage/.tmp/coverage-7.json +1 -0
- package/coverage/.tmp/coverage-8.json +1 -0
- package/coverage/.tmp/coverage-9.json +1 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/builder.ts.html +424 -0
- package/coverage/clover.xml +960 -0
- package/coverage/collector.ts.html +427 -0
- package/coverage/config.ts.html +403 -0
- package/coverage/configurationReader.ts.html +409 -0
- package/coverage/coverage-final.json +11 -0
- package/coverage/events.ts.html +310 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +251 -0
- package/coverage/index.ts.html +106 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/synchronizer.ts.html +394 -0
- package/coverage/transformer.ts.html +607 -0
- package/coverage/utils.ts.html +118 -0
- package/coverage/writer.ts.html +424 -0
- package/dist/index.cjs +416 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +630 -0
- package/package.json +39 -0
- package/src/__tests__/.content-collections/cache/config.001.ts.js +19 -0
- package/src/__tests__/.content-collections/cache/config.002.mjs +16 -0
- package/src/__tests__/.content-collections/cache/config.002.ts.js +16 -0
- package/src/__tests__/.content-collections/cache/config.003.ts.js +24 -0
- package/src/__tests__/.content-collections/cache/config.004.mjs +47 -0
- package/src/__tests__/.content-collections/different-cache-dir/different.config.js +19 -0
- package/src/__tests__/.content-collections/generated-config.002/allPosts.json +13 -0
- package/src/__tests__/.content-collections/generated-config.002/index.d.ts +7 -0
- package/src/__tests__/.content-collections/generated-config.002/index.js +5 -0
- package/src/__tests__/.content-collections/generated-config.004/allAuthors.json +13 -0
- package/src/__tests__/.content-collections/generated-config.004/allPosts.json +13 -0
- package/src/__tests__/.content-collections/generated-config.004/index.d.ts +10 -0
- package/src/__tests__/.content-collections/generated-config.004/index.js +6 -0
- package/src/__tests__/collections/posts.ts +15 -0
- package/src/__tests__/config.001.ts +19 -0
- package/src/__tests__/config.002.ts +14 -0
- package/src/__tests__/config.003.ts +6 -0
- package/src/__tests__/config.004.ts +47 -0
- package/src/__tests__/invalid +1 -0
- package/src/__tests__/sources/authors/trillian.md +8 -0
- package/src/__tests__/sources/posts/first.md +6 -0
- package/src/__tests__/sources/test/001.md +5 -0
- package/src/__tests__/sources/test/002.md +5 -0
- package/src/__tests__/sources/test/broken-frontmatter +6 -0
- package/src/builder.test.ts +180 -0
- package/src/builder.ts +113 -0
- package/src/collector.test.ts +157 -0
- package/src/collector.ts +114 -0
- package/src/config.ts +106 -0
- package/src/configurationReader.test.ts +104 -0
- package/src/configurationReader.ts +108 -0
- package/src/events.test.ts +84 -0
- package/src/events.ts +75 -0
- package/src/index.ts +7 -0
- package/src/synchronizer.test.ts +192 -0
- package/src/synchronizer.ts +103 -0
- package/src/transformer.test.ts +431 -0
- package/src/transformer.ts +174 -0
- package/src/types.test.ts +137 -0
- package/src/types.ts +33 -0
- package/src/utils.test.ts +48 -0
- package/src/utils.ts +11 -0
- package/src/watcher.test.ts +200 -0
- package/src/watcher.ts +56 -0
- package/src/writer.test.ts +135 -0
- package/src/writer.ts +113 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +24 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
import camelcase from "camelcase";
|
|
6
|
+
import pluralize from "pluralize";
|
|
7
|
+
function generateTypeName(name) {
|
|
8
|
+
const singularName = pluralize.singular(name);
|
|
9
|
+
return camelcase(singularName, { pascalCase: true });
|
|
10
|
+
}
|
|
11
|
+
function isDefined(value) {
|
|
12
|
+
return value !== void 0 && value !== null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
function defineCollection(collection) {
|
|
17
|
+
let typeName = collection.typeName;
|
|
18
|
+
if (!typeName) {
|
|
19
|
+
typeName = generateTypeName(collection.name);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
...collection,
|
|
23
|
+
typeName,
|
|
24
|
+
schema: collection.schema(z)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function defineConfig(config) {
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/configurationReader.ts
|
|
32
|
+
import * as esbuild from "esbuild";
|
|
33
|
+
import fs from "fs/promises";
|
|
34
|
+
import path from "path";
|
|
35
|
+
|
|
36
|
+
// package.json
|
|
37
|
+
var package_default = {
|
|
38
|
+
name: "@content-collections/core",
|
|
39
|
+
version: "0.1.0",
|
|
40
|
+
type: "module",
|
|
41
|
+
main: "dist/index.cjs",
|
|
42
|
+
types: "./dist/index.d.ts",
|
|
43
|
+
exports: {
|
|
44
|
+
"./package.json": "./package.json",
|
|
45
|
+
".": {
|
|
46
|
+
import: "./dist/index.js",
|
|
47
|
+
types: "./dist/index.d.ts"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
scripts: {
|
|
51
|
+
build: "tsup src/index.ts --format esm --dts -d dist",
|
|
52
|
+
typecheck: "tsc",
|
|
53
|
+
test: "vitest --run --coverage"
|
|
54
|
+
},
|
|
55
|
+
devDependencies: {
|
|
56
|
+
"@types/micromatch": "^4.0.6",
|
|
57
|
+
"@types/node": "^20.9.0",
|
|
58
|
+
"@types/pluralize": "^0.0.33",
|
|
59
|
+
"@vitest/coverage-v8": "^1.1.0",
|
|
60
|
+
tsup: "^7.2.0",
|
|
61
|
+
tsx: "^4.1.1",
|
|
62
|
+
typescript: "^5.3.3",
|
|
63
|
+
vitest: "^1.1.0"
|
|
64
|
+
},
|
|
65
|
+
dependencies: {
|
|
66
|
+
"@parcel/watcher": "^2.3.0",
|
|
67
|
+
camelcase: "^8.0.0",
|
|
68
|
+
esbuild: "^0.19.5",
|
|
69
|
+
"fast-glob": "^3.3.2",
|
|
70
|
+
"gray-matter": "^4.0.3",
|
|
71
|
+
micromatch: "^4.0.5",
|
|
72
|
+
pluralize: "^8.0.0",
|
|
73
|
+
zod: "^3.22.4"
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/configurationReader.ts
|
|
78
|
+
import { existsSync } from "fs";
|
|
79
|
+
var ConfigurationError = class extends Error {
|
|
80
|
+
type;
|
|
81
|
+
constructor(type, message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.type = type;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var importPathPlugin = {
|
|
87
|
+
name: "import-path",
|
|
88
|
+
setup(build2) {
|
|
89
|
+
build2.onResolve({ filter: /^\@content-collections\/core$/ }, () => {
|
|
90
|
+
return { path: path.join(__dirname, "index.ts"), external: true };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var defaultConfigName = "content-collection-config.mjs";
|
|
95
|
+
function resolveCacheDir(config, options) {
|
|
96
|
+
if (options.cacheDir) {
|
|
97
|
+
return options.cacheDir;
|
|
98
|
+
}
|
|
99
|
+
return path.join(path.dirname(config), ".content-collections", "cache");
|
|
100
|
+
}
|
|
101
|
+
async function compile(configurationPath, outfile) {
|
|
102
|
+
const plugins = [];
|
|
103
|
+
if (process.env.NODE_ENV === "test") {
|
|
104
|
+
plugins.push(importPathPlugin);
|
|
105
|
+
}
|
|
106
|
+
await esbuild.build({
|
|
107
|
+
entryPoints: [configurationPath],
|
|
108
|
+
external: [...Object.keys(package_default.dependencies), "@content-collections/*"],
|
|
109
|
+
bundle: true,
|
|
110
|
+
platform: "node",
|
|
111
|
+
format: "esm",
|
|
112
|
+
plugins,
|
|
113
|
+
outfile
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function createConfigurationReader() {
|
|
117
|
+
return async (configurationPath, options = {
|
|
118
|
+
configName: defaultConfigName
|
|
119
|
+
}) => {
|
|
120
|
+
if (!existsSync(configurationPath)) {
|
|
121
|
+
throw new ConfigurationError(
|
|
122
|
+
"Read",
|
|
123
|
+
`configuration file ${configurationPath} does not exist`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const cacheDir = resolveCacheDir(configurationPath, options);
|
|
127
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
128
|
+
const outfile = path.join(cacheDir, options.configName);
|
|
129
|
+
try {
|
|
130
|
+
await compile(configurationPath, outfile);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new ConfigurationError(
|
|
133
|
+
"Compile",
|
|
134
|
+
`configuration file ${configurationPath} is invalid: ${error}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const module = await import(`file://${path.resolve(outfile)}?x=${Date.now()}`);
|
|
138
|
+
return {
|
|
139
|
+
...module.default,
|
|
140
|
+
path: configurationPath,
|
|
141
|
+
generateTypes: true
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/collector.ts
|
|
147
|
+
import matter from "gray-matter";
|
|
148
|
+
import fg from "fast-glob";
|
|
149
|
+
import { readFile } from "fs/promises";
|
|
150
|
+
import path2 from "path";
|
|
151
|
+
var CollectError = class extends Error {
|
|
152
|
+
type;
|
|
153
|
+
constructor(type, message) {
|
|
154
|
+
super(message);
|
|
155
|
+
this.type = type;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function createCollector(emitter, baseDirectory = ".") {
|
|
159
|
+
async function read(filePath) {
|
|
160
|
+
try {
|
|
161
|
+
return await readFile(filePath, "utf-8");
|
|
162
|
+
} catch (error) {
|
|
163
|
+
emitter.emit("collector:read-error", {
|
|
164
|
+
filePath,
|
|
165
|
+
error: new CollectError("Read", String(error))
|
|
166
|
+
});
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function parse(file) {
|
|
171
|
+
const { data, content } = matter(file);
|
|
172
|
+
return {
|
|
173
|
+
...data,
|
|
174
|
+
content
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function collectFile(directory, filePath) {
|
|
178
|
+
const file = await read(path2.join(directory, filePath));
|
|
179
|
+
if (!file) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const data = parse(file);
|
|
184
|
+
return {
|
|
185
|
+
data,
|
|
186
|
+
path: filePath
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
emitter.emit("collector:parse-error", {
|
|
190
|
+
filePath: path2.join(directory, filePath),
|
|
191
|
+
error: new CollectError("Parse", String(error))
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function resolveCollection(collection) {
|
|
197
|
+
const collectionDirectory = path2.join(baseDirectory, collection.directory);
|
|
198
|
+
const filePaths = await fg(collection.include, {
|
|
199
|
+
cwd: collectionDirectory,
|
|
200
|
+
onlyFiles: true,
|
|
201
|
+
absolute: false
|
|
202
|
+
});
|
|
203
|
+
const promises = filePaths.map(
|
|
204
|
+
(filePath) => collectFile(collectionDirectory, filePath)
|
|
205
|
+
);
|
|
206
|
+
const files = await Promise.all(promises);
|
|
207
|
+
return {
|
|
208
|
+
...collection,
|
|
209
|
+
files: files.filter(isDefined)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function collect(unresolvedCollections) {
|
|
213
|
+
const promises = unresolvedCollections.map(
|
|
214
|
+
(collection) => resolveCollection(collection)
|
|
215
|
+
);
|
|
216
|
+
return await Promise.all(promises);
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
collect,
|
|
220
|
+
collectFile
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/writer.ts
|
|
225
|
+
import fs2 from "fs/promises";
|
|
226
|
+
import path3 from "path";
|
|
227
|
+
import pluralize2 from "pluralize";
|
|
228
|
+
function createArrayConstName(name) {
|
|
229
|
+
let suffix = name.charAt(0).toUpperCase() + name.slice(1);
|
|
230
|
+
return "all" + pluralize2(suffix);
|
|
231
|
+
}
|
|
232
|
+
async function createDataFile(directory, collection) {
|
|
233
|
+
const dataPath = path3.join(
|
|
234
|
+
directory,
|
|
235
|
+
`${createArrayConstName(collection.name)}.json`
|
|
236
|
+
);
|
|
237
|
+
await fs2.writeFile(
|
|
238
|
+
dataPath,
|
|
239
|
+
JSON.stringify(
|
|
240
|
+
collection.documents.map((doc) => doc.document),
|
|
241
|
+
null,
|
|
242
|
+
2
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
function createDataFiles(directory, collections) {
|
|
247
|
+
return Promise.all(
|
|
248
|
+
collections.map((collection) => createDataFile(directory, collection))
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
async function createJavaScriptFile(directory, configuration) {
|
|
252
|
+
const collections = configuration.collections.map(
|
|
253
|
+
({ name }) => createArrayConstName(name)
|
|
254
|
+
);
|
|
255
|
+
let content = `// generated by content-collections at ${/* @__PURE__ */ new Date()}
|
|
256
|
+
|
|
257
|
+
`;
|
|
258
|
+
for (const name of collections) {
|
|
259
|
+
content += `import ${name} from "./${name}.json";
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
content += "\n";
|
|
263
|
+
content += "export { " + collections.join(", ") + " };\n";
|
|
264
|
+
await fs2.writeFile(path3.join(directory, "index.js"), content, "utf-8");
|
|
265
|
+
}
|
|
266
|
+
async function createTypeDefinitionFile(directory, configuration) {
|
|
267
|
+
if (!configuration.generateTypes) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const importPath = path3.relative(directory, configuration.path);
|
|
271
|
+
let content = `import configuration from "${importPath}";
|
|
272
|
+
import { GetTypeByName } from "@content-collections/core";
|
|
273
|
+
`;
|
|
274
|
+
const collections = configuration.collections;
|
|
275
|
+
for (const collection of collections) {
|
|
276
|
+
content += `
|
|
277
|
+
`;
|
|
278
|
+
content += `export type ${collection.typeName} = GetTypeByName<typeof configuration, "${collection.name}">;
|
|
279
|
+
`;
|
|
280
|
+
content += `export declare const ${createArrayConstName(
|
|
281
|
+
collection.name
|
|
282
|
+
)}: Array<${collection.typeName}>;
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
content += "\n";
|
|
286
|
+
content += "export {};\n";
|
|
287
|
+
await fs2.writeFile(path3.join(directory, "index.d.ts"), content, "utf-8");
|
|
288
|
+
}
|
|
289
|
+
async function createWriter(directory) {
|
|
290
|
+
await fs2.mkdir(directory, { recursive: true });
|
|
291
|
+
return {
|
|
292
|
+
createJavaScriptFile: (configuration) => createJavaScriptFile(directory, configuration),
|
|
293
|
+
createTypeDefinitionFile: (configuration) => createTypeDefinitionFile(directory, configuration),
|
|
294
|
+
createDataFiles: (collections) => createDataFiles(directory, collections)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/transformer.ts
|
|
299
|
+
import { basename, dirname, extname } from "path";
|
|
300
|
+
import { z as z2 } from "zod";
|
|
301
|
+
var TransformError = class extends Error {
|
|
302
|
+
type;
|
|
303
|
+
constructor(type, message) {
|
|
304
|
+
super(message);
|
|
305
|
+
this.type = type;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
function createPath(path6, ext) {
|
|
309
|
+
let p = path6.slice(0, -ext.length);
|
|
310
|
+
if (p.endsWith("/index")) {
|
|
311
|
+
p = p.slice(0, -6);
|
|
312
|
+
}
|
|
313
|
+
return p;
|
|
314
|
+
}
|
|
315
|
+
function createTransformer(emitter) {
|
|
316
|
+
function createSchema(schema) {
|
|
317
|
+
return z2.object({
|
|
318
|
+
content: z2.string(),
|
|
319
|
+
...schema
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function parseFile(collection, file) {
|
|
323
|
+
const { data, path: path6 } = file;
|
|
324
|
+
const schema = createSchema(collection.schema);
|
|
325
|
+
let parsedData = await schema.safeParseAsync(data);
|
|
326
|
+
if (!parsedData.success) {
|
|
327
|
+
emitter.emit("transformer:validation-error", {
|
|
328
|
+
collection,
|
|
329
|
+
file,
|
|
330
|
+
error: new TransformError("Validation", parsedData.error.message)
|
|
331
|
+
});
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const ext = extname(path6);
|
|
335
|
+
let extension = ext;
|
|
336
|
+
if (extension.startsWith(".")) {
|
|
337
|
+
extension = extension.slice(1);
|
|
338
|
+
}
|
|
339
|
+
const document = {
|
|
340
|
+
...parsedData.data,
|
|
341
|
+
_meta: {
|
|
342
|
+
filePath: path6,
|
|
343
|
+
fileName: basename(path6),
|
|
344
|
+
directory: dirname(path6),
|
|
345
|
+
extension,
|
|
346
|
+
path: createPath(path6, ext)
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
document
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async function parseCollection(collection) {
|
|
354
|
+
const promises = collection.files.map(
|
|
355
|
+
(file) => parseFile(collection, file)
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
...collection,
|
|
359
|
+
documents: (await Promise.all(promises)).filter(isDefined)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function createContext(collections) {
|
|
363
|
+
return {
|
|
364
|
+
documents: (collection) => {
|
|
365
|
+
const resolved = collections.find((c) => c.name === collection.name);
|
|
366
|
+
if (!resolved) {
|
|
367
|
+
throw new TransformError(
|
|
368
|
+
"Configuration",
|
|
369
|
+
`Collection ${collection.name} not found, do you have registered it in your configuration?`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return resolved.documents.map((doc) => doc.document);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function transformCollection(collections, collection) {
|
|
377
|
+
if (collection.transform) {
|
|
378
|
+
const docs = [];
|
|
379
|
+
const context = createContext(collections);
|
|
380
|
+
for (const doc of collection.documents) {
|
|
381
|
+
try {
|
|
382
|
+
docs.push({
|
|
383
|
+
...doc,
|
|
384
|
+
document: await collection.transform(context, doc.document)
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error instanceof TransformError) {
|
|
388
|
+
emitter.emit("transformer:error", {
|
|
389
|
+
collection,
|
|
390
|
+
error
|
|
391
|
+
});
|
|
392
|
+
} else {
|
|
393
|
+
emitter.emit("transformer:error", {
|
|
394
|
+
collection,
|
|
395
|
+
error: new TransformError("Transform", String(error))
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return docs;
|
|
401
|
+
}
|
|
402
|
+
return collection.documents;
|
|
403
|
+
}
|
|
404
|
+
return async (untransformedCollections) => {
|
|
405
|
+
const promises = untransformedCollections.map(
|
|
406
|
+
(collection) => parseCollection(collection)
|
|
407
|
+
);
|
|
408
|
+
const collections = await Promise.all(promises);
|
|
409
|
+
for (const collection of collections) {
|
|
410
|
+
collection.documents = await transformCollection(collections, collection);
|
|
411
|
+
}
|
|
412
|
+
return collections;
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/synchronizer.ts
|
|
417
|
+
import micromatch from "micromatch";
|
|
418
|
+
import path4 from "path";
|
|
419
|
+
function createSynchronizer(readCollectionFile, collections, baseDirectory = ".") {
|
|
420
|
+
function findCollection(filePath) {
|
|
421
|
+
const resolvedFilePath = path4.resolve(filePath);
|
|
422
|
+
return collections.find((collection) => {
|
|
423
|
+
return resolvedFilePath.startsWith(
|
|
424
|
+
path4.resolve(baseDirectory, collection.directory)
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function createRelativePath(collectionPath, filePath) {
|
|
429
|
+
const resolvedCollectionPath = path4.resolve(baseDirectory, collectionPath);
|
|
430
|
+
const resolvedFilePath = path4.resolve(filePath);
|
|
431
|
+
let relativePath = resolvedFilePath.slice(resolvedCollectionPath.length);
|
|
432
|
+
if (relativePath.startsWith("/")) {
|
|
433
|
+
relativePath = relativePath.slice(1);
|
|
434
|
+
}
|
|
435
|
+
return relativePath;
|
|
436
|
+
}
|
|
437
|
+
function resolve(filePath) {
|
|
438
|
+
const collection = findCollection(filePath);
|
|
439
|
+
if (!collection) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const relativePath = createRelativePath(collection.directory, filePath);
|
|
443
|
+
if (!micromatch.isMatch(relativePath, collection.include)) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
collection,
|
|
448
|
+
relativePath
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function deleted(filePath) {
|
|
452
|
+
const resolved = resolve(filePath);
|
|
453
|
+
if (!resolved) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
const { collection, relativePath } = resolved;
|
|
457
|
+
const index = collection.files.findIndex(
|
|
458
|
+
(file) => file.path === relativePath
|
|
459
|
+
);
|
|
460
|
+
const deleted2 = collection.files.splice(index, 1);
|
|
461
|
+
return deleted2.length > 0;
|
|
462
|
+
}
|
|
463
|
+
async function changed(filePath) {
|
|
464
|
+
const resolved = resolve(filePath);
|
|
465
|
+
if (!resolved) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
const { collection, relativePath } = resolved;
|
|
469
|
+
const index = collection.files.findIndex(
|
|
470
|
+
(file2) => file2.path === relativePath
|
|
471
|
+
);
|
|
472
|
+
const file = await readCollectionFile(
|
|
473
|
+
path4.join(baseDirectory, collection.directory),
|
|
474
|
+
relativePath
|
|
475
|
+
);
|
|
476
|
+
if (!file) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
if (index === -1) {
|
|
480
|
+
collection.files.push(file);
|
|
481
|
+
} else {
|
|
482
|
+
collection.files[index] = file;
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
deleted,
|
|
488
|
+
changed
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/builder.ts
|
|
493
|
+
import path5 from "path";
|
|
494
|
+
|
|
495
|
+
// src/watcher.ts
|
|
496
|
+
import watcher from "@parcel/watcher";
|
|
497
|
+
async function createWatcher(emitter, paths, sync, build2) {
|
|
498
|
+
const onChange = async (error, events) => {
|
|
499
|
+
if (error) {
|
|
500
|
+
console.error(error);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
let rebuild = false;
|
|
504
|
+
for (const event of events) {
|
|
505
|
+
if (await sync(event.type, event.path)) {
|
|
506
|
+
emitter.emit("watcher:file-changed", {
|
|
507
|
+
filePath: event.path,
|
|
508
|
+
modification: event.type
|
|
509
|
+
});
|
|
510
|
+
rebuild = true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (rebuild) {
|
|
514
|
+
await build2();
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
const subscriptions = await Promise.all(
|
|
518
|
+
paths.map((path6) => watcher.subscribe(path6, onChange))
|
|
519
|
+
);
|
|
520
|
+
return {
|
|
521
|
+
unsubscribe: async () => {
|
|
522
|
+
await Promise.all(
|
|
523
|
+
subscriptions.map((subscription) => subscription.unsubscribe())
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/events.ts
|
|
531
|
+
import { EventEmitter } from "events";
|
|
532
|
+
function isEventWithError(event) {
|
|
533
|
+
return typeof event === "object" && event !== null && "error" in event;
|
|
534
|
+
}
|
|
535
|
+
function createEmitter() {
|
|
536
|
+
const emitter = new EventEmitter();
|
|
537
|
+
function on(key, listener) {
|
|
538
|
+
emitter.on(key, listener);
|
|
539
|
+
}
|
|
540
|
+
function emit(key, event) {
|
|
541
|
+
emitter.emit(key, event);
|
|
542
|
+
if (isEventWithError(event)) {
|
|
543
|
+
emitter.emit("_error", {
|
|
544
|
+
...event,
|
|
545
|
+
_event: key
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
emitter.emit("_all", {
|
|
549
|
+
...event,
|
|
550
|
+
_event: key
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
on,
|
|
555
|
+
emit
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/builder.ts
|
|
560
|
+
function resolveOutputDir(baseDirectory, options) {
|
|
561
|
+
if (options.outputDir) {
|
|
562
|
+
return options.outputDir;
|
|
563
|
+
}
|
|
564
|
+
return path5.join(baseDirectory, ".content-collections", "generated");
|
|
565
|
+
}
|
|
566
|
+
async function createBuilder(configurationPath, options = {
|
|
567
|
+
configName: defaultConfigName
|
|
568
|
+
}) {
|
|
569
|
+
const emitter = createEmitter();
|
|
570
|
+
const readConfiguration = createConfigurationReader();
|
|
571
|
+
const configuration = await readConfiguration(configurationPath, options);
|
|
572
|
+
const baseDirectory = path5.dirname(configurationPath);
|
|
573
|
+
const directory = resolveOutputDir(baseDirectory, options);
|
|
574
|
+
const collector = createCollector(emitter, baseDirectory);
|
|
575
|
+
const writer = await createWriter(directory);
|
|
576
|
+
const [resolved] = await Promise.all([
|
|
577
|
+
collector.collect(configuration.collections),
|
|
578
|
+
writer.createJavaScriptFile(configuration),
|
|
579
|
+
writer.createTypeDefinitionFile(configuration)
|
|
580
|
+
]);
|
|
581
|
+
const synchronizer = createSynchronizer(
|
|
582
|
+
collector.collectFile,
|
|
583
|
+
resolved,
|
|
584
|
+
baseDirectory
|
|
585
|
+
);
|
|
586
|
+
const transform = createTransformer(emitter);
|
|
587
|
+
async function sync(modification, filePath) {
|
|
588
|
+
if (modification === "delete") {
|
|
589
|
+
return synchronizer.deleted(filePath);
|
|
590
|
+
}
|
|
591
|
+
return synchronizer.changed(filePath);
|
|
592
|
+
}
|
|
593
|
+
async function build2() {
|
|
594
|
+
const startedAt = Date.now();
|
|
595
|
+
emitter.emit("builder:start", {
|
|
596
|
+
startedAt
|
|
597
|
+
});
|
|
598
|
+
const collections = await transform(resolved);
|
|
599
|
+
await writer.createDataFiles(collections);
|
|
600
|
+
const pendingOnSuccess = collections.filter((collection) => Boolean(collection.onSuccess)).map(
|
|
601
|
+
(collection) => collection.onSuccess?.(collection.documents.map((doc) => doc.document))
|
|
602
|
+
);
|
|
603
|
+
await Promise.all(pendingOnSuccess.filter(isDefined));
|
|
604
|
+
emitter.emit("builder:end", {
|
|
605
|
+
startedAt,
|
|
606
|
+
endedAt: Date.now()
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async function watch() {
|
|
610
|
+
const paths = resolved.map(
|
|
611
|
+
(collection) => path5.join(baseDirectory, collection.directory)
|
|
612
|
+
);
|
|
613
|
+
const watcher2 = await createWatcher(emitter, paths, sync, build2);
|
|
614
|
+
return watcher2;
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
sync,
|
|
618
|
+
build: build2,
|
|
619
|
+
watch,
|
|
620
|
+
on: emitter.on
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
export {
|
|
624
|
+
CollectError,
|
|
625
|
+
ConfigurationError,
|
|
626
|
+
TransformError,
|
|
627
|
+
createBuilder,
|
|
628
|
+
defineCollection,
|
|
629
|
+
defineConfig
|
|
630
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@content-collections/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/micromatch": "^4.0.6",
|
|
16
|
+
"@types/node": "^20.9.0",
|
|
17
|
+
"@types/pluralize": "^0.0.33",
|
|
18
|
+
"@vitest/coverage-v8": "^1.1.0",
|
|
19
|
+
"tsup": "^7.2.0",
|
|
20
|
+
"tsx": "^4.1.1",
|
|
21
|
+
"typescript": "^5.3.3",
|
|
22
|
+
"vitest": "^1.1.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@parcel/watcher": "^2.3.0",
|
|
26
|
+
"camelcase": "^8.0.0",
|
|
27
|
+
"esbuild": "^0.19.5",
|
|
28
|
+
"fast-glob": "^3.3.2",
|
|
29
|
+
"gray-matter": "^4.0.3",
|
|
30
|
+
"micromatch": "^4.0.5",
|
|
31
|
+
"pluralize": "^8.0.0",
|
|
32
|
+
"zod": "^3.22.4"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts -d dist",
|
|
36
|
+
"typecheck": "tsc",
|
|
37
|
+
"test": "vitest --run --coverage"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/__tests__/config.001.ts
|
|
2
|
+
import { defineCollection, defineConfig } from "/Users/sdorra/Projects/sdorra/content-collections/packages/core/src/index.ts";
|
|
3
|
+
var posts = defineCollection({
|
|
4
|
+
name: "posts",
|
|
5
|
+
typeName: "Post",
|
|
6
|
+
schema: (z) => ({
|
|
7
|
+
title: z.string().min(5),
|
|
8
|
+
description: z.string().min(10),
|
|
9
|
+
date: z.union([z.string().regex(/^\d{4}-\d{2}-\d{2}$/), z.date()]).transform((val) => new Date(val))
|
|
10
|
+
}),
|
|
11
|
+
directory: "sources/posts",
|
|
12
|
+
include: "**/*.md(x)?"
|
|
13
|
+
});
|
|
14
|
+
var config_001_default = defineConfig({
|
|
15
|
+
collections: [posts]
|
|
16
|
+
});
|
|
17
|
+
export {
|
|
18
|
+
config_001_default as default
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/__tests__/config.002.ts
|
|
2
|
+
import { defineCollection, defineConfig } from "/Users/sdorra/Projects/sdorra/content-collections/packages/core/src/index.ts";
|
|
3
|
+
var posts = defineCollection({
|
|
4
|
+
name: "posts",
|
|
5
|
+
schema: (z) => ({
|
|
6
|
+
title: z.string()
|
|
7
|
+
}),
|
|
8
|
+
directory: "sources/posts",
|
|
9
|
+
include: "**/*.md(x)?"
|
|
10
|
+
});
|
|
11
|
+
var config_002_default = defineConfig({
|
|
12
|
+
collections: [posts]
|
|
13
|
+
});
|
|
14
|
+
export {
|
|
15
|
+
config_002_default as default
|
|
16
|
+
};
|