From b472efaba345042bfb16b7c996a5ae75aa4f695c Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 9 Oct 2021 13:41:30 -0700 Subject: [PATCH 1/5] fix(require): allow `require()` of ESM in ".js" files; - Closes #7 --- src/require.ts | 61 +++++++++++++++++++++---------- test/fixtures/module/index.js | 6 +++ test/fixtures/module/index.mjs | 6 +++ test/fixtures/module/package.json | 3 ++ test/index.js | 14 +++++++ test/index.mjs | 12 ++++++ 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 test/fixtures/module/index.js create mode 100644 test/fixtures/module/index.mjs create mode 100644 test/fixtures/module/package.json diff --git a/src/require.ts b/src/require.ts index 7593e75..69275a9 100644 --- a/src/require.ts +++ b/src/require.ts @@ -1,7 +1,8 @@ const { extname } = require('path'); +const { readFileSync } = require('fs'); const tsm = require('./utils'); -import type { Config } from 'tsm/config'; +import type { Config, Options } from 'tsm/config'; type TSM = typeof import('./utils.d'); type Module = NodeJS.Module & { @@ -47,29 +48,51 @@ const tsrequire = 'var $$req=require;require=(' + function () { return existsSync(file) ? $$req(file) : $$req(ident); } }) -} + ')();' +} + ')();'; + +function transform(source: string, sourcefile: string, options: Options): string { + let banner = options.banner || ''; + if (/\.[mc]?tsx?$/.test(sourcefile)) { + banner = tsrequire + banner; + } + + esbuild = esbuild || require('esbuild'); + return esbuild.transformSync(source, { + ...options, banner, sourcefile + }).code; +} function loader(Module: Module, sourcefile: string) { - let extn = extname(sourcefile); let pitch = Module._compile!.bind(Module); - - Module._compile = source => { - let options = config[extn]; - if (options == null) return pitch(source, sourcefile); - - let banner = options.banner || ''; - if (/\.[mc]?tsx?$/.test(extn)) { - banner = tsrequire + banner; - } - - esbuild = esbuild || require('esbuild'); - let result = esbuild.transformSync(source, { ...options, banner, sourcefile }); - return pitch(result.code, sourcefile); - }; - - return loadJS(Module, sourcefile); + let extn = extname(sourcefile); + let options = config[extn]; + + if (options != null) { + Module._compile = source => { + let result = transform(source, sourcefile, options); + return pitch(result, sourcefile); + }; + } + + try { + return loadJS(Module, sourcefile); + } catch (err) { + let ec = err && (err as any).code; + if (ec !== 'ERR_REQUIRE_ESM') throw err; + + let input = readFileSync(sourcefile, 'utf8'); + let result = transform(input, sourcefile, { + ...options, format: 'cjs' + }); + + return pitch(result, sourcefile); + } } for (let extn in config) { require.extensions[extn] = loader; } + +if (config['.js'] == null) { + require.extensions['.js'] = loader; +} diff --git a/test/fixtures/module/index.js b/test/fixtures/module/index.js new file mode 100644 index 0000000..ddadf48 --- /dev/null +++ b/test/fixtures/module/index.js @@ -0,0 +1,6 @@ +/** + * @param {string} name + */ +export function hello(name) { + return `hello, ${name}`; +} diff --git a/test/fixtures/module/index.mjs b/test/fixtures/module/index.mjs new file mode 100644 index 0000000..ddadf48 --- /dev/null +++ b/test/fixtures/module/index.mjs @@ -0,0 +1,6 @@ +/** + * @param {string} name + */ +export function hello(name) { + return `hello, ${name}`; +} diff --git a/test/fixtures/module/package.json b/test/fixtures/module/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/test/fixtures/module/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/index.js b/test/index.js index abbb89c..14efd86 100644 --- a/test/index.js +++ b/test/index.js @@ -11,6 +11,10 @@ const ts = require('./fixtures/math.ts'); const mts = require('./fixtures/utils.mts'); // @ts-ignore – prefers extensionless const cts = require('./fixtures/utils.cts'); +// @ts-ignore – prefers extensionless +const esm1 = require('./fixtures/module/index.js'); +// @ts-ignore – prefers extensionless +const esm2 = require('./fixtures/module/index.mjs'); const props = { foo: 'bar' @@ -55,4 +59,14 @@ assert.equal(typeof cts, 'object', 'CTS :: typeof'); assert.equal(typeof cts.dashify, 'function', 'CTS :: typeof :: dashify'); assert.equal(cts.dashify('FooBar'), 'foo-bar', 'CTS :: value :: dashify'); +assert(esm1, 'ESM.js :: typeof'); +assert.equal(typeof esm1, 'object', 'ESM.js :: typeof'); +assert.equal(typeof esm1.hello, 'function', 'ESM.js :: typeof :: hello'); +assert.equal(esm1.hello('you'), 'hello, you', 'ESM.js :: value :: hello'); + +assert(esm2, 'ESM.mjs :: typeof'); +assert.equal(typeof esm2, 'object', 'ESM.mjs :: typeof'); +assert.equal(typeof esm2.hello, 'function', 'ESM.mjs :: typeof :: hello'); +assert.equal(esm2.hello('you'), 'hello, you', 'ESM.mjs :: value :: hello'); + console.log('DONE~!'); diff --git a/test/index.mjs b/test/index.mjs index cf7a521..93e14c6 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -11,6 +11,10 @@ import * as cts from './fixtures/utils.cts'; import * as ts from './fixtures/math.ts'; // @ts-ignore – prefers extensionless import tsx from './fixtures/App2.tsx'; +// @ts-ignore – prefers extensionless +import * as esm1 from './fixtures/module/index.js'; +// @ts-ignore – prefers extensionless +import * as esm2 from './fixtures/module/index.mjs'; const props = { foo: 'bar' @@ -53,4 +57,12 @@ assert.equal(typeof cts, 'object', 'CTS :: typeof'); assert.equal(typeof cts.dashify, 'function', 'CTS :: typeof :: dashify'); assert.equal(cts.dashify('FooBar'), 'foo-bar', 'CTS :: value :: dashify'); +assert.equal(typeof esm1, 'object', 'ESM.js :: typeof'); +assert.equal(typeof esm1.hello, 'function', 'ESM.js :: typeof :: hello'); +assert.equal(esm1.hello('you'), 'hello, you', 'ESM.js :: value :: hello'); + +assert.equal(typeof esm2, 'object', 'ESM.mjs :: typeof'); +assert.equal(typeof esm2.hello, 'function', 'ESM.mjs :: typeof :: hello'); +assert.equal(esm2.hello('you'), 'hello, you', 'ESM.mjs :: value :: hello'); + console.log('DONE~!'); From e73c401b44215ae449aedced4518dcafa4200689 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 9 Oct 2021 13:44:09 -0700 Subject: [PATCH 2/5] chore(require): determine `options.banner` once per loader --- src/require.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/require.ts b/src/require.ts index 69275a9..657c8ae 100644 --- a/src/require.ts +++ b/src/require.ts @@ -51,23 +51,20 @@ const tsrequire = 'var $$req=require;require=(' + function () { } + ')();'; function transform(source: string, sourcefile: string, options: Options): string { - let banner = options.banner || ''; - if (/\.[mc]?tsx?$/.test(sourcefile)) { - banner = tsrequire + banner; - } - esbuild = esbuild || require('esbuild'); - return esbuild.transformSync(source, { - ...options, banner, sourcefile - }).code; + return esbuild.transformSync(source, { ...options, sourcefile }).code; } function loader(Module: Module, sourcefile: string) { - let pitch = Module._compile!.bind(Module); let extn = extname(sourcefile); - let options = config[extn]; + let options = config[extn] || {}; + let pitch = Module._compile!.bind(Module); + + if (/\.[mc]?tsx?$/.test(extn)) { + options.banner = tsrequire + (options.banner || ''); + } - if (options != null) { + if (config[extn] != null) { Module._compile = source => { let result = transform(source, sourcefile, options); return pitch(result, sourcefile); From 2fe31908c007932d34c252d740887399b4927150 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 9 Oct 2021 13:46:53 -0700 Subject: [PATCH 3/5] chore(require): determine `options.sourcefile` once per loader --- src/require.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/require.ts b/src/require.ts index 657c8ae..5d14426 100644 --- a/src/require.ts +++ b/src/require.ts @@ -50,15 +50,16 @@ const tsrequire = 'var $$req=require;require=(' + function () { }) } + ')();'; -function transform(source: string, sourcefile: string, options: Options): string { +function transform(source: string, options: Options): string { esbuild = esbuild || require('esbuild'); - return esbuild.transformSync(source, { ...options, sourcefile }).code; + return esbuild.transformSync(source, options).code; } function loader(Module: Module, sourcefile: string) { let extn = extname(sourcefile); let options = config[extn] || {}; let pitch = Module._compile!.bind(Module); + options.sourcefile = sourcefile; if (/\.[mc]?tsx?$/.test(extn)) { options.banner = tsrequire + (options.banner || ''); @@ -66,7 +67,7 @@ function loader(Module: Module, sourcefile: string) { if (config[extn] != null) { Module._compile = source => { - let result = transform(source, sourcefile, options); + let result = transform(source, options); return pitch(result, sourcefile); }; } @@ -78,10 +79,7 @@ function loader(Module: Module, sourcefile: string) { if (ec !== 'ERR_REQUIRE_ESM') throw err; let input = readFileSync(sourcefile, 'utf8'); - let result = transform(input, sourcefile, { - ...options, format: 'cjs' - }); - + let result = transform(input, { ...options, format: 'cjs' }); return pitch(result, sourcefile); } } From 0036c54e25ca55186a9ec4d543e0f48cf0809842 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 9 Oct 2021 14:19:03 -0700 Subject: [PATCH 4/5] fix(require): ensure no infinite `require` loop --- src/require.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/require.ts b/src/require.ts index 5d14426..bc931db 100644 --- a/src/require.ts +++ b/src/require.ts @@ -17,11 +17,11 @@ let uconf = env.file && require(env.file); let config: Config = (tsm as TSM).$finalize(env, uconf); declare const $$req: NodeJS.Require; -const tsrequire = 'var $$req=require;require=(' + function () { +const tsrequire = 'var $$req=require("module").createRequire(__filename);require=(' + function () { let { existsSync } = $$req('fs'); let { URL, pathToFileURL } = $$req('url'); - return new Proxy($$req, { + return new Proxy(require, { // NOTE: only here if source is TS apply(req, ctx, args: [id: string]) { let [ident] = args; From e033329f9d973d3a7b1b821cc324cf8cc43af53f Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 9 Oct 2021 14:34:36 -0700 Subject: [PATCH 5/5] chore: add typescript + "type: module" tests --- test/config/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/config/index.ts b/test/config/index.ts index c0d4f70..25696b7 100644 --- a/test/config/index.ts +++ b/test/config/index.ts @@ -6,6 +6,8 @@ import * as js from '../fixtures/math.js'; import * as mjs from '../fixtures/utils.mjs'; // @ts-ignore - cannot find types import * as cjs from '../fixtures/utils.cjs'; +// @ts-ignore - cannot find types +import * as esm from '../fixtures/module/index.js'; // NOTE: avoid need for syntheticDefault + analysis import * as data from '../fixtures/data.json'; @@ -31,4 +33,11 @@ assert.equal(typeof cjs, 'object', 'CJS :: typeof'); assert.equal(typeof cjs.dashify, 'function', 'CJS :: typeof :: dashify'); assert.equal(cjs.dashify('FooBar'), 'foo-bar', 'CJS :: value :: dashify'); +// Checking ".js" with ESM content (type: module) +assert.equal(typeof esm, 'object', 'ESM.js :: typeof'); +// @ts-ignore +assert.equal(typeof esm.hello, 'function', 'ESM.js :: typeof :: hello'); +// @ts-ignore +assert.equal(esm.hello('you'), 'hello, you', 'ESM.js :: value :: hello'); + console.log('DONE~!');