Skip to content

Upgrade to Jiti 2 #149

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 6 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ jobs:
- name: Import with both CJS and ESM
if: ${{ always() }}
run: node smoke-tests/smoke-test.js
- name: Import synchronously with CJS
if: ${{ always() }}
run: node smoke-tests/smoke-test-cjs-sync.cjs
- name: Import synchronously with ESM
if: ${{ always() }}
run: node smoke-tests/smoke-test-esm-sync.mjs
- name: Import synchronously with both CJS and ESM
if: ${{ always() }}
run: node smoke-tests/smoke-test-sync.js
- name: lint
if: ${{ always() }}
run: pnpm lint
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

## Usage

Simply add `TypeScriptLoader` to the list of loaders for the `.ts` file type:
Simply add `TypeScriptLoader` to the list of loaders for the `.ts` file type, and `await` loading:

```ts
import { cosmiconfig } from "cosmiconfig";
Expand All @@ -34,7 +34,7 @@ const explorer = cosmiconfig("test", {
},
});

const cfg = explorer.load("./");
const cfg = await explorer.load("./");
```

Or more simply if you only support loading of a TypeScript based configuration file:
Expand All @@ -50,6 +50,24 @@ const explorer = cosmiconfig("test", {
},
});

const cfg = await explorer.load("./amazing.config.ts");
```

### Synchronously loading

With the release of Jiti 2, the synchronous loader has now been deprecated. It can still be used by using the `TypeScriptLoaderSync` export:

```ts
import { cosmiconfig } from "cosmiconfig";
import { TypeScriptLoaderSync } from "cosmiconfig-typescript-loader";

const moduleName = "module";
const explorer = cosmiconfig("test", {
loaders: {
".ts": TypeScriptLoaderSync(),
},
});

