ReviewOS

also looking at this

stacks/dtsx

chore(deps): update all non-major dependencies

#2997
Closed renovate[bot] wants to merge renovate/all-minor-patch into main
29 files +1047 -444
.github/workflows/benchmark.ymlmodified+2-2
Changes to .github/workflows/benchmark.yml
@@ -35,10 +35,10 @@ jobs:
3535 pull-requests: write
3636
3737 steps:
38 - uses: actions/checkout@v6.0.0
38 - uses: actions/checkout@v6.0.2
3939
4040 - name: Install Bun
41 uses: oven-sh/setup-bun@v2.0.2
41 uses: oven-sh/setup-bun@v2.1.2
4242
4343 - name: Use cached node_modules
4444 uses: actions/cache@v4.3.0
.github/workflows/buddy-bot.ymlmodified+2-2
Changes to .github/workflows/buddy-bot.yml
@@ -130,7 +130,7 @@ jobs:
130130
131131 - name: Setup PHP and Composer (if needed)
132132 if: ${{ hashFiles('composer.json') != '' }}
133 uses: shivammathur/setup-php@2.35.5
133 uses: shivammathur/setup-php@2.36.0
134134 with:
135135 php-version: '8.4'
136136 tools: composer
@@ -248,7 +248,7 @@ jobs:
248248
249249 - name: Setup PHP and Composer (if needed)
250250 if: ${{ hashFiles('composer.json') != '' }}
251 uses: shivammathur/setup-php@2.35.5
251 uses: shivammathur/setup-php@2.36.0
252252 with:
253253 php-version: '8.4'
254254 tools: composer
.github/workflows/ci.ymlmodified+8-8
Changes to .github/workflows/ci.yml
@@ -18,10 +18,10 @@ jobs:
1818 runs-on: ubuntu-latest
1919
2020 steps:
21 - uses: actions/checkout@v6.0.0
21 - uses: actions/checkout@v6.0.2
2222
2323 - name: Install Bun
24 uses: oven-sh/setup-bun@v2.0.2
24 uses: oven-sh/setup-bun@v2.1.2
2525
2626 - name: Use cached node_modules
2727 uses: actions/cache@v4.3.0
@@ -41,10 +41,10 @@ jobs:
4141 runs-on: ubuntu-latest
4242
4343 steps:
44 - uses: actions/checkout@v6.0.0
44 - uses: actions/checkout@v6.0.2
4545
4646 - name: Install Bun
47 uses: oven-sh/setup-bun@v2.0.2
47 uses: oven-sh/setup-bun@v2.1.2
4848
4949 - name: Use cached node_modules
5050 uses: actions/cache@v4.3.0
@@ -64,10 +64,10 @@ jobs:
6464 runs-on: ubuntu-latest
6565
6666 steps:
67 - uses: actions/checkout@v6.0.0
67 - uses: actions/checkout@v6.0.2
6868
6969 - name: Install Bun
70 uses: oven-sh/setup-bun@v2.0.2
70 uses: oven-sh/setup-bun@v2.1.2
7171
7272 - name: Use cached node_modules
7373 uses: actions/cache@v4.3.0
@@ -87,10 +87,10 @@ jobs:
8787 runs-on: ubuntu-latest
8888
8989 steps:
90 - uses: actions/checkout@v6.0.0
90 - uses: actions/checkout@v6.0.2
9191
9292 - name: Install Bun
93 uses: oven-sh/setup-bun@v2.0.2
93 uses: oven-sh/setup-bun@v2.1.2
9494
9595 - name: Use cached node_modules
9696 uses: actions/cache@v4.3.0
.github/workflows/release.ymlmodified+1-1
Changes to .github/workflows/release.yml
@@ -34,7 +34,7 @@ jobs:
3434 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
3535
3636 - name: Attach Binaries
37 uses: stacksjs/action-releaser@v1.2.7
37 uses: stacksjs/action-releaser@v1.2.8
3838 with:
3939 files: |
4040 packages/dtsx/bin/dtsx-linux-x64.zip
benchmark/index.tsmodified+2-0
Changes to benchmark/index.ts
@@ -20,6 +20,7 @@ import { bench, run, summary } from 'mitata'
2020import { isolatedDeclarationSync } from 'oxc-transform'
2121import ts from 'typescript'
2222import { clearSourceFileCache } from '../packages/dtsx/src/extractor'
23import { clearDeclarationCache } from '../packages/dtsx/src/extractor/extract'
2324import { processSource } from '../packages/dtsx/src/generator'
2425import { clearProcessorCaches } from '../packages/dtsx/src/processor'
2526
@@ -218,6 +219,7 @@ for (const { name, filename, source } of inputs) {
218219 summary(() => {
219220 bench(`dtsx (no-cache) — ${name}`, () => {
220221 clearSourceFileCache()
222 clearDeclarationCache()
221223 clearProcessorCaches()
222224 processSource(source, filename)
223225 })
bun.lockmodified+13-14
Changes to bun.lock
@@ -12,7 +12,7 @@
1212 "@stacksjs/gitlint": "^0.1.5",
1313 "@stacksjs/logsmith": "^0.2.1",
1414 "@types/bun": "^1.3.8",
15 "@typescript/native-preview": "^7.0.0-dev.20260208.1",
15 "@typescript/native-preview": "^7.0.0-dev.20260209.1",
1616 "buddy-bot": "^0.9.12",
1717 "bun-git-hooks": "^0.3.1",
1818 "bun-plugin-dtsx": "workspace:*",
@@ -40,11 +40,10 @@
4040 "devDependencies": {
4141 "bunfig": "^0.15.6",
4242 "oxc-parser": "^0.112.0",
43 "tinyglobby": "^0.2.15",
44 "typescript": "^5.0.0",
43 "typescript": "^5.9.3",
4544 },
4645 "peerDependencies": {
47 "typescript": ">=5.0.0",
46 "typescript": ">=5.9.3",
4847 },
4948 },
5049 "packages/esbuild-plugin": {
@@ -54,7 +53,7 @@
5453 "@stacksjs/dtsx": "workspace:*",
5554 },
5655 "peerDependencies": {
57 "esbuild": ">=0.17.0",
56 "esbuild": ">=0.27.3",
5857 },
5958 },
6059 "packages/tsup-plugin": {
@@ -75,7 +74,7 @@
7574 },
7675 "devDependencies": {
7776 "@stacksjs/dtsx": "workspace:*",
78 "vite": "^5.0.0",
77 "vite": "^5.4.21",
7978 },
8079 "peerDependencies": {
8180 "vite": "^4.0.0 || ^5.0.0",
@@ -337,21 +336,21 @@
337336
338337 "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
339338
340 "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260208.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260208.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260208.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260208.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260208.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260208.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260208.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260208.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-Uvrv3FciZTvvdSpmaaJscQ3Nut9/IPFkHh5CIy0IuDHIqwCoHvkkTOdIFE/rgMfHkIlQHhnj9oF94kzRu8YnXg=="],
339 "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260209.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260209.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260209.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260209.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260209.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260209.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260209.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260209.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-UdA8RC9ic/qi9ajolQQP7ZG8YwtUbxtTMu6FxKBn4pYWicuXqMjzXqH/Ng+VlqqeYrl088P4Ou0erGPuLu4ajw=="],
341340
342 "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260208.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ixnfsxZVziOh/tsuqrjJvXvfBqcilASOnWCsGLaBL9LwpY/0kZxfwvqR8c9DAyB9ilYsmrbu6mi8VtE39eNL9g=="],
341 "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260209.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TyFP7dGMo/Xz37MI3QNfGl3J2i8AKurYwLLD+bG0EDLWnz213wwBwN6U9vMcyatBzfdxKEHHPgdNP0UYCVx3kQ=="],
343342
344 "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260208.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-LH5gacYZOG/mwCBSCYOVMZSQLWNuvBLjJcvm5W7UrTvnMvij9n/spfjHeRicJ1FdHeskCYvOVttshOUxZTQnOA=="],
343 "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260209.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1Dr8toDQcmqKjXd5cQoTAjzMR46cscaojQiazbAPJsU/1PQFgBT36/Mb/epLpzN+ZKKgf7Xd6u2eqH2ze0kF6Q=="],
345344
346 "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260208.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Ep5dHLBW+q3uJBI3WDIWuqBoazjZAo+EIyY/kkv/eoy8vUPsvMElv4vyvLJEYbhlpSrOFYVk8J2KiV+UqvpoVw=="],
345 "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260209.1", "", { "os": "linux", "cpu": "arm" }, "sha512-svmoHHjs5gDekSDW6yLzk9iyDxhMnLKJZ9Xk6b1bSz0swrQNPPTJdR7mbhVMrv4HtXei0LHPlXdTr85AqI5qOQ=="],
347346
348 "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260208.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-adQ3+tzalW6TbLFoL3PqKpL2MyaAaUW8EfmmKmUSpSM2w1ynKChIYmk0KKOFMQXoK3o3hxkvg8PoQbzk8nSEtQ=="],
347 "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260209.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xmGrxP0ERLeczerjJtask6gOln/QhAeELqTmaNoATvU7hZfEzDDxJOgSXZnX6bCIQHdN/Xn49gsyPjzTaK4rAg=="],
349348
350 "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260208.1", "", { "os": "linux", "cpu": "x64" }, "sha512-lCJU9WYwrMWTLkQdvLs6KmFvz/0yZ951D756vsRdC43rLSmzb1GS4T8u9TJ9m5vuM1UST9Mj0+ID5lq5RfHnVA=="],
349 "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260209.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cK4XK3L7TXPj9fIalQcXRqSErdM+pZSqiNgp6QtNsNCyoH2W6J281hnjUA4TmD4TRMSn8CRn7Exy3CGNC3gZkA=="],
351350
352 "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260208.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZEjw0C5dtr9felIUTcpQ65zlTZANmdKcU+qakczrVOyUnF31+FyQtP/Fp2YPOteOAmwrxfCtCsw1Es4zSgtSeA=="],
351 "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260209.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-U919FWN5FZG/1i75+Cv9mnd80Mw2rdFE/to/wJ6DX9m0dUL8IfZARQYPGDXDO1LEC6sV3CyCpCJ/HqsSkqgaAg=="],
353352
354 "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260208.1", "", { "os": "win32", "cpu": "x64" }, "sha512-2ARKZBZwSyxLvQqIl2uqzHESKOYwmEYLJL02B9gPOYUyJOBG+mA75TyeOVTRuafDQv+Fp4xBDDyPOon5ARh+KQ=="],
353 "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260209.1", "", { "os": "win32", "cpu": "x64" }, "sha512-1U/2fG/A1yZtkP59IkDlOVLw2cPtP6NbLROtTytNN0CLSqme+0OXoh+l7wlN2iSmGY5zIeaVcqs4UIL0SiQInQ=="],
355354
356355 "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
357356
package.jsonmodified+1-1
Changes to package.json
@@ -63,7 +63,7 @@
6363 "@stacksjs/gitlint": "^0.1.5",
6464 "@stacksjs/logsmith": "^0.2.1",
6565 "@types/bun": "^1.3.8",
66 "@typescript/native-preview": "^7.0.0-dev.20260208.1",
66 "@typescript/native-preview": "^7.0.0-dev.20260209.1",
6767 "buddy-bot": "^0.9.12",
6868 "bun-git-hooks": "^0.3.1",
6969 "bun-plugin-dtsx": "workspace:*",
packages/dtsx/bin/cli.tsmodified+107-8
Changes to packages/dtsx/bin/cli.ts
@@ -1,18 +1,12 @@
11// Fast path — skip CLI framework for maximum startup speed
22import type { LogLevel } from '../src/logger'
33import type { DtsGenerationConfig, DtsGenerationOption } from '../src/types'
4import { resolve } from 'node:path'
54import process from 'node:process'
6import { CLI } from '@stacksjs/clapp'
7import { version } from '../../../package.json'
8import { getConfig } from '../src/config'
9import { generate, processSource, watch } from '../src/generator'
105
116const _cmd = process.argv[2]
12if (_cmd === 'stdin' || _cmd === 'emit') {
13 const { processSource } = await import('../src/process-source')
14
7if (_cmd === 'stdin' || _cmd === 'emit' || _cmd === '--project' || _cmd === '--project-worker') {
158 if (_cmd === 'stdin') {
9 const { processSource } = await import('../src/process-source')
1610 const chunks: Buffer[] = []
1711 for await (const chunk of process.stdin) {
1812 chunks.push(chunk)
@@ -23,7 +17,105 @@ if (_cmd === 'stdin' || _cmd === 'emit') {
2317 process.stdout.write('\n')
2418 }
2519 }
20 else if (_cmd === '--project-worker') {
21 // Worker subprocess: receives file list on stdin, processes & writes output
22 const { processSourceDirect } = await import('../src/process-source')
23 const { join } = await import('node:path')
24 const { mkdirSync } = await import('node:fs')
25 const wArgs = process.argv.slice(3)
26 let wDir = ''
27 let wOut = ''
28 for (let i = 0; i < wArgs.length; i++) {
29 if (wArgs[i] === '--outdir' && wArgs[i + 1]) { wOut = wArgs[i + 1]; i++ }
30 else if (!wDir) { wDir = wArgs[i] }
31 }
32 mkdirSync(wOut, { recursive: true })
33
34 // Read file list from stdin
35 const wChunks: Buffer[] = []
36 for await (const chunk of process.stdin) { wChunks.push(chunk) }
37 const wFiles = Buffer.concat(wChunks).toString('utf-8').trim().split('\n').filter(Boolean)
38 if (wFiles.length === 0) { process.exit(0) }
39
40 // JIT warmup
41 processSourceDirect('export interface W{a:string}\nexport function f(x:W):void{}\nexport class C{m():void{}}\nexport type T=string\nexport const v:number=1\nexport enum E{A,B}', 'warmup.ts')
42
43 // Pipeline: read → process → write overlapped
44 const wReadPromises = wFiles.map(f => Bun.file(join(wDir, f)).text())
45 const wWritePromises: Promise<number>[] = new Array(wFiles.length)
46 for (let i = 0; i < wFiles.length; i++) {
47 const wSource = await wReadPromises[i]
48 wWritePromises[i] = Bun.write(
49 join(wOut, wFiles[i].replace(/\.ts$/, '.d.ts')),
50 processSourceDirect(wSource, wFiles[i]),
51 )
52 }
53 await Promise.all(wWritePromises)
54 }
55 else if (_cmd === '--project') {
56 // Usage: dtsx --project <dir> --outdir <outdir>
57 const { readdirSync, mkdirSync } = await import('node:fs')
58 const { join } = await import('node:path')
59 const args = process.argv.slice(3)
60 let dir = ''
61 let out = ''
62 for (let i = 0; i < args.length; i++) {
63 if (args[i] === '--outdir' && args[i + 1]) {
64 out = args[i + 1]
65 i++
66 }
67 else if (!dir) {
68 dir = args[i]
69 }
70 }
71 mkdirSync(out, { recursive: true })
72 const files = readdirSync(dir).filter((f: string) => f.endsWith('.ts') && !f.endsWith('.d.ts'))
73
74 // For large file counts, parallelize across CPU cores using worker subprocesses
75 // Use conservative worker count to balance spawn overhead vs parallelism
76 const PARALLEL_THRESHOLD = 200
77 if (files.length >= PARALLEL_THRESHOLD) {
78 const os = await import('node:os')
79 const numCPUs = os.availableParallelism?.() ?? os.cpus().length
80 const numWorkers = Math.min(Math.min(numCPUs, 6), Math.ceil(files.length / 50))
81 const batchSize = Math.ceil(files.length / numWorkers)
82 const procs: Array<ReturnType<typeof Bun.spawn>> = []
83 for (let w = 0; w < numWorkers; w++) {
84 const start = w * batchSize
85 const batch = files.slice(start, start + batchSize)
86 if (batch.length === 0) break
87 const proc = Bun.spawn([process.execPath, '--project-worker', dir, '--outdir', out], {
88 stdin: 'pipe',
89 stdout: 'ignore',
90 stderr: 'inherit',
91 })
92 proc.stdin!.write(batch.join('\n'))
93 proc.stdin!.end()
94 procs.push(proc)
95 }
96
97 const exitCodes = await Promise.all(procs.map(p => p.exited))
98 if (exitCodes.some(c => c !== 0)) { process.exit(1) }
99 }
100 else {
101 // Sequential batch mode for small file counts
102 const { processSourceDirect } = await import('../src/process-source')
103
104 // JIT warmup
105 processSourceDirect('export interface W{a:string}\nexport function f(x:W):void{}\nexport class C{m():void{}}\nexport type T=string\nexport const v:number=1\nexport enum E{A,B}', 'warmup.ts')
106
107 // Read all → Process all → Write all (batch approach avoids per-file await overhead)
108 const sources = await Promise.all(files.map(f => Bun.file(join(dir, f)).text()))
109 const results: string[] = new Array(files.length)
110 for (let i = 0; i < files.length; i++) {
111 results[i] = processSourceDirect(sources[i], files[i])
112 }
113 await Promise.all(files.map((f, i) => Bun.write(join(out, f.replace(/\.ts$/, '.d.ts')), results[i])))
114 }
115 }
26116 else {
117 // emit command
118 const { processSource } = await import('../src/process-source')
27119 const { readFileSync, writeFileSync, mkdirSync } = await import('node:fs')
28120 const filePath = process.argv[3]!
29121 const source = readFileSync(filePath, 'utf-8')
@@ -41,6 +133,13 @@ if (_cmd === 'stdin' || _cmd === 'emit') {
41133 process.exit(0)
42134}
43135
136// Heavy imports — only loaded when CLI framework is needed (not on fast path)
137const { resolve } = await import('node:path')
138const { CLI } = await import('@stacksjs/clapp')
139const { version } = await import('../../../package.json')
140const { getConfig } = await import('../src/config')
141const { generate, processSource, watch } = await import('../src/generator')
142
44143const cli = new CLI('dtsx')
45144
46145const defaultOptions: DtsGenerationConfig = {
packages/dtsx/bin/emit.tsmodified+14-4
Changes to packages/dtsx/bin/emit.ts
@@ -11,11 +11,21 @@ if (cmd === '--project') {
1111 const outdir = args[3]
1212 const files = readdirSync(dir).filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts'))
1313
14 for (const file of files) {
15 const source = readFileSync(join(dir, file), 'utf-8')
16 const dts = processSource(source, file)
17 writeFileSync(join(outdir, file.replace(/\.ts$/, '.d.ts')), dts)
14 // Read all files in parallel using Bun.file()
15 const sources = await Promise.all(
16 files.map(f => Bun.file(join(dir, f)).text()),
17 )
18
19 // Process all files (CPU-bound, sequential)
20 const results: string[] = new Array(files.length)
21 for (let i = 0; i < files.length; i++) {
22 results[i] = processSource(sources[i], files[i])
1823 }
24
25 // Write all files in parallel using Bun.write()
26 await Promise.all(
27 files.map((f, i) => Bun.write(join(outdir, f.replace(/\.ts$/, '.d.ts')), results[i])),
28 )
1929}
2030else if (cmd === 'emit' && args[1]) {
2131 const filePath = args[1]
packages/dtsx/package.jsonmodified+2-2
Changes to packages/dtsx/package.json
@@ -94,7 +94,7 @@
9494 "zip:darwin-arm64": "zip -j bin/dtsx-darwin-arm64.zip bin/dtsx-darwin-arm64"
9595 },
9696 "peerDependencies": {
97 "typescript": ">=5.0.0"
97 "typescript": ">=5.9.3"
9898 },
9999 "peerDependenciesMeta": {
100100 "typescript": {
@@ -107,7 +107,7 @@
107107 "devDependencies": {
108108 "bunfig": "^0.15.6",
109109 "oxc-parser": "^0.112.0",
110 "typescript": "^5.0.0"
110 "typescript": "^5.9.3"
111111 },
112112 "git-hooks": {
113113 "pre-commit": {
packages/dtsx/src/extractor/cache.tsmodified+3-22
Changes to packages/dtsx/src/extractor/cache.ts
@@ -5,6 +5,9 @@
55
66import type { SourceFile } from 'typescript'
77import { createSourceFile, ScriptKind, ScriptTarget } from 'typescript'
8import { hashContent } from './hash'
9
10export { hashContent } from './hash'
811
912/**
1013 * Cache for parsed SourceFile objects to avoid re-parsing
@@ -12,11 +15,6 @@ import { createSourceFile, ScriptKind, ScriptTarget } from 'typescript'
1215 */
1316const sourceFileCache = new Map<string, { sourceFile: SourceFile, contentHash: number | bigint, lastAccess: number }>()
1417
15/**
16 * Whether Bun.hash is available (much faster than JS hash)
17 */
18const hasBunHash = typeof globalThis.Bun?.hash === 'function'
19
2018/**
2119 * Pending async parse operations to prevent duplicate parsing
2220 */
@@ -50,23 +48,6 @@ export interface AsyncParseConfig {
5048 */
5149const MAX_CACHE_SIZE = 100
5250
53/**
54 * Fast hash function for content comparison.
55 * Uses Bun.hash (native wyhash) when available, falls back to JS implementation.
56 */
57export function hashContent(content: string): number | bigint {
58 if (hasBunHash) {
59 return Bun.hash(content)
60 }
61 let hash = 0
62 for (let i = 0; i < content.length; i++) {
63 const char = content.charCodeAt(i)
64 hash = ((hash << 5) - hash) + char
65 hash = hash & hash // Convert to 32-bit integer
66 }
67 return hash
68}
69
7051/**
7152 * Evict oldest entry if cache exceeds max size
7253 * Uses O(n) scan instead of O(n log n) sort since we add one entry at a time
packages/dtsx/src/extractor/directives.tsadded+48-0
Changes to packages/dtsx/src/extractor/directives.ts
@@ -0,0 +1,48 @@
1/**
2 * Triple-slash directive extraction — NO TypeScript dependency.
3 * Pure string processing for maximum startup speed.
4 */
5
6/**
7 * Extract triple-slash directives from source code.
8 * These are special comments like /// <reference types="..." />
9 */
10export function extractTripleSlashDirectives(sourceCode: string): string[] {
11 const directives: string[] = []
12 let lineStart = 0
13
14 // Scan line-by-line without splitting the entire source (stops early for large files)
15 for (let i = 0; i <= sourceCode.length; i++) {
16 if (i === sourceCode.length || sourceCode[i] === '\n') {
17 // Extract and trim the current line
18 let start = lineStart
19 let end = i
20 while (start < end && (sourceCode[start] === ' ' || sourceCode[start] === '\t' || sourceCode[start] === '\r')) start++
21 while (end > start && (sourceCode[end - 1] === ' ' || sourceCode[end - 1] === '\t' || sourceCode[end - 1] === '\r')) end--
22 const trimmed = sourceCode.slice(start, end)
23 lineStart = i + 1
24
25 // Triple-slash directives must be at the very beginning of the file
26 // (only whitespace and other triple-slash directives can precede them)
27 if (trimmed.startsWith('///')) {
28 if (trimmed.match(/^\/\/\/\s*<reference\s+(path|types|lib|no-default-lib)\s*=\s*["'][^"']+["']\s*\/>/)) {
29 directives.push(trimmed)
30 }
31 else if (trimmed.match(/^\/\/\/\s*<amd-module\s+name\s*=\s*["'][^"']+["']\s*\/>/)) {
32 directives.push(trimmed)
33 }
34 else if (trimmed.match(/^\/\/\/\s*<amd-dependency\s+path\s*=\s*["'][^"']+["']/)) {
35 directives.push(trimmed)
36 }
37 }
38 else if (trimmed === '' || trimmed.startsWith('//')) {
39 continue
40 }
41 else {
42 break
43 }
44 }
45 }
46
47 return directives
48}
packages/dtsx/src/extractor/extract.tsadded+55-0
Changes to packages/dtsx/src/extractor/extract.ts
@@ -0,0 +1,55 @@
1/**
2 * Lightweight extraction module — NO TypeScript dependency.
3 * Only uses the string-based scanner for maximum startup speed.
4 * Used by process-source.ts and the CLI fast path.
5 */
6
7import type { Declaration } from '../types'
8import { hashContent } from './hash'
9import { scanDeclarations } from './scanner'
10
11const MAX_DECLARATION_CACHE_SIZE = 100
12const declarationCache = new Map<string, { declarations: Declaration[], contentHash: number | bigint, lastAccess: number }>()
13
14/**
15 * Extract declarations using the fast string scanner (no TS parser).
16 * Results are cached by file path + keepComments flag.
17 */
18export function extractDeclarations(sourceCode: string, filePath: string, keepComments: boolean = true): Declaration[] {
19 const contentHash = hashContent(sourceCode)
20 const cacheKey = `${filePath}:${keepComments ? 1 : 0}`
21 const cached = declarationCache.get(cacheKey)
22 const now = Date.now()
23
24 if (cached && cached.contentHash === contentHash) {
25 cached.lastAccess = now
26 return cached.declarations
27 }
28
29 const declarations = scanDeclarations(sourceCode, filePath, keepComments)
30
31 declarationCache.set(cacheKey, { declarations, contentHash, lastAccess: now })
32
33 if (declarationCache.size > MAX_DECLARATION_CACHE_SIZE) {
34 let oldestKey: string | null = null
35 let oldestTime = Infinity
36 for (const [key, entry] of declarationCache) {
37 if (entry.lastAccess < oldestTime) {
38 oldestTime = entry.lastAccess
39 oldestKey = key
40 }
41 }
42 if (oldestKey) {
43 declarationCache.delete(oldestKey)
44 }
45 }
46
47 return declarations
48}
49
50/**
51 * Clear the declaration cache (for benchmarks and testing)
52 */
53export function clearDeclarationCache(): void {
54 declarationCache.clear()
55}
packages/dtsx/src/extractor/hash.tsadded+19-0
Changes to packages/dtsx/src/extractor/hash.ts
@@ -0,0 +1,19 @@
1/**
2 * Fast hash function for content comparison.
3 * This module is intentionally TS-free to keep the hot path lightweight.
4 */
5
6const hasBunHash = typeof globalThis.Bun?.hash === 'function'
7
8export function hashContent(content: string): number | bigint {
9 if (hasBunHash) {
10 return Bun.hash(content)
11 }
12 let hash = 0
13 for (let i = 0; i < content.length; i++) {
14 const char = content.charCodeAt(i)
15 hash = ((hash << 5) - hash) + char
16 hash = hash & hash
17 }
18 return hash
19}
packages/dtsx/src/extractor/helpers.tsmodified+2-43
Changes to packages/dtsx/src/extractor/helpers.ts
@@ -235,49 +235,8 @@ export function isBuiltInType(typeName: string): boolean {
235235 return BUILT_IN_TYPES.has(typeName)
236236}
237237
238/**
239 * Extract triple-slash directives from the beginning of source code
240 * These are special comments like /// <reference types="..." />
241 */
242export function extractTripleSlashDirectives(sourceCode: string): string[] {
243 const directives: string[] = []
244 let lineStart = 0
245
246 // Scan line-by-line without splitting the entire source (stops early for large files)
247 for (let i = 0; i <= sourceCode.length; i++) {
248 if (i === sourceCode.length || sourceCode[i] === '\n') {
249 // Extract and trim the current line
250 let start = lineStart
251 let end = i
252 while (start < end && (sourceCode[start] === ' ' || sourceCode[start] === '\t' || sourceCode[start] === '\r')) start++
253 while (end > start && (sourceCode[end - 1] === ' ' || sourceCode[end - 1] === '\t' || sourceCode[end - 1] === '\r')) end--
254 const trimmed = sourceCode.slice(start, end)
255 lineStart = i + 1
256
257 // Triple-slash directives must be at the very beginning of the file
258 // (only whitespace and other triple-slash directives can precede them)
259 if (trimmed.startsWith('///')) {
260 if (trimmed.match(/^\/\/\/\s*<reference\s+(path|types|lib|no-default-lib)\s*=\s*["'][^"']+["']\s*\/>/)) {
261 directives.push(trimmed)
262 }
263 else if (trimmed.match(/^\/\/\/\s*<amd-module\s+name\s*=\s*["'][^"']+["']\s*\/>/)) {
264 directives.push(trimmed)
265 }
266 else if (trimmed.match(/^\/\/\/\s*<amd-dependency\s+path\s*=\s*["'][^"']+["']/)) {
267 directives.push(trimmed)
268 }
269 }
270 else if (trimmed === '' || trimmed.startsWith('//')) {
271 continue
272 }
273 else {
274 break
275 }
276 }
277 }
278
279 return directives
280}
238// Re-exported from directives.ts for backward compatibility
239export { extractTripleSlashDirectives } from './directives'
281240
282241/**
283242 * Extract type names from module/namespace text
packages/dtsx/src/extractor/index.tsmodified+2-1
Changes to packages/dtsx/src/extractor/index.ts
@@ -6,7 +6,8 @@ import type { ClassDeclaration, EnumDeclaration, ExportAssignment, ExportDeclara
66import type { Declaration } from '../types'
77import type { AsyncParseConfig } from './cache'
88import { forEachChild, SyntaxKind } from 'typescript'
9import { getSourceFileAsync, hashContent } from './cache'
9import { getSourceFileAsync } from './cache'
10import { hashContent } from './hash'
1011// Re-export all public APIs
1112import { clearSourceFileCache as _clearSFCache } from './cache'
1213import {
packages/dtsx/src/extractor/scanner.tsmodified+589-181
Changes to packages/dtsx/src/extractor/scanner.ts
@@ -95,6 +95,47 @@ export function scanDeclarations(source: string, filename: string, keepComments:
9595 }
9696 }
9797
98 /** Strip trailing comments from a line, preserving leading whitespace */
99 function stripTrailingComment(line: string): string {
100 const lineLen = line.length
101 let inString = 0 // 0=none, quote char code when in string
102 let end = lineLen
103 for (let i = 0; i < lineLen; i++) {
104 const c = line.charCodeAt(i)
105 if (inString) {
106 if (c === CH_BACKSLASH) { i++; continue }
107 if (c === inString) inString = 0
108 continue
109 }
110 if (c === CH_SQUOTE || c === CH_DQUOTE || c === CH_BACKTICK) {
111 inString = c
112 continue
113 }
114 if (c === CH_SLASH && i + 1 < lineLen) {
115 const next = line.charCodeAt(i + 1)
116 if (next === CH_SLASH) {
117 // Trim trailing whitespace before //
118 end = i
119 while (end > 0 && (line.charCodeAt(end - 1) === CH_SPACE || line.charCodeAt(end - 1) === CH_TAB)) end--
120 return line.slice(0, end)
121 }
122 if (next === CH_STAR) {
123 // Inline /* ... */ — find closing and replace with space
124 const closeIdx = line.indexOf('*/', i + 2)
125 if (closeIdx !== -1) {
126 line = line.slice(0, i) + ' ' + line.slice(closeIdx + 2)
127 i-- // re-scan
128 continue
129 }
130 }
131 }
132 }
133 // Trim trailing whitespace
134 end = lineLen
135 while (end > 0 && (line.charCodeAt(end - 1) === CH_SPACE || line.charCodeAt(end - 1) === CH_TAB || line.charCodeAt(end - 1) === CH_CR)) end--
136 return end < lineLen ? line.slice(0, end) : line
137 }
138
98139 function skipString(quote: number): void {
99140 pos++ // skip opening quote
100141 while (pos < len) {
@@ -165,10 +206,72 @@ export function scanDeclarations(source: string, filename: string, keepComments:
165206 pos += 2
166207 return true
167208 }
209 // Check for regex literal: / not followed by / or *, preceded by non-expression char
210 if (isRegexStart()) {
211 skipRegex()
212 return true
213 }
168214 }
169215 return false
170216 }
171217
218 /** Check if `/` at current pos is the start of a regex literal (not division) */
219 function isRegexStart(): boolean {
220 // Look backward for the previous non-whitespace character
221 let p = pos - 1
222 while (p >= 0 && (source.charCodeAt(p) === CH_SPACE || source.charCodeAt(p) === CH_TAB || source.charCodeAt(p) === CH_LF || source.charCodeAt(p) === CH_CR)) p--
223 if (p < 0)
224 return true // start of file
225 const prev = source.charCodeAt(p)
226 // After these chars, `/` starts a regex (not division)
227 // = ( [ ! & | ? : , ; { } ^ ~ + - * % < > \n
228 if (prev === CH_EQUAL || prev === CH_LPAREN || prev === CH_LBRACKET
229 || prev === 33 /* ! */ || prev === 38 /* & */ || prev === 124 /* | */
230 || prev === CH_QUESTION || prev === CH_COLON || prev === CH_COMMA
231 || prev === CH_SEMI || prev === CH_LBRACE || prev === CH_RBRACE
232 || prev === 94 /* ^ */ || prev === 126 /* ~ */
233 || prev === 43 /* + */ || prev === 45 /* - */ || prev === CH_STAR
234 || prev === 37 /* % */ || prev === CH_LANGLE || prev === CH_RANGLE) {
235 return true
236 }
237 // After keywords like return, typeof, void, delete, throw, new, in, of, case
238 if (isIdentChar(prev)) {
239 let wp = p
240 while (wp >= 0 && isIdentChar(source.charCodeAt(wp))) wp--
241 const word = source.slice(wp + 1, p + 1)
242 if (word === 'return' || word === 'typeof' || word === 'void'
243 || word === 'delete' || word === 'throw' || word === 'new'
244 || word === 'in' || word === 'of' || word === 'case'
245 || word === 'instanceof' || word === 'yield' || word === 'await') {
246 return true
247 }
248 }
249 return false
250 }
251
252 /** Skip a regex literal /.../ including flags */
253 function skipRegex(): void {
254 pos++ // skip opening /
255 let inCharClass = false
256 while (pos < len) {
257 const ch = source.charCodeAt(pos)
258 if (ch === CH_BACKSLASH) { pos += 2; continue } // skip escaped char
259 if (inCharClass) {
260 if (ch === CH_RBRACKET)
261 inCharClass = false
262 pos++
263 continue
264 }
265 if (ch === CH_LBRACKET) { inCharClass = true; pos++; continue }
266 if (ch === CH_SLASH) { pos++; break } // closing /
267 if (ch === CH_LF || ch === CH_CR)
268 break // unterminated regex
269 pos++
270 }
271 // Skip flags (g, i, m, s, u, y, d, v)
272 while (pos < len && isIdentChar(source.charCodeAt(pos))) pos++
273 }
274
172275 /** Read an identifier at current position */
173276 function readIdent(): string {
174277 const start = pos
@@ -192,12 +295,22 @@ export function scanDeclarations(source: string, filename: string, keepComments:
192295
193296 /** Check if current position is at a top-level statement-starting keyword */
194297 function isTopLevelKeyword(): boolean {
195 return matchWord('export') || matchWord('import') || matchWord('function')
196 || matchWord('class') || matchWord('interface') || matchWord('type')
197 || matchWord('enum') || matchWord('const') || matchWord('let')
198 || matchWord('var') || matchWord('declare') || matchWord('module')
199 || matchWord('namespace') || matchWord('abstract') || matchWord('async')
200 || matchWord('default')
298 if (pos >= len) return false
299 const ch = source.charCodeAt(pos)
300 switch (ch) {
301 case 101: /* e */ return matchWord('export') || matchWord('enum')
302 case 105: /* i */ return matchWord('import') || matchWord('interface')
303 case 102: /* f */ return matchWord('function')
304 case 99: /* c */ return matchWord('class') || matchWord('const')
305 case 116: /* t */ return matchWord('type')
306 case 108: /* l */ return matchWord('let')
307 case 118: /* v */ return matchWord('var')
308 case 100: /* d */ return matchWord('declare') || matchWord('default')
309 case 109: /* m */ return matchWord('module')
310 case 110: /* n */ return matchWord('namespace')
311 case 97: /* a */ return matchWord('abstract') || matchWord('async')
312 default: return false
313 }
201314 }
202315
203316 /**
@@ -215,12 +328,24 @@ export function scanDeclarations(source: string, filename: string, keepComments:
215328 while (pos < len) {
216329 const c = source.charCodeAt(pos)
217330 if (c === CH_SPACE || c === CH_TAB || c === CH_CR || c === CH_LF) { pos++; continue }
218 if (c === CH_SLASH && pos + 1 < len && source.charCodeAt(pos + 1) === CH_SLASH) {
219 pos += 2
220 while (pos < len && source.charCodeAt(pos) !== CH_LF) pos++
221 if (pos < len)
222 pos++
223 continue
331 if (c === CH_SLASH && pos + 1 < len) {
332 const next = source.charCodeAt(pos + 1)
333 if (next === CH_SLASH) {
334 // Skip line comment
335 pos += 2
336 while (pos < len && source.charCodeAt(pos) !== CH_LF) pos++
337 if (pos < len)
338 pos++
339 continue
340 }
341 if (next === CH_STAR) {
342 // Skip block/JSDoc comment
343 pos += 2
344 while (pos < len - 1 && !(source.charCodeAt(pos) === CH_STAR && source.charCodeAt(pos + 1) === CH_SLASH)) pos++
345 if (pos < len - 1)
346 pos += 2
347 continue
348 }
224349 }
225350 break
226351 }
@@ -331,10 +456,11 @@ export function scanDeclarations(source: string, filename: string, keepComments:
331456 */
332457 function skipExportBraces(): void {
333458 findMatchingClose(CH_LBRACE, CH_RBRACE)
334 skipWhitespaceAndComments()
459 // Only skip whitespace (not comments) to find 'from' — comments belong to next declaration
460 while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++
335461 if (matchWord('from')) {
336462 pos += 4
337 skipWhitespaceAndComments()
463 while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++
338464 if (pos < len) {
339465 const ch = source.charCodeAt(pos)
340466 if (ch === CH_SQUOTE || ch === CH_DQUOTE)
@@ -352,16 +478,16 @@ export function scanDeclarations(source: string, filename: string, keepComments:
352478 */
353479 function skipExportStar(): void {
354480 pos++ // skip *
355 skipWhitespaceAndComments()
481 while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++
356482 if (matchWord('as')) {
357483 pos += 2
358 skipWhitespaceAndComments()
484 while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++
359485 readIdent()
360 skipWhitespaceAndComments()
486 while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++
361487 }
362488 if (matchWord('from')) {
363489 pos += 4
364 skipWhitespaceAndComments()
490 while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++
365491 if (pos < len) {
366492 const ch = source.charCodeAt(pos)
367493 if (ch === CH_SQUOTE || ch === CH_DQUOTE)
@@ -387,8 +513,10 @@ export function scanDeclarations(source: string, filename: string, keepComments:
387513 return undefined
388514
389515 const comments: string[] = []
516 let hasBlockComment = false
390517
391518 // Scan backwards for consecutive comment blocks
519 // Use push() + reverse at end (O(1) per append vs O(n) for unshift)
392520 while (p >= 0) {
393521 // Check for block comment ending with */
394522 if (p >= 1 && source.charCodeAt(p) === CH_SLASH && source.charCodeAt(p - 1) === CH_STAR) {
@@ -396,7 +524,8 @@ export function scanDeclarations(source: string, filename: string, keepComments:
396524 let start = p - 2
397525 while (start >= 1 && !(source.charCodeAt(start) === CH_SLASH && source.charCodeAt(start + 1) === CH_STAR)) start--
398526 if (start >= 0 && source.charCodeAt(start) === CH_SLASH && source.charCodeAt(start + 1) === CH_STAR) {
399 comments.unshift(source.slice(start, p + 1))
527 comments.push(source.slice(start, p + 1))
528 hasBlockComment = true
400529 p = start - 1
401530 while (p >= 0 && isWhitespace(source.charCodeAt(p))) p--
402531 continue
@@ -411,7 +540,11 @@ export function scanDeclarations(source: string, filename: string, keepComments:
411540 const lineText = source.slice(lineStart, p + 1).trim()
412541
413542 if (lineText.startsWith('//')) {
414 // Collect consecutive single-line comments
543 // Don't include // comments above block comments (they're section headers, not doc comments)
544 if (hasBlockComment)
545 break
546
547 // Collect consecutive single-line comments (push + reverse)
415548 const singleLines: string[] = [lineText]
416549 p = lineStart - 1
417550 while (p >= 0 && (source.charCodeAt(p) === CH_LF || source.charCodeAt(p) === CH_CR)) p--
@@ -421,7 +554,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
421554 while (ls > 0 && source.charCodeAt(ls - 1) !== CH_LF) ls--
422555 const lt = source.slice(ls, p + 1).trim()
423556 if (lt.startsWith('//')) {
424 singleLines.unshift(lt)
557 singleLines.push(lt)
425558 p = ls - 1
426559 while (p >= 0 && (source.charCodeAt(p) === CH_LF || source.charCodeAt(p) === CH_CR)) p--
427560 }
@@ -433,7 +566,8 @@ export function scanDeclarations(source: string, filename: string, keepComments:
433566 break
434567 }
435568 }
436 comments.unshift(singleLines.join('\n'))
569 singleLines.reverse()
570 comments.push(singleLines.join('\n'))
437571 continue
438572 }
439573
@@ -441,7 +575,9 @@ export function scanDeclarations(source: string, filename: string, keepComments:
441575 break
442576 }
443577
444 return comments.length > 0 ? comments : undefined
578 if (comments.length === 0) return undefined
579 comments.reverse()
580 return comments
445581 }
446582
447583 // --- High-level extraction ---
@@ -471,15 +607,50 @@ export function scanDeclarations(source: string, filename: string, keepComments:
471607 }
472608
473609 const text = source.slice(stmtStart, pos).trim()
474 const isTypeOnly = text.startsWith('import type ') || text.startsWith('import type{')
475
476 // Detect side-effect imports
477 const isSideEffect = !text.includes('{') && !text.includes(' as ') && !/import\s+\w/.test(text.replace(/import\s+type\s/, 'import ').replace(/import\s+['"]/, 'import_str '))
478 const isSideEffectImport = /^import\s+['"]/.test(text) || /^import\s+type\s+['"]/.test(text.replace('type ', ''))
479
480 // Extract source module
481 const sourceMatch = text.match(/from\s+['"]([^'"]+)['"]/) || text.match(/import\s+['"]([^'"]+)['"]/)
482 const moduleSrc = sourceMatch ? sourceMatch[1] : ''
610 const isTypeOnly = text.charCodeAt(7) === 116 /* t */ && (text.startsWith('import type ') || text.startsWith('import type{'))
611
612 // Detect side-effect imports: import 'foo' or import "foo" (no names imported)
613 // After 'import' + optional 'type', next non-ws must be a quote
614 let isSideEffectImport = false
615 {
616 let si = 6 // skip 'import'
617 while (si < text.length && (text.charCodeAt(si) === CH_SPACE || text.charCodeAt(si) === CH_TAB)) si++
618 if (si < text.length && text.charCodeAt(si) === 116 /* t */ && text.startsWith('type', si)) {
619 si += 4
620 while (si < text.length && (text.charCodeAt(si) === CH_SPACE || text.charCodeAt(si) === CH_TAB)) si++
621 }
622 if (si < text.length) {
623 const qc = text.charCodeAt(si)
624 isSideEffectImport = qc === CH_SQUOTE || qc === CH_DQUOTE
625 }
626 }
627
628 // Extract source module: find 'from' or direct quote after import
629 let moduleSrc = ''
630 {
631 const fromIdx = text.indexOf('from ')
632 if (fromIdx !== -1) {
633 let mi = fromIdx + 5
634 while (mi < text.length && (text.charCodeAt(mi) === CH_SPACE || text.charCodeAt(mi) === CH_TAB)) mi++
635 if (mi < text.length) {
636 const q = text.charCodeAt(mi)
637 if (q === CH_SQUOTE || q === CH_DQUOTE) {
638 const end = text.indexOf(String.fromCharCode(q), mi + 1)
639 if (end !== -1) moduleSrc = text.slice(mi + 1, end)
640 }
641 }
642 }
643 else if (isSideEffectImport) {
644 // import 'module' — find the quote
645 let mi = 6
646 while (mi < text.length && text.charCodeAt(mi) !== CH_SQUOTE && text.charCodeAt(mi) !== CH_DQUOTE) mi++
647 if (mi < text.length) {
648 const q = text.charCodeAt(mi)
649 const end = text.indexOf(String.fromCharCode(q), mi + 1)
650 if (end !== -1) moduleSrc = text.slice(mi + 1, end)
651 }
652 }
653 }
483654
484655 const comments = extractLeadingComments(stmtStart)
485656
@@ -509,34 +680,87 @@ export function scanDeclarations(source: string, filename: string, keepComments:
509680
510681 /** Strip inline comments from a brace block and normalize indentation */
511682 function cleanBraceBlock(raw: string): string {
512 // Remove inline // and /** */ comments, normalize indentation
683 // Strip standalone comment lines and inline trailing comments, preserve relative indentation
684 // Pass 1: filter + compute min indent simultaneously
513685 const lines = raw.split('\n')
514 const cleaned: string[] = []
515 for (const line of lines) {
516 // Strip full-line comments
517 const trimmed = line.trim()
518 if (trimmed.startsWith('//') || trimmed.startsWith('/**') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
519 continue
520 if (trimmed === '' || trimmed === '{' || trimmed === '}') {
521 cleaned.push(trimmed)
686 const filtered: string[] = []
687 const trimCache: string[] = [] // cache trimmed versions
688 const indentCache: number[] = [] // cache indent levels
689 let inBlockComment = false
690 let minIndent = Infinity
691
692 for (let i = 0; i < lines.length; i++) {
693 const line = lines[i]
694 if (inBlockComment) {
695 if (line.includes('*/'))
696 inBlockComment = false
522697 continue
523698 }
524 // Remove trailing inline comments
525 const result = trimmed.replace(/\s*\/\/.*$/, '').replace(/\s*\/\*.*?\*\/\s*/g, ' ').trim()
526 if (result)
527 cleaned.push(result)
699
700 const trimmed = line.trim()
701
702 // Skip standalone comment lines
703 if (trimmed.length === 0) continue
704 const ch0 = trimmed.charCodeAt(0)
705 if (ch0 === CH_SLASH) {
706 const ch1 = trimmed.charCodeAt(1)
707 if (ch1 === CH_SLASH) continue // //
708 if (ch1 === CH_STAR) { // /* or /**
709 if (!trimmed.includes('*/'))
710 inBlockComment = true
711 continue
712 }
713 }
714 if (ch0 === CH_STAR) continue
715
716 // Remove trailing inline comments without regex
717 let cleaned = stripTrailingComment(line)
718 // Strip trailing semicolons from member lines (DTS convention for interfaces)
719 if (cleaned.charCodeAt(cleaned.length - 1) === CH_SEMI)
720 cleaned = cleaned.slice(0, -1)
721 const ct = cleaned.trim()
722 if (!ct) continue
723
724 filtered.push(cleaned)
725 trimCache.push(ct)
726
727 // Compute indent simultaneously (pass 1 + 2 merged)
728 if (ct !== '{' && ct !== '}') {
729 const indent = cleaned.length - cleaned.trimStart().length
730 if (indent < minIndent) minIndent = indent
731 }
732 indentCache.push(cleaned.length - cleaned.trimStart().length)
528733 }
529 // Rebuild with consistent 2-space indent
530 const members: string[] = []
531 for (const line of cleaned) {
532 if (line === '{' || line === '}')
734
735 if (filtered.length === 0)
736 return '{}'
737
738 if (minIndent === Infinity || minIndent <= 2)
739 return filtered.join('\n') // Already at correct indent
740
741 // Pass 2: re-indent using cached values
742 const offset = minIndent - 2
743 const rebased: string[] = new Array(filtered.length)
744 for (let i = 0; i < filtered.length; i++) {
745 const t = trimCache[i]
746 if (t === '{') {
747 rebased[i] = t
533748 continue
534 if (line)
535 members.push(` ${line}`)
749 }
750 const currentIndent = indentCache[i]
751 if (currentIndent > minIndent) {
752 rebased[i] = filtered[i]
753 }
754 else if (currentIndent === minIndent && (t.charCodeAt(0) === CH_RBRACE || t.charCodeAt(0) === CH_RBRACKET || t.charCodeAt(0) === CH_RPAREN)) {
755 rebased[i] = filtered[i]
756 }
757 else {
758 const newIndent = Math.max(0, currentIndent - offset)
759 rebased[i] = ' '.repeat(newIndent) + t
760 }
536761 }
537 if (members.length === 0)
538 return '{}'
539 return `{\n${members.join('\n')}\n}`
762
763 return rebased.join('\n')
540764 }
541765
542766 /** Extract type parameters <...> (normalized to single line) */
@@ -546,9 +770,25 @@ export function scanDeclarations(source: string, filename: string, keepComments:
546770 const start = pos
547771 findMatchingClose(CH_LANGLE, CH_RANGLE)
548772 const raw = source.slice(start, pos)
549 // Normalize multi-line generics to single line
773 // Normalize multi-line generics to single line (avoid regex)
550774 if (raw.includes('\n')) {
551 return raw.replace(/\s*\n\s*/g, ' ').replace(/\s+/g, ' ').replace(/^<\s+/, '<').replace(/\s+>$/, '>')
775 // Collapse whitespace/newlines to single spaces
776 let result = ''
777 let prevSpace = false
778 for (let i = 0; i < raw.length; i++) {
779 const c = raw.charCodeAt(i)
780 if (c === 32 || c === 9 || c === 10 || c === 13) {
781 if (!prevSpace && result.length > 0) { result += ' '; prevSpace = true }
782 }
783 else {
784 result += raw[i]
785 prevSpace = false
786 }
787 }
788 // Trim spaces around < and >
789 if (result.charCodeAt(1) === 32) result = '<' + result.slice(2)
790 if (result.charCodeAt(result.length - 2) === 32) result = result.slice(0, -2) + '>'
791 return result
552792 }
553793 return raw
554794 }
@@ -618,6 +858,86 @@ export function scanDeclarations(source: string, filename: string, keepComments:
618858 return `(${dtsParams.join(', ')})`
619859 }
620860
861 /** Clean a destructured parameter name: strip defaults and rest operators */
862 function cleanDestructuredName(name: string): string {
863 if (!name.startsWith('{') && !name.startsWith('['))
864 return name
865
866 let result = ''
867 let i = 0
868 let depth = 0
869 let inStr = false
870 let strCh = 0
871
872 while (i < name.length) {
873 const ch = name.charCodeAt(i)
874
875 if (inStr) {
876 if (ch === CH_BACKSLASH && i + 1 < name.length) { result += name[i] + name[i + 1]; i += 2; continue }
877 if (ch === strCh)
878 inStr = false
879 result += name[i]; i++; continue
880 }
881 if (ch === CH_SQUOTE || ch === CH_DQUOTE || ch === CH_BACKTICK) {
882 inStr = true; strCh = ch
883 result += name[i]; i++; continue
884 }
885
886 if (ch === CH_LBRACE || ch === CH_LBRACKET || ch === CH_LPAREN)
887 depth++
888 else if (ch === CH_RBRACE || ch === CH_RBRACKET || ch === CH_RPAREN)
889 depth--
890
891 // At depth 1+, strip `= <default>` by skipping until , or closing bracket
892 if (depth >= 1 && ch === CH_EQUAL
893 && (i + 1 >= name.length || (name.charCodeAt(i + 1) !== CH_EQUAL && name.charCodeAt(i + 1) !== CH_RANGLE))) {
894 // Also strip preceding whitespace
895 while (result.length > 0 && (result[result.length - 1] === ' ' || result[result.length - 1] === '\t' || result[result.length - 1] === '\n'))
896 result = result.slice(0, -1)
897 // Skip the default value
898 i++
899 let skipDepth = 0
900 while (i < name.length) {
901 const sc = name.charCodeAt(i)
902 if (sc === CH_SQUOTE || sc === CH_DQUOTE || sc === CH_BACKTICK) {
903 const q = sc; i++
904 while (i < name.length) {
905 if (name.charCodeAt(i) === CH_BACKSLASH) { i += 2; continue }
906 if (name.charCodeAt(i) === q) { i++; break }
907 i++
908 }
909 continue
910 }
911 if (sc === CH_LBRACE || sc === CH_LBRACKET || sc === CH_LPAREN) {
912 skipDepth++
913 }
914 else if (sc === CH_RBRACE || sc === CH_RBRACKET || sc === CH_RPAREN) {
915 if (skipDepth === 0)
916 break
917 skipDepth--
918 }
919 else if (sc === CH_COMMA && skipDepth === 0) {
920 break
921 }
922 i++
923 }
924 continue
925 }
926
927 // At depth 1, strip `...` rest operator
928 if (depth === 1 && ch === CH_DOT && i + 2 < name.length
929 && name.charCodeAt(i + 1) === CH_DOT && name.charCodeAt(i + 2) === CH_DOT) {
930 i += 3
931 continue
932 }
933
934 result += name[i]
935 i++
936 }
937
938 return result
939 }
940
621941 /** Build a single DTS parameter from raw source text */
622942 function buildSingleDtsParam(raw: string): string {
623943 let p = raw.trim()
@@ -720,6 +1040,28 @@ export function scanDeclarations(source: string, filename: string, keepComments:
7201040 type = 'unknown'
7211041 }
7221042
1043 // Clean destructured parameter names (strip defaults, rest operators)
1044 name = cleanDestructuredName(name)
1045 if ((name.startsWith('{') || name.startsWith('[')) && name.includes('\n')) {
1046 // Collapse to single line only if short enough, otherwise normalize indent
1047 const collapsed = name.replace(/\s*\n\s*/g, ' ').replace(/\s+/g, ' ')
1048 if (collapsed.length <= 40) {
1049 name = collapsed
1050 }
1051 else {
1052 // Normalize to 2-space indent per line, keep braces unindented
1053 const lines = name.split('\n')
1054 name = lines.map((l) => {
1055 const t = l.trim()
1056 if (!t)
1057 return ''
1058 if (t === '{' || t === '}' || t === '[' || t === ']' || t.startsWith('}') || t.startsWith(']'))
1059 return t
1060 return ` ${t}`
1061 }).join('\n')
1062 }
1063 }
1064
7231065 // Handle optional marker
7241066 const isOptional = name.endsWith('?') || hasDefault
7251067 if (name.endsWith('?'))
@@ -732,12 +1074,28 @@ export function scanDeclarations(source: string, filename: string, keepComments:
7321074 return `${name}${optionalMarker}: ${type}`
7331075 }
7341076
1077 /** Check if string is a numeric literal (integer or decimal, optionally negative) */
1078 function isNumericLiteral(v: string): boolean {
1079 let i = 0
1080 if (i < v.length && v.charCodeAt(i) === 45 /* - */) i++
1081 if (i >= v.length) return false
1082 const c = v.charCodeAt(i)
1083 if (c < 48 || c > 57) return false // not a digit
1084 while (i < v.length && v.charCodeAt(i) >= 48 && v.charCodeAt(i) <= 57) i++
1085 if (i < v.length && v.charCodeAt(i) === 46 /* . */) {
1086 i++
1087 if (i >= v.length || v.charCodeAt(i) < 48 || v.charCodeAt(i) > 57) return false
1088 while (i < v.length && v.charCodeAt(i) >= 48 && v.charCodeAt(i) <= 57) i++
1089 }
1090 return i === v.length
1091 }
1092
7351093 /** Infer type from a default value expression (simple cases) */
7361094 function inferTypeFromDefault(value: string): string {
7371095 const v = value.trim()
7381096 if (v === 'true' || v === 'false')
7391097 return 'boolean'
740 if (/^-?\d+(\.\d+)?$/.test(v))
1098 if (isNumericLiteral(v))
7411099 return 'number'
7421100 if ((v.startsWith('\'') && v.endsWith('\'')) || (v.startsWith('"') && v.endsWith('"')))
7431101 return 'string'
@@ -753,7 +1111,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
7531111 const v = value.trim()
7541112 if (v === 'true' || v === 'false')
7551113 return v
756 if (/^-?\d+(\.\d+)?$/.test(v))
1114 if (isNumericLiteral(v))
7571115 return v
7581116 if ((v.startsWith('\'') && v.endsWith('\'')) || (v.startsWith('"') && v.endsWith('"')))
7591117 return v
@@ -764,8 +1122,11 @@ export function scanDeclarations(source: string, filename: string, keepComments:
7641122 function extractAssertion(initText: string): string | null {
7651123 if (initText.endsWith('as const'))
7661124 return null
767 const m = initText.match(/\bas\s+(.+)$/)
768 return m ? m[1].trim() : null
1125 // Find last ' as ' — the spaces already serve as word boundaries
1126 const asIdx = initText.lastIndexOf(' as ')
1127 if (asIdx === -1) return null
1128 const afterAs = initText.slice(asIdx + 4).trim()
1129 return afterAs || null
7691130 }
7701131
7711132 /** Extract return type annotation `: ReturnType` after params, before `{` or `;` */
@@ -877,16 +1238,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
8771238
8781239 // Build DTS text
8791240 const dtsParams = buildDtsParams(rawParams)
880 const parts: string[] = []
881 if (isExported)
882 parts.push('export ')
883 parts.push('declare function ')
884 parts.push(name || 'default')
885 parts.push(generics)
886 parts.push(dtsParams)
887 parts.push(': ', returnType, ';')
888
889 const text = parts.join('')
1241 const text = (isExported ? 'export ' : '') + 'declare function ' + (name || 'default') + generics + dtsParams + ': ' + returnType + ';'
8901242 const comments = extractLeadingComments(declStart)
8911243
8921244 // Record index if this function had a body (implementation signature for overloads)
@@ -1077,16 +1429,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
10771429 const body = cleanBraceBlock(rawBody)
10781430
10791431 // Build DTS text
1080 const parts: string[] = []
1081 if (isExported)
1082 parts.push('export ')
1083 parts.push('declare interface ', name)
1084 parts.push(generics)
1085 if (extendsClause)
1086 parts.push(' extends ', extendsClause)
1087 parts.push(' ', body)
1088
1089 const text = parts.join('')
1432 const text = (isExported ? 'export ' : '') + 'declare interface ' + name + generics + (extendsClause ? ' extends ' + extendsClause : '') + ' ' + body
10901433 const comments = extractLeadingComments(declStart)
10911434
10921435 return {
@@ -1143,12 +1486,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
11431486 pos++
11441487
11451488 // Build DTS text
1146 const parts: string[] = []
1147 if (isExported)
1148 parts.push('export ')
1149 parts.push('type ', name, generics, ' = ', typeBody)
1150
1151 const text = parts.join('')
1489 const text = (isExported ? 'export ' : '') + 'type ' + name + generics + ' = ' + typeBody
11521490 const comments = extractLeadingComments(declStart)
11531491
11541492 return {
@@ -1262,20 +1600,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
12621600 const classBody = buildClassBodyDts()
12631601
12641602 // Build DTS text
1265 const parts: string[] = []
1266 if (isExported)
1267 parts.push('export ')
1268 parts.push('declare ')
1269 if (isAbstract)
1270 parts.push('abstract ')
1271 parts.push('class ', name, generics)
1272 if (extendsClause)
1273 parts.push(' extends ', extendsClause)
1274 if (implementsList && implementsList.length > 0)
1275 parts.push(' implements ', implementsList.join(', '))
1276 parts.push(' ', classBody)
1277
1278 const text = parts.join('')
1603 const text = (isExported ? 'export ' : '') + 'declare ' + (isAbstract ? 'abstract ' : '') + 'class ' + name + generics + (extendsClause ? ' extends ' + extendsClause : '') + (implementsList && implementsList.length > 0 ? ' implements ' + implementsList.join(', ') : '') + ' ' + classBody
12791604 const comments = extractLeadingComments(declStart)
12801605
12811606 return {
@@ -1958,7 +2283,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
19582283 skipWhitespaceAndComments()
19592284 const nname = readIdent()
19602285 skipWhitespaceAndComments()
1961 const body = buildNamespaceBodyDts(`${indent} `)
2286 const body = buildNamespaceBodyDts(indent)
19622287 lines.push(`${indent}${prefix}${kw} ${nname} ${body}`)
19632288 }
19642289 else if (matchWord('abstract')) {
@@ -1995,7 +2320,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
19952320 skipToStatementEnd()
19962321 const defText = source.slice(defStart, pos).trim().replace(/;$/, '')
19972322 if (defText) {
1998 lines.push(`${indent}export default ${defText}`)
2323 lines.push(`${indent}export default ${defText};`)
19992324 }
20002325 }
20012326 else if (!hasExport && (source.charCodeAt(pos) === CH_SQUOTE || source.charCodeAt(pos) === CH_DQUOTE || source.charCodeAt(pos) === CH_BACKTICK)) {
@@ -2055,13 +2380,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
20552380 const body = pos < len && source.charCodeAt(pos) === CH_LBRACE ? buildNamespaceBodyDts() : '{}'
20562381
20572382 // Build DTS text
2058 const parts: string[] = []
2059 if (isExported)
2060 parts.push('export ')
2061 parts.push('declare ')
2062 parts.push(keyword, ' ', name, ' ', body)
2063
2064 const text = parts.join('')
2383 const text = (isExported ? 'export ' : '') + 'declare ' + keyword + ' ' + name + ' ' + body
20652384 const comments = extractLeadingComments(declStart)
20662385 const isAmbient = name.startsWith('\'') || name.startsWith('"')
20672386
@@ -2077,7 +2396,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
20772396 }
20782397 }
20792398
2080 // --- Main scan loop ---
2399 // --- Main scan loop (first-char dispatch for speed) ---
20812400
20822401 while (pos < len) {
20832402 skipWhitespaceAndComments()
@@ -2085,25 +2404,28 @@ export function scanDeclarations(source: string, filename: string, keepComments:
20852404 break
20862405
20872406 const stmtStart = pos
2407 const ch0 = source.charCodeAt(pos)
20882408
2089 // Detect keywords at top level
2090 if (matchWord('import')) {
2409 // Fast dispatch on first character of keyword
2410 if (ch0 === 105 /* i */ && matchWord('import')) {
20912411 declarations.push(extractImport(stmtStart))
20922412 }
2093 else if (matchWord('export')) {
2413 else if (ch0 === 101 /* e */ && matchWord('export')) {
20942414 pos += 6 // skip 'export'
20952415 skipWhitespaceAndComments()
2416 const ech = source.charCodeAt(pos)
20962417
2097 if (matchWord('default')) {
2418 if (ech === 100 /* d */ && matchWord('default')) {
20982419 pos += 7
20992420 skipWhitespaceAndComments()
2421 const dch = source.charCodeAt(pos)
21002422
2101 if (matchWord('function')) {
2423 if (dch === 102 /* f */ && matchWord('function')) {
21022424 const decl = extractFunction(stmtStart, true, false, true)
21032425 if (decl)
21042426 declarations.push(decl)
21052427 }
2106 else if (matchWord('async') && peekAfterWord('async') !== CH_SEMI) {
2428 else if (dch === 97 /* a */ && matchWord('async') && peekAfterWord('async') !== CH_SEMI) {
21072429 pos += 5
21082430 skipWhitespaceAndComments()
21092431 if (matchWord('function')) {
@@ -2113,7 +2435,6 @@ export function scanDeclarations(source: string, filename: string, keepComments:
21132435 }
21142436 else {
21152437 // export default async expression
2116 const text = source.slice(stmtStart, pos).trim()
21172438 skipToStatementEnd()
21182439 const fullText = source.slice(stmtStart, pos).trim()
21192440 declarations.push({
@@ -2127,11 +2448,11 @@ export function scanDeclarations(source: string, filename: string, keepComments:
21272448 })
21282449 }
21292450 }
2130 else if (matchWord('class')) {
2451 else if (dch === 99 /* c */ && matchWord('class')) {
21312452 const decl = extractClass(stmtStart, true, false)
21322453 declarations.push(decl)
21332454 }
2134 else if (matchWord('abstract')) {
2455 else if (dch === 97 /* a */ && matchWord('abstract')) {
21352456 pos += 8
21362457 skipWhitespaceAndComments()
21372458 if (matchWord('class')) {
@@ -2156,7 +2477,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
21562477 })
21572478 }
21582479 }
2159 else if (matchWord('type')) {
2480 else if (ech === 116 /* t */ && matchWord('type')) {
21602481 // Could be `export type Name = ...` or `export type { ... }`
21612482 const savedPos = pos
21622483 pos += 4
@@ -2199,16 +2520,16 @@ export function scanDeclarations(source: string, filename: string, keepComments:
21992520 declarations.push(decl)
22002521 }
22012522 }
2202 else if (matchWord('interface')) {
2523 else if (ech === 105 /* i */ && matchWord('interface')) {
22032524 const decl = extractInterface(stmtStart, true)
22042525 declarations.push(decl)
22052526 }
2206 else if (matchWord('function')) {
2527 else if (ech === 102 /* f */ && matchWord('function')) {
22072528 const decl = extractFunction(stmtStart, true, false, false)
22082529 if (decl)
22092530 declarations.push(decl)
22102531 }
2211 else if (matchWord('async')) {
2532 else if (ech === 97 /* a */ && matchWord('async')) {
22122533 pos += 5
22132534 skipWhitespaceAndComments()
22142535 if (matchWord('function')) {
@@ -2220,62 +2541,67 @@ export function scanDeclarations(source: string, filename: string, keepComments:
22202541 skipToStatementEnd()
22212542 }
22222543 }
2223 else if (matchWord('class')) {
2224 const decl = extractClass(stmtStart, true, false)
2225 declarations.push(decl)
2226 }
2227 else if (matchWord('abstract')) {
2228 pos += 8
2229 skipWhitespaceAndComments()
2544 else if (ech === 99 /* c */) {
22302545 if (matchWord('class')) {
2231 const decl = extractClass(stmtStart, true, true)
2546 const decl = extractClass(stmtStart, true, false)
22322547 declarations.push(decl)
22332548 }
2234 }
2235 else if (matchWord('const')) {
2236 // Could be 'export const enum' or 'export const ...'
2237 const savedPos = pos
2238 pos += 5
2239 skipWhitespaceAndComments()
2240 if (matchWord('enum')) {
2241 pos = savedPos + 5
2549 else if (matchWord('const')) {
2550 // Could be 'export const enum' or 'export const ...'
2551 const savedPos = pos
2552 pos += 5
22422553 skipWhitespaceAndComments()
2243 const decl = extractEnum(stmtStart, true, true)
2244 declarations.push(decl)
2554 if (matchWord('enum')) {
2555 pos = savedPos + 5
2556 skipWhitespaceAndComments()
2557 const decl = extractEnum(stmtStart, true, true)
2558 declarations.push(decl)
2559 }
2560 else {
2561 pos = savedPos
2562 const decls = extractVariable(stmtStart, 'const', true)
2563 declarations.push(...decls)
2564 }
22452565 }
22462566 else {
2247 pos = savedPos
2248 const decls = extractVariable(stmtStart, 'const', true)
2249 declarations.push(...decls)
2567 skipToStatementEnd()
2568 }
2569 }
2570 else if (ech === 97 /* a */ && matchWord('abstract')) {
2571 pos += 8
2572 skipWhitespaceAndComments()
2573 if (matchWord('class')) {
2574 const decl = extractClass(stmtStart, true, true)
2575 declarations.push(decl)
22502576 }
22512577 }
2252 else if (matchWord('let')) {
2578 else if (ech === 108 /* l */ && matchWord('let')) {
22532579 const decls = extractVariable(stmtStart, 'let', true)
22542580 declarations.push(...decls)
22552581 }
2256 else if (matchWord('var')) {
2582 else if (ech === 118 /* v */ && matchWord('var')) {
22572583 const decls = extractVariable(stmtStart, 'var', true)
22582584 declarations.push(...decls)
22592585 }
2260 else if (matchWord('enum')) {
2586 else if (ech === 101 /* e */ && matchWord('enum')) {
22612587 const decl = extractEnum(stmtStart, true, false)
22622588 declarations.push(decl)
22632589 }
2264 else if (matchWord('declare')) {
2590 else if (ech === 100 /* d */ && matchWord('declare')) {
22652591 pos += 7
22662592 skipWhitespaceAndComments()
22672593 // export declare ...
22682594 handleDeclare(stmtStart, true)
22692595 }
2270 else if (matchWord('namespace')) {
2596 else if (ech === 110 /* n */ && matchWord('namespace')) {
22712597 const decl = extractModule(stmtStart, true, 'namespace')
22722598 declarations.push(decl)
22732599 }
2274 else if (matchWord('module')) {
2600 else if (ech === 109 /* m */ && matchWord('module')) {
22752601 const decl = extractModule(stmtStart, true, 'module')
22762602 declarations.push(decl)
22772603 }
2278 else if (source.charCodeAt(pos) === CH_LBRACE) {
2604 else if (ech === CH_LBRACE) {
22792605 // export { ... } or export { ... } from '...'
22802606 skipExportBraces()
22812607 const text = source.slice(stmtStart, pos).trim()
@@ -2292,7 +2618,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
22922618 end: pos,
22932619 })
22942620 }
2295 else if (source.charCodeAt(pos) === CH_STAR) {
2621 else if (ech === CH_STAR) {
22962622 // export * from '...'
22972623 skipExportStar()
22982624 const text = source.slice(stmtStart, pos).trim()
@@ -2326,56 +2652,96 @@ export function scanDeclarations(source: string, filename: string, keepComments:
23262652 }
23272653 }
23282654 }
2329 else if (matchWord('declare')) {
2655 else if (ch0 === 100 /* d */ && matchWord('declare')) {
23302656 pos += 7
23312657 skipWhitespaceAndComments()
23322658 handleDeclare(stmtStart, false)
23332659 }
2334 else if (matchWord('interface')) {
2660 else if (ch0 === 105 /* i */ && matchWord('interface')) {
23352661 // Non-exported interface
23362662 const decl = extractInterface(stmtStart, false)
23372663 nonExportedTypes.set(decl.name, decl)
23382664 }
2339 else if (matchWord('type')) {
2665 else if (ch0 === 116 /* t */ && matchWord('type')) {
23402666 // Non-exported type alias
23412667 const decl = extractTypeAlias(stmtStart, false)
23422668 nonExportedTypes.set(decl.name, decl)
23432669 declarations.push(decl)
23442670 }
2345 else if (matchWord('function') || matchWord('async')) {
2671 else if (ch0 === 102 /* f */ && matchWord('function')) {
23462672 // Non-exported function — skip for DTS
23472673 skipToStatementEnd()
23482674 }
2349 else if (matchWord('class') || matchWord('abstract')) {
2350 // Non-exported class
2351 if (matchWord('abstract')) {
2675 else if (ch0 === 97 /* a */) {
2676 if (matchWord('async')) {
2677 // Non-exported async function — skip for DTS
2678 skipToStatementEnd()
2679 }
2680 else if (matchWord('abstract')) {
23522681 pos += 8
23532682 skipWhitespaceAndComments()
2683 if (matchWord('class')) {
2684 const decl = extractClass(stmtStart, false, false)
2685 nonExportedTypes.set(decl.name, decl)
2686 declarations.push(decl)
2687 }
2688 else {
2689 skipToStatementEnd()
2690 }
2691 }
2692 else {
2693 pos++
2694 skipToStatementEnd()
23542695 }
2696 }
2697 else if (ch0 === 99 /* c */) {
23552698 if (matchWord('class')) {
23562699 const decl = extractClass(stmtStart, false, false)
23572700 nonExportedTypes.set(decl.name, decl)
23582701 declarations.push(decl)
23592702 }
2703 else if (matchWord('const')) {
2704 // Check for 'const enum' before skipping as variable
2705 const savedPos = pos
2706 pos += 5
2707 skipWhitespaceAndComments()
2708 if (matchWord('enum')) {
2709 pos = savedPos + 5
2710 skipWhitespaceAndComments()
2711 const decl = extractEnum(stmtStart, false, true)
2712 nonExportedTypes.set(decl.name, decl)
2713 declarations.push(decl)
2714 }
2715 else {
2716 // Non-exported const variable — skip for DTS
2717 pos = savedPos
2718 skipToStatementEnd()
2719 }
2720 }
23602721 else {
2722 pos++
23612723 skipToStatementEnd()
23622724 }
23632725 }
2364 else if (matchWord('enum')) {
2726 else if (ch0 === 101 /* e */ && matchWord('enum')) {
23652727 // Non-exported enum
23662728 const decl = extractEnum(stmtStart, false, false)
23672729 nonExportedTypes.set(decl.name, decl)
23682730 declarations.push(decl)
23692731 }
2370 else if (matchWord('const') || matchWord('let') || matchWord('var')) {
2732 else if (ch0 === 108 /* l */ && matchWord('let')) {
23712733 // Non-exported variable — skip for DTS
23722734 skipToStatementEnd()
23732735 }
2374 else if (matchWord('module')) {
2736 else if (ch0 === 118 /* v */ && matchWord('var')) {
2737 // Non-exported variable — skip for DTS
2738 skipToStatementEnd()
2739 }
2740 else if (ch0 === 109 /* m */ && matchWord('module')) {
23752741 const decl = extractModule(stmtStart, false, 'module')
23762742 declarations.push(decl)
23772743 }
2378 else if (matchWord('namespace')) {
2744 else if (ch0 === 110 /* n */ && matchWord('namespace')) {
23792745 const decl = extractModule(stmtStart, false, 'namespace')
23802746 declarations.push(decl)
23812747 }
@@ -2539,7 +2905,7 @@ export function scanDeclarations(source: string, filename: string, keepComments:
25392905 // declare global { ... }
25402906 pos += 6
25412907 skipWhitespaceAndComments()
2542 const body = pos < len && source.charCodeAt(pos) === CH_LBRACE ? extractBraceBlock() : '{}'
2908 const body = pos < len && source.charCodeAt(pos) === CH_LBRACE ? buildNamespaceBodyDts() : '{}'
25432909 const text = `declare global ${body}`
25442910 const comments = extractLeadingComments(stmtStart)
25452911 declarations.push({
@@ -2558,27 +2924,69 @@ export function scanDeclarations(source: string, filename: string, keepComments:
25582924 }
25592925}
25602926
2927/** Check if name appears as a whole word in text (fast includes + boundary check) */
2928function isWordInText(name: string, text: string): boolean {
2929 let searchFrom = 0
2930 const nameLen = name.length
2931 while (searchFrom < text.length) {
2932 const idx = text.indexOf(name, searchFrom)
2933 if (idx === -1) return false
2934 // Check word boundaries
2935 const before = idx > 0 ? text.charCodeAt(idx - 1) : 32
2936 const after = idx + nameLen < text.length ? text.charCodeAt(idx + nameLen) : 32
2937 const beforeOk = !((before >= 65 && before <= 90) || (before >= 97 && before <= 122) || (before >= 48 && before <= 57) || before === 95 || before === 36)
2938 const afterOk = !((after >= 65 && after <= 90) || (after >= 97 && after <= 122) || (after >= 48 && after <= 57) || after === 95 || after === 36)
2939 if (beforeOk && afterOk) return true
2940 searchFrom = idx + 1
2941 }
2942 return false
2943}
2944
25612945/** Resolve non-exported types that are referenced by exported declarations */
25622946function resolveReferencedTypes(declarations: Declaration[], nonExportedTypes: Map<string, Declaration>): void {
2563 // Build combined text of all exported declarations
2564 const exportedTexts: string[] = []
2565 for (const decl of declarations) {
2566 if (decl.isExported && decl.kind !== 'import') {
2567 exportedTexts.push(decl.text)
2947 // Iteratively resolve referenced non-exported types (transitive closure)
2948 const resolved = new Set<string>()
2949 const declNames = new Set<string>()
2950 for (const d of declarations) declNames.add(d.name)
2951
2952 for (;;) {
2953 // Build combined text of all current declarations (excluding imports)
2954 let combinedText = ''
2955 for (let i = 0; i < declarations.length; i++) {
2956 if (declarations[i].kind !== 'import') {
2957 combinedText += declarations[i].text
2958 combinedText += '\n'
2959 }
25682960 }
2569 }
2570 const combinedText = exportedTexts.join('\n')
2571
2572 // Check each non-exported type
2573 for (const [name, decl] of nonExportedTypes) {
2574 // Simple word-boundary check
2575 const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
2576 if (re.test(combinedText)) {
2577 // Check it's not already in declarations
2578 const existing = declarations.find(d => d.name === name)
2579 if (!existing) {
2580 declarations.push(decl)
2961
2962 // Collect referenced non-exported types not yet resolved
2963 const toInsert: Declaration[] = []
2964 for (const [name, decl] of nonExportedTypes) {
2965 if (resolved.has(name))
2966 continue
2967 // Fast path: includes() check first (avoids RegExp entirely for non-matches)
2968 if (combinedText.includes(name) && isWordInText(name, combinedText)) {
2969 if (!declNames.has(name)) {
2970 toInsert.push(decl)
2971 declNames.add(name)
2972 }
2973 resolved.add(name)
2974 }
2975 }
2976
2977 if (toInsert.length === 0)
2978 break
2979
2980 // Insert at correct source positions to preserve declaration order
2981 for (const decl of toInsert) {
2982 let insertIdx = declarations.length
2983 for (let i = 0; i < declarations.length; i++) {
2984 if (declarations[i].start > decl.start) {
2985 insertIdx = i
2986 break
2987 }
25812988 }
2989 declarations.splice(insertIdx, 0, decl)
25822990 }
25832991 }
25842992}
packages/dtsx/src/generator.tsmodified+0-3
Changes to packages/dtsx/src/generator.ts
@@ -614,9 +614,6 @@ async function processFileWithStatsFromSource(
614614 filePath,
615615 sourceCode: processedSource,
616616 declarations,
617 imports: new Map(),
618 exports: new Set(),
619 usedTypes: new Set(),
620617 }
621618
622619 // Process declarations to generate DTS
packages/dtsx/src/lsp.tsmodified+0-3
Changes to packages/dtsx/src/lsp.ts
@@ -1055,9 +1055,6 @@ export class DtsxLanguageServer {
10551055 filePath: this.uriToPath(uri),
10561056 sourceCode: doc.content,
10571057 declarations: doc.declarations,
1058 imports: new Map<string, Set<string>>(),
1059 exports: new Set<string>(),
1060 usedTypes: new Set<string>(),
10611058 }
10621059
10631060 return processDeclarations(doc.declarations, context, true, this.config.importOrder)
packages/dtsx/src/parallel-processor.tsmodified+0-3
Changes to packages/dtsx/src/parallel-processor.ts
@@ -234,9 +234,6 @@ async function processFile(
234234 filePath,
235235 sourceCode: content,
236236 declarations,
237 imports: new Map(),
238 exports: new Set(),
239 usedTypes: new Set(),
240237 }
241238
242239 return processDeclarations(declarations, context)
packages/dtsx/src/process-source.tsmodified+21-4
Changes to packages/dtsx/src/process-source.ts
@@ -1,5 +1,6 @@
11import type { ProcessingContext } from './types'
2import { extractDeclarations } from './extractor'
2import { extractDeclarations } from './extractor/extract'
3import { scanDeclarations } from './extractor/scanner'
34import { processDeclarations } from './processor'
45
56/**
@@ -21,11 +22,27 @@ export function processSource(
2122 filePath: filename,
2223 sourceCode,
2324 declarations,
24 imports: new Map(),
25 exports: new Set(),
26 usedTypes: new Set(),
2725 }
2826
2927 // Process declarations to generate DTS
3028 return processDeclarations(declarations, context, keepComments, importOrder)
3129}
30
31/**
32 * Fast path for project mode — skips cache lookup/store.
33 * Use when processing many files once (no cache benefit).
34 */
35export function processSourceDirect(
36 sourceCode: string,
37 filename: string = 'stdin.ts',
38 keepComments: boolean = true,
39 importOrder: string[] = ['bun'],
40): string {
41 const declarations = scanDeclarations(sourceCode, filename, keepComments)
42 const context: ProcessingContext = {
43 filePath: filename,
44 sourceCode,
45 declarations,
46 }
47 return processDeclarations(declarations, context, keepComments, importOrder)
48}
packages/dtsx/src/processor/comments.tsmodified+10-5
Changes to packages/dtsx/src/processor/comments.ts
@@ -10,10 +10,15 @@ export function formatComments(comments: string[] | undefined, keepComments: boo
1010 return ''
1111 }
1212
13 const formattedComments = comments.map((comment) => {
14 // Ensure proper spacing and formatting
15 return comment.trim()
16 }).join('\n')
13 // Fast path: single comment (most common case)
14 if (comments.length === 1)
15 return comments[0].trim() + '\n'
1716
18 return `${formattedComments}\n`
17 // Multiple comments: join with newlines
18 let result = ''
19 for (let i = 0; i < comments.length; i++) {
20 if (i > 0) result += '\n'
21 result += comments[i].trim()
22 }
23 return result + '\n'
1924}
packages/dtsx/src/processor/declarations.tsmodified+30-19
Changes to packages/dtsx/src/processor/declarations.ts
@@ -64,6 +64,12 @@ export function processVariableDeclaration(decl: Declaration, keepComments: bool
6464 // Add comments if present
6565 const comments = formatComments(decl.leadingComments, keepComments)
6666
67 // Fast path: if we have an explicit type annotation and no value needing special inference,
68 // use the scanner's pre-built text directly
69 if (decl.typeAnnotation && !decl.value) {
70 return comments + decl.text
71 }
72
6773 let result = ''
6874
6975 // Add export if needed
@@ -197,11 +203,14 @@ export function processTypeDeclaration(decl: Declaration, keepComments: boolean
197203 result += 'declare '
198204 }
199205
200 // Extract the type definition from the original text
201 // Remove leading/trailing whitespace and comments
202 const typeMatch = decl.text.match(/type\s[^=]+=\s*([\s\S]+)/)
203 if (typeMatch) {
204 const typeDef = typeMatch[0].replace(/;?\s*$/, '')
206 // Extract the type definition from the original text using indexOf instead of regex
207 const typeIdx = decl.text.indexOf('type ')
208 if (typeIdx !== -1) {
209 let typeDef = decl.text.slice(typeIdx)
210 // Strip trailing semicolons and whitespace
211 let end = typeDef.length
212 while (end > 0 && (typeDef.charCodeAt(end - 1) === 59 /* ; */ || typeDef.charCodeAt(end - 1) === 32 || typeDef.charCodeAt(end - 1) === 10 || typeDef.charCodeAt(end - 1) === 13)) end--
213 if (end < typeDef.length) typeDef = typeDef.slice(0, end)
205214 result += typeDef
206215 }
207216 else {
@@ -260,10 +269,10 @@ export function processEnumDeclaration(decl: Declaration, keepComments: boolean
260269 // Add enum name
261270 result += decl.name
262271
263 // Extract the body from the original text
264 const bodyMatch = decl.text.match(/\{[\s\S]*\}/)
265 if (bodyMatch) {
266 result += ` ${bodyMatch[0]}`
272 // Extract the body from the original text using indexOf instead of regex
273 const enumBraceIdx = decl.text.indexOf('{')
274 if (enumBraceIdx !== -1) {
275 result += ' ' + decl.text.slice(enumBraceIdx)
267276 }
268277 else {
269278 result += ' {}'
@@ -280,8 +289,10 @@ export function processImportDeclaration(decl: Declaration): string {
280289 // Just ensure they end with semicolon
281290 let result = decl.text.trim()
282291
283 // Remove any existing semicolon to avoid doubles
284 result = result.replace(/;+$/, '')
292 // Remove trailing semicolons without regex
293 let end = result.length
294 while (end > 0 && result.charCodeAt(end - 1) === 59 /* ; */) end--
295 if (end < result.length) result = result.slice(0, end)
285296
286297 // Add single semicolon
287298 result += ';'
@@ -320,10 +331,10 @@ export function processModuleDeclaration(decl: Declaration, keepComments: boolea
320331 // Add module name
321332 result += decl.name
322333
323 // Extract the body from the original text
324 const bodyMatch = decl.text.match(/\{[\s\S]*\}/)
325 if (bodyMatch) {
326 result += ` ${bodyMatch[0]}`
334 // Extract the body from the original text using indexOf
335 const modBraceIdx = decl.text.indexOf('{')
336 if (modBraceIdx !== -1) {
337 result += ' ' + decl.text.slice(modBraceIdx)
327338 }
328339 else {
329340 result += ' {}'
@@ -351,10 +362,10 @@ export function processModuleDeclaration(decl: Declaration, keepComments: boolea
351362 // Add namespace name
352363 result += decl.name
353364
354 // Extract the body from the original text
355 const bodyMatch = decl.text.match(/\{[\s\S]*\}/)
356 if (bodyMatch) {
357 result += ` ${bodyMatch[0]}`
365 // Extract the body from the original text using indexOf
366 const nsBraceIdx = decl.text.indexOf('{')
367 if (nsBraceIdx !== -1) {
368 result += ' ' + decl.text.slice(nsBraceIdx)
358369 }
359370 else {
360371 result += ' {}'
packages/dtsx/src/processor/index.tsmodified+100-99
Changes to packages/dtsx/src/processor/index.ts
@@ -3,7 +3,7 @@
33 */
44
55import type { Declaration, DeclarationKind, ProcessingContext } from '../types'
6import { extractTripleSlashDirectives } from '../extractor/helpers'
6import { extractTripleSlashDirectives } from '../extractor/directives'
77import { getCachedRegex } from './cache'
88import { formatComments } from './comments'
99import {
@@ -62,10 +62,15 @@ export function processDeclarations(
6262 const output: string[] = []
6363
6464 // Extract and add triple-slash directives at the top of the file
65 const tripleSlashDirectives = extractTripleSlashDirectives(context.sourceCode)
66 if (tripleSlashDirectives.length > 0) {
67 output.push(...tripleSlashDirectives)
68 output.push('') // Blank line after directives
65 // Fast check: only call extractTripleSlashDirectives if source starts with ///
66 const src = context.sourceCode
67 const firstNonWS = src.length > 0 ? src.trimStart() : ''
68 if (firstNonWS.startsWith('///')) {
69 const tripleSlashDirectives = extractTripleSlashDirectives(src)
70 if (tripleSlashDirectives.length > 0) {
71 output.push(...tripleSlashDirectives)
72 output.push('') // Blank line after directives
73 }
6974 }
7075
7176 // Group declarations by type for better organization (single pass)
@@ -99,8 +104,10 @@ export function processDeclarations(
99104
100105 // Parse all exports to understand what's being exported
101106 const exportedItems = new Set<string>()
102 const exportStatements: string[] = []
107 const typeExportStatements: string[] = []
108 const valueExportStatements: string[] = []
103109 const defaultExport: string[] = []
110 const seenExports = new Set<string>()
104111
105112 for (const decl of exports) {
106113 // Prepend comments if present
@@ -129,8 +136,15 @@ export function processDeclarations(
129136 }
130137
131138 const fullExportText = comments + exportText
132 if (!exportStatements.includes(fullExportText)) {
133 exportStatements.push(fullExportText)
139 if (!seenExports.has(fullExportText)) {
140 seenExports.add(fullExportText)
141 // Categorize into type vs value exports in one pass
142 if (fullExportText.includes('export type')) {
143 typeExportStatements.push(fullExportText)
144 }
145 else {
146 valueExportStatements.push(fullExportText)
147 }
134148 }
135149 }
136150 }
@@ -145,55 +159,47 @@ export function processDeclarations(
145159 }
146160 }
147161
148 // Get all unique imported item names for regex matching
149 const allImportedItemNames = Array.from(allImportedItemsMap.keys())
150
151162 // Filter imports to only include those that are used in exports or declarations
152163 const usedImportItems = new Set<string>()
153164
154 // Collect all declaration texts that need to be checked for imports (single pass)
155 const declarationTexts: Array<{ text: string, additionalTexts: string[] }> = []
165 // Build combined text for import usage detection (single allocation)
166 const allTexts: string[] = []
156167
157168 // Add exported functions
158169 for (const func of functions) {
159170 if (func.isExported) {
160 declarationTexts.push({ text: func.text, additionalTexts: [] })
171 allTexts.push(func.text)
161172 }
162173 }
163174
164175 // Add exported variables
165176 for (const variable of variables) {
166177 if (variable.isExported) {
167 const additionalTexts: string[] = []
178 allTexts.push(variable.text)
168179 if (variable.typeAnnotation) {
169 additionalTexts.push(variable.typeAnnotation)
180 allTexts.push(variable.typeAnnotation)
170181 }
171 declarationTexts.push({ text: variable.text, additionalTexts })
172182 }
173183 }
174184
175185 // Build reference check sets for interfaces (optimized: single pass over text sources)
176186 const interfaceReferences = new Set<string>()
177187 if (interfaces.length > 0) {
178 // Collect all texts to search in one array
179 const textsToSearch: string[] = []
188 // Build combined text from functions/classes/types for interface reference check
189 let refCheckText = ''
180190 for (const func of functions) {
181191 if (func.isExported)
182 textsToSearch.push(func.text)
192 refCheckText += func.text + '\n'
183193 }
184194 for (const cls of classes) {
185 textsToSearch.push(cls.text)
195 refCheckText += cls.text + '\n'
186196 }
187197 for (const type of types) {
188 textsToSearch.push(type.text)
198 refCheckText += type.text + '\n'
189199 }
190200
191 // Join all texts for a single search per interface (faster than N*M individual searches)
192 const combinedText = textsToSearch.join('\n')
193
194 // Single pass: check each interface name against combined text
195201 for (const iface of interfaces) {
196 if (combinedText.includes(iface.name)) {
202 if (refCheckText.includes(iface.name)) {
197203 interfaceReferences.add(iface.name)
198204 }
199205 }
@@ -202,43 +208,37 @@ export function processDeclarations(
202208 // Add interfaces (exported or referenced)
203209 for (const iface of interfaces) {
204210 if (iface.isExported || interfaceReferences.has(iface.name)) {
205 declarationTexts.push({ text: iface.text, additionalTexts: [] })
211 allTexts.push(iface.text)
206212 }
207213 }
208214
209 // Add all types, classes, enums, modules (they may be included in DTS)
215 // Add all types, classes, enums, modules
210216 for (const type of types) {
211 declarationTexts.push({ text: type.text, additionalTexts: [] })
217 allTexts.push(type.text)
212218 }
213219 for (const cls of classes) {
214 declarationTexts.push({ text: cls.text, additionalTexts: [] })
220 allTexts.push(cls.text)
215221 }
216222 for (const enumDecl of enums) {
217 declarationTexts.push({ text: enumDecl.text, additionalTexts: [] })
223 allTexts.push(enumDecl.text)
218224 }
219225 for (const mod of modules) {
220 declarationTexts.push({ text: mod.text, additionalTexts: [] })
226 allTexts.push(mod.text)
221227 }
222228
223229 // Add export statements
224230 for (const exp of exports) {
225 declarationTexts.push({ text: exp.text, additionalTexts: [] })
226 }
227
228 // Optimized: combine ALL declaration texts into one and extract identifiers in a single pass
229 const allTexts: string[] = []
230 for (const { text, additionalTexts } of declarationTexts) {
231 allTexts.push(text)
232 if (additionalTexts.length > 0) {
233 allTexts.push(...additionalTexts)
234 }
231 allTexts.push(exp.text)
235232 }
236 const combinedDeclarationText = allTexts.join('\n')
237233
238234 // Two-phase import detection: fast includes() rejection then regex word-boundary check
239 for (const item of allImportedItemNames) {
240 if (combinedDeclarationText.includes(item) && getCachedRegex(item).test(combinedDeclarationText)) {
241 usedImportItems.add(item)
235 // Check each text individually to avoid large join() allocation
236 for (const item of allImportedItemsMap.keys()) {
237 for (let t = 0; t < allTexts.length; t++) {
238 if (allTexts[t].includes(item) && getCachedRegex(item).test(allTexts[t])) {
239 usedImportItems.add(item)
240 break
241 }
242242 }
243243 }
244244
@@ -326,62 +326,63 @@ export function processDeclarations(
326326 output.push('')
327327
328328 // Process type exports first
329 const typeExports = exportStatements.filter(exp => exp.includes('export type'))
330 output.push(...typeExports)
331
332 // Process other declarations (functions, interfaces, etc.)
333 const otherDecls = [...functions, ...variables, ...interfaces, ...types, ...classes, ...enums, ...modules]
334
335 for (const decl of otherDecls) {
336 let processed = ''
337 const kind: DeclarationKind = decl.kind
338 switch (kind) {
339 case 'function':
340 processed = processFunctionDeclaration(decl, keepComments)
341 break
342 case 'variable':
343 processed = processVariableDeclaration(decl, keepComments)
344 break
345 case 'interface':
346 processed = processInterfaceDeclaration(decl, keepComments)
347 break
348 case 'type':
349 processed = processTypeDeclaration(decl, keepComments)
350 break
351 case 'class':
352 processed = processClassDeclaration(decl, keepComments)
353 break
354 case 'enum':
355 processed = processEnumDeclaration(decl, keepComments)
356 break
357 case 'module':
358 processed = processModuleDeclaration(decl, keepComments)
359 break
360 // import and export are handled separately above
361 case 'import':
362 case 'export':
363 break
364 case 'namespace':
365 processed = processModuleDeclaration(decl, keepComments)
366 break
367 case 'unknown':
368 // Skip unknown declarations
369 break
370 default:
371 assertNever(kind, `Unhandled declaration kind in processor: ${kind}`)
372 }
373
374 if (processed) {
375 output.push(processed)
329 for (let i = 0; i < typeExportStatements.length; i++) output.push(typeExportStatements[i])
330
331 // Process other declarations — iterate each group directly (no spread allocation)
332 const declGroups = [functions, variables, interfaces, types, classes, enums, modules]
333 for (let g = 0; g < declGroups.length; g++) {
334 const group = declGroups[g]
335 for (let d = 0; d < group.length; d++) {
336 const decl = group[d]
337 let processed = ''
338 const kind: DeclarationKind = decl.kind
339 switch (kind) {
340 case 'function':
341 processed = processFunctionDeclaration(decl, keepComments)
342 break
343 case 'variable':
344 processed = processVariableDeclaration(decl, keepComments)
345 break
346 case 'interface':
347 processed = processInterfaceDeclaration(decl, keepComments)
348 break
349 case 'type':
350 processed = processTypeDeclaration(decl, keepComments)
351 break
352 case 'class':
353 processed = processClassDeclaration(decl, keepComments)
354 break
355 case 'enum':
356 processed = processEnumDeclaration(decl, keepComments)
357 break
358 case 'module':
359 case 'namespace':
360 processed = processModuleDeclaration(decl, keepComments)
361 break
362 case 'import':
363 case 'export':
364 case 'unknown':
365 break
366 default:
367 assertNever(kind, `Unhandled declaration kind in processor: ${kind}`)
368 }
369 if (processed) output.push(processed)
376370 }
377371 }
378372
379373 // Process value exports
380 const valueExports = exportStatements.filter(exp => !exp.includes('export type'))
381 output.push(...valueExports)
374 for (let i = 0; i < valueExportStatements.length; i++) output.push(valueExportStatements[i])
382375
383376 // Process default export last
384 output.push(...defaultExport)
385
386 return output.filter(line => line !== '').join('\n')
377 for (let i = 0; i < defaultExport.length; i++) output.push(defaultExport[i])
378
379 // Build final output — skip empty strings inline instead of filter()
380 let result = ''
381 for (let i = 0; i < output.length; i++) {
382 if (output[i] !== '') {
383 if (result) result += '\n'
384 result += output[i]
385 }
386 }
387 return result
387388}
packages/dtsx/src/types.tsmodified+3-3
Changes to packages/dtsx/src/types.ts
@@ -476,7 +476,7 @@ export interface ProcessingContext {
476476 filePath: string
477477 sourceCode: string
478478 declarations: Declaration[]
479 imports: Map<string, Set<string>>
480 exports: Set<string>
481 usedTypes: Set<string>
479 imports?: Map<string, Set<string>>
480 exports?: Set<string>
481 usedTypes?: Set<string>
482482}
packages/dtsx/src/worker.tsmodified+0-3
Changes to packages/dtsx/src/worker.ts
@@ -166,9 +166,6 @@ export class WorkerPool {
166166 filePath: task.filePath,
167167 sourceCode: task.sourceCode,
168168 declarations,
169 imports: new Map(),
170 exports: new Set(),
171 usedTypes: new Set(),
172169 }, true, task.config?.importOrder);
173170
174171 return {
packages/dtsx/test/fixtures/output/complex-class.d.tsmodified+11-11
moved, unchanged · 11 hidden lines
Changes to packages/dtsx/test/fixtures/output/complex-class.d.ts
@@ -1,3 +1,11 @@ moved, unchanged
1/**
2 * Logger interface for dependency injection
3 */
4declare interface Logger {
5 info(message: string, ...args: any[]): void
6 warn(message: string, ...args: any[]): void
7 error(message: string, error?: Error): void
8}
19/**
210 * Application events definition
311 */
Changes to packages/dtsx/test/fixtures/output/complex-class.d.ts
@@ -14,21 +22,13 @@ export declare interface Serializable {
1422 serialize(): string
1523 deserialize(data: string): void
1624}
17/**
18 * Logger interface for dependency injection
19 */
20declare interface Logger {
21 info(message: string, ...args: any[]): void
22 warn(message: string, ...args: any[]): void
23 error(message: string, error?: Error): void
24}
2525/**
2626 * Base event emitter class
2727 */
2828export declare abstract class EventEmitter<Events extends Record<string, any[]>> {
2929 on<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void): this;
3030 off<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void): this;
31 protected emit<K extends keyof Events>(event: K, args: Events[K]): void;
31 protected emit<K extends keyof Events>(event: K, ...args: Events[K]): void;
3232 abstract dispose(): void;
3333}
3434/**
@@ -47,9 +47,8 @@ export declare class Application extends EventEmitter<AppEvents> {
4747 * Generic container with static factory methods
4848 */
4949export declare class Container<T> {
50 constructor(items?: T[]);
5150 static empty<T>(): Container<T>;
52 static of<T>(items: T[]): Container<T>;
51 static of<T>(...items: T[]): Container<T>;
5352 static from<T>(iterable: Iterable<T>): Container<T>;
5453 add(item: T): this;
5554 map<U>(fn: (item: T) => U): Container<U>;
@@ -62,6 +61,7 @@ export declare class Container<T> {
6261 * Class implementing interface with index signature
6362 */
6463export declare class DataStore implements Serializable {
64 [key: string]: unknown;
6565 serialize(): string;
6666 deserialize(data: string): void;
6767}
packages/esbuild-plugin/package.jsonmodified+1-1
Changes to packages/esbuild-plugin/package.json
@@ -52,6 +52,6 @@
5252 "@stacksjs/dtsx": "workspace:*"
5353 },
5454 "peerDependencies": {
55 "esbuild": ">=0.17.0"
55 "esbuild": ">=0.27.3"
5656 }
5757}
packages/vite-plugin/package.jsonmodified+1-1
Changes to packages/vite-plugin/package.json
@@ -67,7 +67,7 @@
6767 },
6868 "devDependencies": {
6969 "@stacksjs/dtsx": "workspace:*",
70 "vite": "^5.0.0"
70 "vite": "^5.4.21"
7171 },
7272 "peerDependencies": {
7373 "vite": "^4.0.0 || ^5.0.0"