diff --git a/.github/workflows/integration-workflow.yml b/.github/workflows/integration-workflow.yml index f184d383e874..e1e8189a6189 100644 --- a/.github/workflows/integration-workflow.yml +++ b/.github/workflows/integration-workflow.yml @@ -201,11 +201,11 @@ jobs: fail-fast: false matrix: # We run the ubuntu tests on multiple Node versions with 2 shards since they're the fastest. - node: [18, 19, 20] + node: [18, 19, 20, 21] platform: [[ubuntu, 20.04]] shard: ['1/2', '2/2'] - # We run the rest of the tests on the minimum Node version we support with 3 shards. include: + # We run the rest of the tests on the minimum Node version we support with 3 shards. # Windows tests - {node: 18, platform: [windows, latest], shard: 1/3} - {node: 18, platform: [windows, latest], shard: 2/3} @@ -214,6 +214,15 @@ jobs: - {node: 18, platform: [macos, latest], shard: 1/3} - {node: 18, platform: [macos, latest], shard: 2/3} - {node: 18, platform: [macos, latest], shard: 3/3} + # We also run them on the maximum Node version we support, to catch potential regressions in Node.js. + # Windows tests + - {node: 21, platform: [windows, latest], shard: 1/3} + - {node: 21, platform: [windows, latest], shard: 2/3} + - {node: 21, platform: [windows, latest], shard: 3/3} + # macOS tests + - {node: 21, platform: [macos, latest], shard: 1/3} + - {node: 21, platform: [macos, latest], shard: 2/3} + - {node: 21, platform: [macos, latest], shard: 3/3} name: '${{matrix.platform[0]}}-latest w/ Node.js ${{matrix.node}}.x (${{matrix.shard}})' runs-on: ${{matrix.platform[0]}}-${{matrix.platform[1]}} diff --git a/.yarn/versions/9d9fcf70.yml b/.yarn/versions/9d9fcf70.yml new file mode 100644 index 000000000000..15704565f09c --- /dev/null +++ b/.yarn/versions/9d9fcf70.yml @@ -0,0 +1,39 @@ +releases: + "@yarnpkg/cli": patch + "@yarnpkg/core": patch + "@yarnpkg/fslib": patch + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-exec" + - "@yarnpkg/plugin-file" + - "@yarnpkg/plugin-git" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-http" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-link" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-npm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - vscode-zipfs + - "@yarnpkg/builder" + - "@yarnpkg/doctor" + - "@yarnpkg/extensions" + - "@yarnpkg/libzip" + - "@yarnpkg/nm" + - "@yarnpkg/pnp" + - "@yarnpkg/pnpify" + - "@yarnpkg/sdks" + - "@yarnpkg/shell" diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/pnp-esm.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/pnp-esm.test.ts index c15de2dc58b8..88387157ae65 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/pnp-esm.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/pnp-esm.test.ts @@ -1,6 +1,10 @@ +import {nodeUtils} from '@yarnpkg/core'; import {Filename, npath, ppath, xfs} from '@yarnpkg/fslib'; import {pathToFileURL} from 'url'; +const ifAtLeastNode21It = nodeUtils.major >= 21 ? it : it.skip; +const ifAtMostNode20It = nodeUtils.major <= 20 ? it : it.skip; + describe(`Plug'n'Play - ESM`, () => { test( `it should be able to import a node builtin`, @@ -399,7 +403,7 @@ describe(`Plug'n'Play - ESM`, () => { ), ); - test( + ifAtMostNode20It( `it should not allow extensionless commonjs imports`, makeTemporaryEnv( { }, @@ -420,7 +424,27 @@ describe(`Plug'n'Play - ESM`, () => { ), ); - test( + ifAtLeastNode21It( + `it should allow extensionless commonjs imports`, + makeTemporaryEnv( + { }, + { + pnpEnableEsmLoader: true, + }, + async ({path, run, source}) => { + await xfs.writeFilePromise(ppath.join(path, `index.mjs`), `import bin from './cjs-bin';\nconsole.log(bin)`); + await xfs.writeFilePromise(ppath.join(path, `cjs-bin`), `module.exports = 42`); + + await expect(run(`install`)).resolves.toMatchObject({code: 0}); + + await expect(run(`node`, `./index.mjs`)).resolves.toMatchObject({ + stdout: `42\n`, + }); + }, + ), + ); + + ifAtMostNode20It( `it should not allow extensionless files with {"type": "module"}`, makeTemporaryEnv( { @@ -430,7 +454,7 @@ describe(`Plug'n'Play - ESM`, () => { pnpEnableEsmLoader: true, }, async ({path, run, source}) => { - await xfs.writeFilePromise(ppath.join(path, `index`), ``); + await xfs.writeFilePromise(ppath.join(path, `index`), `console.log(42)`); await expect(run(`install`)).resolves.toMatchObject({code: 0}); @@ -442,6 +466,27 @@ describe(`Plug'n'Play - ESM`, () => { ), ); + ifAtLeastNode21It( + `it should allow extensionless files with {"type": "module"}`, + makeTemporaryEnv( + { + type: `module`, + }, + { + pnpEnableEsmLoader: true, + }, + async ({path, run, source}) => { + await xfs.writeFilePromise(ppath.join(path, `index`), `console.log(42)`); + + await expect(run(`install`)).resolves.toMatchObject({code: 0}); + + await expect(run(`node`, `./index`)).resolves.toMatchObject({ + stdout: `42\n`, + }); + }, + ), + ); + test( `it should support ESM binaries`, makeTemporaryEnv( diff --git a/packages/yarnpkg-core/sources/nodeUtils.ts b/packages/yarnpkg-core/sources/nodeUtils.ts index 90f4927fd790..82e73c77ac50 100644 --- a/packages/yarnpkg-core/sources/nodeUtils.ts +++ b/packages/yarnpkg-core/sources/nodeUtils.ts @@ -4,6 +4,8 @@ import os from 'os'; import * as execUtils from './execUtils'; import * as miscUtils from './miscUtils'; +export const major = Number(process.versions.node.split(`.`)[0]); + const openUrlBinary = new Map([ [`darwin`, `open`], [`linux`, `xdg-open`], diff --git a/packages/yarnpkg-fslib/sources/NodeFS.ts b/packages/yarnpkg-fslib/sources/NodeFS.ts index f0b7ede7a1ac..9e6ec81349e5 100644 --- a/packages/yarnpkg-fslib/sources/NodeFS.ts +++ b/packages/yarnpkg-fslib/sources/NodeFS.ts @@ -4,7 +4,17 @@ import {CreateReadStreamOptions, CreateWriteStreamOptions, Dir, StatWatcher, Wat import {Dirent, SymlinkType, StatSyncOptions, StatOptions} from './FakeFS'; import {BasePortableFakeFS, WriteFileOptions} from './FakeFS'; import {MkdirOptions, RmdirOptions, WatchOptions, WatchCallback, Watcher} from './FakeFS'; -import {FSPath, PortablePath, Filename, ppath, npath} from './path'; +import {FSPath, PortablePath, Filename, ppath, npath, NativePath} from './path'; + +function direntToPortable(dirent: Dirent): Dirent { + // We don't need to return a copy, we can just reuse the object the real fs returned + const portableDirent = dirent as Dirent; + + if (typeof dirent.path === `string`) + portableDirent.path = npath.toPortablePath(dirent.path); + + return portableDirent; +} export class NodeFS extends BasePortableFakeFS { private readonly realFs: typeof fs; @@ -469,9 +479,17 @@ export class NodeFS extends BasePortableFakeFS { async readdirPromise(p: PortablePath, opts?: ReaddirOptions | null): Promise | DirentNoPath | PortablePath>> { return await new Promise((resolve, reject) => { if (opts) { - this.realFs.readdir(npath.fromPortablePath(p), opts as any, this.makeCallback(resolve, reject) as any); + if (opts.recursive && process.platform === `win32`) { + if (opts.withFileTypes) { + this.realFs.readdir(npath.fromPortablePath(p), opts as any, this.makeCallback>>(results => resolve(results.map(direntToPortable)), reject) as any); + } else { + this.realFs.readdir(npath.fromPortablePath(p), opts as any, this.makeCallback>(results => resolve(results.map(npath.toPortablePath)), reject) as any); + } + } else { + this.realFs.readdir(npath.fromPortablePath(p), opts as any, this.makeCallback(resolve, reject) as any); + } } else { - this.realFs.readdir(npath.fromPortablePath(p), this.makeCallback(value => resolve(value as Array), reject)); + this.realFs.readdir(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); } }); } @@ -488,9 +506,17 @@ export class NodeFS extends BasePortableFakeFS { readdirSync(p: PortablePath, opts: {recursive: boolean, withFileTypes: boolean}): Array | DirentNoPath | PortablePath>; readdirSync(p: PortablePath, opts?: ReaddirOptions | null): Array | DirentNoPath | PortablePath> { if (opts) { - return this.realFs.readdirSync(npath.fromPortablePath(p), opts as any) as Array; + if (opts.recursive && process.platform === `win32`) { + if (opts.withFileTypes) { + return (this.realFs.readdirSync(npath.fromPortablePath(p), opts as any) as any as Array>).map(direntToPortable); + } else { + return (this.realFs.readdirSync(npath.fromPortablePath(p), opts as any) as any as Array).map(npath.toPortablePath); + } + } else { + return this.realFs.readdirSync(npath.fromPortablePath(p), opts as any) as Array; + } } else { - return this.realFs.readdirSync(npath.fromPortablePath(p)) as Array; + return this.realFs.readdirSync(npath.fromPortablePath(p)) as Array; } }