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..9766701 100644 --- a/src/jiti.ts +++ b/src/jiti.ts @@ -2,7 +2,18 @@ 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 JitiLoader = () => Promise<{ createJiti: unknown }>; + +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. + */ +export const setJitiLoader = (loader: JitiLoader): void => { + getJiti = cachedImport(loader); +}; export const loadWithJiti = async ( configPath: string, @@ -12,7 +23,7 @@ export const loadWithJiti = async ( 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 new file mode 100644 index 0000000..080ef32 --- /dev/null +++ b/tests/loader-jiti/injection.test.ts @@ -0,0 +1,24 @@ +import { afterEach, expect, rstest, test } from 'rstack/test'; +import { loadConfig, setJitiLoader } from '../../src/index'; + +afterEach(() => { + setJitiLoader(() => import('jiti')); +}); + +test('lazily imports injected jiti once for concurrent configs', async () => { + const importer = rstest.fn(() => import('jiti')); + setJitiLoader(importer); + expect(importer).not.toHaveBeenCalled(); + + const options = { + cwd: import.meta.dirname, + path: 'demo.config.ts', + loader: 'jiti', + } as const; + const [result] = await Promise.all([ + loadConfig(options), + loadConfig(options), + ]); + expect(result.content).toEqual({ loader: 'jiti' }); + expect(importer).toHaveBeenCalledTimes(1); +});