Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(deploy): apply publishConfig to all packages during deploy #7647

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/flat-kids-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-deploy": minor
"@pnpm/directory-fetcher": minor
---

Apply `publishConfig` for workspace packages on directory fetch. Enables a publishable ("exportable") `package.json` on deployment [#6693](https://github.com/pnpm/pnpm/issues/6693).
5 changes: 4 additions & 1 deletion fetching/directory-fetcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@
"@pnpm/logger": "^5.0.0"
},
"dependencies": {
"@pnpm/exportable-manifest": "workspace:*",
"@pnpm/fetcher-base": "workspace:*",
"@pnpm/fs.packlist": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/types": "workspace:*"
"@pnpm/types": "workspace:*",
"@pnpm/write-project-manifest": "workspace:*",
"fast-deep-equal": "^3.1.3"
},
"devDependencies": {
"@pnpm/directory-fetcher": "workspace:*",
Expand Down
46 changes: 29 additions & 17 deletions fetching/directory-fetcher/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { promises as fs, type Stats } from 'fs'
import path from 'path'
import type { DirectoryFetcher, DirectoryFetcherOptions } from '@pnpm/fetcher-base'
import { createExportableManifest } from '@pnpm/exportable-manifest'
import {
type DirectoryFetcher,
type DirectoryFetcherOptions,
type DirectoryFetcherResult,
} from '@pnpm/fetcher-base'
import { logger } from '@pnpm/logger'
import { packlist } from '@pnpm/fs.packlist'
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
import { type DependencyManifest } from '@pnpm/types'
import { writeProjectManifest } from '@pnpm/write-project-manifest'
import equal from 'fast-deep-equal'

const directoryFetcherLogger = logger('directory-fetcher')

Expand Down Expand Up @@ -46,15 +53,9 @@ async function fetchAllFilesFromDir (
readFileStat: ReadFileStat,
dir: string,
opts: FetchFromDirOpts
) {
): Promise<DirectoryFetcherResult> {
const filesIndex = await _fetchAllFilesFromDir(readFileStat, dir)
let manifest: DependencyManifest | undefined
if (opts.readManifest) {
// In a regular pnpm workspace it will probably never happen that a dependency has no package.json file.
// Safe read was added to support the Bit workspace in which the components have no package.json files.
// Related PR in Bit: https://github.com/teambit/bit/pull/5251
manifest = await safeReadProjectManifestOnly(dir) as DependencyManifest ?? undefined
}
const manifest = await safeReadProjectManifestAndMakeExportable(dir, filesIndex)
return {
local: true as const,
filesIndex,
Expand Down Expand Up @@ -127,20 +128,31 @@ async function fileStat (filePath: string): Promise<{ filePath: string, stat: St
async function fetchPackageFilesFromDir (
dir: string,
opts: FetchFromDirOpts
) {
): Promise<DirectoryFetcherResult> {
const files = await packlist(dir)
const filesIndex: Record<string, string> = Object.fromEntries(files.map((file) => [file, path.join(dir, file)]))
let manifest: DependencyManifest | undefined
if (opts.readManifest) {
// In a regular pnpm workspace it will probably never happen that a dependency has no package.json file.
// Safe read was added to support the Bit workspace in which the components have no package.json files.
// Related PR in Bit: https://github.com/teambit/bit/pull/5251
manifest = await safeReadProjectManifestOnly(dir) as DependencyManifest ?? undefined
}
const manifest = await safeReadProjectManifestAndMakeExportable(dir, filesIndex)
return {
local: true as const,
filesIndex,
packageImportMethod: 'hardlink' as const,
manifest,
}
}

async function safeReadProjectManifestAndMakeExportable (
dir: string,
filesIndex: Record<string, string>
): Promise<DependencyManifest | undefined> {
const manifest = await safeReadProjectManifestOnly(dir) as (DependencyManifest | null)
// In a regular pnpm workspace it will probably never happen that a dependency has no package.json file.
// Safe read was added to support the Bit workspace in which the components have no package.json files.
// Related PR in Bit: https://github.com/teambit/bit/pull/5251
if (!manifest) return undefined
const exportableManifest = await createExportableManifest(dir, manifest)
if (equal(manifest, exportableManifest)) return manifest
const manifestPathOverride = path.join(dir, 'node_modules/.pnpm/package.json')
await writeProjectManifest(manifestPathOverride, exportableManifest)
filesIndex['package.json'] = manifestPathOverride
return manifest
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "exportable",
"version": "1.0.0",
"main": "src/index.ts",
"publishConfig": {
"main": "dist/index.js"
}
}
21 changes: 21 additions & 0 deletions fetching/directory-fetcher/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,24 @@ describe('fetch resolves symlinked files to their real locations', () => {
expect(fetchResult.filesIndex['src/index.js']).toBe(path.resolve('src/index.js'))
})
})

test('fetch should return exportable manifest', async () => {
process.chdir(f.find('exportable-manifest'))
const fetcher = createDirectoryFetcher()

// eslint-disable-next-line
const fetchResult = await fetcher.directory({} as any, {
directory: '.',
type: 'directory',
}, {
lockfileDir: process.cwd(),
})

expect(fetchResult.filesIndex['package.json']).not.toBe(path.resolve('package.json'))

expect(JSON.parse(fs.readFileSync(fetchResult.filesIndex['package.json'], 'utf8'))).toStrictEqual({
name: 'exportable',
version: '1.0.0',
main: 'dist/index.js',
})
})
6 changes: 6 additions & 0 deletions fetching/directory-fetcher/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@
{
"path": "../../packages/types"
},
{
"path": "../../pkg-manifest/exportable-manifest"
},
{
"path": "../../pkg-manifest/read-project-manifest"
},
{
"path": "../../pkg-manifest/write-project-manifest"
},
{
"path": "../../resolving/resolver-base"
},
Expand Down
107 changes: 106 additions & 1 deletion pkg-manager/plugin-commands-installation/test/install.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import fs from 'fs'
import delay from 'delay'
import path from 'path'
import { readProjects } from '@pnpm/filter-workspace-packages'
import { add, install } from '@pnpm/plugin-commands-installation'
import { prepareEmpty } from '@pnpm/prepare'
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
import { sync as rimraf } from '@zkochan/rimraf'
import { DEFAULT_OPTS } from './utils'

Expand Down Expand Up @@ -52,3 +53,107 @@ test('install with no store integrity validation', async () => {

expect(fs.readFileSync('node_modules/is-positive/readme.md', 'utf8')).toBe('modified')
})

describe('install injected dependency with publishConfig in manifest', () => {
test.each([true, false])('with shared lockfile: %s', async withLockfile => {
preparePackages([
{
location: '.',
package: {
name: 'root',
version: '1.0.0',
},
},
{
name: 'project-1',
version: '1.0.0',
dependencies: {
'project-2': 'workspace:*',
},
devDependencies: {
'project-3': 'workspace:*',
},
dependenciesMeta: {
'project-2': {
injected: true,
},
},
},
{
name: 'project-2',
version: '1.0.0',
devDependencies: {
'project-3': 'workspace:*',
},
publishConfig: {
exports: {
foo: 'bar',
},
},
},
{
location: 'nested-packages/project-3',
package: {
name: 'project-3',
version: '1.0.0',
},
},
])

const { allProjects, allProjectsGraph, selectedProjectsGraph } = await readProjects(process.cwd(), [])

const lockfileDir = withLockfile ? process.cwd() : undefined

await install.handler({
...DEFAULT_OPTS,
allProjects,
allProjectsGraph,
dir: process.cwd(),
lockfileDir,
selectedProjectsGraph,
workspaceDir: process.cwd(),
})

const originalProject2 = fs.readFileSync('project-2/package.json', 'utf8')
expect(
JSON.parse(originalProject2)
).toEqual({
name: 'project-2',
version: '1.0.0',
devDependencies: {
'project-3': 'workspace:*',
},
publishConfig: {
exports: {
foo: 'bar',
},
},
})

const injectedProject2 = fs.readFileSync('project-1/node_modules/project-2/package.json', 'utf8')
expect(
JSON.parse(injectedProject2)
).toEqual({
name: 'project-2',
version: '1.0.0',
devDependencies: {
'project-3': 'workspace:*',
},
publishConfig: {
exports: {
foo: 'bar',
},
},
})

await install.handler({
...DEFAULT_OPTS,
allProjects,
allProjectsGraph,
dir: process.cwd(),
lockfileDir,
selectedProjectsGraph,
workspaceDir: process.cwd(),
})
})
})
2 changes: 1 addition & 1 deletion pkg-manifest/read-project-manifest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {

type WriteProjectManifest = (manifest: ProjectManifest, force?: boolean) => Promise<void>

export async function safeReadProjectManifestOnly (projectDir: string) {
export async function safeReadProjectManifestOnly (projectDir: string): Promise<ProjectManifest | null> {
try {
return await readProjectManifestOnly(projectDir)
} catch (err: any) { // eslint-disable-line
Expand Down