Skip to content

Add ESM support and change to jiti #106

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 11 commits into from
Jul 25, 2023
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [16.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
Expand All @@ -31,7 +31,7 @@ jobs:
run: npm run test
- name: Import with CJS
if: ${{ always() }}
run: node smoke-tests/smoke-test-cjs.js
run: node smoke-tests/smoke-test-cjs.cjs
- name: Import with ESM
if: ${{ always() }}
run: node smoke-tests/smoke-test-esm.mjs
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jobs:
env:
CI: true
GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }}
NPM_OTP_TOKEN: ${{ github.event.inputs.otp }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
runs-on: ubuntu-latest
steps:
Expand Down
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
.idea
.npmrc
.vscode

node_modules

coverage
dist
node_modules

.npmrc
54 changes: 0 additions & 54 deletions esbuild.config.mjs

This file was deleted.

3 changes: 3 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const config: Config.InitialOptions = {
"!<rootDir>/lib/__fixtures__/**/*",
],
moduleFileExtensions: ["ts", "js"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
transform: {
"^.+\\.ts$": "@swc/jest",
},
Expand Down
78 changes: 56 additions & 22 deletions lib/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,68 @@ describe("TypeScriptLoader", () => {
});

describe("cosmiconfig", () => {
it("should load a valid TS file", async () => {
const cfg = cosmiconfig("test", {
loaders: {
".ts": TypeScriptLoader(),
},
describe("synchronous", () => {
it("should load a valid TS file", () => {
const cfg = cosmiconfigSync("test", {
loaders: {
".ts": TypeScriptLoader(),
},
});
const loadedCfg = cfg.load(
path.resolve(fixturesPath, "valid.fixture.ts")
);

expect(typeof loadedCfg!.config).toStrictEqual("object");
expect(typeof loadedCfg!.config.test).toStrictEqual("object");
expect(loadedCfg!.config.test.cake).toStrictEqual("a lie");
});
const loadedCfg = await cfg.load(
path.resolve(fixturesPath, "valid.fixture.ts")
);

expect(typeof loadedCfg!.config).toStrictEqual("object");
expect(typeof loadedCfg!.config.test).toStrictEqual("object");
expect(loadedCfg!.config.test.cake).toStrictEqual("a lie");
it("should throw an error on loading an invalid TS file", () => {
const cfg = cosmiconfigSync("test", {
loaders: {
".ts": TypeScriptLoader(),
},
});

try {
cfg.load(path.resolve(fixturesPath, "invalid.fixture.ts"));
fail("Should fail to load invalid TS");
} catch (error: any) {
expect(error?.name).toStrictEqual("TypeScriptCompileError");
}
});
});

it("should throw an error on loading an invalid TS file", async () => {
const cfg = cosmiconfig("test", {
loaders: {
".ts": TypeScriptLoader(),
},
describe("asynchronous", () => {
it("should load a valid TS file", async () => {
const cfg = cosmiconfig("test", {
loaders: {
".ts": TypeScriptLoader(),
},
});
const loadedCfg = await cfg.load(
path.resolve(fixturesPath, "valid.fixture.ts")
);

expect(typeof loadedCfg!.config).toStrictEqual("object");
expect(typeof loadedCfg!.config.test).toStrictEqual("object");
expect(loadedCfg!.config.test.cake).toStrictEqual("a lie");
});

try {
await cfg.load(path.resolve(fixturesPath, "invalid.fixture.ts"));
fail("Should fail to load invalid TS");
} catch (error: any) {
expect(error?.name).toStrictEqual("TypeScriptCompileError");
}
it("should throw an error on loading an invalid TS file", async () => {
const cfg = cosmiconfig("test", {
loaders: {
".ts": TypeScriptLoader(),
},
});

try {
await cfg.load(path.resolve(fixturesPath, "invalid.fixture.ts"));
fail("Should fail to load invalid TS");
} catch (error: any) {
expect(error?.name).toStrictEqual("TypeScriptCompileError");
}
});
});
});

Expand Down
4 changes: 2 additions & 2 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { TypeScriptLoader } from "./loader";
export type { TypeScriptCompileError } from "./typescript-compile-error";
export { TypeScriptLoader } from "./loader.js";
export type { TypeScriptCompileError } from "./typescript-compile-error.js";
35 changes: 20 additions & 15 deletions lib/loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ import fs from "node:fs";
import path from "node:path";

import { Loader } from "cosmiconfig";
import * as tsnode from "ts-node";
import * as jiti from "jiti";

import { TypeScriptLoader } from "./loader";
import { TypeScriptCompileError } from "./typescript-compile-error";

// Handle jiti using `export default`
jest.mock("jiti", () => {
const actual = jest.requireActual("jiti");
return {
__esModule: true,
default: jest.fn(actual),
};
});

describe("TypeScriptLoader", () => {
const fixturesPath = path.resolve(__dirname, "__fixtures__");
const tsNodeSpy = jest.spyOn(tsnode, "register");
const jitiSpy = jest.spyOn(jiti, "default");

let loader: Loader;

Expand All @@ -31,11 +40,11 @@ describe("TypeScriptLoader", () => {
expect(() => loader(filePath, readFixtureContent(filePath))).toThrowError();
});

it("should use the same instance of ts-node across multiple calls", () => {
it("should use the same instance of jiti across multiple calls", () => {
const filePath = path.resolve(fixturesPath, "valid.fixture.ts");
loader(filePath, readFixtureContent(filePath));
loader(filePath, readFixtureContent(filePath));
expect(tsNodeSpy).toHaveBeenCalledTimes(1);
expect(jitiSpy).toHaveBeenCalledTimes(1);
});

it("should throw a TypeScriptCompileError on error", () => {
Expand All @@ -50,21 +59,17 @@ describe("TypeScriptLoader", () => {
}
});

describe("ts-node", () => {
describe("jiti", () => {
const unknownError = "Test Error";

let stub: jest.SpyInstance<tsnode.Service, [service: tsnode.Service]>;
let stub: jest.SpyInstance;

beforeEach(() => {
stub = jest.spyOn(tsnode, "register").mockImplementation(
() =>
({
compile: (): string => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw unknownError;
},
} as any)
);
stub = jest.spyOn(jiti, "default").mockImplementation((() => () => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw unknownError;
}) as any);

loader = TypeScriptLoader();
});

Expand Down
17 changes: 6 additions & 11 deletions lib/loader.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import type { Loader } from "cosmiconfig";
import { register, RegisterOptions } from "ts-node";
import jiti, { type JITIOptions } from "jiti";

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

export function TypeScriptLoader(options?: RegisterOptions): Loader {
const tsNodeInstance = register({
...options,
compilerOptions: { module: "commonjs" },
});
return (path: string, content: string) => {
export function TypeScriptLoader(options?: JITIOptions): Loader {
const loader = jiti("", { interopDefault: true, ...options });
return (path: string) => {
try {
// cosmiconfig requires the transpiled configuration to be CJS
tsNodeInstance.compile(content, path);
const result = require(path);
const result = loader(path);

// `default` is used when exporting using export default, some modules
// may still use `module.exports` or if in TS `export = `
Expand Down
17 changes: 5 additions & 12 deletions lib/typescript-compile-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,14 @@ describe("TypeScriptCompileError", () => {
expect(tscError.stack).toBe(testError.stack);
});

it("should replace the legacy tsc error string", () => {
const testMsg =
"TypeScript compiler encountered syntax errors while transpiling. Errors: ";
it("should prefix the jiti parser error", () => {
const testMsg = 'ParseError: Unexpected token, expected ","';
const legacyError = new Error(testMsg);
const tscError = TypeScriptCompileError.fromError(legacyError);

expect(tscError).not.toContainEqual(testMsg);
});

it("should replace the tsc error string", () => {
const testMsg = "⨯ Unable to compile TypeScript:";
const newError = new Error(testMsg);
const tscError = TypeScriptCompileError.fromError(newError);

expect(tscError).not.toContainEqual(testMsg);
expect(tscError.message).toContain(
"TypeScriptLoader failed to compile TypeScript:"
);
});
});
});
7 changes: 1 addition & 6 deletions lib/typescript-compile-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ export class TypeScriptCompileError extends Error {
}

public static fromError(error: Error): TypeScriptCompileError {
const errMsg = error.message.replace(
/(TypeScript compiler encountered syntax errors while transpiling\. Errors:\s?)|(⨯ Unable to compile TypeScript:\s?)/,
""
);

const message = `TypeScriptLoader failed to compile TypeScript:\n${errMsg}`;
const message = `TypeScriptLoader failed to compile TypeScript:\n${error.message}`;

const newError = new TypeScriptCompileError(message);
newError.stack = error.stack;
Expand Down
Loading