Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix data URL format determination in new loader hooks API #1529

Merged
merged 6 commits into from Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions dist-raw/node-primordials.js
Expand Up @@ -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,
Expand Down
68 changes: 56 additions & 12 deletions src/esm.ts
Expand Up @@ -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`
Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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);

Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
19 changes: 17 additions & 2 deletions src/index.ts
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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);
34 changes: 34 additions & 0 deletions src/test/esm-loader.spec.ts
Expand Up @@ -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);

Expand All @@ -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');
});
}
});