arxiv-api-wrapper 1.1.0 → 2.0.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 +318 -250
- package/package.json +6 -4
- package/src/arxivAPIRead.ts +316 -316
- package/src/atom.ts +1 -1
- package/src/index.ts +102 -57
- package/src/oaiClient.ts +425 -0
- package/src/oaiParser.ts +264 -0
- package/src/oaiToArxiv.ts +204 -0
- package/src/oaiTypes.ts +248 -0
- package/src/types.ts +265 -265
- package/tests/arxivAPI.integration.test.ts +144 -144
- package/tests/arxivAPIRead.test.ts +1 -1
- package/tests/fixtures/parseEntries/2507.17541.json.ts +1 -1
- package/tests/fixtures/parseEntries/search_agdur.json.ts +1 -1
- package/tests/oai.integration.test.ts +222 -0
- package/tests/oai.test.ts +248 -0
- package/tests/oaiToArxiv.test.ts +131 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for OAI-PMH URL builder and XML parser (no network).
|
|
3
|
+
* Pagination helpers (oaiListRecordsAll, etc.) are covered by integration tests.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { buildOaiUrl, normalizeOaiIdentifier } from '../src/oaiClient.js';
|
|
7
|
+
import {
|
|
8
|
+
parseIdentify,
|
|
9
|
+
parseListMetadataFormats,
|
|
10
|
+
parseListSets,
|
|
11
|
+
parseGetRecord,
|
|
12
|
+
parseListIdentifiers,
|
|
13
|
+
parseListRecords,
|
|
14
|
+
parseOaiResponse,
|
|
15
|
+
} from '../src/oaiParser.js';
|
|
16
|
+
import { OaiError } from '../src/oaiTypes.js';
|
|
17
|
+
|
|
18
|
+
const OAI_BASE = 'https://oaipmh.arxiv.org/oai';
|
|
19
|
+
|
|
20
|
+
describe('buildOaiUrl', () => {
|
|
21
|
+
it('includes verb only for Identify', () => {
|
|
22
|
+
const url = buildOaiUrl('Identify', {});
|
|
23
|
+
expect(url).toBe(`${OAI_BASE}?verb=Identify`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('encodes identifier and metadataPrefix for GetRecord', () => {
|
|
27
|
+
const url = buildOaiUrl('GetRecord', {
|
|
28
|
+
identifier: 'oai:arXiv.org:cs/0112017',
|
|
29
|
+
metadataPrefix: 'oai_dc',
|
|
30
|
+
});
|
|
31
|
+
expect(url).toContain('verb=GetRecord');
|
|
32
|
+
expect(url).toContain('identifier=' + encodeURIComponent('oai:arXiv.org:cs/0112017'));
|
|
33
|
+
expect(url).toContain('metadataPrefix=oai_dc');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('includes from, until, set for ListRecords', () => {
|
|
37
|
+
const url = buildOaiUrl('ListRecords', {
|
|
38
|
+
metadataPrefix: 'oai_dc',
|
|
39
|
+
from: '2024-01-01',
|
|
40
|
+
until: '2024-01-31',
|
|
41
|
+
set: 'cs:cs.AI',
|
|
42
|
+
});
|
|
43
|
+
expect(url).toContain('verb=ListRecords');
|
|
44
|
+
expect(url).toContain('metadataPrefix=oai_dc');
|
|
45
|
+
expect(url).toContain('from=2024-01-01');
|
|
46
|
+
expect(url).toContain('until=2024-01-31');
|
|
47
|
+
expect(url).toContain('set=cs%3Acs.AI');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('encodes resumptionToken', () => {
|
|
51
|
+
const token = 'token/with/slashes?and=chars';
|
|
52
|
+
const url = buildOaiUrl('ListIdentifiers', { metadataPrefix: 'oai_dc', resumptionToken: token });
|
|
53
|
+
expect(url).toContain('resumptionToken=' + encodeURIComponent(token));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('normalizeOaiIdentifier', () => {
|
|
58
|
+
it('returns full form unchanged', () => {
|
|
59
|
+
expect(normalizeOaiIdentifier('oai:arXiv.org:cs/0112017')).toBe('oai:arXiv.org:cs/0112017');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('prefixes short form', () => {
|
|
63
|
+
expect(normalizeOaiIdentifier('cs/0112017')).toBe('oai:arXiv.org:cs/0112017');
|
|
64
|
+
expect(normalizeOaiIdentifier('2101.01234')).toBe('oai:arXiv.org:2101.01234');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function wrapOaiRoot(inner: string): string {
|
|
69
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
70
|
+
<OAI-PMH xmlns="http://www.openarchives.org/OAI/2.0/">
|
|
71
|
+
<responseDate>2024-01-15T12:00:00Z</responseDate>
|
|
72
|
+
<request verb="Identify">${OAI_BASE}</request>
|
|
73
|
+
${inner}
|
|
74
|
+
</OAI-PMH>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('parseIdentify', () => {
|
|
78
|
+
it('parses Identify response', () => {
|
|
79
|
+
const xml = wrapOaiRoot(`
|
|
80
|
+
<Identify>
|
|
81
|
+
<repositoryName>arXiv</repositoryName>
|
|
82
|
+
<baseURL>https://oaipmh.arxiv.org/oai</baseURL>
|
|
83
|
+
<protocolVersion>2.0</protocolVersion>
|
|
84
|
+
<adminEmail>help@arxiv.org</adminEmail>
|
|
85
|
+
<earliestDatestamp>2005-09-16</earliestDatestamp>
|
|
86
|
+
<deletedRecord>persistent</deletedRecord>
|
|
87
|
+
<granularity>YYYY-MM-DD</granularity>
|
|
88
|
+
</Identify>`);
|
|
89
|
+
const out = parseIdentify(xml);
|
|
90
|
+
expect(out.repositoryName).toBe('arXiv');
|
|
91
|
+
expect(out.baseURL).toBe('https://oaipmh.arxiv.org/oai');
|
|
92
|
+
expect(out.protocolVersion).toBe('2.0');
|
|
93
|
+
expect(out.adminEmail).toEqual(['help@arxiv.org']);
|
|
94
|
+
expect(out.earliestDatestamp).toBe('2005-09-16');
|
|
95
|
+
expect(out.deletedRecord).toBe('persistent');
|
|
96
|
+
expect(out.granularity).toBe('YYYY-MM-DD');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('parseListMetadataFormats', () => {
|
|
101
|
+
it('parses ListMetadataFormats response', () => {
|
|
102
|
+
const inner = `
|
|
103
|
+
<ListMetadataFormats>
|
|
104
|
+
<metadataFormat>
|
|
105
|
+
<metadataPrefix>oai_dc</metadataPrefix>
|
|
106
|
+
<schema>http://www.openarchives.org/OAI/2.0/oai_dc.xsd</schema>
|
|
107
|
+
<metadataNamespace>http://www.openarchives.org/OAI/2.0/oai_dc/</metadataNamespace>
|
|
108
|
+
</metadataFormat>
|
|
109
|
+
<metadataFormat>
|
|
110
|
+
<metadataPrefix>arXiv</metadataPrefix>
|
|
111
|
+
<schema>https://arxiv.org/schemas/arXiv.xsd</schema>
|
|
112
|
+
<metadataNamespace>http://arxiv.org/schemas/arXiv/</metadataNamespace>
|
|
113
|
+
</metadataFormat>
|
|
114
|
+
</ListMetadataFormats>`;
|
|
115
|
+
const xml = wrapOaiRoot(inner).replace('<request verb="Identify">', '<request verb="ListMetadataFormats">');
|
|
116
|
+
const out = parseListMetadataFormats(xml);
|
|
117
|
+
expect(out).toHaveLength(2);
|
|
118
|
+
expect(out[0].metadataPrefix).toBe('oai_dc');
|
|
119
|
+
expect(out[1].metadataPrefix).toBe('arXiv');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('parseListSets', () => {
|
|
124
|
+
it('parses sets and resumptionToken', () => {
|
|
125
|
+
const inner = `
|
|
126
|
+
<ListSets>
|
|
127
|
+
<set>
|
|
128
|
+
<setSpec>cs</setSpec>
|
|
129
|
+
<setName>Computer Science</setName>
|
|
130
|
+
</set>
|
|
131
|
+
<set>
|
|
132
|
+
<setSpec>physics</setSpec>
|
|
133
|
+
<setName>Physics</setName>
|
|
134
|
+
</set>
|
|
135
|
+
<resumptionToken expirationDate="2024-01-16T00:00:00Z" completeListSize="42" cursor="2">next-token</resumptionToken>
|
|
136
|
+
</ListSets>`;
|
|
137
|
+
const xml = wrapOaiRoot(inner).replace('<request verb="Identify">', '<request verb="ListSets">');
|
|
138
|
+
const out = parseListSets(xml);
|
|
139
|
+
expect(out.sets).toHaveLength(2);
|
|
140
|
+
expect(out.sets[0].setSpec).toBe('cs');
|
|
141
|
+
expect(out.sets[0].setName).toBe('Computer Science');
|
|
142
|
+
expect(out.resumptionToken?.value).toBe('next-token');
|
|
143
|
+
expect(out.resumptionToken?.expirationDate).toBe('2024-01-16T00:00:00Z');
|
|
144
|
+
expect(out.resumptionToken?.completeListSize).toBe(42);
|
|
145
|
+
expect(out.resumptionToken?.cursor).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('parseGetRecord', () => {
|
|
150
|
+
it('parses GetRecord with header and metadata', () => {
|
|
151
|
+
const inner = `
|
|
152
|
+
<GetRecord>
|
|
153
|
+
<record>
|
|
154
|
+
<header>
|
|
155
|
+
<identifier>oai:arXiv.org:cs/0112017</identifier>
|
|
156
|
+
<datestamp>2001-12-14</datestamp>
|
|
157
|
+
<setSpec>cs</setSpec>
|
|
158
|
+
<setSpec>math</setSpec>
|
|
159
|
+
</header>
|
|
160
|
+
<metadata>
|
|
161
|
+
<dc xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
162
|
+
<dc:title>Using Structural Metadata to Localize Experience of Digital Content</dc:title>
|
|
163
|
+
<dc:creator>Dushay, Naomi</dc:creator>
|
|
164
|
+
<dc:date>2001-12-14</dc:date>
|
|
165
|
+
</dc>
|
|
166
|
+
</metadata>
|
|
167
|
+
</record>
|
|
168
|
+
</GetRecord>`;
|
|
169
|
+
const xml = wrapOaiRoot(inner).replace('<request verb="Identify">', '<request verb="GetRecord" identifier="oai:arXiv.org:cs/0112017" metadataPrefix="oai_dc">');
|
|
170
|
+
const out = parseGetRecord(xml);
|
|
171
|
+
expect(out.header.identifier).toBe('oai:arXiv.org:cs/0112017');
|
|
172
|
+
expect(out.header.datestamp).toBe('2001-12-14');
|
|
173
|
+
expect(out.header.setSpec).toEqual(['cs', 'math']);
|
|
174
|
+
expect(out.metadata).toBeDefined();
|
|
175
|
+
expect(Object.keys(out.metadata!).length).toBeGreaterThan(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('parseListIdentifiers', () => {
|
|
180
|
+
it('parses headers', () => {
|
|
181
|
+
const inner = `
|
|
182
|
+
<ListIdentifiers>
|
|
183
|
+
<header>
|
|
184
|
+
<identifier>oai:arXiv.org:hep-th/9901001</identifier>
|
|
185
|
+
<datestamp>1999-12-25</datestamp>
|
|
186
|
+
<setSpec>physics:hep-th</setSpec>
|
|
187
|
+
</header>
|
|
188
|
+
<header>
|
|
189
|
+
<identifier>oai:arXiv.org:hep-th/9901002</identifier>
|
|
190
|
+
<datestamp>1999-12-26</datestamp>
|
|
191
|
+
</header>
|
|
192
|
+
</ListIdentifiers>`;
|
|
193
|
+
const xml = wrapOaiRoot(inner).replace('<request verb="Identify">', '<request verb="ListIdentifiers" metadataPrefix="oai_dc">');
|
|
194
|
+
const out = parseListIdentifiers(xml);
|
|
195
|
+
expect(out.headers).toHaveLength(2);
|
|
196
|
+
expect(out.headers[0].identifier).toBe('oai:arXiv.org:hep-th/9901001');
|
|
197
|
+
expect(out.headers[0].setSpec).toEqual(['physics:hep-th']);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('parseListRecords', () => {
|
|
202
|
+
it('parses records and resumptionToken', () => {
|
|
203
|
+
const inner = `
|
|
204
|
+
<ListRecords>
|
|
205
|
+
<record>
|
|
206
|
+
<header>
|
|
207
|
+
<identifier>oai:arXiv.org:cs/0112017</identifier>
|
|
208
|
+
<datestamp>2001-12-14</datestamp>
|
|
209
|
+
<setSpec>cs</setSpec>
|
|
210
|
+
</header>
|
|
211
|
+
<metadata>
|
|
212
|
+
<dc><dc:title>Test Paper</dc:title></dc>
|
|
213
|
+
</metadata>
|
|
214
|
+
</record>
|
|
215
|
+
<resumptionToken cursor="1">resume-123</resumptionToken>
|
|
216
|
+
</ListRecords>`;
|
|
217
|
+
const xml = wrapOaiRoot(inner).replace('<request verb="Identify">', '<request verb="ListRecords" metadataPrefix="oai_dc">');
|
|
218
|
+
const out = parseListRecords(xml);
|
|
219
|
+
expect(out.records).toHaveLength(1);
|
|
220
|
+
expect(out.records[0].header.identifier).toBe('oai:arXiv.org:cs/0112017');
|
|
221
|
+
expect(out.resumptionToken?.value).toBe('resume-123');
|
|
222
|
+
expect(out.resumptionToken?.cursor).toBe(1);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('OAI error handling', () => {
|
|
227
|
+
it('throws OaiError with code and message on error element', () => {
|
|
228
|
+
const xml = wrapOaiRoot(`<error code="idDoesNotExist">No matching identifier in arXiv</error>`);
|
|
229
|
+
expect(() => parseOaiResponse(xml)).toThrow(OaiError);
|
|
230
|
+
try {
|
|
231
|
+
parseOaiResponse(xml);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
expect(e).toBeInstanceOf(OaiError);
|
|
234
|
+
expect((e as OaiError).code).toBe('idDoesNotExist');
|
|
235
|
+
expect((e as OaiError).messageText).toContain('No matching identifier');
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('throws OaiError for noRecordsMatch', () => {
|
|
240
|
+
const xml = wrapOaiRoot(`<error code="noRecordsMatch"/>`);
|
|
241
|
+
expect(() => parseIdentify(xml)).toThrow(OaiError);
|
|
242
|
+
try {
|
|
243
|
+
parseOaiResponse(xml);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
expect((e as OaiError).code).toBe('noRecordsMatch');
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
oaiListRecordsToArxivQueryResult,
|
|
4
|
+
oaiRecordToArxivEntry,
|
|
5
|
+
oaiRecordsToArxivEntries,
|
|
6
|
+
} from '../src/oaiToArxiv.js';
|
|
7
|
+
import type { OaiListRecordsResult, OaiRecord } from '../src/oaiTypes.js';
|
|
8
|
+
|
|
9
|
+
describe('oai->arxiv conversion helpers', () => {
|
|
10
|
+
it('converts arXiv metadata record to ArxivEntry shape', () => {
|
|
11
|
+
const record: OaiRecord = {
|
|
12
|
+
header: {
|
|
13
|
+
identifier: 'oai:arXiv.org:2501.12345',
|
|
14
|
+
datestamp: '2025-01-10',
|
|
15
|
+
setSpec: ['cs'],
|
|
16
|
+
},
|
|
17
|
+
metadata: {
|
|
18
|
+
arXiv: {
|
|
19
|
+
id: '2501.12345v2',
|
|
20
|
+
created: '2025-01-01',
|
|
21
|
+
updated: '2025-01-09',
|
|
22
|
+
title: 'Example Paper',
|
|
23
|
+
abstract: 'Example abstract',
|
|
24
|
+
categories: 'cs.AI cs.LG',
|
|
25
|
+
comments: '12 pages',
|
|
26
|
+
doi: '10.1000/example',
|
|
27
|
+
'journal-ref': 'J. Examples 1 (2025)',
|
|
28
|
+
authors: {
|
|
29
|
+
author: [
|
|
30
|
+
{ keyname: 'Doe', forenames: 'Jane', affiliation: ['University A'] },
|
|
31
|
+
{ keyname: 'Smith', forenames: 'John' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const entry = oaiRecordToArxivEntry(record);
|
|
39
|
+
expect(entry).not.toBeNull();
|
|
40
|
+
expect(entry?.arxivId).toBe('2501.12345v2');
|
|
41
|
+
expect(entry?.id).toBe('https://arxiv.org/abs/2501.12345v2');
|
|
42
|
+
expect(entry?.published).toBe('2025-01-01');
|
|
43
|
+
expect(entry?.updated).toBe('2025-01-09');
|
|
44
|
+
expect(entry?.authors).toEqual([
|
|
45
|
+
{ name: 'Jane Doe', affiliation: 'University A' },
|
|
46
|
+
{ name: 'John Smith' },
|
|
47
|
+
]);
|
|
48
|
+
expect(entry?.categories).toEqual(['cs.AI', 'cs.LG']);
|
|
49
|
+
expect(entry?.primaryCategory).toBe('cs.AI');
|
|
50
|
+
expect(entry?.doi).toBe('10.1000/example');
|
|
51
|
+
expect(entry?.journalRef).toBe('J. Examples 1 (2025)');
|
|
52
|
+
expect(entry?.comment).toBe('12 pages');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('converts arXivRaw versions and appends latest version suffix', () => {
|
|
56
|
+
const record: OaiRecord = {
|
|
57
|
+
header: {
|
|
58
|
+
identifier: 'oai:arXiv.org:hep-th/9901001',
|
|
59
|
+
datestamp: '1999-12-26',
|
|
60
|
+
setSpec: ['physics:hep-th'],
|
|
61
|
+
},
|
|
62
|
+
metadata: {
|
|
63
|
+
arXivRaw: {
|
|
64
|
+
id: 'hep-th/9901001',
|
|
65
|
+
submitter: 'Example User',
|
|
66
|
+
version: [
|
|
67
|
+
{ version: 'v1', date: '1999-12-25' },
|
|
68
|
+
{ version: 'v2', date: '1999-12-26' },
|
|
69
|
+
],
|
|
70
|
+
title: 'Raw Title',
|
|
71
|
+
abstract: 'Raw abstract',
|
|
72
|
+
authors: 'Alice A., Bob B.',
|
|
73
|
+
categories: 'hep-th',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const entry = oaiRecordToArxivEntry(record);
|
|
79
|
+
expect(entry).not.toBeNull();
|
|
80
|
+
expect(entry?.arxivId).toBe('hep-th/9901001v2');
|
|
81
|
+
expect(entry?.published).toBe('1999-12-25');
|
|
82
|
+
expect(entry?.updated).toBe('1999-12-26');
|
|
83
|
+
expect(entry?.authors).toEqual([{ name: 'Alice A.' }, { name: 'Bob B.' }]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('converts list result to ArxivQueryResult and skips deleted records', () => {
|
|
87
|
+
const result: OaiListRecordsResult = {
|
|
88
|
+
records: [
|
|
89
|
+
{
|
|
90
|
+
header: {
|
|
91
|
+
identifier: 'oai:arXiv.org:2101.01234',
|
|
92
|
+
datestamp: '2021-01-10',
|
|
93
|
+
setSpec: ['cs'],
|
|
94
|
+
},
|
|
95
|
+
metadata: {
|
|
96
|
+
dc: {
|
|
97
|
+
title: 'DC title',
|
|
98
|
+
creator: ['Author One'],
|
|
99
|
+
subject: ['cs.AI'],
|
|
100
|
+
description: 'DC abstract',
|
|
101
|
+
date: '2021-01-09',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
header: {
|
|
107
|
+
identifier: 'oai:arXiv.org:2101.09999',
|
|
108
|
+
datestamp: '2021-01-11',
|
|
109
|
+
setSpec: ['cs'],
|
|
110
|
+
status: 'deleted',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
resumptionToken: {
|
|
115
|
+
value: 'next-token',
|
|
116
|
+
cursor: 10,
|
|
117
|
+
completeListSize: 250,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const entries = oaiRecordsToArxivEntries(result.records);
|
|
122
|
+
expect(entries).toHaveLength(1);
|
|
123
|
+
|
|
124
|
+
const queryResult = oaiListRecordsToArxivQueryResult(result);
|
|
125
|
+
expect(queryResult.entries).toHaveLength(1);
|
|
126
|
+
expect(queryResult.entries[0].title).toBe('DC title');
|
|
127
|
+
expect(queryResult.feed.totalResults).toBe(250);
|
|
128
|
+
expect(queryResult.feed.itemsPerPage).toBe(1);
|
|
129
|
+
expect(queryResult.feed.startIndex).toBe(9);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"resolveJsonModule": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*", "tests/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "docs"]
|
|
13
|
+
}
|