diff --git a/.yarn/versions/cf74cfe1.yml b/.yarn/versions/cf74cfe1.yml new file mode 100644 index 000000000000..1c546139c84c --- /dev/null +++ b/.yarn/versions/cf74cfe1.yml @@ -0,0 +1,28 @@ +releases: + "@yarnpkg/cli": minor + "@yarnpkg/plugin-pnp": minor + "@yarnpkg/pnp": minor + +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" diff --git a/packages/plugin-pnp/sources/PnpLinker.ts b/packages/plugin-pnp/sources/PnpLinker.ts index f14b9779b838..f0cbbf988a8f 100644 --- a/packages/plugin-pnp/sources/PnpLinker.ts +++ b/packages/plugin-pnp/sources/PnpLinker.ts @@ -348,7 +348,7 @@ export class PnpInstaller implements Installer { } if (this.isEsmEnabled()) { - this.opts.report.reportWarning(MessageName.UNNAMED, `ESM support for PnP uses the experimental loader API and is therefor experimental`); + this.opts.report.reportWarning(MessageName.UNNAMED, `ESM support for PnP uses the experimental loader API and is therefore experimental`); await xfs.changeFilePromise(pnpPath.esmLoader, getESMLoaderTemplate(), { automaticNewlines: true, mode: 0o644, diff --git a/packages/yarnpkg-pnp/sources/esm-loader/built-loader.js b/packages/yarnpkg-pnp/sources/esm-loader/built-loader.js index c497f779b5d8..fa2266f1643a 100644 --- a/packages/yarnpkg-pnp/sources/esm-loader/built-loader.js +++ b/packages/yarnpkg-pnp/sources/esm-loader/built-loader.js @@ -2,7 +2,7 @@ let hook; module.exports = () => { if (typeof hook === `undefined`) - hook = require('zlib').brotliDecompressSync(Buffer.from('G38aIKwOit4nCivbjiByq2/M91VoTWfT6mefZLkPvDFuAbqE6358BbqWy/99XntaOFkqmsyW+tcM0tuacUxCaoQGUUiRmSA9PrXklEllbP5/+6UF1zq1xpakzt5H3fmzDf6kgKzezARJFUmlcT2+qoqdJ8nSRcg+hmpv0rP9GAQFFDT9G/tQ4Xmp4RVzz7qXt8I5qlx2g0H45MCQLJmLKffWE4EjDX8nr79yVBHw7t/ZQXg3+VlDiryjLtsd6QzAwbuOEKcWGnztoYTYAliPMhOhg/qizQLI/E/eTiqH11e0AruwsNXQwcbjW82iYd2ihsnzk9PTA9A0MP0uEpOUeetBhurAsauPSJP/VTMQblCb5i9gL1rNYAnR2/lhmgEREmgQLIJbCLNh3x8kg0jcWIxie6mXEG8PIpaxSlGA7yhvGq41ws5N87UAb/rfqku/GShqU5aadPa63CjN9AD+mw0bfEzL6aNRjgFNf4mrWmmki7swXPQX3lqlB8vRFySpCNgHBCRyuTlYLSzOJfSLCQJ0JIjJmcWoegp3kpW9jAwV2vvARRmv553DebiyIxOPEfFvc0cApJtdQbiA8w7AWaY3gcga0XwR3uBYjdrfWMjKhpiJBt0SSKhCzxdkWDDI4IXSKSQAVdN5FYI68bCaDj3yhrwqgrwCzSF8FNUx/M86+RKiboW5m/xfTW6KDIeyp2I80fmbMFbLfr1RWLRWavGuj5O7cJisojTYiDtql8ZeyuKjOE8RVE3Oi2IfelNctZyr9/e1bQr/VMOjRdbK4IcXP60Dj4yeuu4ja5d+zlDly8dAwRCXp5gtUevbeCBiuBOGCCoJ9I50+ZM5N3gqgQU1Xy/iUno/CWhgtJYhw5wgjy4WiROW55lOOkN+0ir1QqQr9u5MTv9JkEH8qMxO/L78TuFLJ9Pu8ukIq16Mn3zOTTymxMunoRCChofBRlHlT0cexQ3GIqieygJZb6PauTW8J7lRSHqJB8nVAlIWDRdN+Ik9P08O+S4n/RKXAQLsrm/JNRqqbJXR2CoeZtSPUslOskLp21I5tKMAMI3Elfp882PEEg0ScuBmZfZxLxIIax8/hv5crZFkMp3qFCyuCfofxhAHHZF9MALUPhEUTe2RjhbSA/ActBh5J4UD1ajLvMqsmBBXyXBx22c8wdKJNuWG9ORyNXHShjb6ytkDkifsH+38pDTKHsKPMha7WZG08VFfCYyzP38uRXQLGWlzNQTtHnphZpoIkEj4soZCSC6rDvk5S+U+GPr5Z1w8NvldletzgvCcjd9rpwAE0tKgG5wjILaK2h5se4HkjacFNIjYRmgzXPV2rtU3v3zmC7OT1R3Q7fEiY60qmur6PRqLcX78JHRmVdOTUkw0Osns5fRFW+ZWFSNDSKetcnNqKHDYmETq2zFeMNvJpxr3kBHhrt9nz7NyGzXqS7PBEcJ4prbUrlA+mR5S6aSllmPcBEnNsolSxdGmpjxnzDN5ZXt6EVekNRa/oUWCaBPtm97lKhlim+4rP52/8m9rIBIwzXNgaI+o8B7Dr7GmIdXH7OfwJdkqQQW/NRAULjOvqMsWSG0tWbppgjEnLEQf2A5bSzzltrUrJR9jYUibsblkjkqi4QZ+E9bdrsbVxBQJiKRjALXgQcGizgeams28pYGskaeYGvxwFI+qJcT0DojsI9Kojbz4RrcGqCrj/GgN60+9kJ1Nkxj/SlERTbjdlERPOu/nUd/B0iI+hKs3TLuoF4dvYFkw68UlJd0l7KxESn4+Mz6eJNU44fltfaDEOYsmoNSwq5kx66ilfE8GJBi1Fkd+Z+fu3kp3Vjazwhk6c9fKfsSxdjDHlLiiR3Ob6zCu5Wyetfq8zX6oKYAsSqHyUwNjmLxxUYRltgO//EPv1ZTmiLF2O5M59zvlmSMdjE4LNf9typB7jnpmeFxwNH6De1KAxfQGveDwonnImCJhzkLzmXVcnmdLNaVDL5pl5M9rq2EBc72YyN71XLMLwUgtfXBXJlv+1M15RF22l/1ebaSKB0OF9JTMxe4X627M2yRTKFfbHsBKhqJcvSo2O2Gj0kvYlFSzE5h9Zpi+h9RaEiE16URp8RSewThIIwzTBMzLLVtitEL8I8ZqgfGr1ihzLLkndPclto8TRE6kIdsSD9xRuthaRy7AfC6rlFwFCwy1xdpVJ27YJHcL6EUjYHK7fWJJ1Ia03BdaOYPBnFYzzdjK6jI/bIS5kVoAPjweN1fe0qRc07FYSYygUpULEHf3Y80LQkpnheP1oMqNIVlRZ5/KwaavUKdU1azFXqrNU2n7TFjtrhUP51suC9NJToYEnkN+1siHV1N/da4udkAxKw5SuH974yg82AOyuLC8try+tLq8ngecdMK+DhHvykKFcLN5zkhd2Wt6jY6U9t8HpMngyHMGA2LEa+6Dtnh7gHIKmnOAA2EiKqaWtAeUNpIXdB81qdTiis/1Za71dOnBs9idLr9GpG0DEgHJq/jRTigUS4iy++CWD2X3t6zfss1GXiYqqSz7vm20Whw7hg1KEZIWGrV1jEWuXQunWFpTfaBHtka3x0kyDXnncWw5CLMgWrRRVDxrUsChjQ==', 'base64')).toString(); + hook = require('zlib').brotliDecompressSync(Buffer.from('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', 'base64')).toString(); return hook; }; diff --git a/packages/yarnpkg-pnp/sources/esm-loader/fspatch.ts b/packages/yarnpkg-pnp/sources/esm-loader/fspatch.ts new file mode 100644 index 000000000000..29893f073bc1 --- /dev/null +++ b/packages/yarnpkg-pnp/sources/esm-loader/fspatch.ts @@ -0,0 +1,57 @@ +import fs from 'fs'; + +//#region ESM to CJS support +/* + 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; + +const ZIP_FD = 0x80000000; +binding.fstat = function(...args) { + const [fd, useBigint, req] = args; + if ((fd & ZIP_FD) !== 0 && 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 diff --git a/packages/yarnpkg-pnp/sources/esm-loader/hooks/getFormat.ts b/packages/yarnpkg-pnp/sources/esm-loader/hooks/getFormat.ts new file mode 100644 index 000000000000..b246de37f644 --- /dev/null +++ b/packages/yarnpkg-pnp/sources/esm-loader/hooks/getFormat.ts @@ -0,0 +1,23 @@ +import {fileURLToPath} from 'url'; + +import * as loaderUtils from '../loaderUtils'; + +// The default `getFormat` doesn't support reading from zip files +export async function getFormat( + resolved: string, + context: object, + defaultGetFormat: typeof getFormat, +): Promise<{ format: string }> { + const url = loaderUtils.tryParseURL(resolved); + if (url?.protocol !== `file:`) + return defaultGetFormat(resolved, context, defaultGetFormat); + + const format = loaderUtils.getFileFormat(fileURLToPath(url)); + if (format) { + return { + format, + }; + } + + return defaultGetFormat(resolved, context, defaultGetFormat); +} diff --git a/packages/yarnpkg-pnp/sources/esm-loader/hooks/getSource.ts b/packages/yarnpkg-pnp/sources/esm-loader/hooks/getSource.ts new file mode 100644 index 000000000000..f37e52a8cb8a --- /dev/null +++ b/packages/yarnpkg-pnp/sources/esm-loader/hooks/getSource.ts @@ -0,0 +1,19 @@ +import fs from 'fs'; +import {fileURLToPath} from 'url'; + +import * as loaderUtils from '../loaderUtils'; + +// The default `getSource` doesn't support reading from zip files +export async function getSource( + urlString: string, + context: { format: string }, + defaultGetSource: typeof getSource, +): Promise<{ source: string }> { + const url = loaderUtils.tryParseURL(urlString); + if (url?.protocol !== `file:`) + return defaultGetSource(urlString, context, defaultGetSource); + + return { + source: await fs.promises.readFile(fileURLToPath(url), `utf8`), + }; +} diff --git a/packages/yarnpkg-pnp/sources/esm-loader/hooks/load.ts b/packages/yarnpkg-pnp/sources/esm-loader/hooks/load.ts new file mode 100644 index 000000000000..e66c1d2a7047 --- /dev/null +++ b/packages/yarnpkg-pnp/sources/esm-loader/hooks/load.ts @@ -0,0 +1,26 @@ +import fs from 'fs'; +import {fileURLToPath} from 'url'; + +import * as loaderUtils from '../loaderUtils'; + +// The default `load` doesn't support reading from zip files +export async function load( + urlString: string, + context: { format: string | null | undefined }, + defaultLoad: typeof load, +): Promise<{ format: string; source: string }> { + const url = loaderUtils.tryParseURL(urlString); + if (url?.protocol !== `file:`) + return defaultLoad(urlString, context, defaultLoad); + + const filePath = fileURLToPath(url); + + const format = loaderUtils.getFileFormat(filePath); + if (!format) + return defaultLoad(urlString, context, defaultLoad); + + return { + format, + source: await fs.promises.readFile(filePath, `utf8`), + }; +} diff --git a/packages/yarnpkg-pnp/sources/esm-loader/hooks/resolve.ts b/packages/yarnpkg-pnp/sources/esm-loader/hooks/resolve.ts new file mode 100644 index 000000000000..d2b0b28bc338 --- /dev/null +++ b/packages/yarnpkg-pnp/sources/esm-loader/hooks/resolve.ts @@ -0,0 +1,74 @@ +import {NativePath, PortablePath} from '@yarnpkg/fslib'; +import moduleExports from 'module'; +import {fileURLToPath, pathToFileURL} from 'url'; + +import {PnpApi} from '../../types'; +import * as loaderUtils from '../loaderUtils'; + +const builtins = new Set([...moduleExports.builtinModules]); + +const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/; + +export async function resolve( + originalSpecifier: string, + context: { conditions: Array; parentURL: string | undefined }, + defaultResolver: typeof resolve, +): Promise<{ url: string }> { + const {findPnpApi} = (moduleExports as unknown) as { findPnpApi?: (path: NativePath) => null | PnpApi }; + if (!findPnpApi || builtins.has(originalSpecifier)) + return defaultResolver(originalSpecifier, context, defaultResolver); + + let specifier = originalSpecifier; + const url = loaderUtils.tryParseURL(specifier); + if (url) { + if (url.protocol !== `file:`) + return defaultResolver(originalSpecifier, context, defaultResolver); + + specifier = fileURLToPath(specifier); + } + + const {parentURL, conditions = []} = context; + + const issuer = parentURL ? fileURLToPath(parentURL) : process.cwd(); + + // Get the pnpapi of either the issuer or the specifier. + // The latter is required when the specifier is an absolute path to a + // zip file and the issuer doesn't belong to a pnpapi + const pnpapi = findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null); + if (!pnpapi) + return defaultResolver(originalSpecifier, context, defaultResolver); + + const dependencyNameMatch = specifier.match(pathRegExp); + + let allowLegacyResolve = false; + + if (dependencyNameMatch) { + const [, dependencyName, subPath] = dependencyNameMatch as [unknown, string, PortablePath]; + + // If the package.json doesn't list an `exports` field, Node will tolerate omitting the extension + // https://github.com/nodejs/node/blob/0996eb71edbd47d9f9ec6153331255993fd6f0d1/lib/internal/modules/esm/resolve.js#L686-L691 + if (subPath === ``) { + const resolved = pnpapi.resolveToUnqualified(`${dependencyName}/package.json`, issuer); + if (resolved) { + const content = await loaderUtils.tryReadFile(resolved); + if (content) { + const pkg = JSON.parse(content); + allowLegacyResolve = pkg.exports == null; + } + } + } + } + + const result = pnpapi.resolveRequest(specifier, issuer, { + conditions: new Set(conditions), + // TODO: Handle --experimental-specifier-resolution=node + extensions: allowLegacyResolve ? undefined : [], + }); + + if (!result) + throw new Error(`Resolving '${specifier}' from '${issuer}' failed`); + + return { + url: pathToFileURL(result).href, + }; +} diff --git a/packages/yarnpkg-pnp/sources/esm-loader/loader.ts b/packages/yarnpkg-pnp/sources/esm-loader/loader.ts index 9b2ce6eda6f2..5340dd33f7ba 100644 --- a/packages/yarnpkg-pnp/sources/esm-loader/loader.ts +++ b/packages/yarnpkg-pnp/sources/esm-loader/loader.ts @@ -1,202 +1,15 @@ -import {NativePath, PortablePath} from '@yarnpkg/fslib'; -import fs from 'fs'; -import moduleExports from 'module'; -import path from 'path'; -import {fileURLToPath, pathToFileURL, URL} from 'url'; +import {getFormat as getFormatHook} from './hooks/getFormat'; +import {getSource as getSourceHook} from './hooks/getSource'; +import {load as loadHook} from './hooks/load'; +import {resolve as resolveHook} from './hooks/resolve'; +import './fspatch'; -import * as nodeUtils from '../loader/nodeUtils'; -import {PnpApi} from '../types'; +const [major, minor] = process.versions.node.split(`.`).map(value => parseInt(value, 10)); -function tryParseURL(str: string) { - try { - return new URL(str); - } catch { - return null; - } -} +// The hooks were consolidated in https://github.com/nodejs/node/pull/37468 +const hasConsolidatedHooks = major > 16 || (major === 16 && minor >= 12); -const builtins = new Set([...moduleExports.builtinModules]); - -const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/; - -async function exists(path: string) { - try { - await fs.promises.access(path, fs.constants.R_OK); - return true; - } catch { } - return false; -} - -export async function resolve( - originalSpecifier: string, - context: any, - defaultResolver: any, -) { - const {findPnpApi} = (moduleExports as unknown as { findPnpApi?: (path: NativePath) => null | PnpApi }); - if (!findPnpApi || builtins.has(originalSpecifier)) - return defaultResolver(originalSpecifier, context, defaultResolver); - - let specifier = originalSpecifier; - const url = tryParseURL(specifier); - if (url) { - if (url.protocol !== `file:`) - return defaultResolver(originalSpecifier, context, defaultResolver); - - specifier = fileURLToPath(specifier); - } - - const {parentURL, conditions = []} = context; - - const issuer = parentURL ? fileURLToPath(parentURL) : process.cwd(); - - // Get the pnpapi of either the issuer or the specifier. - // The latter is required when the specifier is an absolute path to a - // zip file and the issuer doesn't belong to a pnpapi - const pnpapi = findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null); - if (!pnpapi) - return defaultResolver(originalSpecifier, context, defaultResolver); - - const dependencyNameMatch = specifier.match(pathRegExp); - - let allowLegacyResolve = false; - - if (dependencyNameMatch) { - const [, dependencyName, subPath] = dependencyNameMatch as [unknown, string, PortablePath]; - - // If the package.json doesn't list an `exports` field, Node will tolerate omitting the extension - // https://github.com/nodejs/node/blob/0996eb71edbd47d9f9ec6153331255993fd6f0d1/lib/internal/modules/esm/resolve.js#L686-L691 - if (subPath === ``) { - const resolved = pnpapi.resolveToUnqualified(`${dependencyName}/package.json`, issuer); - if (resolved && await exists(resolved)) { - const pkg = JSON.parse(await fs.promises.readFile(resolved, `utf8`)); - allowLegacyResolve = pkg.exports == null; - } - } - } - - const result = pnpapi.resolveRequest(specifier, issuer, { - conditions: new Set(conditions), - // TODO: Handle --experimental-specifier-resolution=node - extensions: allowLegacyResolve ? undefined : [], - }); - - if (!result) - throw new Error(`Resolving '${specifier}' from '${issuer}' failed`); - - return { - url: pathToFileURL(result).href, - }; -} - -// The default `getFormat` doesn't support reading from zip files -export async function getFormat( - resolved: string, - context: any, - defaultGetFormat: any, -) { - const url = tryParseURL(resolved); - if (url?.protocol !== `file:`) - return defaultGetFormat(resolved, context, defaultGetFormat); - - const ext = path.extname(url.pathname); - switch (ext) { - case `.mjs`: { - return { - format: `module`, - }; - } - case `.cjs`: { - return { - format: `commonjs`, - }; - } - case `.json`: { - // TODO: Enable if --experimental-json-modules is present - // Waiting on https://github.com/nodejs/node/issues/36935 - throw new Error( - `Unknown file extension ".json" for ${fileURLToPath(resolved)}`, - ); - } - case `.js`: { - const pkg = nodeUtils.readPackageScope(fileURLToPath(resolved)); - if (pkg) { - return { - format: pkg.data.type ?? `commonjs`, - }; - } - } - } - - return defaultGetFormat(resolved, context, defaultGetFormat); -} - -// The default `getSource` doesn't support reading from zip files -export async function getSource( - urlString: string, - context: any, - defaultGetSource: any, -) { - const url = tryParseURL(urlString); - if (url?.protocol !== `file:`) - return defaultGetSource(url, context, defaultGetSource); - - return { - source: await fs.promises.readFile(fileURLToPath(urlString), `utf8`), - }; -} - -//#region ESM to CJS support -/* - 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; - -const ZIP_FD = 0x80000000; -binding.fstat = function(...args) { - const [fd, useBigint, req] = args; - if ((fd & ZIP_FD) !== 0 && 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 +export const resolve = resolveHook; +export const getFormat = hasConsolidatedHooks ? undefined : getFormatHook; +export const getSource = hasConsolidatedHooks ? undefined : getSourceHook; +export const load = hasConsolidatedHooks ? loadHook : undefined; diff --git a/packages/yarnpkg-pnp/sources/esm-loader/loaderUtils.ts b/packages/yarnpkg-pnp/sources/esm-loader/loaderUtils.ts new file mode 100644 index 000000000000..751812cb241e --- /dev/null +++ b/packages/yarnpkg-pnp/sources/esm-loader/loaderUtils.ts @@ -0,0 +1,60 @@ +import {NativePath} from '@yarnpkg/fslib'; +import fs from 'fs'; +import path from 'path'; +import {URL} from 'url'; + +import * as nodeUtils from '../loader/nodeUtils'; + +export async function tryReadFile(path: NativePath): Promise { + try { + return await fs.promises.readFile(path, `utf8`); + } catch (error) { + if (error.code === `ENOENT`) + return null; + + throw error; + } +} + +export function tryParseURL(str: string) { + try { + return new URL(str); + } catch { + return null; + } +} + +export function getFileFormat(filepath: string): string | null { + const ext = path.extname(filepath); + + switch (ext) { + case `.mjs`: { + return `module`; + } + case `.cjs`: { + return `commonjs`; + } + case `.wasm`: { + // TODO: Enable if --experimental-wasm-modules is present + // Waiting on https://github.com/nodejs/node/issues/36935 + throw new Error( + `Unknown file extension ".wasm" for ${filepath}`, + ); + } + case `.json`: { + // TODO: Enable if --experimental-json-modules is present + // Waiting on https://github.com/nodejs/node/issues/36935 + throw new Error( + `Unknown file extension ".json" for ${filepath}`, + ); + } + case `.js`: { + const pkg = nodeUtils.readPackageScope(filepath); + if (pkg) { + return pkg.data.type ?? `commonjs`; + } + } + } + + return null; +}