duto 0.0.0 → 0.260808.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,764 @@
1
+ import { dirname, resolve } from 'node:path'
2
+ import { parseSync, printSync } from '@swc/core'
3
+
4
+ import { IMPORT_RE, BROWSER_ONLY, BROWSER_ONLY_SET, USE_CLIENT_RE, FILE_EXTENSIONS } from './constants'
5
+ import { resolveAlias } from './aliases'
6
+
7
+ import type {
8
+ Metadata,
9
+ FileMetadata,
10
+ DepMetadata,
11
+ ImportUsage, Usage,
12
+ } from './types'
13
+
14
+
15
+ function isBrowserLib(lib: string) {
16
+ if (!lib) return false
17
+ if (BROWSER_ONLY_SET.has(lib)) return true
18
+
19
+ // avoids create template string every time
20
+ for (let i = 0; i < BROWSER_ONLY.length; i++) {
21
+ const base = BROWSER_ONLY[i]
22
+ if (lib.startsWith(base) && lib.charCodeAt(base.length) === 47 /* '/' */) {
23
+ return true
24
+ }
25
+ }
26
+
27
+ return false
28
+ }
29
+
30
+ const resolveImportCache = new Map<string, string>()
31
+ export const resolveImport = (lib: string, importer: string, aliases: Record<string, string>) => {
32
+ if (resolveImportCache.has(lib))
33
+ return resolveImportCache.get(lib)!
34
+
35
+ const id = resolveAlias(lib.startsWith('../') || lib.startsWith('./') ? resolve(importer, lib) : lib, aliases)
36
+
37
+ resolveImportCache.set(lib, id)
38
+ return id
39
+ }
40
+
41
+ export async function getFileMap(meta: Metadata) {
42
+ const map = new Map<string, FileMetadata>()
43
+ const violations = new Map<string, DepMetadata[]>()
44
+
45
+ for await (const path of new Bun.Glob(`**/*.{${FILE_EXTENSIONS.join(',')}}`).scan({
46
+ cwd: meta.root,
47
+ absolute: true,
48
+ onlyFiles: true,
49
+ ignore: [
50
+ '**/node_modules/**',
51
+ '**/.git/**',
52
+ '**/dist/**',
53
+ '**/out/**',
54
+ '**/public/**',
55
+ ],
56
+ })) {
57
+ const file: FileMetadata = { path, content: '', deps: new Map<string, DepMetadata>() }
58
+ file.content = await Bun.file(path).text()
59
+ if (!file.content.includes('import')) {
60
+ map.set(path, file)
61
+ continue
62
+ }
63
+
64
+ const dir = dirname(path)
65
+ let match: RegExpExecArray | null
66
+ const found: DepMetadata[] = []
67
+
68
+ let importsEndIndex = 0
69
+ while ((match = IMPORT_RE.exec(file.content))) {
70
+ const statement = match[0]
71
+ const ref = match[1]
72
+ if (match[0].includes('import type') || !ref) continue
73
+
74
+ const importEnd = match.index + statement.length
75
+ if (importEnd > importsEndIndex) importsEndIndex = importEnd
76
+
77
+ const identifiers = extractIdentifiers(statement)
78
+ if (identifiers.length > 0 && !isAnyIdentifierUsed(identifiers, file.content, importEnd))
79
+ continue
80
+
81
+ const dep = {path: resolveImport(match[1], dir, meta.aliases), ref}
82
+ file.deps.set(dep.path, dep)
83
+ if (isBrowserLib(dep.path)) found.push(dep)
84
+ }
85
+
86
+ map.set(path, file)
87
+
88
+ if (found.length || USE_CLIENT_RE.test(file.content))
89
+ violations.set(path, found)
90
+ }
91
+
92
+ const visited = new Set<string>()
93
+ const isViolator = (path: string, stack = new Set<string>()) => {
94
+ if (violations.has(path)) return true
95
+ if (visited.has(path) || stack.has(path)) return false
96
+
97
+ stack.add(path)
98
+
99
+ const file = map.get(path)
100
+ if (!file) {
101
+ visited.add(path)
102
+ return false
103
+ }
104
+
105
+ for (const [depPath, dep] of file.deps) {
106
+ if (isViolator(depPath, stack)) {
107
+ const _violations = violations.get(path) || []
108
+ violations.set(path, [..._violations, dep])
109
+ visited.add(path)
110
+ return true
111
+ }
112
+ }
113
+
114
+ visited.add(path)
115
+ return false
116
+ }
117
+
118
+ for (const [subpath, _] of map)
119
+ isViolator(subpath)
120
+
121
+ return { map, violations }
122
+ }
123
+
124
+ /**
125
+ * Extracts identifiers from an import line without AST.
126
+ *
127
+ * Supports:
128
+ * import Foo from '...'
129
+ * import { Foo, Bar as Baz } from '...'
130
+ * import * as Foo from '...'
131
+ * import Foo, { Bar } from '...'
132
+ */
133
+ function extractIdentifiers(importStatement: string): string[] {
134
+ // Removes the module path and the word 'import'
135
+ const withoutPath = importStatement.replace(/from\s+['"][^'"]+['"]\s*;?/, '').replace(/^import\s+/, '')
136
+
137
+ const identifiers: string[] = []
138
+
139
+ // import * as Name
140
+ const namespaceMatch = withoutPath.match(/\*\s+as\s+(\w+)/)
141
+ if (namespaceMatch) return [namespaceMatch[1]];
142
+
143
+ // Named imports { Foo, Bar as Baz }
144
+ const namedMatch = withoutPath.match(/\{([^}]+)\}/)
145
+ if (namedMatch) {
146
+ for (const part of namedMatch[1].split(',')) {
147
+ // "Bar as Baz" → usa "Baz" (o alias local)
148
+ const alias = part.trim().match(/(?:.*\bas\s+)?(\w+)$/)
149
+ if (alias) identifiers.push(alias[1]);
150
+ }
151
+ }
152
+
153
+ // Default import (before a comma or the end): import Foo, { ... }
154
+ const defaultMatch = withoutPath.match(/^(\w+)/)
155
+ if (defaultMatch) identifiers.push(defaultMatch[1])
156
+
157
+ return identifiers
158
+ }
159
+
160
+ /**
161
+ * Checks if at least one of the identifiers appears in the code
162
+ * after the end of the import line (avoids false positives with the import itself).
163
+ *
164
+ * Uses indexOf for maximum performance — no regex, no AST.
165
+ */
166
+ function isAnyIdentifierUsed(identifiers: string[], code: string, afterIndex: number): boolean {
167
+ const body = code.slice(afterIndex)
168
+
169
+ for (const id of identifiers) {
170
+ let pos = body.indexOf(id)
171
+ while (pos !== -1) {
172
+ // Checks if it is a whole word (not part of another identifier)
173
+ const before = pos === 0 ? '' : body[pos - 1]
174
+ const after = body[pos + id.length] ?? ''
175
+
176
+ const isWordBoundaryBefore = !/\w/.test(before)
177
+ const isWordBoundaryAfter = !/\w/.test(after)
178
+
179
+ if (isWordBoundaryBefore && isWordBoundaryAfter) return true
180
+
181
+ pos = body.indexOf(id, pos + 1)
182
+ }
183
+ }
184
+
185
+ return false
186
+ }
187
+
188
+ const SKIP_KEYS = new Set([
189
+ 'span',
190
+ 'ctxt',
191
+ 'raw',
192
+ 'hasConstructor',
193
+ 'isGenerator',
194
+ 'isAsync',
195
+ 'typeAnnotation',
196
+ 'typeParams',
197
+ ])
198
+
199
+ function printImport(node: unknown): string {
200
+ const n = node as { span: unknown }
201
+ return printSync({
202
+ type: 'Module',
203
+ span: n.span as never,
204
+ body: [node as never], // @ts-ignore
205
+ interpreter: null,
206
+ }).code
207
+ }
208
+
209
+ function printExpression(node: unknown): string {
210
+ const n = node as { span: unknown }
211
+ return printSync({
212
+ type: 'Module',
213
+ span: n.span as never,
214
+ body: [
215
+ {
216
+ type: 'ExpressionStatement',
217
+ span: n.span,
218
+ expression: node,
219
+ } as never,
220
+ ], // @ts-ignore
221
+ interpreter: null,
222
+ }).code
223
+ .replace(/;$/, '')
224
+ .trim()
225
+ }
226
+
227
+ /**
228
+ * Builds a byteOffset → charIndex map to convert SWC spans
229
+ * (which are UTF-8 byte offsets, base 1) in JS character indices (base 0).
230
+ *
231
+ * Required because characters like →, ─, 🧪 take up more than 1 byte,
232
+ * causing drift between byte offset and charIndex
233
+ */
234
+ export function makeByteToCharMap(code: string): Int32Array {
235
+ const buf = Buffer.from(code, 'utf8')
236
+ const map = new Int32Array(buf.length + 1)
237
+ let charIdx = 0
238
+ let byteIdx = 0
239
+
240
+ while (byteIdx < buf.length) {
241
+ map[byteIdx] = charIdx
242
+ const byte = buf[byteIdx]
243
+ let byteLen = 1
244
+ if ((byte & 0x80) === 0) byteLen = 1
245
+ else if ((byte & 0xe0) === 0xc0) byteLen = 2
246
+ else if ((byte & 0xf0) === 0xe0) byteLen = 3
247
+ else if ((byte & 0xf8) === 0xf0) byteLen = 4
248
+ // surrogate pair: 4-byte UTF-8 = 2 JS chars
249
+ charIdx += byteLen === 4 ? 2 : 1
250
+ byteIdx += byteLen
251
+ }
252
+
253
+ map[buf.length] = charIdx
254
+ return map
255
+ }
256
+
257
+ /**
258
+ * Checks if the usage is inside a JSXExpressionContainer, that is,
259
+ * if it is wrapped in `{` and `}` in JSX. Returns the span of the container if yes.
260
+ */
261
+ function getJSXContainerSpan(
262
+ parent: unknown,
263
+ ): { start: number, end: number } | null {
264
+ const p = parent as Record<string, unknown> | undefined
265
+ if (p?.['type'] === 'JSXExpressionContainer') {
266
+ const span = p['span'] as { start: number, end: number } | undefined
267
+ if (span) return span
268
+ }
269
+ return null
270
+ }
271
+
272
+ export function findUsage(
273
+ code: string,
274
+ target: string,
275
+ _ast?: any,
276
+ ): ImportUsage[] {
277
+ const ast = _ast ?? parseSync(code, { syntax: 'typescript', tsx: true })
278
+ const imports: ImportUsage[] = [] // @ts-ignore
279
+ const source = resolveImport(target)
280
+
281
+ // collect imports
282
+ for (const node of ast.body) {
283
+ if (node.type !== 'ImportDeclaration') continue
284
+
285
+ const decl = node as {
286
+ type: string,
287
+ source: { value: string },
288
+ specifiers: Array<{
289
+ type: string,
290
+ imported?: { value: string },
291
+ local: { value: string },
292
+ }>,
293
+ span: { start: number, end: number },
294
+ }
295
+
296
+ if (decl.source.value !== target) continue
297
+
298
+ for (const specifier of decl.specifiers) {
299
+ if (specifier.type !== 'ImportSpecifier' && specifier.type !== 'ImportDefaultSpecifier') continue
300
+
301
+ imports.push({
302
+ source,
303
+ // source: decl.source.value,
304
+ imported: specifier.type === 'ImportDefaultSpecifier' ? 'default' : specifier.imported?.value ?? specifier.local.value,
305
+ local: specifier.local.value,
306
+ importStart: decl.span.start,
307
+ importEnd: decl.span.end,
308
+ importText: printImport(node),
309
+ usages: [],
310
+ })
311
+ }
312
+ }
313
+
314
+ if (imports.length === 0) return imports
315
+
316
+ // fast lookup
317
+ const map = new Map<string, ImportUsage>()
318
+
319
+ for (const item of imports)
320
+ map.set(item.local, item)
321
+
322
+ // walk — skip the keys that never contain us children
323
+ function walk(node: unknown, parent?: unknown) {
324
+ if (!node || typeof node !== 'object') return
325
+
326
+ const n = node as Record<string, unknown>
327
+
328
+ if (n['type'] === 'JSXElement') { // <Counter /> / <Counter></Counter>
329
+ const opening = n['opening'] as Record<string, unknown> | undefined
330
+
331
+ if (opening?.['name'] && (opening['name'] as Record<string, unknown>)['type'] === 'Identifier') {
332
+ const local = (opening['name'] as Record<string, unknown>)['value'] as string
333
+ const found = map.get(local)
334
+
335
+ if (found) {
336
+ found.usages.push({
337
+ type: 'jsx',
338
+ source: found.source,
339
+ imported: found.imported,
340
+ local,
341
+ start: (n['span'] as { start: number }).start,
342
+ end: (n['span'] as { end: number }).end,
343
+ text: printExpression(node),
344
+ node,
345
+ parent,
346
+ })
347
+ }
348
+ }
349
+ }
350
+
351
+ if (n['type'] === 'CallExpression') { // Counter() or {Counter()}
352
+ const callee = n['callee'] as Record<string, unknown> | undefined
353
+
354
+ if (callee?.['type'] === 'Identifier') {
355
+ const local = callee['value'] as string
356
+ const found = map.get(local)
357
+
358
+ if (found) {
359
+ // If the parent is JSXExpressionContainer ({Counter()}), expand the span
360
+ // to include the braces { }
361
+ const containerSpan = getJSXContainerSpan(parent)
362
+ const nodeSpan = n['span'] as { start: number, end: number }
363
+
364
+ found.usages.push({
365
+ type: 'call',
366
+ source: found.source,
367
+ imported: found.imported,
368
+ local,
369
+ start: containerSpan ? containerSpan.start : nodeSpan.start,
370
+ end: containerSpan ? containerSpan.end : nodeSpan.end,
371
+ text: printExpression(node),
372
+ node,
373
+ parent,
374
+ })
375
+ }
376
+ }
377
+ }
378
+
379
+ if (n['type'] === 'Identifier') { // {Counter} in JSX or Counter as a reference
380
+ const local = n['value'] as string
381
+ const found = map.get(local)
382
+ const p = parent as Record<string, unknown> | undefined
383
+
384
+ if (
385
+ found &&
386
+ p?.['type'] !== 'ImportSpecifier' &&
387
+ p?.['type'] !== 'JSXOpeningElement' &&
388
+ p?.['type'] !== 'JSXClosingElement' && // avoids duplicating usage of <X></X>
389
+ p?.['type'] !== 'CallExpression'
390
+ ) {
391
+ // If the parent is JSXExpressionContainer ({Counter}), expand the span to include the braces { }
392
+ const containerSpan = getJSXContainerSpan(parent)
393
+ const nodeSpan = n['span'] as { start: number, end: number }
394
+
395
+ found.usages.push({
396
+ type: 'reference',
397
+ source: found.source,
398
+ imported: found.imported,
399
+ local,
400
+ start: containerSpan ? containerSpan.start : nodeSpan.start,
401
+ end: containerSpan ? containerSpan.end : nodeSpan.end,
402
+ text: local,
403
+ node,
404
+ parent,
405
+ })
406
+ }
407
+ }
408
+
409
+ for (const key in n) { // recurse
410
+ if (SKIP_KEYS.has(key)) continue
411
+
412
+ const value = n[key]
413
+
414
+ if (Array.isArray(value)) {
415
+ for (const item of value)
416
+ walk(item, node)
417
+ } else if (value && typeof value === 'object') {
418
+ walk(value, node)
419
+ }
420
+ }
421
+ }
422
+
423
+ walk(ast)
424
+ return imports
425
+ }
426
+
427
+ /**
428
+ * Removes snippets corresponding to the reported usages from the source code.
429
+ *
430
+ * SWC spans are UTF-8 (base 1) byte offsets, not JS character indices.
431
+ * The conversion is done internally via makeByteToCharMap — the caller
432
+ * does not need to worry about it.
433
+ */
434
+ export function removeUsage(
435
+ code: string,
436
+ targets: Usage | Usage[],
437
+ ): string {
438
+ const list = Array.isArray(targets) ? targets : [targets]
439
+ if (!list.length) return code
440
+
441
+ // converte byte offset (base 1) → char index JS (base 0)
442
+ const b2c = makeByteToCharMap(code)
443
+ const toChar = (byteOffset: number) => b2c[byteOffset - 1]
444
+
445
+ const raw: Array<[number, number]> = []
446
+
447
+ for (const usage of list) {
448
+ if (usage.start < usage.end)
449
+ raw.push([toChar(usage.start), toChar(usage.end)])
450
+ }
451
+
452
+ if (!raw.length) return code
453
+
454
+ raw.sort((a, b) => a[0] - b[0])
455
+
456
+ const merged: Array<[number, number]> = []
457
+
458
+ for (const [start, end] of raw) {
459
+ const last = merged[merged.length - 1]
460
+ if (last && start >= last[0] && end <= last[1]) continue
461
+ if (last && start <= last[1]) {
462
+ last[1] = Math.max(last[1], end)
463
+ continue
464
+ }
465
+ merged.push([start, end])
466
+ }
467
+
468
+ const parts: string[] = []
469
+ let cursor = 0
470
+
471
+ for (const [start, end] of merged) {
472
+ if (start > cursor) parts.push(code.slice(cursor, start))
473
+ cursor = end
474
+ }
475
+
476
+ if (cursor < code.length) parts.push(code.slice(cursor))
477
+
478
+ return parts.join('').replace(/\n{3,}/g, '\n\n')
479
+ }
480
+
481
+
482
+ /**
483
+ * Extracts props from a JSXElement node from the SWC AST.
484
+ * Supports: string literals, numeric literals, boolean (true/false), simple expressions.
485
+ */
486
+ export function extractPropsFromJSX(node: unknown): Record<string, unknown> {
487
+ const n = node as Record<string, unknown>
488
+ const opening = n['opening'] as Record<string, unknown> | undefined
489
+ if (!opening) return {}
490
+
491
+ const attrs = opening['attributes'] as Array<Record<string, unknown>> | undefined
492
+ if (!attrs) return {}
493
+
494
+ const props: Record<string, unknown> = {}
495
+
496
+ for (const attr of attrs) {
497
+ if (attr['type'] !== 'JSXAttribute') continue
498
+
499
+ const nameNode = attr['name'] as Record<string, unknown>
500
+ const propName = nameNode['value'] as string
501
+
502
+ const valueNode = attr['value'] as Record<string, unknown> | null | undefined
503
+
504
+ if (valueNode === null || valueNode === undefined) {
505
+ // <Comp disabled /> → disabled: true
506
+ props[propName] = true
507
+ continue
508
+ }
509
+
510
+ switch (valueNode['type']) {
511
+ case 'StringLiteral':
512
+ props[propName] = valueNode['value']
513
+ break
514
+
515
+ case 'JSXExpressionContainer': {
516
+ const expr = valueNode['expression'] as Record<string, unknown>
517
+ switch (expr['type']) {
518
+ case 'NumericLiteral':
519
+ case 'StringLiteral':
520
+ props[propName] = expr['value']
521
+ break
522
+ case 'BooleanLiteral':
523
+ props[propName] = expr['value']
524
+ break
525
+ case 'NullLiteral':
526
+ props[propName] = null
527
+ break
528
+ case 'ArrayExpression': {
529
+ // Serialize an array of simple literals
530
+ const elems = expr['elements'] as Array<{ expression: Record<string, unknown> }> | undefined
531
+ props[propName] = elems?.map(e => e?.expression?.['value']) ?? []
532
+ break
533
+ }
534
+ default:
535
+ // Complex expressions: keep as undefined (not safely serializable)
536
+ break
537
+ }
538
+ break
539
+ }
540
+
541
+ default:
542
+ break
543
+ }
544
+ }
545
+
546
+ return props
547
+ }
548
+
549
+
550
+
551
+
552
+ const reCache = new Map<string, string>()
553
+ export function removeExports(id: string, code: string, targets: Set<string>) {
554
+ let should = false
555
+ for (const t of targets) {
556
+ if (code.includes(t)) {
557
+ should = true
558
+ break
559
+ }
560
+ }
561
+ if (!should) return null
562
+
563
+ const cacheKey = id + code.length
564
+ if (reCache.has(cacheKey))
565
+ return reCache.get(cacheKey)!
566
+
567
+ let i = 0
568
+ let result = ''
569
+ let mutated = false
570
+
571
+ while (i < code.length) {
572
+ const exportIdx = code.indexOf('export', i)
573
+
574
+ if (exportIdx === -1) {
575
+ result += code.slice(i)
576
+ break
577
+ }
578
+
579
+ result += code.slice(i, exportIdx)
580
+ i = exportIdx
581
+
582
+ const rest = code.slice(i)
583
+
584
+ // export { ... } (multiline support)
585
+ if (/^export\s*{/.test(rest)) {
586
+ let j = i
587
+ let brace = 0
588
+ let contentStart = -1
589
+ let contentEnd = -1
590
+
591
+ while (j < code.length) {
592
+ const ch = code[j]
593
+
594
+ if (ch === '{') {
595
+ brace++
596
+ if (brace === 1) contentStart = j + 1
597
+ } else if (ch === '}') {
598
+ brace--
599
+ if (brace === 0) {
600
+ contentEnd = j
601
+ j++
602
+ break
603
+ }
604
+ }
605
+
606
+ j++
607
+ }
608
+
609
+ if (contentStart !== -1 && contentEnd !== -1) {
610
+ const raw = code.slice(contentStart, contentEnd)
611
+
612
+ const names = raw
613
+ .split(',')
614
+ .map(s => s.trim())
615
+ .filter(Boolean)
616
+ .map(s => s.split(/\s+as\s+/)[0].trim())
617
+
618
+ const shouldRemoveAll =
619
+ names.length > 0 && names.every((n) => targets.has(n))
620
+
621
+ if (shouldRemoveAll) {
622
+ mutated = true
623
+ i = j
624
+ continue
625
+ }
626
+
627
+ // partial remove
628
+ let newEntries = raw
629
+ .split(',')
630
+ .map(s => s.trim())
631
+ .filter(Boolean)
632
+ .filter(entry => {
633
+ const name = entry.split(/\s+as\s+/)[0].trim()
634
+ return !targets.has(name)
635
+ })
636
+
637
+ if (newEntries.length !== names.length) {
638
+ mutated = true
639
+
640
+ if (newEntries.length === 0) {
641
+ i = j
642
+ continue
643
+ }
644
+
645
+ result += 'export { ' + newEntries.join(', ') + ' }'
646
+ i = j
647
+ continue
648
+ }
649
+ }
650
+ }
651
+
652
+ // export function / async function
653
+ const fnMatch =
654
+ /^export\s+async\s+function\s+([a-zA-Z0-9_$]+)/.exec(rest)
655
+ || /^export\s+function\s+([a-zA-Z0-9_$]+)/.exec(rest)
656
+
657
+ if (fnMatch) {
658
+ const name = fnMatch[1]
659
+
660
+ if (targets.has(name)) {
661
+ mutated = true
662
+
663
+ let brace = 0
664
+ let j = i
665
+
666
+ while (j < code.length) {
667
+ if (code[j] === "{") brace++
668
+ else if (code[j] === "}") {
669
+ brace--
670
+ if (brace === 0) {
671
+ j++
672
+ break
673
+ }
674
+ }
675
+ j++
676
+ }
677
+
678
+ i = j
679
+ continue
680
+ }
681
+ }
682
+
683
+ // export const { ... } destructuring
684
+ const destructMatch = /^export\s+const\s*{/.test(rest)
685
+
686
+ if (destructMatch) {
687
+ let j = i
688
+ let brace = 0
689
+ let contentStart = -1
690
+ let contentEnd = -1
691
+
692
+ while (j < code.length) {
693
+ const ch = code[j]
694
+
695
+ if (ch === '{') {
696
+ brace++
697
+ if (brace === 1) contentStart = j + 1
698
+ } else if (ch === '}') {
699
+ brace--
700
+ if (brace === 0) {
701
+ contentEnd = j
702
+ break
703
+ }
704
+ }
705
+
706
+ j++
707
+ }
708
+
709
+ if (contentStart !== -1 && contentEnd !== -1) {
710
+ const raw = code.slice(contentStart, contentEnd)
711
+
712
+ const names = raw
713
+ .split(',')
714
+ .map((s) => s.trim())
715
+ .filter(Boolean)
716
+ .map((s) => s.split(':')[0].trim())
717
+
718
+ const shouldRemoveAll =
719
+ names.length > 0 && names.every((n) => targets.has(n))
720
+
721
+ if (shouldRemoveAll) {
722
+ mutated = true
723
+
724
+ // remove up to ;
725
+ while (j < code.length && code[j] !== ';' && code[j] !== '\n') {
726
+ j++
727
+ }
728
+ j++
729
+
730
+ i = j
731
+ continue
732
+ }
733
+ }
734
+ }
735
+
736
+ // export const foo =
737
+ const varMatch = /^export\s+const\s+([a-zA-Z0-9_$]+)/.exec(rest)
738
+
739
+ if (varMatch) {
740
+ const name = varMatch[1]
741
+
742
+ if (targets.has(name)) {
743
+ mutated = true
744
+
745
+ let j = i
746
+ while (j < code.length && code[j] !== ';' && code[j] !== '\n') {
747
+ j++
748
+ }
749
+ j++
750
+
751
+ i = j
752
+ continue
753
+ }
754
+ }
755
+
756
+ // fallback
757
+ result += 'export'
758
+ i += 'export'.length
759
+ }
760
+
761
+ const retrn = mutated ? result : code
762
+ reCache.set(cacheKey, retrn)
763
+ return retrn
764
+ }