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

Feature: Expose esm hooks factory to public API #1439

Merged
merged 11 commits into from Oct 10, 2021
21 changes: 13 additions & 8 deletions 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,
Expand All @@ -15,17 +15,22 @@ const {

// Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts

/** @internal */
export function registerAndCreateEsmHooks(opts?: RegisterOptions) {
nonara marked this conversation as resolved.
Show resolved Hide resolved
// Automatically performs registration just like `-r ts-node/register`
const tsNodeInstance = register({
...opts,
experimentalEsmLoader: true,
});

return createEsmHooks(tsNodeInstance);
}

export function createEsmHooks(tsNodeService: Service) {
// 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 };
Expand Down Expand Up @@ -98,17 +103,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') {
Expand Down Expand Up @@ -139,11 +144,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 };
}
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Expand Up @@ -1430,3 +1430,8 @@ function getTokenAtPosition(
return current;
}
}

import type { createEsmHooks as createEsmHooksFn } from './esm';
export const createEsmHooks: typeof createEsmHooksFn = (
tsNodeService: Service
) => require('./esm').createEsmHooks(tsNodeService);
19 changes: 19 additions & 0 deletions src/test/index.spec.ts
Expand Up @@ -1867,6 +1867,25 @@ test.suite('ts-node', (test) => {
});
});

test.suite('createEsmHooks', (test) => {
if (semver.gte(process.version, '12.0.0')) {
Copy link
Contributor Author

@nonara nonara Aug 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the if be above the suite? Also, is the version logic sound?

test('should create proper hooks with provided instance', async () => {
const { err } = await exec(
`node ${experimentalModulesFlag} --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).to.match(/TS6133:\s+'unusedVar'/);
});
}
});

test.suite('esm', (test) => {
if (semver.gte(process.version, '12.16.0')) {
test('should compile and execute as ESM', async () => {
Expand Down
4 changes: 4 additions & 0 deletions tests/esm-custom-loader/index.ts
@@ -0,0 +1,4 @@
export function abc() {
let unusedVar: string;
return true;
}
17 changes: 17 additions & 0 deletions tests/esm-custom-loader/loader.mjs
@@ -0,0 +1,17 @@
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,
},
experimentalEsmLoader: true,
});

export const { resolve, getFormat, transformSource } = createEsmHooks(
tsNodeInstance
);
3 changes: 3 additions & 0 deletions tests/esm-custom-loader/package.json
@@ -0,0 +1,3 @@
{
"type": "module"
}
7 changes: 7 additions & 0 deletions tests/esm-custom-loader/tsconfig.json
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"noUnusedLocals": false
}
}