also looking at this
fix(bundle): leave template literal text alone when renaming identifiers
#3131
3 files
+108
-3
| @@ -332,9 +332,9 @@ function parseModule(file: string, dts: string): ModuleInfo { | ||
| 332 | 332 | } |
| 333 | 333 | |
| 334 | 334 | /** |
| 335 | * Rename identifier references in declaration text. Strings, comments, | |
| 336 | * qualified-name members (`a.b`) and property or parameter names (`{ b: T }`, | |
| 337 | * `(b: T)`) are left alone. | |
| 335 | * Rename identifier references in declaration text. Strings, template literal | |
| 336 | * text (not its `${}` placeholders), comments, qualified-name members (`a.b`) | |
| 337 | * and property or parameter names (`{ b: T }`, `(b: T)`) are left alone. | |
| 338 | 338 | */ |
| 339 | 339 | export function renameIdentifiers(text: string, renames: Map<string, string>): string { |
| 340 | 340 | if (renames.size === 0) return text |
| @@ -342,8 +342,47 @@ export function renameIdentifiers(text: string, renames: Map<string, string>): s | ||
| 342 | 342 | let i = 0 |
| 343 | 343 | const n = text.length |
| 344 | 344 | let lastSignificant = '\n' |
| 345 | // One entry per template literal or placeholder the scan is in: -1 for | |
| 346 | // template text, otherwise the braces open inside that placeholder. | |
| 347 | const templates: number[] = [] | |
| 345 | 348 | while (i < n) { |
| 346 | 349 | const c = text[i] |
| 350 | if (templates.length > 0 && templates[templates.length - 1] === -1) { | |
| 351 | if (c === '\\') { | |
| 352 | out += text.slice(i, i + 2) | |
| 353 | i += 2 | |
| 354 | } | |
| 355 | else if (c === '`') { | |
| 356 | templates.pop() | |
| 357 | out += c | |
| 358 | lastSignificant = c | |
| 359 | i++ | |
| 360 | } | |
| 361 | else if (c === '$' && text[i + 1] === '{') { | |
| 362 | templates.push(0) | |
| 363 | out += '${' | |
| 364 | lastSignificant = '{' | |
| 365 | i += 2 | |
| 366 | } | |
| 367 | else { | |
| 368 | out += c | |
| 369 | i++ | |
| 370 | } | |
| 371 | continue | |
| 372 | } | |
| 373 | if (c === '`') { | |
| 374 | templates.push(-1) | |
| 375 | out += c | |
| 376 | i++ | |
| 377 | continue | |
| 378 | } | |
| 379 | if (templates.length > 0 && c === '{') { | |
| 380 | templates[templates.length - 1]++ | |
| 381 | } | |
| 382 | else if (templates.length > 0 && c === '}') { | |
| 383 | if (templates[templates.length - 1] === 0) templates.pop() | |
| 384 | else templates[templates.length - 1]-- | |
| 385 | } | |
| 347 | 386 | if (c === '/' && text[i + 1] === '*') { |
| 348 | 387 | const end = text.indexOf('*/', i + 2) |
| 349 | 388 | const stop = end === -1 ? n : end + 2 |
| @@ -31,4 +31,18 @@ describe('renameIdentifiers', () => { | ||
| 31 | 31 | 'declare function f(options: Options_1, o: { Options: string, x?: Options_1 }): Ns.Options | \'Options\';', |
| 32 | 32 | ) |
| 33 | 33 | }) |
| 34 | ||
| 35 | it('leaves template literal text alone and renames inside its placeholders', () => { | |
| 36 | const renames = new Map([['Options', 'Options_1']]) | |
| 37 | const text = [ | |
| 38 | 'type A = `Options` | `// Options` | `\\`Options` | `\\${Options}` | Options;', | |
| 39 | 'type B = `${Options}-Options` | `a${`Options-${Options}`}b` | `${{ Options: 1 } extends Options ? \'x\' : \'y\'}`;', | |
| 40 | 'type C = Options;', | |
| 41 | ].join('\n') | |
| 42 | expect(renameIdentifiers(text, renames)).toBe([ | |
| 43 | 'type A = `Options` | `// Options` | `\\`Options` | `\\${Options}` | Options_1;', | |
| 44 | 'type B = `${Options_1}-Options` | `a${`Options-${Options_1}`}b` | `${{ Options: 1 } extends Options_1 ? \'x\' : \'y\'}`;', | |
| 45 | 'type C = Options_1;', | |
| 46 | ].join('\n')) | |
| 47 | }) | |
| 34 | 48 | }) |
| @@ -461,6 +461,58 @@ describe('generate with bundle: true', () => { | ||
| 461 | 461 | await rm(dir, { recursive: true, force: true }) |
| 462 | 462 | }) |
| 463 | 463 | |
| 464 | // Renaming a linked name (`enabled` to the default export's helper, a | |
| 465 | // colliding `Options` to `Options_1`) also rewrote the same word inside | |
| 466 | // template literal types, changing what they accept. | |
| 467 | it('keeps template literal text when renaming', async () => { | |
| 468 | const dir = await mkdtemp(join(tmpdir(), 'dtsx-bundle-template-')) | |
| 469 | await writeFiles(dir, { | |
| 470 | 'src/index.ts': [ | |
| 471 | `import enabled from './flag'`, | |
| 472 | `export * from './labels'`, | |
| 473 | `export type Options = 'x' | 'y'`, | |
| 474 | 'export type Status = `enabled` | `Options`', | |
| 475 | `export function isEnabled(): number { return Number(enabled) }`, | |
| 476 | ].join('\n'), | |
| 477 | 'src/flag.ts': `export default 1`, | |
| 478 | 'src/labels.ts': [ | |
| 479 | `type Options = 'a' | 'b'`, | |
| 480 | 'export type Label = `Options-${Options}`', | |
| 481 | ].join('\n'), | |
| 482 | 'consumer.ts': [ | |
| 483 | `import { isEnabled, type Label, type Options, type Status } from './dist/index'`, | |
| 484 | `const status: Status = 'enabled'`, | |
| 485 | `const other: Status = 'Options'`, | |
| 486 | `const label: Label = 'Options-a'`, | |
| 487 | `const option: Options = 'x'`, | |
| 488 | `export { status, other, label, option, isEnabled }`, | |
| 489 | ].join('\n'), | |
| 490 | }) | |
| 491 | ||
| 492 | await generate({ | |
| 493 | cwd: dir, | |
| 494 | root: 'src', | |
| 495 | entrypoints: ['index.ts'], | |
| 496 | outdir: join(dir, 'dist'), | |
| 497 | clean: false, | |
| 498 | keepComments: false, | |
| 499 | tsconfigPath: '', | |
| 500 | verbose: false, | |
| 501 | bundle: true, | |
| 502 | }) | |
| 503 | const bundled = await Bun.file(join(dir, 'dist', 'index.d.ts')).text() | |
| 504 | ||
| 505 | expect(bundled).toContain('type Status = `enabled` | `Options`;') | |
| 506 | expect(bundled).toContain('type Label = `Options-${Options_1}`;') | |
| 507 | ||
| 508 | if (tscAvailable) { | |
| 509 | const result = runTsc(dir, ['consumer.ts']) | |
| 510 | expect(result.output).toBe('') | |
| 511 | expect(result.ok).toBe(true) | |
| 512 | } | |
| 513 | await rm(dir, { recursive: true, force: true }) | |
| 514 | }) | |
| 515 | ||
| 464 | 516 | it('writes one bundled file per entrypoint', async () => { |
| 465 | 517 | await writeFiles(TMP, { |
| 466 | 518 | 'src/index.ts': `export * from './router';`, |