Skip to content

Commit

Permalink
swc transpiler and new --transpiler option to use third-party transpi…
Browse files Browse the repository at this point in the history
…lers (#1160)

* WIP experimental swc compiler

* Fix optional peer dep

* wip

* properly merge all of ts onto exports object

* fix clobbering of code because swc does not append a //# sourcemap comment

* More changes:
- rename from ts-node/compilers/swc to ts-node/compiler/swc-experimental
- add @swc/wasm fallback when @swc/core is not installed or available
- expand TSCommon to include all api surface used by ts-node; use TSCommon consistently (should be extracted to a different PR)
- ts-node's compiler loading logic detects a createTypescriptCompiler function and will invoke it to get instance of compiler
- fix ts-node's sourcemap comment appender to work even when TS compiler does not append a sourcemap comment.  swc does not append such a comment

* Fix bug in swc loading to allow swc API instance to be passed to factory

* lint fixes

* Fix typo in createTypescriptCompiler function name

* Switch from hacky overloading the "compiler" config to implementing a new, dedicated custom transpiler API

* fix package.json files array and add --transpiler CLI flag

* make --transpiler imply --transpile-only and add tests

* fixes

* add missing test files

* add @swc/core dep to tests

* add some jsdoc to new transpiler api surface

* change transpiler options to be specified as "transpiler: [name, {/*options*/}]"

* fix

* cleanup comments
  • Loading branch information
cspotcode committed Feb 27, 2021
1 parent 3b5b9c2 commit 0274f81
Show file tree
Hide file tree
Showing 13 changed files with 367 additions and 20 deletions.
111 changes: 111 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions package.json
Expand Up @@ -23,6 +23,7 @@
"./esm.mjs": "./esm.mjs",
"./esm/transpile-only": "./esm/transpile-only.mjs",
"./esm/transpile-only.mjs": "./esm/transpile-only.mjs",
"./transpilers/swc-experimental": "./transpilers/swc-experimental.js",
"./node10/tsconfig.json": "./node10/tsconfig.json",
"./node12/tsconfig.json": "./node12/tsconfig.json",
"./node14/tsconfig.json": "./node14/tsconfig.json"
Expand All @@ -36,6 +37,7 @@
"ts-node-transpile-only": "dist/bin-transpile.js"
},
"files": [
"transpilers/",
"dist/",
"dist-raw/",
"register/",
Expand Down Expand Up @@ -105,6 +107,8 @@
"timeout": "300s"
},
"devDependencies": {
"@swc/core": ">=1.2.45",
"@swc/wasm": ">=1.2.45",
"@types/chai": "^4.0.4",
"@types/diff": "^4.0.2",
"@types/lodash": "^4.14.151",
Expand Down Expand Up @@ -134,8 +138,18 @@
"util.promisify": "^1.0.1"
},
"peerDependencies": {
"@swc/core": ">=1.2.45",
"@swc/wasm": ">=1.2.45",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
},
"dependencies": {
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
Expand Down
8 changes: 6 additions & 2 deletions src/bin.ts
Expand Up @@ -41,6 +41,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
'--ignore-diagnostics': [String],
'--ignore': [String],
'--transpile-only': Boolean,
'--transpiler': String,
'--type-check': Boolean,
'--compiler-host': Boolean,
'--pretty': Boolean,
Expand Down Expand Up @@ -95,6 +96,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
'--ignore': ignore,
'--transpile-only': transpileOnly,
'--type-check': typeCheck,
'--transpiler': transpiler,
'--compiler-host': compilerHost,
'--pretty': pretty,
'--skip-project': skipProject,
Expand All @@ -120,11 +122,12 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
--cwd-mode Use current directory instead of <script.ts> for config resolution
--show-config Print resolved configuration and exit
-T, --transpile-only Use TypeScript's faster \`transpileModule\`
-T, --transpile-only Use TypeScript's faster \`transpileModule\` or a third-party transpiler
-H, --compiler-host Use TypeScript's compiler host API
-I, --ignore [pattern] Override the path patterns to skip compilation
-P, --project [path] Path to TypeScript JSON project file
-C, --compiler [name] Specify a custom TypeScript compiler
--transpiler [name] Specify a third-party, non-typechecking transpiler
-D, --ignore-diagnostics [code] Ignore TypeScript warnings by diagnostic code
-O, --compiler-options [opts] JSON object to merge with compiler options
Expand Down Expand Up @@ -159,8 +162,9 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
emit,
files,
pretty,
transpileOnly,
transpileOnly: transpileOnly ?? transpiler != null ? true : undefined, // tslint:disable-line:strict-type-predicates
typeCheck,
transpiler,
compilerHost,
ignore,
preferTsExts,
Expand Down
14 changes: 14 additions & 0 deletions src/index.spec.ts
Expand Up @@ -99,6 +99,8 @@ test.suite('ts-node', (test) => {
testsDirRequire.resolve('ts-node/esm/transpile-only')
testsDirRequire.resolve('ts-node/esm/transpile-only.mjs')

testsDirRequire.resolve('ts-node/transpilers/swc-experimental')

testsDirRequire.resolve('ts-node/node10/tsconfig.json')
testsDirRequire.resolve('ts-node/node12/tsconfig.json')
testsDirRequire.resolve('ts-node/node14/tsconfig.json')
Expand Down Expand Up @@ -272,6 +274,18 @@ test.suite('ts-node', (test) => {
expect(err.message).to.contain('error TS1003: Identifier expected')
})

test('should support third-party transpilers via --transpiler', async () => {
const { err, stdout } = await exec(`${cmdNoProject} --transpiler ts-node/transpilers/swc-experimental transpile-only-swc`)
expect(err).to.equal(null)
expect(stdout).to.contain('hello world')
})

test('should support third-party transpilers via tsconfig', async () => {
const { err, stdout } = await exec(`${cmdNoProject} transpile-only-swc-via-tsconfig`)
expect(err).to.equal(null)
expect(stdout).to.contain('hello world')
})

test('should pipe into `ts-node` and evaluate', async () => {
const execPromise = exec(cmd)
execPromise.child.stdin!.end("console.log('hello')")
Expand Down
78 changes: 61 additions & 17 deletions src/index.ts
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath } from 'url'
import type * as _ts from 'typescript'
import { Module, createRequire as nodeCreateRequire, createRequireFromPath as nodeCreateRequireFromPath } from 'module'
import type _createRequire from 'create-require'
import { Transpiler, TranspilerFactory } from './transpilers/types'
import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'

/** @internal */
Expand Down Expand Up @@ -132,6 +133,19 @@ export interface TSCommon {
parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent
formatDiagnostics: typeof _ts.formatDiagnostics
formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext

createDocumentRegistry: typeof _ts.createDocumentRegistry
JsxEmit: typeof _ts.JsxEmit
createModuleResolutionCache: typeof _ts.createModuleResolutionCache
resolveModuleName: typeof _ts.resolveModuleName
resolveModuleNameFromCache: typeof _ts.resolveModuleNameFromCache
resolveTypeReferenceDirective: typeof _ts.resolveTypeReferenceDirective
createIncrementalCompilerHost: typeof _ts.createIncrementalCompilerHost
createSourceFile: typeof _ts.createSourceFile
getDefaultLibFileName: typeof _ts.getDefaultLibFileName
createIncrementalProgram: typeof _ts.createIncrementalProgram
createEmitAndSemanticDiagnosticsBuilderProgram: typeof _ts.createEmitAndSemanticDiagnosticsBuilderProgram

libs?: string[]
}

Expand All @@ -156,6 +170,10 @@ export namespace TSInternal {
}
}

export interface TSCompilerFactory {
createTypescriptCompiler (options?: any): TSCommon
}

/**
* Export the current version.
*/
Expand Down Expand Up @@ -237,6 +255,10 @@ export interface CreateOptions {
* @default "typescript"
*/
compiler?: string
/**
* Specify a custom transpiler for use with transpileOnly
*/
transpiler?: string | [string, object]
/**
* Paths which should not be compiled.
*
Expand Down Expand Up @@ -568,6 +590,24 @@ export function create (rawOptions: CreateOptions = {}): Service {
getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? x => x : x => x.toLowerCase()
}

if (options.transpileOnly && typeof transformers === 'function') {
throw new TypeError('Transformers function is unavailable in "--transpile-only"')
}
let customTranspiler: Transpiler | undefined = undefined
if (options.transpiler) {
if (!transpileOnly) throw new Error('Custom transpiler can only be used when transpileOnly is enabled.')
const transpilerName = typeof options.transpiler === 'string' ? options.transpiler : options.transpiler[0]
const transpilerOptions = typeof options.transpiler === 'string' ? {} : options.transpiler[1] ?? {}
// TODO mimic fixed resolution logic from loadCompiler master
// TODO refactor into a more generic "resolve dep relative to project" helper
const transpilerPath = require.resolve(transpilerName, { paths: [cwd, __dirname] })
const transpilerFactory: TranspilerFactory = require(transpilerPath).create
customTranspiler = transpilerFactory({
service: { options, config },
...transpilerOptions
})
}

// Install source map support and read from memory cache.
sourceMapSupport.install({
environment: 'node',
Expand Down Expand Up @@ -1022,17 +1062,20 @@ export function create (rawOptions: CreateOptions = {}): Service {
}
}
} else {
if (typeof transformers === 'function') {
throw new TypeError('Transformers function is unavailable in "--transpile-only"')
}

getOutput = (code: string, fileName: string): SourceOutput => {
const result = ts.transpileModule(code, {
fileName,
compilerOptions: config.options,
reportDiagnostics: true,
transformers: transformers
})
let result: _ts.TranspileOutput
if (customTranspiler) {
result = customTranspiler.transpile(code, {
fileName
})
} else {
result = ts.transpileModule(code, {
fileName,
compilerOptions: config.options,
reportDiagnostics: true,
transformers: transformers as _ts.CustomTransformers | undefined
})
}

const diagnosticList = filterDiagnostics(result.diagnostics || [], ignoreDiagnostics)
if (diagnosticList.length) reportTSError(diagnosticList)
Expand Down Expand Up @@ -1285,12 +1328,12 @@ function filterRecognizedTsConfigTsNodeOptions (jsonObject: any): TsConfigOption
const {
compiler, compilerHost, compilerOptions, emit, files, ignore,
ignoreDiagnostics, logError, preferTsExts, pretty, require, skipIgnore,
transpileOnly, typeCheck
transpileOnly, typeCheck, transpiler
} = jsonObject as TsConfigOptions
const filteredTsConfigOptions = {
compiler, compilerHost, compilerOptions, emit, files, ignore,
ignoreDiagnostics, logError, preferTsExts, pretty, require, skipIgnore,
transpileOnly, typeCheck
transpileOnly, typeCheck, transpiler
}
// Use the typechecker to make sure this implementation has the correct set of properties
const catchExtraneousProps: keyof TsConfigOptions = null as any as keyof typeof filteredTsConfigOptions
Expand All @@ -1308,10 +1351,11 @@ type SourceOutput = [string, string]
*/
function updateOutput (outputText: string, fileName: string, sourceMap: string, getExtension: (fileName: string) => string) {
const base64Map = Buffer.from(updateSourceMap(sourceMap, fileName), 'utf8').toString('base64')
const sourceMapContent = `data:application/json;charset=utf-8;base64,${base64Map}`
const sourceMapLength = `${basename(fileName)}.map`.length + (getExtension(fileName).length - extname(fileName).length)

return outputText.slice(0, -sourceMapLength) + sourceMapContent
const sourceMapContent = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}`
// Expected form: `//# sourceMappingURL=foo.js.map` for input file foo.tsx
const sourceMapLength = /*//# sourceMappingURL=*/ 21 + /*foo.tsx*/ basename(fileName).length - /*.tsx*/ extname(fileName).length + /*.js*/ getExtension(fileName).length + /*.map*/ 4
// Only rewrite if existing directive exists, to support compilers that do not append a sourcemap directive
return (outputText.slice(-sourceMapLength, -sourceMapLength + 21) === '//# sourceMappingURL=' ? outputText.slice(0, -sourceMapLength) : outputText) + sourceMapContent
}

/**
Expand All @@ -1337,7 +1381,7 @@ function filterDiagnostics (diagnostics: readonly _ts.Diagnostic[], ignore: numb
*
* Reference: https://github.com/microsoft/TypeScript/blob/fcd9334f57d85b73dd66ad2d21c02e84822f4841/src/services/utilities.ts#L705-L731
*/
function getTokenAtPosition (ts: typeof _ts, sourceFile: _ts.SourceFile, position: number): _ts.Node {
function getTokenAtPosition (ts: TSCommon, sourceFile: _ts.SourceFile, position: number): _ts.Node {
let current: _ts.Node = sourceFile

outer: while (true) {
Expand Down

0 comments on commit 0274f81

Please sign in to comment.