const cfg = explorer.load("./amazing.config.ts");
```

Expand Down
38 changes: 6 additions & 32 deletions lib/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/* eslint-disable @typescript-eslint/no-deprecated */

import path from "node:path";

import { cosmiconfig, cosmiconfigSync } from "cosmiconfig";

import { TypeScriptLoader } from ".";
import { TypeScriptLoader, TypeScriptLoaderSync } from ".";

describe("TypeScriptLoader", () => {
const fixturesPath = path.resolve(__dirname, "__fixtures__");

describe("exports", () => {
it("should export the loader function as a default", () => {
expect(typeof TypeScriptLoader).toStrictEqual("function");
expect(typeof TypeScriptLoaderSync).toStrictEqual("function");
});
});

Expand All @@ -18,7 +21,7 @@ describe("TypeScriptLoader", () => {
it("should load a valid TS file", () => {
const cfg = cosmiconfigSync("test", {
loaders: {
".ts": TypeScriptLoader(),
".ts": TypeScriptLoaderSync(),
},
});
const loadedCfg = cfg.load(
Expand All @@ -33,7 +36,7 @@ describe("TypeScriptLoader", () => {
it("should throw an error on loading an invalid TS file", () => {
const cfg = cosmiconfigSync("test", {
loaders: {
".ts": TypeScriptLoader(),
".ts": TypeScriptLoaderSync(),
},
});

Expand Down Expand Up @@ -80,33 +83,4 @@ describe("TypeScriptLoader", () => {
});
});
});

describe("cosmiconfigSync", () => {
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");
});

it("should throw an error on loading an invalid TS file", () => {
const cfg = cosmiconfigSync("test", {
loaders: {
".ts": TypeScriptLoader(),
},
});

expect(() =>
cfg.load(path.resolve(fixturesPath, "invalid.fixture.ts")),
).toThrow();
});
});
});
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { TypeScriptLoader } from "./loader.js";
export { TypeScriptLoader, TypeScriptLoaderSync } from "./loader.js";
export type { TypeScriptCompileError } from "./typescript-compile-error.js";
168 changes: 115 additions & 53 deletions lib/loader.spec.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,156 @@
import fs from "node:fs";
import path from "node:path";

import { Loader } from "cosmiconfig";
import type { LoaderResult, LoaderSync } from "cosmiconfig";
import * as jiti from "jiti";

import { TypeScriptLoader } from "./loader";
import { TypeScriptLoader, TypeScriptLoaderSync } 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),
createJiti: actual.createJiti,
};
});

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

let loader: Loader;
let jitiCreateJitiSpy: jest.SpyInstance<typeof jiti.createJiti>;

function readFixtureContent(file: string): string {
return fs.readFileSync(file).toString();
}

beforeAll(() => {
loader = TypeScriptLoader();
beforeEach(() => {
jitiCreateJitiSpy = jest.spyOn(jiti, "createJiti");
});

it("should parse a valid TS file", () => {
const filePath = path.resolve(fixturesPath, "valid.fixture.ts");
loader(filePath, readFixtureContent(filePath));
afterEach(() => {
jest.restoreAllMocks();
});

it("should fail on parsing an invalid TS file", () => {
const filePath = path.resolve(fixturesPath, "invalid.fixture.ts");
expect((): unknown =>
loader(filePath, readFixtureContent(filePath)),
).toThrow();
});
describe("asynchronous", () => {
let loader: (filepath: string, content: string) => Promise<LoaderResult>;

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(jitiSpy).toHaveBeenCalledTimes(1);
});
beforeEach(() => {
loader = TypeScriptLoader();
});

it("should parse a valid TS file", async () => {
const filePath = path.resolve(fixturesPath, "valid.fixture.ts");
await loader(filePath, readFixtureContent(filePath));
});

it("should throw a TypeScriptCompileError on error", () => {
try {
it("should fail on parsing an invalid TS file", async () => {
const filePath = path.resolve(fixturesPath, "invalid.fixture.ts");
loader(filePath, readFixtureContent(filePath));
fail(
"Error should be thrown upon failing to transpile an invalid TS file.",
);
} catch (error: unknown) {
expect(error).toBeInstanceOf(TypeScriptCompileError);
}
});
await expect(
loader(filePath, readFixtureContent(filePath)),
).rejects.toThrow();
});

describe("jiti", () => {
const unknownError = "Test Error";
it("should use the same instance of jiti across multiple calls", async () => {
const filePath = path.resolve(fixturesPath, "valid.fixture.ts");
await loader(filePath, readFixtureContent(filePath));
await loader(filePath, readFixtureContent(filePath));
expect(jitiCreateJitiSpy).toHaveBeenCalledTimes(1);
});

let stub: jest.SpyInstance;
it("should throw a TypeScriptCompileError on error", async () => {
const filePath = path.resolve(fixturesPath, "invalid.fixture.ts");
await expect(
loader(filePath, readFixtureContent(filePath)),
).rejects.toThrow(TypeScriptCompileError);
});

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

beforeEach(() => {
jitiCreateJitiSpy.mockImplementation((() => ({
import: () => {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw unknownError;
},
})) as never);

loader = TypeScriptLoader();
});

it("rethrows an error if it is not an instance of Error", async () => {
try {
await loader("filePath", "invalidInput");
fail(
"Error should be thrown upon failing to transpile an invalid TS file.",
);
} catch (error: unknown) {
expect(error).not.toBeInstanceOf(TypeScriptCompileError);
expect(error).toStrictEqual(unknownError);
}
});
});
});

describe("synchronous", () => {
let loader: LoaderSync;

beforeEach(() => {
stub = jest.spyOn(jiti, "default").mockImplementation((() => () => {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw unknownError;
}) as never);
// eslint-disable-next-line @typescript-eslint/no-deprecated
loader = TypeScriptLoaderSync();
});

loader = TypeScriptLoader();
it("should parse a valid TS file", () => {
const filePath = path.resolve(fixturesPath, "valid.fixture.ts");
loader(filePath, readFixtureContent(filePath));
});

it("should fail on parsing an invalid TS file", () => {
const filePath = path.resolve(fixturesPath, "invalid.fixture.ts");
expect((): unknown =>
loader(filePath, readFixtureContent(filePath)),
).toThrow();
});

afterEach(() => {
stub.mockRestore();
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(jitiCreateJitiSpy).toHaveBeenCalledTimes(1);
});

it("should throw a TypeScriptCompileError on error", () => {
const filePath = path.resolve(fixturesPath, "invalid.fixture.ts");
expect((): unknown =>
loader(filePath, readFixtureContent(filePath)),
).toThrow(TypeScriptCompileError);
});

it("rethrows an error if it is not an instance of Error", () => {
try {
loader("filePath", "readFixtureContent(filePath)");
fail(
"Error should be thrown upon failing to transpile an invalid TS file.",
);
} catch (error: unknown) {
expect(error).not.toBeInstanceOf(TypeScriptCompileError);
expect(error).toStrictEqual(unknownError);
}
describe("jiti", () => {
const unknownError = "Test Error";

beforeEach(() => {
jitiCreateJitiSpy.mockImplementation((() => () => {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw unknownError;
}) as never);

// eslint-disable-next-line @typescript-eslint/no-deprecated
loader = TypeScriptLoaderSync();
});

it("rethrows an error if it is not an instance of Error", () => {
try {
loader("filePath", "invalidInput");
fail(
"Error should be thrown upon failing to transpile an invalid TS file.",
);
} catch (error: unknown) {
expect(error).not.toBeInstanceOf(TypeScriptCompileError);
expect(error).toStrictEqual(unknownError);
}
});
});
});
});
40 changes: 33 additions & 7 deletions lib/loader.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
import type { Loader } from "cosmiconfig";
import jiti, { type JITIOptions } from "jiti";
import type { LoaderResult, LoaderSync } from "cosmiconfig";
import { createJiti } from "jiti";

type Jiti = ReturnType<typeof createJiti>;
type JitiOptions = Parameters<typeof createJiti>[1];
import { TypeScriptCompileError } from "./typescript-compile-error.js";

export function TypeScriptLoader(options?: JITIOptions): Loader {
const loader = jiti("", { interopDefault: true, ...options });
return (path: string): unknown => {
type LoaderAsync = (filepath: string, content: string) => Promise<LoaderResult>;

export function TypeScriptLoader(options?: JitiOptions): LoaderAsync {
const loader: Jiti = createJiti("", { interopDefault: true, ...options });
return async (path: string, _content: string): Promise<LoaderResult> => {
try {
const result = (await loader.import(path)) as { default?: unknown };

// `default` is used when exporting using export default, some modules
// may still use `module.exports` or if in TS `export = `
return result.default || result;
} catch (error) {
if (error instanceof Error) {
// Coerce generic error instance into typed error with better logging.
throw TypeScriptCompileError.fromError(error);
}
throw error;
}
};
}

/**
* @deprecated use `TypeScriptLoader`
*/
export function TypeScriptLoaderSync(options?: JitiOptions): LoaderSync {
const loader: Jiti = createJiti("", { interopDefault: true, ...options });
return (path: string, _content: string): LoaderResult => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result: { default?: unknown } = loader(path);
// eslint-disable-next-line @typescript-eslint/no-deprecated
const result = loader(path) as { default?: unknown };

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