feat: a new setting for injecting workspace packages (#8836)

This commit is contained in:
Zoltan Kochan
2024-12-05 17:37:15 +01:00
committed by GitHub
parent b8bda0ac40
commit 6483b646fe
19 changed files with 255 additions and 6 deletions

View File

@@ -0,0 +1,15 @@
---
"@pnpm/resolve-dependencies": minor
"@pnpm/package-requester": minor
"@pnpm/store-controller-types": minor
"@pnpm/lockfile.settings-checker": minor
"@pnpm/resolver-base": minor
"@pnpm/npm-resolver": minor
"@pnpm/core": minor
"@pnpm/lockfile.types": minor
"@pnpm/config": minor
"@pnpm/deps.status": minor
"pnpm": minor
---
A new setting, `inject-workspace-packages`, has been added to allow hard-linking all local workspace dependencies instead of symlinking them. Previously, this behavior was achievable via the [`dependenciesMeta[].injected`](https://pnpm.io/package_json#dependenciesmetainjected) setting, which remains supported [#8836](https://github.com/pnpm/pnpm/pull/8836).

View File

@@ -148,6 +148,7 @@ export interface Config {
reporter?: string
aggregateOutput: boolean
linkWorkspacePackages: boolean | 'deep'
injectWorkspacePackages?: boolean
preferWorkspacePackages: boolean
reverse: boolean
sort: boolean

View File

@@ -147,6 +147,7 @@ export async function getConfig (opts: {
'hoist-workspace-packages': true,
'ignore-workspace-cycles': false,
'ignore-workspace-root-check': false,
'inject-workspace-packages': false,
'link-workspace-packages': false,
'lockfile-include-tarball-url': false,
'manage-package-manager-versions': true,

View File

@@ -42,6 +42,7 @@ export const types = Object.assign({
'ignore-workspace-cycles': Boolean,
'ignore-workspace-root-check': Boolean,
'include-workspace-root': Boolean,
'inject-workspace-packages': Boolean,
'legacy-dir-filtering': Boolean,
'link-workspace-packages': [Boolean, 'deep'],
lockfile: Boolean,

View File

@@ -46,6 +46,7 @@ export type CheckDepsStatusOptions = Pick<Config,
| 'autoInstallPeers'
| 'catalogs'
| 'excludeLinksFromLockfile'
| 'injectWorkspacePackages'
| 'linkWorkspacePackages'
| 'hooks'
| 'peersSuffixMaxLength'
@@ -74,6 +75,7 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
const {
allProjects,
autoInstallPeers,
injectWorkspacePackages,
catalogs,
excludeLinksFromLockfile,
linkWorkspacePackages,
@@ -192,6 +194,7 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
const assertCtx: AssertWantedLockfileUpToDateContext = {
autoInstallPeers,
injectWorkspacePackages,
config: opts,
excludeLinksFromLockfile,
linkWorkspacePackages,
@@ -272,6 +275,7 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
logger.debug({ msg: 'The manifest is newer than the lockfile. Continuing check.' })
await assertWantedLockfileUpToDate({
autoInstallPeers,
injectWorkspacePackages,
config: opts,
excludeLinksFromLockfile,
linkWorkspacePackages,
@@ -311,6 +315,7 @@ interface AssertWantedLockfileUpToDateContext {
autoInstallPeers?: boolean
config: CheckDepsStatusOptions
excludeLinksFromLockfile?: boolean
injectWorkspacePackages?: boolean
linkWorkspacePackages: boolean | 'deep'
getManifestsByDir: () => Record<string, DependencyManifest>
getWorkspacePackages: () => WorkspacePackages | undefined
@@ -359,6 +364,7 @@ async function assertWantedLockfileUpToDate (
const outdatedLockfileSettingName = getOutdatedLockfileSetting(wantedLockfile, {
autoInstallPeers: config.autoInstallPeers,
injectWorkspacePackages: config.injectWorkspacePackages,
excludeLinksFromLockfile: config.excludeLinksFromLockfile,
peersSuffixMaxLength: config.peersSuffixMaxLength,
overrides: createOverridesMapFromParsed(parseOverrides(rootManifestOptions?.overrides ?? {}, config.catalogs)),

View File

@@ -65,6 +65,9 @@ export function convertToLockfileFile (lockfile: Lockfile, opts: NormalizeLockfi
if (newLockfile.settings?.peersSuffixMaxLength === 1000) {
newLockfile.settings = omit(['peersSuffixMaxLength'], newLockfile.settings)
}
if (newLockfile.settings?.injectWorkspacePackages === false) {
delete newLockfile.settings.injectWorkspacePackages
}
return normalizeLockfile(newLockfile, opts)
}

View File

@@ -9,6 +9,7 @@ export type ChangedField =
| 'settings.autoInstallPeers'
| 'settings.excludeLinksFromLockfile'
| 'settings.peersSuffixMaxLength'
| 'settings.injectWorkspacePackages'
| 'pnpmfileChecksum'
export function getOutdatedLockfileSetting (
@@ -22,6 +23,7 @@ export function getOutdatedLockfileSetting (
excludeLinksFromLockfile,
peersSuffixMaxLength,
pnpmfileChecksum,
injectWorkspacePackages,
}: {
overrides?: Record<string, string>
packageExtensionsChecksum?: string
@@ -31,6 +33,7 @@ export function getOutdatedLockfileSetting (
excludeLinksFromLockfile?: boolean
peersSuffixMaxLength?: number
pnpmfileChecksum?: string
injectWorkspacePackages?: boolean
}
): ChangedField | null {
if (!equals(lockfile.overrides ?? {}, overrides ?? {})) {
@@ -60,5 +63,8 @@ export function getOutdatedLockfileSetting (
if (lockfile.pnpmfileChecksum !== pnpmfileChecksum) {
return 'pnpmfileChecksum'
}
if (Boolean(lockfile.settings?.injectWorkspacePackages) !== Boolean(injectWorkspacePackages)) {
return 'settings.injectWorkspacePackages'
}
return null
}

View File

@@ -9,6 +9,7 @@ export interface LockfileSettings {
autoInstallPeers?: boolean
excludeLinksFromLockfile?: boolean
peersSuffixMaxLength?: number
injectWorkspacePackages?: boolean
}
export interface Lockfile {

View File

@@ -46,7 +46,7 @@ export async function getPeerDependencyIssues (
const projectsToResolve = Object.values(ctx.projects).map((project) => ({
...project,
updatePackageManifest: false,
wantedDependencies: getWantedDependencies(project.manifest),
wantedDependencies: getWantedDependencies(project.manifest, opts),
}))
const preferredVersions = getPreferredVersionsFromLockfileAndManifests(
ctx.wantedLockfile.packages,

View File

@@ -155,6 +155,7 @@ export interface StrictInstallOptions {
peersSuffixMaxLength: number
prepareExecutionEnv?: PrepareExecutionEnv
returnListOfDepsRequiringBuild?: boolean
injectWorkspacePackages?: boolean
}
export type InstallOptions =

View File

@@ -361,6 +361,7 @@ export async function mutateModules (
if (!opts.ignorePackageManifest) {
const outdatedLockfileSettingName = getOutdatedLockfileSetting(ctx.wantedLockfile, {
autoInstallPeers: opts.autoInstallPeers,
injectWorkspacePackages: opts.injectWorkspacePackages,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
peersSuffixMaxLength: opts.peersSuffixMaxLength,
overrides: overridesMap,
@@ -384,6 +385,7 @@ export async function mutateModules (
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
peersSuffixMaxLength: opts.peersSuffixMaxLength,
injectWorkspacePackages: opts.injectWorkspacePackages,
}
ctx.wantedLockfile.overrides = overridesMap
ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum
@@ -395,6 +397,7 @@ export async function mutateModules (
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
peersSuffixMaxLength: opts.peersSuffixMaxLength,
injectWorkspacePackages: opts.injectWorkspacePackages,
}
}
if (
@@ -976,6 +979,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot,
supportedArchitectures: opts.supportedArchitectures,
peersSuffixMaxLength: opts.peersSuffixMaxLength,
injectWorkspacePackages: opts.injectWorkspacePackages,
}
)
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
@@ -1355,6 +1359,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => {
includeDirect: opts.includeDirect,
updateWorkspaceDependencies: false,
nodeExecPath: opts.nodeExecPath,
injectWorkspacePackages: opts.injectWorkspacePackages,
}
for (const project of allProjectsLocatedInsideWorkspace) {
if (!newProjects.some(({ rootDir }) => rootDir === project.rootDir)) {

View File

@@ -212,6 +212,204 @@ test('inject local packages', async () => {
}
})
test('inject local packages using the injectWorkspacePackages setting', async () => {
const project1Manifest = {
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
devDependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0',
},
peerDependencies: {
'is-positive': '>=1.0.0',
},
}
const project2Manifest = {
name: 'project-2',
version: '1.0.0',
dependencies: {
'project-1': 'workspace:1.0.0',
},
devDependencies: {
'is-positive': '1.0.0',
},
}
const project3Manifest = {
name: 'project-3',
version: '1.0.0',
dependencies: {
'project-2': 'workspace:1.0.0',
},
devDependencies: {
'is-positive': '2.0.0',
},
}
const projects = preparePackages([
{
location: 'project-1',
package: project1Manifest,
},
{
location: 'project-2',
package: project2Manifest,
},
{
location: 'project-3',
package: project3Manifest,
},
])
const importers: MutatedProject[] = [
{
mutation: 'install',
rootDir: path.resolve('project-1') as ProjectRootDir,
},
{
mutation: 'install',
rootDir: path.resolve('project-2') as ProjectRootDir,
},
{
mutation: 'install',
rootDir: path.resolve('project-3') as ProjectRootDir,
},
]
const allProjects: ProjectOptions[] = [
{
buildIndex: 0,
manifest: project1Manifest,
rootDir: path.resolve('project-1') as ProjectRootDir,
},
{
buildIndex: 0,
manifest: project2Manifest,
rootDir: path.resolve('project-2') as ProjectRootDir,
},
{
buildIndex: 0,
manifest: project3Manifest,
rootDir: path.resolve('project-3') as ProjectRootDir,
},
]
await mutateModules(importers, testDefaults({
autoInstallPeers: false,
allProjects,
injectWorkspacePackages: true,
}))
projects['project-1'].has('is-negative')
projects['project-1'].has('@pnpm.e2e/dep-of-pkg-with-1-dep')
projects['project-1'].hasNot('is-positive')
projects['project-2'].has('is-positive')
projects['project-2'].has('project-1')
projects['project-3'].has('is-positive')
projects['project-3'].has('project-2')
expect(fs.readdirSync('node_modules/.pnpm').length).toBe(8)
const rootModules = assertProject(process.cwd())
{
const lockfile = rootModules.readLockfile()
expect(lockfile.settings.injectWorkspacePackages).toBe(true)
expect(lockfile.importers['project-2'].dependenciesMeta).not.toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['project-1@file:project-1']).toEqual({
resolution: {
directory: 'project-1',
type: 'directory',
},
peerDependencies: {
'is-positive': '>=1.0.0',
},
})
expect(lockfile.snapshots['project-1@file:project-1(is-positive@1.0.0)']).toEqual({
dependencies: {
'is-negative': '1.0.0',
'is-positive': '1.0.0',
},
})
expect(lockfile.packages['project-2@file:project-2']).toEqual({
resolution: {
directory: 'project-2',
type: 'directory',
},
})
expect(lockfile.snapshots['project-2@file:project-2(is-positive@2.0.0)']).toEqual({
dependencies: {
'project-1': 'file:project-1(is-positive@2.0.0)',
},
transitivePeerDependencies: ['is-positive'],
})
const modulesState = rootModules.readModulesManifest()
expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(2)
expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`)
expect(modulesState?.injectedDeps?.['project-1'][1]).toContain(`node_modules${path.sep}.pnpm`)
}
rimraf('node_modules')
rimraf('project-1/node_modules')
rimraf('project-2/node_modules')
rimraf('project-3/node_modules')
await mutateModules(importers, testDefaults({
autoInstallPeers: false,
allProjects,
frozenLockfile: true,
injectWorkspacePackages: true,
}))
projects['project-1'].has('is-negative')
projects['project-1'].has('@pnpm.e2e/dep-of-pkg-with-1-dep')
projects['project-1'].hasNot('is-positive')
projects['project-2'].has('is-positive')
projects['project-2'].has('project-1')
projects['project-3'].has('is-positive')
projects['project-3'].has('project-2')
expect(fs.readdirSync('node_modules/.pnpm').length).toBe(8)
// The injected project is updated when one of its dependencies needs to be updated
allProjects[0].manifest.dependencies!['is-negative'] = '2.0.0'
await mutateModules(importers, testDefaults({ autoInstallPeers: false, allProjects, injectWorkspacePackages: true }))
{
const lockfile = rootModules.readLockfile()
expect(lockfile.settings.injectWorkspacePackages).toBe(true)
expect(lockfile.importers['project-2'].dependenciesMeta).not.toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['project-1@file:project-1']).toEqual({
resolution: {
directory: 'project-1',
type: 'directory',
},
peerDependencies: {
'is-positive': '>=1.0.0',
},
})
expect(lockfile.snapshots['project-1@file:project-1(is-positive@1.0.0)']).toEqual({
dependencies: {
'is-negative': '2.0.0',
'is-positive': '1.0.0',
},
})
const modulesState = rootModules.readModulesManifest()
expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(2)
expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`)
expect(modulesState?.injectedDeps?.['project-1'][1]).toContain(`node_modules${path.sep}.pnpm`)
}
})
test('inject local packages declared via file protocol', async () => {
const project1Manifest = {
name: 'project-1',

View File

@@ -188,6 +188,7 @@ async function resolveAndFetch (
registry: options.registry,
workspacePackages: options.workspacePackages,
updateToLatest: options.updateToLatest,
injectWorkspacePackages: options.injectWorkspacePackages,
}), { priority: options.downloadPriority })
manifest = resolveResult.manifest

View File

@@ -127,6 +127,7 @@ export async function resolveDependencies (
workspacePackages: opts.workspacePackages,
updateToLatest: opts.updateToLatest,
noDependencySelectors: importers.every(({ wantedDependencies }) => wantedDependencies.length === 0),
injectWorkspacePackages: opts.injectWorkspacePackages,
})
const projectsToResolve = await Promise.all(importers.map(async (project) => _toResolveImporter(project)))
const {

View File

@@ -153,6 +153,7 @@ export interface ResolutionContext {
pendingNodes: PendingNode[]
wantedLockfile: Lockfile
currentLockfile: Lockfile
injectWorkspacePackages?: boolean
linkWorkspacePackagesDepth: number
lockfileDir: string
storeController: StoreController
@@ -1261,6 +1262,7 @@ async function resolveDependency (
return err
},
updateToLatest: options.updateToLatest,
injectWorkspacePackages: ctx.injectWorkspacePackages,
})
} catch (err: any) { // eslint-disable-line
const wantedDependencyDetails = {

View File

@@ -118,6 +118,7 @@ export interface ResolveDependenciesOptions {
preferWorkspacePackages?: boolean
resolutionMode?: 'highest' | 'time-based' | 'lowest-direct'
resolvePeersFromWorkspaceRoot?: boolean
injectWorkspacePackages?: boolean
linkWorkspacePackagesDepth?: number
lockfileDir: string
storeController: StoreController
@@ -165,6 +166,7 @@ export async function resolveDependencyTree<T> (
force: opts.force,
forceFullResolution: opts.forceFullResolution,
ignoreScripts: opts.ignoreScripts,
injectWorkspacePackages: opts.injectWorkspacePackages,
linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? -1,
lockfileDir: opts.lockfileDir,
nodeVersion: opts.nodeVersion,

View File

@@ -118,6 +118,7 @@ export type ResolveFromNpmOptions = {
preferredVersions?: PreferredVersions
preferWorkspacePackages?: boolean
updateToLatest?: boolean
injectWorkspacePackages?: boolean
} & ({
projectDir?: string
workspacePackages?: undefined
@@ -143,6 +144,7 @@ async function resolveNpm (
projectDir: opts.projectDir,
registry: opts.registry,
workspacePackages: opts.workspacePackages,
injectWorkspacePackages: opts.injectWorkspacePackages,
})
if (resolvedFromWorkspace != null) {
return resolvedFromWorkspace
@@ -173,7 +175,7 @@ async function resolveNpm (
wantedDependency,
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
})
} catch {
// ignore
@@ -190,7 +192,7 @@ async function resolveNpm (
wantedDependency,
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
})
} catch {
// ignore
@@ -207,7 +209,7 @@ async function resolveNpm (
...resolveFromLocalPackage(matchedPkg, spec.normalizedPref, {
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
}),
latest: meta['dist-tags'].latest,
}
@@ -218,7 +220,7 @@ async function resolveNpm (
...resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec.normalizedPref, {
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
}),
latest: meta['dist-tags'].latest,
}
@@ -249,6 +251,7 @@ function tryResolveFromWorkspace (
projectDir?: string
registry: string
workspacePackages?: WorkspacePackages
injectWorkspacePackages?: boolean
}
): ResolveResult | null {
if (!wantedDependency.pref?.startsWith('workspace:')) {
@@ -267,7 +270,7 @@ function tryResolveFromWorkspace (
return tryResolveFromWorkspacePackages(opts.workspacePackages, spec, {
wantedDependency,
projectDir: opts.projectDir,
hardLinkLocalPackages: wantedDependency.injected,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
lockfileDir: opts.lockfileDir,
})
}

View File

@@ -83,6 +83,7 @@ export interface ResolveOptions {
registry: string
workspacePackages?: WorkspacePackages
updateToLatest?: boolean
injectWorkspacePackages?: boolean
}
export type WantedDependency = {

View File

@@ -132,6 +132,7 @@ export interface RequestPackageOptions {
supportedArchitectures?: SupportedArchitectures
onFetchError?: OnFetchError
updateToLatest?: boolean
injectWorkspacePackages?: boolean
}
export type BundledManifestFunction = () => Promise<BundledManifest | undefined>