-
Notifications
You must be signed in to change notification settings - Fork 69
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
Integrate ESM loading of commands & hooks #160
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
28566d7
integrated ESM loading of commands & hooks
typhonrt 1ab5f7e
module-loader / remove importDynamic / update plugin.ts
typhonrt 0d0c734
Added ModuleLoadError to combine CJS & ESM loader module not found co…
typhonrt 13f95d7
Added unit test for ModuleLoader in ./test/module-loader
typhonrt 5c8e48a
comment clarity & comments in tests
typhonrt f8ba934
correct method to test for ModuleLoader.load
typhonrt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought your original PR had tests? Is this worth unit testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were no added tests. I'm certainly willing to submit tests as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the bare minimum, can you add a new fixture for a esm and a main test for it here.