Skip to content

Commit 889c21d

Browse files
authored
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
1 parent 56b70da commit 889c21d

File tree

4 files changed

+108
-14
lines changed

4 files changed

+108
-14
lines changed

dist-raw/node-primordials.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
ObjectGetOwnPropertyNames: Object.getOwnPropertyNames,
1717
ObjectDefineProperty: Object.defineProperty,
1818
ObjectPrototypeHasOwnProperty: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop),
19+
RegExpPrototypeExec: (obj, string) => RegExp.prototype.exec.call(obj, string),
1920
RegExpPrototypeTest: (obj, string) => RegExp.prototype.test.call(obj, string),
2021
RegExpPrototypeSymbolReplace: (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest),
2122
SafeMap: Map,

src/esm.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,53 @@ const { defaultGetFormat } = require('../dist-raw/node-esm-default-get-format');
3636
// from node, build our implementation of the *new* API on top of it, and implement the *old*
3737
// hooks API as a shim to the *new* API.
3838

39+
export interface NodeLoaderHooksAPI1 {
40+
resolve: NodeLoaderHooksAPI1.ResolveHook;
41+
getFormat: NodeLoaderHooksAPI1.GetFormatHook;
42+
transformSource: NodeLoaderHooksAPI1.TransformSourceHook;
43+
}
44+
export namespace NodeLoaderHooksAPI1 {
45+
export type ResolveHook = NodeLoaderHooksAPI2.ResolveHook;
46+
export type GetFormatHook = (
47+
url: string,
48+
context: {},
49+
defaultGetFormat: GetFormatHook
50+
) => Promise<{ format: NodeLoaderHooksFormat }>;
51+
export type TransformSourceHook = (
52+
source: string | Buffer,
53+
context: { url: string; format: NodeLoaderHooksFormat },
54+
defaultTransformSource: NodeLoaderHooksAPI1.TransformSourceHook
55+
) => Promise<{ source: string | Buffer }>;
56+
}
57+
58+
export interface NodeLoaderHooksAPI2 {
59+
resolve: NodeLoaderHooksAPI2.ResolveHook;
60+
load: NodeLoaderHooksAPI2.LoadHook;
61+
}
62+
export namespace NodeLoaderHooksAPI2 {
63+
export type ResolveHook = (
64+
specifier: string,
65+
context: { parentURL: string },
66+
defaultResolve: ResolveHook
67+
) => Promise<{ url: string }>;
68+
export type LoadHook = (
69+
url: string,
70+
context: { format: NodeLoaderHooksFormat | null | undefined },
71+
defaultLoad: NodeLoaderHooksAPI2['load']
72+
) => Promise<{
73+
format: NodeLoaderHooksFormat;
74+
source: string | Buffer | undefined;
75+
}>;
76+
}
77+
78+
export type NodeLoaderHooksFormat =
79+
| 'builtin'
80+
| 'commonjs'
81+
| 'dynamic'
82+
| 'json'
83+
| 'module'
84+
| 'wasm';
85+
3986
/** @internal */
4087
export function registerAndCreateEsmHooks(opts?: RegisterOptions) {
4188
// Automatically performs registration just like `-r ts-node/register`
@@ -62,12 +109,7 @@ export function createEsmHooks(tsNodeService: Service) {
62109
versionGteLt(process.versions.node, '12.999.999', '13.0.0');
63110

64111
// Explicit return type to avoid TS's non-ideal inferred type
65-
const hooksAPI: {
66-
resolve: typeof resolve;
67-
getFormat: typeof getFormat | undefined;
68-
transformSource: typeof transformSource | undefined;
69-
load: typeof load | undefined;
70-
} = newHooksAPI
112+
const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI
71113
? { resolve, load, getFormat: undefined, transformSource: undefined }
72114
: { resolve, getFormat, transformSource, load: undefined };
73115
return hooksAPI;
@@ -117,9 +159,12 @@ export function createEsmHooks(tsNodeService: Service) {
117159
// `load` from new loader hook API (See description at the top of this file)
118160
async function load(
119161
url: string,
120-
context: { format: Format | null | undefined },
162+
context: { format: NodeLoaderHooksFormat | null | undefined },
121163
defaultLoad: typeof load
122-
): Promise<{ format: Format; source: string | Buffer | undefined }> {
164+
): Promise<{
165+
format: NodeLoaderHooksFormat;
166+
source: string | Buffer | undefined;
167+
}> {
123168
// If we get a format hint from resolve() on the context then use it
124169
// otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node
125170
const format =
@@ -160,12 +205,11 @@ export function createEsmHooks(tsNodeService: Service) {
160205
return { format, source };
161206
}
162207

163-
type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm';
164208
async function getFormat(
165209
url: string,
166210
context: {},
167211
defaultGetFormat: typeof getFormat
168-
): Promise<{ format: Format }> {
212+
): Promise<{ format: NodeLoaderHooksFormat }> {
169213
const defer = (overrideUrl: string = url) =>
170214
defaultGetFormat(overrideUrl, context, defaultGetFormat);
171215

@@ -185,7 +229,7 @@ export function createEsmHooks(tsNodeService: Service) {
185229

186230
// If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js
187231
const ext = extname(nativePath);
188-
let nodeSays: { format: Format };
232+
let nodeSays: { format: NodeLoaderHooksFormat };
189233
if (ext !== '.js' && !tsNodeService.ignored(nativePath)) {
190234
nodeSays = await defer(formatUrl(pathToFileURL(nativePath + '.js')));
191235
} else {
@@ -210,7 +254,7 @@ export function createEsmHooks(tsNodeService: Service) {
210254

211255
async function transformSource(
212256
source: string | Buffer,
213-
context: { url: string; format: Format },
257+
context: { url: string; format: NodeLoaderHooksFormat },
214258
defaultTransformSource: typeof transformSource
215259
): Promise<{ source: string | Buffer }> {
216260
if (source === null || source === undefined) {

src/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ModuleTypeClassifier,
2424
} from './module-type-classifier';
2525
import { createResolverFunctions } from './resolver-functions';
26+
import type { createEsmHooks as createEsmHooksFn } from './esm';
2627

2728
export { TSCommon };
2829
export {
@@ -39,6 +40,11 @@ export type {
3940
TranspileOptions,
4041
Transpiler,
4142
} from './transpilers/types';
43+
export type {
44+
NodeLoaderHooksAPI1,
45+
NodeLoaderHooksAPI2,
46+
NodeLoaderHooksFormat,
47+
} from './esm';
4248

4349
/**
4450
* Does this version of node obey the package.json "type" field
@@ -1486,7 +1492,16 @@ function getTokenAtPosition(
14861492
}
14871493
}
14881494

1489-
import type { createEsmHooks as createEsmHooksFn } from './esm';
1495+
/**
1496+
* Create an implementation of node's ESM loader hooks.
1497+
*
1498+
* This may be useful if you
1499+
* want to wrap or compose the loader hooks to add additional functionality or
1500+
* combine with another loader.
1501+
*
1502+
* Node changed the hooks API, so there are two possible APIs. This function
1503+
* detects your node version and returns the appropriate API.
1504+
*/
14901505
export const createEsmHooks: typeof createEsmHooksFn = (
14911506
tsNodeService: Service
1492-
) => require('./esm').createEsmHooks(tsNodeService);
1507+
) => (require('./esm') as typeof import('./esm')).createEsmHooks(tsNodeService);

src/test/esm-loader.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import semver = require('semver');
77
import {
88
contextTsNodeUnderTest,
99
EXPERIMENTAL_MODULES_FLAG,
10+
resetNodeEnvironment,
1011
TEST_DIR,
1112
} from './helpers';
1213
import { createExec } from './exec-helpers';
1314
import { join } from 'path';
1415
import * as expect from 'expect';
16+
import type { NodeLoaderHooksAPI2 } from '../';
17+
18+
const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0');
1519

1620
const test = context(contextTsNodeUnderTest);
1721

@@ -37,3 +41,33 @@ test.suite('createEsmHooks', (test) => {
3741
});
3842
}
3943
});
44+
45+
test.suite('hooks', (_test) => {
46+
const test = _test.context(async (t) => {
47+
const service = t.context.tsNodeUnderTest.create({
48+
cwd: TEST_DIR,
49+
});
50+
t.teardown(() => {
51+
resetNodeEnvironment();
52+
});
53+
return {
54+
service,
55+
hooks: t.context.tsNodeUnderTest.createEsmHooks(service),
56+
};
57+
});
58+
59+
if (nodeUsesNewHooksApi) {
60+
test('Correctly determines format of data URIs', async (t) => {
61+
const { hooks } = t.context;
62+
const url = 'data:text/javascript,console.log("hello world");';
63+
const result = await (hooks as NodeLoaderHooksAPI2).load(
64+
url,
65+
{ format: undefined },
66+
async (url, context, _ignored) => {
67+
return { format: context.format!, source: '' };
68+
}
69+
);
70+
expect(result.format).toBe('module');
71+
});
72+
}
73+
});

0 commit comments

Comments
 (0)