From d29b0e372de660e22d1bcf796e74cbaba230573a Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 18:28:51 -0400 Subject: [PATCH 1/6] Add regression test --- src/esm.ts | 41 ++++++++++++++++++++++++++++++------- src/test/esm-loader.spec.ts | 32 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/esm.ts b/src/esm.ts index 35bca748a..77a7f87bd 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -36,6 +36,39 @@ 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 NodeHooksAPI1 { + resolve( + specifier: string, + context: { parentURL: string }, + defaultResolve: NodeHooksAPI1['resolve'] + ): Promise<{ url: string }>; + getFormat( + url: string, + context: {}, + defaultGetFormat: NodeHooksAPI1['getFormat'] + ): Promise<{ format: Format }>; + transformSource( + source: string | Buffer, + context: { url: string; format: Format }, + defaultTransformSource: NodeHooksAPI1['transformSource'] + ): Promise<{ source: string | Buffer }>; +} + +export interface NodeHooksAPI2 { + resolve( + specifier: string, + context: { parentURL: string }, + defaultResolve: NodeHooksAPI2['resolve'] + ): Promise<{ url: string }>; + load( + url: string, + context: { format: Format | null | undefined }, + defaultLoad: NodeHooksAPI2['load'] + ): Promise<{ format: Format; source: string | Buffer | undefined }>; +} + +type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; + /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` @@ -62,12 +95,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: NodeHooksAPI1 | NodeHooksAPI2 = newHooksAPI ? { resolve, load, getFormat: undefined, transformSource: undefined } : { resolve, getFormat, transformSource, load: undefined }; return hooksAPI; @@ -160,7 +188,6 @@ export function createEsmHooks(tsNodeService: Service) { return { format, source }; } - type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; async function getFormat( url: string, context: {}, diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index e117e1ef8..e53b3e70b 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 { NodeHooksAPI2 } from '../esm'; + +const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); const test = context(contextTsNodeUnderTest); @@ -37,3 +41,31 @@ test.suite('createEsmHooks', (test) => { }); } }); + +test.suite('hooks', (_test) => { + const test = _test.context(async (t) => { + const service = t.context.tsNodeUnderTest.create(); + 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 NodeHooksAPI2).load( + url, + { format: undefined }, + async (url, context, _ignored) => { + return { format: context.format!, source: '' }; + } + ); + expect(result.format).toBe('module'); + }); + } +}); From f76618ba3b86093c3af2f51e639a7b04915a9e26 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 17:51:49 -0400 Subject: [PATCH 2/6] add missing primordial RegExpPrototypeExec --- dist-raw/node-primordials.js | 1 + 1 file changed, 1 insertion(+) 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, From 6f5584f9bc379fb21b15e21e996808186adf8a33 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 22:06:54 -0400 Subject: [PATCH 3/6] fixg --- src/test/esm-loader.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index e53b3e70b..d89ea260d 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -44,7 +44,9 @@ test.suite('createEsmHooks', (test) => { test.suite('hooks', (_test) => { const test = _test.context(async (t) => { - const service = t.context.tsNodeUnderTest.create(); + const service = t.context.tsNodeUnderTest.create({ + cwd: TEST_DIR + }); t.teardown(() => { resetNodeEnvironment(); }); From cd6d2089052e781b725021cd36c9b93b16e67d15 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 22:26:05 -0400 Subject: [PATCH 4/6] Improve createEsmHooks docs --- src/esm.ts | 36 ++++++++++++++++++------------------ src/index.ts | 3 ++- src/test/esm-loader.spec.ts | 4 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/esm.ts b/src/esm.ts index 77a7f87bd..fb59ef33f 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -36,38 +36,38 @@ 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 NodeHooksAPI1 { +export interface NodeLoaderHooksAPI1 { resolve( specifier: string, context: { parentURL: string }, - defaultResolve: NodeHooksAPI1['resolve'] + defaultResolve: NodeLoaderHooksAPI1['resolve'] ): Promise<{ url: string }>; getFormat( url: string, context: {}, - defaultGetFormat: NodeHooksAPI1['getFormat'] - ): Promise<{ format: Format }>; + defaultGetFormat: NodeLoaderHooksAPI1['getFormat'] + ): Promise<{ format: NodeLoaderHooksFormat }>; transformSource( source: string | Buffer, - context: { url: string; format: Format }, - defaultTransformSource: NodeHooksAPI1['transformSource'] + context: { url: string; format: NodeLoaderHooksFormat }, + defaultTransformSource: NodeLoaderHooksAPI1['transformSource'] ): Promise<{ source: string | Buffer }>; } -export interface NodeHooksAPI2 { +export interface NodeLoaderHooksAPI2 { resolve( specifier: string, context: { parentURL: string }, - defaultResolve: NodeHooksAPI2['resolve'] + defaultResolve: NodeLoaderHooksAPI2['resolve'] ): Promise<{ url: string }>; load( url: string, - context: { format: Format | null | undefined }, - defaultLoad: NodeHooksAPI2['load'] - ): Promise<{ format: Format; source: string | Buffer | undefined }>; + context: { format: NodeLoaderHooksFormat | null | undefined }, + defaultLoad: NodeLoaderHooksAPI2['load'] + ): Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined }>; } -type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; +export type NodeLoaderHooksFormat = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { @@ -95,7 +95,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: NodeHooksAPI1 | NodeHooksAPI2 = newHooksAPI + const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI ? { resolve, load, getFormat: undefined, transformSource: undefined } : { resolve, getFormat, transformSource, load: undefined }; return hooksAPI; @@ -145,9 +145,9 @@ 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 = @@ -192,7 +192,7 @@ export function createEsmHooks(tsNodeService: Service) { url: string, context: {}, defaultGetFormat: typeof getFormat - ): Promise<{ format: Format }> { + ): Promise<{ format: NodeLoaderHooksFormat }> { const defer = (overrideUrl: string = url) => defaultGetFormat(overrideUrl, context, defaultGetFormat); @@ -212,7 +212,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 { @@ -237,7 +237,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..fb4ff7090 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,7 @@ 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 +1488,6 @@ function getTokenAtPosition( } } -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 index d89ea260d..b4ca5862a 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -13,7 +13,7 @@ import { import { createExec } from './exec-helpers'; import { join } from 'path'; import * as expect from 'expect'; -import type { NodeHooksAPI2 } from '../esm'; +import type { NodeLoaderHooksAPI2 } from '../'; const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); @@ -60,7 +60,7 @@ test.suite('hooks', (_test) => { 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 NodeHooksAPI2).load( + const result = await (hooks as NodeLoaderHooksAPI2).load( url, { format: undefined }, async (url, context, _ignored) => { From 4b957a1b9eb2029cd90402829e633d0949209105 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 22:44:36 -0400 Subject: [PATCH 5/6] Fix docs for esm hooks --- src/esm.ts | 37 +++++++++++++++++++++---------------- src/index.ts | 12 +++++++++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/esm.ts b/src/esm.ts index fb59ef33f..70dc70850 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -37,34 +37,39 @@ const { defaultGetFormat } = require('../dist-raw/node-esm-default-get-format'); // hooks API as a shim to the *new* API. export interface NodeLoaderHooksAPI1 { - resolve( - specifier: string, - context: { parentURL: string }, - defaultResolve: NodeLoaderHooksAPI1['resolve'] - ): Promise<{ url: string }>; - getFormat( + resolve: NodeLoaderHooksAPI1.ResolveHook; + getFormat: NodeLoaderHooksAPI1.GetFormatHook; + transformSource: NodeLoaderHooksAPI1.TransformSourceHook; +} +export namespace NodeLoaderHooksAPI1 { + export type ResolveHook = NodeLoaderHooksAPI2.ResolveHook; + export type GetFormatHook = ( url: string, context: {}, - defaultGetFormat: NodeLoaderHooksAPI1['getFormat'] - ): Promise<{ format: NodeLoaderHooksFormat }>; - transformSource( + defaultGetFormat: GetFormatHook + ) => Promise<{ format: NodeLoaderHooksFormat }>; + export type TransformSourceHook = ( source: string | Buffer, context: { url: string; format: NodeLoaderHooksFormat }, - defaultTransformSource: NodeLoaderHooksAPI1['transformSource'] - ): Promise<{ source: string | Buffer }>; + defaultTransformSource: NodeLoaderHooksAPI1.TransformSourceHook + ) => Promise<{ source: string | Buffer }>; } export interface NodeLoaderHooksAPI2 { - resolve( + resolve: NodeLoaderHooksAPI2.ResolveHook; + load: NodeLoaderHooksAPI2.LoadHook; +} +export namespace NodeLoaderHooksAPI2 { + export type ResolveHook = ( specifier: string, context: { parentURL: string }, - defaultResolve: NodeLoaderHooksAPI2['resolve'] - ): Promise<{ url: string }>; - load( + 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 }>; + ) => Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined }>; } export type NodeLoaderHooksFormat = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; diff --git a/src/index.ts b/src/index.ts index fb4ff7090..0fe3e16e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1488,6 +1488,16 @@ function getTokenAtPosition( } } +/** + * 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); From b5ff840f529efca9562a72602262cbcee227a149 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 21 Oct 2021 22:45:26 -0400 Subject: [PATCH 6/6] lint-fix --- src/esm.ts | 18 +++++++++++++++--- src/index.ts | 6 +++++- src/test/esm-loader.spec.ts | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/esm.ts b/src/esm.ts index 70dc70850..c83fd22c4 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -69,10 +69,19 @@ export namespace NodeLoaderHooksAPI2 { url: string, context: { format: NodeLoaderHooksFormat | null | undefined }, defaultLoad: NodeLoaderHooksAPI2['load'] - ) => Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined }>; + ) => Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }>; } -export type NodeLoaderHooksFormat = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; +export type NodeLoaderHooksFormat = + | 'builtin' + | 'commonjs' + | 'dynamic' + | 'json' + | 'module' + | 'wasm'; /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { @@ -152,7 +161,10 @@ export function createEsmHooks(tsNodeService: Service) { url: string, context: { format: NodeLoaderHooksFormat | null | undefined }, defaultLoad: typeof load - ): Promise<{ format: NodeLoaderHooksFormat; 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 = diff --git a/src/index.ts b/src/index.ts index 0fe3e16e8..977922ea5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,11 @@ export type { TranspileOptions, Transpiler, } from './transpilers/types'; -export type {NodeLoaderHooksAPI1, NodeLoaderHooksAPI2, NodeLoaderHooksFormat} from './esm'; +export type { + NodeLoaderHooksAPI1, + NodeLoaderHooksAPI2, + NodeLoaderHooksFormat, +} from './esm'; /** * Does this version of node obey the package.json "type" field diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index b4ca5862a..6c3c1c51a 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -45,7 +45,7 @@ test.suite('createEsmHooks', (test) => { test.suite('hooks', (_test) => { const test = _test.context(async (t) => { const service = t.context.tsNodeUnderTest.create({ - cwd: TEST_DIR + cwd: TEST_DIR, }); t.teardown(() => { resetNodeEnvironment();