This repository has been archived by the owner on Aug 22, 2023. It is now read-only.
Native ES Module support for Oclif w/ continued support of CommonJS #223
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.
A pre-announcement edit: While I have initially prototyped ES Module support in Oclif v1 please refer to my modifications to the newly announced Oclif v2 which is where ESM support should be added. You can follow adding ESM to Oclif v2 in @oclif/core:
oclif/core#130
I am closing this pull request for Oclif v1.
Original message:
Greets...
Please see the associated Issue for pleasantries.
This pull request updates @oclif/config to support loading of CommonJS & ES Modules (ESM) package plugins for commands and associated hooks. In short a package that has
"type": "module"
set in package.json is treated as an ESM package and dynamic import / import() will be used to load any commands and hooks from that package. It is also possible for the main CLI code to also be ESM. These changes transparently support loading both CommonJS & ESM so plugins in a CLI can be a combination of both.The usage of dynamic import is gated by checking that package.json type is set to module and if not set then require() is used like the old code.
The main changes are to
config.ts
,plugin.ts
and the addition ofimport-dynamic.ts
. The latter new source file simply exports a wrapper around import():export default new Function('modulePath', 'return import(modulePath)')
. This is necessary astsc
when set for"module": "commonJS"
will transpile import() into require(). I searched for any tsconfig setting that will selectively not transpile import(), but found no configuration based solution. This seems to be the least impactful option and is concise maintaining no changes to tsconfig.json or the build process. I'm certainly open to learning if there is another way to preventtsc
from altering dynamic imports in CommonJS targeted output. It should be noted that dynamic imports / import() is available to CommonJS code to load ESM on versions of Node that support"type": "module"
set in package.json.The largest flow control change was to
runHook
inconfig.ts
which needs to be restructured to support await import() and require in the same block of code. The previousPromise.all
andArray.map
code was replaced with synchronous looping with a single await line. This ensures that mixed loading with dynamic import and require executes synchronously. The oldPromise.all
/Array.map
control flow had execution of hooks out of order (essentially reverse completion / require first) when combining dynamic import and require loading due to the nested anonymous async function in.map()
with a mixed awaitimport()
/ no await,require()
, situation. I do believe the newrunHook
code is simpler and captures the intention of loading hooks in order. Each hook starts / finishes before the next one is called. The old code only worked due to the synchronous nature ofrequire()
and underlying implementation in Node.For main core CLI code switching to ESM one adds
"type": "module"
to package.json and must change./bin/run
to./bin/run.js
so Node can associate it as ESM and add the following to the top of./bin/run.js
:Using createRequire is the easiest way to get the bootstrapping code to function correctly and also provides a simple way to switch to ESM. The rest of a CLI codebase can be converted to normal ESM at this point. Any plugins referenced can be a mixture of CommonJS or ESM.
This is a potential big change and one that is very timely at this point as ESM is increasingly being used on Node. All tests pass and no new tests were added. I tested these changes in my own CLI which I partially converted to ESM while testing, so I was loading a mixture of ESM / CJS plugins before a full conversion to all ESM. It certainly is recommended to test these changes on older versions of Node though the test suite passes on 10.x / 12.x. The usage of dynamic import is only be engaged when
package.json
indicates it should be used and developers are only setting this for Node versions that support it. I only tested my CLI on Node 14.15.1 on OSX and am releasing my CLI requiring Node 14.x+.My non-trivial CLI test case is
fvttdev
which is published in a pre-alpha but working state:https://github.com/typhonjs-fvtt/fvttdev
https://www.npmjs.com/package/@typhonjs-fvtt/fvttdev
Integration with an actual demo project using the NPM published version:
https://github.com/typhonjs-fvtt/demo-fvttdev-module
I have posted a separate version of my changes to this repo which can be included directly from Github as a replacement to
@oclif/config
:https://github.com/typhonjs-oclif/config
For example from
fvttdev
package.json:https://github.com/typhonjs-fvtt/fvttdev/blob/main/package.json#L15
"@oclif/config": "git+https://github.com/typhonjs-oclif/config.git"
Anyway I look forward to working with any project maintainers to get this update into Oclif.
I have also made modifications to @oclif/command & @oclif/plugin-help to be able to load custom help classes as an ES Module and have tested it along with making sure the test suite passes. I haven't submitted them as PRs as there is a co-dependency between these two modules so the tests would fail unless both of these modules are updated.
The last concern about updating Oclif to support ES Modules natively regards @oclif/dev-cli and particularly the readme command as it invokes the help class. I would be glad to make the modifications necessary to @oclif/dev-cli when I make contact with any project maintainers regarding the willingness to accept support for ES Modules.
All my forks have a long term home in the typhonjs-oclif-scratch Github organization. I do hope to make contact with project maintainers, but am also preparing for things to take a while.