feat: injecting workspace packages (#3915)

close #3510
This commit is contained in:
Zoltan Kochan
2021-11-01 04:14:52 +02:00
committed by GitHub
parent 37dcfceeba
commit 4ab87844a9
61 changed files with 1053 additions and 113 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/types": minor
---
New optional field added to `dependenciesMeta`: `injected`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lifecycle": minor
---
`runLifecycleHooksConcurrently` will relink projects after rebuilding them if they are injected to other projects.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/fetcher-base": minor
---
The files response can point to files that are not in the global content-addressable store. In this case, the response will contain a `local: true` property, and the structure of `filesIndex` will be just a `Record<string, string>`.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/local-resolver": minor
"@pnpm/npm-resolver": minor
---
Support the resolution of injected local dependencies.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lifecycle": major
---
`storeController` is a required new option of `runLifecycleHooksConcurrently()`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/resolver-base": minor
---
New optional property added to `WantedDependency`: `injected`.

View File

@@ -0,0 +1,7 @@
---
"@pnpm/package-requester": minor
"@pnpm/package-store": minor
"@pnpm/resolve-dependencies": minor
---
Added support for "injected" dependencies.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/fetcher-base": minor
---
New optional property is added to `PackageFilesResponse` for specifying how the package needs to be imported to the modules directory. Should it be hard linked, copied, or cloned.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/directory-fetcher": major
---
Initial release.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/client": minor
---
New fetcher added for fetching local directory dependencies.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-rebuild": minor
---
Injected dependencies should be relinked after they are rebuilt.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/lockfile-file": minor
"@pnpm/lockfile-types": minor
---
New optional property added to project snapshots: `dependenciesMeta`.

View File

@@ -0,0 +1,7 @@
---
"@pnpm/client": major
"@pnpm/local-resolver": major
"@pnpm/default-resolver": major
---
Local directory dependencies are resolved to absolute path.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/lockfile-utils": minor
---
New utility function added: `extendProjectsWithTargetDirs()`.

View File

@@ -0,0 +1,59 @@
---
"@pnpm/core": minor
"@pnpm/headless": minor
"@pnpm/plugin-commands-installation": minor
"pnpm": minor
---
New property supported via the `dependenciesMeta` field of `package.json`: `injected`. When `injected` is set to `true`, the package will be hard linked to `node_modules`, not symlinked [#3915](https://github.com/pnpm/pnpm/pull/3915).
For instance, the following `package.json` in a workspace will create a symlink to `bar` in the `node_modules` directory of `foo`:
```json
{
"name": "foo",
"dependencies": {
"bar": "workspace:1.0.0"
}
}
```
But what if `bar` has `react` in its peer dependencies? If all projects in the monorepo use the same version of `react`, then no problem. But what if `bar` is required by `foo` that uses `react` 16 and `qar` with `react` 17? In the past, you'd have to choose a single version of react and install it as dev dependency of `bar`. But now with the `injected` field you can inject `bar` to a package, and `bar` will be installed with the `react` version of that package.
So this will be the `package.json` of `foo`:
```json
{
"name": "foo",
"dependencies": {
"bar": "workspace:1.0.0",
"react": "16"
},
"dependenciesMeta": {
"bar": {
"injected": true
}
}
}
```
`bar` will be hard linked into the dependencies of `foo`, and `react` 16 will be linked to the dependencies of `foo/node_modules/bar`.
And this will be the `package.json` of `qar`:
```json
{
"name": "qar",
"dependencies": {
"bar": "workspace:1.0.0",
"react": "17"
},
"dependenciesMeta": {
"bar": {
"injected": true
}
}
}
```
`bar` will be hard linked into the dependencies of `qar`, and `react` 17 will be linked to the dependencies of `qar/node_modules/bar`.

View File

@@ -32,6 +32,7 @@
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/client#readme",
"dependencies": {
"@pnpm/default-resolver": "workspace:13.0.9",
"@pnpm/directory-fetcher": "workspace:0.0.0",
"@pnpm/fetch": "workspace:4.1.3",
"@pnpm/fetching-types": "workspace:2.2.1",
"@pnpm/git-fetcher": "workspace:4.1.6",

View File

@@ -4,6 +4,7 @@ import createResolve, {
} from '@pnpm/default-resolver'
import { AgentOptions, createFetchFromRegistry } from '@pnpm/fetch'
import { FetchFromRegistry, GetCredentials, RetryTimeoutOptions } from '@pnpm/fetching-types'
import createDirectoryFetcher from '@pnpm/directory-fetcher'
import fetchFromGit from '@pnpm/git-fetcher'
import createTarballFetcher from '@pnpm/tarball-fetcher'
import getCredentialsByURI from 'credentials-by-uri'
@@ -43,5 +44,6 @@ function createFetchers (
return {
...createTarballFetcher(fetchFromRegistry, getCredentials, opts),
...fetchFromGit(),
...createDirectoryFetcher(),
}
}

View File

@@ -12,6 +12,9 @@
{
"path": "../default-resolver"
},
{
"path": "../directory-fetcher"
},
{
"path": "../fetch"
},

View File

@@ -68,6 +68,7 @@ function getWantedDependenciesFromGivenSet (
return {
alias,
dev: depType === 'dev',
injected: opts.dependenciesMeta[alias]?.injected,
optional: depType === 'optional',
nodeExecPath: opts.nodeExecPath ?? opts.dependenciesMeta[alias]?.node,
pinnedVersion: guessPinnedVersionFromExistingSpec(deps[alias]),

View File

@@ -21,11 +21,13 @@ import {
import linkBins, { linkBinsOfPackages } from '@pnpm/link-bins'
import {
ProjectSnapshot,
Lockfile,
writeCurrentLockfile,
writeLockfiles,
writeWantedLockfile,
} from '@pnpm/lockfile-file'
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
import { extendProjectsWithTargetDirs } from '@pnpm/lockfile-utils'
import logger, { streamParser } from '@pnpm/logger'
import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils'
import { write as writeModulesYaml } from '@pnpm/modules-yaml'
@@ -129,6 +131,14 @@ export async function install (
return projects[0].manifest
}
interface ProjectToBeInstalled {
id: string
buildIndex: number
manifest: ProjectManifest
modulesDir: string
rootDir: string
}
export type MutatedProject = ProjectOptions & DependenciesMutation
export async function mutateModules (
@@ -310,13 +320,14 @@ export async function mutateModules (
const projectsToInstall = [] as ImporterToUpdate[]
const projectsToBeInstalled = ctx.projects.filter(({ mutation }) => mutation === 'install') as Array<{ buildIndex: number, rootDir: string, manifest: ProjectManifest, modulesDir: string }>
const projectsToBeInstalled = ctx.projects.filter(({ mutation }) => mutation === 'install') as ProjectToBeInstalled[]
const scriptsOpts: RunLifecycleHooksConcurrentlyOptions = {
extraBinPaths: opts.extraBinPaths,
rawConfig: opts.rawConfig,
scriptShell: opts.scriptShell,
shellEmulator: opts.shellEmulator,
stdio: opts.ownLifecycleHooksStdio,
storeController: opts.storeController,
unsafePerm: opts.unsafePerm || false,
}
@@ -480,14 +491,15 @@ export async function mutateModules (
if (opts.enablePnp) {
scriptsOpts.extraEnv = makeNodeRequireOption(path.join(opts.lockfileDir, '.pnp.cjs'))
}
const projectsToBeBuilt = extendProjectsWithTargetDirs(projectsToBeInstalled, result.newLockfile, ctx)
await runLifecycleHooksConcurrently(['preinstall', 'install', 'postinstall', 'prepare'],
projectsToBeInstalled,
projectsToBeBuilt,
opts.childConcurrency,
scriptsOpts
)
}
return result
return result.projects
}
}
@@ -663,7 +675,7 @@ type InstallFunction = (
pruneVirtualStore: boolean
currentLockfileIsUpToDate: boolean
}
) => Promise<Array<{ rootDir: string, manifest: ProjectManifest }>>
) => Promise<{ projects: Array<{ rootDir: string, manifest: ProjectManifest }>, newLockfile: Lockfile }>
const _installInContext: InstallFunction = async (projects, ctx, opts) => {
if (opts.lockfileOnly && ctx.existsCurrentLockfile) {
@@ -998,7 +1010,10 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
await opts.storeController.close()
return projectsToResolve.map(({ manifest, rootDir }) => ({ rootDir, manifest }))
return {
newLockfile,
projects: projectsToResolve.map(({ manifest, rootDir }) => ({ rootDir, manifest })),
}
}
const installInContext: InstallFunction = async (projects, ctx, opts) => {

View File

@@ -224,3 +224,94 @@ test('allProjectsAreUpToDate(): use link and registry version if linkWorkspacePa
)
).toBeTruthy()
})
test('allProjectsAreUpToDate(): returns false if dependenciesMeta differs', async () => {
expect(await allProjectsAreUpToDate([
{
id: 'bar',
manifest: {
dependencies: {
foo: 'workspace:../foo',
},
dependenciesMeta: {
foo: {
injected: true,
},
},
},
rootDir: 'bar',
},
{
id: 'foo',
manifest: fooManifest,
rootDir: 'foo',
},
], {
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
bar: {
dependencies: {
foo: 'link:../foo',
},
specifiers: {
foo: 'workspace:../foo',
},
},
foo: {
specifiers: {},
},
},
lockfileVersion: 5,
},
workspacePackages,
})).toBeFalsy()
})
test('allProjectsAreUpToDate(): returns true if dependenciesMeta matches', async () => {
expect(await allProjectsAreUpToDate([
{
id: 'bar',
manifest: {
dependencies: {
foo: 'workspace:../foo',
},
dependenciesMeta: {
foo: {
injected: true,
},
},
},
rootDir: 'bar',
},
{
id: 'foo',
manifest: fooManifest,
rootDir: 'foo',
},
], {
linkWorkspacePackages: true,
wantedLockfile: {
importers: {
bar: {
dependencies: {
foo: 'link:../foo',
},
dependenciesMeta: {
foo: {
injected: true,
},
},
specifiers: {
foo: 'workspace:../foo',
},
},
foo: {
specifiers: {},
},
},
lockfileVersion: 5,
},
workspacePackages,
})).toBeTruthy()
})

View File

@@ -0,0 +1,284 @@
import path from 'path'
import assertProject from '@pnpm/assert-project'
import { MutatedProject, mutateModules } from '@pnpm/core'
import { preparePackages } from '@pnpm/prepare'
import rimraf from '@zkochan/rimraf'
import pathExists from 'path-exists'
import { testDefaults } from '../utils'
test('inject local packages', async () => {
const project1Manifest = {
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
devDependencies: {
'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: {
'is-positive': '1.0.0',
'project-1': 'workspace:1.0.0',
},
dependenciesMeta: {
'project-1': {
injected: true,
},
},
}
const projects = preparePackages([
{
location: 'project-1',
package: project1Manifest,
},
{
location: 'project-2',
package: project2Manifest,
},
])
const importers: MutatedProject[] = [
{
buildIndex: 0,
manifest: project1Manifest,
mutation: 'install',
rootDir: path.resolve('project-1'),
},
{
buildIndex: 0,
manifest: project2Manifest,
mutation: 'install',
rootDir: path.resolve('project-2'),
},
]
const workspacePackages = {
'project-1': {
'1.0.0': {
dir: path.resolve('project-1'),
manifest: project1Manifest,
},
},
'project-2': {
'1.0.0': {
dir: path.resolve('project-2'),
manifest: project2Manifest,
},
},
}
await mutateModules(importers, await testDefaults({
workspacePackages,
}))
await projects['project-1'].has('is-negative')
await projects['project-1'].has('dep-of-pkg-with-1-dep')
await projects['project-1'].hasNot('is-positive')
await projects['project-2'].has('is-positive')
await projects['project-2'].has('project-1')
const rootModules = assertProject(process.cwd())
{
const lockfile = await rootModules.readLockfile()
expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['file:project-1_is-positive@1.0.0']).toEqual({
resolution: {
directory: 'project-1',
type: 'directory',
},
id: 'file:project-1',
name: 'project-1',
version: '1.0.0',
peerDependencies: {
'is-positive': '1.0.0',
},
dependencies: {
'is-negative': '1.0.0',
'is-positive': '1.0.0',
},
dev: false,
})
}
await rimraf('node_modules')
await rimraf('project-1/node_modules')
await rimraf('project-2/node_modules')
await mutateModules(importers, await testDefaults({
frozenLockfile: true,
workspacePackages,
}))
await projects['project-1'].has('is-negative')
await projects['project-1'].has('dep-of-pkg-with-1-dep')
await projects['project-1'].hasNot('is-positive')
await projects['project-2'].has('is-positive')
await projects['project-2'].has('project-1')
// The injected project is updated when on of its dependencies needs to be updated
importers[0].manifest.dependencies!['is-negative'] = '2.0.0'
await mutateModules(importers, await testDefaults({ workspacePackages }))
{
const lockfile = await rootModules.readLockfile()
expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['file:project-1_is-positive@1.0.0']).toEqual({
resolution: {
directory: 'project-1',
type: 'directory',
},
id: 'file:project-1',
name: 'project-1',
version: '1.0.0',
peerDependencies: {
'is-positive': '1.0.0',
},
dependencies: {
'is-negative': '2.0.0',
'is-positive': '1.0.0',
},
dev: false,
})
}
})
test('inject local packages and relink them after build', async () => {
const project1Manifest = {
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
devDependencies: {
'dep-of-pkg-with-1-dep': '100.0.0',
},
peerDependencies: {
'is-positive': '1.0.0',
},
scripts: {
prepublishOnly: 'touch main.js',
},
}
const project2Manifest = {
name: 'project-2',
version: '1.0.0',
dependencies: {
'is-positive': '1.0.0',
'project-1': 'workspace:1.0.0',
},
dependenciesMeta: {
'project-1': {
injected: true,
},
},
}
const projects = preparePackages([
{
location: 'project-1',
package: project1Manifest,
},
{
location: 'project-2',
package: project2Manifest,
},
])
const importers: MutatedProject[] = [
{
buildIndex: 0,
manifest: project1Manifest,
mutation: 'install',
rootDir: path.resolve('project-1'),
},
{
buildIndex: 0,
manifest: project2Manifest,
mutation: 'install',
rootDir: path.resolve('project-2'),
},
]
const workspacePackages = {
'project-1': {
'1.0.0': {
dir: path.resolve('project-1'),
manifest: project1Manifest,
},
},
'project-2': {
'1.0.0': {
dir: path.resolve('project-2'),
manifest: project2Manifest,
},
},
}
await mutateModules(importers, await testDefaults({
workspacePackages,
}))
await projects['project-1'].has('is-negative')
await projects['project-1'].has('dep-of-pkg-with-1-dep')
await projects['project-1'].hasNot('is-positive')
await projects['project-2'].has('is-positive')
await projects['project-2'].has('project-1')
expect(await pathExists(path.resolve('project-2/node_modules/project-1/main.js'))).toBeTruthy()
const rootModules = assertProject(process.cwd())
const lockfile = await rootModules.readLockfile()
expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['file:project-1_is-positive@1.0.0']).toEqual({
resolution: {
directory: 'project-1',
type: 'directory',
},
id: 'file:project-1',
name: 'project-1',
version: '1.0.0',
peerDependencies: {
'is-positive': '1.0.0',
},
dependencies: {
'is-negative': '1.0.0',
'is-positive': '1.0.0',
},
dev: false,
})
await rimraf('node_modules')
await rimraf('project-1/main.js')
await rimraf('project-1/node_modules')
await rimraf('project-2/node_modules')
await mutateModules(importers, await testDefaults({
frozenLockfile: true,
workspacePackages,
}))
await projects['project-1'].has('is-negative')
await projects['project-1'].has('dep-of-pkg-with-1-dep')
await projects['project-1'].hasNot('is-positive')
await projects['project-2'].has('is-positive')
await projects['project-2'].has('project-1')
expect(await pathExists(path.resolve('project-2/node_modules/project-1/main.js'))).toBeTruthy()
})

