Skip to content

feat: import() in CommonJS & resolve virtual file extensions #43

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

Merged
merged 4 commits into from
Nov 28, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"devDependencies": {
"@types/node": "16.11.6",
"@types/react": "17.0.33",
"typescript": "4.4.4"
"typescript": "4.9.3"
},
"keywords": [
"esm",
Expand Down
64 changes: 42 additions & 22 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Resolve = (
fallback: Resolve
) => Promisable<{
url: string;
shortCircuit: boolean;
format?: Format;
}>;

Expand All @@ -45,6 +46,7 @@ type Load = (
fallback: Load
) => Promisable<{
format: Format;
shortCircuit: boolean;
source: Source;
}>;

Expand All @@ -56,7 +58,7 @@ async function toConfig(): Promise<Config> {

const EXTN = /\.\w+(?=\?|$)/;
const isTS = /\.[mc]?tsx?(?=\?|$)/;
const isJS = /\.([mc])?js$/;

async function toOptions(uri: string): Promise<Options|void> {
config = config || await toConfig();
let [extn] = EXTN.exec(uri) || [];
Expand All @@ -68,45 +70,63 @@ function check(fileurl: string): string | void {
if (existsSync(tmp)) return fileurl;
}

/**
* extension aliases; runs after checking for extn on disk
* @example `import('./foo.mjs')` but only `foo.mts` exists
*/
const MAPs: Record<Extension, Extension[]> = {
'.js': ['.ts', '.tsx', '.jsx'],
'.jsx': ['.tsx'],
'.mjs': ['.mts'],
'.cjs': ['.cts'],
};

const root = new URL('file:///' + process.cwd() + '/');
export const resolve: Resolve = async function (ident, context, fallback) {
// ignore "prefix:" and non-relative identifiers
if (/^\w+\:?/.test(ident)) return fallback(ident, context, fallback);

let match: RegExpExecArray | null;
let idx: number, ext: Extension, path: string | void;
let output = new URL(ident, context.parentURL || root);
let target = new URL(ident, context.parentURL || root);
let ext: Extension, path: string | void, arr: Extension[];
let match: RegExpExecArray | null, i=0, base: string;

// source ident includes extension
if (match = EXTN.exec(output.href)) {
if (match = EXTN.exec(target.href)) {
ext = match[0] as Extension;
if (!context.parentURL || isTS.test(ext)) {
return { url: output.href, shortCircuit: true };
return { url: target.href, shortCircuit: true };
}
// source ident exists
path = check(output.href);
if (path) return { url: path, shortCircuit: true };
// parent importer is a ts file
// source ident is js & NOT exists
if (isJS.test(ext) && isTS.test(context.parentURL)) {
// reconstruct ".js" -> ".ts" source file
path = output.href.substring(0, idx = match.index);
if (path = check(path + ext.replace('js', 'ts'))) {
idx += ext.length;
if (idx > output.href.length) {
path += output.href.substring(idx);

// target ident exists
if (path = check(target.href)) {
return { url: path, shortCircuit: true };
}

// target is virtual alias
if (arr = MAPs[ext]) {
base = target.href.substring(0, match.index);
for (; i < arr.length; i++) {
if (path = check(base + arr[i])) {
i = match.index + ext.length;
return {
shortCircuit: true,
url: i > target.href.length
// handle target `?args` trailer
? base + target.href.substring(i)
: path
};
}
return { url: path, shortCircuit: true };
}
// return original, let it error
return fallback(ident, context, fallback);
}

// return original behavior, let it error
return fallback(ident, context, fallback);
}

config = config || await toConfig();

for (ext in config) {
path = check(output.href + ext);
path = check(target.href + ext);
if (path) return { url: path, shortCircuit: true };
}

Expand Down
38 changes: 26 additions & 12 deletions src/require.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const { extname } = require('path');
const { readFileSync } = require('fs');
const { extname } = require('path');
const tsm = require('./utils');

import type { Config, Options } from 'tsm/config';
import type { Config, Extension, Options } from 'tsm/config';
type TSM = typeof import('./utils.d');

type Module = NodeJS.Module & {
Expand Down Expand Up @@ -31,21 +31,31 @@ const tsrequire = 'var $$req=require("module").createRequire(__filename);require
if (/^\w+\:?/.test(ident)) return $$req(ident);

// exit early if no extension provided
let match = /\.([mc])?js(?=\?|$)/.exec(ident);
let match = /\.([mc])?[tj]sx?(?=\?|$)/.exec(ident);
if (match == null) return $$req(ident);

let base = $url.pathToFileURL(__filename);
let file = $url.fileURLToPath(new $url.URL(ident, base));
if (existsSync(file)) return $$req(ident);

// ?js -> ?ts file
file = file.replace(
new RegExp(match[0] + '$'),
match[0].replace('js', 'ts')
);
let extn = match[0] as Extension;
let rgx = new RegExp(extn + '$');

// [cm]?jsx? -> [cm]?tsx?
let tmp = file.replace(rgx, extn.replace('js', 'ts'));
if (existsSync(tmp)) return $$req(tmp);

// return the new "[mc]ts" file, or let error
return existsSync(file) ? $$req(file) : $$req(ident);
// look for ".[tj]sx" if ".js" given & still here
if (extn === '.js') {
tmp = file.replace(rgx, '.tsx');
if (existsSync(tmp)) return $$req(tmp);

tmp = file.replace(rgx, '.jsx');
if (existsSync(tmp)) return $$req(tmp);
}

// let it error
return $$req(ident);
}
})
} + ')();';
Expand All @@ -56,13 +66,17 @@ function transform(source: string, options: Options): string {
}

function loader(Module: Module, sourcefile: string) {
let extn = extname(sourcefile);
let extn = extname(sourcefile) as Extension;

let options = config[extn] || {};
let pitch = Module._compile!.bind(Module);
options.sourcefile = sourcefile;

if (/\.[mc]?tsx?$/.test(extn)) {
if (/\.[mc]?[tj]sx?$/.test(extn)) {
options.banner = tsrequire + (options.banner || '');
// https://github.com/lukeed/tsm/issues/27
options.supported = options.supported || {};
options.supported['dynamic-import'] = false;
}

if (config[extn] != null) {
Expand Down
93 changes: 61 additions & 32 deletions test/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,69 @@ import * as assert from 'assert';

// NOTE: doesn't actually exist yet
import * as js from '../fixtures/math.js';
// @ts-ignore - cannot find types
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';

import * as esm1 from '../fixtures/module/index.js';
import * as esm2 from '../fixtures/module/index.mjs';

// NOTE: avoid need for syntheticDefault + analysis
import * as data from '../fixtures/data.json';
assert.equal(typeof data, 'object');

// @ts-ignore - generally doesn't exist
assert.equal(typeof data.default, 'string');

// NOTE: raw JS missing
assert.equal(typeof js, 'object', 'JS :: typeof');
assert.equal(typeof js.sum, 'function', 'JS :: typeof :: sum');
assert.equal(typeof js.div, 'function', 'JS :: typeof :: div');
assert.equal(typeof js.mul, 'function', 'JS :: typeof :: mul');
assert.equal(js.foobar, 3, 'JS :: value :: foobar');

// NOTE: raw MJS missing
assert.equal(typeof mjs, 'object', 'MJS :: typeof');
assert.equal(typeof mjs.capitalize, 'function', 'MJS :: typeof :: capitalize');
assert.equal(mjs.capitalize('hello'), 'Hello', 'MJS :: value :: capitalize');

// NOTE: raw CJS missing
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');
assert.equal(typeof esm.hello, 'function', 'ESM.js :: typeof :: hello');
assert.equal(esm.hello('you'), 'hello, you', 'ESM.js :: value :: hello');

console.log('DONE~!');

// NOTE: for CJS test runner
(async function () {
assert.equal(typeof data, 'object');

// @ts-ignore - generally doesn't exist
assert.equal(typeof data.default, 'string');

// NOTE: raw JS missing
assert.equal(typeof js, 'object', 'JS :: typeof');
assert.equal(typeof js.sum, 'function', 'JS :: typeof :: sum');
assert.equal(typeof js.div, 'function', 'JS :: typeof :: div');
assert.equal(typeof js.mul, 'function', 'JS :: typeof :: mul');
assert.equal(js.foobar, 3, 'JS :: value :: foobar');

// DYANMIC IMPORTS via TS file
assert.equal(typeof js.dynamic, 'object', 'JS :: typeof :: dynamic');
assert.equal(await js.dynamic.cjs(), 'foo-bar', 'JS :: dynamic :: import(cjs)');
assert.equal(await js.dynamic.cts(), 'foo-bar', 'JS :: dynamic :: import(cts)');
assert.equal(await js.dynamic.mjs(), 'Hello', 'JS :: dynamic :: import(mjs)');
assert.equal(await js.dynamic.mts(), 'Hello', 'JS :: dynamic :: import(mts)');

// NOTE: raw MJS missing
assert.equal(typeof mjs, 'object', 'MJS :: typeof');
assert.equal(typeof mjs.capitalize, 'function', 'MJS :: typeof :: capitalize');
assert.equal(mjs.capitalize('hello'), 'Hello', 'MJS :: value :: capitalize');

// NOTE: raw CJS missing
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 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');

// DYANMIC IMPORTS via JS file
assert.equal(typeof esm1.dynamic, 'object', 'ESM.js :: typeof :: dynamic');
assert.equal(await esm1.dynamic.cjs(), 'foo-bar', 'ESM.js :: dynamic :: import(cjs)');
assert.equal(await esm1.dynamic.cts(), 'foo-bar', 'ESM.js :: dynamic :: import(cts)');
assert.equal(await esm1.dynamic.mjs(), 'Hello', 'ESM.js :: dynamic :: import(mjs)');
assert.equal(await esm1.dynamic.mts(), 'Hello', 'ESM.js :: dynamic :: import(mts)');

// Checking ".mjs" with ESM content
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');

// DYANMIC IMPORTS via MJS file
assert.equal(typeof esm2.dynamic, 'object', 'ESM.mjs :: typeof :: dynamic');
assert.equal(await esm2.dynamic.cjs(), 'foo-bar', 'ESM.mjs :: dynamic :: import(cjs)');
assert.equal(await esm2.dynamic.cts(), 'foo-bar', 'ESM.mjs :: dynamic :: import(cts)');
assert.equal(await esm2.dynamic.mjs(), 'Hello', 'ESM.mjs :: dynamic :: import(mjs)');
assert.equal(await esm2.dynamic.mts(), 'Hello', 'ESM.mjs :: dynamic :: import(mts)');

console.log('DONE~!');
})();
23 changes: 23 additions & 0 deletions test/fixtures/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,26 @@ export const div = (a: number, b: number) => a / b;
export const mul = (a: number, b: number) => a * b;

export const foobar = sum(1, 2);

export const dynamic = {
async cjs() {
// @ts-ignore – tsc cant find type defs
let m = await import('./utils.cjs');
return m.dashify('FooBar');
},
async cts() {
// @ts-ignore – tsc doesnt like
let m = await import('./utils.cts');
return m.dashify('FooBar');
},
async mjs() {
// @ts-ignore – tsc cant find type defs
let m = await import('./utils.mjs');
return m.capitalize('hello');
},
async mts() {
// @ts-ignore – tsc doesnt like
let m = await import('./utils.mts');
return m.capitalize('hello');
},
}
21 changes: 21 additions & 0 deletions test/fixtures/module/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,24 @@
export function hello(name) {
return `hello, ${name}`;
}

export const dynamic = {
async cjs() {
let m = await import('../utils.cjs');
return m.dashify('FooBar');
},
async cts() {
// @ts-ignore – tsc doesnt like
let m = await import('../utils.cts');
return m.dashify('FooBar');
},
async mjs() {
let m = await import('../utils.mjs');
return m.capitalize('hello');
},
async mts() {
// @ts-ignore – tsc doesnt like
let m = await import('../utils.mts');
return m.capitalize('hello');
},
}
21 changes: 21 additions & 0 deletions test/fixtures/module/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,24 @@
export function hello(name) {
return `hello, ${name}`;
}

export const dynamic = {
async cjs() {
let m = await import('../utils.cjs');
return m.dashify('FooBar');
},
async cts() {
// @ts-ignore – tsc doesnt like
let m = await import('../utils.cts');
return m.dashify('FooBar');
},
async mjs() {
let m = await import('../utils.mjs');
return m.capitalize('hello');
},
async mts() {
// @ts-ignore – tsc doesnt like
let m = await import('../utils.mts');
return m.capitalize('hello');
},
}
Loading