Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: integrate ESM loading of commands & hooks (#160)
* integrated ESM loading of commands & hooks * module-loader / remove importDynamic / update plugin.ts * Added ModuleLoadError to combine CJS & ESM loader module not found condition * Added unit test for ModuleLoader in ./test/module-loader * comment clarity & comments in tests * correct method to test for ModuleLoader.load
- Loading branch information
Showing
20 changed files
with
426 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import {CLIError} from './cli' | ||
import {OclifError} from '../../interfaces' | ||
|
||
export class ModuleLoadError extends CLIError implements OclifError { | ||
oclif!: { exit: number } | ||
|
||
code = 'MODULE_NOT_FOUND' | ||
|
||
constructor(message: string) { | ||
super(`[MODULE_NOT_FOUND] ${message}`, {exit: 1}) | ||
this.name = 'ModuleLoadError' | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import * as path from 'path' | ||
import * as url from 'url' | ||
|
||
import {ModuleLoadError} from './errors' | ||
import {Config as IConfig} from './interfaces' | ||
import {Plugin as IPlugin} from './interfaces' | ||
import * as Config from './config' | ||
|
||
const getPackageType = require('get-package-type') | ||
|
||
/** | ||
* Provides a mechanism to use dynamic import / import() with tsconfig -> module: commonJS as otherwise import() gets | ||
* transpiled to require(). | ||
*/ | ||
const _importDynamic = new Function('modulePath', 'return import(modulePath)') // eslint-disable-line no-new-func | ||
|
||
/** | ||
* Provides a static class with several utility methods to work with Oclif config / plugin to load ESM or CJS Node | ||
* modules and source files. | ||
* | ||
* @author Michael Leahy <support@typhonjs.io> (https://github.com/typhonrt) | ||
*/ | ||
export default class ModuleLoader { | ||
/** | ||
* Loads and returns a module. | ||
* | ||
* Uses `getPackageType` to determine if `type` is set to 'module. If so loads '.js' files as ESM otherwise uses | ||
* a bare require to load as CJS. Also loads '.mjs' files as ESM. | ||
* | ||
* Uses dynamic import to load ESM source or require for CommonJS. | ||
* | ||
* A unique error, ModuleLoadError, combines both CJS and ESM loader module not found errors into a single error that | ||
* provides a consistent stack trace and info. | ||
* | ||
* @param {IConfig|IPlugin} config - Oclif config or plugin config. | ||
* @param {string} modulePath - NPM module name or file path to load. | ||
* | ||
* @returns {Promise<*>} The entire ESM module from dynamic import or CJS module by require. | ||
*/ | ||
static async load(config: IConfig|IPlugin, modulePath: string): Promise<any> { | ||
const {isESM, filePath} = ModuleLoader.resolvePath(config, modulePath) | ||
try { | ||
// It is important to await on _importDynamic to catch the error code. | ||
return isESM ? await _importDynamic(url.pathToFileURL(filePath)) : require(filePath) | ||
} catch (error) { | ||
if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND') { | ||
throw new ModuleLoadError(`${isESM ? 'import()' : 'require'} failed to load ${filePath}`) | ||
} | ||
throw error | ||
} | ||
} | ||
|
||
/** | ||
* Loads a module and returns an object with the module and data about the module. | ||
* | ||
* Uses `getPackageType` to determine if `type` is set to `module`. If so loads '.js' files as ESM otherwise uses | ||
* a bare require to load as CJS. Also loads '.mjs' files as ESM. | ||
* | ||
* Uses dynamic import to load ESM source or require for CommonJS. | ||
* | ||
* A unique error, ModuleLoadError, combines both CJS and ESM loader module not found errors into a single error that | ||
* provides a consistent stack trace and info. | ||
* | ||
* @param {IConfig|IPlugin} config - Oclif config or plugin config. | ||
* @param {string} modulePath - NPM module name or file path to load. | ||
* | ||
* @returns {Promise<{isESM: boolean, module: *, filePath: string}>} An object with the loaded module & data including | ||
* file path and whether the module is ESM. | ||
*/ | ||
static async loadWithData(config: IConfig|IPlugin, modulePath: string): Promise<{isESM: boolean; module: any; filePath: string}> { | ||
const {isESM, filePath} = ModuleLoader.resolvePath(config, modulePath) | ||
try { | ||
const module = isESM ? await _importDynamic(url.pathToFileURL(filePath)) : require(filePath) | ||
return {isESM, module, filePath} | ||
} catch (error) { | ||
if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND') { | ||
throw new ModuleLoadError(`${isESM ? 'import()' : 'require'} failed to load ${filePath}`) | ||
} | ||
throw error | ||
} | ||
} | ||
|
||
/** | ||
* For `.js` files uses `getPackageType` to determine if `type` is set to `module` in associated `package.json`. If | ||
* the `modulePath` provided ends in `.mjs` it is assumed to be ESM. | ||
* | ||
* @param {string} filePath - File path to test. | ||
* | ||
* @returns {boolean} The modulePath is an ES Module. | ||
* @see https://www.npmjs.com/package/get-package-type | ||
*/ | ||
static isPathModule(filePath: string): boolean { | ||
const extension = path.extname(filePath).toLowerCase() | ||
|
||
switch (extension) { | ||
case '.js': | ||
return getPackageType.sync(filePath) === 'module' | ||
|
||
case '.mjs': | ||
return true | ||
|
||
default: | ||
return false | ||
} | ||
} | ||
|
||
/** | ||
* Resolves a modulePath first by `require.resolve` to allow Node to resolve an actual module. If this fails then | ||
* the `modulePath` is resolved from the root of the provided config. `path.resolve` is used for ESM and `tsPath` | ||
* for non-ESM paths. | ||
* | ||
* @param {IConfig|IPlugin} config - Oclif config or plugin config. | ||
* @param {string} modulePath - File path to load. | ||
* | ||
* @returns {{isESM: boolean, filePath: string}} An object including file path and whether the module is ESM. | ||
*/ | ||
static resolvePath(config: IConfig|IPlugin, modulePath: string): {isESM: boolean; filePath: string} { | ||
let isESM = config.pjson.type === 'module' | ||
let filePath | ||
|
||
try { | ||
filePath = require.resolve(modulePath) | ||
isESM = ModuleLoader.isPathModule(filePath) | ||
} catch (error) { | ||
filePath = isESM ? path.resolve(path.join(config.root, modulePath)) : Config.tsPath(config.root, modulePath) | ||
} | ||
|
||
return {isESM, filePath} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
bad_reference |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"type": "commonjs" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
module.exports = ['SUCCESS']; | ||
module.exports.namedExport = 'SUCCESS_NAMED'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
bad_reference |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"type": "module" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default 'SUCCESS' | ||
|
||
export const namedExport = 'SUCCESS_NAMED' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
module.exports = ['SUCCESS_CJS']; | ||
module.exports.namedExport = 'SUCCESS_NAMED_CJS'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default 'SUCCESS_MJS'; | ||
|
||
export const namedExport = 'SUCCESS_NAMED_MJS'; |
Oops, something went wrong.