From b255b0edf5be54c82e8fa1e94422d7df73c658b3 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 9 Oct 2021 20:43:08 -0400 Subject: [PATCH 01/26] Fix #1488: circular dependency issue in dist-raw (#1489) * Fix #1488 * add regression test --- dist-raw/node-package-json-reader.js | 2 +- src/test/regression.spec.ts | 39 ++++++++++++++++++++++++++++ tests/1488/index.js | 1 + tests/1488/package.json | 1 + tests/1488/tsconfig.json | 8 ++++++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/test/regression.spec.ts create mode 100644 tests/1488/index.js create mode 100644 tests/1488/package.json create mode 100644 tests/1488/tsconfig.json diff --git a/dist-raw/node-package-json-reader.js b/dist-raw/node-package-json-reader.js index 1c36501cd..e9f82c6f4 100644 --- a/dist-raw/node-package-json-reader.js +++ b/dist-raw/node-package-json-reader.js @@ -5,6 +5,7 @@ const { SafeMap } = require('./node-primordials'); const { internalModuleReadJSON } = require('./node-internal-fs'); const { pathToFileURL } = require('url'); const { toNamespacedPath } = require('path'); +// const { getOptionValue } = require('./node-options'); const cache = new SafeMap(); @@ -23,7 +24,6 @@ function read(jsonPath) { toNamespacedPath(jsonPath) ); const result = { string, containsKeys }; - const { getOptionValue } = require('./node-options'); if (string !== undefined) { if (manifest === undefined) { // manifest = getOptionValue('--experimental-policy') ? diff --git a/src/test/regression.spec.ts b/src/test/regression.spec.ts new file mode 100644 index 000000000..db3c44649 --- /dev/null +++ b/src/test/regression.spec.ts @@ -0,0 +1,39 @@ +// Misc regression tests go here if they do not have a better home + +import * as exp from 'expect'; +import { join } from 'path'; +import { createExec, createExecTester } from './exec-helpers'; +import { + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, + contextTsNodeUnderTest, + TEST_DIR, +} from './helpers'; +import { test as _test } from './testlib'; + +const test = _test.context(contextTsNodeUnderTest); +const exec = createExecTester({ + exec: createExec({ + cwd: TEST_DIR, + }), +}); + +test('#1488 regression test', async () => { + // Scenario that caused the bug: + // `allowJs` turned on + // `skipIgnore` turned on so that ts-node tries to compile itself (not ideal but theoretically we should be ok with this) + // Attempt to `require()` a `.js` file + // `assertScriptCanLoadAsCJS` is triggered within `require()` + // `./package.json` needs to be fetched into cache via `assertScriptCanLoadAsCJS` which caused a recursive `require()` call + // Circular dependency warning is emitted by node + + const { stdout, stderr } = await exec({ + exec: createExec({ + cwd: join(TEST_DIR, '1488'), + }), + cmd: `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.js`, + }); + + // Assert that we do *not* get `Warning: Accessing non-existent property 'getOptionValue' of module exports inside circular dependency` + exp(stdout).toBe('foo\n'); // prove that it ran + exp(stderr).toBe(''); // prove that no warnings +}); diff --git a/tests/1488/index.js b/tests/1488/index.js new file mode 100644 index 000000000..81afa3157 --- /dev/null +++ b/tests/1488/index.js @@ -0,0 +1 @@ +console.log('foo'); diff --git a/tests/1488/package.json b/tests/1488/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/1488/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/1488/tsconfig.json b/tests/1488/tsconfig.json new file mode 100644 index 000000000..7a37a1fad --- /dev/null +++ b/tests/1488/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "allowJs": true + }, + "ts-node": { + "skipIgnore": true + } +} From 8ad5292626f1bbac52cf3e1f23de88919d7b2058 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 9 Oct 2021 20:48:09 -0400 Subject: [PATCH 02/26] Redirect source-map-support to @cspotcode/source-map-support (#1496) * initial commit * install source-map-support from git for tests --- .prettierignore | 1 + package-lock.json | 16 ++- package.json | 2 +- src/index.ts | 14 +++ src/test/sourcemaps.spec.ts | 30 ++++++ .../index.ts | 101 ++++++++++++++++++ tests/package.json | 3 +- 7 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/test/sourcemaps.spec.ts create mode 100644 tests/legacy-source-map-support-interop/index.ts diff --git a/.prettierignore b/.prettierignore index 4b463a062..e0158cb5c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,4 @@ tests/main-realpath/symlink/tsconfig.json tests/throw error.ts tests/throw error react tsx.tsx tests/esm/throw error.ts +tests/legacy-source-map-support-interop/index.ts diff --git a/package-lock.json b/package-lock.json index 3f8ca56be..43292e8a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -317,9 +317,8 @@ "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" }, "@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "github:cspotcode/node-source-map-support#63248d0f2b65074dd0fe936ae6f194c1b52b1f74", + "from": "github:cspotcode/node-source-map-support", "requires": { "@cspotcode/source-map-consumer": "0.8.0" } @@ -4668,6 +4667,17 @@ "diff": "^4.0.1", "make-error": "^1.1.1", "yn": "3.1.1" + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + } } }, "tslib": { diff --git a/package.json b/package.json index e440ef3b0..e12c6e74b 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ } }, "dependencies": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "github:cspotcode/node-source-map-support", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", diff --git a/src/index.ts b/src/index.ts index e1db16cac..f93005eea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -677,6 +677,20 @@ export function create(rawOptions: CreateOptions = {}): Service { path = normalizeSlashes(path); return outputCache.get(path)?.content || ''; }, + redirectConflictingLibrary: true, + onConflictingLibraryRedirect( + request, + parent, + isMain, + options, + redirectedRequest + ) { + debug( + `Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${ + (parent as NodeJS.Module).filename + }" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".` + ); + }, }); const shouldHavePrettyErrors = diff --git a/src/test/sourcemaps.spec.ts b/src/test/sourcemaps.spec.ts new file mode 100644 index 000000000..505107c88 --- /dev/null +++ b/src/test/sourcemaps.spec.ts @@ -0,0 +1,30 @@ +import * as expect from 'expect'; +import { createExec, createExecTester } from './exec-helpers'; +import { + CMD_TS_NODE_WITH_PROJECT_FLAG, + contextTsNodeUnderTest, + TEST_DIR, +} from './helpers'; +import { test as _test } from './testlib'; +const test = _test.context(contextTsNodeUnderTest); + +const exec = createExecTester({ + cmd: CMD_TS_NODE_WITH_PROJECT_FLAG, + exec: createExec({ + cwd: TEST_DIR, + }), +}); + +test('Redirects source-map-support to @cspotcode/source-map-support so that third-party libraries get correct source-mapped locations', async () => { + const { stdout } = await exec({ + flags: `./legacy-source-map-support-interop/index.ts`, + }); + expect(stdout.split('\n')).toMatchObject([ + expect.stringContaining('.ts:2 '), + 'true', + 'true', + expect.stringContaining('.ts:100:'), + expect.stringContaining('.ts:101 '), + '', + ]); +}); diff --git a/tests/legacy-source-map-support-interop/index.ts b/tests/legacy-source-map-support-interop/index.ts new file mode 100644 index 000000000..a4ae13c92 --- /dev/null +++ b/tests/legacy-source-map-support-interop/index.ts @@ -0,0 +1,101 @@ +import { Logger } from 'tslog'; +new Logger().info('hi'); +console.log(require.resolve('source-map-support') === require.resolve('@cspotcode/source-map-support')); +console.log(require.resolve('source-map-support/register') === require.resolve('@cspotcode/source-map-support/register')); +/* +tslog uses `require('source-map-support').wrapCallSite` directly. +Without redirection to @cspotcode/source-map-support it does not have access to the sourcemap information we provide. +*/ +interface Foo { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} +console.log(new Error().stack!.split('\n')[1]); +new Logger().info('hi'); diff --git a/tests/package.json b/tests/package.json index 1b91c8b50..371b90084 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@swc/core": "latest", - "ts-node": "file:ts-node-packed.tgz" + "ts-node": "file:ts-node-packed.tgz", + "tslog": "3.2.2" } } From b52ca45a1d8b1178640585417ed5e703f6722b2f Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Oct 2021 13:53:46 -0400 Subject: [PATCH 03/26] Allow composing `register(create())`; refactor tests (#1474) * WIP add ability to compose register(create()) * wip * Fix environmental reset in tests * fix * fix * Fix * fix * fix * fix * fix --- .vscode/launch.json | 16 ++++ .vscode/tasks.json | 13 +++ package.json | 1 + src/index.ts | 90 +++++++++++------- src/test/helpers.ts | 66 ++++++++++++- src/test/register.spec.ts | 189 +++++++++++++++++++++----------------- src/test/testlib.ts | 4 +- 7 files changed, 257 insertions(+), 122 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..8b1ffa3f4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Debug AVA test file", + "type": "node", + "request": "launch", + "preLaunchTask": "npm: pre-debug", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", + "program": "${file}", + "outputCapture": "std", + "skipFiles": [ + "/**/*.js" + ], + } + ], +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..b39ad703a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "tasks": [ + { + "type": "npm", + "script": "pre-debug", + "problemMatcher": [ + "$tsc" + ], + "label": "npm: pre-debug", + "detail": "npm run build-tsc && npm run build-pack" + } + ] +} diff --git a/package.json b/package.json index e12c6e74b..f00637cbc 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "test-cov": "nyc ava", "test": "npm run build && npm run lint && npm run test-cov --", "test-local": "npm run lint-fix && npm run build-tsc && npm run build-pack && npm run test-spec --", + "pre-debug": "npm run build-tsc && npm run build-pack", "coverage-report": "nyc report --reporter=lcov", "prepare": "npm run clean && npm run build-nopack", "api-extractor": "api-extractor run --local --verbose" diff --git a/src/index.ts b/src/index.ts index f93005eea..dee5fa1b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -427,10 +427,14 @@ export class TSError extends BaseError { } } +const TS_NODE_SERVICE_BRAND = Symbol('TS_NODE_SERVICE_BRAND'); + /** * Primary ts-node service, which wraps the TypeScript API and can compile TypeScript to JavaScript */ export interface Service { + /** @internal */ + [TS_NODE_SERVICE_BRAND]: true; ts: TSCommon; config: _ts.ParsedCommandLine; options: RegisterOptions; @@ -446,6 +450,8 @@ export interface Service { readonly shouldReplAwait: boolean; /** @internal */ addDiagnosticFilter(filter: DiagnosticFilter): void; + /** @internal */ + installSourceMapSupport(): void; } /** @@ -477,12 +483,25 @@ export function getExtensions(config: _ts.ParsedCommandLine) { return { tsExtensions, jsExtensions }; } +/** + * Create a new TypeScript compiler instance and register it onto node.js + */ +export function register(opts?: RegisterOptions): Service; /** * Register TypeScript compiler instance onto node.js */ -export function register(opts: RegisterOptions = {}): Service { +export function register(service: Service): Service; +export function register( + serviceOrOpts: Service | RegisterOptions | undefined +): Service { + // Is this a Service or a RegisterOptions? + let service = serviceOrOpts as Service; + if (!(serviceOrOpts as Service)?.[TS_NODE_SERVICE_BRAND]) { + // Not a service; is options + service = create((serviceOrOpts ?? {}) as RegisterOptions); + } + const originalJsHandler = require.extensions['.js']; - const service = create(opts); const { tsExtensions, jsExtensions } = getExtensions(service.config); const extensions = [...tsExtensions, ...jsExtensions]; @@ -660,38 +679,41 @@ export function create(rawOptions: CreateOptions = {}): Service { } // Install source map support and read from memory cache. - sourceMapSupport.install({ - environment: 'node', - retrieveFile(pathOrUrl: string) { - let path = pathOrUrl; - // If it's a file URL, convert to local path - // Note: fileURLToPath does not exist on early node v10 - // I could not find a way to handle non-URLs except to swallow an error - if (options.experimentalEsmLoader && path.startsWith('file://')) { - try { - path = fileURLToPath(path); - } catch (e) { - /* swallow error */ + installSourceMapSupport(); + function installSourceMapSupport() { + sourceMapSupport.install({ + environment: 'node', + retrieveFile(pathOrUrl: string) { + let path = pathOrUrl; + // If it's a file URL, convert to local path + // Note: fileURLToPath does not exist on early node v10 + // I could not find a way to handle non-URLs except to swallow an error + if (options.experimentalEsmLoader && path.startsWith('file://')) { + try { + path = fileURLToPath(path); + } catch (e) { + /* swallow error */ + } } - } - path = normalizeSlashes(path); - return outputCache.get(path)?.content || ''; - }, - redirectConflictingLibrary: true, - onConflictingLibraryRedirect( - request, - parent, - isMain, - options, - redirectedRequest - ) { - debug( - `Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${ - (parent as NodeJS.Module).filename - }" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".` - ); - }, - }); + path = normalizeSlashes(path); + return outputCache.get(path)?.content || ''; + }, + redirectConflictingLibrary: true, + onConflictingLibraryRedirect( + request, + parent, + isMain, + options, + redirectedRequest + ) { + debug( + `Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${ + (parent as NodeJS.Module).filename + }" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".` + ); + }, + }); + } const shouldHavePrettyErrors = options.pretty === undefined ? process.stdout.isTTY : options.pretty; @@ -1239,6 +1261,7 @@ export function create(rawOptions: CreateOptions = {}): Service { } return { + [TS_NODE_SERVICE_BRAND]: true, ts, config, compile, @@ -1250,6 +1273,7 @@ export function create(rawOptions: CreateOptions = {}): Service { moduleTypeClassifier, shouldReplAwait, addDiagnosticFilter, + installSourceMapSupport, }; } diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 6683b0558..00b59d12f 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -12,9 +12,9 @@ import type { Readable } from 'stream'; */ import type * as tsNodeTypes from '../index'; import type _createRequire from 'create-require'; -import { once } from 'lodash'; +import { has, once } from 'lodash'; import semver = require('semver'); -import { isConstructSignatureDeclaration } from 'typescript'; +import * as expect from 'expect'; const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; @@ -45,7 +45,7 @@ export const xfs = new NodeFS(fs); /** Pass to `test.context()` to get access to the ts-node API under test */ export const contextTsNodeUnderTest = once(async () => { await installTsNode(); - const tsNodeUnderTest = testsDirRequire('ts-node'); + const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node'); return { tsNodeUnderTest, }; @@ -155,3 +155,63 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) { combinedString = combinedBuffer.toString('utf8'); } } + +const defaultRequireExtensions = captureObjectState(require.extensions); +const defaultProcess = captureObjectState(process); +const defaultModule = captureObjectState(require('module')); +const defaultError = captureObjectState(Error); +const defaultGlobal = captureObjectState(global); + +/** + * Undo all of ts-node & co's installed hooks, resetting the node environment to default + * so we can run multiple test cases which `.register()` ts-node. + * + * Must also play nice with `nyc`'s environmental mutations. + */ +export function resetNodeEnvironment() { + // We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test. + require('@cspotcode/source-map-support').uninstall(); + + // Modified by ts-node hooks + resetObject(require.extensions, defaultRequireExtensions); + + // ts-node attaches a property when it registers an instance + // source-map-support monkey-patches the emit function + resetObject(process, defaultProcess); + + // source-map-support swaps out the prepareStackTrace function + resetObject(Error, defaultError); + + // _resolveFilename is modified by tsconfig-paths, future versions of source-map-support, and maybe future versions of ts-node + resetObject(require('module'), defaultModule); + + // May be modified by REPL tests, since the REPL sets globals. + resetObject(global, defaultGlobal); +} + +function captureObjectState(object: any) { + return { + descriptors: Object.getOwnPropertyDescriptors(object), + values: { ...object }, + }; +} +// Redefine all property descriptors and delete any new properties +function resetObject( + object: any, + state: ReturnType +) { + const currentDescriptors = Object.getOwnPropertyDescriptors(object); + for (const key of Object.keys(currentDescriptors)) { + if (!has(state.descriptors, key)) { + delete object[key]; + } + } + // Trigger nyc's setter functions + for (const [key, value] of Object.entries(state.values)) { + try { + object[key] = value; + } catch {} + } + // Reset descriptors + Object.defineProperties(object, state.descriptors); +} diff --git a/src/test/register.spec.ts b/src/test/register.spec.ts index 5708b9c37..db04b2846 100644 --- a/src/test/register.spec.ts +++ b/src/test/register.spec.ts @@ -1,45 +1,72 @@ import { once } from 'lodash'; import { - installTsNode, + contextTsNodeUnderTest, PROJECT, - testsDirRequire, + resetNodeEnvironment, TEST_DIR, tsNodeTypes, } from './helpers'; -import { test } from './testlib'; +import { context } from './testlib'; import { expect } from 'chai'; -import { join } from 'path'; +import * as exp from 'expect'; +import { join, resolve } from 'path'; import proxyquire = require('proxyquire'); -import type * as Module from 'module'; const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; -// Set after ts-node is installed locally -let { register }: typeof tsNodeTypes = {} as any; -test.beforeAll(async () => { - await installTsNode(); - ({ register } = testsDirRequire('ts-node')); +const createOptions: tsNodeTypes.CreateOptions = { + project: PROJECT, + compilerOptions: { + jsx: 'preserve', + }, +}; + +const test = context(contextTsNodeUnderTest).context( + once(async (t) => { + return { + moduleTestPath: resolve(__dirname, '../../tests/module.ts'), + service: t.context.tsNodeUnderTest.create(createOptions), + }; + }) +); +test.beforeEach(async (t) => { + // Un-install all hook and remove our test module from cache + resetNodeEnvironment(); + delete require.cache[t.context.moduleTestPath]; + // Paranoid check that we are truly uninstalled + exp(() => require(t.context.moduleTestPath)).toThrow( + "Unexpected token 'export'" + ); }); - -test.suite('register', (_test) => { - const test = _test.context( - once(async () => { - return { - registered: register({ - project: PROJECT, - compilerOptions: { - jsx: 'preserve', - }, - }), - moduleTestPath: require.resolve('../../tests/module'), - }; - }) +test.runSerially(); + +test('create() does not register()', async (t) => { + // nyc sets its own `require.extensions` hooks; to truly detect if we're + // installed we must attempt to load a TS file + t.context.tsNodeUnderTest.create(createOptions); + // This error indicates node attempted to run the code as .js + exp(() => require(t.context.moduleTestPath)).toThrow( + "Unexpected token 'export'" ); - test.beforeEach(async ({ context: { registered } }) => { +}); + +test('register(options) is shorthand for register(create(options))', (t) => { + t.context.tsNodeUnderTest.register(createOptions); + require(t.context.moduleTestPath); +}); + +test('register(service) registers a previously-created service', (t) => { + t.context.tsNodeUnderTest.register(t.context.service); + require(t.context.moduleTestPath); +}); + +test.suite('register(create(options))', (test) => { + test.beforeEach(async (t) => { // Re-enable project for every test. - registered.enabled(true); + t.context.service.enabled(true); + t.context.tsNodeUnderTest.register(t.context.service); + t.context.service.installSourceMapSupport(); }); - test.runSerially(); test('should be able to require typescript', ({ context: { moduleTestPath }, @@ -50,75 +77,29 @@ test.suite('register', (_test) => { }); test('should support dynamically disabling', ({ - context: { registered, moduleTestPath }, + context: { service, moduleTestPath }, }) => { delete require.cache[moduleTestPath]; - expect(registered.enabled(false)).to.equal(false); + expect(service.enabled(false)).to.equal(false); expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); delete require.cache[moduleTestPath]; - expect(registered.enabled()).to.equal(false); + expect(service.enabled()).to.equal(false); expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); delete require.cache[moduleTestPath]; - expect(registered.enabled(true)).to.equal(true); + expect(service.enabled(true)).to.equal(true); expect(() => require(moduleTestPath)).to.not.throw(); delete require.cache[moduleTestPath]; - expect(registered.enabled()).to.equal(true); + expect(service.enabled()).to.equal(true); expect(() => require(moduleTestPath)).to.not.throw(); }); - test('should support compiler scopes', ({ - context: { registered, moduleTestPath }, - }) => { - const calls: string[] = []; - - registered.enabled(false); - - const compilers = [ - register({ - projectSearchDir: join(TEST_DIR, 'scope/a'), - scopeDir: join(TEST_DIR, 'scope/a'), - scope: true, - }), - register({ - projectSearchDir: join(TEST_DIR, 'scope/a'), - scopeDir: join(TEST_DIR, 'scope/b'), - scope: true, - }), - ]; - - compilers.forEach((c) => { - const old = c.compile; - c.compile = (code, fileName, lineOffset) => { - calls.push(fileName); - - return old(code, fileName, lineOffset); - }; - }); - - try { - expect(require('../../tests/scope/a').ext).to.equal('.ts'); - expect(require('../../tests/scope/b').ext).to.equal('.ts'); - } finally { - compilers.forEach((c) => c.enabled(false)); - } - - expect(calls).to.deep.equal([ - join(TEST_DIR, 'scope/a/index.ts'), - join(TEST_DIR, 'scope/b/index.ts'), - ]); - - delete require.cache[moduleTestPath]; - - expect(() => require(moduleTestPath)).to.throw(); - }); - test('should compile through js and ts', () => { const m = require('../../tests/complex'); @@ -143,7 +124,7 @@ test.suite('register', (_test) => { try { require('../../tests/throw error'); } catch (error: any) { - expect(error.stack).to.contain( + exp(error.stack).toMatch( [ 'Error: this is a demo', ` at Foo.bar (${join(TEST_DIR, './throw error.ts')}:100:17)`, @@ -153,12 +134,10 @@ test.suite('register', (_test) => { }); test.suite('JSX preserve', (test) => { - let old: (m: Module, filename: string) => any; let compiled: string; - test.runSerially(); test.beforeAll(async () => { - old = require.extensions['.tsx']!; + const old = require.extensions['.tsx']!; require.extensions['.tsx'] = (m: any, fileName) => { const _compile = m._compile; @@ -172,9 +151,6 @@ test.suite('register', (_test) => { }); test('should use source maps', async (t) => { - t.teardown(() => { - require.extensions['.tsx'] = old; - }); try { require('../../tests/with-jsx.tsx'); } catch (error: any) { @@ -185,3 +161,46 @@ test.suite('register', (_test) => { }); }); }); + +test('should support compiler scopes w/multiple registered compiler services at once', (t) => { + const { moduleTestPath, tsNodeUnderTest } = t.context; + const calls: string[] = []; + + const compilers = [ + tsNodeUnderTest.register({ + projectSearchDir: join(TEST_DIR, 'scope/a'), + scopeDir: join(TEST_DIR, 'scope/a'), + scope: true, + }), + tsNodeUnderTest.register({ + projectSearchDir: join(TEST_DIR, 'scope/a'), + scopeDir: join(TEST_DIR, 'scope/b'), + scope: true, + }), + ]; + + compilers.forEach((c) => { + const old = c.compile; + c.compile = (code, fileName, lineOffset) => { + calls.push(fileName); + + return old(code, fileName, lineOffset); + }; + }); + + try { + expect(require('../../tests/scope/a').ext).to.equal('.ts'); + expect(require('../../tests/scope/b').ext).to.equal('.ts'); + } finally { + compilers.forEach((c) => c.enabled(false)); + } + + expect(calls).to.deep.equal([ + join(TEST_DIR, 'scope/a/index.ts'), + join(TEST_DIR, 'scope/b/index.ts'), + ]); + + delete require.cache[moduleTestPath]; + + expect(() => require(moduleTestPath)).to.throw(); +}); diff --git a/src/test/testlib.ts b/src/test/testlib.ts index 5d090d7d7..ce97a07a3 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -37,6 +37,8 @@ export const test = createTestInterface({ }); // In case someone wants to `const test = _test.context()` export { test as _test }; +// Or import `context` +export const context = test.context; export interface TestInterface< Context @@ -85,7 +87,7 @@ export interface TestInterface< beforeAll(cb: (t: ExecutionContext) => Promise): void; beforeEach(cb: (t: ExecutionContext) => Promise): void; - context( + context( cb: (t: ExecutionContext) => Promise ): TestInterface; suite(title: string, cb: (test: TestInterface) => void): void; From 4a0db317a31f03e6c16e4a27a62eada2c898bf71 Mon Sep 17 00:00:00 2001 From: Ron S Date: Sun, 10 Oct 2021 14:53:49 -0400 Subject: [PATCH 04/26] Feature: Expose esm hooks factory to public API (#1439) * Add esm to project exports * Convert internal `registerAndCreateEsmHooks` to `createEsmHooks` API * Revert "Add esm to project exports" This reverts commit f53ac63e8dfb2f301ce7885330d306a7716396c2. * Revert esm loaders + add `registerAndCreateEsmHooks` * Add tests for `createEsmHooks` * refactor experimentalEsmLoader into an @internal method on the Service: enableExperimentalEsmLoaderInterop() This avoids consumers needing to pass an @internal flag at service creation time, since we can call the method automatically within createEsmHooks. * lint-fix * Make test case more robust * fix * Fix version check; we do not support ESM loader on less than node 12.16 Co-authored-by: Andrew Bradley --- src/bin.ts | 1 - src/configuration.ts | 5 +++- src/esm.ts | 28 ++++++++++--------- src/index.ts | 27 ++++++++++++------- src/test/esm-loader.spec.ts | 39 +++++++++++++++++++++++++++ tests/esm-custom-loader/index.ts | 4 +++ tests/esm-custom-loader/loader.mjs | 16 +++++++++++ tests/esm-custom-loader/package.json | 3 +++ tests/esm-custom-loader/tsconfig.json | 7 +++++ 9 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 src/test/esm-loader.spec.ts create mode 100755 tests/esm-custom-loader/index.ts create mode 100755 tests/esm-custom-loader/loader.mjs create mode 100755 tests/esm-custom-loader/package.json create mode 100755 tests/esm-custom-loader/tsconfig.json diff --git a/src/bin.ts b/src/bin.ts index 900383d71..95c7f9320 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -299,7 +299,6 @@ export function main( ['ts-node']: { ...service.options, optionBasePaths: undefined, - experimentalEsmLoader: undefined, compilerOptions: undefined, project: service.configFilePath ?? service.options.project, }, diff --git a/src/configuration.ts b/src/configuration.ts index 6bc8e1113..a970b49c4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -251,7 +251,10 @@ export function readConfig( */ function filterRecognizedTsConfigTsNodeOptions( jsonObject: any -): { recognized: TsConfigOptions; unrecognized: any } { +): { + recognized: TsConfigOptions; + unrecognized: any; +} { if (jsonObject == null) return { recognized: {}, unrecognized: {} }; const { compiler, diff --git a/src/esm.ts b/src/esm.ts index 53e14fd71..ab1638eb1 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -1,4 +1,4 @@ -import { register, getExtensions, RegisterOptions } from './index'; +import { getExtensions, register, RegisterOptions, Service } from './index'; import { parse as parseUrl, format as formatUrl, @@ -15,17 +15,21 @@ const { // Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts +/** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` - const tsNodeInstance = register({ - ...opts, - experimentalEsmLoader: true, - }); + const tsNodeInstance = register(opts); + + return createEsmHooks(tsNodeInstance); +} + +export function createEsmHooks(tsNodeService: Service) { + tsNodeService.enableExperimentalEsmLoaderInterop(); // Custom implementation that considers additional file extensions and automatically adds file extensions const nodeResolveImplementation = createResolve({ - ...getExtensions(tsNodeInstance.config), - preferTsExts: tsNodeInstance.options.preferTsExts, + ...getExtensions(tsNodeService.config), + preferTsExts: tsNodeService.options.preferTsExts, }); return { resolve, getFormat, transformSource }; @@ -98,17 +102,17 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js const ext = extname(nativePath); let nodeSays: { format: Format }; - if (ext !== '.js' && !tsNodeInstance.ignored(nativePath)) { + if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { nodeSays = await defer(formatUrl(pathToFileURL(nativePath + '.js'))); } else { nodeSays = await defer(); } // For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification if ( - !tsNodeInstance.ignored(nativePath) && + !tsNodeService.ignored(nativePath) && (nodeSays.format === 'commonjs' || nodeSays.format === 'module') ) { - const { moduleType } = tsNodeInstance.moduleTypeClassifier.classifyModule( + const { moduleType } = tsNodeService.moduleTypeClassifier.classifyModule( normalizeSlashes(nativePath) ); if (moduleType === 'cjs') { @@ -139,11 +143,11 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { } const nativePath = fileURLToPath(url); - if (tsNodeInstance.ignored(nativePath)) { + if (tsNodeService.ignored(nativePath)) { return defer(); } - const emittedJs = tsNodeInstance.compile(sourceAsString, nativePath); + const emittedJs = tsNodeService.compile(sourceAsString, nativePath); return { source: emittedJs }; } diff --git a/src/index.ts b/src/index.ts index dee5fa1b4..5f9c5c9aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -294,12 +294,6 @@ export interface CreateOptions { transformers?: | _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers); - /** - * True if require() hooks should interop with experimental ESM loader. - * Enabled explicitly via a flag since it is a breaking change. - * @internal - */ - experimentalEsmLoader?: boolean; /** * Allows the usage of top level await in REPL. * @@ -369,7 +363,6 @@ export interface TsConfigOptions | 'dir' | 'cwd' | 'projectSearchDir' - | 'experimentalEsmLoader' | 'optionBasePaths' > {} @@ -405,7 +398,6 @@ export const DEFAULTS: RegisterOptions = { typeCheck: yn(env.TS_NODE_TYPE_CHECK), compilerHost: yn(env.TS_NODE_COMPILER_HOST), logError: yn(env.TS_NODE_LOG_ERROR), - experimentalEsmLoader: false, experimentalReplAwait: yn(env.TS_NODE_EXPERIMENTAL_REPL_AWAIT) ?? undefined, }; @@ -452,6 +444,8 @@ export interface Service { addDiagnosticFilter(filter: DiagnosticFilter): void; /** @internal */ installSourceMapSupport(): void; + /** @internal */ + enableExperimentalEsmLoaderInterop(): void; } /** @@ -688,7 +682,7 @@ export function create(rawOptions: CreateOptions = {}): Service { // If it's a file URL, convert to local path // Note: fileURLToPath does not exist on early node v10 // I could not find a way to handle non-URLs except to swallow an error - if (options.experimentalEsmLoader && path.startsWith('file://')) { + if (experimentalEsmLoader && path.startsWith('file://')) { try { path = fileURLToPath(path); } catch (e) { @@ -1260,6 +1254,15 @@ export function create(rawOptions: CreateOptions = {}): Service { }); } + /** + * True if require() hooks should interop with experimental ESM loader. + * Enabled explicitly via a flag since it is a breaking change. + */ + let experimentalEsmLoader = false; + function enableExperimentalEsmLoaderInterop() { + experimentalEsmLoader = true; + } + return { [TS_NODE_SERVICE_BRAND]: true, ts, @@ -1274,6 +1277,7 @@ export function create(rawOptions: CreateOptions = {}): Service { shouldReplAwait, addDiagnosticFilter, installSourceMapSupport, + enableExperimentalEsmLoaderInterop, }; } @@ -1468,3 +1472,8 @@ function getTokenAtPosition( return current; } } + +import type { createEsmHooks as createEsmHooksFn } from './esm'; +export const createEsmHooks: typeof createEsmHooksFn = ( + tsNodeService: Service +) => require('./esm').createEsmHooks(tsNodeService); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts new file mode 100644 index 000000000..e117e1ef8 --- /dev/null +++ b/src/test/esm-loader.spec.ts @@ -0,0 +1,39 @@ +// ESM loader hook tests +// TODO: at the time of writing, other ESM loader hook tests have not been moved into this file. +// Should consolidate them here. + +import { context } from './testlib'; +import semver = require('semver'); +import { + contextTsNodeUnderTest, + EXPERIMENTAL_MODULES_FLAG, + TEST_DIR, +} from './helpers'; +import { createExec } from './exec-helpers'; +import { join } from 'path'; +import * as expect from 'expect'; + +const test = context(contextTsNodeUnderTest); + +const exec = createExec({ + cwd: TEST_DIR, +}); + +test.suite('createEsmHooks', (test) => { + if (semver.gte(process.version, '12.16.0')) { + test('should create proper hooks with provided instance', async () => { + const { err } = await exec( + `node ${EXPERIMENTAL_MODULES_FLAG} --loader ./loader.mjs index.ts`, + { + cwd: join(TEST_DIR, './esm-custom-loader'), + } + ); + + if (err === null) { + throw new Error('Command was expected to fail, but it succeeded.'); + } + + expect(err.message).toMatch(/TS6133:\s+'unusedVar'/); + }); + } +}); diff --git a/tests/esm-custom-loader/index.ts b/tests/esm-custom-loader/index.ts new file mode 100755 index 000000000..89efb1cf9 --- /dev/null +++ b/tests/esm-custom-loader/index.ts @@ -0,0 +1,4 @@ +export function abc() { + let unusedVar: string; + return true; +} diff --git a/tests/esm-custom-loader/loader.mjs b/tests/esm-custom-loader/loader.mjs new file mode 100755 index 000000000..3b0ee683c --- /dev/null +++ b/tests/esm-custom-loader/loader.mjs @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +const require = createRequire(fileURLToPath(import.meta.url)); + +/** @type {import('../../dist')} **/ +const { createEsmHooks, register } = require('ts-node'); + +const tsNodeInstance = register({ + compilerOptions: { + noUnusedLocals: true, + }, +}); + +export const { resolve, getFormat, transformSource } = createEsmHooks( + tsNodeInstance +); diff --git a/tests/esm-custom-loader/package.json b/tests/esm-custom-loader/package.json new file mode 100755 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-custom-loader/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-custom-loader/tsconfig.json b/tests/esm-custom-loader/tsconfig.json new file mode 100755 index 000000000..ad01eee33 --- /dev/null +++ b/tests/esm-custom-loader/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "noUnusedLocals": false + } +} From a979dd6364770f3ef149f1d24795c825e8ea2d9d Mon Sep 17 00:00:00 2001 From: Jonas Kello Date: Sun, 10 Oct 2021 22:57:04 +0200 Subject: [PATCH 05/26] Update esm loader hooks API (#1457) * Initial commit * Update hooks * wip impl of load * Expose old hooks for backward compat * Some logging * Add raw copy of default get format * Adapt defaultGetFormat() from node source * Fix defaultTransformSource * Add missing newline * Fix require * Check node version to avoid deprecation warning * Remove load from old hooks * Add some comments * Use versionGte * Remove logging * Refine comments * Wording * Use format hint if available * One more comment * Nitpicky changes to comments * Update index.ts * lint-fix * attempt at downloading node nightly in tests * fix * fix * Windows install of node nightly * update version checks to be ready for node backporting * Add guards for undefined source * More error info * Skip source transform for builtin and commonjs * Update transpile-only.mjs * Tweak `createEsmHooks` type * fix test to accomodate new api Co-authored-by: Andrew Bradley --- .github/workflows/continuous-integration.yml | 31 ++++++- dist-raw/node-esm-default-get-format.js | 83 ++++++++++++++++++ esm.mjs | 1 + esm/transpile-only.mjs | 1 + package.json | 7 +- src/esm.ts | 92 +++++++++++++++++++- src/index.ts | 39 ++++++--- tests/esm-custom-loader/loader.mjs | 2 +- 8 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 dist-raw/node-esm-default-get-format.js diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e279668f4..bc954cd9e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -48,7 +48,7 @@ jobs: matrix: os: [ubuntu, windows] # Don't forget to add all new flavors to this list! - flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] include: # Node 12.15 # TODO Add comments about why we test 12.15; I think git blame says it's because of an ESM behavioral change that happened at 12.16 @@ -112,14 +112,43 @@ jobs: typescript: next typescriptFlag: next downgradeNpm: true + # Node nightly + - flavor: 11 + node: nightly + nodeFlag: nightly + typescript: latest + typescriptFlag: latest + downgradeNpm: true steps: # checkout code - uses: actions/checkout@v2 # install node - name: Use Node.js ${{ matrix.node }} + if: matrix.node != 'nightly' uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} + - name: Use Node.js 16, will be subsequently overridden by download of nightly + if: matrix.node == 'nightly' + uses: actions/setup-node@v1 + with: + node-version: 16 + - name: Download Node.js nightly + if: matrix.node == 'nightly' && matrix.os == 'ubuntu' + run: | + export N_PREFIX=$(pwd)/n + npm install -g n + n nightly + sudo cp "${N_PREFIX}/bin/node" "$(which node)" + node --version + - name: Download Node.js nightly + if: matrix.node == 'nightly' && matrix.os == 'windows' + run: | + $version = (Invoke-WebRequest https://nodejs.org/download/nightly/index.json | ConvertFrom-json)[0].version + $url = "https://nodejs.org/download/nightly/$version/win-x64/node.exe" + $targetPath = (Get-Command node.exe).Source + Invoke-WebRequest -Uri $url -OutFile $targetPath + node --version # lint, build, test # Downgrade from npm 7 to 6 because 7 still seems buggy to me - if: ${{ matrix.downgradeNpm }} diff --git a/dist-raw/node-esm-default-get-format.js b/dist-raw/node-esm-default-get-format.js new file mode 100644 index 000000000..d8af956f3 --- /dev/null +++ b/dist-raw/node-esm-default-get-format.js @@ -0,0 +1,83 @@ +// Copied from https://raw.githubusercontent.com/nodejs/node/v15.3.0/lib/internal/modules/esm/get_format.js +// Then modified to suite our needs. +// Formatting is intentionally bad to keep the diff as small as possible, to make it easier to merge +// upstream changes and understand our modifications. + +'use strict'; +const { + RegExpPrototypeExec, + StringPrototypeStartsWith, +} = require('./node-primordials'); +const { extname } = require('path'); +const { getOptionValue } = require('./node-options'); + +const experimentalJsonModules = getOptionValue('--experimental-json-modules'); +const experimentalSpeciferResolution = + getOptionValue('--experimental-specifier-resolution'); +const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); +const { getPackageType } = require('./node-esm-resolve-implementation.js').createResolve({tsExtensions: [], jsExtensions: []}); +const { URL, fileURLToPath } = require('url'); +const { ERR_UNKNOWN_FILE_EXTENSION } = require('./node-errors').codes; + +const extensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'module', + '.mjs': 'module' +}; + +const legacyExtensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'commonjs', + '.json': 'commonjs', + '.mjs': 'module', + '.node': 'commonjs' +}; + +if (experimentalWasmModules) + extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm'; + +if (experimentalJsonModules) + extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; + +function defaultGetFormat(url, context, defaultGetFormatUnused) { + if (StringPrototypeStartsWith(url, 'node:')) { + return { format: 'builtin' }; + } + const parsed = new URL(url); + if (parsed.protocol === 'data:') { + const [ , mime ] = RegExpPrototypeExec( + /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, + parsed.pathname, + ) || [ null, null, null ]; + const format = ({ + '__proto__': null, + 'text/javascript': 'module', + 'application/json': experimentalJsonModules ? 'json' : null, + 'application/wasm': experimentalWasmModules ? 'wasm' : null + })[mime] || null; + return { format }; + } else if (parsed.protocol === 'file:') { + const ext = extname(parsed.pathname); + let format; + if (ext === '.js') { + format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs'; + } else { + format = extensionFormatMap[ext]; + } + if (!format) { + if (experimentalSpeciferResolution === 'node') { + process.emitWarning( + 'The Node.js specifier resolution in ESM is experimental.', + 'ExperimentalWarning'); + format = legacyExtensionFormatMap[ext]; + } else { + throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url)); + } + } + return { format: format || null }; + } + return { format: null }; +} +exports.defaultGetFormat = defaultGetFormat; diff --git a/esm.mjs b/esm.mjs index 2a11ac36e..4d404070d 100644 --- a/esm.mjs +++ b/esm.mjs @@ -6,6 +6,7 @@ const require = createRequire(fileURLToPath(import.meta.url)); const esm = require('./dist/esm'); export const { resolve, + load, getFormat, transformSource, } = esm.registerAndCreateEsmHooks(); diff --git a/esm/transpile-only.mjs b/esm/transpile-only.mjs index c19132284..07b2c7ae6 100644 --- a/esm/transpile-only.mjs +++ b/esm/transpile-only.mjs @@ -6,6 +6,7 @@ const require = createRequire(fileURLToPath(import.meta.url)); const esm = require('../dist/esm'); export const { resolve, + load, getFormat, transformSource, } = esm.registerAndCreateEsmHooks({ transpileOnly: true }); diff --git a/package.json b/package.json index f00637cbc..8ed02f65f 100644 --- a/package.json +++ b/package.json @@ -70,10 +70,9 @@ "pre-debug": "npm run build-tsc && npm run build-pack", "coverage-report": "nyc report --reporter=lcov", "prepare": "npm run clean && npm run build-nopack", - "api-extractor": "api-extractor run --local --verbose" - }, - "engines": { - "node": ">=12.0.0" + "api-extractor": "api-extractor run --local --verbose", + "esm-usage-example": "npm run build-tsc && cd esm-usage-example && node --experimental-specifier-resolution node --loader ../esm.mjs ./index", + "esm-usage-example2": "npm run build-tsc && cd tests && TS_NODE_PROJECT=./module-types/override-to-cjs/tsconfig.json node --loader ../esm.mjs ./module-types/override-to-cjs/test.cjs" }, "repository": { "type": "git", diff --git a/src/esm.ts b/src/esm.ts index ab1638eb1..5502d0155 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -1,4 +1,10 @@ -import { getExtensions, register, RegisterOptions, Service } from './index'; +import { + register, + getExtensions, + RegisterOptions, + Service, + versionGteLt, +} from './index'; import { parse as parseUrl, format as formatUrl, @@ -12,9 +18,24 @@ import { normalizeSlashes } from './util'; const { createResolve, } = require('../dist-raw/node-esm-resolve-implementation'); +const { defaultGetFormat } = require('../dist-raw/node-esm-default-get-format'); // Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts +// NOTE ABOUT MULTIPLE EXPERIMENTAL LOADER APIS +// +// At the time of writing, this file implements 2x different loader APIs. +// Node made a breaking change to the loader API in https://github.com/nodejs/node/pull/37468 +// +// We check the node version number and export either the *old* or the *new* API. +// +// Today, we are implementing the *new* API on top of our implementation of the *old* API, +// which relies on copy-pasted code from the *old* hooks implementation in node. +// +// In the future, we will likely invert this: we will copy-paste the *new* API implementation +// from node, build our implementation of the *new* API on top of it, and implement the *old* +// hooks API as a shim to the *new* API. + /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` @@ -32,7 +53,24 @@ export function createEsmHooks(tsNodeService: Service) { preferTsExts: tsNodeService.options.preferTsExts, }); - return { resolve, getFormat, transformSource }; + // The hooks API changed in node version X so we need to check for backwards compatibility. + // TODO: When the new API is backported to v12, v14, v16, update these version checks accordingly. + const newHooksAPI = + versionGteLt(process.versions.node, '17.0.0') || + versionGteLt(process.versions.node, '16.999.999', '17.0.0') || + versionGteLt(process.versions.node, '14.999.999', '15.0.0') || + versionGteLt(process.versions.node, '12.999.999', '13.0.0'); + + // Explicit return type to avoid TS's non-ideal inferred type + const hooksAPI: { + resolve: typeof resolve; + getFormat: typeof getFormat | undefined; + transformSource: typeof transformSource | undefined; + load: typeof load | undefined; + } = newHooksAPI + ? { resolve, load, getFormat: undefined, transformSource: undefined } + : { resolve, getFormat, transformSource, load: undefined }; + return hooksAPI; function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) { // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo` @@ -76,6 +114,52 @@ export function createEsmHooks(tsNodeService: Service) { ); } + // `load` from new loader hook API (See description at the top of this file) + async function load( + url: string, + context: { format: Format | null | undefined }, + defaultLoad: typeof load + ): Promise<{ format: Format; source: string | Buffer | undefined }> { + // If we get a format hint from resolve() on the context then use it + // otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node + const format = + context.format ?? + (await getFormat(url, context, defaultGetFormat)).format; + + let source = undefined; + if (format !== 'builtin' && format !== 'commonjs') { + // Call the new defaultLoad() to get the source + const { source: rawSource } = await defaultLoad( + url, + { format }, + defaultLoad + ); + + if (rawSource === undefined || rawSource === null) { + throw new Error( + `Failed to load raw source: Format was '${format}' and url was '${url}''.` + ); + } + + // Emulate node's built-in old defaultTransformSource() so we can re-use the old transformSource() hook + const defaultTransformSource: typeof transformSource = async ( + source, + _context, + _defaultTransformSource + ) => ({ source }); + + // Call the old hook + const { source: transformedSource } = await transformSource( + rawSource, + { url, format }, + defaultTransformSource + ); + source = transformedSource; + } + + return { format, source }; + } + type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; async function getFormat( url: string, @@ -129,6 +213,10 @@ export function createEsmHooks(tsNodeService: Service) { context: { url: string; format: Format }, defaultTransformSource: typeof transformSource ): Promise<{ source: string | Buffer }> { + if (source === null || source === undefined) { + throw new Error('No source'); + } + const defer = () => defaultTransformSource(source, context, defaultTransformSource); diff --git a/src/index.ts b/src/index.ts index 5f9c5c9aa..7570d07a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,18 +47,31 @@ export type { const engineSupportsPackageTypeField = parseInt(process.versions.node.split('.')[0], 10) >= 12; -function versionGte(version: string, requirement: string) { - const [major, minor, patch, extra] = version - .split(/[\.-]/) - .map((s) => parseInt(s, 10)); - const [reqMajor, reqMinor, reqPatch] = requirement - .split('.') - .map((s) => parseInt(s, 10)); - return ( - major > reqMajor || - (major === reqMajor && - (minor > reqMinor || (minor === reqMinor && patch >= reqPatch))) - ); +/** @internal */ +export function versionGteLt( + version: string, + gteRequirement: string, + ltRequirement?: string +) { + const [major, minor, patch, extra] = parse(version); + const [gteMajor, gteMinor, gtePatch] = parse(gteRequirement); + const isGte = + major > gteMajor || + (major === gteMajor && + (minor > gteMinor || (minor === gteMinor && patch >= gtePatch))); + let isLt = true; + if (ltRequirement) { + const [ltMajor, ltMinor, ltPatch] = parse(ltRequirement); + isLt = + major < ltMajor || + (major === ltMajor && + (minor < ltMinor || (minor === ltMinor && patch < ltPatch))); + } + return isGte && isLt; + + function parse(requirement: string) { + return requirement.split(/[\.-]/).map((s) => parseInt(s, 10)); + } } /** @@ -570,7 +583,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ); } // Top-level await was added in TS 3.8 - const tsVersionSupportsTla = versionGte(ts.version, '3.8.0'); + const tsVersionSupportsTla = versionGteLt(ts.version, '3.8.0'); if (options.experimentalReplAwait === true && !tsVersionSupportsTla) { throw new Error( 'Experimental REPL await is not compatible with TypeScript versions older than 3.8' diff --git a/tests/esm-custom-loader/loader.mjs b/tests/esm-custom-loader/loader.mjs index 3b0ee683c..bf82e766b 100755 --- a/tests/esm-custom-loader/loader.mjs +++ b/tests/esm-custom-loader/loader.mjs @@ -11,6 +11,6 @@ const tsNodeInstance = register({ }, }); -export const { resolve, getFormat, transformSource } = createEsmHooks( +export const { resolve, getFormat, transformSource, load } = createEsmHooks( tsNodeInstance ); From 86c5d6ea5c593fb650e99a1f583d38472164cdec Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Oct 2021 19:05:47 -0400 Subject: [PATCH 06/26] Declare types for node builtin modules in REPL so you do not need to import them (#1500) * Declare types for node builtin modules in REPL so you do not need to import them * fix * fix --- src/index.ts | 18 +++++++-------- src/repl.ts | 17 ++++++++++++++ src/test/helpers.ts | 2 ++ src/test/index.spec.ts | 2 +- src/test/repl/node-repl-tla.ts | 2 +- src/test/repl/repl-environment.spec.ts | 8 +++---- src/test/repl/repl.spec.ts | 31 ++++++++++++++++++++++++-- 7 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7570d07a0..15fbb0ad0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -685,6 +685,15 @@ export function create(rawOptions: CreateOptions = {}): Service { }); } + /** + * True if require() hooks should interop with experimental ESM loader. + * Enabled explicitly via a flag since it is a breaking change. + */ + let experimentalEsmLoader = false; + function enableExperimentalEsmLoaderInterop() { + experimentalEsmLoader = true; + } + // Install source map support and read from memory cache. installSourceMapSupport(); function installSourceMapSupport() { @@ -1267,15 +1276,6 @@ export function create(rawOptions: CreateOptions = {}): Service { }); } - /** - * True if require() hooks should interop with experimental ESM loader. - * Enabled explicitly via a flag since it is a breaking change. - */ - let experimentalEsmLoader = false; - function enableExperimentalEsmLoaderInterop() { - experimentalEsmLoader = true; - } - return { [TS_NODE_SERVICE_BRAND]: true, ts, diff --git a/src/repl.ts b/src/repl.ts index 03d4d97d5..0ef51017d 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -14,6 +14,7 @@ import { Console } from 'console'; import * as assert from 'assert'; import type * as tty from 'tty'; import type * as Module from 'module'; +import { builtinModules } from 'module'; // Lazy-loaded. let _processTopLevelAwait: (src: string) => string | null; @@ -356,6 +357,22 @@ export function createRepl(options: CreateReplOptions = {}) { if (forceToBeModule) { state.input += 'export {};void 0;\n'; } + + // Declare node builtins. + // Skip the same builtins as `addBuiltinLibsToObject`: + // those starting with _ + // those containing / + // those that already exist as globals + // Intentionally suppress type errors in case @types/node does not declare any of them. + state.input += `// @ts-ignore\n${builtinModules + .filter( + (name) => + !name.startsWith('_') && + !name.includes('/') && + !['console', 'module', 'process'].includes(name) + ) + .map((name) => `declare import ${name} = require('${name}')`) + .join(';')}\n`; } reset(); diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 00b59d12f..245e0a924 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -40,6 +40,8 @@ export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG} // `createRequire` does not exist on older node versions export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); +export const ts = testsDirRequire('typescript'); + export const xfs = new NodeFS(fs); /** Pass to `test.context()` to get access to the ts-node API under test */ diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 3307ae73d..730d1a3be 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -3,7 +3,7 @@ import * as expect from 'expect'; import { join, resolve, sep as pathSep } from 'path'; import { tmpdir } from 'os'; import semver = require('semver'); -import ts = require('typescript'); +import { ts } from './helpers'; import { lstatSync, mkdtempSync } from 'fs'; import { npath } from '@yarnpkg/fslib'; import type _createRequire from 'create-require'; diff --git a/src/test/repl/node-repl-tla.ts b/src/test/repl/node-repl-tla.ts index 210d22ec0..616237926 100644 --- a/src/test/repl/node-repl-tla.ts +++ b/src/test/repl/node-repl-tla.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import type { Key } from 'readline'; import { Stream } from 'stream'; import semver = require('semver'); -import ts = require('typescript'); +import { ts } from '../helpers'; import type { ContextWithTsNodeUnderTest } from './helpers'; interface SharedObjects extends ContextWithTsNodeUnderTest { diff --git a/src/test/repl/repl-environment.spec.ts b/src/test/repl/repl-environment.spec.ts index 4becc6f10..9071688c6 100644 --- a/src/test/repl/repl-environment.spec.ts +++ b/src/test/repl/repl-environment.spec.ts @@ -94,7 +94,7 @@ test.suite( } ); - const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any, fs: any;`; + const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any;`; function setReportGlobal(type: 'repl' | 'stdin' | 'eval') { return ` ${declareGlobals} @@ -107,7 +107,7 @@ test.suite( modulePaths: typeof module !== 'undefined' && [...module.paths], exportsTest: typeof exports !== 'undefined' ? module.exports === exports : null, stackTest: new Error().stack!.split('\\n')[1], - moduleAccessorsTest: typeof fs === 'undefined' ? null : fs === require('fs'), + moduleAccessorsTest: eval('typeof fs') === 'undefined' ? null : eval('fs') === require('fs'), argv: [...process.argv] }; `.replace(/\n/g, ''); @@ -203,7 +203,7 @@ test.suite( exportsTest: true, // Note: vanilla node uses different name. See #1360 stackTest: expect.stringContaining( - ` at ${join(TEST_DIR, '.ts')}:2:` + ` at ${join(TEST_DIR, '.ts')}:4:` ), moduleAccessorsTest: true, argv: [tsNodeExe], @@ -356,7 +356,7 @@ test.suite( exportsTest: true, // Note: vanilla node uses different name. See #1360 stackTest: expect.stringContaining( - ` at ${join(TEST_DIR, '.ts')}:2:` + ` at ${join(TEST_DIR, '.ts')}:4:` ), moduleAccessorsTest: true, argv: [tsNodeExe], diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index b30f1b790..2ef41d15d 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -1,4 +1,4 @@ -import ts = require('typescript'); +import { ts } from '../helpers'; import semver = require('semver'); import * as expect from 'expect'; import { @@ -212,7 +212,7 @@ test.suite('top level await', (_test) => { expect(stdout).toBe('> > '); expect(stderr.replace(/\r\n/g, '\n')).toBe( - '.ts(2,7): error TS2322: ' + + '.ts(4,7): error TS2322: ' + (semver.gte(ts.version, '4.0.0') ? `Type 'number' is not assignable to type 'string'.\n` : `Type '1' is not assignable to type 'string'.\n`) + @@ -411,3 +411,30 @@ test.suite( ); } ); + +test.serial('REPL declares types for node built-ins within REPL', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `util.promisify(setTimeout)("should not be a string" as string) + type Duplex = stream.Duplex + const s = stream + 'done'`, + { + registerHooks: true, + waitPattern: `done`, + startInternalOptions: { + useGlobal: false, + }, + } + ); + + // Assert that we receive a typechecking error about improperly using + // `util.promisify` but *not* an error about the absence of `util` + expect(stderr).not.toMatch("Cannot find name 'util'"); + expect(stderr).toMatch( + "Argument of type 'string' is not assignable to parameter of type 'number'" + ); + // Assert that both types and values can be used without error + expect(stderr).not.toMatch("Cannot find namespace 'stream'"); + expect(stderr).not.toMatch("Cannot find name 'stream'"); + expect(stdout).toMatch(`done`); +}); From d6fefb769302c0b580b5f41cc90e5c61cfb21ded Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Oct 2021 20:09:24 -0400 Subject: [PATCH 07/26] Amending #1496: Redirect source-map-support to @cspotcode/source-map-support (#1497) * Update package.json * update package-lock.json --- package-lock.json | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43292e8a8..8e2e732dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -317,8 +317,9 @@ "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" }, "@cspotcode/source-map-support": { - "version": "github:cspotcode/node-source-map-support#63248d0f2b65074dd0fe936ae6f194c1b52b1f74", - "from": "github:cspotcode/node-source-map-support", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "requires": { "@cspotcode/source-map-consumer": "0.8.0" } diff --git a/package.json b/package.json index 8ed02f65f..277cb7b68 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ } }, "dependencies": { - "@cspotcode/source-map-support": "github:cspotcode/node-source-map-support", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", From 45524ae96c92592e50b65e01f20c4354a7f85f34 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Oct 2021 22:40:58 -0400 Subject: [PATCH 08/26] Docs -> main for 10.3.0 release (#1502) * upgrade typedoc (#1454) * update esm messaging in docs (#1455) * Update vscode debug config (#1466) * changed all references to ts-node consistent (#1481) * changed all references to ts-node consistent * Pesky maintainer-prerogative nitpicks Co-authored-by: Andrew Bradley Co-authored-by: Anima <18208134+animafps@users.noreply.github.com> --- .github/workflows/website.yml | 3 +-- package.json | 4 ++++ website/docs/configuration.md | 14 ++++++------ website/docs/how-it-works.md | 4 ++-- website/docs/imports.md | 9 ++++---- website/docs/module-type-overrides.md | 4 ++-- website/docs/overview.md | 2 +- website/docs/paths.md | 6 ++--- website/docs/performance.md | 4 ++-- website/docs/recipes/visual-studio-code.md | 26 ++++++++++++---------- website/docs/transpilers.md | 2 +- website/docs/troubleshooting.md | 8 +++---- website/docs/types.md | 2 +- website/docs/usage.md | 8 +++---- website/readme-sources/prefix.md | 6 +---- 15 files changed, 51 insertions(+), 51 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 6e907f718..940088257 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -17,8 +17,7 @@ jobs: with: node-version: 14 # Render typedoc - # Using custom branch to workaround: https://github.com/TypeStrong/typedoc/issues/1585 - - run: npm install && git clone --depth 1 https://github.com/cspotcode/typedoc --branch patch-2 && pushd typedoc && npm install && npm run build || true && popd && ./typedoc/bin/typedoc + - run: npm install && npx typedoc # Render docusaurus and deploy website - run: | set -euo pipefail diff --git a/package.json b/package.json index 277cb7b68..705f6d2c0 100644 --- a/package.json +++ b/package.json @@ -167,5 +167,9 @@ }, "prettier": { "singleQuote": true + }, + "volta": { + "node": "16.9.1", + "npm": "6.14.15" } } diff --git a/website/docs/configuration.md b/website/docs/configuration.md index ad9998328..5a1d43bea 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -2,13 +2,13 @@ title: Configuration --- -`ts-node` supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. +ts-node supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. For a complete list, see [Options](./options.md). ## CLI flags -`ts-node` CLI flags must come *before* the entrypoint script. For example: +ts-node CLI flags must come *before* the entrypoint script. For example: ```shell $ ts-node --project tsconfig-dev.json say-hello.ts Ronald @@ -17,7 +17,7 @@ Hello, Ronald! ## Via tsconfig.json (recommended) -`ts-node` automatically finds and loads `tsconfig.json`. Most `ts-node` options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. +ts-node automatically finds and loads `tsconfig.json`. Most ts-node options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. Use `--skip-project` to skip loading the `tsconfig.json`. Use `--project` to explicitly specify the path to a `tsconfig.json`. @@ -55,7 +55,7 @@ Our bundled [JSON schema](https://unpkg.com/browse/ts-node@latest/tsconfig.schem ### @tsconfig/bases [@tsconfig/bases](https://github.com/tsconfig/bases) maintains recommended configurations for several node versions. -As a convenience, these are bundled with `ts-node`. +As a convenience, these are bundled with ts-node. ```json title="tsconfig.json" { @@ -68,7 +68,7 @@ As a convenience, these are bundled with `ts-node`. ### Default config -If no `tsconfig.json` is loaded from disk, `ts-node` will use the newest recommended defaults from +If no `tsconfig.json` is loaded from disk, ts-node will use the newest recommended defaults from [@tsconfig/bases](https://github.com/tsconfig/bases/) compatible with your `node` and `typescript` versions. With the latest `node` and `typescript`, this is [`@tsconfig/node16`](https://github.com/tsconfig/bases/blob/master/bases/node16.json). @@ -78,7 +78,7 @@ When in doubt, `ts-node --show-config` will log the configuration being used, an ## `node` flags -[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the `ts-node` binary nor can they be specified in `tsconfig.json` +[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the ts-node binary nor can they be specified in `tsconfig.json` We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node_options_options) environment variable to pass options to `node`. @@ -86,7 +86,7 @@ We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node NODE_OPTIONS='--trace-deprecation --abort-on-uncaught-exception' ts-node ./index.ts ``` -Alternatively, you can invoke `node` directly and install `ts-node` via `--require`/`-r` +Alternatively, you can invoke `node` directly and install ts-node via `--require`/`-r` ```shell node --trace-deprecation --abort-on-uncaught-exception -r ts-node/register ./index.ts diff --git a/website/docs/how-it-works.md b/website/docs/how-it-works.md index 28dec08aa..923c0592c 100644 --- a/website/docs/how-it-works.md +++ b/website/docs/how-it-works.md @@ -2,7 +2,7 @@ title: How It Works --- -`ts-node` works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. +ts-node works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook runs in the middle, transforming code from TypeScript to JavaScript and passing the result to `node` for execution. This transformation will respect your `tsconfig.json` as if you had compiled via `tsc`. @@ -12,7 +12,7 @@ Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook > **Warning:** if a file is ignored or its file extension is not registered, node will either fail to resolve the file or will attempt to execute it as JavaScript without any transformation. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. -> **Warning:** When `ts-node` is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. +> **Warning:** When ts-node is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. ## Skipping `node_modules` diff --git a/website/docs/imports.md b/website/docs/imports.md index 9f759d207..6b04f776f 100644 --- a/website/docs/imports.md +++ b/website/docs/imports.md @@ -2,7 +2,7 @@ title: "CommonJS vs native ECMAScript modules" --- -TypeScript is almost always written using modern `import` syntax, but you can choose to either transform to CommonJS or use node's native ESM support. Configuration is different for each. +TypeScript is almost always written using modern `import` syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native `import` syntax, using node's native ESM support. Configuration is different for each. Here is a brief comparison of the two. @@ -11,7 +11,7 @@ Here is a brief comparison of the two. | Write native `import` syntax | Write native `import` syntax | | Transforms `import` into `require()` | Does not transform `import` | | Node executes scripts using the classic [CommonJS loader](https://nodejs.org/dist/latest-v16.x/docs/api/modules.html) | Node executes scripts using the new [ESM loader](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html) | -| Use any of:
`ts-node` CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | +| Use any of:
ts-node CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | ## CommonJS @@ -32,7 +32,7 @@ Transforming to CommonJS is typically simpler and more widely supported because } ``` -If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for `ts-node`. +If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for ts-node. ```json title="tsconfig.json" { @@ -49,8 +49,7 @@ If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, ## Native ECMAScript modules -[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. `ts-node`'s ESM support is also experimental. They may have -breaking changes in minor and patch releases and are not recommended for production. +[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. ts-node's ESM support is as stable as possible, but it relies on APIs which node can *and will* break in new versions of node. Thus it is not recommended for production. For complete usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). diff --git a/website/docs/module-type-overrides.md b/website/docs/module-type-overrides.md index 1656d9629..43ab19f55 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -2,7 +2,7 @@ title: Module type overrides --- -When deciding between CommonJS and native ECMAScript modules, `ts-node` defaults to matching vanilla `node` and `tsc` +When deciding between CommonJS and native ECMAScript modules, ts-node defaults to matching vanilla `node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` option and executed according to node's rules for the `package.json` `"type"` field. @@ -15,7 +15,7 @@ In these situations, our `moduleTypes` option lets you override certain files, f CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file extensions, but `.ts` files cannot use them. `moduleTypes` achieves the same effect, and *also* overrides your `tsconfig.json` `"module"` config appropriately. -The following example tells `ts-node` to execute a webpack config as CommonJS: +The following example tells ts-node to execute a webpack config as CommonJS: ```json title=tsconfig.json { diff --git a/website/docs/overview.md b/website/docs/overview.md index 9835306f9..4afde82d2 100644 --- a/website/docs/overview.md +++ b/website/docs/overview.md @@ -3,7 +3,7 @@ title: Overview slug: / --- -`ts-node` is a TypeScript execution engine and REPL for Node.js. +ts-node is a TypeScript execution engine and REPL for Node.js. It JIT transforms TypeScript into JavaScript, enabling you to directly execute TypeScript on Node.js without precompiling. This is accomplished by hooking node's module loading APIs, enabling it to be used seamlessly alongside other Node.js diff --git a/website/docs/paths.md b/website/docs/paths.md index 6bc60fecc..f514e8043 100644 --- a/website/docs/paths.md +++ b/website/docs/paths.md @@ -3,7 +3,7 @@ title: | paths and baseUrl --- -You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. +You can use ts-node together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. ```json title="tsconfig.json" { @@ -14,7 +14,7 @@ You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/packa } ``` -## Why is this not built-in to `ts-node`? +## Why is this not built-in to ts-node? The official TypeScript Handbook explains the intended purpose for `"paths"` in ["Additional module resolution flags"](https://www.typescriptlang.org/docs/handbook/module-resolution.html#additional-module-resolution-flags). @@ -23,4 +23,4 @@ The official TypeScript Handbook explains the intended purpose for `"paths"` in > It is important to note that the compiler will not perform any of these transformations; it just uses these pieces of information to guide the process of resolving a module import to its definition file. This means `"paths"` are intended to describe mappings that the build tool or runtime *already* performs, not to tell the build tool or -runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, `ts-node` does not modify `node`'s module resolution behavior to implement `"paths"` mappings. +runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, ts-node does not modify `node`'s module resolution behavior to implement `"paths"` mappings. diff --git a/website/docs/performance.md b/website/docs/performance.md index 625e1cd96..a875bd2eb 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -2,11 +2,11 @@ title: Make it fast --- -These tricks will make `ts-node` faster. +These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, `ts-node` can skip typechecking. +It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. * Enable [`transpileOnly`](./options.md) to skip typechecking * Use our [`swc` integration](./transpilers.md#bundled-swc-integration) diff --git a/website/docs/recipes/visual-studio-code.md b/website/docs/recipes/visual-studio-code.md index 1d8d44b7f..e3327c03b 100644 --- a/website/docs/recipes/visual-studio-code.md +++ b/website/docs/recipes/visual-studio-code.md @@ -2,20 +2,22 @@ title: Visual Studio Code --- -Create a new node.js configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). +Create a new Node.js debug configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). -```json +```json title=".vscode/launch.json" { - "type": "node", - "request": "launch", - "name": "Launch Program", - "runtimeArgs": [ - "-r", - "ts-node/register" - ], - "args": [ - "${workspaceFolder}/index.ts" - ] + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Launch Program", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "${workspaceFolder}/src/index.ts" + ] + }], } ``` diff --git a/website/docs/transpilers.md b/website/docs/transpilers.md index 2df5fa2e8..6343a61ef 100644 --- a/website/docs/transpilers.md +++ b/website/docs/transpilers.md @@ -4,7 +4,7 @@ title: Third-party transpilers In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a third-party transpiler to transform TypeScript into JavaScript even faster. You will still benefit from -`ts-node`'s automatic `tsconfig.json` discovery, sourcemap support, and global `ts-node` CLI. Integrations +ts-node's automatic `tsconfig.json` discovery, sourcemap support, and global ts-node CLI. Integrations can automatically derive an appropriate configuration from your existing `tsconfig.json` which simplifies project boilerplate. diff --git a/website/docs/troubleshooting.md b/website/docs/troubleshooting.md index 4ddac6910..2ee75225d 100644 --- a/website/docs/troubleshooting.md +++ b/website/docs/troubleshooting.md @@ -4,11 +4,11 @@ title: Troubleshooting ## Understanding configuration -`ts-node` uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you +ts-node uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you have one. If you are unsure which configuration is used, you can log it with `ts-node --show-config`. This is similar to `tsc --showConfig` but includes `"ts-node"` options as well. -`ts-node` also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed +ts-node also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed `typescript`. If you are unsure which versions are used, `ts-node -vv` will log them. ```shell @@ -54,7 +54,7 @@ $ ts-node --show-config ## Understanding Errors -It is important to differentiate between errors from `ts-node`, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. +It is important to differentiate between errors from ts-node, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. ### `TSError` @@ -62,7 +62,7 @@ Type errors from the compiler are thrown as a `TSError`. These are the same as ### `SyntaxError` -Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or `ts-node`. These are bugs in your code or configuration. +Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or ts-node. These are bugs in your code or configuration. #### Unsupported JavaScript syntax diff --git a/website/docs/types.md b/website/docs/types.md index 168c5c6a7..eaf425a93 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -2,7 +2,7 @@ title: "Help! My Types Are Missing!" --- -**TypeScript Node** does _not_ use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, `ts-node` starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. +ts-node does _not_ use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). diff --git a/website/docs/usage.md b/website/docs/usage.md index 76048706c..ecd6721bc 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -42,14 +42,14 @@ Passing CLI arguments via shebang is allowed on Mac but not Linux. For example, // This shebang is not portable. It only works on Mac ``` -Instead, specify all `ts-node` options in your `tsconfig.json`. +Instead, specify all ts-node options in your `tsconfig.json`. ## Programmatic -You can require `ts-node` and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. +You can require ts-node and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. -**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of the `ts-node` CLI. +**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of ts-node's CLI. ### Developers -`ts-node` exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. +ts-node exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. diff --git a/website/readme-sources/prefix.md b/website/readme-sources/prefix.md index 2b5fe41ab..a24738ff9 100644 --- a/website/readme-sources/prefix.md +++ b/website/readme-sources/prefix.md @@ -21,12 +21,8 @@ You can build the readme with this command: [![Build status](https://img.shields.io/github/workflow/status/TypeStrong/ts-node/Continuous%20Integration)](https://github.com/TypeStrong/ts-node/actions?query=workflow%3A%22Continuous+Integration%22) [![Test coverage](https://codecov.io/gh/TypeStrong/ts-node/branch/main/graph/badge.svg)](https://codecov.io/gh/TypeStrong/ts-node) -> TypeScript execution and REPL for node.js, with source map support. **Works with `typescript@>=2.7`**. +> TypeScript execution and REPL for node.js, with source map and native ESM support. The latest documentation can also be found on our website: [https://typestrong.org/ts-node](https://typestrong.org/ts-node) -### *Experimental ESM support* - -Native ESM support is currently experimental. For usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). - # Table of Contents From 3ca5df830130b1988a64b263499eda4492c5df8e Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Oct 2021 22:43:19 -0400 Subject: [PATCH 09/26] update the readme --- README.md | 97 +++++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 6a550beff..76d096bfd 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,10 @@ You can build the readme with this command: [![Build status](https://img.shields.io/github/workflow/status/TypeStrong/ts-node/Continuous%20Integration)](https://github.com/TypeStrong/ts-node/actions?query=workflow%3A%22Continuous+Integration%22) [![Test coverage](https://codecov.io/gh/TypeStrong/ts-node/branch/main/graph/badge.svg)](https://codecov.io/gh/TypeStrong/ts-node) -> TypeScript execution and REPL for node.js, with source map support. **Works with `typescript@>=2.7`**. +> TypeScript execution and REPL for node.js, with source map and native ESM support. The latest documentation can also be found on our website: -### *Experimental ESM support* - -Native ESM support is currently experimental. For usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). - # Table of Contents * [Overview](#overview) @@ -66,7 +62,7 @@ Native ESM support is currently experimental. For usage, limitations, and to pro * [Skipping `node_modules`](#skipping-node_modules) * [paths and baseUrl ](#paths-and-baseurl) - * [Why is this not built-in to `ts-node`?](#why-is-this-not-built-in-to-ts-node) + * [Why is this not built-in to ts-node?](#why-is-this-not-built-in-to-ts-node) * [Help! My Types Are Missing!](#help-my-types-are-missing) * [Third-party compilers](#third-party-compilers) * [Third-party transpilers](#third-party-transpilers) @@ -91,7 +87,7 @@ Native ESM support is currently experimental. For usage, limitations, and to pro # Overview -`ts-node` is a TypeScript execution engine and REPL for Node.js. +ts-node is a TypeScript execution engine and REPL for Node.js. It JIT transforms TypeScript into JavaScript, enabling you to directly execute TypeScript on Node.js without precompiling. This is accomplished by hooking node's module loading APIs, enabling it to be used seamlessly alongside other Node.js @@ -170,27 +166,27 @@ Passing CLI arguments via shebang is allowed on Mac but not Linux. For example, #!/usr/bin/env ts-node --files // This shebang is not portable. It only works on Mac -Instead, specify all `ts-node` options in your `tsconfig.json`. +Instead, specify all ts-node options in your `tsconfig.json`. ## Programmatic -You can require `ts-node` and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. +You can require ts-node and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. -**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of the `ts-node` CLI. +**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of ts-node's CLI. ### Developers -`ts-node` exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. +ts-node exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. # Configuration -`ts-node` supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. +ts-node supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. For a complete list, see [Options](#options). ## CLI flags -`ts-node` CLI flags must come *before* the entrypoint script. For example: +ts-node CLI flags must come *before* the entrypoint script. For example: ```shell $ ts-node --project tsconfig-dev.json say-hello.ts Ronald @@ -199,7 +195,7 @@ Hello, Ronald! ## Via tsconfig.json (recommended) -`ts-node` automatically finds and loads `tsconfig.json`. Most `ts-node` options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. +ts-node automatically finds and loads `tsconfig.json`. Most ts-node options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. Use `--skip-project` to skip loading the `tsconfig.json`. Use `--project` to explicitly specify the path to a `tsconfig.json`. @@ -237,7 +233,7 @@ Our bundled [JSON schema](https://unpkg.com/browse/ts-node@latest/tsconfig.schem ### @tsconfig/bases [@tsconfig/bases](https://github.com/tsconfig/bases) maintains recommended configurations for several node versions. -As a convenience, these are bundled with `ts-node`. +As a convenience, these are bundled with ts-node. ```jsonc title="tsconfig.json" { @@ -250,7 +246,7 @@ As a convenience, these are bundled with `ts-node`. ### Default config -If no `tsconfig.json` is loaded from disk, `ts-node` will use the newest recommended defaults from +If no `tsconfig.json` is loaded from disk, ts-node will use the newest recommended defaults from [@tsconfig/bases](https://github.com/tsconfig/bases/) compatible with your `node` and `typescript` versions. With the latest `node` and `typescript`, this is [`@tsconfig/node16`](https://github.com/tsconfig/bases/blob/master/bases/node16.json). @@ -260,7 +256,7 @@ When in doubt, `ts-node --show-config` will log the configuration being used, an ## `node` flags -[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the `ts-node` binary nor can they be specified in `tsconfig.json` +[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the ts-node binary nor can they be specified in `tsconfig.json` We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node_options_options) environment variable to pass options to `node`. @@ -268,7 +264,7 @@ We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node NODE_OPTIONS='--trace-deprecation --abort-on-uncaught-exception' ts-node ./index.ts ``` -Alternatively, you can invoke `node` directly and install `ts-node` via `--require`/`-r` +Alternatively, you can invoke `node` directly and install ts-node via `--require`/`-r` ```shell node --trace-deprecation --abort-on-uncaught-exception -r ts-node/register ./index.ts @@ -335,7 +331,7 @@ The API includes [additional options](https://typestrong.org/ts-node/api/interfa # CommonJS vs native ECMAScript modules -TypeScript is almost always written using modern `import` syntax, but you can choose to either transform to CommonJS or use node's native ESM support. Configuration is different for each. +TypeScript is almost always written using modern `import` syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native `import` syntax, using node's native ESM support. Configuration is different for each. Here is a brief comparison of the two. @@ -344,7 +340,7 @@ Here is a brief comparison of the two. | Write native `import` syntax | Write native `import` syntax | | Transforms `import` into `require()` | Does not transform `import` | | Node executes scripts using the classic [CommonJS loader](https://nodejs.org/dist/latest-v16.x/docs/api/modules.html) | Node executes scripts using the new [ESM loader](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html) | -| Use any of:
`ts-node` CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | +| Use any of:
ts-node CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | ## CommonJS @@ -365,7 +361,7 @@ Transforming to CommonJS is typically simpler and more widely supported because } ``` -If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for `ts-node`. +If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for ts-node. ```jsonc title="tsconfig.json" { @@ -382,8 +378,7 @@ If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, ## Native ECMAScript modules -[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. `ts-node`'s ESM support is also experimental. They may have -breaking changes in minor and patch releases and are not recommended for production. +[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. ts-node's ESM support is as stable as possible, but it relies on APIs which node can *and will* break in new versions of node. Thus it is not recommended for production. For complete usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). @@ -407,11 +402,11 @@ You must set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_ ## Understanding configuration -`ts-node` uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you +ts-node uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you have one. If you are unsure which configuration is used, you can log it with `ts-node --show-config`. This is similar to `tsc --showConfig` but includes `"ts-node"` options as well. -`ts-node` also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed +ts-node also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed `typescript`. If you are unsure which versions are used, `ts-node -vv` will log them. ```shell @@ -457,7 +452,7 @@ $ ts-node --show-config ## Understanding Errors -It is important to differentiate between errors from `ts-node`, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. +It is important to differentiate between errors from ts-node, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. ### `TSError` @@ -465,7 +460,7 @@ Type errors from the compiler are thrown as a `TSError`. These are the same as ### `SyntaxError` -Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or `ts-node`. These are bugs in your code or configuration. +Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or ts-node. These are bugs in your code or configuration. #### Unsupported JavaScript syntax @@ -488,11 +483,11 @@ When you try to run this code, node 12 will throw a `SyntaxError`. To fix this, # Make it fast -These tricks will make `ts-node` faster. +These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, `ts-node` can skip typechecking. +It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. * Enable [`transpileOnly`](#options) to skip typechecking * Use our [`swc` integration](#bundled-swc-integration) @@ -510,7 +505,7 @@ It is often better to use `tsc --noEmit` to typecheck once before your tests run ## How It Works -`ts-node` works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. +ts-node works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook runs in the middle, transforming code from TypeScript to JavaScript and passing the result to `node` for execution. This transformation will respect your `tsconfig.json` as if you had compiled via `tsc`. @@ -520,7 +515,7 @@ Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook > **Warning:** if a file is ignored or its file extension is not registered, node will either fail to resolve the file or will attempt to execute it as JavaScript without any transformation. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. -> **Warning:** When `ts-node` is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. +> **Warning:** When ts-node is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. ### Skipping `node_modules` @@ -532,7 +527,7 @@ By default, **TypeScript Node** avoids compiling files in `/node_modules/` for t ## paths and baseUrl -You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. +You can use ts-node together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. ```jsonc title="tsconfig.json" { @@ -543,7 +538,7 @@ You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/packa } ``` -### Why is this not built-in to `ts-node`? +### Why is this not built-in to ts-node? The official TypeScript Handbook explains the intended purpose for `"paths"` in ["Additional module resolution flags"](https://www.typescriptlang.org/docs/handbook/module-resolution.html#additional-module-resolution-flags). @@ -552,11 +547,11 @@ The official TypeScript Handbook explains the intended purpose for `"paths"` in > It is important to note that the compiler will not perform any of these transformations; it just uses these pieces of information to guide the process of resolving a module import to its definition file. This means `"paths"` are intended to describe mappings that the build tool or runtime *already* performs, not to tell the build tool or -runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, `ts-node` does not modify `node`'s module resolution behavior to implement `"paths"` mappings. +runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, ts-node does not modify `node`'s module resolution behavior to implement `"paths"` mappings. ## Help! My Types Are Missing! -**TypeScript Node** does *not* use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, `ts-node` starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. +ts-node does *not* use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). @@ -637,7 +632,7 @@ For example, to use `ttypescript` and `ts-transformer-keys`, add this to your `t In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a third-party transpiler to transform TypeScript into JavaScript even faster. You will still benefit from -`ts-node`'s automatic `tsconfig.json` discovery, sourcemap support, and global `ts-node` CLI. Integrations +ts-node's automatic `tsconfig.json` discovery, sourcemap support, and global ts-node CLI. Integrations can automatically derive an appropriate configuration from your existing `tsconfig.json` which simplifies project boilerplate. @@ -681,7 +676,7 @@ Integrations are `require()`d, so they can be published to npm. The module must ## Module type overrides -When deciding between CommonJS and native ECMAScript modules, `ts-node` defaults to matching vanilla `node` and `tsc` +When deciding between CommonJS and native ECMAScript modules, ts-node defaults to matching vanilla `node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` option and executed according to node's rules for the `package.json` `"type"` field. @@ -694,7 +689,7 @@ In these situations, our `moduleTypes` option lets you override certain files, f CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file extensions, but `.ts` files cannot use them. `moduleTypes` achieves the same effect, and *also* overrides your `tsconfig.json` `"module"` config appropriately. -The following example tells `ts-node` to execute a webpack config as CommonJS: +The following example tells ts-node to execute a webpack config as CommonJS: ```jsonc title=tsconfig.json { @@ -836,20 +831,22 @@ ts-node node_modules/tape/bin/tape [...args] ## Visual Studio Code -Create a new node.js configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). +Create a new Node.js debug configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). -```jsonc +```jsonc title=".vscode/launch.json" { - "type": "node", - "request": "launch", - "name": "Launch Program", - "runtimeArgs": [ - "-r", - "ts-node/register" - ], - "args": [ - "${workspaceFolder}/index.ts" - ] + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Launch Program", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "${workspaceFolder}/src/index.ts" + ] + }], } ``` From aa19440d3fc37ac12373a77ab66540363ab90917 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Oct 2021 22:50:16 -0400 Subject: [PATCH 10/26] update api-extractor report for 10.3.0 --- api-extractor/ts-node.api.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index 8801e708d..2849b88a2 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -10,6 +10,11 @@ import type * as _ts from 'typescript'; // @public export function create(rawOptions?: CreateOptions): Service; +// Warning: (ae-forgotten-export) The symbol "createEsmHooks" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const createEsmHooks: typeof createEsmHooks_2; + // @public export interface CreateOptions { compiler?: string; @@ -79,6 +84,9 @@ export type Register = Service; // @public export function register(opts?: RegisterOptions): Service; +// @public +export function register(service: Service): Service; + // @public export const REGISTER_INSTANCE: unique symbol; @@ -213,7 +221,7 @@ export interface TSCommon { } // @public -export interface TsConfigOptions extends Omit { +export interface TsConfigOptions extends Omit { } // @public From c671a329d5e3dc2e9ebe97a74693d15406d461fe Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 11 Oct 2021 13:54:05 -0400 Subject: [PATCH 11/26] add template for release notes --- development-docs/release-template.md | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 development-docs/release-template.md diff --git a/development-docs/release-template.md b/development-docs/release-template.md new file mode 100644 index 000000000..e1f32c582 --- /dev/null +++ b/development-docs/release-template.md @@ -0,0 +1,56 @@ +## Template to be copy-pasted as a template for new release notes. + +--- + + +Questions about this release? Ask in the official discussion thread: #TODO + + + +*Breaking changes are prefixed with **[BREAKING]*** + +**Added** + +- Adds description of thing added (#Issue, #PR, #etc) @contributor-name + + + - Optionally add details ([docs](link to docs)) + - Or multiple docs links ([CLI docs](link to docusaurus page), [API docs](link to typedoc page)) + +**Changed** + +- **[BREAKING]** Make yadda yadda... + +**Deprecated** + +**Removed** + +**Fixed** + +- Fix #TODO: Description of fix (#Issue, #PR, #etc) + +**Docs** + +- In the past I've documented major improvements to docs, new docsite, new docs sections about confusing bits +- In general should avoid changelog entries that do not affect ts-node consumers + +**Misc** + +- In the past I've documented improvements to testing, codecov, etc. +- In general should avoid changelog entries that do not affect ts-node consumers + +https://github.com/TypeStrong/ts-node/compare/vTODO...vTODO +https://github.com/TypeStrong/ts-node/milestone/TODO + +--- + +## Discussion thread template + +--- + +Discussion thread for the vTODO release. + +[Release notes](https://github.com/TypeStrong/ts-node/releases/tag/vTODO) From 1cc91d9e2cdc9ad6676296f6d3e2d190e592652b Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 11 Oct 2021 14:02:56 -0400 Subject: [PATCH 12/26] 10.3.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e2e732dd..32fe6e233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.2.1", + "version": "10.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 705f6d2c0..22aea1e52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.2.1", + "version": "10.3.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { From 3b403651193c0ce942b215c44f6db805b4218f4c Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 18 Oct 2021 00:49:17 -0400 Subject: [PATCH 13/26] Add info about tests to CONTRIBUTING.md (#1520) --- CONTRIBUTING.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1f0363f9..2131146ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,32 @@ compiled code in `dist`. `dist-raw` is for larger chunks of code which are not compiled nor linted because they have been copy-pasted from `node`'s source code. +## Tests + +Test cases are declared in `src/test/*.spec.ts`, and test fixtures live in `./tests`. They can be run with `npm run test-local`. + +Tests are run with AVA, but using a custom wrapper API to enable some TS-friendly features and grouped test suites. + +The tests `npm pack` ts-node into a tarball and `npm install` it into `./tests/node_modules`. This makes `./tests` a better testing environment +because it more closely matches the end-user's environment. Complex `require()` / `import` / `--require` / `--loader` invocations behave +the way they would in a users's project. + +Historically, it has been difficult to test ts-node in-process because it mutates the node environment: installing require hooks, stack trace formatters, etc. +`nyc`, `ava`, and `ts-node` all mutate the node environment, so it is tricky to setup and teardown individual tests in isolation, because ts-node's hooks need to be +reset without disturbing `nyc` or `ava` hooks. For this reason, many tests are integration style, spawning ts-node's CLI in an external process, asking it to +execute one of the fixture projects in `./tests`. + +Over time, I've gradually added setup and teardown logic so that more components can be tested in-process. + +We have a debug configuration for VSCode. + +1. Open a `*.spec.ts` so it is the active/focused file. +2. (optional) set breakpoints. +3. Invoke debugger with F5. + +Note that some tests might misbehave in the debugger. REPL tests in particular. I'm not sure why, but I think it is related to how `console` does not write to +stdout when in a debug session. + ## Documentation Documentation is written in markdown in `website/docs` and rendered into a website by Docusaurus. The README is also generated from these markdown files. From b16134b91f5d3e3be41563508f43065bafa75d15 Mon Sep 17 00:00:00 2001 From: Shrujal Shah Date: Thu, 21 Oct 2021 10:29:42 +0530 Subject: [PATCH 14/26] update version checks for v16 (#1522) * update version checks for v16 Ref: https://github.com/nodejs/node/pull/40504 * add test to check old ESM Loader Hooks API on v16.11.1 * fix wrong version number --- .github/workflows/continuous-integration.yml | 15 +++++++++++---- src/esm.ts | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index bc954cd9e..4bd5898c5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -48,7 +48,7 @@ jobs: matrix: os: [ubuntu, windows] # Don't forget to add all new flavors to this list! - flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] include: # Node 12.15 # TODO Add comments about why we test 12.15; I think git blame says it's because of an ESM behavioral change that happened at 12.16 @@ -94,26 +94,33 @@ jobs: typescript: next typescriptFlag: next # Node 16 + # Node 16.11.1 + # Earliest version that supports old ESM Loader Hooks API: https://github.com/TypeStrong/ts-node/pull/1522 - flavor: 8 + node: 16.11.1 + nodeFlag: 16_11_1 + typescript: latest + typescriptFlag: latest + - flavor: 9 node: 16 nodeFlag: 16 typescript: latest typescriptFlag: latest downgradeNpm: true - - flavor: 9 + - flavor: 10 node: 16 nodeFlag: 16 typescript: 2.7 typescriptFlag: 2_7 downgradeNpm: true - - flavor: 10 + - flavor: 11 node: 16 nodeFlag: 16 typescript: next typescriptFlag: next downgradeNpm: true # Node nightly - - flavor: 11 + - flavor: 12 node: nightly nodeFlag: nightly typescript: latest diff --git a/src/esm.ts b/src/esm.ts index 5502d0155..35bca748a 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -54,10 +54,10 @@ export function createEsmHooks(tsNodeService: Service) { }); // The hooks API changed in node version X so we need to check for backwards compatibility. - // TODO: When the new API is backported to v12, v14, v16, update these version checks accordingly. + // TODO: When the new API is backported to v12, v14, update these version checks accordingly. const newHooksAPI = versionGteLt(process.versions.node, '17.0.0') || - versionGteLt(process.versions.node, '16.999.999', '17.0.0') || + versionGteLt(process.versions.node, '16.12.0', '17.0.0') || versionGteLt(process.versions.node, '14.999.999', '15.0.0') || versionGteLt(process.versions.node, '12.999.999', '13.0.0'); From 8cde25b51ea0b38f673ed51fc75e1af1c584b412 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 11:52:26 -0400 Subject: [PATCH 15/26] 10.3.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32fe6e233..e913799d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.3.0", + "version": "10.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 22aea1e52..a249b3497 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.3.0", + "version": "10.3.1", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { From 56b70da9d0f776fd70e58bd560006aeb54e66032 Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Fri, 22 Oct 2021 06:23:23 +0200 Subject: [PATCH 16/26] add Target ES2021 mapping in transpilers/swc (#1521) * add Target ES2021 mapping in transpilers/swc I was getting error: `jsc.target should be es5 or upper to use getter / setter` and after investigation I've isolated issue and here is the "fix" * fix and add tests * fix * lintfix Co-authored-by: Andrew Bradley --- src/test/transpilers.spec.ts | 47 ++++++++++++++++++++++++++++++++++++ src/transpilers/swc.ts | 40 +++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 src/test/transpilers.spec.ts diff --git a/src/test/transpilers.spec.ts b/src/test/transpilers.spec.ts new file mode 100644 index 000000000..f1d30172a --- /dev/null +++ b/src/test/transpilers.spec.ts @@ -0,0 +1,47 @@ +// third-party transpiler and swc transpiler tests +// TODO: at the time of writing, other transpiler tests have not been moved into this file. +// Should consolidate them here. + +import { context } from './testlib'; +import { contextTsNodeUnderTest, testsDirRequire } from './helpers'; +import * as expect from 'expect'; + +const test = context(contextTsNodeUnderTest); + +test.suite('swc', (test) => { + test('verify that TS->SWC target mappings suppport all possible values from both TS and SWC', async (t) => { + const swcTranspiler = testsDirRequire( + 'ts-node/transpilers/swc-experimental' + ) as typeof import('../transpilers/swc'); + + // Detect when mapping is missing any ts.ScriptTargets + const ts = testsDirRequire('typescript') as typeof import('typescript'); + for (const key of Object.keys(ts.ScriptTarget)) { + if (/^\d+$/.test(key)) continue; + if (key === 'JSON') continue; + expect( + swcTranspiler.targetMapping.has(ts.ScriptTarget[key as any] as any) + ).toBe(true); + } + + // Detect when mapping is missing any swc targets + // Assuming that tests/package.json declares @swc/core: latest + const swc = testsDirRequire('@swc/core'); + let msg: string | undefined = undefined; + try { + swc.transformSync('', { jsc: { target: 'invalid' } }); + } catch (e) { + msg = (e as Error).message; + } + expect(msg).toBeDefined(); + // Error looks like: + // unknown variant `invalid`, expected one of `es3`, `es5`, `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020`, `es2021` at line 1 column 28 + const match = msg!.match(/unknown variant.*, expected one of (.*) at line/); + expect(match).toBeDefined(); + const targets = match![1].split(', ').map((v: string) => v.slice(1, -1)); + + for (const target of targets) { + expect([...swcTranspiler.targetMapping.values()]).toContain(target); + } + }); +}); diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 9b7361a08..6955bc90e 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -56,7 +56,20 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { const nonTsxOptions = createSwcOptions(false); const tsxOptions = createSwcOptions(true); function createSwcOptions(isTsx: boolean): swcTypes.Options { - const swcTarget = targetMapping.get(target!) ?? 'es3'; + let swcTarget = targetMapping.get(target!) ?? 'es3'; + // Downgrade to lower target if swc does not support the selected target. + // Perhaps project has an older version of swc. + // TODO cache the results of this; slightly faster + let swcTargetIndex = swcTargets.indexOf(swcTarget); + for (; swcTargetIndex >= 0; swcTargetIndex--) { + try { + swcInstance.transformSync('', { + jsc: { target: swcTargets[swcTargetIndex] }, + }); + break; + } catch (e) {} + } + swcTarget = swcTargets[swcTargetIndex]; const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; const moduleType = module === ModuleKind.CommonJS @@ -119,7 +132,8 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { }; } -const targetMapping = new Map(); +/** @internal */ +export const targetMapping = new Map(); targetMapping.set(/* ts.ScriptTarget.ES3 */ 0, 'es3'); targetMapping.set(/* ts.ScriptTarget.ES5 */ 1, 'es5'); targetMapping.set(/* ts.ScriptTarget.ES2015 */ 2, 'es2015'); @@ -127,8 +141,26 @@ targetMapping.set(/* ts.ScriptTarget.ES2016 */ 3, 'es2016'); targetMapping.set(/* ts.ScriptTarget.ES2017 */ 4, 'es2017'); targetMapping.set(/* ts.ScriptTarget.ES2018 */ 5, 'es2018'); targetMapping.set(/* ts.ScriptTarget.ES2019 */ 6, 'es2019'); -targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2019'); -targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2019'); +targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2020'); +targetMapping.set(/* ts.ScriptTarget.ES2021 */ 8, 'es2021'); +targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2021'); + +type SwcTarget = typeof swcTargets[number]; +/** + * @internal + * We use this list to downgrade to a prior target when we probe swc to detect if it supports a particular target + */ +const swcTargets = [ + 'es3', + 'es5', + 'es2015', + 'es2016', + 'es2017', + 'es2018', + 'es2019', + 'es2020', + 'es2021', +] as const; const ModuleKind = { None: 0, From 889c21d448157e3809e757e7fb91982ec1fd8cca Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 22 Oct 2021 00:24:13 -0400 Subject: [PATCH 17/26] Fix data URL format determination in new loader hooks API (#1529) * Add regression test * add missing primordial RegExpPrototypeExec * fixg * Improve createEsmHooks docs * Fix docs for esm hooks * lint-fix --- dist-raw/node-primordials.js | 1 + src/esm.ts | 68 +++++++++++++++++++++++++++++------- src/index.ts | 19 ++++++++-- src/test/esm-loader.spec.ts | 34 ++++++++++++++++++ 4 files changed, 108 insertions(+), 14 deletions(-) diff --git a/dist-raw/node-primordials.js b/dist-raw/node-primordials.js index ec8083460..ae3b8b911 100644 --- a/dist-raw/node-primordials.js +++ b/dist-raw/node-primordials.js @@ -16,6 +16,7 @@ module.exports = { ObjectGetOwnPropertyNames: Object.getOwnPropertyNames, ObjectDefineProperty: Object.defineProperty, ObjectPrototypeHasOwnProperty: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop), + RegExpPrototypeExec: (obj, string) => RegExp.prototype.exec.call(obj, string), RegExpPrototypeTest: (obj, string) => RegExp.prototype.test.call(obj, string), RegExpPrototypeSymbolReplace: (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest), SafeMap: Map, diff --git a/src/esm.ts b/src/esm.ts index 35bca748a..c83fd22c4 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -36,6 +36,53 @@ const { defaultGetFormat } = require('../dist-raw/node-esm-default-get-format'); // from node, build our implementation of the *new* API on top of it, and implement the *old* // hooks API as a shim to the *new* API. +export interface NodeLoaderHooksAPI1 { + resolve: NodeLoaderHooksAPI1.ResolveHook; + getFormat: NodeLoaderHooksAPI1.GetFormatHook; + transformSource: NodeLoaderHooksAPI1.TransformSourceHook; +} +export namespace NodeLoaderHooksAPI1 { + export type ResolveHook = NodeLoaderHooksAPI2.ResolveHook; + export type GetFormatHook = ( + url: string, + context: {}, + defaultGetFormat: GetFormatHook + ) => Promise<{ format: NodeLoaderHooksFormat }>; + export type TransformSourceHook = ( + source: string | Buffer, + context: { url: string; format: NodeLoaderHooksFormat }, + defaultTransformSource: NodeLoaderHooksAPI1.TransformSourceHook + ) => Promise<{ source: string | Buffer }>; +} + +export interface NodeLoaderHooksAPI2 { + resolve: NodeLoaderHooksAPI2.ResolveHook; + load: NodeLoaderHooksAPI2.LoadHook; +} +export namespace NodeLoaderHooksAPI2 { + export type ResolveHook = ( + specifier: string, + context: { parentURL: string }, + defaultResolve: ResolveHook + ) => Promise<{ url: string }>; + export type LoadHook = ( + url: string, + context: { format: NodeLoaderHooksFormat | null | undefined }, + defaultLoad: NodeLoaderHooksAPI2['load'] + ) => Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }>; +} + +export type NodeLoaderHooksFormat = + | 'builtin' + | 'commonjs' + | 'dynamic' + | 'json' + | 'module' + | 'wasm'; + /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` @@ -62,12 +109,7 @@ export function createEsmHooks(tsNodeService: Service) { versionGteLt(process.versions.node, '12.999.999', '13.0.0'); // Explicit return type to avoid TS's non-ideal inferred type - const hooksAPI: { - resolve: typeof resolve; - getFormat: typeof getFormat | undefined; - transformSource: typeof transformSource | undefined; - load: typeof load | undefined; - } = newHooksAPI + const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI ? { resolve, load, getFormat: undefined, transformSource: undefined } : { resolve, getFormat, transformSource, load: undefined }; return hooksAPI; @@ -117,9 +159,12 @@ export function createEsmHooks(tsNodeService: Service) { // `load` from new loader hook API (See description at the top of this file) async function load( url: string, - context: { format: Format | null | undefined }, + context: { format: NodeLoaderHooksFormat | null | undefined }, defaultLoad: typeof load - ): Promise<{ format: Format; source: string | Buffer | undefined }> { + ): Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }> { // If we get a format hint from resolve() on the context then use it // otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node const format = @@ -160,12 +205,11 @@ export function createEsmHooks(tsNodeService: Service) { return { format, source }; } - type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; async function getFormat( url: string, context: {}, defaultGetFormat: typeof getFormat - ): Promise<{ format: Format }> { + ): Promise<{ format: NodeLoaderHooksFormat }> { const defer = (overrideUrl: string = url) => defaultGetFormat(overrideUrl, context, defaultGetFormat); @@ -185,7 +229,7 @@ export function createEsmHooks(tsNodeService: Service) { // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js const ext = extname(nativePath); - let nodeSays: { format: Format }; + let nodeSays: { format: NodeLoaderHooksFormat }; if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { nodeSays = await defer(formatUrl(pathToFileURL(nativePath + '.js'))); } else { @@ -210,7 +254,7 @@ export function createEsmHooks(tsNodeService: Service) { async function transformSource( source: string | Buffer, - context: { url: string; format: Format }, + context: { url: string; format: NodeLoaderHooksFormat }, defaultTransformSource: typeof transformSource ): Promise<{ source: string | Buffer }> { if (source === null || source === undefined) { diff --git a/src/index.ts b/src/index.ts index 15fbb0ad0..977922ea5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { ModuleTypeClassifier, } from './module-type-classifier'; import { createResolverFunctions } from './resolver-functions'; +import type { createEsmHooks as createEsmHooksFn } from './esm'; export { TSCommon }; export { @@ -39,6 +40,11 @@ export type { TranspileOptions, Transpiler, } from './transpilers/types'; +export type { + NodeLoaderHooksAPI1, + NodeLoaderHooksAPI2, + NodeLoaderHooksFormat, +} from './esm'; /** * Does this version of node obey the package.json "type" field @@ -1486,7 +1492,16 @@ function getTokenAtPosition( } } -import type { createEsmHooks as createEsmHooksFn } from './esm'; +/** + * Create an implementation of node's ESM loader hooks. + * + * This may be useful if you + * want to wrap or compose the loader hooks to add additional functionality or + * combine with another loader. + * + * Node changed the hooks API, so there are two possible APIs. This function + * detects your node version and returns the appropriate API. + */ export const createEsmHooks: typeof createEsmHooksFn = ( tsNodeService: Service -) => require('./esm').createEsmHooks(tsNodeService); +) => (require('./esm') as typeof import('./esm')).createEsmHooks(tsNodeService); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index e117e1ef8..6c3c1c51a 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -7,11 +7,15 @@ import semver = require('semver'); import { contextTsNodeUnderTest, EXPERIMENTAL_MODULES_FLAG, + resetNodeEnvironment, TEST_DIR, } from './helpers'; import { createExec } from './exec-helpers'; import { join } from 'path'; import * as expect from 'expect'; +import type { NodeLoaderHooksAPI2 } from '../'; + +const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); const test = context(contextTsNodeUnderTest); @@ -37,3 +41,33 @@ test.suite('createEsmHooks', (test) => { }); } }); + +test.suite('hooks', (_test) => { + const test = _test.context(async (t) => { + const service = t.context.tsNodeUnderTest.create({ + cwd: TEST_DIR, + }); + t.teardown(() => { + resetNodeEnvironment(); + }); + return { + service, + hooks: t.context.tsNodeUnderTest.createEsmHooks(service), + }; + }); + + if (nodeUsesNewHooksApi) { + test('Correctly determines format of data URIs', async (t) => { + const { hooks } = t.context; + const url = 'data:text/javascript,console.log("hello world");'; + const result = await (hooks as NodeLoaderHooksAPI2).load( + url, + { format: undefined }, + async (url, context, _ignored) => { + return { format: context.format!, source: '' }; + } + ); + expect(result.format).toBe('module'); + }); + } +}); From ccf8c8e11becb475c15d3579a2ab290c28808e90 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 22 Oct 2021 15:01:26 -0400 Subject: [PATCH 18/26] Docs merge for 10.3.2 (#1530) * Fix inconsistent reference to ts-node in docs * docs: regenerator-runtime dependency for swc (#1508) (#1509) * docs: regenerator-runtime dependency for swc (#1508) * docs(transpilers): update advice for installing regenerator-runtime * Update transpilers.md Co-authored-by: Andrew Bradley Co-authored-by: Peter Coulton --- website/docs/installation.md | 2 +- website/docs/transpilers.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/installation.md b/website/docs/installation.md index 4b22d54d3..541434b49 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -15,4 +15,4 @@ npm install -g ts-node npm install -D tslib @types/node ``` -**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. TS Node will always resolve the compiler from `cwd` before checking relative to its own installation. +**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. ts-node will always resolve the compiler from `cwd` before checking relative to its own installation. diff --git a/website/docs/transpilers.md b/website/docs/transpilers.md index 6343a61ef..eb80fd11f 100644 --- a/website/docs/transpilers.md +++ b/website/docs/transpilers.md @@ -20,10 +20,10 @@ We have bundled an experimental `swc` integration. [`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than `transpileModule`. -To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. +To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. ```shell -npm i -D @swc/core @swc/helpers +npm i -D @swc/core @swc/helpers regenerator-runtime ``` Then add the following to your `tsconfig.json`. From fe58f2c41c71d4bd3e1856b38ac1467b2be7e4d2 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 22 Oct 2021 15:02:44 -0400 Subject: [PATCH 19/26] Rebuild readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 76d096bfd..d55198566 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ npm install -g ts-node npm install -D tslib @types/node ``` -**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. TS Node will always resolve the compiler from `cwd` before checking relative to its own installation. +**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. ts-node will always resolve the compiler from `cwd` before checking relative to its own installation. # Usage @@ -648,10 +648,10 @@ We have bundled an experimental `swc` integration. [`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than `transpileModule`. -To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. +To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. ```shell -npm i -D @swc/core @swc/helpers +npm i -D @swc/core @swc/helpers regenerator-runtime ``` Then add the following to your `tsconfig.json`. From f556c751640bce58605e910025a6d0cb6b3017b6 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 22 Oct 2021 15:06:04 -0400 Subject: [PATCH 20/26] update api-extractor report --- api-extractor/ts-node.api.md | 57 +++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index 2849b88a2..6c6e8ee6d 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -12,7 +12,7 @@ export function create(rawOptions?: CreateOptions): Service; // Warning: (ae-forgotten-export) The symbol "createEsmHooks" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export const createEsmHooks: typeof createEsmHooks_2; // @public @@ -78,6 +78,61 @@ export interface CreateTranspilerOptions { // @public export type EvalAwarePartialHost = Pick; +// @public (undocumented) +export interface NodeLoaderHooksAPI1 { + // (undocumented) + getFormat: NodeLoaderHooksAPI1.GetFormatHook; + // (undocumented) + resolve: NodeLoaderHooksAPI1.ResolveHook; + // (undocumented) + transformSource: NodeLoaderHooksAPI1.TransformSourceHook; +} + +// @public (undocumented) +export namespace NodeLoaderHooksAPI1 { + // (undocumented) + export type GetFormatHook = (url: string, context: {}, defaultGetFormat: GetFormatHook) => Promise<{ + format: NodeLoaderHooksFormat; + }>; + // (undocumented) + export type ResolveHook = NodeLoaderHooksAPI2.ResolveHook; + // (undocumented) + export type TransformSourceHook = (source: string | Buffer, context: { + url: string; + format: NodeLoaderHooksFormat; + }, defaultTransformSource: NodeLoaderHooksAPI1.TransformSourceHook) => Promise<{ + source: string | Buffer; + }>; +} + +// @public (undocumented) +export interface NodeLoaderHooksAPI2 { + // (undocumented) + load: NodeLoaderHooksAPI2.LoadHook; + // (undocumented) + resolve: NodeLoaderHooksAPI2.ResolveHook; +} + +// @public (undocumented) +export namespace NodeLoaderHooksAPI2 { + // (undocumented) + export type LoadHook = (url: string, context: { + format: NodeLoaderHooksFormat | null | undefined; + }, defaultLoad: NodeLoaderHooksAPI2['load']) => Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }>; + // (undocumented) + export type ResolveHook = (specifier: string, context: { + parentURL: string; + }, defaultResolve: ResolveHook) => Promise<{ + url: string; + }>; +} + +// @public (undocumented) +export type NodeLoaderHooksFormat = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; + // @public @deprecated export type Register = Service; From bc03a3e18d04197f61f9e91befdc294590758077 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 22 Oct 2021 15:20:08 -0400 Subject: [PATCH 21/26] 10.4.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e913799d6..db9178e3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.3.1", + "version": "10.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a249b3497..060a78c44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.3.1", + "version": "10.4.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { From 3e85ca1866085d09912e3718e7fe56d9112a446b Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 31 Oct 2021 22:07:52 -0400 Subject: [PATCH 22/26] Add swc target "es2022" (#1535) * add swc target es2022 * lint fix; --- src/transpilers/swc.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 6955bc90e..a5ba44265 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -143,7 +143,7 @@ targetMapping.set(/* ts.ScriptTarget.ES2018 */ 5, 'es2018'); targetMapping.set(/* ts.ScriptTarget.ES2019 */ 6, 'es2019'); targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2020'); targetMapping.set(/* ts.ScriptTarget.ES2021 */ 8, 'es2021'); -targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2021'); +targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2022'); type SwcTarget = typeof swcTargets[number]; /** @@ -160,6 +160,7 @@ const swcTargets = [ 'es2019', 'es2020', 'es2021', + 'es2022', ] as const; const ModuleKind = { From 2385967e6b23d98c3c355d0e630eafce5fba6f66 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 3 Nov 2021 01:41:33 -0400 Subject: [PATCH 23/26] Add --swc shorthand for the built-in swc transpiler; graduate the swc transpiler out of experimental status (#1536) * Add --swc shorthand for the built-in swc transpiler; graduate the swc transpiler out of experimental status * lint-fix * fix * fix tests * Improve swc / transpiler tests to prove that third-party transpiler was invoked * lintfix --- package.json | 1 + src/bin.ts | 4 ++ src/configuration.ts | 2 + src/index.ts | 38 ++++++++++++++--- src/test/index.spec.ts | 41 ++++++++++++------- tests/spy-swc-transpiler.js | 16 ++++++++ .../index.ts | 13 ++++++ .../tsconfig.json | 12 ++++++ .../transpile-only-swc-via-tsconfig/index.ts | 2 +- .../tsconfig.json | 2 +- tests/transpile-only-swc/index.ts | 2 +- transpilers/swc.js | 1 + website/docs/options.md | 1 + website/docs/performance.md | 2 +- website/docs/transpilers.md | 35 ++++++++++++---- 15 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 tests/spy-swc-transpiler.js create mode 100644 tests/transpile-only-swc-shorthand-via-tsconfig/index.ts create mode 100644 tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json create mode 100644 transpilers/swc.js diff --git a/package.json b/package.json index 060a78c44..8b9d0daab 100644 --- a/package.json +++ b/package.json @@ -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": "./transpilers/swc.js", "./transpilers/swc-experimental": "./transpilers/swc-experimental.js", "./node10/tsconfig.json": "./node10/tsconfig.json", "./node12/tsconfig.json": "./node12/tsconfig.json", diff --git a/src/bin.ts b/src/bin.ts index 95c7f9320..d97261a3b 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -55,6 +55,7 @@ export function main( '--ignore': [String], '--transpile-only': Boolean, '--transpiler': String, + '--swc': Boolean, '--type-check': Boolean, '--compiler-host': Boolean, '--pretty': Boolean, @@ -116,6 +117,7 @@ export function main( '--transpile-only': transpileOnly, '--type-check': typeCheck, '--transpiler': transpiler, + '--swc': swc, '--compiler-host': compilerHost, '--pretty': pretty, '--skip-project': skipProject, @@ -145,6 +147,7 @@ export function main( --show-config Print resolved configuration and exit -T, --transpile-only Use TypeScript's faster \`transpileModule\` or a third-party transpiler + --swc Use the swc 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 @@ -256,6 +259,7 @@ export function main( experimentalReplAwait: noExperimentalReplAwait ? false : undefined, typeCheck, transpiler, + swc, compilerHost, ignore, preferTsExts, diff --git a/src/configuration.ts b/src/configuration.ts index a970b49c4..b9bc9da5b 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -276,6 +276,7 @@ function filterRecognizedTsConfigTsNodeOptions( scopeDir, moduleTypes, experimentalReplAwait, + swc, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -298,6 +299,7 @@ function filterRecognizedTsConfigTsNodeOptions( scope, scopeDir, moduleTypes, + swc, }; // 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; diff --git a/src/index.ts b/src/index.ts index 977922ea5..1fc07cd88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -256,6 +256,12 @@ export interface CreateOptions { * Specify a custom transpiler for use with transpileOnly */ transpiler?: string | [string, object]; + /** + * Transpile with swc instead of the TypeScript compiler, and skip typechecking. + * + * Equivalent to setting both `transpileOnly: true` and `transpiler: 'ts-node/transpilers/swc'` + */ + swc?: boolean; /** * Paths which should not be compiled. * @@ -608,11 +614,33 @@ export function create(rawOptions: CreateOptions = {}): Service { ({ compiler, ts } = loadCompiler(options.compiler, configFilePath)); } + // swc implies two other options + // typeCheck option was implemented specifically to allow overriding tsconfig transpileOnly from the command-line + // So we should allow using typeCheck to override swc + if (options.swc && !options.typeCheck) { + if (options.transpileOnly === false) { + throw new Error( + "Cannot enable 'swc' option with 'transpileOnly: false'. 'swc' implies 'transpileOnly'." + ); + } + if (options.transpiler) { + throw new Error( + "Cannot specify both 'swc' and 'transpiler' options. 'swc' uses the built-in swc transpiler." + ); + } + } + const readFile = options.readFile || ts.sys.readFile; const fileExists = options.fileExists || ts.sys.fileExists; // typeCheck can override transpileOnly, useful for CLI flag to override config file const transpileOnly = - options.transpileOnly === true && options.typeCheck !== true; + (options.transpileOnly === true || options.swc === true) && + options.typeCheck !== true; + const transpiler = options.transpiler + ? options.transpiler + : options.swc + ? require.resolve('./transpilers/swc.js') + : undefined; const transformers = options.transformers || undefined; const diagnosticFilters: Array = [ { @@ -668,17 +696,15 @@ export function create(rawOptions: CreateOptions = {}): Service { ); } let customTranspiler: Transpiler | undefined = undefined; - if (options.transpiler) { + if (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]; + typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = - typeof options.transpiler === 'string' ? {} : options.transpiler[1] ?? {}; + typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; // TODO mimic fixed resolution logic from loadCompiler main // TODO refactor into a more generic "resolve dep relative to project" helper const transpilerPath = require.resolve(transpilerName, { diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 730d1a3be..7b613db66 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -70,6 +70,7 @@ 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'); testsDirRequire.resolve('ts-node/transpilers/swc-experimental'); testsDirRequire.resolve('ts-node/node10/tsconfig.json'); @@ -304,21 +305,31 @@ test.suite('ts-node', (test) => { expect(err.message).toMatch('error TS1003: Identifier expected'); }); - test('should support third-party transpilers via --transpiler', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --transpiler ts-node/transpilers/swc-experimental transpile-only-swc` - ); - expect(err).toBe(null); - expect(stdout).toMatch('Hello World!'); - }); - - test('should support third-party transpilers via tsconfig', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} transpile-only-swc-via-tsconfig` - ); - expect(err).toBe(null); - expect(stdout).toMatch('Hello World!'); - }); + for (const flavor of [ + '--transpiler ts-node/transpilers/swc transpile-only-swc', + '--transpiler ts-node/transpilers/swc-experimental transpile-only-swc', + '--swc transpile-only-swc', + 'transpile-only-swc-via-tsconfig', + 'transpile-only-swc-shorthand-via-tsconfig', + ]) { + test(`should support swc and third-party transpilers: ${flavor}`, async () => { + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ${flavor}`, + { + env: { + ...process.env, + NODE_OPTIONS: `${ + process.env.NODE_OPTIONS || '' + } --require ${require.resolve('../../tests/spy-swc-transpiler')}`, + }, + } + ); + expect(err).toBe(null); + expect(stdout).toMatch( + 'Hello World! swc transpiler invocation count: 1\n' + ); + }); + } if (semver.gte(process.version, '12.16.0')) { test('swc transpiler supports native ESM emit', async () => { diff --git a/tests/spy-swc-transpiler.js b/tests/spy-swc-transpiler.js new file mode 100644 index 000000000..99e77acc3 --- /dev/null +++ b/tests/spy-swc-transpiler.js @@ -0,0 +1,16 @@ +// Spy on the swc transpiler so that tests can prove it was used rather than +// TypeScript's `transpileModule`. +const swcTranspiler = require('ts-node/transpilers/swc'); + +global.swcTranspilerCalls = 0; + +const wrappedCreate = swcTranspiler.create; +swcTranspiler.create = function (...args) { + const transpiler = wrappedCreate(...args); + const wrappedTranspile = transpiler.transpile; + transpiler.transpile = function (...args) { + global.swcTranspilerCalls++; + return wrappedTranspile.call(this, ...args); + }; + return transpiler; +}; diff --git a/tests/transpile-only-swc-shorthand-via-tsconfig/index.ts b/tests/transpile-only-swc-shorthand-via-tsconfig/index.ts new file mode 100644 index 000000000..909fde693 --- /dev/null +++ b/tests/transpile-only-swc-shorthand-via-tsconfig/index.ts @@ -0,0 +1,13 @@ +// Test for #1343 +const Decorator = function () {}; +@Decorator +class World {} + +// intentional type errors to check transpile-only ESM loader skips type checking +parseInt(1101, 2); +const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`; +console.log(x); + +// test module type emit +import { readFileSync } from 'fs'; +readFileSync; diff --git a/tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json b/tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json new file mode 100644 index 000000000..9dba8805a --- /dev/null +++ b/tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json @@ -0,0 +1,12 @@ +{ + "ts-node": { + "swc": true + }, + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "allowJs": true, + "jsx": "react", + "experimentalDecorators": true + } +} diff --git a/tests/transpile-only-swc-via-tsconfig/index.ts b/tests/transpile-only-swc-via-tsconfig/index.ts index b7dfa09cd..909fde693 100644 --- a/tests/transpile-only-swc-via-tsconfig/index.ts +++ b/tests/transpile-only-swc-via-tsconfig/index.ts @@ -5,7 +5,7 @@ class World {} // intentional type errors to check transpile-only ESM loader skips type checking parseInt(1101, 2); -const x: number = `Hello ${World.name}!`; +const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`; console.log(x); // test module type emit diff --git a/tests/transpile-only-swc-via-tsconfig/tsconfig.json b/tests/transpile-only-swc-via-tsconfig/tsconfig.json index 5097d63ac..863b6c4f3 100644 --- a/tests/transpile-only-swc-via-tsconfig/tsconfig.json +++ b/tests/transpile-only-swc-via-tsconfig/tsconfig.json @@ -1,7 +1,7 @@ { "ts-node": { "transpileOnly": true, - "transpiler": "ts-node/transpilers/swc-experimental" + "transpiler": "ts-node/transpilers/swc" }, "compilerOptions": { "target": "ES2018", diff --git a/tests/transpile-only-swc/index.ts b/tests/transpile-only-swc/index.ts index b7dfa09cd..909fde693 100644 --- a/tests/transpile-only-swc/index.ts +++ b/tests/transpile-only-swc/index.ts @@ -5,7 +5,7 @@ class World {} // intentional type errors to check transpile-only ESM loader skips type checking parseInt(1101, 2); -const x: number = `Hello ${World.name}!`; +const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`; console.log(x); // test module type emit diff --git a/transpilers/swc.js b/transpilers/swc.js new file mode 100644 index 000000000..7cf79b13c --- /dev/null +++ b/transpilers/swc.js @@ -0,0 +1 @@ +module.exports = require('../dist/transpilers/swc') diff --git a/website/docs/options.md b/website/docs/options.md index 9139ea85c..90878e4f9 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -35,6 +35,7 @@ _Environment variables, where available, are in `ALL_CAPS`_ - `-I, --ignore [pattern]` Override the path patterns to skip compilation
*Default:* `/node_modules/`
*Environment:* `TS_NODE_IGNORE` - `--skip-ignore` Skip ignore checks
*Default:* `false`
*Environment:* `TS_NODE_SKIP_IGNORE` - `-C, --compiler [name]` Specify a custom TypeScript compiler
*Default:* `typescript`
*Environment:* `TS_NODE_COMPILER` +- `--swc` Transpile with [swc](./transpilers.md#swc). Implies `--transpile-only`
*Default:* `false` - `--transpiler [name]` Specify a third-party, non-typechecking transpiler - `--prefer-ts-exts` Re-order file extensions so that TypeScript imports are preferred
*Default:* `false`
*Environment:* `TS_NODE_PREFER_TS_EXTS` diff --git a/website/docs/performance.md b/website/docs/performance.md index a875bd2eb..69b2c6cba 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -9,7 +9,7 @@ These tricks will make ts-node faster. It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. * Enable [`transpileOnly`](./options.md) to skip typechecking -* Use our [`swc` integration](./transpilers.md#bundled-swc-integration) +* Use our [`swc` integration](./transpilers.md#swc) * This is by far the fastest option ## With typechecking diff --git a/website/docs/transpilers.md b/website/docs/transpilers.md index eb80fd11f..6d6f37095 100644 --- a/website/docs/transpilers.md +++ b/website/docs/transpilers.md @@ -1,5 +1,5 @@ --- -title: Third-party transpilers +title: Transpilers --- In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a @@ -13,12 +13,11 @@ boilerplate. > For our purposes, a compiler implements TypeScript's API and can perform typechecking. > A third-party transpiler does not. Both transform TypeScript into JavaScript. -## Bundled `swc` integration +## swc -We have bundled an experimental `swc` integration. +swc support is built-in via the `--swc` flag or `"swc": true` tsconfig option. -[`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster -than `transpileModule`. +[`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than vanilla `transpileOnly`. To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. @@ -31,17 +30,35 @@ Then add the following to your `tsconfig.json`. ```json title="tsconfig.json" { "ts-node": { - "transpileOnly": true, - "transpiler": "ts-node/transpilers/swc-experimental" + "swc": true } } ``` > `swc` uses `@swc/helpers` instead of `tslib`. If you have enabled `importHelpers`, you must also install `@swc/helpers`. +## Third-party transpilers + +The `transpiler` option allows using third-party transpiler integrations with ts-node. `transpiler` must be given the +name of a module which can be `require()`d. The built-in `swc` integration is exposed as `ts-node/transpilers/swc`. + +For example, to use a hypothetical "speedy-ts-compiler", first install it into your project: `npm install speedy-ts-compiler` + +Then add the following to your tsconfig: + +```json title="tsconfig.json" +{ + "ts-node": { + "transpileOnly": true, + "transpiler": "speedy-ts-compiler" + } +} +``` + ## Writing your own integration To write your own transpiler integration, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html). -Integrations are `require()`d, so they can be published to npm. The module must export a `create` function matching the -[`TranspilerModule`](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html) interface. +Integrations are `require()`d by ts-node, so they can be published to npm for convenience. The module must export a `create` function described by our +[`TranspilerModule`](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html) interface. `create` is invoked by ts-node +at startup to create the transpiler. The transpiler is used repeatedly to transform TypeScript into JavaScript. From d44c1ecaef9d22ae7f5a65607bab94d0c2f84b41 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 3 Nov 2021 01:41:50 -0400 Subject: [PATCH 24/26] allow disabling "use strict" emit in SWC when you reeeally want to (#1537) * allow disabling "use strict" emit in SWC when you reeeally want to * lint-fix --- package-lock.json | 125 +++++++++++++++++++++++------------------ src/transpilers/swc.ts | 23 +++++++- 2 files changed, 92 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index db9178e3b..f075aa828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -510,27 +510,18 @@ } }, "@napi-rs/triples": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.0.2.tgz", - "integrity": "sha512-EL3SiX43m9poFSnhDx4d4fn9SSaqyO2rHsCNhETi9bWPmjXK3uPJ0QpPFtx39FEdHcz1vJmsiW41kqc0AgvtzQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.0.3.tgz", + "integrity": "sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA==", "dev": true }, "@node-rs/helper": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.1.0.tgz", - "integrity": "sha512-r43YnnrY5JNzDuXJdW3sBJrKzvejvFmFWbiItUEoBJsaPzOIWFMhXB7i5j4c9EMXcFfxveF4l7hT+rLmwtjrVQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", + "integrity": "sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==", "dev": true, "requires": { - "@napi-rs/triples": "^1.0.2", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - } + "@napi-rs/triples": "^1.0.3" } }, "@nodelib/fs.scandir": { @@ -709,83 +700,107 @@ "dev": true }, "@swc/core": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.58.tgz", - "integrity": "sha512-u/vaon34x4ISDDdgZLaxacPB4Ly8SqqmFkBKPp2VtUDbD12VqKzb6EoLDC3A5EULFQDgIdIMHuVlBB+mc8dq0w==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.106.tgz", + "integrity": "sha512-9uw8gqU+lsk7KROAcSNhsrnBgNiC5H4MIaps5LlnnEevJmKu/o1ws22tXc2qjJg+F4/V1ynUbh8E0rYlmo1XGw==", "dev": true, "requires": { "@node-rs/helper": "^1.0.0", - "@swc/core-android-arm64": "^1.2.58", - "@swc/core-darwin-arm64": "^1.2.58", - "@swc/core-darwin-x64": "^1.2.58", - "@swc/core-linux-arm-gnueabihf": "^1.2.58", - "@swc/core-linux-arm64-gnu": "^1.2.58", - "@swc/core-linux-x64-gnu": "^1.2.58", - "@swc/core-linux-x64-musl": "^1.2.58", - "@swc/core-win32-ia32-msvc": "^1.2.58", - "@swc/core-win32-x64-msvc": "^1.2.58" + "@swc/core-android-arm64": "^1.2.106", + "@swc/core-darwin-arm64": "^1.2.106", + "@swc/core-darwin-x64": "^1.2.106", + "@swc/core-freebsd-x64": "^1.2.106", + "@swc/core-linux-arm-gnueabihf": "^1.2.106", + "@swc/core-linux-arm64-gnu": "^1.2.106", + "@swc/core-linux-arm64-musl": "^1.2.106", + "@swc/core-linux-x64-gnu": "^1.2.106", + "@swc/core-linux-x64-musl": "^1.2.106", + "@swc/core-win32-arm64-msvc": "^1.2.106", + "@swc/core-win32-ia32-msvc": "^1.2.106", + "@swc/core-win32-x64-msvc": "^1.2.106" } }, "@swc/core-android-arm64": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.58.tgz", - "integrity": "sha512-eSNNt/KiAbseOZ/lbaHnXClWOOeEPBRJBjxIBDX6U4oXaHLBCwgwU+qhWziVV4Lq6gX0zqcw6JY7Pxz9r2Pxzw==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.106.tgz", + "integrity": "sha512-F5T6kP3yV9S0/oXyco305QaAyE6rLNsNSdR0QI4CtACwKadiPwTOptwNIDCiTNLNgWlWLQmIRkPpxg+G4doT6Q==", "dev": true, "optional": true }, "@swc/core-darwin-arm64": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.58.tgz", - "integrity": "sha512-PHZm9kYi4KjWgac86fhr1238elI7M1K8Zh634eDCJCZbU7LHWUWOyeTpT9G8dxOuAUTOUZDaCHNW/+63N5XWPA==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.106.tgz", + "integrity": "sha512-bgKzzYLFnc+mv2mDS/DLwzBvx5DCC9ZCKYY46b4dAnBfasr+SMHj+v/WI84HtilbjLBMUfYZ2hgYKls3CebIIQ==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.58.tgz", - "integrity": "sha512-jKJNNxBbt/ckp49QUP28P+YEGDS3baruCBRbVkgJQY5Nj5GKw5kay6prVf6ajhoegmtjLr+1p3By7S5XOgIc8g==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.106.tgz", + "integrity": "sha512-I5Uhit5RqbXaMIV2+v9jL+MIQeR3lT1DqVwzxZs1bTARclAheFZQpTmg+h6QmichjCiUT74SXQb6Apc/vqYKog==", + "dev": true, + "optional": true + }, + "@swc/core-freebsd-x64": { + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.106.tgz", + "integrity": "sha512-ZSK3vgzbA2Pkpw2LgHlAkUdx4okIpdXXTbLXuc5jkZMw1KhRWpeQaDlwbrN7XVynAYjkj2qgGQ7wv1tD43vQig==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.58.tgz", - "integrity": "sha512-dnZcurOjTEr2IkSdWakyoVlE6ay3QQSSTv/9IsBH3eI7CI2+W8m9AtQ+KyN5BKPBSK5NjswF59xA3gocbsUpng==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.106.tgz", + "integrity": "sha512-WZh6XV8cQ9Fh3IQNX9z87Tv68+sLtfnT51ghMQxceRhfvc5gIaYW+PCppezDDdlPJnWXhybGWNPAl5SHppWb2g==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.58.tgz", - "integrity": "sha512-lSOd73EqFLx0I0f9UJq2wbwjQc+tbXbLznJp89tEZeLOljuMJkF3O22l2Nv6Vet6NBPbTQYiKy6ouibFvOqMag==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.106.tgz", + "integrity": "sha512-OSI9VUWPsRrCbUlRQ4KdYqdwV63VYBC5ahSNq+72DXhtRwVbLSFuF7MNsnXgUSMHidxbc0No3/bPPamshqHdsQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.106.tgz", + "integrity": "sha512-de8AAUOP8D2/tZIpQ399xw+pGGKlR1+l5Jmy4lW7ixarEI4xKkBSF4bS9eXtC1jckmenzrLPiK/5sSbQSf6BWQ==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.58.tgz", - "integrity": "sha512-bDU2LiURs4MKXWNNUKxVU1KKCO6lp1ILszkLPYuRAHbbQCtoQUe5JCbFlCqniFOxZOm2NBtZF4a+z5bGFpb0QA==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.106.tgz", + "integrity": "sha512-QzFC7+lBSuVBmX5tS2pdM+74voiJcGgIMJ+x9pcjUu3GkDl3ow6WC6ta2WUzlgGopCGNp6IdZaFemKRzjLr3lw==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.58.tgz", - "integrity": "sha512-wdF8nHrlMI4PUL13PQFo4BMfCr9HL3kNWftiA8i+mJhfp8Z2xfyarOnVkeXmYmQYGPoqSDCsskui6n5PvBLePw==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.106.tgz", + "integrity": "sha512-QZ1gFqNiCJefkNMihbmYc7nr5stERyjoQpWgAIN6dzrgMUzRHXHGDRl/p1qsXW2VKos+okSdLwPFEmRT94H+1A==", + "dev": true, + "optional": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.106.tgz", + "integrity": "sha512-MbuQwk+s43bfBNnAZTKnoQlfo4UPSOsy6t9F15yU4P3rVUuFtcxdZg6CpDnUqNPbojILXujp8z4SSigRYh5cgg==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.58.tgz", - "integrity": "sha512-Qrox0Kz3KQSYnMwAH55DYXzOG+L0PPYQHaQnJCh5rywKDUx2n/Ar5zKkVkEhRf0ehPgKajt0h2BYHsTpqNA9/w==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.106.tgz", + "integrity": "sha512-BFxWpcPxsG2LLQZ+8K8ma45rbTckjpPbnvOOhybQ0hEhLgoVzMVPp3RIUGmC+RMZe6DkGSaEQf/Rjn2cbMdQhw==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.58.tgz", - "integrity": "sha512-HPmxovhC7DbNcXLJe5nUmo+4o6Ea2d7oFdli3IvTgDri0IynQaRlfVWIuNnZmEsN7Gl1kW7PUK5WZXPUosMn8A==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.106.tgz", + "integrity": "sha512-Emn5akqApGXzPsA7ntSXEohL0AH0WjQMHy6mT3MS9Yil42yTJ96dJGf68ejKVptxwg7Iz798mT+J9r1JbAFBgg==", "dev": true, "optional": true }, diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index a5ba44265..ed837dae2 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -52,6 +52,9 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { module, jsxFactory, jsxFragmentFactory, + strict, + alwaysStrict, + noImplicitUseStrict, } = compilerOptions; const nonTsxOptions = createSwcOptions(false); const tsxOptions = createSwcOptions(true); @@ -71,6 +74,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } swcTarget = swcTargets[swcTargetIndex]; const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; + // swc only supports these 4x module options const moduleType = module === ModuleKind.CommonJS ? 'commonjs' @@ -78,7 +82,23 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { ? 'amd' : module === ModuleKind.UMD ? 'umd' - : undefined; + : 'es6'; + // In swc: + // strictMode means `"use strict"` is *always* emitted for non-ES module, *never* for ES module where it is assumed it can be omitted. + // (this assumption is invalid, but that's the way swc behaves) + // tsc is a bit more complex: + // alwaysStrict will force emitting it always unless `import`/`export` syntax is emitted which implies it per the JS spec. + // if not alwaysStrict, will emit implicitly whenever module target is non-ES *and* transformed module syntax is emitted. + // For node, best option is to assume that all scripts are modules (commonjs or esm) and thus should get tsc's implicit strict behavior. + + // Always set strictMode, *unless* alwaysStrict is disabled and noImplicitUseStrict is enabled + const strictMode = + // if `alwaysStrict` is disabled, remembering that `strict` defaults `alwaysStrict` to true + (alwaysStrict === false || (alwaysStrict !== true && strict !== true)) && + // if noImplicitUseStrict is enabled + noImplicitUseStrict === true + ? false + : true; return { sourceMaps: sourceMap, // isModule: true, @@ -86,6 +106,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { ? ({ noInterop: !esModuleInterop, type: moduleType, + strictMode, } as swcTypes.ModuleConfig) : undefined, swcrc: false, From dd4c04ed56a7b3cb1d00245669820c467cd5d8fd Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 7 Nov 2021 13:33:43 -0500 Subject: [PATCH 25/26] fix (#1540) --- src/transpilers/swc.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index ed837dae2..3111bfc16 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -164,6 +164,7 @@ targetMapping.set(/* ts.ScriptTarget.ES2018 */ 5, 'es2018'); targetMapping.set(/* ts.ScriptTarget.ES2019 */ 6, 'es2019'); targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2020'); targetMapping.set(/* ts.ScriptTarget.ES2021 */ 8, 'es2021'); +targetMapping.set(/* ts.ScriptTarget.ES2022 */ 9, 'es2022'); targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2022'); type SwcTarget = typeof swcTargets[number]; From 2832a59f4f193f8faf1f6807da88390a38e5540f Mon Sep 17 00:00:00 2001 From: Trevor Paley <10186337+TheUnlocked@users.noreply.github.com> Date: Sun, 7 Nov 2021 14:44:07 -0500 Subject: [PATCH 26/26] Invoke TS compiler before starting REPL prompt (#1498) --- src/repl.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/repl.ts b/src/repl.ts index 0ef51017d..ad399b850 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -330,6 +330,11 @@ export function createRepl(options: CreateReplOptions = {}) { } } + // In case the typescript compiler hasn't compiled anything yet, + // make it run though compilation at least one time before + // the REPL starts for a snappier user experience on startup. + service?.compile('', state.path); + const repl = nodeReplStart({ prompt: '> ', input: replService.stdin,