View File

@@ -0,0 +1,15 @@
# @pnpm/directory-fetcher
> Fetcher for local directory packages
[![npm version](https://img.shields.io/npm/v/@pnpm/directory-fetcher.svg)](https://www.npmjs.com/package/@pnpm/directory-fetcher)
## Installation
```
<pnpm|npm|yarn> add @pnpm/directory-fetcher
```
## License
MIT

View File

@@ -0,0 +1 @@
module.exports = require('../../jest.config.js')

View File

@@ -0,0 +1,40 @@
{
"name": "@pnpm/directory-fetcher",
"version": "0.0.0",
"description": "A fetcher for local directory packages",
"funding": "https://opencollective.com/pnpm",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"scripts": {
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"lint": "eslint src/**/*.ts test/**/*.ts",
"prepublishOnly": "pnpm run compile",
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/master/packages/directory-fetcher",
"engines": {
"node": ">=12.17"
},
"keywords": [
"pnpm6",
"pnpm",
"fetcher"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/directory-fetcher#readme",
"dependencies": {
"@pnpm/fetcher-base": "workspace:11.0.3",
"@pnpm/resolver-base": "workspace:8.0.4",
"load-json-file": "^6.2.0",
"npm-packlist": "^2.2.2",
"ramda": "^0.27.1"
}
}

View File

@@ -0,0 +1,36 @@
import path from 'path'
import { Cafs, DeferredManifestPromise } from '@pnpm/fetcher-base'
import { DirectoryResolution } from '@pnpm/resolver-base'
import fromPairs from 'ramda/src/fromPairs'
import loadJsonFile from 'load-json-file'
import packlist from 'npm-packlist'
export interface DirectoryFetcherOptions {
manifest?: DeferredManifestPromise
}
export default () => {
return {
directory: (
cafs: Cafs,
resolution: DirectoryResolution,
opts: DirectoryFetcherOptions
) => fetchFromDir(resolution.directory, opts),
}
}
export async function fetchFromDir (
dir: string,
opts: DirectoryFetcherOptions
) {
const files = await packlist({ path: dir })
const filesIndex: Record<string, string> = fromPairs(files.map((file) => [file, path.join(dir, file)]))
if (opts.manifest) {
opts.manifest.resolve(await loadJsonFile(path.join(dir, 'package.json')))
}
return {
local: true as const,
filesIndex,
packageImportMethod: 'hardlink' as const,
}
}

View File

@@ -0,0 +1,23 @@
/// <reference path="../../../typings/index.d.ts"/>
import path from 'path'
import createFetcher from '@pnpm/directory-fetcher'
test('fetch', async () => {
const fetcher = createFetcher()
// eslint-disable-next-line
const fetchResult = await fetcher.directory({} as any, { directory: path.join(__dirname, '..'), type: 'directory' }, {})
expect(fetchResult.local).toBe(true)
expect(fetchResult.packageImportMethod).toBe('hardlink')
expect(fetchResult.filesIndex['package.json']).toBe(path.join(__dirname, '../package.json'))
// Only those files are included which would get published
expect(Object.keys(fetchResult.filesIndex).sort()).toStrictEqual([
'README.md',
'lib/index.d.ts',
'lib/index.js',
'lib/index.js.map',
'package.json',
])
})

View File

@@ -0,0 +1,19 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../typings/**/*.d.ts"
],
"references": [
{
"path": "../fetcher-base"
},
{
"path": "../resolver-base"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../typings/**/*.d.ts"
]
}

View File

@@ -9,11 +9,17 @@ export interface PackageFileInfo {
size: number
}
export interface PackageFilesResponse {
export type PackageFilesResponse = {
fromStore: boolean
filesIndex: Record<string, PackageFileInfo>
packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'clone'
sideEffects?: Record<string, Record<string, PackageFileInfo>>
}
} & ({
local: true
filesIndex: Record<string, string>
} | {
local?: false
filesIndex: Record<string, PackageFileInfo>
})
export type ImportPackageFunction = (
to: string,
@@ -49,8 +55,12 @@ export type FetchFunction = (
opts: FetchOptions
) => Promise<FetchResult>
export interface FetchResult {
export type FetchResult = {
local?: false
filesIndex: FilesIndex
} | {
local: true
filesIndex: Record<string, string>
}
export interface FileWriteResult {

View File

@@ -34,6 +34,7 @@ import {
} from '@pnpm/lockfile-file'
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
import {
extendProjectsWithTargetDirs,
nameVerFromPkgSnapshot,
packageIdFromSnapshot,
pkgSnapshotToResolution,
@@ -165,6 +166,7 @@ export default async (opts: HeadlessOptions) => {
scriptShell: opts.scriptShell,
shellEmulator: opts.shellEmulator,
stdio: opts.ownLifecycleHooksStdio ?? 'inherit',
storeController: opts.storeController,
unsafePerm: opts.unsafePerm || false,
}
@@ -431,9 +433,13 @@ export default async (opts: HeadlessOptions) => {
await opts.storeController.close()
if (!opts.ignoreScripts && !opts.ignorePackageManifest) {
const projectsToBeBuilt = extendProjectsWithTargetDirs(opts.projects, wantedLockfile, {
lockfileDir: opts.lockfileDir,
virtualStoreDir,
})
await runLifecycleHooksConcurrently(
['preinstall', 'install', 'postinstall', 'prepublish', 'prepare'],
opts.projects,
['preinstall', 'install', 'postinstall', 'prepare'],
projectsToBeBuilt,
opts.childConcurrency ?? 5,
scriptsOpts
)

View File

@@ -36,8 +36,10 @@
},
"dependencies": {
"@pnpm/core-loggers": "workspace:6.0.4",
"@pnpm/directory-fetcher": "workspace:0.0.0",
"@pnpm/npm-lifecycle": "^1.0.0",
"@pnpm/read-package-json": "workspace:5.0.4",
"@pnpm/store-controller-types": "workspace:11.0.5",
"@pnpm/types": "workspace:7.4.0",
"path-exists": "^4.0.0",
"run-groups": "^3.0.1"

View File

@@ -1,3 +1,5 @@
import { fetchFromDir } from '@pnpm/directory-fetcher'
import { StoreController } from '@pnpm/store-controller-types'
import { ProjectManifest } from '@pnpm/types'
import runGroups from 'run-groups'
import runLifecycleHook, { RunLifecycleHookOptions } from './runLifecycleHook'
@@ -6,15 +8,26 @@ export type RunLifecycleHooksConcurrentlyOptions = Omit<RunLifecycleHookOptions,
| 'depPath'
| 'pkgRoot'
| 'rootModulesDir'
>
> & {
storeController: StoreController
}
export interface Importer {
buildIndex: number
manifest: ProjectManifest
rootDir: string
modulesDir: string
stages?: string[]
targetDirs?: string[]
}
export default async function runLifecycleHooksConcurrently (
stages: string[],
importers: Array<{ buildIndex: number, manifest: ProjectManifest, rootDir: string, modulesDir: string }>,
importers: Importer[],
childConcurrency: number,
opts: RunLifecycleHooksConcurrentlyOptions
) {
const importersByBuildIndex = new Map<number, Array<{ rootDir: string, manifest: ProjectManifest, modulesDir: string }>>()
const importersByBuildIndex = new Map<number, Importer[]>()
for (const importer of importers) {
if (!importersByBuildIndex.has(importer.buildIndex)) {
importersByBuildIndex.set(importer.buildIndex, [importer])
@@ -24,8 +37,8 @@ export default async function runLifecycleHooksConcurrently (
}
const sortedBuildIndexes = Array.from(importersByBuildIndex.keys()).sort()
const groups = sortedBuildIndexes.map((buildIndex) => {
const importers = importersByBuildIndex.get(buildIndex) as Array<{ rootDir: string, manifest: ProjectManifest, modulesDir: string }>
return importers.map(({ manifest, modulesDir, rootDir }) =>
const importers = importersByBuildIndex.get(buildIndex)!
return importers.map(({ manifest, modulesDir, rootDir, stages: importerStages, targetDirs }) =>
async () => {
const runLifecycleHookOpts = {
...opts,
@@ -33,10 +46,21 @@ export default async function runLifecycleHooksConcurrently (
pkgRoot: rootDir,
rootModulesDir: modulesDir,
}
for (const stage of stages) {
for (const stage of (importerStages ?? stages)) {
if ((manifest.scripts == null) || !manifest.scripts[stage]) continue
await runLifecycleHook(stage, manifest, runLifecycleHookOpts)
}
if (targetDirs == null) return
const filesResponse = await fetchFromDir(rootDir, {})
await Promise.all(
targetDirs.map((targetDir) => opts.storeController.importPackage(targetDir, {
filesResponse: {
fromStore: false,
...filesResponse,
},
force: false,
}))
)
}
)
})

View File

@@ -12,9 +12,15 @@
{
"path": "../core-loggers"
},
{
"path": "../directory-fetcher"
},
{
"path": "../read-package-json"
},
{
"path": "../store-controller-types"
},
{
"path": "../types"
}

View File

@@ -10,13 +10,15 @@ import {
} from '@pnpm/resolver-base'
import { DependencyManifest } from '@pnpm/types'
import ssri from 'ssri'
import parsePref from './parsePref'
import parsePref, { WantedLocalDependency } from './parsePref'
export { WantedLocalDependency }
/**
* Resolves a package hosted on the local filesystem
*/
export default async function resolveLocal (
wantedDependency: {pref: string},
wantedDependency: WantedLocalDependency,
opts: {
lockfileDir?: string
projectDir: string
@@ -34,7 +36,7 @@ export default async function resolveLocal (
)
) | null
> {
const spec = parsePref(wantedDependency.pref, opts.projectDir, opts.lockfileDir ?? opts.projectDir)
const spec = parsePref(wantedDependency, opts.projectDir, opts.lockfileDir ?? opts.projectDir)
if (spec == null) return null
if (spec.type === 'file') {
return {

View File

@@ -17,29 +17,34 @@ export interface LocalPackageSpec {
normalizedPref: string
}
export interface WantedLocalDependency {
pref: string
injected?: boolean
}
export default function parsePref (
pref: string,
wd: WantedLocalDependency,
projectDir: string,
lockfileDir: string
): LocalPackageSpec | null {
if (pref.startsWith('link:') || pref.startsWith('workspace:')) {
return fromLocal(pref, projectDir, lockfileDir, 'directory')
if (wd.pref.startsWith('link:') || wd.pref.startsWith('workspace:')) {
return fromLocal(wd, projectDir, lockfileDir, 'directory')
}
if (pref.endsWith('.tgz') ||
pref.endsWith('.tar.gz') ||
pref.endsWith('.tar') ||
pref.includes(path.sep) ||
pref.startsWith('file:') ||
isFilespec.test(pref)
if (wd.pref.endsWith('.tgz') ||
wd.pref.endsWith('.tar.gz') ||
wd.pref.endsWith('.tar') ||
wd.pref.includes(path.sep) ||
wd.pref.startsWith('file:') ||
isFilespec.test(wd.pref)
) {
const type = isFilename.test(pref) ? 'file' : 'directory'
return fromLocal(pref, projectDir, lockfileDir, type)
const type = isFilename.test(wd.pref) ? 'file' : 'directory'
return fromLocal(wd, projectDir, lockfileDir, type)
}
if (pref.startsWith('path:')) {
if (wd.pref.startsWith('path:')) {
const err = new PnpmError('PATH_IS_UNSUPPORTED_PROTOCOL', 'Local dependencies via `path:` protocol are not supported. ' +
'Use the `link:` protocol for folder dependencies and `file:` for local tarballs')
/* eslint-disable @typescript-eslint/dot-notation */
err['pref'] = pref
err['pref'] = wd.pref
err['protocol'] = 'path:'
/* eslint-enable @typescript-eslint/dot-notation */
throw err
@@ -48,7 +53,7 @@ export default function parsePref (
}
function fromLocal (
pref: string,
{ pref, injected }: WantedLocalDependency,
projectDir: string,
lockfileDir: string,
type: 'file' | 'directory'
@@ -57,7 +62,7 @@ function fromLocal (
.replace(/^(file|link|workspace):[/]*([A-Za-z]:)/, '$2') // drive name paths on windows
.replace(/^(file|link|workspace):(?:[/]*([~./]))?/, '$2')
const protocol = type === 'directory' ? 'link:' : 'file:'
const protocol = type === 'directory' && !injected ? 'link:' : 'file:'
let fetchSpec!: string
let normalizedPref!: string
if (/^~[/]/.test(spec)) {
@@ -73,9 +78,9 @@ function fromLocal (
}
}
const dependencyPath = normalize(path.relative(projectDir, fetchSpec))
const id = type === 'directory' || projectDir === lockfileDir
? `${protocol}${dependencyPath}`
const dependencyPath = normalize(path.resolve(fetchSpec))
const id = !injected && (type === 'directory' || projectDir === lockfileDir)
? `${protocol}${normalize(path.relative(projectDir, fetchSpec))}`
: `${protocol}${normalize(path.relative(lockfileDir, fetchSpec))}`
return {

View File

@@ -1,13 +1,23 @@
/// <reference path="../../../typings/index.d.ts"/>
import path from 'path'
import resolveFromLocal from '@pnpm/local-resolver'
import normalize from 'normalize-path'
test('resolve directory', async () => {
const resolveResult = await resolveFromLocal({ pref: '..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect(resolveResult!.resolution['directory']).toEqual('..')
expect(resolveResult!.resolution['directory']).toEqual(normalize(path.join(__dirname, '..')))
expect(resolveResult!.resolution['type']).toEqual('directory')
})
test('resolve injected directory', async () => {
const resolveResult = await resolveFromLocal({ injected: true, pref: '..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('file:..')
expect(resolveResult!.normalizedPref).toEqual('file:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect(resolveResult!.resolution['directory']).toEqual(normalize(path.join(__dirname, '..')))
expect(resolveResult!.resolution['type']).toEqual('directory')
})
@@ -16,7 +26,7 @@ test('resolve workspace directory', async () => {
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect(resolveResult!.resolution['directory']).toEqual('..')
expect(resolveResult!.resolution['directory']).toEqual(normalize(path.join(__dirname, '..')))
expect(resolveResult!.resolution['type']).toEqual('directory')
})
@@ -25,7 +35,7 @@ test('resolve directory specified using the file: protocol', async () => {
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect(resolveResult!.resolution['directory']).toEqual('..')
expect(resolveResult!.resolution['directory']).toEqual(normalize(path.join(__dirname, '..')))
expect(resolveResult!.resolution['type']).toEqual('directory')
})
@@ -34,7 +44,7 @@ test('resolve directoty specified using the link: protocol', async () => {
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect(resolveResult!.resolution['directory']).toEqual('..')
expect(resolveResult!.resolution['directory']).toEqual(normalize(path.join(__dirname, '..')))
expect(resolveResult!.resolution['type']).toEqual('directory')
})

View File

@@ -100,6 +100,9 @@ export function normalizeLockfile (lockfile: Lockfile, forceSharedFormat: boolea
const normalizedImporter = {
specifiers: importer.specifiers ?? {},
}
if (importer.dependenciesMeta != null && !isEmpty(importer.dependenciesMeta)) {
normalizedImporter['dependenciesMeta'] = importer.dependenciesMeta
}
for (const depType of DEPENDENCIES_FIELDS) {
if (!isEmpty(importer[depType] ?? {})) {
normalizedImporter[depType] = importer[depType]

View File

@@ -27,5 +27,8 @@
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix",
"prepublishOnly": "pnpm run compile"
},
"funding": "https://opencollective.com/pnpm"
"funding": "https://opencollective.com/pnpm",
"dependencies": {
"@pnpm/types": "workspace:7.4.0"
}
}

View File

@@ -1,3 +1,5 @@
import { DependenciesMeta } from '@pnpm/types'
export interface Lockfile {
importers: Record<string, ProjectSnapshot>
lockfileVersion: number
@@ -12,6 +14,7 @@ export interface ProjectSnapshot {
dependencies?: ResolvedDependencies
optionalDependencies?: ResolvedDependencies
devDependencies?: ResolvedDependencies
dependenciesMeta?: DependenciesMeta
}
export interface PackageSnapshots {

View File

@@ -8,5 +8,9 @@
"src/**/*.ts",
"../../typings/**/*.d.ts"
],
"references": []
"references": [
{
"path": "../types"
}
]
}

View File

@@ -0,0 +1,27 @@
import path from 'path'
import { Lockfile } from '@pnpm/lockfile-types'
import { depPathToFilename } from 'dependency-path'
import fromPairs from 'ramda/src/fromPairs'
export default function extendProjectsWithTargetDirs<T> (
projects: Array<T & { id: string }>,
lockfile: Lockfile,
ctx: {
lockfileDir: string
virtualStoreDir: string
}
) {
const projectsById: Record<string, T & { targetDirs: string[], stages?: string[] }> =
fromPairs(projects.map((project) => [project.id, { ...project, targetDirs: [] as string[] }]))
Object.entries(lockfile.packages ?? {})
.forEach(([depPath, pkg]) => {
if (pkg.resolution?.['type'] !== 'directory') return
const pkgId = pkg.id ?? depPath
const importerId = pkgId.replace(/^file:/, '')
if (projectsById[importerId] == null) return
const localLocation = path.join(ctx.virtualStoreDir, depPathToFilename(depPath, ctx.lockfileDir), 'node_modules', pkg.name!)
projectsById[importerId].targetDirs.push(localLocation)
projectsById[importerId].stages = ['preinstall', 'install', 'postinstall', 'prepare', 'prepublishOnly']
})
return Object.values(projectsById)
}

View File

@@ -1,4 +1,5 @@
import { refToRelative } from 'dependency-path'
import extendProjectsWithTargetDirs from './extendProjectsWithTargetDirs'
import nameVerFromPkgSnapshot from './nameVerFromPkgSnapshot'
import packageIdFromSnapshot from './packageIdFromSnapshot'
import packageIsIndependent from './packageIsIndependent'
@@ -8,6 +9,7 @@ import satisfiesPackageManifest from './satisfiesPackageManifest'
export * from '@pnpm/lockfile-types'
export {
extendProjectsWithTargetDirs,
nameVerFromPkgSnapshot,
packageIdFromSnapshot,
packageIsIndependent,

View File

@@ -11,6 +11,7 @@ export default (lockfile: Lockfile, pkg: ProjectManifest, importerId: string) =>
if (!equals({ ...pkg.devDependencies, ...pkg.dependencies, ...pkg.optionalDependencies }, importer.specifiers)) {
return false
}
if (!equals(pkg.dependenciesMeta, importer.dependenciesMeta)) return false
for (const depField of DEPENDENCIES_FIELDS) {
const importerDeps = importer[depField] ?? {}
const pkgDeps = pkg[depField] ?? {}

View File

@@ -101,6 +101,7 @@ export type ResolveFromNpmOptions = {
alwaysTryWorkspacePackages?: boolean
defaultTag?: string
dryRun?: boolean
lockfileDir?: string
registry: string
preferredVersions?: PreferredVersions
preferWorkspacePackages?: boolean
@@ -125,6 +126,7 @@ async function resolveNpm (
if (wantedDependency.pref.startsWith('workspace:.')) return null
const resolvedFromWorkspace = tryResolveFromWorkspace(wantedDependency, {
defaultTag,
lockfileDir: opts.lockfileDir,
projectDir: opts.projectDir,
registry: opts.registry,
workspacePackages: opts.workspacePackages,
@@ -150,7 +152,11 @@ async function resolveNpm (
})
} catch (err: any) { // eslint-disable-line
if ((workspacePackages != null) && opts.projectDir) {
const resolvedFromLocal = tryResolveFromWorkspacePackages(workspacePackages, spec, opts.projectDir)
const resolvedFromLocal = tryResolveFromWorkspacePackages(workspacePackages, spec, {
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
})
if (resolvedFromLocal != null) return resolvedFromLocal
}
throw err
@@ -159,7 +165,11 @@ async function resolveNpm (
const meta = pickResult.meta
if (pickedPackage == null) {
if ((workspacePackages != null) && opts.projectDir) {
const resolvedFromLocal = tryResolveFromWorkspacePackages(workspacePackages, spec, opts.projectDir)
const resolvedFromLocal = tryResolveFromWorkspacePackages(workspacePackages, spec, {
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
})
if (resolvedFromLocal != null) return resolvedFromLocal
}
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta })
@@ -168,14 +178,22 @@ async function resolveNpm (
if (((workspacePackages?.[pickedPackage.name]) != null) && opts.projectDir) {
if (workspacePackages[pickedPackage.name][pickedPackage.version]) {
return {
...resolveFromLocalPackage(workspacePackages[pickedPackage.name][pickedPackage.version], spec.normalizedPref, opts.projectDir),
...resolveFromLocalPackage(workspacePackages[pickedPackage.name][pickedPackage.version], spec.normalizedPref, {
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
}),
latest: meta['dist-tags'].latest,
}
}
const localVersion = pickMatchingLocalVersionOrNull(workspacePackages[pickedPackage.name], spec)
if (localVersion && (semver.gt(localVersion, pickedPackage.version) || opts.preferWorkspacePackages)) {
return {
...resolveFromLocalPackage(workspacePackages[pickedPackage.name][localVersion], spec.normalizedPref, opts.projectDir),
...resolveFromLocalPackage(workspacePackages[pickedPackage.name][localVersion], spec.normalizedPref, {
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: wantedDependency.injected,
}),
latest: meta['dist-tags'].latest,
}
}
@@ -201,6 +219,7 @@ function tryResolveFromWorkspace (
wantedDependency: WantedDependency,
opts: {
defaultTag: string
lockfileDir?: string
projectDir?: string
registry: string
workspacePackages?: WorkspacePackages
@@ -219,7 +238,11 @@ function tryResolveFromWorkspace (
if (!opts.projectDir) {
throw new Error('Cannot resolve package from workspace because opts.projectDir is not defined')
}
const resolvedFromLocal = tryResolveFromWorkspacePackages(opts.workspacePackages, spec, opts.projectDir)
const resolvedFromLocal = tryResolveFromWorkspacePackages(opts.workspacePackages, spec, {
projectDir: opts.projectDir,
hardLinkLocalPackages: wantedDependency.injected,
lockfileDir: opts.lockfileDir,
})
if (resolvedFromLocal == null) {
throw new PnpmError(
'NO_MATCHING_VERSION_INSIDE_WORKSPACE',
@@ -232,12 +255,16 @@ function tryResolveFromWorkspace (
function tryResolveFromWorkspacePackages (
workspacePackages: WorkspacePackages,
spec: RegistryPackageSpec,
projectDir: string
opts: {
hardLinkLocalPackages?: boolean
projectDir: string
lockfileDir?: string
}
) {
if (!workspacePackages[spec.name]) return null
const localVersion = pickMatchingLocalVersionOrNull(workspacePackages[spec.name], spec)
if (!localVersion) return null
return resolveFromLocalPackage(workspacePackages[spec.name][localVersion], spec.normalizedPref, projectDir)
return resolveFromLocalPackage(workspacePackages[spec.name][localVersion], spec.normalizedPref, opts)
}
function pickMatchingLocalVersionOrNull (
@@ -268,10 +295,16 @@ function resolveFromLocalPackage (
manifest: DependencyManifest
},
normalizedPref: string | undefined,
projectDir: string
opts: {
hardLinkLocalPackages?: boolean
projectDir: string
lockfileDir?: string
}
) {
return {
id: `link:${normalize(path.relative(projectDir, localPackage.dir))}`,
id: opts.hardLinkLocalPackages
? `file:${normalize(path.relative(opts.lockfileDir!, localPackage.dir))}`
: `link:${normalize(path.relative(opts.projectDir, localPackage.dir))}`,
manifest: localPackage.manifest,
normalizedPref,
resolution: {

View File

@@ -944,6 +944,44 @@ test('resolve from local directory when it matches the latest version of the pac
expect(resolveResult!.manifest!.version).toBe('1.0.0')
})
test('resolve injected dependency from local directory when it matches the latest version of the package', async () => {
nock(registry)
.get('/is-positive')
.reply(200, isPositiveMeta)
const cacheDir = tempy.directory()
const resolve = createResolveFromNpm({
cacheDir,
})
const resolveResult = await resolve({ alias: 'is-positive', injected: true, pref: '1.0.0' }, {
projectDir: '/home/istvan/src',
lockfileDir: '/home/istvan/src',
registry,
workspacePackages: {
'is-positive': {
'1.0.0': {
dir: '/home/istvan/src/is-positive',
manifest: {
name: 'is-positive',
version: '1.0.0',
},
},
},
},
})
expect(resolveResult!.resolvedVia).toBe('local-filesystem')
expect(resolveResult!.id).toBe('file:is-positive')
expect(resolveResult!.latest!.split('.').length).toBe(3)
expect(resolveResult!.resolution).toStrictEqual({
directory: '/home/istvan/src/is-positive',
type: 'directory',
})
expect(resolveResult!.manifest).toBeTruthy()
expect(resolveResult!.manifest!.name).toBe('is-positive')
expect(resolveResult!.manifest!.version).toBe('1.0.0')
})
test('do not resolve from local directory when alwaysTryWorkspacePackages is false', async () => {
nock(registry)
.get('/is-positive')

View File

@@ -184,7 +184,7 @@ async function resolveAndFetch (
const id = pkgId as string
if (resolution.type === 'directory') {
if (resolution.type === 'directory' && !wantedDependency.injected) {
if (manifest == null) {
throw new Error(`Couldn't read package.json of local dependency ${wantedDependency.alias ? wantedDependency.alias + '@' : ''}${wantedDependency.pref ?? ''}`)
}
@@ -368,10 +368,13 @@ function fetchToStore (
if (opts.fetchRawManifest && (result.bundledManifest == null)) {
result.bundledManifest = removeKeyOnFail(
result.files.then(async ({ filesIndex }) => {
const { integrity, mode } = filesIndex['package.json']
const manifestPath = ctx.getFilePathByModeInCafs(integrity, mode)
return readBundledManifest(manifestPath)
result.files.then(async (filesResult) => {
if (!filesResult.local) {
const { integrity, mode } = filesResult.filesIndex['package.json']
const manifestPath = ctx.getFilePathByModeInCafs(integrity, mode)
return readBundledManifest(manifestPath)
}
return readBundledManifest(filesResult.filesIndex['package.json'])
})
)
}
@@ -400,13 +403,15 @@ function fetchToStore (
) {
try {
const isLocalTarballDep = opts.pkg.id.startsWith('file:')
const isLocalPkg = opts.pkg.resolution.type === 'directory'
if (
!opts.force &&
(
!isLocalTarballDep ||
await tarballIsUpToDate(opts.pkg.resolution as any, target, opts.lockfileDir) // eslint-disable-line
)
) &&
!isLocalPkg
) {
let pkgFilesIndex
try {
@@ -494,42 +499,52 @@ Actual package in the store by the given integrity: ${pkgFilesIndex.name}@${pkgF
}
), { priority })
const filesIndex = fetchedPackage.filesIndex
// Ideally, files wouldn't care about when integrity is calculated.
// However, we can only rename the temp folder once we know the package name.
// And we cannot rename the temp folder till we're calculating integrities.
const integrity: Record<string, PackageFileInfo> = {}
await Promise.all(
Object.keys(filesIndex)
.map(async (filename) => {
const {
checkedAt,
integrity: fileIntegrity,
} = await filesIndex[filename].writeResult
integrity[filename] = {
checkedAt,
integrity: fileIntegrity.toString(), // TODO: use the raw Integrity object
mode: filesIndex[filename].mode,
size: filesIndex[filename].size,
}
})
)
await writeJsonFile(filesIndexFile, {
name: opts.pkg.name,
version: opts.pkg.version,
files: integrity,
})
let filesResult!: PackageFilesResponse
if (!fetchedPackage.local) {
const filesIndex = fetchedPackage.filesIndex
// Ideally, files wouldn't care about when integrity is calculated.
// However, we can only rename the temp folder once we know the package name.
// And we cannot rename the temp folder till we're calculating integrities.
const integrity: Record<string, PackageFileInfo> = {}
await Promise.all(
Object.keys(filesIndex)
.map(async (filename) => {
const {
checkedAt,
integrity: fileIntegrity,
} = await filesIndex[filename].writeResult
integrity[filename] = {
checkedAt,
integrity: fileIntegrity.toString(), // TODO: use the raw Integrity object
mode: filesIndex[filename].mode,
size: filesIndex[filename].size,
}
})
)
await writeJsonFile(filesIndexFile, {
name: opts.pkg.name,
version: opts.pkg.version,
files: integrity,
})
filesResult = {
fromStore: false,
filesIndex: integrity,
}
} else {
filesResult = {
local: true,
fromStore: false,
filesIndex: fetchedPackage.filesIndex,
packageImportMethod: fetchedPackage['packageImportMethod'],
}
}
if (isLocalTarballDep && opts.pkg.resolution['integrity']) { // eslint-disable-line @typescript-eslint/dot-notation
await fs.mkdir(target, { recursive: true })
await gfs.writeFile(path.join(target, TARBALL_INTEGRITY_FILENAME), opts.pkg.resolution['integrity'], 'utf8') // eslint-disable-line @typescript-eslint/dot-notation
}
files.resolve({
filesIndex: integrity,
fromStore: false,
})
files.resolve(filesResult)
finishing.resolve(undefined)
} catch (err: any) { // eslint-disable-line
files.reject(err)

View File

@@ -496,7 +496,7 @@ test('fetchPackageToStore() concurrency check', async () => {
const fetchResult = fetchResults[0]
const files = await fetchResult.files()
ino1 = statSync(getFilePathInCafs(cafsDir, files.filesIndex['package.json'].integrity, 'nonexec')).ino
ino1 = statSync(getFilePathInCafs(cafsDir, files.filesIndex['package.json']['integrity'], 'nonexec')).ino
expect(Object.keys(files.filesIndex).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort())
expect(files.fromStore).toBeFalsy()
@@ -508,7 +508,7 @@ test('fetchPackageToStore() concurrency check', async () => {
const fetchResult = fetchResults[1]
const files = await fetchResult.files()
ino2 = statSync(getFilePathInCafs(cafsDir, files.filesIndex['package.json'].integrity, 'nonexec')).ino
ino2 = statSync(getFilePathInCafs(cafsDir, files.filesIndex['package.json']['integrity'], 'nonexec')).ino
expect(Object.keys(files.filesIndex).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort())
expect(files.fromStore).toBeFalsy()
@@ -734,7 +734,7 @@ test('refetch package to store if it has been modified', async () => {
})
const { filesIndex } = await fetchResult.files()
indexJsFile = getFilePathInCafs(cafsDir, filesIndex['index.js'].integrity, 'nonexec')
indexJsFile = getFilePathInCafs(cafsDir, filesIndex['index.js']['integrity'], 'nonexec')
}
await delay(200)

View File

@@ -25,6 +25,7 @@
"@zkochan/rimraf": "^2.1.1",
"load-json-file": "^6.2.0",
"make-empty-dir": "^2.0.0",
"mem": "^8.0.0",
"p-limit": "^3.1.0",
"path-exists": "^4.0.0",
"path-temp": "^2.0.0",

View File

@@ -1,10 +1,10 @@
import { promises as fs } from 'fs'
import path from 'path'
import createCafs, {
getFilePathByModeInCafs as _getFilePathByModeInCafs,
getFilePathByModeInCafs,
PackageFilesIndex,
} from '@pnpm/cafs'
import { FetchFunction } from '@pnpm/fetcher-base'
import { FetchFunction, PackageFilesResponse } from '@pnpm/fetcher-base'
import createPackageRequester from '@pnpm/package-requester'
import { ResolveFunction } from '@pnpm/resolver-base'
import {
@@ -13,6 +13,7 @@ import {
StoreController,
} from '@pnpm/store-controller-types'
import loadJsonFile from 'load-json-file'
import memoize from 'mem'
import pathTemp from 'path-temp'
import writeJsonFile from 'write-json-file'
import createImportPackage from './createImportPackage'
@@ -24,27 +25,44 @@ function createPackageImporter (
cafsDir: string
}
): ImportPackageFunction {
const impPkg = createImportPackage(opts.packageImportMethod)
const getFilePathByModeInCafs = _getFilePathByModeInCafs.bind(null, opts.cafsDir)
const cachedImporterCreator = memoize(createImportPackage)
const packageImportMethod = opts.packageImportMethod
const gfm = getFlatMap.bind(null, opts.cafsDir)
return async (to, opts) => {
const filesMap = {} as Record<string, string>
let isBuilt!: boolean
let filesIndex!: Record<string, PackageFileInfo>
if (opts.targetEngine && ((opts.filesResponse.sideEffects?.[opts.targetEngine]) != null)) {
filesIndex = opts.filesResponse.sideEffects?.[opts.targetEngine]
isBuilt = true
} else {
filesIndex = opts.filesResponse.filesIndex
isBuilt = false
}
for (const [fileName, fileMeta] of Object.entries(filesIndex)) {
filesMap[fileName] = getFilePathByModeInCafs(fileMeta.integrity, fileMeta.mode)
}
const { filesMap, isBuilt } = gfm(opts.filesResponse, opts.targetEngine)
const impPkg = cachedImporterCreator(opts.filesResponse.packageImportMethod ?? packageImportMethod)
const importMethod = await impPkg(to, { filesMap, fromStore: opts.filesResponse.fromStore, force: opts.force })
return { importMethod, isBuilt }
}
}
function getFlatMap (
cafsDir: string,
filesResponse: PackageFilesResponse,
targetEngine?: string
): { filesMap: Record<string, string>, isBuilt: boolean } {
if (filesResponse.local) {
return {
filesMap: filesResponse.filesIndex,
isBuilt: false,
}
}
let isBuilt!: boolean
let filesIndex!: Record<string, PackageFileInfo>
if (targetEngine && ((filesResponse.sideEffects?.[targetEngine]) != null)) {
filesIndex = filesResponse.sideEffects?.[targetEngine]
isBuilt = true
} else {
filesIndex = filesResponse.filesIndex
isBuilt = false
}
const filesMap = {}
for (const [fileName, fileMeta] of Object.entries(filesIndex)) {
filesMap[fileName] = getFilePathByModeInCafs(cafsDir, fileMeta.integrity, fileMeta.mode)
}
return { filesMap, isBuilt }
}
export function createCafsStore (
storeDir: string,
opts?: {

View File

@@ -34,6 +34,7 @@
"@pnpm/config": "workspace:13.4.0",
"@pnpm/error": "workspace:2.0.0",
"@pnpm/fetch": "workspace:4.1.3",
"@pnpm/fetcher-base": "workspace:11.0.3",
"@pnpm/package-store": "workspace:12.0.15",
"@pnpm/store-path": "^5.0.0",
"@pnpm/tarball-fetcher": "workspace:9.3.7",

View File

@@ -2,6 +2,7 @@ import fs from 'fs'
import path from 'path'
import { Config } from '@pnpm/config'
import fetch, { createFetchFromRegistry, FetchFromRegistry } from '@pnpm/fetch'
import { FilesIndex } from '@pnpm/fetcher-base'
import { createCafsStore } from '@pnpm/package-store'
import storePath from '@pnpm/store-path'
import createFetcher, { waitForFilesIndex } from '@pnpm/tarball-fetcher'
@@ -87,7 +88,7 @@ async function installNode (wantedNodeVersion: string, versionDir: string, opts:
})
await cafs.importPackage(versionDir, {
filesResponse: {
filesIndex: await waitForFilesIndex(filesIndex),
filesIndex: await waitForFilesIndex(filesIndex as FilesIndex),
fromStore: false,
},
force: true,

View File

@@ -24,6 +24,9 @@
{
"path": "../fetch"
},
{
"path": "../fetcher-base"
},
{
"path": "../package-store"
},

View File

@@ -6,6 +6,7 @@ import { Registries } from '@pnpm/types'
import loadJsonFile from 'load-json-file'
export interface StrictRebuildOptions {
cacheDir: string
childConcurrency: number
extraBinPaths: string[]
lockfileDir: string

View File

@@ -19,6 +19,7 @@ import {
import lockfileWalker, { LockfileWalkerStep } from '@pnpm/lockfile-walker'
import logger, { streamParser } from '@pnpm/logger'
import { write as writeModulesYaml } from '@pnpm/modules-yaml'
import { createOrConnectStoreController } from '@pnpm/store-connection-manager'
import { ProjectManifest } from '@pnpm/types'
import * as dp from 'dependency-path'
import runGroups from 'run-groups'
@@ -150,11 +151,13 @@ export async function rebuild (
ctx.pendingBuilds = ctx.pendingBuilds.filter((depPath) => !pkgsThatWereRebuilt.has(depPath))
const store = await createOrConnectStoreController(opts)
const scriptsOpts = {
extraBinPaths: ctx.extraBinPaths,
rawConfig: opts.rawConfig,
scriptShell: opts.scriptShell,
shellEmulator: opts.shellEmulator,
storeController: store.ctrl,
unsafePerm: opts.unsafePerm || false,
}
await runLifecycleHooksConcurrently(

View File

@@ -5,6 +5,7 @@ export interface WantedDependency {
pref: string // package reference
dev: boolean
optional: boolean
injected?: boolean
}
export default function getNonDevWantedDependencies (pkg: DependencyManifest) {

View File

@@ -153,12 +153,15 @@ export default async function (
virtualStoreDir: opts.virtualStoreDir,
})
for (const { id } of projectsToLink) {
for (const { id, manifest } of projectsToLink) {
for (const [alias, depPath] of Object.entries(dependenciesByProjectId[id])) {
const depNode = dependenciesGraph[depPath]
if (depNode.isPure) continue
const projectSnapshot = opts.wantedLockfile.importers[id]
if (manifest.dependenciesMeta != null) {
projectSnapshot.dependenciesMeta = manifest.dependenciesMeta
}
const ref = depPathToRef(depPath, {
alias,
realName: depNode.name,

View File

@@ -67,7 +67,7 @@ function toLockfileDependency (
}
): PackageSnapshot {
const lockfileResolution = toLockfileResolution(
{ name: pkg.name, version: pkg.version },
{ id: pkg.id, name: pkg.name, version: pkg.version },
opts.depPath,
pkg.resolution,
opts.registry
@@ -210,6 +210,7 @@ function updateResolvedDeps (
function toLockfileResolution (
pkg: {
id: string
name: string
version: string
},
@@ -219,6 +220,12 @@ function toLockfileResolution (
): LockfileResolution {
/* eslint-disable @typescript-eslint/dot-notation */
if (dp.isAbsolute(depPath) || resolution.type !== undefined || !resolution['integrity']) {
if (resolution.type === 'directory') {
return {
type: 'directory',
directory: pkg.id.replace(/^file:/, ''),
}
}
return resolution as LockfileResolution
}
const base = registry !== resolution['registry'] ? { registry: resolution['registry'] } : {}

View File

@@ -64,11 +64,13 @@ export interface ResolveOptions {
}
export type WantedDependency = {
injected?: boolean
} & ({
alias?: string
pref: string
} | {
alias: string
pref?: string
}
})
export type ResolveFunction = (wantedDependency: WantedDependency, opts: ResolveOptions) => Promise<ResolveResult>

View File

@@ -48,6 +48,7 @@ export interface PeerDependenciesMeta {
export interface DependenciesMeta {
[dependencyName: string]: {
injected?: boolean
node?: string
}
}

30
pnpm-lock.yaml generated
View File

@@ -285,6 +285,7 @@ importers:
specifiers:
'@pnpm/client': 'link:'
'@pnpm/default-resolver': workspace:13.0.9
'@pnpm/directory-fetcher': workspace:0.0.0
'@pnpm/fetch': workspace:4.1.3
'@pnpm/fetching-types': workspace:2.2.1
'@pnpm/git-fetcher': workspace:4.1.6
@@ -295,6 +296,7 @@ importers:
mem: ^8.0.0
dependencies:
'@pnpm/default-resolver': link:../default-resolver
'@pnpm/directory-fetcher': link:../directory-fetcher
'@pnpm/fetch': link:../fetch
'@pnpm/fetching-types': link:../fetching-types
'@pnpm/git-fetcher': link:../git-fetcher
@@ -673,6 +675,23 @@ importers:
'@types/semver': 7.3.9
dependency-path: 'link:'
packages/directory-fetcher:
specifiers:
'@pnpm/directory-fetcher': 'link:'
'@pnpm/fetcher-base': workspace:11.0.3
'@pnpm/resolver-base': workspace:8.0.4
load-json-file: ^6.2.0
npm-packlist: ^2.2.2
ramda: ^0.27.1
dependencies:
'@pnpm/fetcher-base': link:../fetcher-base
'@pnpm/resolver-base': link:../resolver-base
load-json-file: 6.2.0
npm-packlist: 2.2.2
ramda: 0.27.1
devDependencies:
'@pnpm/directory-fetcher': 'link:'
packages/error:
specifiers:
'@pnpm/error': 'link:'
@@ -1142,10 +1161,12 @@ importers:
packages/lifecycle:
specifiers:
'@pnpm/core-loggers': workspace:6.0.4
'@pnpm/directory-fetcher': workspace:0.0.0
'@pnpm/lifecycle': 'link:'
'@pnpm/logger': ^4.0.0
'@pnpm/npm-lifecycle': ^1.0.0
'@pnpm/read-package-json': workspace:5.0.4
'@pnpm/store-controller-types': workspace:11.0.5
'@pnpm/types': workspace:7.4.0
'@types/rimraf': ^3.0.0
json-append: 1.1.1
@@ -1154,8 +1175,10 @@ importers:
run-groups: ^3.0.1
dependencies:
'@pnpm/core-loggers': link:../core-loggers
'@pnpm/directory-fetcher': link:../directory-fetcher
'@pnpm/npm-lifecycle': 1.0.0
'@pnpm/read-package-json': link:../read-package-json
'@pnpm/store-controller-types': link:../store-controller-types
'@pnpm/types': link:../types
path-exists: 4.0.0
run-groups: 3.0.1
@@ -1372,6 +1395,9 @@ importers:
packages/lockfile-types:
specifiers:
'@pnpm/lockfile-types': 'link:'
'@pnpm/types': workspace:7.4.0
dependencies:
'@pnpm/types': link:../types
devDependencies:
'@pnpm/lockfile-types': 'link:'
@@ -1846,6 +1872,7 @@ importers:
'@zkochan/rimraf': ^2.1.1
load-json-file: ^6.2.0
make-empty-dir: ^2.0.0
mem: ^8.0.0
p-limit: ^3.1.0
path-exists: ^4.0.0
path-temp: ^2.0.0
@@ -1866,6 +1893,7 @@ importers:
'@zkochan/rimraf': 2.1.1
load-json-file: 6.2.0
make-empty-dir: 2.0.0
mem: 8.1.1
p-limit: 3.1.0
path-exists: 4.0.0
path-temp: 2.0.0
@@ -1988,6 +2016,7 @@ importers:
'@pnpm/config': workspace:13.4.0
'@pnpm/error': workspace:2.0.0
'@pnpm/fetch': workspace:4.1.3
'@pnpm/fetcher-base': workspace:11.0.3
'@pnpm/package-store': workspace:12.0.15
'@pnpm/plugin-commands-env': 'link:'
'@pnpm/prepare': workspace:0.0.26
@@ -2010,6 +2039,7 @@ importers:
'@pnpm/config': link:../config
'@pnpm/error': link:../error
'@pnpm/fetch': link:../fetch
'@pnpm/fetcher-base': link:../fetcher-base
'@pnpm/package-store': link:../package-store
'@pnpm/store-path': 5.0.0
'@pnpm/tarball-fetcher': link:../tarball-fetcher