defuddle-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,603 @@
1
+ import TurndownService from 'turndown';
2
+
3
+ const footnotes: { [key: string]: string } = {};
4
+
5
+ export function createMarkdownContent(content: string, url: string) {
6
+ const turndownService = new TurndownService({
7
+ headingStyle: 'atx',
8
+ hr: '---',
9
+ bulletListMarker: '-',
10
+ codeBlockStyle: 'fenced',
11
+ emDelimiter: '*',
12
+ preformattedCode: true,
13
+ });
14
+
15
+ turndownService.addRule('table', {
16
+ filter: 'table',
17
+ replacement: function(content, node) {
18
+ if (!(node instanceof HTMLTableElement)) return content;
19
+
20
+ // Check if it's an ArXiv equation table
21
+ if (node.classList.contains('ltx_equation') || node.classList.contains('ltx_eqn_table')) {
22
+ return handleNestedEquations(node);
23
+ }
24
+
25
+ // Check if the table has colspan or rowspan
26
+ const hasComplexStructure = Array.from(node.querySelectorAll('td, th')).some(cell =>
27
+ cell.hasAttribute('colspan') || cell.hasAttribute('rowspan')
28
+ );
29
+
30
+ if (hasComplexStructure) {
31
+ // Clean up the table HTML
32
+ const cleanedTable = cleanupTableHTML(node);
33
+ return '\n\n' + cleanedTable + '\n\n';
34
+ }
35
+
36
+ // Process simple tables as before
37
+ const rows = Array.from(node.rows).map(row => {
38
+ const cells = Array.from(row.cells).map(cell => {
39
+ // Remove newlines and trim the content
40
+ let cellContent = turndownService.turndown(cell.innerHTML)
41
+ .replace(/\n/g, ' ')
42
+ .trim();
43
+ // Escape pipe characters
44
+ cellContent = cellContent.replace(/\|/g, '\\|');
45
+ return cellContent;
46
+ });
47
+ return `| ${cells.join(' | ')} |`;
48
+ });
49
+
50
+ // Create the separator row
51
+ const separatorRow = `| ${Array(rows[0].split('|').length - 2).fill('---').join(' | ')} |`;
52
+
53
+ // Combine all rows
54
+ const tableContent = [rows[0], separatorRow, ...rows.slice(1)].join('\n');
55
+
56
+ return `\n\n${tableContent}\n\n`;
57
+ }
58
+ });
59
+
60
+ turndownService.remove(['style', 'script']);
61
+
62
+ // Keep iframes, video, audio, sup, and sub elements
63
+ // @ts-ignore
64
+ turndownService.keep(['iframe', 'video', 'audio', 'sup', 'sub', 'svg', 'math']);
65
+ turndownService.remove(['button']);
66
+
67
+ turndownService.addRule('list', {
68
+ filter: ['ul', 'ol'],
69
+ replacement: function (content: string, node: Node) {
70
+ // Remove trailing newlines/spaces from content
71
+ content = content.trim();
72
+
73
+ // Add a newline before the list if it's a top-level list
74
+ const isTopLevel = !(node.parentNode && (node.parentNode.nodeName === 'UL' || node.parentNode.nodeName === 'OL'));
75
+ return (isTopLevel ? '\n' : '') + content + '\n';
76
+ }
77
+ });
78
+
79
+ // Lists with tab indentation
80
+ turndownService.addRule('listItem', {
81
+ filter: 'li',
82
+ replacement: function (content: string, node: Node, options: TurndownService.Options) {
83
+ if (!(node instanceof HTMLElement)) return content;
84
+
85
+ // Handle task list items
86
+ const isTaskListItem = node.classList.contains('task-list-item');
87
+ const checkbox = node.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
88
+ let taskListMarker = '';
89
+
90
+ if (isTaskListItem && checkbox) {
91
+ // Remove the checkbox from content since we'll add markdown checkbox
92
+ content = content.replace(/<input[^>]*>/, '');
93
+ taskListMarker = checkbox.checked ? '[x] ' : '[ ] ';
94
+ }
95
+
96
+ content = content
97
+ // Remove trailing newlines
98
+ .replace(/\n+$/, '')
99
+ // Split into lines
100
+ .split('\n')
101
+ // Remove empty lines
102
+ .filter(line => line.length > 0)
103
+ // Add indentation to continued lines
104
+ .join('\n\t');
105
+
106
+ let prefix = options.bulletListMarker + ' ';
107
+ let parent = node.parentNode;
108
+
109
+ // Calculate the nesting level
110
+ let level = 0;
111
+ let currentParent = node.parentNode;
112
+ while (currentParent && (currentParent.nodeName === 'UL' || currentParent.nodeName === 'OL')) {
113
+ level++;
114
+ currentParent = currentParent.parentNode;
115
+ }
116
+
117
+ // Add tab indentation based on nesting level, ensuring it's never negative
118
+ const indentLevel = Math.max(0, level - 1);
119
+ prefix = '\t'.repeat(indentLevel) + prefix;
120
+
121
+ if (parent instanceof HTMLOListElement) {
122
+ let start = parent.getAttribute('start');
123
+ let index = Array.from(parent.children).indexOf(node as HTMLElement) + 1;
124
+ prefix = '\t'.repeat(level - 1) + (start ? Number(start) + index - 1 : index) + '. ';
125
+ }
126
+
127
+ return prefix + taskListMarker + content.trim() + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
128
+ }
129
+ });
130
+
131
+ turndownService.addRule('figure', {
132
+ filter: 'figure',
133
+ replacement: function(content, node) {
134
+ const figure = node as HTMLElement;
135
+ const img = figure.querySelector('img');
136
+ const figcaption = figure.querySelector('figcaption');
137
+
138
+ if (!img) return content;
139
+
140
+ const alt = img.getAttribute('alt') || '';
141
+ const src = img.getAttribute('src') || '';
142
+ let caption = '';
143
+
144
+ if (figcaption) {
145
+ const tagSpan = figcaption.querySelector('.ltx_tag_figure');
146
+ const tagText = tagSpan ? tagSpan.textContent?.trim() : '';
147
+
148
+ // Process the caption content, including math elements
149
+ let captionContent = figcaption.innerHTML;
150
+ captionContent = captionContent.replace(/<math.*?>(.*?)<\/math>/g, (match, mathContent, offset, string) => {
151
+ const mathElement = new DOMParser().parseFromString(match, 'text/html').body.firstChild as Element;
152
+ const latex = extractLatex(mathElement);
153
+ const prevChar = string[offset - 1] || '';
154
+ const nextChar = string[offset + match.length] || '';
155
+
156
+ const isStartOfLine = offset === 0 || /\s/.test(prevChar);
157
+ const isEndOfLine = offset + match.length === string.length || /\s/.test(nextChar);
158
+
159
+ const leftSpace = (!isStartOfLine && !/[\s$]/.test(prevChar)) ? ' ' : '';
160
+ const rightSpace = (!isEndOfLine && !/[\s$]/.test(nextChar)) ? ' ' : '';
161
+
162
+ return `${leftSpace}$${latex}$${rightSpace}`;
163
+ });
164
+
165
+ // Convert the processed caption content to markdown
166
+ const captionMarkdown = turndownService.turndown(captionContent);
167
+
168
+ // Combine tag and processed caption
169
+ caption = `${tagText} ${captionMarkdown}`.trim();
170
+ }
171
+
172
+ // Handle references in the caption
173
+ caption = caption.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
174
+ return `[${text}](${href})`;
175
+ });
176
+
177
+ return `![${alt}](${src})\n\n${caption}\n\n`;
178
+ }
179
+ });
180
+
181
+ // Use Obsidian format for YouTube embeds and tweets
182
+ turndownService.addRule('embedToMarkdown', {
183
+ filter: function (node: Node): boolean {
184
+ if (node instanceof HTMLIFrameElement) {
185
+ const src = node.getAttribute('src');
186
+ return !!src && (
187
+ !!src.match(/(?:youtube\.com|youtu\.be)/) ||
188
+ !!src.match(/(?:twitter\.com|x\.com)/)
189
+ );
190
+ }
191
+ return false;
192
+ },
193
+ replacement: function (content: string, node: Node): string {
194
+ if (node instanceof HTMLIFrameElement) {
195
+ const src = node.getAttribute('src');
196
+ if (src) {
197
+ const youtubeMatch = src.match(/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:embed\/|watch\?v=)?([a-zA-Z0-9_-]+)/);
198
+ if (youtubeMatch && youtubeMatch[1]) {
199
+ return `![](https://www.youtube.com/watch?v=${youtubeMatch[1]})`;
200
+ }
201
+ const tweetMatch = src.match(/(?:twitter\.com|x\.com)\/.*?(?:status|statuses)\/(\d+)/);
202
+ if (tweetMatch && tweetMatch[1]) {
203
+ return `![](https://x.com/i/status/${tweetMatch[1]})`;
204
+ }
205
+ }
206
+ }
207
+ return content;
208
+ }
209
+ });
210
+
211
+ turndownService.addRule('highlight', {
212
+ filter: 'mark',
213
+ replacement: function(content) {
214
+ return '==' + content + '==';
215
+ }
216
+ });
217
+
218
+ turndownService.addRule('strikethrough', {
219
+ filter: (node: Node) =>
220
+ node.nodeName === 'DEL' ||
221
+ node.nodeName === 'S' ||
222
+ node.nodeName === 'STRIKE',
223
+ replacement: function(content) {
224
+ return '~~' + content + '~~';
225
+ }
226
+ });
227
+
228
+ // Add a new custom rule for complex link structures
229
+ turndownService.addRule('complexLinkStructure', {
230
+ filter: function (node, options) {
231
+ return (
232
+ node.nodeName === 'A' &&
233
+ node.childNodes.length > 1 &&
234
+ Array.from(node.childNodes).some(child => ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(child.nodeName))
235
+ );
236
+ },
237
+ replacement: function (content, node, options) {
238
+ if (!(node instanceof HTMLElement)) return content;
239
+ const href = node.getAttribute('href');
240
+ const title = node.getAttribute('title');
241
+
242
+ // Extract the heading
243
+ const headingNode = node.querySelector('h1, h2, h3, h4, h5, h6');
244
+ const headingContent = headingNode ? turndownService.turndown(headingNode.innerHTML) : '';
245
+
246
+ // Remove the heading from the content
247
+ if (headingNode) {
248
+ headingNode.remove();
249
+ }
250
+
251
+ // Convert the remaining content
252
+ const remainingContent = turndownService.turndown(node.innerHTML);
253
+
254
+ // Construct the new markdown
255
+ let markdown = `${headingContent}\n\n${remainingContent}\n\n`;
256
+ if (href) {
257
+ markdown += `[View original](${href})`;
258
+ if (title) {
259
+ markdown += ` "${title}"`;
260
+ }
261
+ }
262
+
263
+ return markdown;
264
+ }
265
+ });
266
+
267
+ turndownService.addRule('arXivEnumerate', {
268
+ filter: (node) => {
269
+ return node.nodeName === 'OL' && node.classList.contains('ltx_enumerate');
270
+ },
271
+ replacement: function(content, node) {
272
+ if (!(node instanceof HTMLElement)) return content;
273
+
274
+ const items = Array.from(node.children).map((item, index) => {
275
+ if (item instanceof HTMLElement) {
276
+ const itemContent = item.innerHTML.replace(/^<span class="ltx_tag ltx_tag_item">\d+\.<\/span>\s*/, '');
277
+ return `${index + 1}. ${turndownService.turndown(itemContent)}`;
278
+ }
279
+ return '';
280
+ });
281
+
282
+ return '\n\n' + items.join('\n\n') + '\n\n';
283
+ }
284
+ });
285
+
286
+ turndownService.addRule('removeHiddenElements', {
287
+ filter: function (node) {
288
+ return (
289
+ node.style.display === 'none'
290
+ );
291
+ },
292
+ replacement: function () {
293
+ return '';
294
+ }
295
+ });
296
+
297
+ turndownService.addRule('citations', {
298
+ filter: (node: Node): boolean => {
299
+ if (node instanceof Element) {
300
+ return (
301
+ (node.nodeName === 'SUP' && node.id.startsWith('fnref:'))
302
+ );
303
+ }
304
+ return false;
305
+ },
306
+ replacement: (content, node) => {
307
+ if (node instanceof HTMLElement) {
308
+ if (node.nodeName === 'SUP' && node.id.startsWith('fnref:')) {
309
+ const id = node.id.replace('fnref:', '');
310
+ // Extract only the primary number before any hyphen
311
+ const primaryNumber = id.split('-')[0];
312
+ return `[^${primaryNumber}]`;
313
+ }
314
+ }
315
+ return content;
316
+ }
317
+ });
318
+
319
+ // Footnotes list
320
+ turndownService.addRule('footnotesList', {
321
+ filter: (node: Node): boolean => {
322
+ if (node instanceof HTMLOListElement) {
323
+ return (
324
+ node.parentElement?.id === 'footnotes'
325
+ );
326
+ }
327
+ return false;
328
+ },
329
+ replacement: (content, node) => {
330
+ if (node instanceof HTMLElement) {
331
+ const references = Array.from(node.children).map(li => {
332
+ let id;
333
+ if (li.id.startsWith('fn:')) {
334
+ id = li.id.replace('fn:', '');
335
+ } else {
336
+ const match = li.id.split('/').pop()?.match(/cite_note-(.+)/);
337
+ id = match ? match[1] : li.id;
338
+ }
339
+
340
+ // Remove the leading sup element if its content matches the footnote id
341
+ const supElement = li.querySelector('sup');
342
+ if (supElement && supElement.textContent?.trim() === id) {
343
+ supElement.remove();
344
+ }
345
+
346
+ const referenceContent = turndownService.turndown(li.innerHTML);
347
+ // Remove the backlink from the footnote content
348
+ const cleanedContent = referenceContent.replace(/\s*↩︎$/, '').trim();
349
+ return `[^${id.toLowerCase()}]: ${cleanedContent}`;
350
+ });
351
+ return '\n\n' + references.join('\n\n') + '\n\n';
352
+ }
353
+ return content;
354
+ }
355
+ });
356
+
357
+ // General removal rules for varous website elements
358
+ turndownService.addRule('removals', {
359
+ filter: function (node) {
360
+ if (!(node instanceof HTMLElement)) return false;
361
+ // Remove the Defuddle backlink from the footnote content
362
+ if (node.getAttribute('href')?.includes('#fnref')) return true;
363
+ if (node.classList.contains('footnote-backref')) return true;
364
+ return false;
365
+ },
366
+ replacement: function (content, node) {
367
+ return '';
368
+ }
369
+ });
370
+
371
+ turndownService.addRule('handleTextNodesInTables', {
372
+ filter: function (node: Node): boolean {
373
+ return node.nodeType === Node.TEXT_NODE &&
374
+ node.parentNode !== null &&
375
+ node.parentNode.nodeName === 'TD';
376
+ },
377
+ replacement: function (content: string): string {
378
+ return content;
379
+ }
380
+ });
381
+
382
+ turndownService.addRule('preformattedCode', {
383
+ filter: (node) => {
384
+ return node.nodeName === 'PRE';
385
+ },
386
+ replacement: (content, node) => {
387
+ if (!(node instanceof HTMLElement)) return content;
388
+
389
+ const codeElement = node.querySelector('code');
390
+ if (!codeElement) return content;
391
+
392
+ const language = codeElement.getAttribute('data-lang') || '';
393
+ const code = codeElement.textContent || '';
394
+
395
+ // Clean up the content and escape backticks
396
+ const cleanCode = code
397
+ .trim()
398
+ .replace(/`/g, '\\`');
399
+
400
+ return `\n\`\`\`${language}\n${cleanCode}\n\`\`\`\n`;
401
+ }
402
+ });
403
+
404
+ turndownService.addRule('math', {
405
+ filter: (node) => {
406
+ return node.nodeName.toLowerCase() === 'math' ||
407
+ (node instanceof Element && node.classList &&
408
+ (node.classList.contains('mwe-math-element') ||
409
+ node.classList.contains('mwe-math-fallback-image-inline') ||
410
+ node.classList.contains('mwe-math-fallback-image-display')));
411
+ },
412
+ replacement: (content, node) => {
413
+ if (!(node instanceof Element)) return content;
414
+
415
+ let latex = extractLatex(node);
416
+
417
+ // Remove leading and trailing whitespace
418
+ latex = latex.trim();
419
+
420
+ // Check if the math element is within a table
421
+ const isInTable = node.closest('table') !== null;
422
+
423
+ // Check if it's an inline or block math element
424
+ if (!isInTable && (
425
+ node.getAttribute('display') === 'block' ||
426
+ node.classList.contains('mwe-math-fallback-image-display') ||
427
+ (node.parentElement && node.parentElement.classList.contains('mwe-math-element') &&
428
+ node.parentElement.previousElementSibling &&
429
+ node.parentElement.previousElementSibling.nodeName.toLowerCase() === 'p')
430
+ )) {
431
+ return `\n$$\n${latex}\n$$\n`;
432
+ } else {
433
+ // For inline math, ensure there's a space before and after only if needed
434
+ const prevNode = node.previousSibling;
435
+ const nextNode = node.nextSibling;
436
+ const prevChar = prevNode?.textContent?.slice(-1) || '';
437
+ const nextChar = nextNode?.textContent?.[0] || '';
438
+
439
+ const isStartOfLine = !prevNode || (prevNode.nodeType === Node.TEXT_NODE && prevNode.textContent?.trim() === '');
440
+ const isEndOfLine = !nextNode || (nextNode.nodeType === Node.TEXT_NODE && nextNode.textContent?.trim() === '');
441
+
442
+ const leftSpace = (!isStartOfLine && prevChar && !/[\s$]/.test(prevChar)) ? ' ' : '';
443
+ const rightSpace = (!isEndOfLine && nextChar && !/[\s$]/.test(nextChar)) ? ' ' : '';
444
+
445
+ return `${leftSpace}$${latex}$${rightSpace}`;
446
+ }
447
+ }
448
+ });
449
+
450
+ turndownService.addRule('katex', {
451
+ filter: (node) => {
452
+ return node instanceof HTMLElement &&
453
+ (node.classList.contains('math') || node.classList.contains('katex'));
454
+ },
455
+ replacement: (content, node) => {
456
+ if (!(node instanceof HTMLElement)) return content;
457
+
458
+ // Try to find the original LaTeX content
459
+ // 1. Check data-latex attribute
460
+ let latex = node.getAttribute('data-latex');
461
+
462
+ // 2. If no data-latex, try to get from .katex-mathml
463
+ if (!latex) {
464
+ const mathml = node.querySelector('.katex-mathml annotation[encoding="application/x-tex"]');
465
+ latex = mathml?.textContent || '';
466
+ }
467
+
468
+ // 3. If still no content, use text content as fallback
469
+ if (!latex) {
470
+ latex = node.textContent?.trim() || '';
471
+ }
472
+
473
+ // Determine if it's an inline formula
474
+ const mathElement = node.querySelector('.katex-mathml math');
475
+ const isInline = node.classList.contains('math-inline') ||
476
+ (mathElement && mathElement.getAttribute('display') !== 'block');
477
+
478
+ if (isInline) {
479
+ return `$${latex}$`;
480
+ } else {
481
+ return `\n$$\n${latex}\n$$\n`;
482
+ }
483
+ }
484
+ });
485
+
486
+ turndownService.addRule('callout', {
487
+ filter: (node) => {
488
+ return (
489
+ node.nodeName.toLowerCase() === 'div' &&
490
+ node.classList.contains('markdown-alert')
491
+ );
492
+ },
493
+ replacement: (content, node) => {
494
+ const element = node as HTMLElement;
495
+
496
+ // Get alert type from the class (e.g., markdown-alert-note -> NOTE)
497
+ const alertClasses = Array.from(element.classList);
498
+ const typeClass = alertClasses.find(c => c.startsWith('markdown-alert-') && c !== 'markdown-alert');
499
+ const type = typeClass ? typeClass.replace('markdown-alert-', '').toUpperCase() : 'NOTE';
500
+
501
+ // Find the title element and content
502
+ const titleElement = element.querySelector('.markdown-alert-title');
503
+ const contentElement = element.querySelector('p:not(.markdown-alert-title)');
504
+
505
+ // Extract content, removing the title from it if present
506
+ let alertContent = content;
507
+ if (titleElement && titleElement.textContent) {
508
+ alertContent = contentElement?.textContent || content.replace(titleElement.textContent, '');
509
+ }
510
+
511
+ // Format as Obsidian callout
512
+ return `\n> [!${type}]\n> ${alertContent.trim().replace(/\n/g, '\n> ')}\n`;
513
+ }
514
+ });
515
+
516
+ function handleNestedEquations(table: Element): string {
517
+ const mathElements = table.querySelectorAll('math[alttext]');
518
+ if (mathElements.length === 0) return '';
519
+
520
+ return Array.from(mathElements).map(mathElement => {
521
+ const alttext = mathElement.getAttribute('alttext');
522
+ if (alttext) {
523
+ // Check if it's an inline or block equation
524
+ const isInline = mathElement.closest('.ltx_eqn_inline') !== null;
525
+ return isInline ? `$${alttext.trim()}$` : `\n$$\n${alttext.trim()}\n$$`;
526
+ }
527
+ return '';
528
+ }).join('\n\n');
529
+ }
530
+
531
+ function cleanupTableHTML(table: HTMLTableElement): string {
532
+ const allowedAttributes = ['src', 'href', 'style', 'align', 'width', 'height', 'rowspan', 'colspan', 'bgcolor', 'scope', 'valign', 'headers'];
533
+
534
+ const cleanElement = (element: Element) => {
535
+ Array.from(element.attributes).forEach(attr => {
536
+ if (!allowedAttributes.includes(attr.name)) {
537
+ element.removeAttribute(attr.name);
538
+ }
539
+ });
540
+
541
+ element.childNodes.forEach(child => {
542
+ if (child instanceof Element) {
543
+ cleanElement(child);
544
+ }
545
+ });
546
+ };
547
+
548
+ // Create a clone of the table to avoid modifying the original DOM
549
+ const tableClone = table.cloneNode(true) as HTMLTableElement;
550
+ cleanElement(tableClone);
551
+
552
+ return tableClone.outerHTML;
553
+ }
554
+
555
+ function extractLatex(element: Element): string {
556
+ // Check if the element is a <math> element and has an alttext attribute
557
+ if (element.nodeName.toLowerCase() === 'math') {
558
+ let latex = element.getAttribute('data-latex');
559
+ let alttext = element.getAttribute('alttext');
560
+ if (latex) {
561
+ return latex.trim();
562
+ } else if (alttext) {
563
+ return alttext.trim();
564
+ }
565
+ console.log('No latex or alttext found for math element:', element);
566
+ }
567
+ return ''; // Return empty string for non-math elements
568
+ }
569
+
570
+ try {
571
+ let markdown = turndownService.turndown(content);
572
+
573
+ // Remove the title from the beginning of the content if it exists
574
+ const titleMatch = markdown.match(/^# .+\n+/);
575
+ if (titleMatch) {
576
+ markdown = markdown.slice(titleMatch[0].length);
577
+ }
578
+
579
+ // Remove any empty links e.g. [](example.com) that remain, along with surrounding newlines
580
+ // But don't affect image links like ![](image.jpg)
581
+ markdown = markdown.replace(/\n*(?<!!)\[]\([^)]+\)\n*/g, '');
582
+
583
+ // Remove any consecutive newlines more than two
584
+ markdown = markdown.replace(/\n{3,}/g, '\n\n');
585
+
586
+ // Append footnotes at the end of the document
587
+ if (Object.keys(footnotes).length > 0) {
588
+ markdown += '\n\n---\n\n';
589
+ for (const [id, content] of Object.entries(footnotes)) {
590
+ markdown += `[^${id}]: ${content}\n\n`;
591
+ }
592
+ }
593
+
594
+ // Clear the footnotes object for the next conversion
595
+ Object.keys(footnotes).forEach(key => delete footnotes[key]);
596
+
597
+ return markdown.trim();
598
+ } catch (error) {
599
+ console.error('Error converting HTML to Markdown:', error);
600
+ console.log('Problematic content:', content.substring(0, 1000) + '...');
601
+ return `Partial conversion completed with errors. Original HTML:\n\n${content}`;
602
+ }
603
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }