@paroicms/markdown-to-tiptap-json 0.2.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/README.md +112 -0
- package/dist/converter.d.ts +6 -0
- package/dist/converter.js +291 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @paroicms/markdown-to-tiptap-json
|
|
2
|
+
|
|
3
|
+
A lightweight JavaScript/TypeScript library for converting Markdown to Tiptap JSON format **without loading the Tiptap editor**. This package uses `markdown-it` for parsing, making it efficient and suitable for server-side operations where you need to convert Markdown to Tiptap JSON without the overhead of loading the full editor.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @paroicms/markdown-to-tiptap-json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { convertMarkdownToTiptap } from "@paroicms/markdown-to-tiptap-json";
|
|
15
|
+
|
|
16
|
+
const markdown = `# Hello World
|
|
17
|
+
|
|
18
|
+
This is a paragraph with **bold** and *italic* text.
|
|
19
|
+
|
|
20
|
+
- List item 1
|
|
21
|
+
- List item 2
|
|
22
|
+
- List item 3`;
|
|
23
|
+
|
|
24
|
+
const { result, issues } = convertMarkdownToTiptap(markdown);
|
|
25
|
+
|
|
26
|
+
console.log(JSON.stringify(result, null, 2));
|
|
27
|
+
// {
|
|
28
|
+
// "type": "doc",
|
|
29
|
+
// "content": [
|
|
30
|
+
// {
|
|
31
|
+
// "type": "heading",
|
|
32
|
+
// "attrs": { "level": 1 },
|
|
33
|
+
// "content": [{ "type": "text", "text": "Hello World" }]
|
|
34
|
+
// },
|
|
35
|
+
// {
|
|
36
|
+
// "type": "paragraph",
|
|
37
|
+
// "content": [
|
|
38
|
+
// { "type": "text", "text": "This is a paragraph with " },
|
|
39
|
+
// { "type": "text", "text": "bold", "marks": [{ "type": "bold" }] },
|
|
40
|
+
// { "type": "text", "text": " and " },
|
|
41
|
+
// { "type": "text", "text": "italic", "marks": [{ "type": "italic" }] },
|
|
42
|
+
// { "type": "text", "text": " text." }
|
|
43
|
+
// ]
|
|
44
|
+
// },
|
|
45
|
+
// {
|
|
46
|
+
// "type": "bulletList",
|
|
47
|
+
// "content": [
|
|
48
|
+
// { "type": "listItem", "content": [...] },
|
|
49
|
+
// { "type": "listItem", "content": [...] },
|
|
50
|
+
// { "type": "listItem", "content": [...] }
|
|
51
|
+
// ]
|
|
52
|
+
// }
|
|
53
|
+
// ]
|
|
54
|
+
// }
|
|
55
|
+
|
|
56
|
+
// Check for any issues during conversion
|
|
57
|
+
if (issues) {
|
|
58
|
+
console.warn("Conversion issues:", issues);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### `convertMarkdownToTiptap(markdown: string): ConversionResult`
|
|
65
|
+
|
|
66
|
+
Converts a Markdown string to Tiptap JSON format.
|
|
67
|
+
|
|
68
|
+
**Parameters:**
|
|
69
|
+
|
|
70
|
+
- `markdown`: The Markdown string to convert
|
|
71
|
+
|
|
72
|
+
**Returns:** `ConversionResult` object containing:
|
|
73
|
+
|
|
74
|
+
- `result`: The Tiptap JSON document (type `TiptapJsonValue`)
|
|
75
|
+
- `issues` (optional): Array of warning messages for skipped or problematic content
|
|
76
|
+
|
|
77
|
+
## Supported Markdown Features
|
|
78
|
+
|
|
79
|
+
### Block Elements
|
|
80
|
+
|
|
81
|
+
- **Headings**: `# H1` through `###### H6`
|
|
82
|
+
- **Paragraphs**: Regular text blocks
|
|
83
|
+
- **Blockquotes**: `> Quote text`
|
|
84
|
+
- **Code blocks**: Fenced (` ``` `) and indented
|
|
85
|
+
- **Horizontal rules**: `---`, `***`, or `___`
|
|
86
|
+
- **Lists**: Bullet (`-`, `*`, `+`) and ordered (`1.`, `2.`, etc.)
|
|
87
|
+
- **Nested lists**: Multi-level list structures
|
|
88
|
+
- **Tables**: GitHub-flavored markdown tables
|
|
89
|
+
|
|
90
|
+
### Inline Elements
|
|
91
|
+
|
|
92
|
+
- **Bold**: `**bold**` or `__bold__`
|
|
93
|
+
- **Italic**: `*italic*` or `_italic_`
|
|
94
|
+
- **Strikethrough**: `~~strikethrough~~`
|
|
95
|
+
- **Inline code**: `` `code` ``
|
|
96
|
+
- **Links**: `[text](url)`
|
|
97
|
+
- **Images**: ``
|
|
98
|
+
- **Hard breaks**: Two spaces at end of line
|
|
99
|
+
|
|
100
|
+
### Special Handling
|
|
101
|
+
|
|
102
|
+
- **HTML blocks and inline HTML**: Skipped with warnings in `issues` array
|
|
103
|
+
- **Empty documents**: Returns empty doc with no issues
|
|
104
|
+
- **Malformed markdown**: Returns minimal valid doc with error in `issues`
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
109
|
+
|
|
110
|
+
## Related Packages
|
|
111
|
+
|
|
112
|
+
This package can be used as a standalone library. It's also part of the ParoiCMS ecosystem. For more information, visit the [ParoiCMS repository](https://gitlab.com/paroi/opensource/paroicms).
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
export function convertMarkdownToTiptap(markdown) {
|
|
3
|
+
if (markdown === "") {
|
|
4
|
+
return { result: { type: "doc", content: [] } };
|
|
5
|
+
}
|
|
6
|
+
const md = new MarkdownIt({ html: true });
|
|
7
|
+
const issues = [];
|
|
8
|
+
try {
|
|
9
|
+
const tokens = md.parse(markdown, {});
|
|
10
|
+
const content = processTokens(tokens, issues);
|
|
11
|
+
return {
|
|
12
|
+
result: { type: "doc", content },
|
|
13
|
+
issues: issues.length > 0 ? issues : undefined,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
issues.push(`Failed to parse markdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
18
|
+
return {
|
|
19
|
+
result: { type: "doc", content: [{ type: "paragraph" }] },
|
|
20
|
+
issues,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function processTokens(tokens, issues) {
|
|
25
|
+
const nodes = [];
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < tokens.length) {
|
|
28
|
+
const token = tokens[i];
|
|
29
|
+
if (token.type === "heading_open") {
|
|
30
|
+
const level = Number.parseInt(token.tag.slice(1), 10);
|
|
31
|
+
const inlineToken = tokens[i + 1];
|
|
32
|
+
const content = inlineToken ? processInlineTokens(inlineToken.children ?? [], issues) : [];
|
|
33
|
+
nodes.push({ type: "heading", attrs: { level }, content });
|
|
34
|
+
i += 3; // Skip heading_open, inline, heading_close
|
|
35
|
+
}
|
|
36
|
+
else if (token.type === "paragraph_open") {
|
|
37
|
+
const inlineToken = tokens[i + 1];
|
|
38
|
+
const content = inlineToken ? processInlineTokens(inlineToken.children ?? [], issues) : [];
|
|
39
|
+
nodes.push({ type: "paragraph", content });
|
|
40
|
+
i += 3; // Skip paragraph_open, inline, paragraph_close
|
|
41
|
+
}
|
|
42
|
+
else if (token.type === "blockquote_open") {
|
|
43
|
+
const { node, consumed } = processBlockquote(tokens, i, issues);
|
|
44
|
+
nodes.push(node);
|
|
45
|
+
i += consumed;
|
|
46
|
+
}
|
|
47
|
+
else if (token.type === "bullet_list_open") {
|
|
48
|
+
const { node, consumed } = processList(tokens, i, "bulletList", issues);
|
|
49
|
+
nodes.push(node);
|
|
50
|
+
i += consumed;
|
|
51
|
+
}
|
|
52
|
+
else if (token.type === "ordered_list_open") {
|
|
53
|
+
const { node, consumed } = processList(tokens, i, "orderedList", issues);
|
|
54
|
+
nodes.push(node);
|
|
55
|
+
i += consumed;
|
|
56
|
+
}
|
|
57
|
+
else if (token.type === "hr") {
|
|
58
|
+
nodes.push({ type: "horizontalRule" });
|
|
59
|
+
++i;
|
|
60
|
+
}
|
|
61
|
+
else if (token.type === "code_block" || token.type === "fence") {
|
|
62
|
+
const language = token.info || undefined;
|
|
63
|
+
const attrs = language ? { language } : undefined;
|
|
64
|
+
nodes.push({
|
|
65
|
+
type: "codeBlock",
|
|
66
|
+
attrs,
|
|
67
|
+
content: [{ type: "text", text: token.content }],
|
|
68
|
+
});
|
|
69
|
+
++i;
|
|
70
|
+
}
|
|
71
|
+
else if (token.type === "table_open") {
|
|
72
|
+
const { node, consumed } = processTable(tokens, i, issues);
|
|
73
|
+
nodes.push(node);
|
|
74
|
+
i += consumed;
|
|
75
|
+
}
|
|
76
|
+
else if (token.type === "html_block") {
|
|
77
|
+
const preview = token.content.substring(0, 50);
|
|
78
|
+
issues.push(`Skipped HTML block: ${preview}${token.content.length > 50 ? "..." : ""}`);
|
|
79
|
+
++i;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
++i;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return nodes;
|
|
86
|
+
}
|
|
87
|
+
function processInlineTokens(tokens, issues) {
|
|
88
|
+
const nodes = [];
|
|
89
|
+
const marksStack = [];
|
|
90
|
+
for (let i = 0; i < tokens.length; ++i) {
|
|
91
|
+
const token = tokens[i];
|
|
92
|
+
if (token.type === "text") {
|
|
93
|
+
// Skip empty text tokens (they occur with nested formatting)
|
|
94
|
+
if (token.content === "")
|
|
95
|
+
continue;
|
|
96
|
+
nodes.push({
|
|
97
|
+
type: "text",
|
|
98
|
+
text: token.content,
|
|
99
|
+
marks: marksStack.length > 0 ? [...marksStack] : undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (token.type === "code_inline") {
|
|
103
|
+
nodes.push({
|
|
104
|
+
type: "text",
|
|
105
|
+
text: token.content,
|
|
106
|
+
marks: [...marksStack, { type: "code" }],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else if (token.type === "hardbreak") {
|
|
110
|
+
nodes.push({ type: "hardBreak" });
|
|
111
|
+
}
|
|
112
|
+
else if (token.type === "strong_open") {
|
|
113
|
+
marksStack.push({ type: "bold" });
|
|
114
|
+
}
|
|
115
|
+
else if (token.type === "strong_close") {
|
|
116
|
+
removeMarkFromStack(marksStack, "bold");
|
|
117
|
+
}
|
|
118
|
+
else if (token.type === "em_open") {
|
|
119
|
+
marksStack.push({ type: "italic" });
|
|
120
|
+
}
|
|
121
|
+
else if (token.type === "em_close") {
|
|
122
|
+
removeMarkFromStack(marksStack, "italic");
|
|
123
|
+
}
|
|
124
|
+
else if (token.type === "s_open") {
|
|
125
|
+
marksStack.push({ type: "strike" });
|
|
126
|
+
}
|
|
127
|
+
else if (token.type === "s_close") {
|
|
128
|
+
removeMarkFromStack(marksStack, "strike");
|
|
129
|
+
}
|
|
130
|
+
else if (token.type === "link_open") {
|
|
131
|
+
const href = token.attrGet("href");
|
|
132
|
+
if (href) {
|
|
133
|
+
marksStack.push({ type: "link", attrs: { href } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (token.type === "link_close") {
|
|
137
|
+
removeMarkFromStack(marksStack, "link");
|
|
138
|
+
}
|
|
139
|
+
else if (token.type === "image") {
|
|
140
|
+
const src = token.attrGet("src");
|
|
141
|
+
const alt = token.content || undefined;
|
|
142
|
+
if (src) {
|
|
143
|
+
const attrs = { src };
|
|
144
|
+
if (alt) {
|
|
145
|
+
attrs.alt = alt;
|
|
146
|
+
}
|
|
147
|
+
nodes.push({ type: "image", attrs });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (token.type === "html_inline") {
|
|
151
|
+
const preview = token.content.substring(0, 50);
|
|
152
|
+
issues.push(`Skipped HTML inline: ${preview}${token.content.length > 50 ? "..." : ""}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return nodes;
|
|
156
|
+
}
|
|
157
|
+
function processBlockquote(tokens, startIndex, issues) {
|
|
158
|
+
let i = startIndex + 1; // Skip blockquote_open
|
|
159
|
+
const content = [];
|
|
160
|
+
while (i < tokens.length && tokens[i].type !== "blockquote_close") {
|
|
161
|
+
if (tokens[i].type === "paragraph_open") {
|
|
162
|
+
const inlineToken = tokens[i + 1];
|
|
163
|
+
const paragraphContent = inlineToken
|
|
164
|
+
? processInlineTokens(inlineToken.children ?? [], issues)
|
|
165
|
+
: [];
|
|
166
|
+
content.push({ type: "paragraph", content: paragraphContent });
|
|
167
|
+
i += 3; // Skip paragraph_open, inline, paragraph_close
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
++i;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
node: { type: "blockquote", content },
|
|
175
|
+
consumed: i - startIndex + 1,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function processList(tokens, startIndex, listType, issues) {
|
|
179
|
+
let i = startIndex + 1; // Skip list_open
|
|
180
|
+
const content = [];
|
|
181
|
+
while (i < tokens.length &&
|
|
182
|
+
tokens[i].type !== "bullet_list_close" &&
|
|
183
|
+
tokens[i].type !== "ordered_list_close") {
|
|
184
|
+
if (tokens[i].type === "list_item_open") {
|
|
185
|
+
const { node, consumed } = processListItem(tokens, i, issues);
|
|
186
|
+
content.push(node);
|
|
187
|
+
i += consumed;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
++i;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
node: { type: listType, content },
|
|
195
|
+
consumed: i - startIndex + 1,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function processListItem(tokens, startIndex, issues) {
|
|
199
|
+
let i = startIndex + 1; // Skip list_item_open
|
|
200
|
+
const content = [];
|
|
201
|
+
while (i < tokens.length && tokens[i].type !== "list_item_close") {
|
|
202
|
+
const token = tokens[i];
|
|
203
|
+
if (token.type === "paragraph_open") {
|
|
204
|
+
const inlineToken = tokens[i + 1];
|
|
205
|
+
const paragraphContent = inlineToken
|
|
206
|
+
? processInlineTokens(inlineToken.children ?? [], issues)
|
|
207
|
+
: [];
|
|
208
|
+
content.push({ type: "paragraph", content: paragraphContent });
|
|
209
|
+
i += 3; // Skip paragraph_open, inline, paragraph_close
|
|
210
|
+
}
|
|
211
|
+
else if (token.type === "bullet_list_open") {
|
|
212
|
+
const { node, consumed } = processList(tokens, i, "bulletList", issues);
|
|
213
|
+
content.push(node);
|
|
214
|
+
i += consumed;
|
|
215
|
+
}
|
|
216
|
+
else if (token.type === "ordered_list_open") {
|
|
217
|
+
const { node, consumed } = processList(tokens, i, "orderedList", issues);
|
|
218
|
+
content.push(node);
|
|
219
|
+
i += consumed;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
++i;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
node: { type: "listItem", content },
|
|
227
|
+
consumed: i - startIndex + 1,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function processTable(tokens, startIndex, issues) {
|
|
231
|
+
let i = startIndex + 1; // Skip table_open
|
|
232
|
+
const content = [];
|
|
233
|
+
while (i < tokens.length && tokens[i].type !== "table_close") {
|
|
234
|
+
if (tokens[i].type === "thead_open" || tokens[i].type === "tbody_open") {
|
|
235
|
+
const isHeader = tokens[i].type === "thead_open";
|
|
236
|
+
++i; // Skip thead_open or tbody_open
|
|
237
|
+
while (i < tokens.length &&
|
|
238
|
+
tokens[i].type !== "thead_close" &&
|
|
239
|
+
tokens[i].type !== "tbody_close") {
|
|
240
|
+
if (tokens[i].type === "tr_open") {
|
|
241
|
+
const { node, consumed } = processTableRow(tokens, i, isHeader, issues);
|
|
242
|
+
content.push(node);
|
|
243
|
+
i += consumed;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
++i;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
++i; // Skip thead_close or tbody_close
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
++i;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
node: { type: "table", content },
|
|
257
|
+
consumed: i - startIndex + 1,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function processTableRow(tokens, startIndex, isHeader, issues) {
|
|
261
|
+
let i = startIndex + 1; // Skip tr_open
|
|
262
|
+
const content = [];
|
|
263
|
+
while (i < tokens.length && tokens[i].type !== "tr_close") {
|
|
264
|
+
if (tokens[i].type === "th_open" || tokens[i].type === "td_open") {
|
|
265
|
+
const cellType = isHeader || tokens[i].type === "th_open" ? "tableHeader" : "tableCell";
|
|
266
|
+
const inlineToken = tokens[i + 1];
|
|
267
|
+
const cellContent = inlineToken
|
|
268
|
+
? processInlineTokens(inlineToken.children ?? [], issues)
|
|
269
|
+
: [];
|
|
270
|
+
// Table cells must contain at least one paragraph
|
|
271
|
+
content.push({
|
|
272
|
+
type: cellType,
|
|
273
|
+
content: [{ type: "paragraph", content: cellContent }],
|
|
274
|
+
});
|
|
275
|
+
i += 3; // Skip th_open/td_open, inline, th_close/td_close
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
++i;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
node: { type: "tableRow", content },
|
|
283
|
+
consumed: i - startIndex + 1,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function removeMarkFromStack(marksStack, markType) {
|
|
287
|
+
const index = marksStack.findIndex((mark) => mark.type === markType);
|
|
288
|
+
if (index !== -1) {
|
|
289
|
+
marksStack.splice(index, 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { convertMarkdownToTiptap } from "./converter.js";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface TiptapJsonValue {
|
|
2
|
+
type: "doc";
|
|
3
|
+
content?: TiptapJsonNode[];
|
|
4
|
+
}
|
|
5
|
+
export interface TiptapJsonNode {
|
|
6
|
+
type: string;
|
|
7
|
+
attrs?: Record<string, any>;
|
|
8
|
+
content?: TiptapJsonNode[];
|
|
9
|
+
marks?: TiptapJsonMark[];
|
|
10
|
+
text?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TiptapJsonMark {
|
|
13
|
+
type: string;
|
|
14
|
+
attrs?: Record<string, any>;
|
|
15
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paroicms/markdown-to-tiptap-json",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Convert Markdown to Tiptap JSON",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"tiptap",
|
|
8
|
+
"json",
|
|
9
|
+
"converter"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://gitlab.com/paroi/opensource/paroicms.git",
|
|
14
|
+
"directory": "packages/markdown-to-tiptap-json"
|
|
15
|
+
},
|
|
16
|
+
"author": "Paroi Team",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"typings": "dist/index.d.ts",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"clear": "rimraf dist/*",
|
|
24
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"markdown-it": "~14.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/markdown-it": "~14.1.2",
|
|
33
|
+
"rimraf": "~6.0.1",
|
|
34
|
+
"typescript": "~5.9.3",
|
|
35
|
+
"vitest": "~3.2.4"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
]
|
|
40
|
+
}
|