Issue 1:
Current webpack modules have several details that are not exactly spec-compliant, particularly when circular references are involved. It is "good enough" for most current cases, but in order to be as close to the spec as we can get without Proxy, I was thinking of how to change the way webpack emits ES modules.

This tries to emit modules as close to the ES2017 and the current draft of EPS 002 as possible.

To simplify things, I'll write the emits as if NamedModulesPlugin is being used.

The generated export names are also not minified here, but should (usually) be minified by webpack.

The actual values of the generated identifiers don't matter; e.g.

__imported_NAME__ could still be the same __WEBPACK_IMPORTED_MODULE_N_NAME__; I'm only using it as a shorthand.

The helpers attached to __webpack_require__ would also, of course, be minimized similarly to current helpers.

It is not yet decided for sure whether commonjs modules will expose object keys, or only as a default export. It is, however, decided, that the namespace created for commonjs modules will still be a module namespace exotic object, so commonjs exports are never directly returned.

__webpack_require__ helpers

__webpack_require__.module = function module(moduleId) {
// Check if module is in cache
if(installedModules[moduleId] && installedModules[moduleId].ns /*namespace*/)
return installedModules[moduleId].ns;

var exports = __webpack_require__(moduleId);

// this should actually be in the base __webpack_require__,
// should any commonjs require of an ES module happen.
if (installedModules[moduleId].m) {
installedModules[moduleId].exports = installedModules[moduleId].ns;
}

if (!installedModules[moduleId].m/*odule*/) {
if (exports.__esModule) {
installedModules[moduleId].ns = exports;
} else {
// https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md#541-default-imports
// is it safe to capture 'exports' instead of 'installedModules[moduleId]' even when hot reloading?
var defs = { "default": function() { return installedModules[moduleId].exports; } };
for (var name in exports) {
if (name === "default") continue;
if (!Object.prototype.hasOwnProperty.call(exports, name)) continue;
defs[name] = function() { return installedModules[moduleId].exports[name]; };
}
__webpack_require__.export(installedModules[moduleId], defs);
installedModules[moduleId].m = 0; // falsy; minifies better than false
}
}

return installedModules[moduleId].ns;
}

compat is only true when the module is also consumed by a commonjs module. nofreeze is used

to support chaining with the other helpers; but the last helper called should always be allowed to

freeze the namespace.

__webpack_require__.export = function export(module, specifiers, nofreeze, compat) {
module.m/*odule*/ = 1; // truthy; 1 minifies better than true

var ns = module.ns /*namespace*/ = module.ns || (Object.create ? Object.create(null) : {});
if (typeof Symbol === "function" && Symbol.toStringTag) {
Object.defineProperty(ns, Symbol.toStringTag, { value: "Module" })
}
if (compat) {
Object.defineProperty(ns, "__esModule", { value: true });
}
for (var name of specifiers) {
if (!Object.prototype.hasOwnProperty.call(specifiers, name)) continue;
Object.defineProperty(ns, name, { enumerable: true, get: specifiers[name] });
}
if (!nofreeze) Object.freeze(ns);
}

This helper is used when accessing a property that cannot be statically verified

to exist in the namespace.

__webpack_require__.namespaceGet = function namespaceGet(ns, name) {
if (!Object.prototype.hasOwnProperty.call(ns, name)) throw new SyntaxError("Unknown export " + name);
return ns[name];
}

This helper is only used if implicit dependencies are allowed by the spec.

__webpack_require__.reexport = function reexport(module, reexports, nofreeze, compat) {
if (!module.m) __webpack_require__.export(module, {}, true, compat);

var specifiers = {};
for (var name in reexports) {
if (typeof reexports[name] !== "object") {
specifiers[name] = makeNsReexport(reexports[name]);
} else {
var moduleId = reexports[name][0];
var reexport = reexports[name][1];
specifiers[name] = makeReexport(moduleId, reexport);
}
}
__webpack_require__.export(module, specifiers, nofreeze)

function makeNsReexport(moduleId) {
return function () { return __webpack_require__.module(moduleId); }
}
function makeReexport(moduleId, reexport) {
return function() { return __webpack_require__.module(moduleId)[reexport]; }
}
}

