@jk2908/mdsrc 0.4.0 → 0.5.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
@@ -132,11 +132,6 @@ function debounce(fn, wait) {
132
132
  }, wait);
133
133
  };
134
134
  }
135
- function dedent(str) {
136
- return str.replace(/^\n/, "").replace(/\s+$/, "").split(`
137
- `).filter(Boolean).map((line) => line.replace(/^\s+/, "")).join(`
138
- `);
139
- }
140
135
  function isRecord(value) {
141
136
  return typeof value === "object" && value !== null && !Array.isArray(value);
142
137
  }
@@ -151,128 +146,151 @@ function deep(obj, path, value) {
151
146
  cur[parts.at(-1)] = value;
152
147
  }
153
148
 
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;
237
- }
149
+ // src/types.ts
150
+ var PRIMITIVE_NAMES = ["string", "number", "boolean", "date", "array"];
151
+ var MODIFIER_NAMES = ["max", "min"];
152
+
153
+ // src/validate.ts
238
154
  function validate(input, schema) {
239
155
  const validated = {};
240
156
  const issues = [];
241
157
  if (typeof input !== "object" || input === null) {
242
- issues.push({ message: "Input must be an object" });
158
+ issues.push({ message: "Input must be an object", code: "INVALID_INPUT" });
243
159
  return { issues };
244
160
  }
161
+ const schemaKeys = new Set(Object.keys(schema).map((k) => parseKey(k).key));
162
+ for (const key in input) {
163
+ if (!schemaKeys.has(key)) {
164
+ issues.push({ message: `Unknown key: ${key}`, code: "UNKNOWN_KEY" });
165
+ }
166
+ }
167
+ if (issues.length)
168
+ return { issues };
245
169
  function walk(key, schemaValue, data) {
246
170
  const { optional, key: parsedKey } = parseKey(key);
247
171
  if (data === undefined) {
248
172
  if (!optional) {
249
- issues.push({ message: `Missing required key: ${parsedKey}` });
173
+ issues.push({
174
+ message: `Missing required key: ${parsedKey}`,
175
+ code: "MISSING_REQUIRED"
176
+ });
250
177
  }
251
178
  return;
252
179
  }
253
180
  if (typeof schemaValue === "string") {
254
- switch (schemaValue) {
255
- case "string": {
181
+ const { types, modifiers } = parseSchemaValue(schemaValue);
182
+ for (const type of types) {
183
+ if (type === "string") {
256
184
  if (typeof data !== "string") {
257
- issues.push({ message: `Key ${parsedKey} must be a string` });
258
- return;
185
+ if (types.length === 1) {
186
+ issues.push({
187
+ message: `Key ${parsedKey} must be a string`,
188
+ code: "INVALID_TYPE"
189
+ });
190
+ return;
191
+ }
192
+ continue;
193
+ }
194
+ if (modifiers.min) {
195
+ const min = Number(modifiers.min);
196
+ if (Number.isNaN(min)) {
197
+ issues.push({
198
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to type (number)`,
199
+ code: "BAD_MODIFIER"
200
+ });
201
+ return;
202
+ }
203
+ if (data.length < min) {
204
+ if (types.length === 1) {
205
+ issues.push({
206
+ message: `Key ${parsedKey} must be greater than or equal to minimum length (${min})`,
207
+ code: "INVALID_LENGTH"
208
+ });
209
+ return;
210
+ }
211
+ continue;
212
+ }
213
+ }
214
+ if (modifiers.max) {
215
+ const max = Number(modifiers.max);
216
+ if (Number.isNaN(max)) {
217
+ issues.push({
218
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to type (number)`,
219
+ code: "BAD_MODIFIER"
220
+ });
221
+ return;
222
+ }
223
+ if (data.length > max) {
224
+ if (types.length === 1) {
225
+ issues.push({
226
+ message: `Key ${parsedKey} must be less than or equal to maximum length (${max})`,
227
+ code: "INVALID_LENGTH"
228
+ });
229
+ return;
230
+ }
231
+ continue;
232
+ }
259
233
  }
260
234
  deep(validated, parsedKey, data);
261
- break;
262
- }
263
- case "number": {
235
+ return;
236
+ } else if (type === "number") {
264
237
  let num = data;
265
238
  if (typeof data === "string" && !Number.isNaN(Number(data))) {
266
239
  num = Number(data);
267
240
  }
268
241
  if (typeof num !== "number" || Number.isNaN(num)) {
269
- issues.push({ message: `Key ${parsedKey} must be a number` });
270
- return;
242
+ if (types.length === 1) {
243
+ issues.push({
244
+ message: `Key ${parsedKey} must be a number`,
245
+ code: "INVALID_TYPE"
246
+ });
247
+ return;
248
+ }
249
+ continue;
250
+ }
251
+ if (modifiers.min) {
252
+ const min = Number(modifiers.min);
253
+ if (Number.isNaN(min)) {
254
+ issues.push({
255
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to type (number)`,
256
+ code: "BAD_MODIFIER"
257
+ });
258
+ return;
259
+ }
260
+ if (num < min) {
261
+ if (types.length === 1) {
262
+ issues.push({
263
+ message: `Key ${parsedKey} must be greater than or equal to minimum size (${min})`,
264
+ code: "INVALID_SIZE"
265
+ });
266
+ return;
267
+ }
268
+ continue;
269
+ }
270
+ }
271
+ if (modifiers.max) {
272
+ const max = Number(modifiers.max);
273
+ if (Number.isNaN(max)) {
274
+ issues.push({
275
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to type (number)`,
276
+ code: "BAD_MODIFIER"
277
+ });
278
+ return;
279
+ }
280
+ if (num > max) {
281
+ if (types.length === 1) {
282
+ issues.push({
283
+ message: `Key ${parsedKey} must be less than or equal to maximum size (${max})`,
284
+ code: "INVALID_SIZE"
285
+ });
286
+ return;
287
+ }
288
+ continue;
289
+ }
271
290
  }
272
291
  deep(validated, parsedKey, num);
273
- break;
274
- }
275
- case "boolean": {
292
+ return;
293
+ } else if (type === "boolean") {
276
294
  let bool = data;
277
295
  if (typeof data === "string") {
278
296
  if (data.toLowerCase() === "true") {
@@ -282,29 +300,151 @@ function validate(input, schema) {
282
300
  }
283
301
  }
284
302
  if (typeof bool !== "boolean") {
285
- issues.push({ message: `Key ${parsedKey} must be a boolean` });
286
- return;
303
+ if (types.length === 1) {
304
+ issues.push({
305
+ message: `Key ${parsedKey} must be a boolean`,
306
+ code: "INVALID_TYPE"
307
+ });
308
+ return;
309
+ }
310
+ continue;
287
311
  }
288
312
  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;
313
+ return;
314
+ } else if (type === "date") {
315
+ let date;
316
+ if (data instanceof Date) {
317
+ date = data;
318
+ } else if (typeof data === "string" || typeof data === "number") {
319
+ date = new Date(data);
320
+ } else {
321
+ if (types.length === 1) {
322
+ issues.push({
323
+ message: `Key ${parsedKey} must be a Date, string or number`,
324
+ code: "INVALID_TYPE"
325
+ });
326
+ return;
327
+ }
328
+ continue;
295
329
  }
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;
330
+ const dt = date.getTime();
331
+ if (Number.isNaN(dt)) {
332
+ if (types.length === 1) {
333
+ issues.push({
334
+ message: `Key ${parsedKey} must be a valid date`,
335
+ code: "INVALID_DATE"
336
+ });
337
+ return;
338
+ }
339
+ continue;
340
+ }
341
+ if (modifiers.min) {
342
+ const min = new Date(Number(modifiers.min));
343
+ if (Number.isNaN(min.getTime())) {
344
+ issues.push({
345
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to instance (Date)`,
346
+ code: "BAD_MODIFIER"
347
+ });
348
+ return;
349
+ }
350
+ if (dt < min.getTime()) {
351
+ if (types.length === 1) {
352
+ issues.push({
353
+ message: `Key ${parsedKey} must be greater than or equal to minimum date (${min.toISOString()})`,
354
+ code: "INVALID_DATE"
355
+ });
356
+ return;
357
+ }
358
+ continue;
359
+ }
360
+ }
361
+ if (modifiers.max) {
362
+ const max = new Date(Number(modifiers.max));
363
+ if (Number.isNaN(max.getTime())) {
364
+ issues.push({
365
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to instance (Date)`,
366
+ code: "BAD_MODIFIER"
367
+ });
368
+ return;
369
+ }
370
+ if (dt > max.getTime()) {
371
+ if (types.length === 1) {
372
+ issues.push({
373
+ message: `Key ${parsedKey} must be less than or equal to maximum date (${max.toISOString()})`,
374
+ code: "INVALID_DATE"
375
+ });
376
+ return;
377
+ }
378
+ continue;
379
+ }
300
380
  }
301
381
  deep(validated, parsedKey, date.toISOString());
302
- break;
382
+ return;
383
+ } else if (type === "array") {
384
+ if (!Array.isArray(data)) {
385
+ if (types.length === 1) {
386
+ issues.push({
387
+ message: `Key ${parsedKey} must be an array`,
388
+ code: "INVALID_TYPE"
389
+ });
390
+ return;
391
+ }
392
+ continue;
393
+ }
394
+ if (modifiers.min) {
395
+ const min = Number(modifiers.min);
396
+ if (Number.isNaN(min)) {
397
+ issues.push({
398
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.min}) that could not be converted to type (number)`,
399
+ code: "BAD_MODIFIER"
400
+ });
401
+ return;
402
+ }
403
+ if (data.length < min) {
404
+ if (types.length === 1) {
405
+ issues.push({
406
+ message: `Key ${parsedKey} must be greater than or equal to minimum array length (${min})`,
407
+ code: "INVALID_LENGTH"
408
+ });
409
+ return;
410
+ }
411
+ continue;
412
+ }
413
+ }
414
+ if (modifiers.max) {
415
+ const max = Number(modifiers.max);
416
+ if (Number.isNaN(max)) {
417
+ issues.push({
418
+ message: `Key ${parsedKey} contains a bad modifier (${modifiers.max}) that could not be converted to type (number)`,
419
+ code: "BAD_MODIFIER"
420
+ });
421
+ return;
422
+ }
423
+ if (data.length > max) {
424
+ if (types.length === 1) {
425
+ issues.push({
426
+ message: `Key ${parsedKey} must be less than or equal to maximum array length (${max})`,
427
+ code: "INVALID_LENGTH"
428
+ });
429
+ return;
430
+ }
431
+ continue;
432
+ }
433
+ }
434
+ deep(validated, parsedKey, data);
435
+ return;
303
436
  }
304
437
  }
438
+ issues.push({
439
+ message: `Key ${parsedKey} must be one of: ${types.join(", ")}`,
440
+ code: "INVALID_TYPE"
441
+ });
305
442
  } else {
306
443
  if (!isRecord(data)) {
307
- issues.push({ message: `Key ${parsedKey} must be an object` });
444
+ issues.push({
445
+ message: `Key ${parsedKey} must be an object`,
446
+ code: "INVALID_TYPE"
447
+ });
308
448
  return;
309
449
  }
310
450
  const obj = data;
@@ -325,14 +465,136 @@ function parseKey(k) {
325
465
  key: optional ? k.slice(0, -1) : k
326
466
  };
327
467
  }
328
- function schemaValueToType(schema) {
468
+ function parseSchemaValue(value) {
469
+ if (isRecord(value))
470
+ throw new Error("Cannot parse object schema values");
471
+ const parts = value.split("|");
472
+ const types = [];
473
+ const modifiers = {};
474
+ for (const p of parts) {
475
+ if (p.indexOf("=") > -1) {
476
+ const [m, v] = p.split("=");
477
+ if (!isModifierName(m))
478
+ throw new Error(`Unrecognised modifier: ${m}`);
479
+ modifiers[m] = v;
480
+ } else {
481
+ if (!isPrimitive(p))
482
+ throw new Error(`Unrecognised type: ${p}`);
483
+ types.push(p);
484
+ }
485
+ }
486
+ return { types, modifiers };
487
+ }
488
+ function isModifierName(name) {
489
+ return MODIFIER_NAMES.some((n) => n === name);
490
+ }
491
+ function isPrimitive(name) {
492
+ return PRIMITIVE_NAMES.some((n) => n === name);
493
+ }
494
+
495
+ // src/index.ts
496
+ var DEFAULT_COMPILE_OPTIONS = {
497
+ features: {
498
+ frontmatter: true
499
+ }
500
+ };
501
+ async function parse(frontmatter) {
502
+ if (!frontmatter)
503
+ return {};
504
+ const { kind, value } = frontmatter;
505
+ switch (kind) {
506
+ case "yaml": {
507
+ return (await import("yaml")).parse(value);
508
+ }
509
+ case "toml": {
510
+ return (await import("smol-toml")).parse(value);
511
+ }
512
+ }
513
+ }
514
+ async function create(dir, buildContext) {
515
+ const { logger: logger2, compileOptions = {} } = buildContext;
516
+ const { features, ...restCompileOptions } = compileOptions;
517
+ try {
518
+ const files = (await fs.readdir(dir)).filter((file) => path.extname(file) === ".md");
519
+ const filePaths = files.map((file) => path.join(dir, file));
520
+ if (!files.length) {
521
+ logger2.warn(`mdsrc: ${dir} is empty`);
522
+ return [];
523
+ }
524
+ return Promise.all(filePaths.map(async (filePath) => {
525
+ const file = path.basename(filePath);
526
+ const { html, frontmatter: rawFrontmatter } = markdownToHtml(await fs.readFile(filePath, "utf-8"), {
527
+ features: {
528
+ ...DEFAULT_COMPILE_OPTIONS.features,
529
+ ...features
530
+ },
531
+ ...restCompileOptions
532
+ });
533
+ const frontmatter = await parse(rawFrontmatter);
534
+ return {
535
+ ...frontmatter,
536
+ __mdsrc: {
537
+ slug: path.basename(file, ".md").toLowerCase().replace(/\s+/g, "-"),
538
+ filename: file
539
+ },
540
+ body: html.trim()
541
+ };
542
+ }));
543
+ } catch (err) {
544
+ logger2.error("[create]: failed to create entries", err);
545
+ throw err;
546
+ }
547
+ }
548
+ function isENOENT(err) {
549
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
550
+ }
551
+ var fileCache = new Map;
552
+ var FILE_CACHE_MAX_SIZE = 100;
553
+ function setFileCache(filePath, content) {
554
+ fileCache.delete(filePath);
555
+ if (fileCache.size >= FILE_CACHE_MAX_SIZE) {
556
+ const lru = fileCache.keys().next().value;
557
+ if (lru !== undefined) {
558
+ fileCache.delete(lru);
559
+ }
560
+ }
561
+ fileCache.set(filePath, content);
562
+ }
563
+ function getFileCache(filePath) {
564
+ const content = fileCache.get(filePath);
565
+ if (content !== undefined) {
566
+ setFileCache(filePath, content);
567
+ }
568
+ return content;
569
+ }
570
+ async function maybeWrite(filePath, content) {
571
+ const cached = getFileCache(filePath);
572
+ if (cached !== content) {
573
+ await fs.writeFile(filePath, content);
574
+ setFileCache(filePath, content);
575
+ return true;
576
+ }
577
+ try {
578
+ if (await fs.readFile(filePath, "utf-8") === content) {
579
+ setFileCache(filePath, content);
580
+ return false;
581
+ }
582
+ } catch (err) {
583
+ if (!isENOENT(err))
584
+ throw err;
585
+ }
586
+ await fs.writeFile(filePath, content);
587
+ setFileCache(filePath, content);
588
+ return true;
589
+ }
590
+ function schemaToType(schema) {
329
591
  const fields = Object.entries(schema).map(([k, v]) => {
330
592
  const { key, optional } = parseKey(k);
331
593
  let type;
332
594
  if (typeof v === "string") {
333
- type = v === "date" ? "string" : v;
595
+ type = v === "date" ? "string" : v === "array" ? "any[]" : v;
334
596
  } else {
335
- type = schemaValueToType(v);
597
+ type = schemaToType(v);
336
598
  }
337
599
  return `${key}${optional ? "?" : ""}: ${type}`;
338
600
  }).join(`
@@ -349,24 +611,19 @@ async function build(src, buildContext) {
349
611
  await fs.mkdir(outDir, { recursive: true });
350
612
  for (const collection of src) {
351
613
  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
- }));
614
+ const validated = raw.map((item) => {
615
+ const { body, __mdsrc, ...metadata } = item;
616
+ const res = validate(metadata, collection.schema);
617
+ if (res.issues)
618
+ throw new Error(JSON.stringify(res.issues, null, 2));
619
+ return {
620
+ ...res.value,
621
+ body,
622
+ __mdsrc
623
+ };
624
+ });
368
625
  collections[collection.name] = {
369
- items: validated.filter((e) => e !== null),
626
+ items: validated,
370
627
  schema: collection.schema
371
628
  };
372
629
  }
@@ -374,7 +631,7 @@ async function build(src, buildContext) {
374
631
  const promises = [];
375
632
  promises.push(maybeWrite(path.join(outDir, "types.ts"), `
376
633
  ${names.map((name) => `
377
- export type ${capitalise(name)} = ${schemaValueToType(collections[name].schema)} & {
634
+ export type ${capitalise(name)} = ${schemaToType(collections[name].schema)} & {
378
635
  body: string,
379
636
  __mdsrc: {
380
637
  slug: string
@@ -506,15 +763,13 @@ function mdsrc(config) {
506
763
  server.watcher.on("add", (p) => rebuild("add", p)).on("change", (p) => rebuild("change", p)).on("unlink", (p) => rebuild("unlink", p));
507
764
  },
508
765
  resolveId(id) {
509
- if (id === PKG_NAME) {
766
+ if (id === PKG_NAME)
510
767
  return path.join(outDir, "index.js");
511
- }
512
768
  if (id.startsWith(`${PKG_NAME}/`)) {
513
769
  const subpath = id.slice(PKG_NAME.length + 1);
514
770
  const match = buildContext.names.find((name) => name === subpath || toModuleName(name) === subpath);
515
- if (match) {
771
+ if (match)
516
772
  return path.join(outDir, `${toModuleName(match)}.js`);
517
- }
518
773
  }
519
774
  return null;
520
775
  }
@@ -523,42 +778,19 @@ function mdsrc(config) {
523
778
  function toModuleName(name) {
524
779
  return name.toLowerCase();
525
780
  }
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
- });
558
- }
559
781
  export {
782
+ toModuleName,
783
+ setFileCache,
784
+ schemaToType,
785
+ parse,
786
+ maybeWrite,
787
+ isENOENT,
788
+ getFileCache,
789
+ fileCache,
560
790
  mdsrc as default,
561
- create
791
+ create,
792
+ FILE_CACHE_MAX_SIZE,
793
+ DEFAULT_COMPILE_OPTIONS
562
794
  };
563
795
 
564
- //# debugId=0F1D108961D2552964756E2164756E21
796
+ //# debugId=49D42D1A4493323164756E2164756E21