Skip to content

Commit

Permalink
feat: Allow JavaScript extensions for TypeScript imports (#2)
Browse files Browse the repository at this point in the history
* feat(loader): map js -> ts when source is ts

* feat(require): map js -> ts when source is ts

* fix(require): work after minify
  • Loading branch information
lukeed authored Oct 5, 2021
1 parent 4f936ad commit c935537
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 7 deletions.
43 changes: 38 additions & 5 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as url from 'url';
import { existsSync } from 'fs';
import * as tsm from './utils.js';

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

let config: Config;
Expand Down Expand Up @@ -43,26 +43,59 @@ async function load(): Promise<Config> {
}

const EXTN = /\.\w+(?=\?|$)/;
const isTS = /\.[mc]?tsx?(?=\?|$)/;
const isJS = /\.([mc])?js$/;
async function toOptions(uri: string): Promise<Options|void> {
config = config || await load();
let [extn] = EXTN.exec(uri) || [];
return config[extn as `.${string}`];
}

function check(fileurl: string): string | void {
let tmp = url.fileURLToPath(fileurl);
if (existsSync(tmp)) return fileurl;
}

const root = url.pathToFileURL(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.URL(ident, context.parentURL || root);
if (EXTN.test(output.pathname)) return { url: output.href };

// source ident includes extension
if (match = EXTN.exec(output.href)) {
ext = match[0] as Extension;
if (!context.parentURL || isTS.test(ext)) {
return { url: output.href };
}
// source ident exists
path = check(output.href);
if (path) return { url: path };
// 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);
}
return { url: path };
}
// return original, let it error
return fallback(ident, context, fallback);
}
}

config = config || await load();

let tmp, ext, path;
for (ext in config) {
path = url.fileURLToPath(tmp = output.href + ext);
if (existsSync(path)) return { url: tmp };
path = check(output.href + ext);
if (path) return { url: path };
}

return fallback(ident, context, fallback);
Expand Down
41 changes: 40 additions & 1 deletion src/require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ let env = (tsm as TSM).$defaults('cjs');
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 () {
let { existsSync } = $$req('fs');
let { URL, pathToFileURL } = $$req('url');

return new Proxy($$req, {
// NOTE: only here if source is TS
apply(req, ctx, args: [id: string]) {
let [ident] = args;
if (!ident) return req.apply(ctx || $$req, args);

// ignore "prefix:" and non-relative identifiers
if (/^\w+\:?/.test(ident)) return $$req(ident);

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

let base = pathToFileURL(__filename) as import('url').URL;
let file = new URL(ident, base).pathname as string;
if (existsSync(file)) return $$req(ident);

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

// return the new "[mc]ts" file, or let error
return existsSync(file) ? $$req(file) : $$req(ident);
}
})
} + ')();'

function loader(Module: Module, sourcefile: string) {
let extn = extname(sourcefile);
let pitch = Module._compile!.bind(Module);
Expand All @@ -23,8 +57,13 @@ function loader(Module: Module, sourcefile: string) {
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, sourcefile });
let result = esbuild.transformSync(source, { ...options, banner, sourcefile });
return pitch(result.code, sourcefile);
};

Expand Down
1 change: 0 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { Format } from 'esbuild';
import type * as tsm from 'tsm/config';
import type { Defaults } from './utils.d';


exports.$defaults = function (format: Format): Defaults {
let { FORCE_COLOR, NO_COLOR, NODE_DISABLE_COLORS, TERM } = process.env;

Expand Down
10 changes: 10 additions & 0 deletions test/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import * as assert from 'assert';

// NOTE: doesn't actually exist yet
import * as js from '../fixtures/math.js';

// 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');

console.log('DONE~!');

0 comments on commit c935537

Please sign in to comment.