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

fix(pnp): throw ERR_REQUIRE_ESM when requiring an ES Module #4024

Merged
merged 5 commits into from Jan 27, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/e2e-cra-workflow.yml
Expand Up @@ -30,6 +30,7 @@ jobs:
yarn add -D eslint-config-react-app eslint

yarn build
yarn test
Copy link
Member Author

@merceyz merceyz Jan 26, 2022

Choose a reason for hiding this comment

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

This fails without this PR when run outside of the CI where file watchers are enabled.


- name: 'Running the TypeScript integration test'
run: |
Expand All @@ -43,5 +44,6 @@ jobs:
yarn add -D @types/testing-library__jest-dom

yarn build
yarn test
if: |
always()
45 changes: 45 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions .yarn/versions/f89dc89d.yml
@@ -0,0 +1,28 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/plugin-pnp": patch
"@yarnpkg/pnp": patch

declined:
- "@yarnpkg/esbuild-plugin-pnp"
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
- "@yarnpkg/nm"
- "@yarnpkg/pnpify"
- "@yarnpkg/sdks"
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -43,6 +43,7 @@ Various improvements have been made in the core to improve performance. Addition
- The PnP filesystem now handles `read` and `readSync` using options.
- The PnP filesystem now handles UNC paths using forward slashes.
- The PnP filesystem now sets the proper `path` property on streams created by `createReadStream()` and obtained from zip archives.
- The PnP runtime now throws an `ERR_REQUIRE_ESM` error when attempting to require an ES Module, matching the default Node.js behaviour.
- Updates the PnP compatibility layer for TypeScript 4.6 Beta (it's possible we'll need to publish another patch update once the 4.6 enters stable).

### Bugfixes
Expand Down
@@ -0,0 +1 @@
import 'fs'
@@ -0,0 +1,5 @@
{
"name": "no-deps-esm",
"version": "1.0.0",
"type": "module"
}
@@ -0,0 +1 @@
import 'fs'
@@ -0,0 +1,4 @@
{
"name": "no-deps-mjs",
"version": "1.0.0"
}
60 changes: 60 additions & 0 deletions packages/acceptance-tests/pkg-tests-specs/sources/pnp-esm.test.ts
Expand Up @@ -499,4 +499,64 @@ describe(`Plug'n'Play - ESM`, () => {
},
),
);

test(
`it should throw ERR_REQUIRE_ESM when requiring a file with type=module`,
makeTemporaryEnv(
{
dependencies: {
'no-deps-esm': `1.0.0`,
},
},
{
pnpEnableEsmLoader: true,
},
async ({path, run, source}) => {
await expect(run(`install`)).resolves.toMatchObject({code: 0});

await xfs.writeFilePromise(ppath.join(path, `index.js` as Filename), `
try {
require('no-deps-esm')
} catch (err) {
console.log(err.code)
}
`);

await expect(run(`node`, `index.js`)).resolves.toMatchObject({
code: 0,
stdout: `ERR_REQUIRE_ESM\n`,
});
},
),
);

test(
`it should throw ERR_REQUIRE_ESM when requiring a .mjs file`,
makeTemporaryEnv(
{
dependencies: {
'no-deps-mjs': `1.0.0`,
},
},
{
pnpEnableEsmLoader: true,
},
async ({path, run, source}) => {
await expect(run(`install`)).resolves.toMatchObject({code: 0});

await xfs.writeFilePromise(ppath.join(path, `index.js` as Filename), `
try {
require('no-deps-mjs/index.mjs')
} catch (err) {
console.log(err.code)
}
`);

await expect(run(`node`, `index.js`)).resolves.toMatchObject({
code: 0,
stdout: `ERR_REQUIRE_ESM\n`,
});
},
),
);
});
Expand Up @@ -2087,7 +2087,7 @@ describe(`Plug'n'Play`, () => {
return originalStatSync(__filename);
}

console.log(require('${path}/does/not/exist.js'))
console.log(require('${path}/does/not/exist.cjs'))
`);

await expect(run(`node`, `./index.js`)).resolves.toMatchObject({
Expand Down
2 changes: 1 addition & 1 deletion packages/yarnpkg-pnp/sources/hook.js

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions packages/yarnpkg-pnp/sources/loader/applyPatch.ts
Expand Up @@ -396,6 +396,21 @@ export function applyPatch(pnpapi: PnpApi, opts: ApplyPatchOptions) {
return false;
};

// https://github.com/nodejs/node/blob/3743406b0a44e13de491c8590386a964dbe327bb/lib/internal/modules/cjs/loader.js#L1110-L1154
const originalExtensionJSFunction = Module._extensions[`.js`] as (module: Module, filename: string) => void;
Module._extensions[`.js`] = function (module: Module, filename: string) {
if (filename.endsWith(`.js`)) {
const pkg = nodeUtils.readPackageScope(filename);
if (pkg && pkg.data?.type === `module`) {
const err = nodeUtils.ERR_REQUIRE_ESM(filename, module.parent?.filename);
Error.captureStackTrace(err);
throw err;
}
}

originalExtensionJSFunction.call(this, module, filename);
};

// When using the ESM loader Node.js prints the following warning
//
// (node:14632) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
Expand Down
19 changes: 19 additions & 0 deletions packages/yarnpkg-pnp/sources/loader/nodeUtils.ts
@@ -1,6 +1,7 @@
import {NativePath, npath} from '@yarnpkg/fslib';
import fs from 'fs';
import {Module} from 'module';
import path from 'path';

// @ts-expect-error
const builtinModules = new Set(Module.builtinModules || Object.keys(process.binding(`natives`)));
Expand Down Expand Up @@ -36,3 +37,21 @@ export function readPackage(requestPath: NativePath) {

return JSON.parse(fs.readFileSync(jsonPath, `utf8`));
}

// https://github.com/nodejs/node/blob/972d9218559877f7fff4bb6086afacac8933f8d1/lib/internal/errors.js#L1450-L1478
// Our error isn't as detailed since we don't have access to acorn to check
// if the file contains ESM syntax
export function ERR_REQUIRE_ESM(filename: string, parentPath: string | null = null) {
const basename =
parentPath && path.basename(filename) === path.basename(parentPath)
? filename
: path.basename(filename);

const msg =
`require() of ES Module ${filename}${parentPath ? ` from ${parentPath}` : ``} not supported.
Instead change the require of ${basename} in ${parentPath} to a dynamic import() which is available in all CommonJS modules.`;

const err = new Error(msg) as Error & { code: string };
err.code = `ERR_REQUIRE_ESM`;
return err;
}