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.
- package/LICENSE +21 -0
- package/README.md +21 -12
- package/package.json +56 -2
- package/src/cli/commands/build.ts +74 -0
- package/src/cli/commands/dev-old.ts +213 -0
- package/src/cli/commands/dev.ts +87 -0
- package/src/cli/commands/preview.ts +33 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/utils.ts +146 -0
- package/src/client/app.tsx +123 -0
- package/src/client/hmr.ts +13 -0
- package/src/client/island.ts +136 -0
- package/src/client/link.ts +70 -0
- package/src/client/router.ts +329 -0
- package/src/client/storage.ts +95 -0
- package/src/compiler/aliases.ts +36 -0
- package/src/compiler/assets.ts +43 -0
- package/src/compiler/ast.ts +764 -0
- package/src/compiler/constants.ts +24 -0
- package/src/compiler/file.ts +60 -0
- package/src/compiler/index.ts +307 -0
- package/src/compiler/island.ts +108 -0
- package/src/compiler/meta.ts +601 -0
- package/src/compiler/routes.ts +29 -0
- package/src/compiler/types.ts +209 -0
- package/src/compiler/utils.ts +5 -0
- package/src/compiler/vite-plugin.ts +324 -0
- package/src/components/link.tsx +38 -0
- package/src/i18n/client.ts +403 -0
- package/src/i18n/element.ts +44 -0
- package/src/i18n/server.ts +56 -0
- package/src/index.ts +28 -0
- package/src/preact/client-dev.ts +5 -0
- package/src/preact/client.ts +87 -0
- package/src/preact/context.ts +41 -0
- package/src/preact/server.ts +186 -0
- package/src/preact/signals.ts +89 -0
- package/src/preact/static-html.ts +45 -0
- package/src/preact/types.ts +26 -0
|
@@ -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
|
+
}
|