reexportAll always should always be the last helper called, so it always freezes the exports.

This should only be called for reexporting commonjs dependencies, or other modules whose set of exports is not known (if they, say, reexport a commonjs dependency).

This helper (and the special case for out-of-order importing) is only needed because EPS 002 says the module.export keys should be hoisted. Should that requirement be removed and the only apparent export is default, all reexportAll can just compile into corresponding imports -- default keys are never reexported via star reexporting.

It must necessarily require the modules in order to be able to see their exports and add the

accessors before the freeze. The exports are added after requiring each module to avoid further

trouble with circular references.

The duplicate reexports actually should be allowable when their values are SameValue,

but it is unclear how that should apply to mutable bindings, since they can always mutate to not

be SameValue anymore.

The reexport getters call __webpack_require__.module to try and deal better with hot reloading.

__webpack_require__.reexportAll = function reexportAll(module, cjsDeps, ownExports, compat) {
if (!module.m) __webpack_require__.export(module, {}, true, compat);

for (var i = 0; i < cjsDeps.length; i++) {
var dep = cjsDeps[i];
var specifiers = {};
var ns = __webpack_require__.module(dep);
for (var name of ns) {
if (name === "default") continue;
if (!Object.prototype.hasOwnProperty.call(ns, name)) continue;
if (ownExports.indexOf(name) >= 0) continue;
if (Object.prototype.hasOwnProperty.call(module.ns, name)) throw new SyntaxError("Duplicate reexport " + name);
specifiers[name] = makeReexportAll(dep, name);
}
__webpack_require__.export(module, specifiers, true);
}

Object.freeze(module.ns);

function makeReexportAll(moduleId, reexport) {
return function() { return __webpack_require__.namespaceGet(__webpack_require__.module(moduleId), reexport); }
}
}

This helper should be called by the DelegatedModule plugin when the exposed module

is an ES module. CJS modules should be exposed with the currently existing template.

__webpack_require__.expose = function expose(module, moduleId) {
__webpack_require__.module(moduleId);
var exposed = installedModules[moduleId];
module.m = exposed.m;
module.ns = exposed.ns;
module.exports = exposed.exports;
}

Imports

All imports and export default should use let for correct TDZ, but the emit would not be Uglify-compatible, so they are written with var.

All of the sample code has an implied "use strict" unless noted.

default

A default import of, say, import identifier from will be treated exactly as import { default as identifier } from, that is, it's seen as a named import.

import defaultExport from './esmodule'
defaultExport

var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");
__imported_esmodule__["default"];

named

import { named as renamed } from './esmodule'
renamed

var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");
__imported_esmodule__.named;

namespace with static member access

This includes bracketed access with statically-analyzable keys.

import * as namespace from './esmodule'
namespace.named

var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");
__imported_esmodule__.named;

namespace with dynamic member access

This cannot have perfect coverage without using Proxy, but should be close

enough for most cases.

import * as namespace from './esmodule'
namespace[(() => 'named')()]

var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");
__webpack_require__.namespaceGet(__imported_esmodule__, (() => 'named')());

commonjs require

Should have no changes.

const namespace = require('./esmodule')

Output should not have an implied "use strict".

const namespace = __webpack_require__("./esmodule.js");

Exports

In ES modules, the wrapper function should be changed to:

function (__webpack_module__, _ignored_unique_identifier, /* rest */)

None of the helpers, or any of the generated code, should depend on the second argument to the wrapper

function. However, it still needs to be provided for compatibility with CJS modules.

The __webpack_default__ identifier mentioned in the output should be a guaranteed-unique

