@jk2908/mdsrc 0.4.1 → 0.6.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/CHANGELOG.md +19 -0
- package/README.md +54 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +47 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +524 -193
- package/dist/index.js.map +8 -6
- package/dist/types.d.ts +41 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -0
- package/dist/utils.js.map +1 -1
- package/dist/validate.d.ts +18 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +366 -0
- package/dist/validate.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,12 +21,16 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
21
21
|
import { realpathSync } from "node:fs";
|
|
22
22
|
import fs from "node:fs/promises";
|
|
23
23
|
import path from "node:path";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
markdownToHtml,
|
|
26
|
+
mdxToJs
|
|
27
|
+
} from "satteri";
|
|
25
28
|
|
|
26
29
|
// src/config.ts
|
|
27
30
|
var NAME = "mdsrc";
|
|
28
31
|
var PKG_NAME = `@jk2908/${NAME}`;
|
|
29
32
|
var GENERATED_DIR = `.${NAME}`;
|
|
33
|
+
var AUTOGEN_MSG = `// auto-generated by ${NAME}`;
|
|
30
34
|
|
|
31
35
|
// src/logger.ts
|
|
32
36
|
var LEVELS = {
|
|
@@ -121,6 +125,9 @@ function capitalise(str) {
|
|
|
121
125
|
function pluralise(str, count) {
|
|
122
126
|
return count === 1 ? str : str.endsWith("s") ? str : `${str}s`;
|
|
123
127
|
}
|
|
128
|
+
function singularise(str, suffix = "s") {
|
|
129
|
+
return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
|
|
130
|
+
}
|
|
124
131
|
function debounce(fn, wait) {
|
|
125
132
|
let timeoutId = null;
|
|
126
133
|
return (...args) => {
|
|
@@ -132,11 +139,6 @@ function debounce(fn, wait) {
|
|
|
132
139
|
}, wait);
|
|
133
140
|
};
|
|
134
141
|
}
|
|
135
|
-
function dedent(str) {
|
|
136
|
-
return str.replace(/^\n/, "").replace(/\s+$/, "").split(`
|
|
137
|
-
`).filter(Boolean).map((line) => line.replace(/^\s+/, "")).join(`
|
|
138
|
-
`);
|
|
139
|
-
}
|
|
140
142
|
function isRecord(value) {
|
|
141
143
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
142
144
|
}
|
|
@@ -150,129 +152,155 @@ function deep(obj, path, value) {
|
|
|
150
152
|
}
|
|
151
153
|
cur[parts.at(-1)] = value;
|
|
152
154
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
var fileCache = new Map;
|
|
156
|
-
var DEFAULT_COMPILE_OPTIONS = {
|
|
157
|
-
features: {
|
|
158
|
-
frontmatter: true
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
async function parse(frontmatter) {
|
|
162
|
-
if (!frontmatter)
|
|
163
|
-
return {};
|
|
164
|
-
const { kind, value } = frontmatter;
|
|
165
|
-
switch (kind) {
|
|
166
|
-
case "yaml": {
|
|
167
|
-
return (await import("yaml")).parse(value);
|
|
168
|
-
}
|
|
169
|
-
case "toml": {
|
|
170
|
-
return (await import("smol-toml")).parse(value);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
async function create(dir, buildContext) {
|
|
175
|
-
const { logger: logger2, compileOptions = {} } = buildContext;
|
|
176
|
-
const { features, ...restCompileOptions } = compileOptions;
|
|
177
|
-
try {
|
|
178
|
-
const files = (await fs.readdir(dir)).filter((file) => path.extname(file) === ".md");
|
|
179
|
-
const filePaths = files.map((file) => path.join(dir, file));
|
|
180
|
-
if (!files.length) {
|
|
181
|
-
console.warn(`mdsrc: ${dir} is empty`);
|
|
182
|
-
return [];
|
|
183
|
-
}
|
|
184
|
-
return Promise.all(filePaths.map(async (filePath) => {
|
|
185
|
-
const file = path.basename(filePath);
|
|
186
|
-
const { html, frontmatter: rawFrontmatter } = markdownToHtml(await fs.readFile(filePath, "utf-8"), {
|
|
187
|
-
features: {
|
|
188
|
-
...DEFAULT_COMPILE_OPTIONS.features,
|
|
189
|
-
...features
|
|
190
|
-
},
|
|
191
|
-
...restCompileOptions
|
|
192
|
-
});
|
|
193
|
-
const frontmatter = await parse(rawFrontmatter);
|
|
194
|
-
return {
|
|
195
|
-
...frontmatter,
|
|
196
|
-
__mdsrc: {
|
|
197
|
-
slug: path.basename(file, ".md").toLowerCase().replace(/\s+/g, "-"),
|
|
198
|
-
filename: file
|
|
199
|
-
},
|
|
200
|
-
body: html.trim()
|
|
201
|
-
};
|
|
202
|
-
}));
|
|
203
|
-
} catch (err) {
|
|
204
|
-
logger2.error("[create]: failed to create entries", err);
|
|
205
|
-
return [];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
async function maybeWrite(filePath, content) {
|
|
209
|
-
const cached = fileCache.get(filePath);
|
|
210
|
-
if (cached === content) {
|
|
211
|
-
try {
|
|
212
|
-
await fs.access(filePath);
|
|
213
|
-
return false;
|
|
214
|
-
} catch (err) {
|
|
215
|
-
if (!(err instanceof Error) || !("code" in err) || err.code !== "ENOENT") {
|
|
216
|
-
throw err;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (cached === undefined) {
|
|
221
|
-
try {
|
|
222
|
-
const current = await fs.readFile(filePath, "utf-8");
|
|
223
|
-
fileCache.set(filePath, current);
|
|
224
|
-
if (current === content) {
|
|
225
|
-
fileCache.set(filePath, content);
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
if (!(err instanceof Error) || !("code" in err) || err.code !== "ENOENT") {
|
|
230
|
-
throw err;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
await fs.writeFile(filePath, content);
|
|
235
|
-
fileCache.set(filePath, content);
|
|
236
|
-
return true;
|
|
155
|
+
function slugify(str) {
|
|
156
|
+
return str.toLowerCase().replace(/\s/g, "-");
|
|
237
157
|
}
|
|
158
|
+
|
|
159
|
+
// src/types.ts
|
|
160
|
+
var PRIMITIVE_NAMES = ["string", "number", "boolean", "date", "array"];
|
|
161
|
+
var MODIFIER_NAMES = ["max", "min"];
|
|
162
|
+
|
|
163
|
+
// src/validate.ts
|
|
238
164
|
function validate(input, schema) {
|
|
239
165
|
const validated = {};
|
|
240
166
|
const issues = [];
|
|
241
167
|
if (typeof input !== "object" || input === null) {
|
|
242
|
-
issues.push({ message: "Input must be an object" });
|
|
168
|
+
issues.push({ message: "Input must be an object", code: "INVALID_INPUT" });
|
|
243
169
|
return { issues };
|
|
244
170
|
}
|
|
171
|
+
const schemaKeys = new Set(Object.keys(schema).map((k) => parseKey(k).key));
|
|
172
|
+
for (const key in input) {
|
|
173
|
+
if (!schemaKeys.has(key)) {
|
|
174
|
+
issues.push({ message: `Unknown key: ${key}`, code: "UNKNOWN_KEY" });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (issues.length)
|
|
178
|
+
return { issues };
|
|
245
179
|
function walk(key, schemaValue, data) {
|
|
246
180
|
const { optional, key: parsedKey } = parseKey(key);
|
|
247
181
|
if (data === undefined) {
|
|
248
182
|
if (!optional) {
|
|
249
|
-
issues.push({
|
|
183
|
+
issues.push({
|
|
184
|
+
message: `Missing required key: ${parsedKey}`,
|
|
185
|
+
code: "MISSING_REQUIRED"
|
|
186
|
+
});
|
|
250
187
|
}
|
|
251
188
|
return;
|
|
252
189
|
}
|
|
253
190
|
if (typeof schemaValue === "string") {
|
|
254
|
-
|
|
255
|
-
|
|
191
|
+
const { types, modifiers } = parseSchemaValue(schemaValue);
|
|
192
|
+
for (const type of types) {
|
|
193
|
+
if (type === "string") {
|
|
256
194
|
if (typeof data !== "string") {
|
|
257
|
-
|
|
258
|
-
|
|
195
|
+
if (types.length === 1) {
|
|
196
|
+
issues.push({
|
|
197
|
+
message: `Key ${parsedKey} must be a string`,
|
|
198
|
+
code: "INVALID_TYPE"
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (modifiers.min) {
|
|
205
|
+
const min = Number(modifiers.min);
|
|
206
|
+
if (Number.isNaN(min)) {
|
|
207
|
+
issues.push({
|
|
208
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to type (number)`,
|
|
209
|
+
code: "BAD_MODIFIER"
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (data.length < min) {
|
|
214
|
+
if (types.length === 1) {
|
|
215
|
+
issues.push({
|
|
216
|
+
message: `Key ${parsedKey} must be greater than or equal to minimum length (${min})`,
|
|
217
|
+
code: "INVALID_LENGTH"
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (modifiers.max) {
|
|
225
|
+
const max = Number(modifiers.max);
|
|
226
|
+
if (Number.isNaN(max)) {
|
|
227
|
+
issues.push({
|
|
228
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to type (number)`,
|
|
229
|
+
code: "BAD_MODIFIER"
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (data.length > max) {
|
|
234
|
+
if (types.length === 1) {
|
|
235
|
+
issues.push({
|
|
236
|
+
message: `Key ${parsedKey} must be less than or equal to maximum length (${max})`,
|
|
237
|
+
code: "INVALID_LENGTH"
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
259
243
|
}
|
|
260
244
|
deep(validated, parsedKey, data);
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
case "number": {
|
|
245
|
+
return;
|
|
246
|
+
} else if (type === "number") {
|
|
264
247
|
let num = data;
|
|
265
248
|
if (typeof data === "string" && !Number.isNaN(Number(data))) {
|
|
266
249
|
num = Number(data);
|
|
267
250
|
}
|
|
268
251
|
if (typeof num !== "number" || Number.isNaN(num)) {
|
|
269
|
-
|
|
270
|
-
|
|
252
|
+
if (types.length === 1) {
|
|
253
|
+
issues.push({
|
|
254
|
+
message: `Key ${parsedKey} must be a number`,
|
|
255
|
+
code: "INVALID_TYPE"
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (modifiers.min) {
|
|
262
|
+
const min = Number(modifiers.min);
|
|
263
|
+
if (Number.isNaN(min)) {
|
|
264
|
+
issues.push({
|
|
265
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to type (number)`,
|
|
266
|
+
code: "BAD_MODIFIER"
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (num < min) {
|
|
271
|
+
if (types.length === 1) {
|
|
272
|
+
issues.push({
|
|
273
|
+
message: `Key ${parsedKey} must be greater than or equal to minimum size (${min})`,
|
|
274
|
+
code: "INVALID_SIZE"
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (modifiers.max) {
|
|
282
|
+
const max = Number(modifiers.max);
|
|
283
|
+
if (Number.isNaN(max)) {
|
|
284
|
+
issues.push({
|
|
285
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to type (number)`,
|
|
286
|
+
code: "BAD_MODIFIER"
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (num > max) {
|
|
291
|
+
if (types.length === 1) {
|
|
292
|
+
issues.push({
|
|
293
|
+
message: `Key ${parsedKey} must be less than or equal to maximum size (${max})`,
|
|
294
|
+
code: "INVALID_SIZE"
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
271
300
|
}
|
|
272
301
|
deep(validated, parsedKey, num);
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
case "boolean": {
|
|
302
|
+
return;
|
|
303
|
+
} else if (type === "boolean") {
|
|
276
304
|
let bool = data;
|
|
277
305
|
if (typeof data === "string") {
|
|
278
306
|
if (data.toLowerCase() === "true") {
|
|
@@ -282,29 +310,151 @@ function validate(input, schema) {
|
|
|
282
310
|
}
|
|
283
311
|
}
|
|
284
312
|
if (typeof bool !== "boolean") {
|
|
285
|
-
|
|
286
|
-
|
|
313
|
+
if (types.length === 1) {
|
|
314
|
+
issues.push({
|
|
315
|
+
message: `Key ${parsedKey} must be a boolean`,
|
|
316
|
+
code: "INVALID_TYPE"
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
287
321
|
}
|
|
288
322
|
deep(validated, parsedKey, bool);
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
return;
|
|
324
|
+
} else if (type === "date") {
|
|
325
|
+
let date;
|
|
326
|
+
if (data instanceof Date) {
|
|
327
|
+
date = data;
|
|
328
|
+
} else if (typeof data === "string" || typeof data === "number") {
|
|
329
|
+
date = new Date(data);
|
|
330
|
+
} else {
|
|
331
|
+
if (types.length === 1) {
|
|
332
|
+
issues.push({
|
|
333
|
+
message: `Key ${parsedKey} must be a Date, string or number`,
|
|
334
|
+
code: "INVALID_TYPE"
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const dt = date.getTime();
|
|
341
|
+
if (Number.isNaN(dt)) {
|
|
342
|
+
if (types.length === 1) {
|
|
343
|
+
issues.push({
|
|
344
|
+
message: `Key ${parsedKey} must be a valid date`,
|
|
345
|
+
code: "INVALID_DATE"
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (modifiers.min) {
|
|
352
|
+
const min = new Date(Number(modifiers.min));
|
|
353
|
+
if (Number.isNaN(min.getTime())) {
|
|
354
|
+
issues.push({
|
|
355
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to instance (Date)`,
|
|
356
|
+
code: "BAD_MODIFIER"
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (dt < min.getTime()) {
|
|
361
|
+
if (types.length === 1) {
|
|
362
|
+
issues.push({
|
|
363
|
+
message: `Key ${parsedKey} must be greater than or equal to minimum date (${min.toISOString()})`,
|
|
364
|
+
code: "INVALID_DATE"
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
295
370
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
371
|
+
if (modifiers.max) {
|
|
372
|
+
const max = new Date(Number(modifiers.max));
|
|
373
|
+
if (Number.isNaN(max.getTime())) {
|
|
374
|
+
issues.push({
|
|
375
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to instance (Date)`,
|
|
376
|
+
code: "BAD_MODIFIER"
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (dt > max.getTime()) {
|
|
381
|
+
if (types.length === 1) {
|
|
382
|
+
issues.push({
|
|
383
|
+
message: `Key ${parsedKey} must be less than or equal to maximum date (${max.toISOString()})`,
|
|
384
|
+
code: "INVALID_DATE"
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
300
390
|
}
|
|
301
391
|
deep(validated, parsedKey, date.toISOString());
|
|
302
|
-
|
|
392
|
+
return;
|
|
393
|
+
} else if (type === "array") {
|
|
394
|
+
if (!Array.isArray(data)) {
|
|
395
|
+
if (types.length === 1) {
|
|
396
|
+
issues.push({
|
|
397
|
+
message: `Key ${parsedKey} must be an array`,
|
|
398
|
+
code: "INVALID_TYPE"
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (modifiers.min) {
|
|
405
|
+
const min = Number(modifiers.min);
|
|
406
|
+
if (Number.isNaN(min)) {
|
|
407
|
+
issues.push({
|
|
408
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to type (number)`,
|
|
409
|
+
code: "BAD_MODIFIER"
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (data.length < min) {
|
|
414
|
+
if (types.length === 1) {
|
|
415
|
+
issues.push({
|
|
416
|
+
message: `Key ${parsedKey} must be greater than or equal to minimum array length (${min})`,
|
|
417
|
+
code: "INVALID_LENGTH"
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (modifiers.max) {
|
|
425
|
+
const max = Number(modifiers.max);
|
|
426
|
+
if (Number.isNaN(max)) {
|
|
427
|
+
issues.push({
|
|
428
|
+
message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to type (number)`,
|
|
429
|
+
code: "BAD_MODIFIER"
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (data.length > max) {
|
|
434
|
+
if (types.length === 1) {
|
|
435
|
+
issues.push({
|
|
436
|
+
message: `Key ${parsedKey} must be less than or equal to maximum array length (${max})`,
|
|
437
|
+
code: "INVALID_LENGTH"
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
deep(validated, parsedKey, data);
|
|
445
|
+
return;
|
|
303
446
|
}
|
|
304
447
|
}
|
|
448
|
+
issues.push({
|
|
449
|
+
message: `Key ${parsedKey} must be one of: ${types.join(", ")}`,
|
|
450
|
+
code: "INVALID_TYPE"
|
|
451
|
+
});
|
|
305
452
|
} else {
|
|
306
453
|
if (!isRecord(data)) {
|
|
307
|
-
issues.push({
|
|
454
|
+
issues.push({
|
|
455
|
+
message: `Key ${parsedKey} must be an object`,
|
|
456
|
+
code: "INVALID_TYPE"
|
|
457
|
+
});
|
|
308
458
|
return;
|
|
309
459
|
}
|
|
310
460
|
const obj = data;
|
|
@@ -325,92 +475,293 @@ function parseKey(k) {
|
|
|
325
475
|
key: optional ? k.slice(0, -1) : k
|
|
326
476
|
};
|
|
327
477
|
}
|
|
328
|
-
function
|
|
478
|
+
function parseSchemaValue(value) {
|
|
479
|
+
if (isRecord(value))
|
|
480
|
+
throw new Error("Cannot parse object schema values");
|
|
481
|
+
const parts = value.split("|");
|
|
482
|
+
const types = [];
|
|
483
|
+
const modifiers = {};
|
|
484
|
+
for (const p of parts) {
|
|
485
|
+
if (p.indexOf("=") > -1) {
|
|
486
|
+
const [m, v] = p.split("=");
|
|
487
|
+
if (!isModifierName(m))
|
|
488
|
+
throw new Error(`Unrecognised modifier: ${m}`);
|
|
489
|
+
modifiers[m] = v;
|
|
490
|
+
} else {
|
|
491
|
+
if (!isPrimitive(p))
|
|
492
|
+
throw new Error(`Unrecognised type: ${p}`);
|
|
493
|
+
types.push(p);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { types, modifiers };
|
|
497
|
+
}
|
|
498
|
+
function isModifierName(name) {
|
|
499
|
+
return MODIFIER_NAMES.some((n) => n === name);
|
|
500
|
+
}
|
|
501
|
+
function isPrimitive(name) {
|
|
502
|
+
return PRIMITIVE_NAMES.some((n) => n === name);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/index.ts
|
|
506
|
+
var DEFAULT_COMPILE_OPTIONS = {
|
|
507
|
+
features: {
|
|
508
|
+
frontmatter: true
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
var ACCEPTED_EXTENSIONS = ["md", "mdx"];
|
|
512
|
+
async function parse(frontmatter) {
|
|
513
|
+
if (!frontmatter)
|
|
514
|
+
return {};
|
|
515
|
+
const { kind, value } = frontmatter;
|
|
516
|
+
switch (kind) {
|
|
517
|
+
case "yaml": {
|
|
518
|
+
return (await import("yaml")).parse(value);
|
|
519
|
+
}
|
|
520
|
+
case "toml": {
|
|
521
|
+
return (await import("smol-toml")).parse(value);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function create(dir, buildContext) {
|
|
526
|
+
const { logger: logger2, compileOptions = {} } = buildContext;
|
|
527
|
+
const { features, ...restCompileOptions } = compileOptions;
|
|
528
|
+
try {
|
|
529
|
+
const files = (await fs.readdir(dir)).filter((file) => ACCEPTED_EXTENSIONS.some((e) => `.${e}` === path.extname(file)));
|
|
530
|
+
const filePaths = files.map((file) => path.join(dir, file));
|
|
531
|
+
if (!files.length) {
|
|
532
|
+
logger2.warn(`mdsrc: ${dir} is empty`);
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
const parserArgs = {
|
|
536
|
+
features: {
|
|
537
|
+
...DEFAULT_COMPILE_OPTIONS.features,
|
|
538
|
+
...features
|
|
539
|
+
},
|
|
540
|
+
...restCompileOptions
|
|
541
|
+
};
|
|
542
|
+
return Promise.all(filePaths.map(async (filePath) => {
|
|
543
|
+
const file = path.basename(filePath);
|
|
544
|
+
const ext = path.extname(filePath);
|
|
545
|
+
const md = ext === ".md";
|
|
546
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
547
|
+
let res = ext === ".md" ? markdownToHtml(content, parserArgs) : mdxToJs(content, parserArgs);
|
|
548
|
+
if (res instanceof Promise)
|
|
549
|
+
res = await res;
|
|
550
|
+
const frontmatter = await parse(res.frontmatter);
|
|
551
|
+
const body = "html" in res ? res.html : res.code;
|
|
552
|
+
const slug = slugify(path.basename(file, md ? ".md" : ".mdx"));
|
|
553
|
+
return md ? {
|
|
554
|
+
...frontmatter,
|
|
555
|
+
__mdsrc: { slug, filename: file, type: "md" },
|
|
556
|
+
html: body.trim()
|
|
557
|
+
} : {
|
|
558
|
+
...frontmatter,
|
|
559
|
+
__mdsrc: { slug, filename: file, type: "mdx" },
|
|
560
|
+
code: body.trim()
|
|
561
|
+
};
|
|
562
|
+
}));
|
|
563
|
+
} catch (err) {
|
|
564
|
+
logger2.error("[create]: failed to create entries", err);
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function isENOENT(err) {
|
|
569
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
570
|
+
}
|
|
571
|
+
var fileCache = new Map;
|
|
572
|
+
var FILE_CACHE_MAX_SIZE = 100;
|
|
573
|
+
function setFileCache(filePath, content) {
|
|
574
|
+
fileCache.delete(filePath);
|
|
575
|
+
if (fileCache.size >= FILE_CACHE_MAX_SIZE) {
|
|
576
|
+
const lru = fileCache.keys().next().value;
|
|
577
|
+
if (lru !== undefined) {
|
|
578
|
+
fileCache.delete(lru);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
fileCache.set(filePath, content);
|
|
582
|
+
}
|
|
583
|
+
function getFileCache(filePath) {
|
|
584
|
+
const content = fileCache.get(filePath);
|
|
585
|
+
if (content !== undefined) {
|
|
586
|
+
setFileCache(filePath, content);
|
|
587
|
+
}
|
|
588
|
+
return content;
|
|
589
|
+
}
|
|
590
|
+
async function maybeWrite(filePath, content) {
|
|
591
|
+
const cached = getFileCache(filePath);
|
|
592
|
+
if (cached !== content) {
|
|
593
|
+
await fs.writeFile(filePath, content);
|
|
594
|
+
setFileCache(filePath, content);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
if (await fs.readFile(filePath, "utf-8") === content) {
|
|
599
|
+
setFileCache(filePath, content);
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
} catch (err) {
|
|
603
|
+
if (!isENOENT(err))
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
await fs.writeFile(filePath, content);
|
|
607
|
+
setFileCache(filePath, content);
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
function schemaToType(schema) {
|
|
329
611
|
const fields = Object.entries(schema).map(([k, v]) => {
|
|
330
612
|
const { key, optional } = parseKey(k);
|
|
331
613
|
let type;
|
|
332
614
|
if (typeof v === "string") {
|
|
333
|
-
type = v === "date" ? "string" : v;
|
|
615
|
+
type = v === "date" ? "string" : v === "array" ? "any[]" : v;
|
|
334
616
|
} else {
|
|
335
|
-
type =
|
|
617
|
+
type = schemaToType(v);
|
|
336
618
|
}
|
|
337
619
|
return `${key}${optional ? "?" : ""}: ${type}`;
|
|
338
620
|
}).join(`
|
|
339
621
|
`);
|
|
340
622
|
return `{ ${fields} }`;
|
|
341
623
|
}
|
|
624
|
+
async function getManifest(outDir) {
|
|
625
|
+
return fs.readFile(path.join(outDir, "manifest.json"), "utf-8").then(JSON.parse).then((manifest) => {
|
|
626
|
+
if (!isRecord(manifest))
|
|
627
|
+
return null;
|
|
628
|
+
const entries = Object.entries(manifest).filter((entry) => typeof entry[0] === "string" && Array.isArray(entry[1]) && entry[1].every((value) => typeof value === "string"));
|
|
629
|
+
return Object.fromEntries(entries);
|
|
630
|
+
}).catch(() => null);
|
|
631
|
+
}
|
|
632
|
+
async function cleanup(outDir, manifest, prevManifest) {
|
|
633
|
+
if (!prevManifest)
|
|
634
|
+
return false;
|
|
635
|
+
const files = new Set(Object.values(manifest).flat());
|
|
636
|
+
const prevFiles = Object.values(prevManifest).flat();
|
|
637
|
+
const staleDirs = new Set;
|
|
638
|
+
let cleaned = false;
|
|
639
|
+
for (const filePath of prevFiles) {
|
|
640
|
+
if (files.has(filePath))
|
|
641
|
+
continue;
|
|
642
|
+
await fs.rm(filePath, { force: true });
|
|
643
|
+
fileCache.delete(filePath);
|
|
644
|
+
staleDirs.add(path.dirname(filePath));
|
|
645
|
+
cleaned = true;
|
|
646
|
+
}
|
|
647
|
+
for (const dir of [...staleDirs].toSorted((a, b) => b.length - a.length)) {
|
|
648
|
+
if (dir === outDir)
|
|
649
|
+
continue;
|
|
650
|
+
try {
|
|
651
|
+
if ((await fs.readdir(dir)).length)
|
|
652
|
+
continue;
|
|
653
|
+
} catch (err) {
|
|
654
|
+
if (!isENOENT(err))
|
|
655
|
+
throw err;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
659
|
+
cleaned = true;
|
|
660
|
+
}
|
|
661
|
+
return cleaned;
|
|
662
|
+
}
|
|
342
663
|
async function build(src, buildContext) {
|
|
343
664
|
const { logger: logger2, outDir } = buildContext;
|
|
344
665
|
let names = [];
|
|
345
666
|
const collections = {};
|
|
667
|
+
const manifest = {};
|
|
346
668
|
try {
|
|
347
669
|
if (!outDir)
|
|
348
670
|
throw new Error("Output directory is not defined");
|
|
349
671
|
await fs.mkdir(outDir, { recursive: true });
|
|
672
|
+
const prevManifest = await getManifest(outDir);
|
|
350
673
|
for (const collection of src) {
|
|
351
674
|
const raw = await create(path.join(process.cwd(), collection.dir), buildContext);
|
|
352
|
-
const validated =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
...res.value,
|
|
360
|
-
body,
|
|
361
|
-
__mdsrc
|
|
362
|
-
};
|
|
363
|
-
} catch (err) {
|
|
364
|
-
logger2.error(`[buildStart]: failed to validate item in ${collection.name}`, err);
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
}));
|
|
675
|
+
const validated = raw.map((item) => {
|
|
676
|
+
const { html, code, __mdsrc, ...metadata } = item;
|
|
677
|
+
const res = validate(metadata, collection.schema);
|
|
678
|
+
if (res.issues)
|
|
679
|
+
throw new Error(JSON.stringify(res.issues, null, 2));
|
|
680
|
+
return __mdsrc.type === "md" ? { ...res.value, __mdsrc, html } : { ...res.value, __mdsrc, code };
|
|
681
|
+
});
|
|
368
682
|
collections[collection.name] = {
|
|
369
|
-
items: validated
|
|
683
|
+
items: validated,
|
|
370
684
|
schema: collection.schema
|
|
371
685
|
};
|
|
686
|
+
manifest[collection.name] = [];
|
|
372
687
|
}
|
|
373
688
|
names = Object.keys(collections);
|
|
374
689
|
const promises = [];
|
|
375
|
-
promises.push(maybeWrite(path.join(outDir, "types.ts"), `
|
|
690
|
+
promises.push(maybeWrite(path.join(outDir, "types.ts"), ` ${AUTOGEN_MSG}
|
|
691
|
+
|
|
692
|
+
import type { Collection } from '${PKG_NAME}'
|
|
693
|
+
|
|
376
694
|
${names.map((name) => `
|
|
377
|
-
export type ${capitalise(name)} = ${
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
filename: string
|
|
382
|
-
},
|
|
383
|
-
}
|
|
695
|
+
export type ${capitalise(singularise(name))} = ${schemaToType(collections[name].schema)} & {
|
|
696
|
+
html?: string,
|
|
697
|
+
Component?: any,
|
|
698
|
+
} & Collection.Metadata
|
|
384
699
|
`).join(`
|
|
385
700
|
|
|
386
701
|
`)}`.trim()));
|
|
387
|
-
promises.push(maybeWrite(path.join(outDir, "index.d.ts"), `
|
|
388
|
-
|
|
702
|
+
promises.push(maybeWrite(path.join(outDir, "index.d.ts"), ` ${AUTOGEN_MSG}
|
|
703
|
+
|
|
704
|
+
import type { ${names.map((name) => capitalise(singularise(name))).join(", ")} } from './types.js'
|
|
389
705
|
|
|
390
706
|
${names.map((name) => `
|
|
391
|
-
export const all${capitalise(pluralise(name, 2))}: ${capitalise(name)}[]
|
|
707
|
+
export const all${capitalise(pluralise(name, 2))}: ${capitalise(singularise(name))}[]
|
|
392
708
|
`).join(`
|
|
393
709
|
|
|
394
710
|
`)}
|
|
395
711
|
|
|
396
712
|
declare module '${PKG_NAME}' {
|
|
397
713
|
${names.map((name) => `
|
|
398
|
-
export const all${capitalise(pluralise(name, 2))}: ${capitalise(name)}[]
|
|
714
|
+
export const all${capitalise(pluralise(name, 2))}: ${capitalise(singularise(name))}[]
|
|
399
715
|
`).join(`
|
|
400
716
|
|
|
401
717
|
`)}
|
|
402
718
|
}
|
|
403
719
|
`.trim()));
|
|
404
720
|
for (const name of names) {
|
|
405
|
-
const collection = collections[name]?.items;
|
|
721
|
+
const collection = collections[name]?.items ?? [];
|
|
406
722
|
const fileName = toModuleName(name);
|
|
407
|
-
|
|
723
|
+
const filePath = `${fileName}.js`;
|
|
724
|
+
const imports = [];
|
|
725
|
+
const entries = [];
|
|
726
|
+
if (collection.some(isMdx)) {
|
|
727
|
+
await fs.mkdir(path.join(outDir, fileName), { recursive: true });
|
|
728
|
+
}
|
|
729
|
+
for (let i = 0;i < collection.length; i++) {
|
|
730
|
+
const item = collection[i];
|
|
731
|
+
if (isMdx(item)) {
|
|
732
|
+
const slug = item.__mdsrc.slug;
|
|
733
|
+
const fullPath2 = path.join(outDir, fileName, `${slug}.js`);
|
|
734
|
+
manifest[name].push(fullPath2);
|
|
735
|
+
promises.push(maybeWrite(fullPath2, item.code));
|
|
736
|
+
const importName = `C${i}`;
|
|
737
|
+
imports.push(`import ${importName} from './${fileName}/${slug}.js'`);
|
|
738
|
+
const { code, ...rest } = item;
|
|
739
|
+
entries.push(`{ ...${JSON.stringify(rest)}, Component: ${importName} }`);
|
|
740
|
+
} else {
|
|
741
|
+
entries.push(JSON.stringify(item));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const fullPath = path.join(outDir, filePath);
|
|
745
|
+
manifest[name].push(fullPath);
|
|
746
|
+
promises.push(maybeWrite(fullPath, ` ${AUTOGEN_MSG}
|
|
747
|
+
|
|
748
|
+
${imports.join(`
|
|
749
|
+
`)}
|
|
750
|
+
|
|
751
|
+
export const all${capitalise(pluralise(name, 2))} = [${entries.join(`,
|
|
752
|
+
`)}]`.trim()));
|
|
408
753
|
}
|
|
409
|
-
promises.push(maybeWrite(path.join(outDir, "index.js"),
|
|
410
|
-
|
|
754
|
+
promises.push(maybeWrite(path.join(outDir, "index.js"), `${AUTOGEN_MSG}
|
|
755
|
+
|
|
756
|
+
${names.map((name) => `
|
|
757
|
+
export * from './${toModuleName(name)}.js'
|
|
758
|
+
`).join(`
|
|
759
|
+
`)}`));
|
|
760
|
+
promises.push(maybeWrite(path.join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2)));
|
|
411
761
|
const writes = await Promise.all(promises);
|
|
762
|
+
const cleaned = await cleanup(outDir, manifest, prevManifest);
|
|
412
763
|
buildContext.names = names;
|
|
413
|
-
return writes.some((
|
|
764
|
+
return writes.some((c) => c) || cleaned;
|
|
414
765
|
} catch (err) {
|
|
415
766
|
logger2.error("[build]: failed to generate data", err);
|
|
416
767
|
throw err;
|
|
@@ -506,15 +857,13 @@ function mdsrc(config) {
|
|
|
506
857
|
server.watcher.on("add", (p) => rebuild("add", p)).on("change", (p) => rebuild("change", p)).on("unlink", (p) => rebuild("unlink", p));
|
|
507
858
|
},
|
|
508
859
|
resolveId(id) {
|
|
509
|
-
if (id === PKG_NAME)
|
|
860
|
+
if (id === PKG_NAME)
|
|
510
861
|
return path.join(outDir, "index.js");
|
|
511
|
-
}
|
|
512
862
|
if (id.startsWith(`${PKG_NAME}/`)) {
|
|
513
863
|
const subpath = id.slice(PKG_NAME.length + 1);
|
|
514
864
|
const match = buildContext.names.find((name) => name === subpath || toModuleName(name) === subpath);
|
|
515
|
-
if (match)
|
|
865
|
+
if (match)
|
|
516
866
|
return path.join(outDir, `${toModuleName(match)}.js`);
|
|
517
|
-
}
|
|
518
867
|
}
|
|
519
868
|
return null;
|
|
520
869
|
}
|
|
@@ -523,42 +872,24 @@ function mdsrc(config) {
|
|
|
523
872
|
function toModuleName(name) {
|
|
524
873
|
return name.toLowerCase();
|
|
525
874
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const now = Date.now();
|
|
529
|
-
const yaml = {
|
|
530
|
-
kind: "yaml",
|
|
531
|
-
value: dedent(`
|
|
532
|
-
title: mdsrc
|
|
533
|
-
date: ${now}
|
|
534
|
-
`)
|
|
535
|
-
};
|
|
536
|
-
const toml = {
|
|
537
|
-
kind: "toml",
|
|
538
|
-
value: dedent(`
|
|
539
|
-
title = "mdsrc"
|
|
540
|
-
date = ${now}
|
|
541
|
-
`)
|
|
542
|
-
};
|
|
543
|
-
describe("markdown parsing", () => {
|
|
544
|
-
it("parses frontmatter", async () => {
|
|
545
|
-
for (const f of [yaml, toml]) {
|
|
546
|
-
const frontmatter = await parse(f);
|
|
547
|
-
expect(frontmatter).toEqual({
|
|
548
|
-
title: "mdsrc",
|
|
549
|
-
date: now
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
it("returns empty object for missing frontmatter", async () => {
|
|
554
|
-
const frontmatter = await parse(null);
|
|
555
|
-
expect(frontmatter).toEqual({});
|
|
556
|
-
});
|
|
557
|
-
});
|
|
875
|
+
function isMdx(item) {
|
|
876
|
+
return item.__mdsrc.type === "mdx";
|
|
558
877
|
}
|
|
559
878
|
export {
|
|
879
|
+
toModuleName,
|
|
880
|
+
setFileCache,
|
|
881
|
+
schemaToType,
|
|
882
|
+
parse,
|
|
883
|
+
maybeWrite,
|
|
884
|
+
isENOENT,
|
|
885
|
+
getManifest,
|
|
886
|
+
getFileCache,
|
|
887
|
+
fileCache,
|
|
560
888
|
mdsrc as default,
|
|
561
|
-
create
|
|
889
|
+
create,
|
|
890
|
+
cleanup,
|
|
891
|
+
FILE_CACHE_MAX_SIZE,
|
|
892
|
+
DEFAULT_COMPILE_OPTIONS
|
|
562
893
|
};
|
|
563
894
|
|
|
564
|
-
//# debugId=
|
|
895
|
+
//# debugId=708A8C4A969C717264756E2164756E21
|