diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/package.json b/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/package.json new file mode 100644 index 00000000000..9be8a41e23e --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/package.json @@ -0,0 +1,10 @@ +{ + "name": "augmenter", + "version": "1.0.0", + "source": "src/index.ts", + "types": "dist/index.d.ts", + "main": "dist/index.js", + "dependencies": { + "original": "1.0.0" + } +} diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/src/expected.d.ts b/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/src/expected.d.ts new file mode 100644 index 00000000000..b408dfa3fe9 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/src/expected.d.ts @@ -0,0 +1,9 @@ +export const anotherThing: string; +declare module "original" { + interface Person { + greet(): string; + } +} +export const somethingElse: string; + +//# sourceMappingURL=index.d.ts.map diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/src/index.ts b/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/src/index.ts new file mode 100644 index 00000000000..a4b42add7e0 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/augmenter/src/index.ts @@ -0,0 +1,12 @@ +import { Person } from "original"; +Person.prototype.greet = function() { return `Hello ${this.name}!` } + +export const anotherThing: string = "hello"; + +declare module "original" { + interface Person { + greet(): string; + } +} + +export const somethingElse: string = "goodbye"; \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/original/built-src/index.d.ts b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/built-src/index.d.ts new file mode 100644 index 00000000000..92fb12cd0d0 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/built-src/index.d.ts @@ -0,0 +1,4 @@ +export declare class Person { + name: string; + constructor(name: string); +} diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/original/built-src/index.js b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/built-src/index.js new file mode 100644 index 00000000000..de87b1fd3b5 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/built-src/index.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Person = void 0; +class Person { + constructor(name) { + this.name = name; + } +} +exports.Person = Person; diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/original/package.json b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/package.json new file mode 100644 index 00000000000..5137c43d6c4 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/package.json @@ -0,0 +1,7 @@ +{ + "name": "original", + "version": "1.0.0", + "source": "src/index.ts", + "main": "built-src/index.js", + "types": "built-src/index.d.ts" +} diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/original/src/index.ts b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/src/index.ts new file mode 100644 index 00000000000..e99d9ddf2d3 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/original/src/index.ts @@ -0,0 +1,3 @@ +export class Person { + constructor(public name: string) {} +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/package.json b/packages/core/integration-tests/test/integration/ts-types/augmentation/package.json new file mode 100644 index 00000000000..90a0e144830 --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "workspaces": [ + "augmenter", + "original" + ] +} diff --git a/packages/core/integration-tests/test/integration/ts-types/augmentation/yarn.lock b/packages/core/integration-tests/test/integration/ts-types/augmentation/yarn.lock new file mode 100644 index 00000000000..fb57ccd13af --- /dev/null +++ b/packages/core/integration-tests/test/integration/ts-types/augmentation/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/packages/core/integration-tests/test/ts-types.js b/packages/core/integration-tests/test/ts-types.js index b06890f96ed..7f57f6bcf9b 100644 --- a/packages/core/integration-tests/test/ts-types.js +++ b/packages/core/integration-tests/test/ts-types.js @@ -323,4 +323,42 @@ describe('typescript types', function () { ); assert.equal(dist, expected); }); + + it('should work with module augmentation', async function () { + let fixtureDir = path.join(__dirname, 'integration/ts-types/augmentation'); + await outputFS.mkdirp(path.join(fixtureDir, 'node_modules')); + await ncp(fixtureDir, fixtureDir); + await outputFS.symlink( + path.join(fixtureDir, 'original'), + path.join(fixtureDir, 'node_modules/original'), + ); + + let b = await bundle(path.join(fixtureDir, 'augmenter'), { + inputFS: overlayFS, + }); + assertBundles(b, [ + { + name: 'index.js', + type: 'js', + assets: ['index.ts'], + }, + { + name: 'index.d.ts', + type: 'ts', + assets: ['index.ts'], + }, + ]); + + let dist = ( + await outputFS.readFile( + path.join(fixtureDir, 'augmenter/dist/index.d.ts'), + 'utf8', + ) + ).replace(/\r\n/g, '\n'); + let expected = await inputFS.readFile( + path.join(fixtureDir, 'augmenter/src/expected.d.ts'), + 'utf8', + ); + assert.equal(dist, expected); + }); }); diff --git a/packages/transformers/typescript-types/src/collect.js b/packages/transformers/typescript-types/src/collect.js index e337ac2f29b..ac3bf7fb7fe 100644 --- a/packages/transformers/typescript-types/src/collect.js +++ b/packages/transformers/typescript-types/src/collect.js @@ -11,6 +11,9 @@ export function collect( context: any, sourceFile: any, ): any { + // When module definitions are nested inside each other (e.g with module augmentation), + // we want to keep track of the hierarchy so we can associated nodes with the right module. + const moduleStack: Array = []; let _currentModule: ?TSModule; let visit = (node: any): any => { if (ts.isBundle(node)) { @@ -18,6 +21,7 @@ export function collect( } if (ts.isModuleDeclaration(node)) { + moduleStack.push(_currentModule); _currentModule = new TSModule(); moduleGraph.addModule(node.name.text, _currentModule); } @@ -105,7 +109,13 @@ export function collect( } } - return ts.visitEachChild(node, visit, context); + const results = ts.visitEachChild(node, visit, context); + // After we finish traversing the children of a module definition, + // we need to make sure that subsequent nodes get associated with the next-highest level module. + if (ts.isModuleDeclaration(node)) { + _currentModule = moduleStack.pop(); + } + return results; }; return ts.visitNode(sourceFile, visit); diff --git a/packages/transformers/typescript-types/src/shake.js b/packages/transformers/typescript-types/src/shake.js index bbb4dd7311d..4bab766885f 100644 --- a/packages/transformers/typescript-types/src/shake.js +++ b/packages/transformers/typescript-types/src/shake.js @@ -19,6 +19,12 @@ export function shake( // Propagate exports from the main module to determine what types should be included let exportedNames = moduleGraph.propagate(context); + // When module definitions are nested inside each other (e.g with module augmentation), + // we want to keep track of the hierarchy so we can associated nodes with the right module. + const moduleStack: Array = []; + + let addedGeneratedImports = false; + let _currentModule: ?TSModule; let visit = (node: any): any => { if (ts.isBundle(node)) { @@ -27,12 +33,22 @@ export function shake( // Flatten all module declarations into the top-level scope if (ts.isModuleDeclaration(node)) { + // Deeply nested module declarations are assumed to be module augmentations and left alone. + if (moduleStack.length >= 1) { + // Since we are hoisting them to the top-level scope, we need to add a "declare" keyword to make them ambient. + node.modifiers.unshift(ts.createModifier(ts.SyntaxKind.DeclareKeyword)); + return node; + } + + moduleStack.push(_currentModule); let isFirstModule = !_currentModule; _currentModule = moduleGraph.getModule(node.name.text); let statements = ts.visitEachChild(node, visit, context).body.statements; + _currentModule = moduleStack.pop(); - if (isFirstModule) { + if (isFirstModule && !addedGeneratedImports) { statements.unshift(...generateImports(moduleGraph)); + addedGeneratedImports = true; } return statements;