From b27555243de668001ed5f0e99cedf9a65310288c Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 21 Dec 2021 00:32:59 -0500 Subject: [PATCH] Fix class imports from node modules --- src/index.ts | 25 ++++++- tests/import.ts | 172 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 tests/import.ts diff --git a/src/index.ts b/src/index.ts index 6aa9c37..547b3ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1087,15 +1087,38 @@ export function processProject( outFile.addStatements(functions.join('\n')) + // Memoize imports within local source file + const importsMap = new Map() + for (const impDeclaration of sourceFile.getImportDeclarations()) { + impDeclaration.getNamedImports().forEach(impSpecifier => { + importsMap.set( + impSpecifier.getText(), + impDeclaration.getModuleSpecifierValue() + ) + }) + } + outFile.addImportDeclarations( Array.from(dependencies.entries()).reduce( (structures, [importFile, imports]) => { if (outFile === importFile) { return structures } - const moduleSpecifier = outFile.getRelativePathAsModuleSpecifierTo( + + let moduleSpecifier = outFile.getRelativePathAsModuleSpecifierTo( importFile ) + + if (importFile.isInNodeModules()) { + // Packages within node_modules should not be referenced via relative path + for (const im in imports) { + const importDeclaration = importsMap.get(im) + if (importDeclaration) { + moduleSpecifier = importDeclaration + } + } + } + const defaultImport = imports.default delete imports.default const namedImports = Object.entries(imports).map(([alias, name]) => diff --git a/tests/import.ts b/tests/import.ts new file mode 100644 index 0000000..a293937 --- /dev/null +++ b/tests/import.ts @@ -0,0 +1,172 @@ +import test from 'tape' +import path from 'path' +import fs from 'fs' +import { Project } from 'ts-morph' +import { processProject } from '../src' +const WorkingDir = path.dirname(__filename) +const TestFile = 'ImportTest.ts' +const TestFilePath = path.join(WorkingDir, TestFile) + +interface TestDefinition { + message: string + inputFile: string + guardFile: string +} + +// Test blueprint for running different test definitions +class Blueprint { + inputContents: string + expectedContents: string + message: string + constructor(message: string, inputFile: string, guardFile: string) { + this.inputContents = inputFile + this.expectedContents = guardFile + this.message = message + } + createTestFile() { + fs.writeFileSync(TestFilePath, this.inputContents) + } + deleteTestFile() { + fs.unlinkSync(TestFilePath) + } + buildProject() { + const project = new Project({ + skipAddingFilesFromTsConfig: true, + compilerOptions: { strict: true }, + useInMemoryFileSystem: false, + }) + project.addSourceFileAtPath(TestFilePath) + project.saveSync() + return project + } + run() { + test(this.message, t => { + this.createTestFile() + const project = this.buildProject() + t.doesNotThrow(() => { + processProject(project, { exportAll: true }) + }) + const guardFile = project.getSourceFiles()[0] + guardFile.formatText() + t.equal(guardFile.getText(), this.expectedContents) + t.end() + this.deleteTestFile() + }) + } +} + +function genBlueprint(def: TestDefinition) { + return new Blueprint(def.message, def.inputFile, def.guardFile) +} + +// Define grouping of tests +const blueprints = [ + genBlueprint({ + message: + 'interfaces from scoped package in node modules requires no import', + inputFile: `import { InMemoryFileSystemHostOptions } from "@ts-morph/common"; +export interface Foo { + target: InMemoryFileSystemHostOptions +}`, + guardFile: `import { Foo } from "./ImportTest"; + +export function isFoo(obj: any, _argumentName?: string): obj is Foo { + return ( + (obj !== null && + typeof obj === "object" || + typeof obj === "function") && + (obj.target !== null && + typeof obj.target === "object" || + typeof obj.target === "function") && + (typeof obj.target.skipLoadingLibFiles === "undefined" || + obj.target.skipLoadingLibFiles === false || + obj.target.skipLoadingLibFiles === true) + ) +} +`, + }), + genBlueprint({ + message: 'type from scoped package in node modules requires no import', + inputFile: `import { ResolutionHostFactory } from "@ts-morph/common"; +export interface Foo { + target: ResolutionHostFactory +}`, + guardFile: `import { Foo } from "./ImportTest"; + +export function isFoo(obj: any, _argumentName?: string): obj is Foo { + return ( + (obj !== null && + typeof obj === "object" || + typeof obj === "function") && + typeof obj.target === "function" + ) +} +`, + }), + genBlueprint({ + message: 'using class from scoped package in node modules', + inputFile: `import { CompilerOptionsContainer } from "@ts-morph/common"; +export interface Foo { + target: CompilerOptionsContainer +}`, + guardFile: `import { CompilerOptionsContainer } from "@ts-morph/common"; +import { Foo } from "./ImportTest"; + +export function isFoo(obj: any, _argumentName?: string): obj is Foo { + return ( + (obj !== null && + typeof obj === "object" || + typeof obj === "function") && + obj.target instanceof CompilerOptionsContainer + ) +} +`, + }), + genBlueprint({ + message: 'using multiple classes from scoped package in node modules', + inputFile: `import { CompilerOptionsContainer, TsConfigResolver, InMemoryFileSystemHost } from "@ts-morph/common"; +export interface Foo { + target: CompilerOptionsContainer, + res: TsConfigResolver, + fs: InMemoryFileSystemHost +}`, + guardFile: `import { CompilerOptionsContainer, TsConfigResolver, InMemoryFileSystemHost } from "@ts-morph/common"; +import { Foo } from "./ImportTest"; + +export function isFoo(obj: any, _argumentName?: string): obj is Foo { + return ( + (obj !== null && + typeof obj === "object" || + typeof obj === "function") && + obj.target instanceof CompilerOptionsContainer && + obj.res instanceof TsConfigResolver && + obj.fs instanceof InMemoryFileSystemHost + ) +} +`, + }), + genBlueprint({ + message: 'using class from unscoped package in node modules', + inputFile: `import { Directory } from "ts-morph"; +export interface Foo { + dir: Directory +}`, + guardFile: `import { Directory } from "ts-morph"; +import { Foo } from "./ImportTest"; + +export function isFoo(obj: any, _argumentName?: string): obj is Foo { + return ( + (obj !== null && + typeof obj === "object" || + typeof obj === "function") && + obj.dir instanceof Directory + ) +} +`, + }), +] + +// Run all tests +blueprints.forEach(bp => { + bp.run() +})