writr 4.5.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,18 @@ plugins and working with the processor directly.
56
56
  - [`.loadFromFileSync(filePath: string)`](#loadfromfilesyncfilepath-string)
57
57
  - [`.saveToFile(filePath: string)`](#savetofilefilepath-string)
58
58
  - [`.saveToFileSync(filePath: string)`](#savetofilesyncfilepath-string)
59
+ - [Caching On Render](#caching-on-render)
60
+ - [GitHub Flavored Markdown (GFM)](#github-flavored-markdown-gfm)
61
+ - [GFM Features](#gfm-features)
62
+ - [Using GFM](#using-gfm)
63
+ - [Disabling GFM](#disabling-gfm)
59
64
  - [Hooks](#hooks)
65
+ - [Emitters](#emitters)
66
+ - [Error Events](#error-events)
67
+ - [Listening to Error Events](#listening-to-error-events)
68
+ - [Methods that Emit Errors](#methods-that-emit-errors)
69
+ - [Error Event Examples](#error-event-examples)
70
+ - [Event Emitter Methods](#event-emitter-methods)
60
71
  - [ESM and Node Version Support](#esm-and-node-version-support)
61
72
  - [Code of Conduct and Contributing](#code-of-conduct-and-contributing)
62
73
  - [License](#license)
@@ -413,6 +424,117 @@ writr.cache.store.lruSize = 100;
413
424
  writr.cache.store.ttl = '5m'; // setting it to 5 minutes
414
425
  ```
415
426
 
427
+ # GitHub Flavored Markdown (GFM)
428
+
429
+ Writr includes full support for [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM) through the `remark-gfm` and `remark-github-blockquote-alert` plugins. GFM is enabled by default and adds several powerful features to standard Markdown.
430
+
431
+ ## GFM Features
432
+
433
+ When GFM is enabled (which it is by default), you get access to the following features:
434
+
435
+ ### Tables
436
+
437
+ Create tables using pipes and hyphens:
438
+
439
+ ```markdown
440
+ | Feature | Supported |
441
+ |---------|-----------|
442
+ | Tables | Yes |
443
+ | Alerts | Yes |
444
+ ```
445
+
446
+ ### Strikethrough
447
+
448
+ Use `~~` to create strikethrough text:
449
+
450
+ ```markdown
451
+ ~~This text is crossed out~~
452
+ ```
453
+
454
+ ### Task Lists
455
+
456
+ Create interactive checkboxes:
457
+
458
+ ```markdown
459
+ - [x] Completed task
460
+ - [ ] Incomplete task
461
+ - [ ] Another task
462
+ ```
463
+
464
+ ### Autolinks
465
+
466
+ URLs are automatically converted to clickable links:
467
+
468
+ ```markdown
469
+ https://github.com
470
+ ```
471
+
472
+ ### GitHub Blockquote Alerts
473
+
474
+ GitHub-style alerts are supported to emphasize critical information. These are blockquote-based admonitions that render with special styling:
475
+
476
+ ```markdown
477
+ > [!NOTE]
478
+ > Useful information that users should know, even when skimming content.
479
+
480
+ > [!TIP]
481
+ > Helpful advice for doing things better or more easily.
482
+
483
+ > [!IMPORTANT]
484
+ > Key information users need to know to achieve their goal.
485
+
486
+ > [!WARNING]
487
+ > Urgent info that needs immediate user attention to avoid problems.
488
+
489
+ > [!CAUTION]
490
+ > Advises about risks or negative outcomes of certain actions.
491
+ ```
492
+
493
+ ## Using GFM
494
+
495
+ GFM is enabled by default. Here's an example:
496
+
497
+ ```javascript
498
+ import { Writr } from 'writr';
499
+
500
+ const markdown = `
501
+ # Task List Example
502
+
503
+ - [x] Learn Writr basics
504
+ - [ ] Master GFM features
505
+
506
+ > [!NOTE]
507
+ > GitHub Flavored Markdown is enabled by default!
508
+
509
+ | Feature | Status |
510
+ |---------|--------|
511
+ | GFM | ✓ |
512
+ `;
513
+
514
+ const writr = new Writr(markdown);
515
+ const html = await writr.render(); // Renders with full GFM support
516
+ ```
517
+
518
+ ## Disabling GFM
519
+
520
+ If you need to disable GFM features, you can set `gfm: false` in the render options:
521
+
522
+ ```javascript
523
+ import { Writr } from 'writr';
524
+
525
+ const writr = new Writr('~~strikethrough~~ text');
526
+
527
+ // Disable GFM
528
+ const html = await writr.render({ gfm: false });
529
+ // Output: <p>~~strikethrough~~ text</p>
530
+
531
+ // With GFM enabled (default)
532
+ const htmlWithGfm = await writr.render({ gfm: true });
533
+ // Output: <p><del>strikethrough</del> text</p>
534
+ ```
535
+
536
+ Note: When GFM is disabled, GitHub blockquote alerts will not be processed and will render as regular blockquotes.
537
+
416
538
  # Hooks
417
539
 
418
540
  Hooks are a way to add additional parsing to the render pipeline. You can add hooks to the the Writr instance. Here is an example of adding a hook to the instance of Writr:
@@ -476,6 +598,135 @@ export type loadFromFileData = {
476
598
 
477
599
  This is called when you call `loadFromFile`, `loadFromFileSync`.
478
600
 
601
+ # Emitters
602
+
603
+ Writr extends the [Hookified](https://github.com/jaredwray/hookified) class, which provides event emitter capabilities. This means you can listen to events emitted by Writr during its lifecycle, particularly error events.
604
+
605
+ ## Error Events
606
+
607
+ Writr emits an `error` event whenever an error occurs in any of its methods. This provides a centralized way to handle errors without wrapping every method call in a try/catch block.
608
+
609
+ ### Listening to Error Events
610
+
611
+ You can listen to error events using the `.on()` method:
612
+
613
+ ```javascript
614
+ import { Writr } from 'writr';
615
+
616
+ const writr = new Writr('# Hello World');
617
+
618
+ // Listen for any errors
619
+ writr.on('error', (error) => {
620
+ console.error('An error occurred:', error.message);
621
+ // Handle the error appropriately
622
+ // Log to error tracking service, display to user, etc.
623
+ });
624
+
625
+ // Now when any error occurs, your listener will be notified
626
+ try {
627
+ await writr.render();
628
+ } catch (error) {
629
+ // Error is also thrown, so you can handle it here too
630
+ }
631
+ ```
632
+
633
+ ### Methods that Emit Errors
634
+
635
+ The following methods emit error events when they fail:
636
+
637
+ **Rendering Methods:**
638
+ - `render()` - Emits error before throwing when markdown rendering fails
639
+ - `renderSync()` - Emits error before throwing when markdown rendering fails
640
+ - `renderReact()` - Emits error before throwing when React rendering fails
641
+ - `renderReactSync()` - Emits error before throwing when React rendering fails
642
+
643
+ **Validation Methods:**
644
+ - `validate()` - Emits error when validation fails (returns error in result object)
645
+ - `validateSync()` - Emits error when validation fails (returns error in result object)
646
+
647
+ **File Operations:**
648
+ - `renderToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
649
+ - `renderToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
650
+ - `loadFromFile()` - Emits error when file reading fails (does not throw if `throwErrors: false`)
651
+ - `loadFromFileSync()` - Emits error when file reading fails (does not throw if `throwErrors: false`)
652
+ - `saveToFile()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
653
+ - `saveToFileSync()` - Emits error when file writing fails (does not throw if `throwErrors: false`)
654
+
655
+ **Front Matter Operations:**
656
+ - `frontMatter` getter - Emits error when YAML parsing fails
657
+ - `frontMatter` setter - Emits error when YAML serialization fails
658
+
659
+ ### Error Event Examples
660
+
661
+ **Example 1: Global Error Handler**
662
+
663
+ ```javascript
664
+ import { Writr } from 'writr';
665
+
666
+ const writr = new Writr();
667
+
668
+ // Set up a global error handler
669
+ writr.on('error', (error) => {
670
+ // Log to your monitoring service
671
+ console.error('Writr error:', error);
672
+
673
+ // Send to error tracking (e.g., Sentry, Rollbar)
674
+ // errorTracker.captureException(error);
675
+ });
676
+
677
+ // All errors will be emitted to the listener above
678
+ await writr.loadFromFile('./content.md');
679
+ const html = await writr.render();
680
+ ```
681
+
682
+ **Example 2: Validation with Error Listening**
683
+
684
+ ```javascript
685
+ import { Writr } from 'writr';
686
+
687
+ const writr = new Writr('# My Content');
688
+ let lastError = null;
689
+
690
+ writr.on('error', (error) => {
691
+ lastError = error;
692
+ });
693
+
694
+ const result = await writr.validate();
695
+
696
+ if (!result.valid) {
697
+ console.log('Validation failed');
698
+ console.log('Error details:', lastError);
699
+ // result.error is also available
700
+ }
701
+ ```
702
+
703
+ **Example 3: File Operations Without Try/Catch**
704
+
705
+ ```javascript
706
+ import { Writr } from 'writr';
707
+
708
+ const writr = new Writr('# Content', { throwErrors: false });
709
+
710
+ writr.on('error', (error) => {
711
+ console.error('File operation failed:', error.message);
712
+ // Handle gracefully - maybe use default content
713
+ });
714
+
715
+ // Won't throw, but will emit error event if file doesn't exist
716
+ await writr.loadFromFile('./maybe-missing.md');
717
+ ```
718
+
719
+ ### Event Emitter Methods
720
+
721
+ Since Writr extends Hookified, you have access to standard event emitter methods:
722
+
723
+ - `writr.on(event, handler)` - Add an event listener
724
+ - `writr.once(event, handler)` - Add a one-time event listener
725
+ - `writr.off(event, handler)` - Remove an event listener
726
+ - `writr.emit(event, data)` - Emit an event (used internally)
727
+
728
+ For more information about event handling capabilities, see the [Hookified documentation](https://github.com/jaredwray/hookified).
729
+
479
730
  # ESM and Node Version Support
480
731
 
481
732
  This package is ESM only and tested on the current lts version and its previous. Please don't open issues for questions regarding CommonJS / ESM or previous Nodejs versions.
package/dist/writr.d.ts CHANGED
@@ -16,6 +16,13 @@ declare class WritrCache {
16
16
  set(markdown: string, value: string, options?: RenderOptions): void;
17
17
  clear(): void;
18
18
  hash(markdown: string, options?: RenderOptions): string;
19
+ /**
20
+ * Sanitizes render options to only include serializable properties for caching.
21
+ * This prevents issues with structuredClone when options contain Promises, functions, or circular references.
22
+ * @param {RenderOptions} [options] The render options to sanitize
23
+ * @returns {RenderOptions | undefined} A new object with only the known RenderOptions properties
24
+ */
25
+ private sanitizeOptions;
19
26
  }
20
27
 
21
28
  /**
@@ -37,8 +44,8 @@ type WritrOptions = {
37
44
  * @property {boolean} [highlight] - Code highlighting (default: true)
38
45
  * @property {boolean} [gfm] - Github flavor markdown (default: true)
39
46
  * @property {boolean} [math] - Math support (default: true)
40
- * @property {boolean} [mdx] - MDX support (default: true)
41
- * @property {boolean} [caching] - Caching (default: false)
47
+ * @property {boolean} [mdx] - MDX support (default: false)
48
+ * @property {boolean} [caching] - Caching (default: true)
42
49
  */
43
50
  type RenderOptions = {
44
51
  emoji?: boolean;
@@ -208,9 +215,9 @@ declare class Writr extends Hookified {
208
215
  * @returns {void}
209
216
  */
210
217
  saveToFileSync(filePath: string): void;
218
+ mergeOptions(current: WritrOptions, options: WritrOptions): WritrOptions;
211
219
  private isCacheEnabled;
212
220
  private createProcessor;
213
- private mergeOptions;
214
221
  private mergeRenderOptions;
215
222
  }
216
223
 
package/dist/writr.js CHANGED
@@ -10,6 +10,7 @@ import rehypeSlug from "rehype-slug";
10
10
  import rehypeStringify from "rehype-stringify";
11
11
  import remarkEmoji from "remark-emoji";
12
12
  import remarkGfm from "remark-gfm";
13
+ import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
13
14
  import remarkMath from "remark-math";
14
15
  import remarkMDX from "remark-mdx";
15
16
  import remarkParse from "remark-parse";
@@ -18,11 +19,12 @@ import remarkToc from "remark-toc";
18
19
  import { unified } from "unified";
19
20
 
20
21
  // src/writr-cache.ts
21
- import { Cacheable, CacheableMemory } from "cacheable";
22
+ import { CacheableMemory } from "cacheable";
23
+ import { Hashery } from "hashery";
22
24
  var WritrCache = class {
23
25
  _store = new CacheableMemory();
24
26
  _hashStore = new CacheableMemory();
25
- _hash = new Cacheable();
27
+ _hash = new Hashery();
26
28
  get store() {
27
29
  return this._store;
28
30
  }
@@ -42,15 +44,53 @@ var WritrCache = class {
42
44
  this._hashStore.clear();
43
45
  }
44
46
  hash(markdown, options) {
45
- const content = { markdown, options };
47
+ const sanitizedOptions = this.sanitizeOptions(options);
48
+ const content = { markdown, options: sanitizedOptions };
46
49
  const key = JSON.stringify(content);
47
- let result = this._hashStore.get(key);
50
+ const result = this._hashStore.get(key);
48
51
  if (result) {
49
52
  return result;
50
53
  }
51
- result = this._hash.hash(content);
52
- this._hashStore.set(key, result);
53
- return result;
54
+ const hash = this._hash.toHashSync(content);
55
+ this._hashStore.set(key, hash);
56
+ return hash;
57
+ }
58
+ /**
59
+ * Sanitizes render options to only include serializable properties for caching.
60
+ * This prevents issues with structuredClone when options contain Promises, functions, or circular references.
61
+ * @param {RenderOptions} [options] The render options to sanitize
62
+ * @returns {RenderOptions | undefined} A new object with only the known RenderOptions properties
63
+ */
64
+ sanitizeOptions(options) {
65
+ if (!options) {
66
+ return void 0;
67
+ }
68
+ const sanitized = {};
69
+ if (options.emoji !== void 0) {
70
+ sanitized.emoji = options.emoji;
71
+ }
72
+ if (options.toc !== void 0) {
73
+ sanitized.toc = options.toc;
74
+ }
75
+ if (options.slug !== void 0) {
76
+ sanitized.slug = options.slug;
77
+ }
78
+ if (options.highlight !== void 0) {
79
+ sanitized.highlight = options.highlight;
80
+ }
81
+ if (options.gfm !== void 0) {
82
+ sanitized.gfm = options.gfm;
83
+ }
84
+ if (options.math !== void 0) {
85
+ sanitized.math = options.math;
86
+ }
87
+ if (options.mdx !== void 0) {
88
+ sanitized.mdx = options.mdx;
89
+ }
90
+ if (options.caching !== void 0) {
91
+ sanitized.caching = options.caching;
92
+ }
93
+ return sanitized;
54
94
  }
55
95
  };
56
96
 
@@ -64,7 +104,7 @@ var WritrHooks = /* @__PURE__ */ ((WritrHooks2) => {
64
104
  return WritrHooks2;
65
105
  })(WritrHooks || {});
66
106
  var Writr = class extends Hookified {
67
- engine = unified().use(remarkParse).use(remarkGfm).use(remarkToc).use(remarkEmoji).use(remarkRehype).use(rehypeSlug).use(remarkMath).use(rehypeKatex).use(rehypeHighlight).use(remarkMDX).use(rehypeStringify);
107
+ engine = unified().use(remarkParse).use(remarkGfm).use(remarkToc).use(remarkEmoji).use(remarkRehype).use(rehypeSlug).use(remarkMath).use(rehypeKatex).use(rehypeHighlight).use(rehypeStringify);
68
108
  // Stringify HTML
69
109
  _options = {
70
110
  throwErrors: false,
@@ -75,8 +115,8 @@ var Writr = class extends Hookified {
75
115
  highlight: true,
76
116
  gfm: true,
77
117
  math: true,
78
- mdx: true,
79
- caching: false
118
+ mdx: false,
119
+ caching: true
80
120
  }
81
121
  };
82
122
  _content = "";
@@ -191,12 +231,16 @@ var Writr = class extends Hookified {
191
231
  */
192
232
  // biome-ignore lint/suspicious/noExplicitAny: expected
193
233
  set frontMatter(data) {
194
- const frontMatter = this.frontMatterRaw;
195
- const yamlString = yaml.dump(data);
196
- const newFrontMatter = `---
234
+ try {
235
+ const frontMatter = this.frontMatterRaw;
236
+ const yamlString = yaml.dump(data);
237
+ const newFrontMatter = `---
197
238
  ${yamlString}---
198
239
  `;
199
- this._content = this._content.replace(frontMatter, newFrontMatter);
240
+ this._content = this._content.replace(frontMatter, newFrontMatter);
241
+ } catch (error) {
242
+ this.emit("error", error);
243
+ }
200
244
  }
201
245
  /**
202
246
  * Get the front matter value for a key.
@@ -245,6 +289,7 @@ ${yamlString}---
245
289
  await this.hook("afterRender" /* afterRender */, resultData);
246
290
  return resultData.result;
247
291
  } catch (error) {
292
+ this.emit("error", error);
248
293
  throw new Error(`Failed to render markdown: ${error.message}`);
249
294
  }
250
295
  }
@@ -287,6 +332,7 @@ ${yamlString}---
287
332
  this.hook("afterRender" /* afterRender */, resultData);
288
333
  return resultData.result;
289
334
  } catch (error) {
335
+ this.emit("error", error);
290
336
  throw new Error(`Failed to render markdown: ${error.message}`);
291
337
  }
292
338
  }
@@ -317,6 +363,7 @@ ${yamlString}---
317
363
  }
318
364
  return { valid: true };
319
365
  } catch (error) {
366
+ this.emit("error", error);
320
367
  if (content !== void 0) {
321
368
  this._content = originalContent;
322
369
  }
@@ -350,6 +397,7 @@ ${yamlString}---
350
397
  }
351
398
  return { valid: true };
352
399
  } catch (error) {
400
+ this.emit("error", error);
353
401
  if (content !== void 0) {
354
402
  this._content = originalContent;
355
403
  }
@@ -410,8 +458,13 @@ ${yamlString}---
410
458
  * @returns {Promise<string | React.JSX.Element | React.JSX.Element[]>} The rendered React content.
411
459
  */
412
460
  async renderReact(options, reactParseOptions) {
413
- const html = await this.render(options);
414
- return parse(html, reactParseOptions);
461
+ try {
462
+ const html = await this.render(options);
463
+ return parse(html, reactParseOptions);
464
+ } catch (error) {
465
+ this.emit("error", error);
466
+ throw new Error(`Failed to render React: ${error.message}`);
467
+ }
415
468
  }
416
469
  /**
417
470
  * Render the markdown content to React synchronously.
@@ -420,8 +473,13 @@ ${yamlString}---
420
473
  * @returns {string | React.JSX.Element | React.JSX.Element[]} The rendered React content.
421
474
  */
422
475
  renderReactSync(options, reactParseOptions) {
423
- const html = this.renderSync(options);
424
- return parse(html, reactParseOptions);
476
+ try {
477
+ const html = this.renderSync(options);
478
+ return parse(html, reactParseOptions);
479
+ } catch (error) {
480
+ this.emit("error", error);
481
+ throw new Error(`Failed to render React: ${error.message}`);
482
+ }
425
483
  }
426
484
  /**
427
485
  * Load markdown content from a file.
@@ -509,6 +567,16 @@ ${yamlString}---
509
567
  }
510
568
  }
511
569
  }
570
+ mergeOptions(current, options) {
571
+ if (options.throwErrors !== void 0) {
572
+ current.throwErrors = options.throwErrors;
573
+ }
574
+ if (options.renderOptions) {
575
+ current.renderOptions ??= {};
576
+ this.mergeRenderOptions(current.renderOptions, options.renderOptions);
577
+ }
578
+ return current;
579
+ }
512
580
  isCacheEnabled(options) {
513
581
  if (options?.caching !== void 0) {
514
582
  return options.caching;
@@ -520,6 +588,7 @@ ${yamlString}---
520
588
  const processor = unified().use(remarkParse);
521
589
  if (options.gfm) {
522
590
  processor.use(remarkGfm);
591
+ processor.use(remarkGithubBlockquoteAlert);
523
592
  }
524
593
  if (options.toc) {
525
594
  processor.use(remarkToc, { heading: "toc|table of contents" });
@@ -543,16 +612,6 @@ ${yamlString}---
543
612
  processor.use(rehypeStringify);
544
613
  return processor;
545
614
  }
546
- mergeOptions(current, options) {
547
- if (options.throwErrors !== void 0) {
548
- current.throwErrors = options.throwErrors;
549
- }
550
- if (options.renderOptions) {
551
- current.renderOptions ??= {};
552
- this.mergeRenderOptions(current.renderOptions, options.renderOptions);
553
- }
554
- return current;
555
- }
556
615
  mergeRenderOptions(current, options) {
557
616
  if (options.emoji !== void 0) {
558
617
  current.emoji = options.emoji;
@@ -585,3 +644,4 @@ export {
585
644
  Writr,
586
645
  WritrHooks
587
646
  };
647
+ /* v8 ignore next -- @preserve */
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "writr",
3
- "version": "4.5.1",
3
+ "version": "5.0.1",
4
4
  "description": "Markdown Rendering Simplified",
5
5
  "type": "module",
6
6
  "main": "./dist/writr.js",
7
7
  "types": "./dist/writr.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/writr.d.ts",
10
11
  "import": "./dist/writr.js"
11
12
  }
12
13
  },
13
- "repository": "https://github.com/jaredwray/writr.git",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/jaredwray/writr.git"
17
+ },
14
18
  "author": "Jared Wray <me@jaredwray.com>",
15
19
  "engines": {
16
20
  "node": ">=20"
@@ -44,28 +48,20 @@
44
48
  "react-markdown",
45
49
  "markdown-to-react"
46
50
  ],
47
- "scripts": {
48
- "clean": "rimraf ./dist ./coverage ./node_modules ./pnpm-lock.yaml ./site/README.md ./site/dist",
49
- "build": "rimraf ./dist && tsup src/writr.ts --format esm --dts --clean",
50
- "prepare": "pnpm build",
51
- "lint": "biome check --write --error-on-warnings",
52
- "test": "pnpm lint && vitest run --coverage",
53
- "test:ci": "biome check --error-on-warnings && vitest run --coverage",
54
- "website:build": "rimraf ./site/README.md ./site/dist && npx docula build -s ./site -o ./site/dist",
55
- "website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist"
56
- },
57
51
  "dependencies": {
58
- "cacheable": "^2.0.2",
59
- "hookified": "^1.12.1",
60
- "html-react-parser": "^5.2.6",
61
- "js-yaml": "^4.1.0",
62
- "react": "^19.1.1",
52
+ "cacheable": "^2.2.0",
53
+ "hashery": "^1.2.0",
54
+ "hookified": "^1.13.0",
55
+ "html-react-parser": "^5.2.10",
56
+ "js-yaml": "^4.1.1",
57
+ "react": "^19.2.0",
63
58
  "rehype-highlight": "^7.0.2",
64
59
  "rehype-katex": "^7.0.1",
65
60
  "rehype-slug": "^6.0.0",
66
61
  "rehype-stringify": "^10.0.1",
67
62
  "remark-emoji": "^5.0.2",
68
63
  "remark-gfm": "^4.0.1",
64
+ "remark-github-blockquote-alert": "^2.0.0",
69
65
  "remark-math": "^6.0.0",
70
66
  "remark-mdx": "^3.1.1",
71
67
  "remark-parse": "^11.0.0",
@@ -74,27 +70,29 @@
74
70
  "unified": "^11.0.5"
75
71
  },
76
72
  "devDependencies": {
77
- "@biomejs/biome": "^2.2.4",
73
+ "@biomejs/biome": "^2.3.7",
78
74
  "@types/js-yaml": "^4.0.9",
79
- "@types/node": "^24.5.2",
80
- "@types/react": "^19.1.13",
81
- "@vitest/coverage-v8": "^3.2.4",
82
- "docula": "^0.30.0",
83
- "rimraf": "^6.0.1",
84
- "ts-node": "^10.9.2",
85
- "tsup": "^8.5.0",
86
- "typescript": "^5.9.2",
87
- "vitest": "^3.2.4",
88
- "webpack": "^5.101.3"
89
- },
90
- "xo": {
91
- "ignores": [
92
- "docula.config.*"
93
- ]
75
+ "@types/node": "^24.10.1",
76
+ "@types/react": "^19.2.7",
77
+ "@vitest/coverage-v8": "^4.0.14",
78
+ "docula": "^0.31.1",
79
+ "rimraf": "^6.1.2",
80
+ "tsup": "^8.5.1",
81
+ "typescript": "^5.9.3",
82
+ "vitest": "^4.0.14"
94
83
  },
95
84
  "files": [
96
85
  "dist",
97
86
  "README.md",
98
87
  "LICENSE"
99
- ]
100
- }
88
+ ],
89
+ "scripts": {
90
+ "clean": "rimraf ./dist ./coverage ./node_modules ./pnpm-lock.yaml ./site/README.md ./site/dist",
91
+ "build": "rimraf ./dist && tsup src/writr.ts --format esm --dts --clean",
92
+ "lint": "biome check --write --error-on-warnings",
93
+ "test": "pnpm lint && vitest run --coverage",
94
+ "test:ci": "biome check --error-on-warnings && vitest run --coverage",
95
+ "website:build": "rimraf ./site/README.md ./site/dist && npx docula build -s ./site -o ./site/dist",
96
+ "website:serve": "rimraf ./site/README.md ./site/dist && npx docula serve -s ./site -o ./site/dist"
97
+ }
98
+ }