Skip to content
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

module: remove dynamicInstantiate loader hook #33501

Closed
wants to merge 1 commit into from

Conversation

jkrems
Copy link
Contributor

@jkrems jkrems commented May 21, 2020

The dynamicInstantiate loader hook requires that the hooks run in the
same global scope as the code being loaded. We don't want to commit to
this being true in the future. It stops us from sharing hooks between
multiple worker threads or isolating loader hook from the application
code.

Using getSource and getGlobalPreloadCode the same use cases should
be covered.

Checklist
  • make -j4 test passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added errors Issues and PRs related to JavaScript errors originated in Node.js core. esm Issues and PRs related to the ECMAScript Modules implementation. labels May 21, 2020
@jkrems jkrems force-pushed the loader-layers branch 2 times, most recently from 6241235 to be7253c Compare May 21, 2020 16:45
@jkrems
Copy link
Contributor Author

jkrems commented May 21, 2020

/cc @nodejs/modules-active-members

@jkrems jkrems requested review from bmeck and guybedford May 21, 2020 16:46
@@ -193,8 +177,8 @@ class Loader {
if (resolve !== undefined)
this._resolve = FunctionPrototypeBind(resolve, null);
if (dynamicInstantiate !== undefined) {
this._dynamicInstantiate =
FunctionPrototypeBind(dynamicInstantiate, null);
process.emitWarning(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't needed

Copy link
Contributor Author

@jkrems jkrems May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I started without it. But then the test still passed and I was super confused if anything happened. Having the warning did help me be confident that it actually ignored the hook.

That being said - happy to remove this last trace as well.

The dynamicInstantiate loader hook requires that the hooks run in the
same global scope as the code being loaded. We don't want to commit to
this being true in the future. It stops us from sharing hooks between
multiple worker threads or isolating loader hook from the application
code.

Using `getSource` and `getGlobalPreloadCode` the same use cases should
be covered.
if (module.builtinModules.includes(url)) {
const GET_BUILTIN = `$__get_builtin_hole_${Date.now()}`;

export function getGlobalPreloadCode() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason for needing this is because this hook is messing with the builtin module module. Otherwise the generated code could import {createRequire} from 'module' without needing a global.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to push users into this hook now, then I think we should aim to work on this usability first - you shouldn't need to define a secret global for the common use case...

const builtinExports = [
...Object.keys(builtinInstance),
];
return `\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems horrible enough to constitute figuring out some sort of replacement for dynamicInstantiate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the hackiest version of generating the code. I don't agree that the loader hooks providing proper JS code is horrible in general. It's possible to create a dynamicInstantiate-like API on top of this primitive.

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'm approving this PR, but on the condition that we put some thought into the new hook usability before landing.

if (module.builtinModules.includes(url)) {
const GET_BUILTIN = `$__get_builtin_hole_${Date.now()}`;

export function getGlobalPreloadCode() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to push users into this hook now, then I think we should aim to work on this usability first - you shouldn't need to define a secret global for the common use case...

@bmeck
Copy link
Member

bmeck commented May 22, 2020

I've been trying to review what the ecosystem has been doing with loaders in our branch before making any sudden movements. I'd agree that for now they are using globals (and writing blog posts about it). I think we definitely need a 1st class communications channel between modules and getPreloadCode but I'm unclear on what it would look like. However, there don't appear to be needs for everyone to use dynamic instantiate to achieve workflows and some appear to be avoiding the hook. I think we might want to figure out a communications channel before landing this PR, but I would not want to block on solving the communications channel issue.

Copy link
Member

@bmeck bmeck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM pending if we solve first class values to comms between getGlobalPreloadCode and generated code in a follow up.

@devsnek
Copy link
Member

devsnek commented May 22, 2020

I'd like us to have some definitive way of creating modules from data (not transformed code) that doesn't require generating javascript source text.

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

That sound like it would require a new format then (“data”?). That seems possible as long as we specify it as transferable data.

@devsnek
Copy link
Member

devsnek commented May 22, 2020

@jkrems well it is a bit more complex, you're generally working with live values, for example webassembly instances, a kind of data you might want to expose as an es module, can't be transferred.

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

One thing I would say, in a world where loaders start being integrated: I’m starting to think that our window is closing for a change like this, experimental status or not. So a possible outcome here would be that we give up on scope isolation and loaders are just stuck running in the app context.

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

@devsnek I assume you can’t instantiate a WebAssembly module from outside the isolate. So is that a general objection to shared loaders?

@devsnek
Copy link
Member

devsnek commented May 22, 2020

@jkrems i don't object to shared loaders, but i'm not a fan of removing unshared loaders.

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

I’m not sure I follow - as in: some loaders would run in one isolate but then others wouldn’t run in that isolate. How would that compose? If the typescript loader is shared and makes assumptions based on being shared, would it be impossible to use it in an app that also needs a non-shared loader? I can’t think of a layout where we could have both without it being pretty confusing. I think we’d have to make a choice on how loaders work, otherwise I’m afraid of a complexity explosion.

@devsnek
Copy link
Member

devsnek commented May 22, 2020

@jkrems i don't think it makes that much complexity, aside from having to choose where the loader you're writing runs. from the perspective of the node internal loader it's just a bunch of loader.resolveOrWhatever calls which might communicate with a worker thread or call a function directly.

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

But if losers compose, their return values get chained. So a loader that runs after a non-shared loader has to be non-shared. Otherwise it wouldn’t be able to access the return value of its parent that ran on a different isolate.

@devsnek
Copy link
Member

devsnek commented May 22, 2020

@jkrems you shouldn't be able to chain the result of hooks which create actual module instances, regardless of where they run, there's no composition you can do at that point.

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

It would create a situation where loaders have to choose between mutually exclusive modes which means that loaders can’t be directly combined in userland. And it means that a loader that wants to proxy/instrument others can’t be shared. As I said: I’d consider that situation pretty bad UX. I think it would effectively mean that shared loaders are DOA because there’s always a risk that weeks into development of a shared one it would turn out that compat with a non-shared loader forces to abandon sharing.

So, at least from perspective I wouldn’t want to make loaders even more complicated for something that would be a foot gun to users. So if there are objections to making loaders exclusively shared, I would rather invest time in making non-shared loaders better than to tack on a semi-supported shared mode. It’s unfortunate for resource usage etc but maybe that can be worked out in userland.

@devsnek
Copy link
Member

devsnek commented May 22, 2020

@jkrems i don't understand at all why allowing this means loaders can't be composed. can you expand on that point?

@jkrems
Copy link
Contributor Author

jkrems commented May 22, 2020

Let’s say I want to write a userland package that allows various transformation and instrumentation. The order of each of the things it’s trying to do is nice and linear. But some of the tasks are implemented in packages written as shared loaders, some in packages written as non-shared loaders. I would somehow have to tell node to run this combined/composed loader as both a shared and a non-shared loader. Actually, likely multiple times so I can actually run the interleaved steps in the correct mode. I don’t think this is acceptable UX.

@devsnek
Copy link
Member

devsnek commented May 22, 2020

@jkrems i think the loaders say whether they are shared or not, you just pass --loader x --loader y like normal.

as both a shared and a non-shared loader

not sure what you mean here, i'm imagining they're one or the other

@jkrems jkrems added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label May 26, 2020
jkrems added a commit that referenced this pull request May 27, 2020
The dynamicInstantiate loader hook requires that the hooks run in the
same global scope as the code being loaded. We don't want to commit to
this being true in the future. It stops us from sharing hooks between
multiple worker threads or isolating loader hook from the application
code.

Using `getSource` and `getGlobalPreloadCode` the same use cases should
be covered.

PR-URL: #33501
Reviewed-By: Geoffrey Booth <[email protected]>
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: Bradley Farias <[email protected]>
@jkrems
Copy link
Contributor Author

jkrems commented May 27, 2020

Landed in d12d5ef

@jkrems jkrems closed this May 27, 2020
@GeoffreyBooth GeoffreyBooth removed the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label May 27, 2020
codebytere pushed a commit that referenced this pull request Jun 18, 2020
The dynamicInstantiate loader hook requires that the hooks run in the
same global scope as the code being loaded. We don't want to commit to
this being true in the future. It stops us from sharing hooks between
multiple worker threads or isolating loader hook from the application
code.

Using `getSource` and `getGlobalPreloadCode` the same use cases should
be covered.

PR-URL: #33501
Reviewed-By: Geoffrey Booth <[email protected]>
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: Bradley Farias <[email protected]>
@codebytere codebytere mentioned this pull request Jun 28, 2020
codebytere pushed a commit that referenced this pull request Jun 30, 2020
The dynamicInstantiate loader hook requires that the hooks run in the
same global scope as the code being loaded. We don't want to commit to
this being true in the future. It stops us from sharing hooks between
multiple worker threads or isolating loader hook from the application
code.

Using `getSource` and `getGlobalPreloadCode` the same use cases should
be covered.

PR-URL: #33501
Reviewed-By: Geoffrey Booth <[email protected]>
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: Bradley Farias <[email protected]>
yorkie added a commit to alibaba/pipcook that referenced this pull request Jul 15, 2020
See nodejs/node#33501
At Node.js 14.5.0, it removes the dynamic module format, so we need to
make compatible works for the new usage with esm loader.

Node.js Release PR: nodejs/node#34093
yorkie added a commit to alibaba/pipcook that referenced this pull request Jul 15, 2020
See nodejs/node#33501
At Node.js 14.5.0, it removes the dynamic module format, so we need to
make compatible works for the new usage with esm loader.

Node.js Release PR: nodejs/node#34093
gindis pushed a commit to gindis/pipcook that referenced this pull request Sep 11, 2020
See nodejs/node#33501
At Node.js 14.5.0, it removes the dynamic module format, so we need to
make compatible works for the new usage with esm loader.

Node.js Release PR: nodejs/node#34093
@EntraptaJ
Copy link

I'm unable to recreate my CommonJS exporter system in getSource or getGlobalPreloadCode I was using dynamicInstantiate to support deconstructed imports in ESM code of CommonJS modules, which I now can't find another way of doing. Meaning my TS-ESNode is no longer a drop in replacement, leading to the adoption of ESM Modules not being as simple as I was able to with pre Node 14.4

export async function dynamicInstantiate(
  url: string,
): Promise<DynamicInstantiateResponse> {
  const urlParts = url.split('/node_modules/');

  // Extract the module name after node_modules.
  const moduleName = urlParts.pop()!;

  // With NPM, this is just top-level node_modules.
  // With PNPM, this is the innermost node_modules.
  const nodeModulesPath = urlParts.join('/node_modules/');

  // Create a require function next to node_module, and import the CommonJS module.
  const require = createRequire(`${nodeModulesPath}/noop.js`);
  let dynModule = require(moduleName);

  // Adapt to default exports in CommonJS module.
  if (dynModule.default && dynModule !== dynModule.default) {
    dynModule = {
      ...dynModule.default,
      ...dynModule,
    };
  }

  // Export as ES Module.
  const linkKeys = Object.keys(dynModule);
  const exports = dynModule.default ? linkKeys : [...linkKeys, 'default'];
  return {
    exports,
    execute: (module): void => {
      module.default.set(dynModule);
      for (const linkKey of linkKeys) {
        module[linkKey].set(dynModule[linkKey]);
      }
    },
  };
}

@jkrems
Copy link
Contributor Author

jkrems commented Sep 15, 2020

@KristianFJones If you want to support named imports[1], you can generate a wrapper module. It's a bit more manual but works very similar:

return {
  type: 'module',
  source: `
import {createRequire} from 'module';

const require = createRequire(new URL(import.meta.url));
const cjs = require(${JSON.stringify(cjsFilePath)});

${exports.map(
  prop => `let $${prop} = cjs[${JSON.stringify(prop)}];`
).join(';\n')}

export {
${exports.map(
  prop => `  $${prop} as ${prop},`
).join('\n')}
}
`,
};

I will throw in that by doing what you do here, you're opting into a different execution order than what people may be expecting: All CJS will execute first and then the ESM modules execute. Which likely won't match how the code ran when everything was compiled to CJS.

[1] "Deconstruct" is usually used when talking about creating new local variables and initializing them with values of object properties or array elements. Named imports expose the original variables and don't create new ones so "deconstruct" is a bit misleading as a term here.

@EntraptaJ
Copy link

@jkrems Thanks, is that actual code or just a quick napkin snippet, because I'm unaware of a hook that returns the type and the source. I've been out of the programming scene for 3 months, so I'm a bit behind on the latest development.

@jkrems
Copy link
Contributor Author

jkrems commented Sep 15, 2020

It's something a getSource hook could return. It does require a getFormat hook as well right now to return the format IIRC. It's a napkin snippet but it's pretty close to the actual APIs.

@EntraptaJ
Copy link

EntraptaJ commented Sep 15, 2020

@jkrems You're a genius. Thanks for your help. I think from this I can get @K-FOSS/TS-ESNode working again! I've gotten it working mostly locally, just need to fix all the other broken things from the last 3 months

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
errors Issues and PRs related to JavaScript errors originated in Node.js core. esm Issues and PRs related to the ECMAScript Modules implementation.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants