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

feat: add new external type "module-import" #18620

Merged
merged 7 commits into from
Aug 6, 2024

Conversation

fi3ework
Copy link
Contributor

@fi3ework fi3ework commented Jul 29, 2024

What kind of change does this PR introduce?

Resolve #17986.

Documentation update: webpack/webpack.js.org#7345.


Following content maybe somehow out of context. This PR's final implementation is to resolve #17986.

A new external type has been introduced, mainly for the purpose of output clean artifact during bundling library. The name of this "module-import" is referenced from #17986 (comment), but I'm not sure if the current implementation fully meets the intention of this comment. This PR is an incomplete implementation, only implementing the handling of external processing for dynamic imports.

Here's what it does:

Given following config:

// webpack.config.js
externals: {
  path: 'path node-commonjs',
  fs: 'module-import fs',
  'node:fs': 'module-import node:fs',
  'node-fs': 'module-import fs',
},

source

it("should allow async externals", async () => {
	const fs1 = await import("fs");
	const fs2 = await import("node:fs");
	const fs3 = await import("node-fs");

	expect(fs1).toStrictEqual(fs2);
	expect(fs1).toStrictEqual(fs3);
});

output

var __webpack_exports__ = {};
/*!******************!*\
  !*** ./index.js ***!
  \******************/
it("should allow async externals", async () => {
	const fs1 = await import("fs");
	const fs2 = await import("node:fs");
	const fs3 = await import("fs");

	expect(fs1).toStrictEqual(fs2);
	expect(fs1).toStrictEqual(fs3);
});

There is a slightly hacky bit here, where we're deleting ImportDependency at finishModules stage to prevent any runtime code from being generated. But I couldn't find a better way to prevent ImportDependencyTemplate from modifying the runtime code. Possibly by introducing a new DependencyLibraryTemplate specifically for library patterns could achieve that. Do you have any suggestions on how to suppress runtime code generation? @alexander-akait

Did you add tests for your changes?

Yes.

Does this PR introduce a breaking change?

No.

What needs to be documented once your changes are merged?

The PR is still in draft mode, it should add update the document before merged.

@webpack-bot
Copy link
Contributor

webpack-bot commented Jul 29, 2024

For maintainers only:

  • This needs to be documented (issue in webpack/webpack.js.org will be filed when merged)
  • This needs to be backported to webpack 4 (issue will be created when merged)

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

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

I think we can preserve import ... from '...' and import() using instanceof without extra hooks like here https://github.com/webpack/webpack/blob/main/lib/ExternalModuleFactoryPlugin.js#L120

@fi3ework
Copy link
Contributor Author

fi3ework commented Jul 30, 2024

Thanks for the suggestion. IIUC, does the code snippet demonstrate whether it meets your suggestions? Or keep the original method that removing the dep from module block.

// lib/ExternalModuleFactoryPlugin.js

 					if (
 						type === "module-import" &&
 						dependency instanceof ImportDependency
 					) {
 						dependency.runTemplate = false;
 					}

					callback(
						null,
						new ExternalModule(
							externalConfig,
							type || globalType,
							dependency.request,
							dependencyMeta
						)
					);
// lib/dependencies/ImportDependency.js

 		if (dep.run_template === false) {
 			return;
 		}

		const content = runtimeTemplate.moduleNamespacePromise({
			chunkGraph,
			block: block,
			module: /** @type {Module} */ (moduleGraph.getModule(dep)),
			request: dep.request,
			strict: /** @type {BuildMeta} */ (module.buildMeta).strictHarmonyModule,
			message: "import()",
			runtimeRequirements
		});
		source.replace(dep.range[0], dep.range[1] - 1, content);
	}

@alexander-akait
Copy link
Member

I don't think we need ExternalModuleImportDependency at all, just check if it is ImportDependency or HarmonyImportDependency and so use import() or import ... from '...' (just set it in dependencyMeta) when we use module-import

@fi3ework
Copy link
Contributor Author

just check if it is ImportDependency or HarmonyImportDependency and so use import() or import ... from '...' (just set it in dependencyMeta) when we use module-import

IIUC, ImportDependencyTemplate will replace import('xxx') to Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, "xxx")) (https://github.com/webpack/webpack/blob/main/lib/dependencies/ImportDependency.js#L125-L135). How could we stop the source replacement through dependencyMeta? 🤔

I don't think we need ExternalModuleImportDependency at all

Based on above, if we stop ImportDependencyTemplate from replacing the code. Using ExternalModuleImportDependency could achieve alias mapping (e.g. mapping import fs from 'fs' to 'import fs from 'fs-xxx') like other external.

@alexander-akait
Copy link
Member

IIUC, ImportDependencyTemplate will replace import('xxx') to Promise.resolve(/* import() */).then(webpack_require.bind(webpack_require, "xxx")) (https://github.com/webpack/webpack/blob/main/lib/dependencies/ImportDependency.js#L125-L135). How could we stop the source replacement through dependencyMeta? 🤔

Hm, intresting, it seemed to me that we do nothing in this case if the module is external, can you check it?

@fi3ework
Copy link
Contributor Author

fi3ework commented Jul 30, 2024

Not sure what should I check, I created a online demo to show how "import" and "module" external type be processed now. 👀 hopes it might help. https://stackblitz.com/edit/github-ztep8o-umy9wa?file=dist/main.js&terminal=

The methods to deal with the ImportDependencyTemplate I know now:

  • In this PR: remove the ImportDependency in ExternalsPlugin, but this may affect unknown features.
  • The methods thought of in the above discussion: add a template flag to ImportDependencyTemplate from ExternalModuleFactoryPlugin.

@fi3ework
Copy link
Contributor Author

fi3ework commented Aug 1, 2024

Is there any other information I can provide to help now? 👀

@alexander-akait
Copy link
Member

alexander-akait commented Aug 2, 2024

@fi3ework What about this? Now we keep import ... from '...'; and import('...') logic as they were written (the original issue about it), anyway if you want to keep await import('...') without extra wrapping we should make it under library: "modern-module" (and use hooks to remove/change modules there)

@fi3ework
Copy link
Contributor Author

fi3ework commented Aug 4, 2024

Thanks for the kind response and code update. I wrongly mixed the mission of "module-import" with "modern-module" before.

module-import external type (proposed at #17986): just a mixing of existing module and import, use module for static import and import for dynamic import, determined by the type of dependency as described above.

As for wiping out the extra runtime for modern-module library type, I'll finish that in separated PRs. module-import will be a good first step for it.

I'll update this PR later.

@fi3ework
Copy link
Contributor Author

fi3ework commented Aug 5, 2024

PR updated and documentation update alongside (webpack/webpack.js.org#7345).

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

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

Looks good, I think we can use this value as default (https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L271) when ES modules enabled, because modules are experemental we can change the value, I think this is more semantically correct and also reduces loading time avoiding initial loading, what do you think?

@alexander-akait
Copy link
Member

Also looks like we need to fix one our test with with { ... } syntax 😄

@fi3ework
Copy link
Contributor Author

fi3ework commented Aug 5, 2024

Looks good, I think we can use this value as default (main/lib/config/defaults.js#L271) when ES modules enabled, because modules are experemental we can change the value, I think this is more semantically correct and also reduces loading time avoiding initial loading, what do you think?

I changed the default value. Agreed that the import() should keep dynamic in semantics.

Also looks like we need to fix one our test with with { ... } syntax 😄

Fixed 😄.

@alexander-akait
Copy link
Member

Let's update snapshot 😄

@alexander-akait alexander-akait merged commit 740ec9e into webpack:main Aug 6, 2024
54 of 57 checks passed
@webpack-bot
Copy link
Contributor

I've created an issue to document this in webpack/webpack.js.org.

@alexander-akait
Copy link
Member

Thank you

@fi3ework
Copy link
Contributor Author

fi3ework commented Aug 8, 2024

@alexander-akait We implement "module-import", but the premise is that we're using import or import() for external request, However, for example, if we use require('...'), "module-import" will failed to know which externals type to adopt ("import", "module", or even "node-commonjs"), and it will crash.

The possible options we may want to fall back to:

  1. node-commonjs: to let require('path') code could run in Node.js ESM (this is widely used).
  2. import: maybe for some specific reason.
  3. module: maybe for some specific reason.
  4. commonjs: level the require there, to be bundled by a bundler.

Here are some possible ways to adding the fallback:

  1. Work with externalsPresets.

    • when externalsPresets is a node.js-related presets, fallback to node-commonjs;
    • when externalsPresets === 'web', fallback to module;
    • when externalsPresets === 'web-async', fallback to import;

    If externalsPresets is not specified, we could set the fallback to commonjs (🤔 I'm not sure, this might break module-import's semantics)

  2. Adding module-import-node-commonjs, module-import-import, module-import-module, module-import-commonjs, maybe too verbose.

  3. Adding a new config exteralsFallback, but this will only work with "module-import", but adding an option on the top is a high tradeoff.

Personally, I would choose Plan 1.

What do you think, I could make a quick fix before next version releasing if the fallback is confirmed.

@alexander-akait
Copy link
Member

@fi3ework hm, yeah, I see it, let's try to use 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dynamic Imports that are marked external are hoisted
3 participants