Skip to content

Commit

Permalink
fix(ts-transform-paths): bad import/export in output js if importing/…
Browse files Browse the repository at this point in the history
…exporting pure interfaces
  • Loading branch information
zerkalica committed May 29, 2018
1 parent 45caff2 commit ea509a5
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 93 deletions.
97 changes: 41 additions & 56 deletions packages/ts-transform-paths/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,7 @@
import transformPathPlugin from '../src'
import * as ts from 'typescript'

function fixCompilerOptions(
factory: ts.TransformerFactory<ts.SourceFile>,
paths: ts.MapLike<string[]>
): ts.TransformerFactory<ts.SourceFile> {
return (ctx: ts.TransformationContext) => {
const oldMethod = ctx.getCompilerOptions

ctx.getCompilerOptions = function() {
const result = oldMethod.call(this)
return {...result, paths}
}

return factory(ctx)
}
}

export function createOptions(opts?: ts.TranspileOptions): ts.TranspileOptions {
const compilerOptions = {
target: ts.ScriptTarget.ES2015,
module: ts.ModuleKind.ESNext,
declarations: true,
baseUrl: '.',
paths: {
'someRoot/*': ['some/*']
}
}

return {
...opts,
compilerOptions: {
...compilerOptions,
...(opts && opts.compilerOptions)
},
transformers: {
before: [
fixCompilerOptions(
transformPathPlugin().before,
compilerOptions.paths
)
]
}
}
}
import * as path from 'path'
import * as fs from 'fs'

export interface VFile {
path: string;
Expand All @@ -56,10 +14,11 @@ export function transpile(files: VFile[], raw?: ts.CompilerOptions) {
newLine: ts.NewLineKind.LineFeed,
target: ts.ScriptTarget.ES2015,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
baseUrl: '.',
paths: {
'someRoot/*': ['some/*']
},
lib: ['lib.dom', 'lib.esnext'],
types: ['node'],
paths: {},
...raw,
}

Expand All @@ -75,7 +34,16 @@ export function transpile(files: VFile[], raw?: ts.CompilerOptions) {

const service = ts.createLanguageService(host, ts.createDocumentRegistry())

return service.getEmitOutput(files[0].path)
const id = files[0].path
const data = service.getEmitOutput(id)
const diags = [
...service.getSyntacticDiagnostics(id),
...service.getSemanticDiagnostics(id)
].map(diags => JSON.stringify(diags.messageText, null, ' '))

if (diags.length) throw new Error(`${diags.join('\n')}`)

return data
}

export class TestHost implements ts.LanguageServiceHost {
Expand All @@ -85,12 +53,25 @@ export class TestHost implements ts.LanguageServiceHost {
private files: VFile[]
) {}

private cache = new Map()

public getScriptSnapshot(
fileName: string
): ts.IScriptSnapshot | undefined {
if (this.cache.has(fileName)) return this.cache.get(fileName)
const file = this.files.find(file => file.path === fileName)
if (!file) return undefined
return ts.ScriptSnapshot.fromString(file.content)
const content = file
? file.content
: (
fs.existsSync(fileName)
? fs.readFileSync(fileName).toString()
: undefined
)

const snap = ts.ScriptSnapshot.fromString(content)
this.cache.set(fileName, snap)

return snap
}

public getCurrentDirectory() {
Expand Down Expand Up @@ -124,25 +105,29 @@ export class TestHost implements ts.LanguageServiceHost {
exclude?: string[],
include?: string[]
): string[] {
return []
return this.files.map(file => file.path)
}

public readFile(path: string, encoding?: string): string | undefined {
return this.files
.map(file => file.path)
.find(filePath => filePath === path)
const file = this.files
.find(file => file.path === path)

return file ? file.content : undefined
}

public fileExists(path: string): boolean {
return !!this.files.find(file => file.path === path)
const exists = !!this.files.find(file => file.path === path)
return exists || ts.sys.fileExists(path)
}

public getTypeRootsVersion(): number {
return 0
}

public directoryExists(directoryName: string): boolean {
return false
const exists = !!this.files.find(file => path.dirname(file.path) === './' + directoryName)

return exists || ts.sys.directoryExists(directoryName)
}

public getDirectories(directoryName: string): string[] {
Expand Down
149 changes: 116 additions & 33 deletions packages/ts-transform-paths/__tests__/transforms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,143 @@ import {transpile} from './helpers'
import * as ts from 'typescript'

describe('transforms', () => {
const interfaceLib = {
path: './lib/Some.ts',
content: `export interface Some { self: string }`,
}

const files = [
{
title: 'non-default import',
path: 'index.ts',
content: `import {some} from 'someRoot/lib'
export default some
`,
esnext: `import { some } from './some/lib';
export default some;
`,
files: [
{
path: 'index.ts',
content: `import {Some} from "someRoot/Some"
export default Some`,
},
interfaceLib,
],
esnext: ``,
commonjs: `"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lib_1 = require("./some/lib");
exports.default = lib_1.some;
`,
declaration: `import { some } from './some/lib';
export default some;
`,
Object.defineProperty(exports, "__esModule", { value: true });`,
declaration: `import { Some } from "./lib/Some";
export default Some;`,
},

{
title: 'interface import',
path: 'index.ts',
content: `import {Some} from 'someRoot/lib'
export const some: Some = { self: 'test' }
`,
esnext: `export const some = { self: 'test' };
`,
files: [
{
path: 'index.ts',
content: `import {Some} from "someRoot/Some"
export const some: Some = { self: 'test' }`,
},
interfaceLib,
],
esnext: `export const some = { self: 'test' };`,
commonjs: `"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.some = { self: 'test' };
`,
declaration: `import { Some } from './some/lib';
export declare const some: Some;
`,
exports.some = { self: 'test' };`,
declaration: `import { Some } from "./lib/Some";
export declare const some: Some;`,
},

{
title: 'require',
files: [
{
path: 'index.ts',
content: `const some = require("someRoot/Some")`,
},
interfaceLib,
],
esnext: `const some = require("./lib/Some");`,
commonjs: `const some = require("./lib/Some");`,
declaration: `declare const some: any;`,
},

{
title: 'non-default export',
files: [
{
path: 'index.ts',
content: `export {Some} from "someRoot/Some"`,
},
interfaceLib,
],
esnext: ``,
commonjs: `"use strict";
Object.defineProperty(exports, "__esModule", { value: true });`,
declaration: `export { Some } from "./lib/Some";`,
},

{
title: 'mixed non-default export',
files: [
{
path: 'index.ts',
content: `export {Some, SomeImpl} from "someRoot/Some"`,
},
{
path: './lib/Some.ts',
content: `export interface Some { self: string }
export class SomeImpl implements Some {
self: string = '123'
}`,
},
],
esnext: `export { SomeImpl } from "./lib/Some";`,
commonjs: `"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Some_1 = require("./lib/Some");
exports.SomeImpl = Some_1.SomeImpl;`,
declaration: `export { Some, SomeImpl } from "./lib/Some";`,
},

{
title: 'star export',
files: [
{
path: 'index.ts',
content: `export * from "someRoot/Some2"`,
},
interfaceLib,
{
path: './lib/Some2.ts',
content: `import {Some} from "someRoot/Some"
export const some: Some = {self: 'test'}`,
},
],
esnext: `export * from "./lib/Some2";`,
commonjs: `"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./lib/Some2"));`,
}
]

const paths = {
'someRoot/*': ['lib/*']
}

;files.forEach(item => {
it(`${item.title} transform to esnext`, () => {
const data = transpile([item], { module: ts.ModuleKind.ESNext })
expect(data.outputFiles[0].text).toEqual(item.esnext)
it(`${item.title} esnext`, () => {
const data = transpile(item.files, { module: ts.ModuleKind.ESNext, paths })
expect(data.outputFiles[0].text.trim()).toEqual(item.esnext.trim())
})

if (item.commonjs)
it(`${item.title} transform to commonjs`, () => {
const data = transpile([item], { module: ts.ModuleKind.CommonJS })
expect(data.outputFiles[0].text).toEqual(item.commonjs)
it(`${item.title} commonjs`, () => {
const data = transpile(item.files, { module: ts.ModuleKind.CommonJS, paths })
expect(data.outputFiles[0].text.trim()).toEqual(item.commonjs.trim())
})

if (item.declaration)
it(`${item.title} declaration`, () => {
const data = transpile([item], { module: ts.ModuleKind.CommonJS })
expect(data.outputFiles[1].text).toEqual(item.declaration)
const data = transpile(item.files, { module: ts.ModuleKind.CommonJS, paths })
expect(data.outputFiles[1].text.trim()).toEqual(item.declaration.trim())
})
})
})
57 changes: 56 additions & 1 deletion packages/ts-transform-paths/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,62 @@ function importPathVisitor(
fixNode.end = cachedPos + newStr.length

const newSpec = ts.createLiteral(newImport)
;(fixNode as any).text = newImport

let newNode: ts.Node | void
if (ts.isImportDeclaration(node)) {
newNode = ts.updateImportDeclaration(
node, node.decorators, node.modifiers, node.importClause, newSpec
)

/**
* Without this hack ts generates bad import of pure interface in output js,
* this causes warning "module has no exports" in bundlers.
*
* index.ts
* ```ts
* import {Some} from './lib'
* export const some: Some = { self: 'test' }
* ```
*
* lib.ts
* ```ts
* export interface Some { self: string }
* ```
*
* output: index.js
* ```js
* import { Some } from "./some/lib"
* export const some = { self: 'test' }
* ```
*/
newNode.flags = node.flags
}

if (ts.isExportDeclaration(node)) {
const exportNode = ts.updateExportDeclaration(
node, node.decorators, node.modifiers, node.exportClause, newSpec
)
if (exportNode.flags !== node.flags) {
/**
* Additional hacks for exports. Without it ts throw exception, if flags changed in export node.
*/
const ms = exportNode.moduleSpecifier
const oms = node.moduleSpecifier
ms.pos = oms.pos
ms.end = oms.end
ms.parent = oms.parent

newNode = exportNode

newNode.flags = node.flags
}
}

if (ts.isCallExpression(node)) newNode = ts.updateCall(
node, node.expression, node.typeArguments, [newSpec]
)

return newNode
}

function patchEmitFiles(host: any): ts.TransformerFactory<ts.SourceFile>[] {
Expand Down
Loading

0 comments on commit ea509a5

Please sign in to comment.