diff --git a/package-lock.json b/package-lock.json index a32e00a..10a4ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,10 +26,7 @@ "node-version": "^3.0.0", "read-pkg-up": "^9.1.0", "resolve-pkg": "^2.0.0", - "ts-import": "^5.0.0-beta.0", - "ts-node": "^10.9.1", "tsconfck": "^2.1.1", - "tsconfig-paths": "^4.2.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -1718,6 +1715,8 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -1729,6 +1728,8 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4020,22 +4021,30 @@ "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true, + "peer": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "peer": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "peer": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "peer": true }, "node_modules/@tufjs/canonical-json": { "version": "1.0.0", @@ -4189,7 +4198,8 @@ "node_modules/@types/node": { "version": "20.3.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", - "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==" + "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==", + "dev": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -4689,6 +4699,7 @@ "version": "8.9.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4709,6 +4720,8 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -4894,7 +4907,9 @@ "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "peer": true }, "node_modules/argparse": { "version": "2.0.1", @@ -5990,6 +6005,8 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "peer": true, "engines": { "node": ">= 12.0.0" } @@ -6761,7 +6778,9 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -7123,6 +7142,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -11097,7 +11118,9 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "peer": true }, "node_modules/make-fetch-happen": { "version": "11.1.1", @@ -16223,7 +16246,9 @@ "node_modules/options-defaults": { "version": "2.0.40", "resolved": "https://registry.npmjs.org/options-defaults/-/options-defaults-2.0.40.tgz", - "integrity": "sha512-a0oW0AMaP/Uqk1gU7s3unE83wzs/MACy3wsCnNREn4wqp4KCcxRdulRjf0d2FeIxENbGJ4EBGtHTQ6J30XB6Cw==" + "integrity": "sha512-a0oW0AMaP/Uqk1gU7s3unE83wzs/MACy3wsCnNREn4wqp4KCcxRdulRjf0d2FeIxENbGJ4EBGtHTQ6J30XB6Cw==", + "dev": true, + "peer": true }, "node_modules/over-9000": { "version": "9000.1.4", @@ -19644,6 +19669,8 @@ "version": "5.0.0-beta.0", "resolved": "https://registry.npmjs.org/ts-import/-/ts-import-5.0.0-beta.0.tgz", "integrity": "sha512-YOe/xCmwDWughfeaAaGJ4UWzlCKNnt9e+oda3St6mUMkRJCTBhBso+7XApIijw7Mr9SS6NLOdav8i5EJrx7UVQ==", + "dev": true, + "peer": true, "dependencies": { "comment-parser": "1.3.1", "options-defaults": "2.0.40", @@ -19659,12 +19686,16 @@ "node_modules/ts-import/node_modules/tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true, + "peer": true }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -19752,6 +19783,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "peer": true, "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", @@ -20153,7 +20186,9 @@ "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "peer": true }, "node_modules/v8-to-istanbul": { "version": "9.1.0", @@ -21301,6 +21336,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "peer": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index 15fae2c..a555088 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,7 @@ "node-version": "^3.0.0", "read-pkg-up": "^9.1.0", "resolve-pkg": "^2.0.0", - "ts-import": "^5.0.0-beta.0", - "ts-node": "^10.9.1", "tsconfck": "^2.1.1", - "tsconfig-paths": "^4.2.0", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/src/lib/configuration/index.ts b/src/lib/configuration/index.ts index 891c534..64e40e1 100644 --- a/src/lib/configuration/index.ts +++ b/src/lib/configuration/index.ts @@ -17,29 +17,41 @@ import ConfigurationLoader from 'lib/configuration/loader'; export default async function loadConfiguration(options: SaffronCosmiconfigOptions) { const { fileName, key, searchFrom, ...cosmicOptions } = validators.cosmiconfigOptions(options); - const configResult = await cosmiconfig(fileName, merge({ + const mergedOptions = merge({ loaders: { ...defaultLoaders, + '.ts': ConfigurationLoader, - '.cts': ConfigurationLoader, + '.tsx': ConfigurationLoader, '.mts': ConfigurationLoader, + '.cts': ConfigurationLoader, + '.js': ConfigurationLoader, - '.cjs': ConfigurationLoader, - '.mjs': ConfigurationLoader + '.jsx': ConfigurationLoader, + '.mjs': ConfigurationLoader, + '.cjs': ConfigurationLoader }, searchPlaces: [ `${fileName}.config.ts`, + `${fileName}.config.tsx`, `${fileName}.config.mts`, `${fileName}.config.cts`, + `${fileName}.config.js`, + `${fileName}.config.jsx`, `${fileName}.config.mjs`, `${fileName}.config.cjs`, + `${fileName}rc.ts`, + `${fileName}rc.tsx`, `${fileName}rc.mts`, `${fileName}rc.cts`, + `${fileName}rc.js`, + `${fileName}rc.jsx`, `${fileName}rc.mjs`, `${fileName}rc.cjs`, + `.${fileName}.json`, `.${fileName}.yaml`, `.${fileName}.yml`, @@ -52,7 +64,9 @@ export default async function loadConfiguration(options: SaffronCosmiconfigOp // our value. return [...source, ...target]; } - })).search(searchFrom); + }); + + const configResult = await cosmiconfig(fileName, mergedOptions).search(searchFrom); // If we loaded a non-empty file and the user specified a sub-key that they // want to drill-down into, ensure that the root configuration object has that diff --git a/src/lib/configuration/loader.ts b/src/lib/configuration/loader.ts index a4681fb..3d71264 100644 --- a/src/lib/configuration/loader.ts +++ b/src/lib/configuration/loader.ts @@ -2,24 +2,23 @@ import path from 'path'; import { babelRegisterStrategy } from 'lib/configuration/strategies/babel-register'; import { esbuildStrategy } from 'lib/configuration/strategies/esbuild'; -// import { tsImportStrategy } from 'lib/configuration/strategies/ts-import'; -// import { TypeScriptLoader } from 'lib/configuration/strategies/ts-node'; import log from 'lib/log'; import { getPackageInfo } from 'lib/package'; /** - * Cosmiconfig custom loader that supports ESM syntax. It allows host - * applications to be written in ESM or CJS, and for the consumers of those - * applications to write configuration files as ESM or CJS. + * Cosmiconfig custom loader. It is designed to work with with dependents that + * are written in ESM or CJS, and for the consumers of those applications to be + * written in ESM or CJS with configuration files written in either ESM or CJS. */ export default async function configurationLoader(filePath: string /* , content: string */) { + const prefix = log.prefix('config'); const errors: Array = []; - log.verbose(log.prefix('configurationLoader'), `Using configuration file: ${log.chalk.green(filePath)}`); + log.verbose(prefix, `Found configuration file: ${log.chalk.green(filePath)}`); const pkgInfo = getPackageInfo({ cwd: path.dirname(filePath) }); - if (!pkgInfo?.root) throw new Error(`${log.prefix('configurationLoader')} Unable to compute host package root directory.`); + if (!pkgInfo?.root) throw new Error(`${prefix} Unable to compute host package root directory.`); /** @@ -31,10 +30,10 @@ export default async function configurationLoader(filePath: string /* , content: */ try { const config = await import(filePath); - log.verbose(log.prefix('configurationLoader'), 'Used strategy:', log.chalk.bold('import()')); + log.verbose(prefix, 'Used strategy:', log.chalk.bold('import()')); return config?.default ?? config; } catch (err: any) { - errors.push(new Error(`${log.prefix('configurationLoader')} Failed to load configuration with ${log.chalk.bold('dynamic import')}: ${err}`)); + errors.push(new Error(`${prefix} Failed to load file with ${log.chalk.bold('import()')}: ${err}`)); } @@ -43,59 +42,29 @@ export default async function configurationLoader(filePath: string /* , content: */ try { const config = await esbuildStrategy(filePath, pkgInfo); - log.verbose(log.prefix('configurationLoader'), 'Used strategy:', log.chalk.bold('esbuild')); + log.verbose(prefix, 'Used strategy:', log.chalk.bold('esbuild')); return config.default ?? config; } catch (err: any) { - errors.push(new Error(`${log.prefix('configurationLoader')} Failed to load configuration with ${log.chalk.bold('esbuild')}: ${err}`)); + errors.push(new Error(`${prefix} Failed to load file with ${log.chalk.bold('esbuild')}: ${err}`)); } /** - * Strategy 3: ts-import - */ - // try { - // const config = await tsImportStrategy(filePath); - // log.verbose(log.prefix('configurationLoader'), 'Used strategy:', log.chalk.bold('ts-import')); - // return config?.default ?? config; - // } catch (err: any) { - // errors.push(new Error(`${log.prefix('configurationLoader')} Failed to load configuration with ${log.chalk.bold('ts-import')}: ${err}`)); - // } - - - /** - * Strategy 4: ts-node - * - * This strategy is in place in the event that @babel/register did not work - * for some reason, but it is not ideal for reasons explained above. - */ - // try { - // const tsLoader = TypeScriptLoader(); - // const config = await tsLoader(filePath, content, pkgInfo); - // log.verbose(log.prefix('configurationLoader'), 'Used strategy:', log.chalk.bold('ts-node')); - // return config; - // } catch (err: any) { - // errors.push(new Error(`${log.prefix('configurationLoader')} Failed to load configuration with ${log.chalk.bold('TypeScriptLoader')}: ${err}`)); - // } - - - /** - * Strategy 5: @babel/register + * Strategy 3: @babel/register * - * This strategy uses a custom loader that uses @babel/register to transpile - * code. The loader will work with TypeScript configuration files, and it will - * additionally configure Babel to use path mappings defined in tsconfig.json. - * The host project need not have a Babel configuration file in place for this - * strategy to work. If the first strategy failed, this one should work for - * the majority of cases. We try this before ts-node because TypeScript does - * not have any mechanism that allows us to preserve dynamic import statements - * when transpiling to CommonJS. + * This strategy creates a custom loader that uses @babel/register to + * transpile code. The loader will work with TypeScript configuration files, + * and it will additionally configure Babel to use path mappings defined in + * tsconfig.json. The host project need not have a Babel configuration file in + * place for this strategy to work. If the previous strategies failed, this + * one should work for the majority of remaining cases. */ try { const config = await babelRegisterStrategy(filePath, pkgInfo); - log.verbose(log.prefix('configurationLoader'), 'Used strategy:', log.chalk.bold('@babel/register')); + log.verbose(prefix, 'Used strategy:', log.chalk.bold('@babel/register')); return config?.default ?? config; } catch (err: any) { - errors.push(err); + errors.push(new Error(`${prefix} Failed to load file with ${log.chalk.bold('@babel/register')}: ${err}`)); } if (errors.length > 0) throw new AggregateError( diff --git a/src/lib/configuration/strategies/babel-register.ts b/src/lib/configuration/strategies/babel-register.ts index 19dc7a9..e4b324b 100644 --- a/src/lib/configuration/strategies/babel-register.ts +++ b/src/lib/configuration/strategies/babel-register.ts @@ -15,6 +15,8 @@ import type { PackageInfo } from 'lib/package'; * results. */ export async function babelRegisterStrategy(filePath: string, pkgInfo: PackageInfo) { + const prefix = log.prefix('strategy:babel'); + try { if (!pkgInfo.root) throw new Error('Unable to determine host package root directory.'); @@ -98,7 +100,7 @@ export async function babelRegisterStrategy(filePath: string, pkgInfo: PackageIn `; if (tsConfigFilePath) { - log.silly(log.prefix('babelRegisterStrategy'), `Loaded tsconfig.json from: ${log.chalk.green(tsConfigFilePath)}`); + log.silly(prefix, `Loaded tsconfig.json from: ${log.chalk.green(tsConfigFilePath)}`); } const tempDir = path.resolve(pkgInfo.root, 'node_modules', '.saffron-config'); @@ -114,10 +116,10 @@ export async function babelRegisterStrategy(filePath: string, pkgInfo: PackageIn await fs.remove(tempDir); return result.default ?? result; - } catch (cause: any) { + } catch (err: any) { throw new Error( - `${log.prefix('babelRegisterStrategy')} Failed to load configuration file: ${cause}`, - { cause } + `${log.chalk.red(`[${prefix}] Failed to import() configuration file ${filePath}:`)} ${err.message}`, + { cause: err } ); } } diff --git a/src/lib/configuration/strategies/esbuild.ts b/src/lib/configuration/strategies/esbuild.ts index 58e3f72..1338bc8 100644 --- a/src/lib/configuration/strategies/esbuild.ts +++ b/src/lib/configuration/strategies/esbuild.ts @@ -12,32 +12,45 @@ import type { PackageInfo } from 'lib/package'; /** - * Uses esbuild to transpile the user's configuration file. + * Uses esbuild to transpile the user's configuration file. This strategy + * creates a temporary transpiled configuration file in the same directory as + * the source file, then attempts to dynamically import it. An output format + * and extension are chosen based on the project's configuration that are the + * least likely to produce errors. Finally, the temporary file is removed. */ export async function esbuildStrategy(filePath: string, pkgInfo: PackageInfo) { - const parsedFileName = path.parse(filePath); - const isExplicitCommonJs = ['.cjs', '.cts'].includes(parsedFileName.ext); - const isExplicitESM = ['.mjs', '.mts'].includes(parsedFileName.ext); + const prefix = log.prefix('strategy:esbuild'); + const parsedFilePath = path.parse(filePath); + // Map of possible config file extensions to the most sensible output format + // that we can dynamically import without producing an + // ERR_UNKNOWN_FILE_EXTENSION error from Node. const extMap: Record = { + '.js': '.js', '.ts': '.js', '.tsx': '.js', - '.cts': '.cjs', - '.mts': '.mjs', '.jsx':' .js', + // User explicitly wants CommonJS. + '.cts': '.cjs', '.cjs': '.cjs', + // User explicitly wants ESM. + '.mts': '.mjs', '.mjs': '.mjs' }; - const outExt = extMap[parsedFileName.ext]; + const isExplicitCommonJs = ['.cjs', '.cts'].includes(parsedFilePath.ext); + const isExplicitESM = ['.mjs', '.mts'].includes(parsedFilePath.ext); + + const outExt = extMap[parsedFilePath.ext]; if (!outExt) throw new Error( - `${log.prefix('strategy:esbuild')} Unable to determine output file extension from input extension: ${parsedFileName.ext}` + `${prefix} Unable to determine output file extension from input extension: ${parsedFilePath.ext}` ); // Determine the output format to use by first honoring any explicit // transpilation hints based on file extension, then fall back to relying on - // the "type" field in package.json. + // the "type" field in package.json. The aim here is to use an output format + // that is the least likely to produce errors when being dynamically imported. const format = isExplicitESM ? 'esm' : isExplicitCommonJs @@ -46,11 +59,11 @@ export async function esbuildStrategy(filePath: string, pkgInfo: PackageInfo) { ? 'esm' : 'cjs'; - log.verbose(log.prefix('strategy:esbuild'), `Using format: ${log.chalk.bold(format)}`); + log.verbose(prefix, `Using format: ${log.chalk.bold(format)}`); - const tempFileName = `.${parsedFileName.name}.${Date.now()}${outExt}`; + const tempFileName = `.${parsedFilePath.name}.${Date.now()}${outExt}`; const tempFilePath = path.join(path.dirname(filePath), tempFileName); - log.silly(log.prefix('strategy:esbuild'), `Temporary file will be written to: ${log.chalk.green(tempFilePath)}`); + log.silly(prefix, `Temporary file will be written to: ${log.chalk.green(tempFilePath)}`); try { const buildOptions: esbuild.BuildOptions = { @@ -60,7 +73,7 @@ export async function esbuildStrategy(filePath: string, pkgInfo: PackageInfo) { format, plugins: [], banner: { - 'js': '/** This file was generated by @darkobits/saffron and can be safely removed. */\n' + 'js': '/** This file was generated by @darkobits/saffron (esbuild) and can be safely removed. */\n' } }; @@ -69,17 +82,18 @@ export async function esbuildStrategy(filePath: string, pkgInfo: PackageInfo) { const tsConfigFilePath = await tsConfck.find(filePath); if (tsConfigFilePath) { - log.verbose(log.prefix('strategy:esbuild'), `Using TypeScript configuration: ${log.chalk.green(tsConfigFilePath)}`); + log.verbose(prefix, 'Using:', log.chalk.green(tsConfigFilePath)); buildOptions.tsconfig = tsConfigFilePath; buildOptions.plugins?.push(TsconfigPathsPlugin({ tsconfig: tsConfigFilePath })); } // Transpile the input file. await esbuild.build(buildOptions); - } catch (cause: any) { + } catch (err: any) { + // Handle any transpilation-related errors. throw new Error( - `${log.prefix('strategy:esbuild')} ${log.chalk.red('Failed to transpile configuration file')} ${log.chalk.green(filePath)}: ${cause}`, - { cause } + `${log.chalk.red(`[${prefix}] Failed to transpile configuration file ${filePath}:`)} ${err.message}`, + { cause: err } ); } @@ -89,15 +103,14 @@ export async function esbuildStrategy(filePath: string, pkgInfo: PackageInfo) { // Note: This assumes the consumer wants the default export. return result?.default ?? result; - } catch (cause: any) { + } catch (err: any) { + // Handle any import-related errors. throw new Error( - `${log.prefix('strategy:esbuild')} ${log.chalk.red('Failed to load configuration file')} ${log.chalk.green(tempFilePath)}: ${cause}`, - { cause } + `${log.chalk.red(`[${prefix}] Failed to import() configuration file ${filePath}:`)} ${err.message}`, + { cause: err } ); } finally { // Remove the temporary file. - if (await fs.exists(tempFilePath)) { - await fs.remove(tempFilePath); - } + if (await fs.exists(tempFilePath)) await fs.remove(tempFilePath); } } diff --git a/src/lib/configuration/strategies/ts-import.ts b/src/lib/configuration/strategies/ts-import.ts deleted file mode 100644 index 82c9ac1..0000000 --- a/src/lib/configuration/strategies/ts-import.ts +++ /dev/null @@ -1,35 +0,0 @@ -import path from 'path'; - -import fs from 'fs-extra'; -import * as tsImport from 'ts-import'; - -import log from 'lib/log'; - - -/** - * Uses ts-import to dynamically import TypeScript configuration files. - */ -export async function tsImportStrategy(filePath: string) { - // Clean-up the cache directory left behind by ts-import. - const cacheDir = path.join(path.dirname(filePath), '.cache'); - - try { - const result = await tsImport.load(filePath, { - // Slower, but allows files to be considered as part of larger TypeScript - // programs, which should allow path mappings to work. - mode: tsImport.LoadMode.Compile, - // N.B. Even with this set to false, ts-import still seems to create (and - // leave behind) a .cache directory. - useCache: false - }); - - return result?.default || result; - } catch (cause: any) { - throw new Error( - `${log.prefix('tsImportStrategy')} Failed to load configuration file: ${cause}`, - { cause } - ); - } finally { - if (await fs.exists(cacheDir)) await fs.remove(cacheDir); - } -} diff --git a/src/lib/configuration/strategies/ts-node.ts b/src/lib/configuration/strategies/ts-node.ts deleted file mode 100644 index 2153539..0000000 --- a/src/lib/configuration/strategies/ts-node.ts +++ /dev/null @@ -1,131 +0,0 @@ -import path from 'path'; - -import merge from 'deepmerge'; -import fs from 'fs-extra'; -import resolvePkg from 'resolve-pkg'; -import { - register, - type RegisterOptions -} from 'ts-node'; - -import log from 'lib/log'; - -import type { Loader } from 'cosmiconfig'; -import type { PackageInfo } from 'lib/package'; - - -/** - * @private - * - * Computes the extension we should use for the temporary (re: transpiled) - * configuration file. - */ -function computeFileExtension(fileName: string) { - const parsedFileName = path.parse(fileName); - Reflect.deleteProperty(parsedFileName, 'base'); - - switch (parsedFileName.ext) { - case '.ts': - parsedFileName.ext = '.js'; - break; - case '.cts': - parsedFileName.ext = '.cjs'; - break; - case '.mts': - parsedFileName.ext = '.mjs'; - break; - } - - return path.format(parsedFileName); -} - - -/** - * Convoluted way to dynamically transpile and import() TypeScript files at - * runtime. - */ -export function TypeScriptLoader(options: RegisterOptions = {}) { - // N.B. Signature for Cosmiconfig loaders. - return async (filePath: string, content: string, pkgInfo: PackageInfo) => { - if (!pkgInfo.root) throw new Error('Unable to determine host package root directory.'); - - const tsConfigPathsRegisterPath = resolvePkg('tsconfig-paths/register', { cwd: pkgInfo.root }); - if (!tsConfigPathsRegisterPath) throw new Error('Unable to resolve path to tsconfig-paths/register.'); - - let tempConfigFilePath = ''; - - try { - // ----- [1] Prepare ts-node Instance ------------------------------------ - - const { ext } = path.parse(filePath); - - // What we are really testing for here is whether the user has - // _explicitly_ requested that the configuration file be parsed as CJS - // despite what their package.json / tsconfig.json may indicate for the - // rest of the project. In other cases, we may still parse the - // configuration file as CJS, but we will not need to use the overrides - // triggered by this flag. - const isExplicitCommonJs = ['.cjs', '.cts'].includes(ext); - - const ourOptions: RegisterOptions = { - require: [tsConfigPathsRegisterPath] - }; - - if (isExplicitCommonJs) { - ourOptions.compilerOptions = { - module: 'NodeNext', - moduleResolution: 'Node16' - }; - } - - const finalOptions = merge(options ?? {}, ourOptions); - log.silly(log.prefix('TypeScriptLoader'), 'ts-node options', finalOptions); - const tsNodeInstance = register(finalOptions); - - - // ----- [2] Compute Path for Temporary Configuration File --------------- - - const tempConfigFileName = computeFileExtension(`.${path.basename(filePath)}.${Date.now()}`); - const tempConfigFileDirectory = path.dirname(filePath); - tempConfigFilePath = path.join(tempConfigFileDirectory, tempConfigFileName); - - - // ----- [3] Transpile Configuration File -------------------------------- - - // N.B. Not clear why ts-node (or possibly by extension, TypeScript) needs - // both a file name _and_ its contents here. - let transpiledConfig = tsNodeInstance.compile(content, filePath); - - - // ----- [4] Clean Transpiled Configuration File ------------------------- - - // Remove empty export declarations added by ts-node, which will sometimes - // be added even if we are transpiling to CommonJS where such declarations - // are illegal. - if (isExplicitCommonJs) { - transpiledConfig = transpiledConfig.replaceAll(/export\s+{\s?};?/g, ''); - } - - - // ----- [5] Write and Import Transpiled Configuration File -------------- - - if (isExplicitCommonJs) { - log.verbose(log.prefix('TypeScriptLoader'), log.chalk.yellow('Used overrides for explicit CommonJS.')); - } - - await fs.writeFile(tempConfigFilePath, transpiledConfig); - const result = await import(tempConfigFilePath); - - log.verbose('Loaded configuration using TypeScript loader.'); - - // `default` is used when exporting using export default, some modules - // may still use `module.exports` or if in TS `export = ` - return (result.default || result) as Loader; - } finally { - if (tempConfigFilePath.length > 0) { - await fs.remove(tempConfigFilePath); - log.silly(log.prefix('TypeScriptLoader'), `Removed temporary file: ${log.chalk.green(tempConfigFilePath)}`); - } - } - }; -}