From 2c972db8f3bd2756430476d92c58a2530b85cec9 Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 13 Oct 2026 12:56:54 +0800 Subject: [PATCH 1/3] feat: support lazy jiti loader injection --- src/index.ts | 1 + src/jiti.ts | 29 ++++++++++++++-- tests/loader-jiti/injection.test.ts | 54 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/loader-jiti/injection.test.ts diff --git a/src/index.ts b/src/index.ts index 9c8d65f..8e87eea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import type { LoadConfigResult, } from './types.js'; +export { setJitiLoader } from './jiti.js'; export { withConfigMeta } from './meta.js'; export type { ConfigDefinition, diff --git a/src/jiti.ts b/src/jiti.ts index 1abba59..e7e24eb 100644 --- a/src/jiti.ts +++ b/src/jiti.ts @@ -2,14 +2,39 @@ import { cachedImport } from './cachedImport.js'; import { getConfigExport } from './helpers.js'; import type { ConfigDefinition, LoadedConfig } from './types.js'; -const getJiti = cachedImport(() => import('jiti')); +// Keep the injection API independent of the optional jiti package's types. +type JitiModule = { + createJiti: ( + id: string, + options: { + moduleCache: boolean; + interopDefault: boolean; + nativeModules: string[]; + }, + ) => { + import: (id: string, options?: { default?: true }) => Promise; + }; +}; + +type JitiLoader = () => Promise; + +let getJiti: JitiLoader = cachedImport(() => import('jiti')); + +/** + * Set the lazy jiti importer for this module instance before loading configs. + * Replaces the cached import without invoking the new importer. + * @internal + */ +export const setJitiLoader = (loader: JitiLoader): void => { + getJiti = cachedImport(loader); +}; export const loadWithJiti = async ( configPath: string, exportName: string | false, fresh: boolean, ): Promise> => { - let createJiti: (typeof import('jiti'))['createJiti']; + let createJiti: JitiModule['createJiti']; try { ({ createJiti } = await getJiti()); diff --git a/tests/loader-jiti/injection.test.ts b/tests/loader-jiti/injection.test.ts new file mode 100644 index 0000000..c2c2e80 --- /dev/null +++ b/tests/loader-jiti/injection.test.ts @@ -0,0 +1,54 @@ +import { afterEach, expect, test } from 'rstack/test'; +import { loadConfig, setJitiLoader } from '../../src/index'; + +const load = () => + loadConfig({ + cwd: import.meta.dirname, + path: 'demo.config.ts', + loader: 'jiti', + fresh: true, + }); + +afterEach(() => { + setJitiLoader(() => import('jiti')); +}); + +test('replaces the cached importer and loads jiti lazily once for concurrent configs', async () => { + await load(); + + let imports = 0; + setJitiLoader(async () => { + imports++; + return import('jiti'); + }); + + expect(imports).toBe(0); + await loadConfig({ + cwd: import.meta.dirname, + path: '../basic/demo.config.mjs', + loader: 'auto', + }); + expect(imports).toBe(0); + + const results = await Promise.all([load(), load()]); + expect(results.map(({ content }) => content)).toEqual([ + { loader: 'jiti' }, + { loader: 'jiti' }, + ]); + expect(imports).toBe(1); +}); + +test('retries an injected importer after it rejects', async () => { + const error = new Error('failed to import bundled jiti'); + let imports = 0; + setJitiLoader(async () => { + if (++imports === 1) { + throw error; + } + return import('jiti'); + }); + + await expect(load()).rejects.toMatchObject({ cause: error }); + expect((await load()).content).toEqual({ loader: 'jiti' }); + expect(imports).toBe(2); +}); From 5069ba3862a82bed526d8dc26b31e9dd7d0af65e Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 13 Oct 2026 13:00:00 +0800 Subject: [PATCH 2/3] refactor: simplify jiti injection types and tests --- src/jiti.ts | 19 ++-------- tests/loader-jiti/injection.test.ts | 58 +++++++---------------------- 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/src/jiti.ts b/src/jiti.ts index e7e24eb..894a5c4 100644 --- a/src/jiti.ts +++ b/src/jiti.ts @@ -3,20 +3,7 @@ import { getConfigExport } from './helpers.js'; import type { ConfigDefinition, LoadedConfig } from './types.js'; // Keep the injection API independent of the optional jiti package's types. -type JitiModule = { - createJiti: ( - id: string, - options: { - moduleCache: boolean; - interopDefault: boolean; - nativeModules: string[]; - }, - ) => { - import: (id: string, options?: { default?: true }) => Promise; - }; -}; - -type JitiLoader = () => Promise; +type JitiLoader = () => Promise<{ createJiti: unknown }>; let getJiti: JitiLoader = cachedImport(() => import('jiti')); @@ -34,10 +21,10 @@ export const loadWithJiti = async ( exportName: string | false, fresh: boolean, ): Promise> => { - let createJiti: JitiModule['createJiti']; + let createJiti: (typeof import('jiti'))['createJiti']; try { - ({ createJiti } = await getJiti()); + ({ createJiti } = (await getJiti()) as typeof import('jiti')); } catch (error) { throw new Error( 'The "jiti" package is required to load this config. Install it with your package manager.', diff --git a/tests/loader-jiti/injection.test.ts b/tests/loader-jiti/injection.test.ts index c2c2e80..080ef32 100644 --- a/tests/loader-jiti/injection.test.ts +++ b/tests/loader-jiti/injection.test.ts @@ -1,54 +1,24 @@ -import { afterEach, expect, test } from 'rstack/test'; +import { afterEach, expect, rstest, test } from 'rstack/test'; import { loadConfig, setJitiLoader } from '../../src/index'; -const load = () => - loadConfig({ - cwd: import.meta.dirname, - path: 'demo.config.ts', - loader: 'jiti', - fresh: true, - }); - afterEach(() => { setJitiLoader(() => import('jiti')); }); -test('replaces the cached importer and loads jiti lazily once for concurrent configs', async () => { - await load(); - - let imports = 0; - setJitiLoader(async () => { - imports++; - return import('jiti'); - }); +test('lazily imports injected jiti once for concurrent configs', async () => { + const importer = rstest.fn(() => import('jiti')); + setJitiLoader(importer); + expect(importer).not.toHaveBeenCalled(); - expect(imports).toBe(0); - await loadConfig({ + const options = { cwd: import.meta.dirname, - path: '../basic/demo.config.mjs', - loader: 'auto', - }); - expect(imports).toBe(0); - - const results = await Promise.all([load(), load()]); - expect(results.map(({ content }) => content)).toEqual([ - { loader: 'jiti' }, - { loader: 'jiti' }, + path: 'demo.config.ts', + loader: 'jiti', + } as const; + const [result] = await Promise.all([ + loadConfig(options), + loadConfig(options), ]); - expect(imports).toBe(1); -}); - -test('retries an injected importer after it rejects', async () => { - const error = new Error('failed to import bundled jiti'); - let imports = 0; - setJitiLoader(async () => { - if (++imports === 1) { - throw error; - } - return import('jiti'); - }); - - await expect(load()).rejects.toMatchObject({ cause: error }); - expect((await load()).content).toEqual({ loader: 'jiti' }); - expect(imports).toBe(2); + expect(result.content).toEqual({ loader: 'jiti' }); + expect(importer).toHaveBeenCalledTimes(1); }); From 82cdc6685738449493b5f274dc3e895e4a155587 Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 13 Oct 2026 13:03:21 +0800 Subject: [PATCH 3/3] docs: remove internal annotation from setJitiLoader --- src/jiti.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jiti.ts b/src/jiti.ts index 894a5c4..9766701 100644 --- a/src/jiti.ts +++ b/src/jiti.ts @@ -10,7 +10,6 @@ let getJiti: JitiLoader = cachedImport(() => import('jiti')); /** * Set the lazy jiti importer for this module instance before loading configs. * Replaces the cached import without invoking the new importer. - * @internal */ export const setJitiLoader = (loader: JitiLoader): void => { getJiti = cachedImport(loader);