Skip to content

Commit c879324

Browse files
committed
feat(esm): Generate esm sources and support import
1 parent ce5ce9e commit c879324

File tree

8 files changed

+118
-37
lines changed

8 files changed

+118
-37
lines changed

lib/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { TypeScriptLoader } from "./loader";
2-
export type { TypeScriptCompileError } from "./typescript-compile-error";
1+
export { TypeScriptLoader } from "./loader.js";
2+
export type { TypeScriptCompileError } from "./typescript-compile-error.js";

lib/loader.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import type { Loader } from "cosmiconfig";
22
import { register, RegisterOptions } from "ts-node";
33

4-
import { TypeScriptCompileError } from "./typescript-compile-error";
4+
import { TypeScriptCompileError } from "./typescript-compile-error.js";
55

66
export function TypeScriptLoader(options?: RegisterOptions): Loader {
77
const tsNodeInstance = register({
88
...options,
99
compilerOptions: { module: "commonjs" },
1010
});
11-
return (path: string, content: string) => {
11+
return async (path: string, content: string) => {
1212
try {
1313
// cosmiconfig requires the transpiled configuration to be CJS
1414
tsNodeInstance.compile(content, path);
15-
const result = require(path);
15+
let result;
16+
if (typeof require !== "undefined") {
17+
result = require(path);
18+
} else {
19+
result = await import(path);
20+
}
1621

1722
// `default` is used when exporting using export default, some modules
1823
// may still use `module.exports` or if in TS `export = `

package.json

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,21 @@
1515
"files": [
1616
"dist/**/*"
1717
],
18-
"main": "dist/cjs/index.js",
18+
"main": "dist/cjs/index.cjs",
19+
"module": "dist/esm/index.mjs",
1920
"types": "dist/types/index.d.ts",
21+
"exports": {
22+
".": {
23+
"import": {
24+
"types": "./dist/types/index.d.ts",
25+
"default": "./dist/esm/index.mjs"
26+
},
27+
"require": {
28+
"types": "./dist/types/index.d.ts",
29+
"default": "./dist/cjs/index.cjs"
30+
}
31+
}
32+
},
2033
"scripts": {
2134
"build": "npm run build:types && npm run build:bundles",
2235
"build:bundles": "node ./scripts/esbuild.config.mjs",

scripts/esbuild.config.mjs

+88-26
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,73 @@
1+
// @ts-check
2+
import fs from "node:fs/promises";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
16
import chalk from "chalk";
27
import { analyzeMetafile, build } from "esbuild";
3-
import { readFile } from "fs/promises";
4-
import path from "path";
5-
import { fileURLToPath } from "url";
6-
7-
const nodeTargets = ["node14", "node16", "node18"];
88

99
const __dirname = path.dirname(fileURLToPath(import.meta.url));
10-
const packageJsonPath = path.resolve(__dirname, "..", "package.json");
1110

12-
async function getExternals() {
13-
const packageJson = JSON.parse(await readFile(packageJsonPath));
11+
/**
12+
* Available formats: https://esbuild.github.io/api/#format
13+
*
14+
* @type {Array<"cjs" | "esm">}
15+
*/
16+
const FORMATS = ["cjs", "esm"];
17+
/**
18+
* @type {import('esbuild').BuildOptions['target']}
19+
*/
20+
const TARGETS = ["node14", "node16", "node18", "node20", "esnext"];
21+
22+
const ROOT = path.resolve(__dirname, "..");
23+
const SOURCES_ROOT = path.resolve(ROOT, "./lib");
24+
25+
/**
26+
* @param {"cjs" | "esm"} format
27+
* @return {import('esbuild').Plugin}
28+
*/
29+
function addExtension(format) {
30+
const newExtension = format === "cjs" ? ".cjs" : ".mjs";
31+
return {
32+
name: "add-extension",
33+
setup(build) {
34+
build.onResolve({ filter: /.*/ }, (args) => {
35+
if (args.importer)
36+
return {
37+
path: args.path.replace(/\.js$/, newExtension),
38+
external: true,
39+
};
40+
});
41+
},
42+
};
43+
}
44+
45+
/**
46+
* @param {string[]} entryPoints
47+
* @param {"cjs" | "esm"} format
48+
*/
49+
async function buildSources(entryPoints, format) {
50+
return build({
51+
entryPoints: entryPoints,
52+
outdir: `${ROOT}/dist/${format}`,
53+
target: TARGETS,
54+
platform: "node",
55+
format: format,
56+
metafile: true,
1457

15-
return Object.keys(packageJson.peerDependencies);
58+
// These allow us to build compliant exports and imports based on modern node
59+
bundle: true,
60+
outExtension: { ".js": format === "cjs" ? ".cjs" : ".mjs" },
61+
plugins: [addExtension(format)],
62+
});
63+
}
64+
65+
async function getSourceEntryPoints() {
66+
const entryPoints = await fs.readdir(SOURCES_ROOT);
67+
68+
return entryPoints
69+
.filter((file) => !/__fixtures__|\.spec\./.test(file))
70+
.map((file) => path.resolve(SOURCES_ROOT, file));
1671
}
1772

1873
(async () => {
@@ -24,27 +79,34 @@ async function getExternals() {
2479
)
2580
);
2681

27-
const externalDeps = await getExternals();
82+
console.info("- Generate sources");
2883

29-
const result = await build({
30-
entryPoints: ["./lib/index.ts"],
31-
outfile: "dist/cjs/index.js",
32-
metafile: true,
33-
bundle: true,
34-
format: "cjs",
35-
external: externalDeps,
36-
platform: "node",
37-
target: nodeTargets,
38-
treeShaking: true,
39-
});
84+
const sourceEntryPoints = await getSourceEntryPoints();
4085

41-
const analysis = await analyzeMetafile(result.metafile);
42-
console.info(`📝 Bundle Analysis:${analysis}`);
86+
for (const format of FORMATS) {
87+
console.info(`- Generating ${chalk.bold.greenBright(format)} sources`);
88+
89+
const result = await buildSources(sourceEntryPoints, format);
90+
const analysis = await analyzeMetafile(result.metafile);
91+
console.info(
92+
`${analysis
93+
.trim()
94+
.split(/\n\r/)
95+
.map((line) => ` ${line}`)
96+
.join()}`
97+
);
98+
99+
console.info(
100+
`${chalk.bold.greenBright("✔")} Generating ${chalk.bold.greenBright(
101+
format
102+
)} sources completed!\n`
103+
);
104+
}
43105

44106
console.info(
45-
`${chalk.bold.green("✔ Bundled successfully!")} (${
46-
Date.now() - startTime
47-
}ms)`
107+
chalk.bold.green(
108+
`✔ Generate sources completed! (${Date.now() - startTime}ms)`
109+
)
48110
);
49111
} catch (error) {
50112
console.error(`🧨 ${chalk.red.bold("Failed:")} ${error.message}`);
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { TypeScriptLoader } = require("../dist/cjs/index.js");
1+
const { TypeScriptLoader } = require("../dist/cjs/index.cjs");
22
TypeScriptLoader();
33

44
console.info("Loaded with CJS successfully");

smoke-tests/smoke-test-esm.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TypeScriptLoader } from "../dist/cjs/index.js";
1+
import { TypeScriptLoader } from "../dist/esm/index.mjs";
22
TypeScriptLoader();
33

44
console.info("Loaded with ESM successfully");

smoke-tests/smoke-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ const assert = require("node:assert");
22

33
(async () => {
44
try {
5-
const { TypeScriptLoader: esm } = await import("../dist/cjs/index.js");
6-
const { TypeScriptLoader: cjs } = require("../dist/cjs/index.js");
5+
const { TypeScriptLoader: esm } = await import("../dist/esm/index.mjs");
6+
const { TypeScriptLoader: cjs } = require("../dist/cjs/index.cjs");
77

88
assert.strictEqual(esm, cjs, "esm === cjs");
99

tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"compilerOptions": {
3-
"target": "es2016",
3+
"module": "esnext",
4+
"target": "es2020",
45
"noEmit": true,
56
"pretty": true,
67
"sourceMap": true,

0 commit comments

Comments
 (0)