@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/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 { markdownToHtml } from "satteri";
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
- // src/index.ts
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({ message: `Missing required key: ${parsedKey}` });
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
- switch (schemaValue) {
255
- case "string": {
191
+ const { types, modifiers } = parseSchemaValue(schemaValue);
192
+ for (const type of types) {
193
+ if (type === "string") {
256
194
  if (typeof data !== "string") {
257
- issues.push({ message: `Key ${parsedKey} must be a string` });
258
- return;
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
- break;
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
- issues.push({ message: `Key ${parsedKey} must be a number` });
270
- return;
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
- break;
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
- issues.push({ message: `Key ${parsedKey} must be a boolean` });
286
- return;
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
- break;
290
- }
291
- case "date": {
292
- if (typeof data !== "string") {
293
- issues.push({ message: `Key ${parsedKey} must be a date` });
294
- return;
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
- const date = new Date(data);
297
- if (Number.isNaN(date.getTime())) {
298
- issues.push({ message: `Key ${parsedKey} must be a valid date` });
299
- return;
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
- break;
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({ message: `Key ${parsedKey} must be an object` });
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 schemaValueToType(schema) {
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 = schemaValueToType(v);
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 = await Promise.all(raw.map(async (item) => {
353
- try {
354
- const { body, __mdsrc, ...metadata } = item;
355
- const res = validate(metadata, collection.schema);
356
- if (res.issues)
357
- throw new Error(JSON.stringify(res.issues, null, 2));
358
- return {
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.filter((e) => e !== null),
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)} = ${schemaValueToType(collections[name].schema)} & {
378
- body: string,
379
- __mdsrc: {
380
- slug: string
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
- import type { ${names.map((name) => capitalise(name)).join(", ")} } from './types.js'
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
- promises.push(maybeWrite(path.join(outDir, `${fileName}.js`), `export const all${capitalise(pluralise(name, 2))} = ${collection?.length ? JSON.stringify(collection) : "[]"}`.trim()));
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"), names.map((name) => `export * from './${toModuleName(name)}.js'`).join(`
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((changed) => changed);
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
- if (import.meta.vitest) {
527
- const { it, expect, describe } = import.meta.vitest;
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=0F1D108961D2552964756E2164756E21
895
+ //# debugId=708A8C4A969C717264756E2164756E21