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

When inferring the module system (esm vs cjs) the nearest pacakge.jso… #771

Merged
merged 3 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@
"commonjs": false,
"es6": true
}
},
{
"files": ["test/**/fixtures/cjs/esm/*.js"],
"parserOptions": {
"ecmaVersion": 2015,
"sourceType": "module"
},
"env": {
"commonjs": false,
"es6": true
}
},
{
"files": ["test/**/fixtures/esm/cjs/*.js"],
"env": {
"commonjs": true,
"es6": true
}
}
]
}
6 changes: 1 addition & 5 deletions loader/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ function ReactRefreshLoader(source, inputSourceMap, meta) {
*/
async function _loader(source, inputSourceMap) {
/** @type {'esm' | 'cjs'} */
let moduleSystem = 'cjs';
// Only try to resolve the module system if the environment is known to support ES syntax
if (this.environment != null) {
moduleSystem = await getModuleSystem.call(this, ModuleFilenameHelpers, options);
}
const moduleSystem = await getModuleSystem.call(this, ModuleFilenameHelpers, options);

const RefreshSetupRuntime = RefreshSetupRuntimes[moduleSystem];
const RefreshModuleRuntime = getRefreshModuleRuntime(Template, {
Expand Down
94 changes: 72 additions & 22 deletions loader/utils/getModuleSystem.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const { promises: fsPromises } = require('fs');
const path = require('path');
const commonPathPrefix = require('common-path-prefix');
const findUp = require('find-up');

/** @type {Map<string, string | undefined>} */
let packageJsonTypeMap = new Map();
Expand Down Expand Up @@ -43,29 +41,81 @@ async function getModuleSystem(ModuleFilenameHelpers, options) {
if (/\.mjs$/.test(this.resourcePath)) return 'esm';
if (/\.cjs$/.test(this.resourcePath)) return 'cjs';

// Load users' `package.json` -
// We will cache the results in a global variable so it will only be parsed once.
let packageJsonType = packageJsonTypeMap.get(this.rootContext);
if (!packageJsonType) {
if (typeof this.addMissingDependency !== 'function') {
// This is Webpack 4 which does not support `import.meta` and cannot use ESM anwyay. We assume .js files
// are commonjs because the output cannot be ESM anyway
return 'cjs';
}

// We will assume commonjs if we cannot determine otherwise
let packageJsonType = '';

// We begin our search for relevant package.json files at the directory of the
// resource being loaded.
// These paths should already be resolved but we resolve them again to ensure
// we are dealing with an aboslute path
const resourceContext = path.dirname(this.resourcePath);
let searchPath = resourceContext;
let previousSearchPath = '';
// We start our search just above the root context of the webpack compilation
const stopPath = path.dirname(this.rootContext);

// if the module context is a resolved symlink outside the rootContext path then we will never find
// the stopPath so we also halt when we hit the root. Note that there is a potential that the wrong
// package.json is found in some pathalogical cases like if a folder that is conceptually a package
// but does not have an ancestor package.json but there is a package.json higher up. This might happen if
// you have a folder of utility js files that you symlink but did not organize as a package. We consider
// this an edge case for now
while (searchPath !== stopPath && searchPath !== previousSearchPath) {
// If we have already determined the package.json type for this path we can stop searching. We do however
// still need to cache the found value from the resourcePath folder up to the matching searchPath to avoid
// retracing these steps when processing sibling resources.
if (packageJsonTypeMap.has(searchPath)) {
packageJsonType = packageJsonTypeMap.get(searchPath);

let currentPath = resourceContext;
while (currentPath !== searchPath) {
// We set the found type at least level from this.resourcePath folder up to the matching searchPath
packageJsonTypeMap.set(currentPath, packageJsonType);
currentPath = path.dirname(currentPath);
}
break;
}

let packageJsonPath = path.join(searchPath, 'package.json');
try {
const commonPath = commonPathPrefix([this.rootContext, this.resourcePath], '/');
const stopPath = path.resolve(commonPath, '..');

const packageJsonPath = await findUp(
(dir) => {
if (dir === stopPath) return findUp.stop;
return 'package.json';
},
{ cwd: path.dirname(this.resourcePath) }
);

const buffer = await fsPromises.readFile(packageJsonPath, { encoding: 'utf-8' });
const rawPackageJson = buffer.toString('utf-8');
({ type: packageJsonType } = JSON.parse(rawPackageJson));
packageJsonTypeMap.set(this.rootContext, packageJsonType);
const packageSource = await fsPromises.readFile(packageJsonPath, 'utf-8');
try {
const packageObject = JSON.parse(packageSource);

// Any package.json is sufficient as long as it can be parsed. If it does not explicitly have a type "module"
// it will be assumed to be commonjs.
packageJsonType = typeof packageObject.type === 'string' ? packageObject.type : '';
packageJsonTypeMap.set(searchPath, packageJsonType);

// We set the type in the cache for all paths from the resourcePath folder up to the
// matching searchPath to avoid retracing these steps when processing sibling resources.
let currentPath = resourceContext;
while (currentPath !== searchPath) {
packageJsonTypeMap.set(currentPath, packageJsonType);
currentPath = path.dirname(currentPath);
}
} catch (e) {
// package.json exists but could not be parsed. we track it as a dependency so we can reload if
// this file changes
}
this.addDependency(packageJsonPath);

break;
} catch (e) {
// Failed to parse `package.json`, do nothing.
// package.json does not exist. We track it as a missing dependency so we can reload if this
// file is added
this.addMissingDependency(packageJsonPath);
}

// try again at the next level up
previousSearchPath = searchPath;
searchPath = path.dirname(searchPath);
}

// Check `package.json` for the `type` field -
Expand Down
1 change: 1 addition & 0 deletions test/helpers/compilation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async function getCompilation(subContext, options = {}) {
module: {
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
use: [
{
Expand Down
1 change: 1 addition & 0 deletions test/loader/fixtures/cjs/esm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'esm';
3 changes: 3 additions & 0 deletions test/loader/fixtures/cjs/esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
1 change: 1 addition & 0 deletions test/loader/fixtures/esm/cjs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs';
3 changes: 3 additions & 0 deletions test/loader/fixtures/esm/cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
3 changes: 2 additions & 1 deletion test/loader/loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,13 +384,14 @@ describe('loader', () => {
\\\\******************/
/***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {

var react_refresh__WEBPACK_IMPORTED_MODULE_0___namespace_cache;
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ \\"default\\": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var react_refresh__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react-refresh */ \\"../../../../node_modules/react-refresh/runtime.js\\");

__webpack_require__.$Refresh$.runtime = react_refresh__WEBPACK_IMPORTED_MODULE_0__;
__webpack_require__.$Refresh$.runtime = /*#__PURE__*/ (react_refresh__WEBPACK_IMPORTED_MODULE_0___namespace_cache || (react_refresh__WEBPACK_IMPORTED_MODULE_0___namespace_cache = __webpack_require__.t(react_refresh__WEBPACK_IMPORTED_MODULE_0__, 2)));
Comment on lines +387 to +394
Copy link
Contributor Author

Choose a reason for hiding this comment

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

previously this test caused the react-refresh runtime to be injected with a import statement putting the module into esm mode. when imported into index.js (also ESM) the exports don't require any "conversion" so the load was simple.

Now the react-refresh runtime it's being injected with the plugin (it's excluded by the webpack config /node_modules/ rule) so it stays as a cjs module. Becasue of this webpack adds this translation code because we are crossing from cjs into esm land.


/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ('Test');

Expand Down
36 changes: 36 additions & 0 deletions test/loader/unit/getModuleSystem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ describe('getModuleSystem', () => {
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/esm', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/esm'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
)
).resolves.toBe('esm');
});

it('should return `esm` when `package.json` uses the `module` type nested inside a cjs package', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/cjs/esm', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/cjs'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
Expand All @@ -77,6 +94,23 @@ describe('getModuleSystem', () => {
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/cjs', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/cjs'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
)
).resolves.toBe('cjs');
});

it('should return `cjs` when `package.json` uses the `commonjs` type nexted insdie an esm package', async () => {
await expect(
getModuleSystem.call(
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/esm/cjs', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/esm'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{}
Expand All @@ -90,6 +124,8 @@ describe('getModuleSystem', () => {
{
resourcePath: path.resolve(__dirname, '..', 'fixtures/auto', 'index.js'),
rootContext: path.resolve(__dirname, '..', 'fixtures/auto'),
addDependency: () => {},
addMissingDependency: () => {},
},
ModuleFilenameHelpers,
{ esModule: {} }
Expand Down