Skip to content

Commit

Permalink
fix(pnp) esm - support loaders importing named exports from commonjs (#…
Browse files Browse the repository at this point in the history
…5961)

**What's the problem this PR addresses?**

nodejs/node#48842 changed it so that our fs
patch is loaded after the internal translators so ESM importing named
CJS exports in loaders doesn't work.

Fixes #5951

**How did you fix it?**

Updated our ESM loader patching to target the `fs` bindings used
internally.

**Checklist**
- [x] I have read the [Contributing
Guide](https://yarnpkg.com/advanced/contributing).
- [x] I have set the packages that need to be released for my changes to
be effective.
- [x] I will check that all automated PR checks pass before the PR gets
reviewed.

(cherry picked from commit 6e920f9)
  • Loading branch information
merceyz committed Nov 14, 2023
1 parent fd8cee6 commit f8ceed4
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 53 deletions.
27 changes: 27 additions & 0 deletions .yarn/versions/3fde6039.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/plugin-pnp": patch
"@yarnpkg/pnp": patch

declined:
- "@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"
28 changes: 28 additions & 0 deletions packages/acceptance-tests/pkg-tests-specs/sources/pnp-esm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,34 @@ describe(`Plug'n'Play - ESM`, () => {
),
);

// Tests /packages/yarnpkg-pnp/sources/esm-loader/fspatch.ts
(loaderFlags.HAS_LOADERS_AFFECTING_LOADERS ? it : it.skip)(
`should support loaders importing named exports from commonjs files`,
makeTemporaryEnv(
{
dependencies: {
'no-deps-exports': `1.0.0`,
},
type: `module`,
},
async ({path, run, source}) => {
await xfs.writeFilePromise(ppath.join(path, `loader.mjs` as Filename), `
import {foo} from 'no-deps-exports';
console.log(foo);
`);
await xfs.writeFilePromise(ppath.join(path, `index.js` as Filename), ``);

await expect(run(`install`)).resolves.toMatchObject({code: 0});

await expect(run(`node`, `--loader`, `./loader.mjs`, `./index.js`)).resolves.toMatchObject({
code: 0,
stdout: `42\n`,
stderr: ``,
});
},
),
);

describe(`private import mappings`, () => {
test(
`it should support private import mappings`,
Expand Down
2 changes: 1 addition & 1 deletion packages/yarnpkg-pnp/sources/esm-loader/built-loader.js

Large diffs are not rendered by default.

131 changes: 81 additions & 50 deletions packages/yarnpkg-pnp/sources/esm-loader/fspatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,92 @@ import {HAS_LAZY_LOADED_TRANSLATORS} from './loaderFlags';

//#region ESM to CJS support
if (!HAS_LAZY_LOADED_TRANSLATORS) {
/*
In order to import CJS files from ESM Node does some translating
internally[1]. This translator calls an unpatched `readFileSync`[2]
which itself calls an internal `tryStatSync`[3] which calls
`binding.fstat`[4]. A PR[5] has been made to use the monkey-patchable
`fs.readFileSync` but assuming that wont be merged this region of code
patches that final `binding.fstat` call.
1: https://github.com/nodejs/node/blob/d872aaf1cf20d5b6f56a699e2e3a64300e034269/lib/internal/modules/esm/translators.js#L177-L277
2: https://github.com/nodejs/node/blob/d872aaf1cf20d5b6f56a699e2e3a64300e034269/lib/internal/modules/esm/translators.js#L240
3: https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/fs.js#L452
4: https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/fs.js#L403
5: https://github.com/nodejs/node/pull/39513
*/

const binding = (process as any).binding(`fs`) as {
fstat: (fd: number, useBigint: false, req: any, ctx: object) => Float64Array;
/**
* Added in https://github.com/nodejs/node/pull/48658 / v20.5.0
* Renamed in https://github.com/nodejs/node/pull/49593 / v20.8.0
*/
readFileSync?: (path: string, flag: number) => string;
/**
* Added in https://github.com/nodejs/node/pull/49593
*/
readFileUtf8?: (path: string, flag: number) => string;
};
const originalfstat = binding.fstat;

// Those values must be synced with packages/yarnpkg-fslib/sources/ZipOpenFS.ts
const ZIP_MASK = 0xff000000;
const ZIP_MAGIC = 0x2a000000;

binding.fstat = function(...args) {
const [fd, useBigint, req] = args;
if ((fd & ZIP_MASK) === ZIP_MAGIC && useBigint === false && req === undefined) {
const originalReadFile = binding.readFileUtf8 || binding.readFileSync;
if (originalReadFile) {
// @ts-expect-error - No index signature
binding[originalReadFile.name] = function (...args: Parameters<typeof originalReadFile>) {
try {
const stats = fs.fstatSync(fd);
// The reverse of this internal util
// https://github.com/nodejs/node/blob/8886b63cf66c29d453fdc1ece2e489dace97ae9d/lib/internal/fs/utils.js#L542-L551
return new Float64Array([
stats.dev,
stats.mode,
stats.nlink,
stats.uid,
stats.gid,
stats.rdev,
stats.blksize,
stats.ino,
stats.size,
stats.blocks,
// atime sec
// atime ns
// mtime sec
// mtime ns
// ctime sec
// ctime ns
// birthtime sec
// birthtime ns
]);
} catch {}
}
return fs.readFileSync(args[0], {
encoding: `utf8`,
// @ts-expect-error - The docs says it needs to be a string but
// links to https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#file-system-flags
// which says it can be a number which matches the implementation.
flag: args[1],
});
} catch { }

return originalfstat.apply(this, args);
};
return originalReadFile.apply(this, args);
};
} else {
/*
In order to import CJS files from ESM Node does some translating
internally[1]. This translator calls an unpatched `readFileSync`[2]
which itself calls an internal `tryStatSync`[3] which calls
`binding.fstat`[4]. A PR[5] has been made to use the monkey-patchable
`fs.readFileSync` but assuming that wont be merged this region of code
patches that final `binding.fstat` call.
1: https://github.com/nodejs/node/blob/d872aaf1cf20d5b6f56a699e2e3a64300e034269/lib/internal/modules/esm/translators.js#L177-L277
2: https://github.com/nodejs/node/blob/d872aaf1cf20d5b6f56a699e2e3a64300e034269/lib/internal/modules/esm/translators.js#L240
3: https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/fs.js#L452
4: https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/fs.js#L403
5: https://github.com/nodejs/node/pull/39513
*/

const binding = (process as any).binding(`fs`) as {
fstat: (fd: number, useBigint: false, req: any, ctx: object) => Float64Array;
};
const originalfstat = binding.fstat;

// Those values must be synced with packages/yarnpkg-fslib/sources/ZipOpenFS.ts
const ZIP_MASK = 0xff000000;
const ZIP_MAGIC = 0x2a000000;

binding.fstat = function (...args) {
const [fd, useBigint, req] = args;
if ((fd & ZIP_MASK) === ZIP_MAGIC && useBigint === false && req === undefined) {
try {
const stats = fs.fstatSync(fd);
// The reverse of this internal util
// https://github.com/nodejs/node/blob/8886b63cf66c29d453fdc1ece2e489dace97ae9d/lib/internal/fs/utils.js#L542-L551
return new Float64Array([
stats.dev,
stats.mode,
stats.nlink,
stats.uid,
stats.gid,
stats.rdev,
stats.blksize,
stats.ino,
stats.size,
stats.blocks,
// atime sec
// atime ns
// mtime sec
// mtime ns
// ctime sec
// ctime ns
// birthtime sec
// birthtime ns
]);
} catch { }
}

return originalfstat.apply(this, args);
};
}
}
//#endregion
9 changes: 7 additions & 2 deletions packages/yarnpkg-pnp/sources/esm-loader/loaderFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ export const HAS_JSON_IMPORT_ASSERTION_REQUIREMENT = major > 17 || (major === 17
// The message switched to using an array in https://github.com/nodejs/node/pull/45348
export const WATCH_MODE_MESSAGE_USES_ARRAYS = major > 19 || (major === 19 && minor >= 2) || (major === 18 && minor >= 13);

// https://github.com/nodejs/node/pull/45659 changed the internal translators to be lazy loaded
// https://github.com/nodejs/node/pull/45659 changed the internal translators to be lazy loaded so they use our patch.
// https://github.com/nodejs/node/pull/48842 changed it so that our patch is loaded after the internal translators.
// TODO: Update the version range if https://github.com/nodejs/node/pull/46425 lands.
export const HAS_LAZY_LOADED_TRANSLATORS = major > 19 || (major === 19 && minor >= 3);
export const HAS_LAZY_LOADED_TRANSLATORS = (major === 20 && minor < 6) || (major === 19 && minor >= 3);

// https://github.com/nodejs/node/pull/43772
// TODO: Update the version range if it gets backported to v18.
export const HAS_LOADERS_AFFECTING_LOADERS = major > 19 || (major === 19 && minor >= 6);

// https://github.com/nodejs/node/pull/42881
export const ALLOWS_NON_FILE_PARENT = major > 18 || (major === 18 && minor >= 1) || (major === 16 && minor >= 17);

0 comments on commit f8ceed4

Please sign in to comment.