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 17, 2023
1 parent 9365e68 commit 0729698
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 19 deletions.
10 changes: 10 additions & 0 deletions .changeset/quiet-dingos-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@pnpm/read-project-manifest": minor
"@pnpm/plugin-commands-deploy": minor
"@pnpm/directory-fetcher": minor
"@pnpm/cli-utils": patch
---

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: 2 additions & 2 deletions cli/cli-utils/src/readProjectManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function readProjectManifest (
): Promise<{
fileName: string
manifest: ProjectManifest
writeProjectManifest: (manifest: ProjectManifest, force?: boolean) => Promise<void>
writeProjectManifest: utils.WriteProjectManifest
}> {
const { fileName, manifest, writeProjectManifest } = await utils.readProjectManifest(projectDir)
packageIsInstallable(projectDir, manifest as any, opts) // eslint-disable-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -39,7 +39,7 @@ export async function tryReadProjectManifest (
): Promise<{
fileName: string
manifest: ProjectManifest | null
writeProjectManifest: (manifest: ProjectManifest, force?: boolean) => Promise<void>
writeProjectManifest: utils.WriteProjectManifest
}> {
const { fileName, manifest, writeProjectManifest } = await utils.tryReadProjectManifest(projectDir)
if (manifest == null) return { fileName, manifest, writeProjectManifest }
Expand Down
2 changes: 2 additions & 0 deletions fetching/directory-fetcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
"@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:*",
"npm-packlist": "^5.1.3"
},
"devDependencies": {
Expand Down
41 changes: 30 additions & 11 deletions fetching/directory-fetcher/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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 packlist from 'npm-packlist'

const directoryFetcherLogger = logger('directory-fetcher')
Expand Down Expand Up @@ -48,11 +50,8 @@ 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) ?? {}
opts.manifest.resolve(manifest as any) // eslint-disable-line @typescript-eslint/no-explicit-any
const manifest = await safeReadProjectManifestAndMakeExportable(dir, filesIndex)
opts.manifest.resolve(manifest ?? {} as any) // eslint-disable-line @typescript-eslint/no-explicit-any
}
return {
local: true as const,
Expand Down Expand Up @@ -129,15 +128,35 @@ 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) ?? {}
opts.manifest.resolve(manifest as any) // eslint-disable-line @typescript-eslint/no-explicit-any
const manifest = await safeReadProjectManifestAndMakeExportable(dir, filesIndex)
opts.manifest.resolve(manifest ?? {} as any) // eslint-disable-line @typescript-eslint/no-explicit-any
}
return {
local: true as const,
filesIndex,
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 manifest = await safeReadProjectManifest(dir)

if (manifest) {
const { fileName } = manifest
const exportableManifest = await createExportableManifest(
dir,
manifest.manifest
)
const manifestPathOverride = path.join(dir, 'node_modules', '.pnpm', fileName)
await manifest.writeProjectManifest(exportableManifest, { manifestPathOverride })
filesIndex[fileName] = manifestPathOverride
}

return manifest?.manifest ?? null
}
6 changes: 6 additions & 0 deletions fetching/directory-fetcher/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
{
"path": "../../__utils__/test-fixtures"
},
{
"path": "../../packages/types"
},
{
"path": "../../pkg-manifest/exportable-manifest"
},
{
"path": "../../pkg-manifest/read-project-manifest"
},
Expand Down
29 changes: 24 additions & 5 deletions pkg-manifest/read-project-manifest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@ import {
readJsonFile,
} from './readFile'

type WriteProjectManifest = (manifest: ProjectManifest, force?: boolean) => Promise<void>
interface WriteProjectManifestOptions {
force?: boolean
manifestPathOverride?: string
}
export type WriteProjectManifest = (manifest: ProjectManifest, opts?: WriteProjectManifestOptions) => 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 Expand Up @@ -191,9 +200,19 @@ function createManifestWriter (
}
): (WriteProjectManifest) {
let initialManifest = normalize(opts.initialManifest)
return async (updatedManifest: ProjectManifest, force?: boolean) => {
return async (
updatedManifest: ProjectManifest,
{
force = false,
manifestPathOverride = opts.manifestPath,
} = {}
) => {
updatedManifest = normalize(updatedManifest)
if (force === true || !equal(initialManifest, updatedManifest)) {
if (
force ||
path.resolve(manifestPathOverride) !== path.resolve(opts.manifestPath) ||
!equal(initialManifest, updatedManifest)
) {
await writeProjectManifest(opts.manifestPath, updatedManifest, {
comments: opts.comments,
indent: opts.indent,
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

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

5 changes: 4 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 Down
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 0729698

Please sign in to comment.