Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix class imports from node modules #165

Merged
merged 1 commit into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,15 +1087,38 @@ export function processProject(

outFile.addStatements(functions.join('\n'))

// Memoize imports within local source file
const importsMap = new Map<string, string>()
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]) =>
Expand Down
172 changes: 172 additions & 0 deletions tests/import.ts
Original file line number Diff line number Diff line change
@@ -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()
})