identifier (i.e. not visible from user code). Ideally, this would be the same with __webpack_module__,

but it must still be available to support webpack's meta-properties like module.hot.

Speaking of the meta-properties, they should be automatically translated from module to apply to

__webpack_module__ instead.

export default

of anonymous function

Note the {default: function() {}}.default wrapper. This is needed to ensure the anonymous function or class receives the correct Function#name. This also should be hoisted to happen immediately after the "use strict";, before any helpers or imports are called (barring special cases like ProvidePlugin).

This also applies to async function.

export default function () {}

var __webpack_default__ = { "default": function () {} }["default"];
__webpack_require__.export(__webpack_module__, {
"default": function() { return __webpack_default__; }
});

of anonymous class

The same wrapper is employed, but it is not hoisted like in the anonymous function case. All engines that support class also support let, so it should be safe to use let in this case to ensure proper TDZ.

export default class {}

__webpack_require__.export(__webpack_module__, {
"default": function() { return __webpack_default__; }
});
let __webpack_default__ = { "default": class {} }["default"];

of named function or class

No __webpack_default__ or explicit hoisting involved.

export default class Name {}

__webpack_require__.export(__webpack_module__, {
"default": function() { return Name; }
});
class Name {}

of expression (i.e. anything else)

No hoisting allowed.

export default 1 + 1

__webpack_require__.export(__webpack_module__, {
"default": function() { return __webpack_default__; }
});
var __webpack_default__ = (1 + 1);

Named export

With specifier

A specifier without as is treated as if it was exported as itself. For example, export { foo } is treated the same as export { foo as foo }.

let foo = 1
export { foo as bar }

__webpack_require__.export(__webpack_module__, {
"bar": function () { return foo; }
});
let foo = 1

With declarations

The emit is the same regardless of the declaration type; however, var, let and const can specify more than one name.

export function foo () {}
export class Bar {}
export let hoge, fuga

__webpack_require__.export(__webpack_module__, {
"foo": function () { return foo; },
"Bar": function () { return Bar; },
"hoge": function () { return hoge; },
"fuga": function () { return fuga; }
});
function foo () {}
class Bar {}
let hoge, fuga

Reexports

This depends on the answer to https://gist.github.com/Kovensky/2caf0eaefc9c00b46ab5afaed59ebd0b

If it's allowed to be an implicit dependency, a much simpler reexport emit can be produced.

If it has to be a TDZ error, then reexports are emitted identically to import { name } from followed by export { name }.

of namespace

import * as namespace from './esmodule'
export { namespace }

Implicit dependency:

__webpack_require__.reexport(__webpack_module__, {
"namespace": "./esmodule.js"
});
var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");

