-
Notifications
You must be signed in to change notification settings - Fork 2
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
fix(support): support ESM packages & cjs file type #26
Conversation
87e9d87
to
4354e57
Compare
4354e57
to
84be503
Compare
env: | ||
NODE_OPTIONS: ${{ matrix.node-options }} |
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.
question: Should this be in the section below, not in the node 10 section?
Out of curiosity, why separate 10 out instead of including it in the matrix below?
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.
For node 10 we can run the tests with and without experimental support flag. For Node 12 onwards, Github runs the latest node builds, which all have native support for ESM. For them the NODE_OPTIONS flags make no sense. That's why I separated the node 10 build to avoid having an additional build for every other node version
The import code is (and has always been) particularly tricksy; if you would be willing to write a few sentences on the details of what changed in |
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.
hey @dbushong . I added some more information on the change to the PR description as well as comment to the code.
Thank you for reviewing it 🍰
function directRequire(id, cwd) { | ||
if (!path.extname(id)) { | ||
id = `${id}.js`; | ||
} | ||
id = path.join(cwd, !isLocalRef(id) ? 'node_modules' : '', id); | ||
|
||
// eslint-disable-next-line import/no-dynamic-require | ||
return require(id); |
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.
@dbushong this the fallback code that is able to load files when this.require
usage fails e.g for ERR_PACKAGE_PATH_NOT_EXPORTED
(in this.import()
and this.requireOrNull()
|
||
const localFiles = await this.cachedGlob( | ||
`{modules,lib}/*/${basename}.{js,mjs}` | ||
`{modules,lib}/*/${basename}${!extension ? patterns : ''}` |
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.
supports extension when calling project.loadInterfaceFiles('object-graph.js')
for (const ext of extensions) { | ||
res = await this.importOrNull(`${specifier}.${ext}`); | ||
if (res !== null) { | ||
break; | ||
} | ||
} |
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.
iterates over possible extensions - on the first successful import/require it breaks. so if you have mjs
and js
source files, it picks mjs over js. for js
and cjs
it would try loading js
first
); | ||
if (moduleNamespace === null) return null; | ||
/** @type {({default: *}|{default: string}|null|undefined)[]} */ | ||
const moduleNamespaces = await Promise.all( |
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 kind of decoupled the logic of require/import and building the interface file array. makes it easier to debug
// handle ERR_PACKAGE_PATH_NOT_EXPORTED for modules with "exports" section (Node 13/14+) | ||
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') { | ||
try { | ||
return { default: directRequire(id, this.root) }; |
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.
if exports lacks the file declaration require directly. see https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_main_entry_point_export
return { default: directRequire(id, this.root) }; | ||
} catch (err) { | ||
// eslint-disable-next-line no-ex-assign | ||
e = err; |
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.
if direct require fails for example with MODULE_NOT_FOUND
or ERR_REQUIRE_ESM
I'm re-assigning the error, so later code or importOrNull
code will handle the errors.
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.
Sorry for the delay. LGTM - make sure to test it against a real project (one with esm, one with commonjs, maybe one with both?)
if (!isModule) { | ||
return ['mjs', 'js']; |
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.
question: At a glance this seems backwards - why are we looking for mjs files if type isn't module? (and vice-versa) Is it because otherwise they'd be found automatically?
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.
so normally the loadInterfaceFiles()
method is usually called without any file extension - eg. await project.loadInterfaceFiles('open-graph')
. Till now loading local mjs files (if they existed) used to work due the globbing https://github.com/groupon/nilo/blob/main/lib/project.js#L123-L125. By supporting type="module"
and also workaround incomplete exports
npm package settings, it became necessary to append the file type in order to be able to load the file. Otherwise you will likely get the ERR_REQUIRE_ESM
error.
I set the order of those file types is ESM type first, normal type next.
lib/project.js
Outdated
async _getLocalTargets(basename) { | ||
const extension = path.extname(basename); | ||
const isModule = this.packageJson.type === 'module'; | ||
const patterns = `.{${getExtensions(isModule).join()}}`; |
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.
nitpick: would prefer being explicit about the join character, to avoid people having to look up the quirk that it defaults to ,
const patterns = `.{${getExtensions(isModule).join()}}`; | |
const patterns = `.{${getExtensions(isModule).join(',')}}`; |
lib/project.js
Outdated
await this.importOrNull(specifier) | ||
); | ||
if (moduleNamespace === null) return null; | ||
/** @type {({default: *}|{default: string}|null|undefined)[]} */ |
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.
/** @type {({default: *}|{default: string}|null|undefined)[]} */ | |
/** @type {({ default: unknown } | null | undefined)[]} */ |
nitpick (optional): I think unknown
is what you want in this case. It's like any
or *
, but with the guarantee that you won't try to do anything with the contents.
lib/project.js
Outdated
} | ||
|
||
/** | ||
* @param {string} id | ||
* @returns {Promise<unknown>} | ||
* @returns {Promise<{default: *}|{default: string}>} |
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.
nitpick (optional):
* @returns {Promise<{default: *}|{default: string}>} | |
* @returns {Promise<{ default: unknown }>} |
0640d28
to
622293b
Compare
Tested it and works, thanks! |
Problems in current nilo version
exports
section but misses to export its package.json, then nilo will receive the errorERR_PACKAGE_PATH_NOT_EXPORTED
type="module"
, nilo was not able to load any files due toproject.loadInterfaceFiles('object-graph')
is generally called without specific file typeChanges
type="module"
) (with or withoutexports
package section)mjs/js
files (with or withoutexports
package section)