Skip to content

Commit

Permalink
feat(deploy): apply publishConfig to all packages during deploy
Browse files Browse the repository at this point in the history
When deploying packages, the package.json of the deployed package
(as well as any other locally defined dependencies)
should be treated as if it published, and mutate the package.json
according to `publishConfig` and local `workspace:` dependencies.

Issue: #6693
  • Loading branch information
JacobLey committed Aug 18, 2023
1 parent f73eeac commit b12b3e9
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 13 deletions.
9 changes: 9 additions & 0 deletions .changeset/flat-kids-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@pnpm/read-project-manifest": minor
"@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.
Issue: [#6693](https://github.com/pnpm/pnpm/issues/6693)
4 changes: 4 additions & 0 deletions fetching/directory-fetcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
"@pnpm/logger": "^5.0.0"
},
"dependencies": {
"@pnpm/exportable-manifest": "workspace:*",
"@pnpm/fetcher-base": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/write-project-manifest": "workspace:*",
"fast-deep-equal": "^3.1.3",
"npm-packlist": "^5.1.3"
},
"devDependencies": {
Expand Down
38 changes: 29 additions & 9 deletions fetching/directory-fetcher/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { promises as fs, type Stats } from 'fs'
import path from 'path'
import { createExportableManifest } from '@pnpm/exportable-manifest'
import type { DirectoryFetcher, DirectoryFetcherOptions } from '@pnpm/fetcher-base'
import { logger } from '@pnpm/logger'
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
import { safeReadProjectManifest } from '@pnpm/read-project-manifest'
import type { ProjectManifest } from '@pnpm/types'
import { writeProjectManifest } from '@pnpm/write-project-manifest'
import equal from 'fast-deep-equal'
import packlist from 'npm-packlist'

const directoryFetcherLogger = logger('directory-fetcher')
Expand Down Expand Up @@ -48,10 +52,7 @@ async function fetchAllFilesFromDir (
) {
const filesIndex = await _fetchAllFilesFromDir(readFileStat, dir)
if (opts.manifest) {
// 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
const manifest = await safeReadProjectManifestOnly(dir) ?? {}
const manifest = await safeReadProjectManifestAndMakeExportable(dir, filesIndex) ?? {}
opts.manifest.resolve(manifest as any) // eslint-disable-line @typescript-eslint/no-explicit-any
}
return {
Expand Down Expand Up @@ -129,10 +130,7 @@ async function fetchPackageFilesFromDir (
const files = await packlist({ path: dir })
const filesIndex: Record<string, string> = Object.fromEntries(files.map((file) => [file, path.join(dir, file)]))
if (opts.manifest) {
// 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
const manifest = await safeReadProjectManifestOnly(dir) ?? {}
const manifest = await safeReadProjectManifestAndMakeExportable(dir, filesIndex) ?? {}
opts.manifest.resolve(manifest as any) // eslint-disable-line @typescript-eslint/no-explicit-any
}
return {
Expand All @@ -141,3 +139,25 @@ async function fetchPackageFilesFromDir (
packageImportMethod: 'hardlink' as const,
}
}

async function safeReadProjectManifestAndMakeExportable (
dir: string,
filesIndex: Record<string, string>
): Promise<ProjectManifest | 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
const result = await safeReadProjectManifest(dir)

if (result) {
const { fileName, manifest } = result
const exportableManifest = await createExportableManifest(dir, manifest)
if (!equal(manifest, exportableManifest)) {
const manifestPathOverride = path.join(dir, 'node_modules', '.pnpm', fileName)
await writeProjectManifest(manifestPathOverride, exportableManifest)
filesIndex[fileName] = manifestPathOverride
}
return manifest
}
return null
}
9 changes: 9 additions & 0 deletions fetching/directory-fetcher/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@
{
"path": "../../__utils__/test-fixtures"
},
{
"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
9 changes: 7 additions & 2 deletions pkg-manifest/read-project-manifest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import {
type WriteProjectManifest = (manifest: ProjectManifest, force?: boolean) => Promise<void>

export async function safeReadProjectManifestOnly (projectDir: string) {
const result = await safeReadProjectManifest(projectDir)
return result?.manifest ?? null
}

export async function safeReadProjectManifest (projectDir: string) {
try {
return await readProjectManifestOnly(projectDir)
} catch (err: any) { // eslint-disable-line
return await readProjectManifest(projectDir)
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code === 'ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND') {
return null
}
Expand Down
24 changes: 24 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion releasing/plugin-commands-deploy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
"@pnpm/lockfile-types": "workspace:*",
"@pnpm/plugin-commands-deploy": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.11.0"
"@pnpm/registry-mock": "3.11.0",
"@types/cross-spawn": "^6.0.2",
"cross-spawn": "^7.0.3",
"write-yaml-file": "^5.0.0"
},
"dependencies": {
"@pnpm/cli-utils": "workspace:*",
Expand All @@ -50,6 +53,7 @@
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/types": "workspace:*",
"@zkochan/rimraf": "^2.1.2",
"p-defer": "^3.0.0",
"render-help": "^1.0.3"
},
"peerDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion releasing/plugin-commands-deploy/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { install } from '@pnpm/plugin-commands-installation'
import { FILTERING } from '@pnpm/common-cli-options-help'
import { PnpmError } from '@pnpm/error'
import rimraf from '@zkochan/rimraf'
import pDefer from 'p-defer'
import renderHelp from 'render-help'
import { deployHook } from './deployHook'

Expand Down Expand Up @@ -107,7 +108,7 @@ export async function handler (
}

async function copyProject (src: string, dest: string, opts: { includeOnlyPackageFiles: boolean }) {
const { filesIndex } = await fetchFromDir(src, opts)
const { filesIndex } = await fetchFromDir(src, { ...opts, manifest: pDefer() })
const importPkg = createIndexedPkgImporter('clone-or-copy')
await importPkg(dest, { filesMap: filesIndex, force: true, fromStore: true })
}
42 changes: 42 additions & 0 deletions releasing/plugin-commands-deploy/test/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import path from 'path'
import { deploy } from '@pnpm/plugin-commands-deploy'
import { assertProject } from '@pnpm/assert-project'
import { preparePackages } from '@pnpm/prepare'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import { readProjects } from '@pnpm/filter-workspace-packages'
import crossSpawn from 'cross-spawn'
import writeYamlFile from 'write-yaml-file'
import { DEFAULT_OPTS } from './utils'

const pnpmBin = path.join(__dirname, '../../../pnpm/bin/pnpm.cjs')

test('deploy', async () => {
preparePackages([
{
Expand All @@ -20,6 +25,10 @@ test('deploy', async () => {
'project-3': 'workspace:*',
'is-negative': '1.0.0',
},
main: 'local-file-1.js',
publishConfig: {
main: 'publish-file-1.js',
},
},
{
name: 'project-2',
Expand All @@ -29,6 +38,10 @@ test('deploy', async () => {
'project-3': 'workspace:*',
'is-odd': '1.0.0',
},
main: 'local-file-2.js',
publishConfig: {
main: 'publish-file-2.js',
},
},
{
name: 'project-3',
Expand All @@ -46,6 +59,10 @@ test('deploy', async () => {
fs.writeFileSync(`${name}/index.js`, '', 'utf8')
})

await writeYamlFile('pnpm-workspace.yaml', { packages: ['*'] })
crossSpawn.sync(pnpmBin, ['install', '--ignore-scripts', '--store-dir=../store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`])
fs.rmSync('pnpm-lock.yaml')

const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), [{ namePattern: 'project-1' }])

await deploy.handler({
Expand Down Expand Up @@ -73,6 +90,31 @@ test('deploy', async () => {
expect(fs.existsSync('deploy/node_modules/.pnpm/file+project-3/node_modules/project-3/index.js')).toBeTruthy()
expect(fs.existsSync('deploy/node_modules/.pnpm/file+project-3/node_modules/project-3/test.js')).toBeFalsy()
expect(fs.existsSync('pnpm-lock.yaml')).toBeFalsy() // no changes to the lockfile are written
const [
project1Package,
project2Package,
] = await Promise.all([
fs.promises.readFile('deploy/package.json', 'utf8').then(file => JSON.parse(file)),
fs.promises.readFile('deploy/node_modules/.pnpm/file+project-2/node_modules/project-2/package.json', 'utf8').then(file => JSON.parse(file)),
])
expect(project1Package).toMatchObject({
name: 'project-1',
main: 'publish-file-1.js',
dependencies: {
'is-positive': '1.0.0',
'project-2': '2.0.0',
},
})
expect(project1Package).not.toHaveProperty('publishConfig')
expect(project2Package).toMatchObject({
name: 'project-2',
main: 'publish-file-2.js',
dependencies: {
'project-3': '2.0.0',
'is-odd': '1.0.0',
},
})
expect(project2Package).not.toHaveProperty('publishConfig')
})

test('deploy with dedupePeerDependents=true ignores the value of dedupePeerDependents', async () => {
Expand Down

0 comments on commit b12b3e9

Please sign in to comment.