TDZ (doesn't quite work because of the var instead of const, but...):

__webpack_require__.export(__webpack_module__, {
"namespace": function () { return __imported_esmodule__; }
});
var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");

If the exported namespace comes from a cjs module, or a harmony module that reexports

a cjs module, all accesses to members of this export should use the namespaceGet helper.

of named

export { default, foo as bar } from './esmodule'

Implicit dependency:

__webpack_require__.reexport(__webpack_module__, {
"default": ["./esmodule.js", "default"],
"bar": ["./esmodule.js", "foo"]
});
var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");

TDZ:

__webpack_require__.export(__webpack_module__, {
"default": function () { return __imported_esmodule__["default"]; }
"bar": function () { return __imported_esmodule__["foo"]; }
});
var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");

Webpack will not be able to statically check if the reexported symbols actually

exist in a commonjs module, so when reexporting names from them, the generated

getter should return something similar to

__webpack_require__.namespaceGet(__imported_cjs__, "foo").

Star Reexports

Assuming "./esmodule.js" has default, foo and bar as exports.

of ES module

export * from './esmodule'
export function foo () {}

Implicit dependency:

As, in this example, exports are combined with reexports, an additional argument to __webpack_require__.export is required.

__webpack_require__.export(__webpack_module__, {
"foo": function () { return foo; }
}, true);
__webpack_require__.reexport(__webpack_module__, {
"bar": [ "./esmodule.js", "bar" ]
});
var __imported_esmodule__ = __webpack_require__("./esmodule.js");
function foo () {}

TDZ:

__webpack_require__.export(__webpack_module__, {
"foo": function () { return foo; },
"bar": function () { return __imported_esmodule__["bar"]; }
});
var __imported_esmodule__ = __webpack_require__("./esmodule.js");
function foo () {}

of CJS module

This emit is the same regardless of Implicit dependency or TDZ.

reexportAll receives the list of all named exports local to this module.

This includes explicit reexports, but does not include names coming from

other star reexports.

As reexportAll will freeze the exports, it will have to internally

__webpack_require__ the dependent modules in order to be able to expose

their exports.

This also poisons all imports of this module such that they have to use

the __webpack_require__.namespaceGet helper to be able to read any

export that is not statically known to exist.

export * from './cjs'
export function foo () {}

__webpack_require__.export(__webpack_module__, {
"foo": function () { return foo; }
}, true);
__webpack_require__.reexportAll(__webpack_module__, [
"./cjs.js"
], [ "foo" ])
function foo () {}

All export kinds combined

With the same esmodule as the previous section.

Also has an import and a default anonymous export to demonstrate ordering.

import * as some from 'some-module'
export { some }
export default function () {}
require('other-module')
export let foo
export { default as baz } from './esmodule'
export * from './esmodule';
export * from './cjs';

Implicit dependency:

When combining ES star reexport with CJS star reexport, a true third argument is required on reexport as well.

//exports
var __webpack_default__ = { "default": function () {} }["default"];
__webpack_require__.export(__webpack_module__, {
"default": function () { return __webpack_default__; },
"foo": function () { return foo; }
}, true);
__webpack_require__.reexport(__webpack_module__, {
"some": "some-module",
"baz": [ "./esmodule.js", "default" ]
}, true);
__webpack_require__.reexportAll(__webpack_module__, [
"./cjs.js"
], [ "some", "foo", "baz" ]);
// imports
var __imported_some_module__ = __webpack_require__.module("some-module");
var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");
// code
__webpack_require__("other-module");
let foo;

TDZ:

// exports
var __webpack_default__ = { "default": function () {} }["default"];
__webpack_require__.export(__webpack_module__, {
"some": function () { return __imported_some_module__; },
"default": function () { return __webpack_default__; },
"foo": function () { return foo; },
"baz": function () { return __imported_esmodule__["default"]; }
}, true);
__webpack_require__.reexportAll(__webpack_module__, [
"./cjs.js"
], [ "some", "foo", "baz" ]);
// imports
var __imported_some_module__ = __webpack_require__.module("some-module");
var __imported_esmodule__ = __webpack_require__.module("./esmodule.js");
// code
__webpack_require__("other-module");
let foo;

Issue 2:
Bug report

I have an issue with planout that bundled fine with Webpack 4 but no longer works with Webpack 5.

What is the current behavior?

Module is not loaded:

Uncaught TypeError: Getter must be a function: A
at Function.defineProperty (<anonymous>)
at Function.__webpack_require__.d (bundle.js:963)
at eval (index.js:2)
at Module../build/index.js (planout.js:102)
at __nested_webpack_require_539__ (planout.js:25)
at eval (planout.js:89)
at eval (planout.js:92)
at webpackUniversalModuleDefinition (planout.js:3)
at eval (planout.js:5)
at Object../node_modules/planout/dist/planout.js (bundle.js:291)

If the current behavior is a bug, please provide the steps to reproduce.

Repo Here:

https://github.com/gita-v/webpack_issue

What is the expected behavior?

For module to load :)

Other relevant information:

webpack version: 5.3.2

Node.js version: 14.14.0