mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: loading ESM pnpmfiles (#9730)
This commit is contained in:
6
.changeset/real-papers-cut.md
Normal file
6
.changeset/real-papers-cut.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/pnpmfile": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added support for pnpmfiles written in ESM. They should have the `.mjs` extension: `.pnpmfile.mjs` [#9730](https://github.com/pnpm/pnpm/pull/9730).
|
||||
@@ -43,7 +43,7 @@ export async function getConfig (
|
||||
const configModulesDir = path.join(config.lockfileDir ?? config.rootProjectManifestDir, 'node_modules/.pnpm-config')
|
||||
pnpmfiles.unshift(...calcPnpmfilePathsOfPluginDeps(configModulesDir, config.configDependencies))
|
||||
}
|
||||
const { hooks, finders, resolvedPnpmfilePaths } = requireHooks(config.lockfileDir ?? config.dir, {
|
||||
const { hooks, finders, resolvedPnpmfilePaths } = await requireHooks(config.lockfileDir ?? config.dir, {
|
||||
globalPnpmfile: config.globalPnpmfile,
|
||||
pnpmfiles,
|
||||
tryLoadDefaultPnpmfile: config.tryLoadDefaultPnpmfile,
|
||||
|
||||
@@ -45,14 +45,14 @@ export interface RequireHooksResult {
|
||||
resolvedPnpmfilePaths: string[]
|
||||
}
|
||||
|
||||
export function requireHooks (
|
||||
export async function requireHooks (
|
||||
prefix: string,
|
||||
opts: {
|
||||
globalPnpmfile?: string
|
||||
pnpmfiles?: string[]
|
||||
tryLoadDefaultPnpmfile?: boolean
|
||||
}
|
||||
): RequireHooksResult {
|
||||
): Promise<RequireHooksResult> {
|
||||
const pnpmfiles: PnpmfileEntry[] = []
|
||||
if (opts.globalPnpmfile) {
|
||||
pnpmfiles.push({
|
||||
@@ -77,11 +77,11 @@ export function requireHooks (
|
||||
}
|
||||
const entries: PnpmfileEntryLoaded[] = []
|
||||
const loadedFiles: string[] = []
|
||||
for (const { path, includeInChecksum, optional } of pnpmfiles) {
|
||||
await Promise.all(pnpmfiles.map(async ({ path, includeInChecksum, optional }) => {
|
||||
const file = pathAbsolute(path, prefix)
|
||||
if (!loadedFiles.includes(file)) {
|
||||
loadedFiles.push(file)
|
||||
const requirePnpmfileResult = requirePnpmfile(file, prefix)
|
||||
const requirePnpmfileResult = await requirePnpmfile(file, prefix)
|
||||
if (requirePnpmfileResult != null) {
|
||||
entries.push({
|
||||
file,
|
||||
@@ -93,7 +93,7 @@ export function requireHooks (
|
||||
throw new PnpmError('PNPMFILE_NOT_FOUND', `pnpmfile at "${file}" is not found`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const mergedFinders: Finders = {}
|
||||
const cookedHooks: CookedHooks & Required<Pick<CookedHooks, 'readPackage' | 'preResolution' | 'afterAllResolved' | 'filterLog' | 'updateConfig'>> = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import assert from 'assert'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import util from 'util'
|
||||
import { pathToFileURL } from 'url'
|
||||
import { createRequire } from 'module'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { logger } from '@pnpm/logger'
|
||||
@@ -37,9 +39,17 @@ export interface Pnpmfile {
|
||||
finders?: Finders
|
||||
}
|
||||
|
||||
export function requirePnpmfile (pnpmFilePath: string, prefix: string): { pnpmfileModule: Pnpmfile | undefined } | undefined {
|
||||
export async function requirePnpmfile (pnpmFilePath: string, prefix: string): Promise<{ pnpmfileModule: Pnpmfile | undefined } | undefined> {
|
||||
try {
|
||||
const pnpmfile: Pnpmfile = require(pnpmFilePath)
|
||||
let pnpmfile: Pnpmfile
|
||||
// Check if it's an ESM module (ends with .mjs)
|
||||
if (pnpmFilePath.endsWith('.mjs')) {
|
||||
const url = pathToFileURL(path.resolve(pnpmFilePath)).href
|
||||
pnpmfile = await import(url)
|
||||
} else {
|
||||
// Use require for CommonJS modules
|
||||
pnpmfile = require(pnpmFilePath)
|
||||
}
|
||||
if (typeof pnpmfile === 'undefined') {
|
||||
logger.warn({
|
||||
message: `Ignoring the pnpmfile at "${pnpmFilePath}". It exports "undefined".`,
|
||||
@@ -89,7 +99,7 @@ export function requirePnpmfile (pnpmFilePath: string, prefix: string): { pnpmfi
|
||||
}
|
||||
|
||||
function pnpmFileExistsSync (pnpmFilePath: string): boolean {
|
||||
const pnpmFileRealName = pnpmFilePath.endsWith('.cjs')
|
||||
const pnpmFileRealName = pnpmFilePath.endsWith('.cjs') || pnpmFilePath.endsWith('.mjs')
|
||||
? pnpmFilePath
|
||||
: `${pnpmFilePath}.cjs`
|
||||
return fs.existsSync(pnpmFileRealName)
|
||||
|
||||
@@ -7,32 +7,32 @@ import { requirePnpmfile } from '../src/requirePnpmfile.js'
|
||||
const defaultHookContext: HookContext = { log () {} }
|
||||
const f = fixtures(import.meta.dirname)
|
||||
|
||||
test('ignoring a pnpmfile that exports undefined', () => {
|
||||
const { pnpmfileModule: pnpmfile } = requirePnpmfile(path.join(import.meta.dirname, '__fixtures__/undefined.js'), import.meta.dirname)!
|
||||
test('ignoring a pnpmfile that exports undefined', async () => {
|
||||
const { pnpmfileModule: pnpmfile } = (await requirePnpmfile(path.join(import.meta.dirname, '__fixtures__/undefined.js'), import.meta.dirname))!
|
||||
expect(pnpmfile).toBeUndefined()
|
||||
})
|
||||
|
||||
test('readPackage hook run fails when returns undefined', () => {
|
||||
test('readPackage hook run fails when returns undefined', async () => {
|
||||
const pnpmfilePath = path.join(import.meta.dirname, '__fixtures__/readPackageNoReturn.js')
|
||||
const { pnpmfileModule: pnpmfile } = requirePnpmfile(pnpmfilePath, import.meta.dirname)!
|
||||
const { pnpmfileModule: pnpmfile } = (await requirePnpmfile(pnpmfilePath, import.meta.dirname))!
|
||||
|
||||
return expect(
|
||||
pnpmfile!.hooks!.readPackage!({}, defaultHookContext)
|
||||
).rejects.toEqual(new BadReadPackageHookError(pnpmfilePath, 'readPackage hook did not return a package manifest object.'))
|
||||
})
|
||||
|
||||
test('readPackage hook run fails when returned dependencies is not an object', () => {
|
||||
test('readPackage hook run fails when returned dependencies is not an object', async () => {
|
||||
const pnpmfilePath = path.join(import.meta.dirname, '__fixtures__/readPackageNoObject.js')
|
||||
const { pnpmfileModule: pnpmfile } = requirePnpmfile(pnpmfilePath, import.meta.dirname)!
|
||||
const { pnpmfileModule: pnpmfile } = (await requirePnpmfile(pnpmfilePath, import.meta.dirname))!
|
||||
return expect(
|
||||
pnpmfile!.hooks!.readPackage!({}, defaultHookContext)
|
||||
).rejects.toEqual(new BadReadPackageHookError(pnpmfilePath, 'readPackage hook returned package manifest object\'s property \'dependencies\' must be an object.'))
|
||||
})
|
||||
|
||||
test('filterLog hook combines with the global hook', () => {
|
||||
test('filterLog hook combines with the global hook', async () => {
|
||||
const globalPnpmfile = path.join(import.meta.dirname, '__fixtures__/globalFilterLog.js')
|
||||
const pnpmfile = path.join(import.meta.dirname, '__fixtures__/filterLog.js')
|
||||
const { hooks } = requireHooks(import.meta.dirname, { globalPnpmfile, pnpmfiles: [pnpmfile] })
|
||||
const { hooks } = await requireHooks(import.meta.dirname, { globalPnpmfile, pnpmfiles: [pnpmfile] })
|
||||
|
||||
expect(hooks.filterLog).toBeDefined()
|
||||
expect(hooks.filterLog!).toHaveLength(2)
|
||||
@@ -49,55 +49,55 @@ test('filterLog hook combines with the global hook', () => {
|
||||
})).toBeFalsy()
|
||||
})
|
||||
|
||||
test('ignoring the default pnpmfile if tryLoadDefaultPnpmfile is not set', () => {
|
||||
const { hooks } = requireHooks(path.join(import.meta.dirname, '__fixtures__/default'), {})
|
||||
test('ignoring the default pnpmfile if tryLoadDefaultPnpmfile is not set', async () => {
|
||||
const { hooks } = await requireHooks(path.join(import.meta.dirname, '__fixtures__/default'), {})
|
||||
expect(hooks.readPackage?.length).toBe(0)
|
||||
})
|
||||
|
||||
test('loading the default pnpmfile if tryLoadDefaultPnpmfile is set to true', () => {
|
||||
const { hooks } = requireHooks(path.join(import.meta.dirname, '__fixtures__/default'), { tryLoadDefaultPnpmfile: true })
|
||||
test('loading the default pnpmfile if tryLoadDefaultPnpmfile is set to true', async () => {
|
||||
const { hooks } = await requireHooks(path.join(import.meta.dirname, '__fixtures__/default'), { tryLoadDefaultPnpmfile: true })
|
||||
expect(hooks.readPackage?.length).toBe(1)
|
||||
})
|
||||
|
||||
test('calculatePnpmfileChecksum is undefined when pnpmfile does not exist', async () => {
|
||||
const { hooks } = requireHooks(import.meta.dirname, {})
|
||||
const { hooks } = await requireHooks(import.meta.dirname, {})
|
||||
expect(hooks.calculatePnpmfileChecksum).toBeUndefined()
|
||||
})
|
||||
|
||||
test('calculatePnpmfileChecksum resolves to hash string for existing pnpmfile', async () => {
|
||||
const pnpmfile = path.join(import.meta.dirname, '__fixtures__/readPackageNoObject.js')
|
||||
const { hooks } = requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile] })
|
||||
const { hooks } = await requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile] })
|
||||
expect(typeof await hooks.calculatePnpmfileChecksum?.()).toBe('string')
|
||||
})
|
||||
|
||||
test('calculatePnpmfileChecksum is undefined if pnpmfile even when it exports undefined', async () => {
|
||||
const pnpmfile = path.join(import.meta.dirname, '__fixtures__/undefined.js')
|
||||
const { hooks } = requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile] })
|
||||
const { hooks } = await requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile] })
|
||||
expect(hooks.calculatePnpmfileChecksum).toBeUndefined()
|
||||
})
|
||||
|
||||
test('updateConfig throws an error if it returns undefined', async () => {
|
||||
const pnpmfile = path.join(import.meta.dirname, '__fixtures__/updateConfigReturnsUndefined.js')
|
||||
const { hooks } = requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile] })
|
||||
const { hooks } = await requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile] })
|
||||
expect(() => hooks.updateConfig![0]!({})).toThrow('The updateConfig hook returned undefined')
|
||||
})
|
||||
|
||||
test('requireHooks throw an error if one of the specified pnpmfiles does not exist', async () => {
|
||||
expect(() => requireHooks(import.meta.dirname, { pnpmfiles: ['does-not-exist.cjs'] })).toThrow('is not found')
|
||||
await expect(requireHooks(import.meta.dirname, { pnpmfiles: ['does-not-exist.cjs'] })).rejects.toThrow('is not found')
|
||||
})
|
||||
|
||||
test('requireHooks throws an error if there are two finders with the same name', async () => {
|
||||
const findersDir = f.find('finders')
|
||||
const pnpmfile1 = path.join(findersDir, 'finderFoo1.js')
|
||||
const pnpmfile2 = path.join(findersDir, 'finderFoo2.js')
|
||||
expect(() => requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile1, pnpmfile2] })).toThrow('Finder "foo" defined in both')
|
||||
await expect(requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile1, pnpmfile2] })).rejects.toThrow('Finder "foo" defined in both')
|
||||
})
|
||||
|
||||
test('requireHooks merges all the finders', async () => {
|
||||
const findersDir = f.find('finders')
|
||||
const pnpmfile1 = path.join(findersDir, 'finderFoo1.js')
|
||||
const pnpmfile2 = path.join(findersDir, 'finderBar.js')
|
||||
const { finders } = requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile1, pnpmfile2] })
|
||||
const { finders } = await requireHooks(import.meta.dirname, { pnpmfiles: [pnpmfile1, pnpmfile2] })
|
||||
expect(finders.foo).toBeDefined()
|
||||
expect(finders.bar).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -314,8 +314,8 @@ export async function recursive (
|
||||
limitInstallation(async () => {
|
||||
const hooks = opts.ignorePnpmfile
|
||||
? {}
|
||||
: (() => {
|
||||
const { hooks: pnpmfileHooks } = requireHooks(rootDir, opts)
|
||||
: await (async () => {
|
||||
const { hooks: pnpmfileHooks } = await requireHooks(rootDir, opts)
|
||||
return {
|
||||
...opts.hooks,
|
||||
...pnpmfileHooks,
|
||||
|
||||
@@ -335,6 +335,25 @@ module.exports = {
|
||||
expect(nodeModulesFiles).toContain('is-number')
|
||||
})
|
||||
|
||||
test('loading an ESM pnpmfile', async () => {
|
||||
prepare()
|
||||
|
||||
fs.writeFileSync('.pnpmfile.mjs', `
|
||||
export const hooks = {
|
||||
updateConfig: (config) => ({
|
||||
...config,
|
||||
nodeLinker: 'hoisted',
|
||||
}),
|
||||
}`, 'utf8')
|
||||
writeYamlFile('pnpm-workspace.yaml', { pnpmfile: ['.pnpmfile.mjs'] })
|
||||
|
||||
await execPnpm(['add', 'is-odd@1.0.0'])
|
||||
|
||||
const nodeModulesFiles = fs.readdirSync('node_modules')
|
||||
expect(nodeModulesFiles).toContain('kind-of')
|
||||
expect(nodeModulesFiles).toContain('is-number')
|
||||
})
|
||||
|
||||
test('loading multiple pnpmfiles', async () => {
|
||||
prepare()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user