also looking at this
chore(deps): update all non-major dependencies
#2997
29 files
+1047
-444
| @@ -35,10 +35,10 @@ jobs: | ||
| 35 | 35 | pull-requests: write |
| 36 | 36 | |
| 37 | 37 | steps: |
| 38 | - uses: actions/checkout@v6.0.0 | |
| 38 | - uses: actions/checkout@v6.0.2 | |
| 39 | 39 | |
| 40 | 40 | - name: Install Bun |
| 41 | uses: oven-sh/setup-bun@v2.0.2 | |
| 41 | uses: oven-sh/setup-bun@v2.1.2 | |
| 42 | 42 | |
| 43 | 43 | - name: Use cached node_modules |
| 44 | 44 | uses: actions/cache@v4.3.0 |
| @@ -130,7 +130,7 @@ jobs: | ||
| 130 | 130 | |
| 131 | 131 | - name: Setup PHP and Composer (if needed) |
| 132 | 132 | if: ${{ hashFiles('composer.json') != '' }} |
| 133 | uses: shivammathur/setup-php@2.35.5 | |
| 133 | uses: shivammathur/setup-php@2.36.0 | |
| 134 | 134 | with: |
| 135 | 135 | php-version: '8.4' |
| 136 | 136 | tools: composer |
| @@ -248,7 +248,7 @@ jobs: | ||
| 248 | 248 | |
| 249 | 249 | - name: Setup PHP and Composer (if needed) |
| 250 | 250 | if: ${{ hashFiles('composer.json') != '' }} |
| 251 | uses: shivammathur/setup-php@2.35.5 | |
| 251 | uses: shivammathur/setup-php@2.36.0 | |
| 252 | 252 | with: |
| 253 | 253 | php-version: '8.4' |
| 254 | 254 | tools: composer |
| @@ -18,10 +18,10 @@ jobs: | ||
| 18 | 18 | runs-on: ubuntu-latest |
| 19 | 19 | |
| 20 | 20 | steps: |
| 21 | - uses: actions/checkout@v6.0.0 | |
| 21 | - uses: actions/checkout@v6.0.2 | |
| 22 | 22 | |
| 23 | 23 | - name: Install Bun |
| 24 | uses: oven-sh/setup-bun@v2.0.2 | |
| 24 | uses: oven-sh/setup-bun@v2.1.2 | |
| 25 | 25 | |
| 26 | 26 | - name: Use cached node_modules |
| 27 | 27 | uses: actions/cache@v4.3.0 |
| @@ -41,10 +41,10 @@ jobs: | ||
| 41 | 41 | runs-on: ubuntu-latest |
| 42 | 42 | |
| 43 | 43 | steps: |
| 44 | - uses: actions/checkout@v6.0.0 | |
| 44 | - uses: actions/checkout@v6.0.2 | |
| 45 | 45 | |
| 46 | 46 | - name: Install Bun |
| 47 | uses: oven-sh/setup-bun@v2.0.2 | |
| 47 | uses: oven-sh/setup-bun@v2.1.2 | |
| 48 | 48 | |
| 49 | 49 | - name: Use cached node_modules |
| 50 | 50 | uses: actions/cache@v4.3.0 |
| @@ -64,10 +64,10 @@ jobs: | ||
| 64 | 64 | runs-on: ubuntu-latest |
| 65 | 65 | |
| 66 | 66 | steps: |
| 67 | - uses: actions/checkout@v6.0.0 | |
| 67 | - uses: actions/checkout@v6.0.2 | |
| 68 | 68 | |
| 69 | 69 | - name: Install Bun |
| 70 | uses: oven-sh/setup-bun@v2.0.2 | |
| 70 | uses: oven-sh/setup-bun@v2.1.2 | |
| 71 | 71 | |
| 72 | 72 | - name: Use cached node_modules |
| 73 | 73 | uses: actions/cache@v4.3.0 |
| @@ -87,10 +87,10 @@ jobs: | ||
| 87 | 87 | runs-on: ubuntu-latest |
| 88 | 88 | |
| 89 | 89 | steps: |
| 90 | - uses: actions/checkout@v6.0.0 | |
| 90 | - uses: actions/checkout@v6.0.2 | |
| 91 | 91 | |
| 92 | 92 | - name: Install Bun |
| 93 | uses: oven-sh/setup-bun@v2.0.2 | |
| 93 | uses: oven-sh/setup-bun@v2.1.2 | |
| 94 | 94 | |
| 95 | 95 | - name: Use cached node_modules |
| 96 | 96 | uses: actions/cache@v4.3.0 |
| @@ -20,6 +20,7 @@ import { bench, run, summary } from 'mitata' | ||
| 20 | 20 | import { isolatedDeclarationSync } from 'oxc-transform' |
| 21 | 21 | import ts from 'typescript' |
| 22 | 22 | import { clearSourceFileCache } from '../packages/dtsx/src/extractor' |
| 23 | import { clearDeclarationCache } from '../packages/dtsx/src/extractor/extract' | |
| 23 | 24 | import { processSource } from '../packages/dtsx/src/generator' |
| 24 | 25 | import { clearProcessorCaches } from '../packages/dtsx/src/processor' |
| 25 | 26 | |
| @@ -218,6 +219,7 @@ for (const { name, filename, source } of inputs) { | ||
| 218 | 219 | summary(() => { |
| 219 | 220 | bench(`dtsx (no-cache) — ${name}`, () => { |
| 220 | 221 | clearSourceFileCache() |
| 222 | clearDeclarationCache() | |
| 221 | 223 | clearProcessorCaches() |
| 222 | 224 | processSource(source, filename) |
| 223 | 225 | }) |
bun.lockmodified+13-14
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | "@stacksjs/gitlint": "^0.1.5", |
| 13 | 13 | "@stacksjs/logsmith": "^0.2.1", |
| 14 | 14 | "@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", | |
| 16 | 16 | "buddy-bot": "^0.9.12", |
| 17 | 17 | "bun-git-hooks": "^0.3.1", |
| 18 | 18 | "bun-plugin-dtsx": "workspace:*", |
| @@ -40,11 +40,10 @@ | ||
| 40 | 40 | "devDependencies": { |
| 41 | 41 | "bunfig": "^0.15.6", |
| 42 | 42 | "oxc-parser": "^0.112.0", |
| 43 | "tinyglobby": "^0.2.15", | |
| 44 | "typescript": "^5.0.0", | |
| 43 | "typescript": "^5.9.3", | |
| 45 | 44 | }, |
| 46 | 45 | "peerDependencies": { |
| 47 | "typescript": ">=5.0.0", | |
| 46 | "typescript": ">=5.9.3", | |
| 48 | 47 | }, |
| 49 | 48 | }, |
| 50 | 49 | "packages/esbuild-plugin": { |
| @@ -54,7 +53,7 @@ | ||
| 54 | 53 | "@stacksjs/dtsx": "workspace:*", |
| 55 | 54 | }, |
| 56 | 55 | "peerDependencies": { |
| 57 | "esbuild": ">=0.17.0", | |
| 56 | "esbuild": ">=0.27.3", | |
| 58 | 57 | }, |
| 59 | 58 | }, |
| 60 | 59 | "packages/tsup-plugin": { |
| @@ -75,7 +74,7 @@ | ||
| 75 | 74 | }, |
| 76 | 75 | "devDependencies": { |
| 77 | 76 | "@stacksjs/dtsx": "workspace:*", |
| 78 | "vite": "^5.0.0", | |
| 77 | "vite": "^5.4.21", | |
| 79 | 78 | }, |
| 80 | 79 | "peerDependencies": { |
| 81 | 80 | "vite": "^4.0.0 || ^5.0.0", |
| @@ -337,21 +336,21 @@ | ||
| 337 | 336 | |
| 338 | 337 | "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], |
| 339 | 338 | |
| 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=="], | |
| 341 | 340 | |
| 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=="], | |
| 343 | 342 | |
| 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=="], | |
| 345 | 344 | |
| 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=="], | |
| 347 | 346 | |
| 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=="], | |
| 349 | 348 | |
| 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=="], | |
| 351 | 350 | |
| 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=="], | |
| 353 | 352 | |
| 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=="], | |
| 355 | 354 | |
| 356 | 355 | "@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=="], |
| 357 | 356 | |
| @@ -63,7 +63,7 @@ | ||
| 63 | 63 | "@stacksjs/gitlint": "^0.1.5", |
| 64 | 64 | "@stacksjs/logsmith": "^0.2.1", |
| 65 | 65 | "@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", | |
| 67 | 67 | "buddy-bot": "^0.9.12", |
| 68 | 68 | "bun-git-hooks": "^0.3.1", |
| 69 | 69 | "bun-plugin-dtsx": "workspace:*", |
| @@ -1,18 +1,12 @@ | ||
| 1 | 1 | // Fast path — skip CLI framework for maximum startup speed |
| 2 | 2 | import type { LogLevel } from '../src/logger' |
| 3 | 3 | import type { DtsGenerationConfig, DtsGenerationOption } from '../src/types' |
| 4 | import { resolve } from 'node:path' | |
| 5 | 4 | import process from 'node:process' |
| 6 | import { CLI } from '@stacksjs/clapp' | |
| 7 | import { version } from '../../../package.json' | |
| 8 | import { getConfig } from '../src/config' | |
| 9 | import { generate, processSource, watch } from '../src/generator' | |
| 10 | 5 | |
| 11 | 6 | const _cmd = process.argv[2] |
| 12 | if (_cmd === 'stdin' || _cmd === 'emit') { | |
| 13 | const { processSource } = await import('../src/process-source') | |
| 14 | ||
| 7 | if (_cmd === 'stdin' || _cmd === 'emit' || _cmd === '--project' || _cmd === '--project-worker') { | |
| 15 | 8 | if (_cmd === 'stdin') { |
| 9 | const { processSource } = await import('../src/process-source') | |
| 16 | 10 | const chunks: Buffer[] = [] |
| 17 | 11 | for await (const chunk of process.stdin) { |
| 18 | 12 | chunks.push(chunk) |
| @@ -23,7 +17,105 @@ if (_cmd === 'stdin' || _cmd === 'emit') { | ||
| 23 | 17 | process.stdout.write('\n') |
| 24 | 18 | } |
| 25 | 19 | } |
| 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 | } | |
| 26 | 116 | else { |
| 117 | // emit command | |
| 118 | const { processSource } = await import('../src/process-source') | |
| 27 | 119 | const { readFileSync, writeFileSync, mkdirSync } = await import('node:fs') |
| 28 | 120 | const filePath = process.argv[3]! |
| 29 | 121 | const source = readFileSync(filePath, 'utf-8') |
| @@ -41,6 +133,13 @@ if (_cmd === 'stdin' || _cmd === 'emit') { | ||
| 41 | 133 | process.exit(0) |
| 42 | 134 | } |
| 43 | 135 | |
| 136 | // Heavy imports — only loaded when CLI framework is needed (not on fast path) | |
| 137 | const { resolve } = await import('node:path') | |
| 138 | const { CLI } = await import('@stacksjs/clapp') | |
| 139 | const { version } = await import('../../../package.json') | |
| 140 | const { getConfig } = await import('../src/config') | |
| 141 | const { generate, processSource, watch } = await import('../src/generator') | |
| 142 | ||
| 44 | 143 | const cli = new CLI('dtsx') |
| 45 | 144 | |
| 46 | 145 | const defaultOptions: DtsGenerationConfig = { |
| @@ -11,11 +11,21 @@ if (cmd === '--project') { | ||
| 11 | 11 | const outdir = args[3] |
| 12 | 12 | const files = readdirSync(dir).filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts')) |
| 13 | 13 | |
| 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]) | |
| 18 | 23 | } |
| 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 | ) | |
| 19 | 29 | } |
| 20 | 30 | else if (cmd === 'emit' && args[1]) { |
| 21 | 31 | const filePath = args[1] |
| @@ -94,7 +94,7 @@ | ||
| 94 | 94 | "zip:darwin-arm64": "zip -j bin/dtsx-darwin-arm64.zip bin/dtsx-darwin-arm64" |
| 95 | 95 | }, |
| 96 | 96 | "peerDependencies": { |
| 97 | "typescript": ">=5.0.0" | |
| 97 | "typescript": ">=5.9.3" | |
| 98 | 98 | }, |
| 99 | 99 | "peerDependenciesMeta": { |
| 100 | 100 | "typescript": { |
| @@ -107,7 +107,7 @@ | ||
| 107 | 107 | "devDependencies": { |
| 108 | 108 | "bunfig": "^0.15.6", |
| 109 | 109 | "oxc-parser": "^0.112.0", |
| 110 | "typescript": "^5.0.0" | |
| 110 | "typescript": "^5.9.3" | |
| 111 | 111 | }, |
| 112 | 112 | "git-hooks": { |
| 113 | 113 | "pre-commit": { |
| @@ -5,6 +5,9 @@ | ||
| 5 | 5 | |
| 6 | 6 | import type { SourceFile } from 'typescript' |
| 7 | 7 | import { createSourceFile, ScriptKind, ScriptTarget } from 'typescript' |
| 8 | import { hashContent } from './hash' | |
| 9 | ||
| 10 | export { hashContent } from './hash' | |
| 8 | 11 | |
| 9 | 12 | /** |
| 10 | 13 | * Cache for parsed SourceFile objects to avoid re-parsing |
| @@ -12,11 +15,6 @@ import { createSourceFile, ScriptKind, ScriptTarget } from 'typescript' | ||
| 12 | 15 | */ |
| 13 | 16 | const sourceFileCache = new Map<string, { sourceFile: SourceFile, contentHash: number | bigint, lastAccess: number }>() |
| 14 | 17 | |
| 15 | /** | |
| 16 | * Whether Bun.hash is available (much faster than JS hash) | |
| 17 | */ | |
| 18 | const hasBunHash = typeof globalThis.Bun?.hash === 'function' | |
| 19 | ||
| 20 | 18 | /** |
| 21 | 19 | * Pending async parse operations to prevent duplicate parsing |
| 22 | 20 | */ |
| @@ -50,23 +48,6 @@ export interface AsyncParseConfig { | ||
| 50 | 48 | */ |
| 51 | 49 | const MAX_CACHE_SIZE = 100 |
| 52 | 50 | |
| 53 | /** | |
| 54 | * Fast hash function for content comparison. | |
| 55 | * Uses Bun.hash (native wyhash) when available, falls back to JS implementation. | |
| 56 | */ | |
| 57 | export 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 | ||
| 70 | 51 | /** |
| 71 | 52 | * Evict oldest entry if cache exceeds max size |
| 72 | 53 | * Uses O(n) scan instead of O(n log n) sort since we add one entry at a time |
| @@ -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 | */ | |
| 10 | export 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 | } | |
| @@ -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 | ||
| 7 | import type { Declaration } from '../types' | |
| 8 | import { hashContent } from './hash' | |
| 9 | import { scanDeclarations } from './scanner' | |
| 10 | ||
| 11 | const MAX_DECLARATION_CACHE_SIZE = 100 | |
| 12 | const 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 | */ | |
| 18 | export 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 | */ | |
| 53 | export function clearDeclarationCache(): void { | |
| 54 | declarationCache.clear() | |
| 55 | } | |
| @@ -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 | ||
| 6 | const hasBunHash = typeof globalThis.Bun?.hash === 'function' | |
| 7 | ||
| 8 | export 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 | } | |
| @@ -235,49 +235,8 @@ export function isBuiltInType(typeName: string): boolean { | ||
| 235 | 235 | return BUILT_IN_TYPES.has(typeName) |
| 236 | 236 | } |
| 237 | 237 | |
| 238 | /** | |
| 239 | * Extract triple-slash directives from the beginning of source code | |
| 240 | * These are special comments like /// <reference types="..." /> | |
| 241 | */ | |
| 242 | export 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 | |
| 239 | export { extractTripleSlashDirectives } from './directives' | |
| 281 | 240 | |
| 282 | 241 | /** |
| 283 | 242 | * Extract type names from module/namespace text |
| @@ -6,7 +6,8 @@ import type { ClassDeclaration, EnumDeclaration, ExportAssignment, ExportDeclara | ||
| 6 | 6 | import type { Declaration } from '../types' |
| 7 | 7 | import type { AsyncParseConfig } from './cache' |
| 8 | 8 | import { forEachChild, SyntaxKind } from 'typescript' |
| 9 | import { getSourceFileAsync, hashContent } from './cache' | |
| 9 | import { getSourceFileAsync } from './cache' | |
| 10 | import { hashContent } from './hash' | |
| 10 | 11 | // Re-export all public APIs |
| 11 | 12 | import { clearSourceFileCache as _clearSFCache } from './cache' |
| 12 | 13 | import { |
packages/dtsx/src/extractor/scanner.tsmodified+589-181
| @@ -95,6 +95,47 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 95 | 95 | } |
| 96 | 96 | } |
| 97 | 97 | |
| 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 | ||
| 98 | 139 | function skipString(quote: number): void { |
| 99 | 140 | pos++ // skip opening quote |
| 100 | 141 | while (pos < len) { |
| @@ -165,10 +206,72 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 165 | 206 | pos += 2 |
| 166 | 207 | return true |
| 167 | 208 | } |
| 209 | // Check for regex literal: / not followed by / or *, preceded by non-expression char | |
| 210 | if (isRegexStart()) { | |
| 211 | skipRegex() | |
| 212 | return true | |
| 213 | } | |
| 168 | 214 | } |
| 169 | 215 | return false |
| 170 | 216 | } |
| 171 | 217 | |
| 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 | ||
| 172 | 275 | /** Read an identifier at current position */ |
| 173 | 276 | function readIdent(): string { |
| 174 | 277 | const start = pos |
| @@ -192,12 +295,22 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 192 | 295 | |
| 193 | 296 | /** Check if current position is at a top-level statement-starting keyword */ |
| 194 | 297 | 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 | } | |
| 201 | 314 | } |
| 202 | 315 | |
| 203 | 316 | /** |
| @@ -215,12 +328,24 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 215 | 328 | while (pos < len) { |
| 216 | 329 | const c = source.charCodeAt(pos) |
| 217 | 330 | 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 | } | |
| 224 | 349 | } |
| 225 | 350 | break |
| 226 | 351 | } |
| @@ -331,10 +456,11 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 331 | 456 | */ |
| 332 | 457 | function skipExportBraces(): void { |
| 333 | 458 | 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++ | |
| 335 | 461 | if (matchWord('from')) { |
| 336 | 462 | pos += 4 |
| 337 | skipWhitespaceAndComments() | |
| 463 | while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++ | |
| 338 | 464 | if (pos < len) { |
| 339 | 465 | const ch = source.charCodeAt(pos) |
| 340 | 466 | if (ch === CH_SQUOTE || ch === CH_DQUOTE) |
| @@ -352,16 +478,16 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 352 | 478 | */ |
| 353 | 479 | function skipExportStar(): void { |
| 354 | 480 | pos++ // skip * |
| 355 | skipWhitespaceAndComments() | |
| 481 | while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++ | |
| 356 | 482 | if (matchWord('as')) { |
| 357 | 483 | pos += 2 |
| 358 | skipWhitespaceAndComments() | |
| 484 | while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++ | |
| 359 | 485 | readIdent() |
| 360 | skipWhitespaceAndComments() | |
| 486 | while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++ | |
| 361 | 487 | } |
| 362 | 488 | if (matchWord('from')) { |
| 363 | 489 | pos += 4 |
| 364 | skipWhitespaceAndComments() | |
| 490 | while (pos < len && isWhitespace(source.charCodeAt(pos))) pos++ | |
| 365 | 491 | if (pos < len) { |
| 366 | 492 | const ch = source.charCodeAt(pos) |
| 367 | 493 | if (ch === CH_SQUOTE || ch === CH_DQUOTE) |
| @@ -387,8 +513,10 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 387 | 513 | return undefined |
| 388 | 514 | |
| 389 | 515 | const comments: string[] = [] |
| 516 | let hasBlockComment = false | |
| 390 | 517 | |
| 391 | 518 | // Scan backwards for consecutive comment blocks |
| 519 | // Use push() + reverse at end (O(1) per append vs O(n) for unshift) | |
| 392 | 520 | while (p >= 0) { |
| 393 | 521 | // Check for block comment ending with */ |
| 394 | 522 | 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: | ||
| 396 | 524 | let start = p - 2 |
| 397 | 525 | while (start >= 1 && !(source.charCodeAt(start) === CH_SLASH && source.charCodeAt(start + 1) === CH_STAR)) start-- |
| 398 | 526 | 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 | |
| 400 | 529 | p = start - 1 |
| 401 | 530 | while (p >= 0 && isWhitespace(source.charCodeAt(p))) p-- |
| 402 | 531 | continue |
| @@ -411,7 +540,11 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 411 | 540 | const lineText = source.slice(lineStart, p + 1).trim() |
| 412 | 541 | |
| 413 | 542 | 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) | |
| 415 | 548 | const singleLines: string[] = [lineText] |
| 416 | 549 | p = lineStart - 1 |
| 417 | 550 | 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: | ||
| 421 | 554 | while (ls > 0 && source.charCodeAt(ls - 1) !== CH_LF) ls-- |
| 422 | 555 | const lt = source.slice(ls, p + 1).trim() |
| 423 | 556 | if (lt.startsWith('//')) { |
| 424 | singleLines.unshift(lt) | |
| 557 | singleLines.push(lt) | |
| 425 | 558 | p = ls - 1 |
| 426 | 559 | while (p >= 0 && (source.charCodeAt(p) === CH_LF || source.charCodeAt(p) === CH_CR)) p-- |
| 427 | 560 | } |
| @@ -433,7 +566,8 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 433 | 566 | break |
| 434 | 567 | } |
| 435 | 568 | } |
| 436 | comments.unshift(singleLines.join('\n')) | |
| 569 | singleLines.reverse() | |
| 570 | comments.push(singleLines.join('\n')) | |
| 437 | 571 | continue |
| 438 | 572 | } |
| 439 | 573 | |
| @@ -441,7 +575,9 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 441 | 575 | break |
| 442 | 576 | } |
| 443 | 577 | |
| 444 | return comments.length > 0 ? comments : undefined | |
| 578 | if (comments.length === 0) return undefined | |
| 579 | comments.reverse() | |
| 580 | return comments | |
| 445 | 581 | } |
| 446 | 582 | |
| 447 | 583 | // --- High-level extraction --- |
| @@ -471,15 +607,50 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 471 | 607 | } |
| 472 | 608 | |
| 473 | 609 | 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 | } | |
| 483 | 654 | |
| 484 | 655 | const comments = extractLeadingComments(stmtStart) |
| 485 | 656 | |
| @@ -509,34 +680,87 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 509 | 680 | |
| 510 | 681 | /** Strip inline comments from a brace block and normalize indentation */ |
| 511 | 682 | 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 | |
| 513 | 685 | 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 | |
| 522 | 697 | continue |
| 523 | 698 | } |
| 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) | |
| 528 | 733 | } |
| 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 | |
| 533 | 748 | 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 | } | |
| 536 | 761 | } |
| 537 | if (members.length === 0) | |
| 538 | return '{}' | |
| 539 | return `{\n${members.join('\n')}\n}` | |
| 762 | ||
| 763 | return rebased.join('\n') | |
| 540 | 764 | } |
| 541 | 765 | |
| 542 | 766 | /** Extract type parameters <...> (normalized to single line) */ |
| @@ -546,9 +770,25 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 546 | 770 | const start = pos |
| 547 | 771 | findMatchingClose(CH_LANGLE, CH_RANGLE) |
| 548 | 772 | const raw = source.slice(start, pos) |
| 549 | // Normalize multi-line generics to single line | |
| 773 | // Normalize multi-line generics to single line (avoid regex) | |
| 550 | 774 | 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 | |
| 552 | 792 | } |
| 553 | 793 | return raw |
| 554 | 794 | } |
| @@ -618,6 +858,86 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 618 | 858 | return `(${dtsParams.join(', ')})` |
| 619 | 859 | } |
| 620 | 860 | |
| 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 | ||
| 621 | 941 | /** Build a single DTS parameter from raw source text */ |
| 622 | 942 | function buildSingleDtsParam(raw: string): string { |
| 623 | 943 | let p = raw.trim() |
| @@ -720,6 +1040,28 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 720 | 1040 | type = 'unknown' |
| 721 | 1041 | } |
| 722 | 1042 | |
| 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 | ||
| 723 | 1065 | // Handle optional marker |
| 724 | 1066 | const isOptional = name.endsWith('?') || hasDefault |
| 725 | 1067 | if (name.endsWith('?')) |
| @@ -732,12 +1074,28 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 732 | 1074 | return `${name}${optionalMarker}: ${type}` |
| 733 | 1075 | } |
| 734 | 1076 | |
| 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 | ||
| 735 | 1093 | /** Infer type from a default value expression (simple cases) */ |
| 736 | 1094 | function inferTypeFromDefault(value: string): string { |
| 737 | 1095 | const v = value.trim() |
| 738 | 1096 | if (v === 'true' || v === 'false') |
| 739 | 1097 | return 'boolean' |
| 740 | if (/^-?\d+(\.\d+)?$/.test(v)) | |
| 1098 | if (isNumericLiteral(v)) | |
| 741 | 1099 | return 'number' |
| 742 | 1100 | if ((v.startsWith('\'') && v.endsWith('\'')) || (v.startsWith('"') && v.endsWith('"'))) |
| 743 | 1101 | return 'string' |
| @@ -753,7 +1111,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 753 | 1111 | const v = value.trim() |
| 754 | 1112 | if (v === 'true' || v === 'false') |
| 755 | 1113 | return v |
| 756 | if (/^-?\d+(\.\d+)?$/.test(v)) | |
| 1114 | if (isNumericLiteral(v)) | |
| 757 | 1115 | return v |
| 758 | 1116 | if ((v.startsWith('\'') && v.endsWith('\'')) || (v.startsWith('"') && v.endsWith('"'))) |
| 759 | 1117 | return v |
| @@ -764,8 +1122,11 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 764 | 1122 | function extractAssertion(initText: string): string | null { |
| 765 | 1123 | if (initText.endsWith('as const')) |
| 766 | 1124 | 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 | |
| 769 | 1130 | } |
| 770 | 1131 | |
| 771 | 1132 | /** Extract return type annotation `: ReturnType` after params, before `{` or `;` */ |
| @@ -877,16 +1238,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 877 | 1238 | |
| 878 | 1239 | // Build DTS text |
| 879 | 1240 | 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 + ';' | |
| 890 | 1242 | const comments = extractLeadingComments(declStart) |
| 891 | 1243 | |
| 892 | 1244 | // Record index if this function had a body (implementation signature for overloads) |
| @@ -1077,16 +1429,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 1077 | 1429 | const body = cleanBraceBlock(rawBody) |
| 1078 | 1430 | |
| 1079 | 1431 | // 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 | |
| 1090 | 1433 | const comments = extractLeadingComments(declStart) |
| 1091 | 1434 | |
| 1092 | 1435 | return { |
| @@ -1143,12 +1486,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 1143 | 1486 | pos++ |
| 1144 | 1487 | |
| 1145 | 1488 | // 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 | |
| 1152 | 1490 | const comments = extractLeadingComments(declStart) |
| 1153 | 1491 | |
| 1154 | 1492 | return { |
| @@ -1262,20 +1600,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 1262 | 1600 | const classBody = buildClassBodyDts() |
| 1263 | 1601 | |
| 1264 | 1602 | // 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 | |
| 1279 | 1604 | const comments = extractLeadingComments(declStart) |
| 1280 | 1605 | |
| 1281 | 1606 | return { |
| @@ -1958,7 +2283,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 1958 | 2283 | skipWhitespaceAndComments() |
| 1959 | 2284 | const nname = readIdent() |
| 1960 | 2285 | skipWhitespaceAndComments() |
| 1961 | const body = buildNamespaceBodyDts(`${indent} `) | |
| 2286 | const body = buildNamespaceBodyDts(indent) | |
| 1962 | 2287 | lines.push(`${indent}${prefix}${kw} ${nname} ${body}`) |
| 1963 | 2288 | } |
| 1964 | 2289 | else if (matchWord('abstract')) { |
| @@ -1995,7 +2320,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 1995 | 2320 | skipToStatementEnd() |
| 1996 | 2321 | const defText = source.slice(defStart, pos).trim().replace(/;$/, '') |
| 1997 | 2322 | if (defText) { |
| 1998 | lines.push(`${indent}export default ${defText}`) | |
| 2323 | lines.push(`${indent}export default ${defText};`) | |
| 1999 | 2324 | } |
| 2000 | 2325 | } |
| 2001 | 2326 | 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: | ||
| 2055 | 2380 | const body = pos < len && source.charCodeAt(pos) === CH_LBRACE ? buildNamespaceBodyDts() : '{}' |
| 2056 | 2381 | |
| 2057 | 2382 | // 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 | |
| 2065 | 2384 | const comments = extractLeadingComments(declStart) |
| 2066 | 2385 | const isAmbient = name.startsWith('\'') || name.startsWith('"') |
| 2067 | 2386 | |
| @@ -2077,7 +2396,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2077 | 2396 | } |
| 2078 | 2397 | } |
| 2079 | 2398 | |
| 2080 | // --- Main scan loop --- | |
| 2399 | // --- Main scan loop (first-char dispatch for speed) --- | |
| 2081 | 2400 | |
| 2082 | 2401 | while (pos < len) { |
| 2083 | 2402 | skipWhitespaceAndComments() |
| @@ -2085,25 +2404,28 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2085 | 2404 | break |
| 2086 | 2405 | |
| 2087 | 2406 | const stmtStart = pos |
| 2407 | const ch0 = source.charCodeAt(pos) | |
| 2088 | 2408 | |
| 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')) { | |
| 2091 | 2411 | declarations.push(extractImport(stmtStart)) |
| 2092 | 2412 | } |
| 2093 | else if (matchWord('export')) { | |
| 2413 | else if (ch0 === 101 /* e */ && matchWord('export')) { | |
| 2094 | 2414 | pos += 6 // skip 'export' |
| 2095 | 2415 | skipWhitespaceAndComments() |
| 2416 | const ech = source.charCodeAt(pos) | |
| 2096 | 2417 | |
| 2097 | if (matchWord('default')) { | |
| 2418 | if (ech === 100 /* d */ && matchWord('default')) { | |
| 2098 | 2419 | pos += 7 |
| 2099 | 2420 | skipWhitespaceAndComments() |
| 2421 | const dch = source.charCodeAt(pos) | |
| 2100 | 2422 | |
| 2101 | if (matchWord('function')) { | |
| 2423 | if (dch === 102 /* f */ && matchWord('function')) { | |
| 2102 | 2424 | const decl = extractFunction(stmtStart, true, false, true) |
| 2103 | 2425 | if (decl) |
| 2104 | 2426 | declarations.push(decl) |
| 2105 | 2427 | } |
| 2106 | else if (matchWord('async') && peekAfterWord('async') !== CH_SEMI) { | |
| 2428 | else if (dch === 97 /* a */ && matchWord('async') && peekAfterWord('async') !== CH_SEMI) { | |
| 2107 | 2429 | pos += 5 |
| 2108 | 2430 | skipWhitespaceAndComments() |
| 2109 | 2431 | if (matchWord('function')) { |
| @@ -2113,7 +2435,6 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2113 | 2435 | } |
| 2114 | 2436 | else { |
| 2115 | 2437 | // export default async expression |
| 2116 | const text = source.slice(stmtStart, pos).trim() | |
| 2117 | 2438 | skipToStatementEnd() |
| 2118 | 2439 | const fullText = source.slice(stmtStart, pos).trim() |
| 2119 | 2440 | declarations.push({ |
| @@ -2127,11 +2448,11 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2127 | 2448 | }) |
| 2128 | 2449 | } |
| 2129 | 2450 | } |
| 2130 | else if (matchWord('class')) { | |
| 2451 | else if (dch === 99 /* c */ && matchWord('class')) { | |
| 2131 | 2452 | const decl = extractClass(stmtStart, true, false) |
| 2132 | 2453 | declarations.push(decl) |
| 2133 | 2454 | } |
| 2134 | else if (matchWord('abstract')) { | |
| 2455 | else if (dch === 97 /* a */ && matchWord('abstract')) { | |
| 2135 | 2456 | pos += 8 |
| 2136 | 2457 | skipWhitespaceAndComments() |
| 2137 | 2458 | if (matchWord('class')) { |
| @@ -2156,7 +2477,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2156 | 2477 | }) |
| 2157 | 2478 | } |
| 2158 | 2479 | } |
| 2159 | else if (matchWord('type')) { | |
| 2480 | else if (ech === 116 /* t */ && matchWord('type')) { | |
| 2160 | 2481 | // Could be `export type Name = ...` or `export type { ... }` |
| 2161 | 2482 | const savedPos = pos |
| 2162 | 2483 | pos += 4 |
| @@ -2199,16 +2520,16 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2199 | 2520 | declarations.push(decl) |
| 2200 | 2521 | } |
| 2201 | 2522 | } |
| 2202 | else if (matchWord('interface')) { | |
| 2523 | else if (ech === 105 /* i */ && matchWord('interface')) { | |
| 2203 | 2524 | const decl = extractInterface(stmtStart, true) |
| 2204 | 2525 | declarations.push(decl) |
| 2205 | 2526 | } |
| 2206 | else if (matchWord('function')) { | |
| 2527 | else if (ech === 102 /* f */ && matchWord('function')) { | |
| 2207 | 2528 | const decl = extractFunction(stmtStart, true, false, false) |
| 2208 | 2529 | if (decl) |
| 2209 | 2530 | declarations.push(decl) |
| 2210 | 2531 | } |
| 2211 | else if (matchWord('async')) { | |
| 2532 | else if (ech === 97 /* a */ && matchWord('async')) { | |
| 2212 | 2533 | pos += 5 |
| 2213 | 2534 | skipWhitespaceAndComments() |
| 2214 | 2535 | if (matchWord('function')) { |
| @@ -2220,62 +2541,67 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2220 | 2541 | skipToStatementEnd() |
| 2221 | 2542 | } |
| 2222 | 2543 | } |
| 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 */) { | |
| 2230 | 2545 | if (matchWord('class')) { |
| 2231 | const decl = extractClass(stmtStart, true, true) | |
| 2546 | const decl = extractClass(stmtStart, true, false) | |
| 2232 | 2547 | declarations.push(decl) |
| 2233 | 2548 | } |
| 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 | |
| 2242 | 2553 | 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 | } | |
| 2245 | 2565 | } |
| 2246 | 2566 | 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) | |
| 2250 | 2576 | } |
| 2251 | 2577 | } |
| 2252 | else if (matchWord('let')) { | |
| 2578 | else if (ech === 108 /* l */ && matchWord('let')) { | |
| 2253 | 2579 | const decls = extractVariable(stmtStart, 'let', true) |
| 2254 | 2580 | declarations.push(...decls) |
| 2255 | 2581 | } |
| 2256 | else if (matchWord('var')) { | |
| 2582 | else if (ech === 118 /* v */ && matchWord('var')) { | |
| 2257 | 2583 | const decls = extractVariable(stmtStart, 'var', true) |
| 2258 | 2584 | declarations.push(...decls) |
| 2259 | 2585 | } |
| 2260 | else if (matchWord('enum')) { | |
| 2586 | else if (ech === 101 /* e */ && matchWord('enum')) { | |
| 2261 | 2587 | const decl = extractEnum(stmtStart, true, false) |
| 2262 | 2588 | declarations.push(decl) |
| 2263 | 2589 | } |
| 2264 | else if (matchWord('declare')) { | |
| 2590 | else if (ech === 100 /* d */ && matchWord('declare')) { | |
| 2265 | 2591 | pos += 7 |
| 2266 | 2592 | skipWhitespaceAndComments() |
| 2267 | 2593 | // export declare ... |
| 2268 | 2594 | handleDeclare(stmtStart, true) |
| 2269 | 2595 | } |
| 2270 | else if (matchWord('namespace')) { | |
| 2596 | else if (ech === 110 /* n */ && matchWord('namespace')) { | |
| 2271 | 2597 | const decl = extractModule(stmtStart, true, 'namespace') |
| 2272 | 2598 | declarations.push(decl) |
| 2273 | 2599 | } |
| 2274 | else if (matchWord('module')) { | |
| 2600 | else if (ech === 109 /* m */ && matchWord('module')) { | |
| 2275 | 2601 | const decl = extractModule(stmtStart, true, 'module') |
| 2276 | 2602 | declarations.push(decl) |
| 2277 | 2603 | } |
| 2278 | else if (source.charCodeAt(pos) === CH_LBRACE) { | |
| 2604 | else if (ech === CH_LBRACE) { | |
| 2279 | 2605 | // export { ... } or export { ... } from '...' |
| 2280 | 2606 | skipExportBraces() |
| 2281 | 2607 | const text = source.slice(stmtStart, pos).trim() |
| @@ -2292,7 +2618,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2292 | 2618 | end: pos, |
| 2293 | 2619 | }) |
| 2294 | 2620 | } |
| 2295 | else if (source.charCodeAt(pos) === CH_STAR) { | |
| 2621 | else if (ech === CH_STAR) { | |
| 2296 | 2622 | // export * from '...' |
| 2297 | 2623 | skipExportStar() |
| 2298 | 2624 | const text = source.slice(stmtStart, pos).trim() |
| @@ -2326,56 +2652,96 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2326 | 2652 | } |
| 2327 | 2653 | } |
| 2328 | 2654 | } |
| 2329 | else if (matchWord('declare')) { | |
| 2655 | else if (ch0 === 100 /* d */ && matchWord('declare')) { | |
| 2330 | 2656 | pos += 7 |
| 2331 | 2657 | skipWhitespaceAndComments() |
| 2332 | 2658 | handleDeclare(stmtStart, false) |
| 2333 | 2659 | } |
| 2334 | else if (matchWord('interface')) { | |
| 2660 | else if (ch0 === 105 /* i */ && matchWord('interface')) { | |
| 2335 | 2661 | // Non-exported interface |
| 2336 | 2662 | const decl = extractInterface(stmtStart, false) |
| 2337 | 2663 | nonExportedTypes.set(decl.name, decl) |
| 2338 | 2664 | } |
| 2339 | else if (matchWord('type')) { | |
| 2665 | else if (ch0 === 116 /* t */ && matchWord('type')) { | |
| 2340 | 2666 | // Non-exported type alias |
| 2341 | 2667 | const decl = extractTypeAlias(stmtStart, false) |
| 2342 | 2668 | nonExportedTypes.set(decl.name, decl) |
| 2343 | 2669 | declarations.push(decl) |
| 2344 | 2670 | } |
| 2345 | else if (matchWord('function') || matchWord('async')) { | |
| 2671 | else if (ch0 === 102 /* f */ && matchWord('function')) { | |
| 2346 | 2672 | // Non-exported function — skip for DTS |
| 2347 | 2673 | skipToStatementEnd() |
| 2348 | 2674 | } |
| 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')) { | |
| 2352 | 2681 | pos += 8 |
| 2353 | 2682 | 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() | |
| 2354 | 2695 | } |
| 2696 | } | |
| 2697 | else if (ch0 === 99 /* c */) { | |
| 2355 | 2698 | if (matchWord('class')) { |
| 2356 | 2699 | const decl = extractClass(stmtStart, false, false) |
| 2357 | 2700 | nonExportedTypes.set(decl.name, decl) |
| 2358 | 2701 | declarations.push(decl) |
| 2359 | 2702 | } |
| 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 | } | |
| 2360 | 2721 | else { |
| 2722 | pos++ | |
| 2361 | 2723 | skipToStatementEnd() |
| 2362 | 2724 | } |
| 2363 | 2725 | } |
| 2364 | else if (matchWord('enum')) { | |
| 2726 | else if (ch0 === 101 /* e */ && matchWord('enum')) { | |
| 2365 | 2727 | // Non-exported enum |
| 2366 | 2728 | const decl = extractEnum(stmtStart, false, false) |
| 2367 | 2729 | nonExportedTypes.set(decl.name, decl) |
| 2368 | 2730 | declarations.push(decl) |
| 2369 | 2731 | } |
| 2370 | else if (matchWord('const') || matchWord('let') || matchWord('var')) { | |
| 2732 | else if (ch0 === 108 /* l */ && matchWord('let')) { | |
| 2371 | 2733 | // Non-exported variable — skip for DTS |
| 2372 | 2734 | skipToStatementEnd() |
| 2373 | 2735 | } |
| 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')) { | |
| 2375 | 2741 | const decl = extractModule(stmtStart, false, 'module') |
| 2376 | 2742 | declarations.push(decl) |
| 2377 | 2743 | } |
| 2378 | else if (matchWord('namespace')) { | |
| 2744 | else if (ch0 === 110 /* n */ && matchWord('namespace')) { | |
| 2379 | 2745 | const decl = extractModule(stmtStart, false, 'namespace') |
| 2380 | 2746 | declarations.push(decl) |
| 2381 | 2747 | } |
| @@ -2539,7 +2905,7 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2539 | 2905 | // declare global { ... } |
| 2540 | 2906 | pos += 6 |
| 2541 | 2907 | skipWhitespaceAndComments() |
| 2542 | const body = pos < len && source.charCodeAt(pos) === CH_LBRACE ? extractBraceBlock() : '{}' | |
| 2908 | const body = pos < len && source.charCodeAt(pos) === CH_LBRACE ? buildNamespaceBodyDts() : '{}' | |
| 2543 | 2909 | const text = `declare global ${body}` |
| 2544 | 2910 | const comments = extractLeadingComments(stmtStart) |
| 2545 | 2911 | declarations.push({ |
| @@ -2558,27 +2924,69 @@ export function scanDeclarations(source: string, filename: string, keepComments: | ||
| 2558 | 2924 | } |
| 2559 | 2925 | } |
| 2560 | 2926 | |
| 2927 | /** Check if name appears as a whole word in text (fast includes + boundary check) */ | |
| 2928 | function 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 | ||
| 2561 | 2945 | /** Resolve non-exported types that are referenced by exported declarations */ |
| 2562 | 2946 | function 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 | } | |
| 2568 | 2960 | } |
| 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 | } | |
| 2581 | 2988 | } |
| 2989 | declarations.splice(insertIdx, 0, decl) | |
| 2582 | 2990 | } |
| 2583 | 2991 | } |
| 2584 | 2992 | } |
| @@ -614,9 +614,6 @@ async function processFileWithStatsFromSource( | ||
| 614 | 614 | filePath, |
| 615 | 615 | sourceCode: processedSource, |
| 616 | 616 | declarations, |
| 617 | imports: new Map(), | |
| 618 | exports: new Set(), | |
| 619 | usedTypes: new Set(), | |
| 620 | 617 | } |
| 621 | 618 | |
| 622 | 619 | // Process declarations to generate DTS |
| @@ -1055,9 +1055,6 @@ export class DtsxLanguageServer { | ||
| 1055 | 1055 | filePath: this.uriToPath(uri), |
| 1056 | 1056 | sourceCode: doc.content, |
| 1057 | 1057 | declarations: doc.declarations, |
| 1058 | imports: new Map<string, Set<string>>(), | |
| 1059 | exports: new Set<string>(), | |
| 1060 | usedTypes: new Set<string>(), | |
| 1061 | 1058 | } |
| 1062 | 1059 | |
| 1063 | 1060 | return processDeclarations(doc.declarations, context, true, this.config.importOrder) |
| @@ -234,9 +234,6 @@ async function processFile( | ||
| 234 | 234 | filePath, |
| 235 | 235 | sourceCode: content, |
| 236 | 236 | declarations, |
| 237 | imports: new Map(), | |
| 238 | exports: new Set(), | |
| 239 | usedTypes: new Set(), | |
| 240 | 237 | } |
| 241 | 238 | |
| 242 | 239 | return processDeclarations(declarations, context) |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { ProcessingContext } from './types' |
| 2 | import { extractDeclarations } from './extractor' | |
| 2 | import { extractDeclarations } from './extractor/extract' | |
| 3 | import { scanDeclarations } from './extractor/scanner' | |
| 3 | 4 | import { processDeclarations } from './processor' |
| 4 | 5 | |
| 5 | 6 | /** |
| @@ -21,11 +22,27 @@ export function processSource( | ||
| 21 | 22 | filePath: filename, |
| 22 | 23 | sourceCode, |
| 23 | 24 | declarations, |
| 24 | imports: new Map(), | |
| 25 | exports: new Set(), | |
| 26 | usedTypes: new Set(), | |
| 27 | 25 | } |
| 28 | 26 | |
| 29 | 27 | // Process declarations to generate DTS |
| 30 | 28 | return processDeclarations(declarations, context, keepComments, importOrder) |
| 31 | 29 | } |
| 30 | ||
| 31 | /** | |
| 32 | * Fast path for project mode — skips cache lookup/store. | |
| 33 | * Use when processing many files once (no cache benefit). | |
| 34 | */ | |
| 35 | export 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 | } | |
| @@ -10,10 +10,15 @@ export function formatComments(comments: string[] | undefined, keepComments: boo | ||
| 10 | 10 | return '' |
| 11 | 11 | } |
| 12 | 12 | |
| 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' | |
| 17 | 16 | |
| 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' | |
| 19 | 24 | } |
| @@ -64,6 +64,12 @@ export function processVariableDeclaration(decl: Declaration, keepComments: bool | ||
| 64 | 64 | // Add comments if present |
| 65 | 65 | const comments = formatComments(decl.leadingComments, keepComments) |
| 66 | 66 | |
| 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 | ||
| 67 | 73 | let result = '' |
| 68 | 74 | |
| 69 | 75 | // Add export if needed |
| @@ -197,11 +203,14 @@ export function processTypeDeclaration(decl: Declaration, keepComments: boolean | ||
| 197 | 203 | result += 'declare ' |
| 198 | 204 | } |
| 199 | 205 | |
| 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) | |
| 205 | 214 | result += typeDef |
| 206 | 215 | } |
| 207 | 216 | else { |
| @@ -260,10 +269,10 @@ export function processEnumDeclaration(decl: Declaration, keepComments: boolean | ||
| 260 | 269 | // Add enum name |
| 261 | 270 | result += decl.name |
| 262 | 271 | |
| 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) | |
| 267 | 276 | } |
| 268 | 277 | else { |
| 269 | 278 | result += ' {}' |
| @@ -280,8 +289,10 @@ export function processImportDeclaration(decl: Declaration): string { | ||
| 280 | 289 | // Just ensure they end with semicolon |
| 281 | 290 | let result = decl.text.trim() |
| 282 | 291 | |
| 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) | |
| 285 | 296 | |
| 286 | 297 | // Add single semicolon |
| 287 | 298 | result += ';' |
| @@ -320,10 +331,10 @@ export function processModuleDeclaration(decl: Declaration, keepComments: boolea | ||
| 320 | 331 | // Add module name |
| 321 | 332 | result += decl.name |
| 322 | 333 | |
| 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) | |
| 327 | 338 | } |
| 328 | 339 | else { |
| 329 | 340 | result += ' {}' |
| @@ -351,10 +362,10 @@ export function processModuleDeclaration(decl: Declaration, keepComments: boolea | ||
| 351 | 362 | // Add namespace name |
| 352 | 363 | result += decl.name |
| 353 | 364 | |
| 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) | |
| 358 | 369 | } |
| 359 | 370 | else { |
| 360 | 371 | result += ' {}' |
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | */ |
| 4 | 4 | |
| 5 | 5 | import type { Declaration, DeclarationKind, ProcessingContext } from '../types' |
| 6 | import { extractTripleSlashDirectives } from '../extractor/helpers' | |
| 6 | import { extractTripleSlashDirectives } from '../extractor/directives' | |
| 7 | 7 | import { getCachedRegex } from './cache' |
| 8 | 8 | import { formatComments } from './comments' |
| 9 | 9 | import { |
| @@ -62,10 +62,15 @@ export function processDeclarations( | ||
| 62 | 62 | const output: string[] = [] |
| 63 | 63 | |
| 64 | 64 | // 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 | } | |
| 69 | 74 | } |
| 70 | 75 | |
| 71 | 76 | // Group declarations by type for better organization (single pass) |
| @@ -99,8 +104,10 @@ export function processDeclarations( | ||
| 99 | 104 | |
| 100 | 105 | // Parse all exports to understand what's being exported |
| 101 | 106 | const exportedItems = new Set<string>() |
| 102 | const exportStatements: string[] = [] | |
| 107 | const typeExportStatements: string[] = [] | |
| 108 | const valueExportStatements: string[] = [] | |
| 103 | 109 | const defaultExport: string[] = [] |
| 110 | const seenExports = new Set<string>() | |
| 104 | 111 | |
| 105 | 112 | for (const decl of exports) { |
| 106 | 113 | // Prepend comments if present |
| @@ -129,8 +136,15 @@ export function processDeclarations( | ||
| 129 | 136 | } |
| 130 | 137 | |
| 131 | 138 | 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 | } | |
| 134 | 148 | } |
| 135 | 149 | } |
| 136 | 150 | } |
| @@ -145,55 +159,47 @@ export function processDeclarations( | ||
| 145 | 159 | } |
| 146 | 160 | } |
| 147 | 161 | |
| 148 | // Get all unique imported item names for regex matching | |
| 149 | const allImportedItemNames = Array.from(allImportedItemsMap.keys()) | |
| 150 | ||
| 151 | 162 | // Filter imports to only include those that are used in exports or declarations |
| 152 | 163 | const usedImportItems = new Set<string>() |
| 153 | 164 | |
| 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[] = [] | |
| 156 | 167 | |
| 157 | 168 | // Add exported functions |
| 158 | 169 | for (const func of functions) { |
| 159 | 170 | if (func.isExported) { |
| 160 | declarationTexts.push({ text: func.text, additionalTexts: [] }) | |
| 171 | allTexts.push(func.text) | |
| 161 | 172 | } |
| 162 | 173 | } |
| 163 | 174 | |
| 164 | 175 | // Add exported variables |
| 165 | 176 | for (const variable of variables) { |
| 166 | 177 | if (variable.isExported) { |
| 167 | const additionalTexts: string[] = [] | |
| 178 | allTexts.push(variable.text) | |
| 168 | 179 | if (variable.typeAnnotation) { |
| 169 | additionalTexts.push(variable.typeAnnotation) | |
| 180 | allTexts.push(variable.typeAnnotation) | |
| 170 | 181 | } |
| 171 | declarationTexts.push({ text: variable.text, additionalTexts }) | |
| 172 | 182 | } |
| 173 | 183 | } |
| 174 | 184 | |
| 175 | 185 | // Build reference check sets for interfaces (optimized: single pass over text sources) |
| 176 | 186 | const interfaceReferences = new Set<string>() |
| 177 | 187 | 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 = '' | |
| 180 | 190 | for (const func of functions) { |
| 181 | 191 | if (func.isExported) |
| 182 | textsToSearch.push(func.text) | |
| 192 | refCheckText += func.text + '\n' | |
| 183 | 193 | } |
| 184 | 194 | for (const cls of classes) { |
| 185 | textsToSearch.push(cls.text) | |
| 195 | refCheckText += cls.text + '\n' | |
| 186 | 196 | } |
| 187 | 197 | for (const type of types) { |
| 188 | textsToSearch.push(type.text) | |
| 198 | refCheckText += type.text + '\n' | |
| 189 | 199 | } |
| 190 | 200 | |
| 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 | |
| 195 | 201 | for (const iface of interfaces) { |
| 196 | if (combinedText.includes(iface.name)) { | |
| 202 | if (refCheckText.includes(iface.name)) { | |
| 197 | 203 | interfaceReferences.add(iface.name) |
| 198 | 204 | } |
| 199 | 205 | } |
| @@ -202,43 +208,37 @@ export function processDeclarations( | ||
| 202 | 208 | // Add interfaces (exported or referenced) |
| 203 | 209 | for (const iface of interfaces) { |
| 204 | 210 | if (iface.isExported || interfaceReferences.has(iface.name)) { |
| 205 | declarationTexts.push({ text: iface.text, additionalTexts: [] }) | |
| 211 | allTexts.push(iface.text) | |
| 206 | 212 | } |
| 207 | 213 | } |
| 208 | 214 | |
| 209 | // Add all types, classes, enums, modules (they may be included in DTS) | |
| 215 | // Add all types, classes, enums, modules | |
| 210 | 216 | for (const type of types) { |
| 211 | declarationTexts.push({ text: type.text, additionalTexts: [] }) | |
| 217 | allTexts.push(type.text) | |
| 212 | 218 | } |
| 213 | 219 | for (const cls of classes) { |
| 214 | declarationTexts.push({ text: cls.text, additionalTexts: [] }) | |
| 220 | allTexts.push(cls.text) | |
| 215 | 221 | } |
| 216 | 222 | for (const enumDecl of enums) { |
| 217 | declarationTexts.push({ text: enumDecl.text, additionalTexts: [] }) | |
| 223 | allTexts.push(enumDecl.text) | |
| 218 | 224 | } |
| 219 | 225 | for (const mod of modules) { |
| 220 | declarationTexts.push({ text: mod.text, additionalTexts: [] }) | |
| 226 | allTexts.push(mod.text) | |
| 221 | 227 | } |
| 222 | 228 | |
| 223 | 229 | // Add export statements |
| 224 | 230 | 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) | |
| 235 | 232 | } |
| 236 | const combinedDeclarationText = allTexts.join('\n') | |
| 237 | 233 | |
| 238 | 234 | // 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 | } | |
| 242 | 242 | } |
| 243 | 243 | } |
| 244 | 244 | |
| @@ -326,62 +326,63 @@ export function processDeclarations( | ||
| 326 | 326 | output.push('') |
| 327 | 327 | |
| 328 | 328 | // 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) | |
| 376 | 370 | } |
| 377 | 371 | } |
| 378 | 372 | |
| 379 | 373 | // 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]) | |
| 382 | 375 | |
| 383 | 376 | // 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 | |
| 387 | 388 | } |
| @@ -476,7 +476,7 @@ export interface ProcessingContext { | ||
| 476 | 476 | filePath: string |
| 477 | 477 | sourceCode: string |
| 478 | 478 | 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> | |
| 482 | 482 | } |
| @@ -166,9 +166,6 @@ export class WorkerPool { | ||
| 166 | 166 | filePath: task.filePath, |
| 167 | 167 | sourceCode: task.sourceCode, |
| 168 | 168 | declarations, |
| 169 | imports: new Map(), | |
| 170 | exports: new Set(), | |
| 171 | usedTypes: new Set(), | |
| 172 | 169 | }, true, task.config?.importOrder); |
| 173 | 170 | |
| 174 | 171 | return { |
moved, unchanged · 11 hidden lines
| @@ -1,3 +1,11 @@ moved, unchanged | ||
| 1 | /** | |
| 2 | * Logger interface for dependency injection | |
| 3 | */ | |
| 4 | declare interface Logger { | |
| 5 | info(message: string, ...args: any[]): void | |
| 6 | warn(message: string, ...args: any[]): void | |
| 7 | error(message: string, error?: Error): void | |
| 8 | } | |
| 1 | 9 | /** |
| 2 | 10 | * Application events definition |
| 3 | 11 | */ |
| @@ -14,21 +22,13 @@ export declare interface Serializable { | ||
| 14 | 22 | serialize(): string |
| 15 | 23 | deserialize(data: string): void |
| 16 | 24 | } |
| 17 | /** | |
| 18 | * Logger interface for dependency injection | |
| 19 | */ | |
| 20 | declare interface Logger { | |
| 21 | info(message: string, ...args: any[]): void | |
| 22 | warn(message: string, ...args: any[]): void | |
| 23 | error(message: string, error?: Error): void | |
| 24 | } | |
| 25 | 25 | /** |
| 26 | 26 | * Base event emitter class |
| 27 | 27 | */ |
| 28 | 28 | export declare abstract class EventEmitter<Events extends Record<string, any[]>> { |
| 29 | 29 | on<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void): this; |
| 30 | 30 | 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; | |
| 32 | 32 | abstract dispose(): void; |
| 33 | 33 | } |
| 34 | 34 | /** |
| @@ -47,9 +47,8 @@ export declare class Application extends EventEmitter<AppEvents> { | ||
| 47 | 47 | * Generic container with static factory methods |
| 48 | 48 | */ |
| 49 | 49 | export declare class Container<T> { |
| 50 | constructor(items?: T[]); | |
| 51 | 50 | static empty<T>(): Container<T>; |
| 52 | static of<T>(items: T[]): Container<T>; | |
| 51 | static of<T>(...items: T[]): Container<T>; | |
| 53 | 52 | static from<T>(iterable: Iterable<T>): Container<T>; |
| 54 | 53 | add(item: T): this; |
| 55 | 54 | map<U>(fn: (item: T) => U): Container<U>; |
| @@ -62,6 +61,7 @@ export declare class Container<T> { | ||
| 62 | 61 | * Class implementing interface with index signature |
| 63 | 62 | */ |
| 64 | 63 | export declare class DataStore implements Serializable { |
| 64 | [key: string]: unknown; | |
| 65 | 65 | serialize(): string; |
| 66 | 66 | deserialize(data: string): void; |
| 67 | 67 | } |