Skip to content

Commit 22eb43e

Browse files
committed
Add support for modifying package.json of workspaces
1 parent 6d7c4ee commit 22eb43e

File tree

4 files changed

+169
-127
lines changed

4 files changed

+169
-127
lines changed

.dep-stats.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"synp": "^1.9.13",
2222
"tinyglobby": "^0.2.10",
2323
"which": "^5.0.0",
24+
"yaml": "^2.6.0",
2425
"yargs-parser": "^21.1.1"
2526
},
2627
"devDependencies": {},
@@ -116,6 +117,7 @@
116117
"tinyglobby": "^0.2.10",
117118
"which": "^5.0.0",
118119
"write-file-atomic": "^6.0.0",
120+
"yaml": "^2.6.0",
119121
"yargs-parser": "^21.1.1"
120122
},
121123
"transitives": {

src/commands/optimize.ts

Lines changed: 152 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'fs/promises'
12
import path from 'node:path'
23

34
import spawn from '@npmcli/promise-spawn'
@@ -8,9 +9,12 @@ import npmPackageArg from 'npm-package-arg'
89
import ora from 'ora'
910
import pacote from 'pacote'
1011
import semver from 'semver'
12+
import { glob as tinyGlob } from 'tinyglobby'
13+
import { parse as yamlParse } from 'yaml'
1114

1215
import { commonFlags } from '../flags'
1316
import { printFlagList } from '../utils/formatting'
17+
import { existsSync } from '../utils/fs'
1418
import { hasOwn } from '../utils/objects'
1519
import { detect } from '../utils/package-manager-detector'
1620
import { pEach } from '../utils/promises'
@@ -27,12 +31,12 @@ import type {
2731
StringKeyValueObject
2832
} from '../utils/package-manager-detector'
2933

30-
const distPath = __dirname
31-
3234
const COMMAND_TITLE = 'Socket Optimize'
3335
const OVERRIDES_FIELD_NAME = 'overrides'
36+
const PNPM_WORKSPACE = 'pnpm-workspace'
3437
const RESOLUTIONS_FIELD_NAME = 'resolutions'
3538

39+
const distPath = __dirname
3640
const manifestNpmOverrides = getManifestData('npm')!
3741
const packumentCache = new Map()
3842

@@ -101,74 +105,39 @@ const lockIncludesByAgent: Record<Agent, LockIncludes> = {
101105
}
102106

103107
type ModifyManifest = (
104-
editablePkgJson: EditablePackageJson,
108+
pkgJson: EditablePackageJson,
105109
overrides: Overrides
106110
) => void
107111

108-
const updateManifestByAgent: Record<Agent, ModifyManifest> = (<any>{
109-
__proto__: null,
110-
npm(editablePkgJson: EditablePackageJson, overrides: Overrides) {
111-
editablePkgJson.update({
112-
__proto__: null,
112+
const updateManifestByAgent: Record<Agent, ModifyManifest> = {
113+
npm(pkgJson: EditablePackageJson, overrides: Overrides) {
114+
pkgJson.update({
113115
[OVERRIDES_FIELD_NAME]: overrides
114116
})
115117
},
116-
pnpm(editablePkgJson: EditablePackageJson, overrides: Overrides) {
117-
editablePkgJson.update({
118+
pnpm(pkgJson: EditablePackageJson, overrides: Overrides) {
119+
pkgJson.update({
118120
pnpm: {
119-
__proto__: null,
120-
...(<object>editablePkgJson.content['pnpm']),
121+
...(<object>pkgJson.content['pnpm']),
121122
[OVERRIDES_FIELD_NAME]: overrides
122123
}
123124
})
124125
},
125-
yarn(editablePkgJson: EditablePackageJson, overrides: PnpmOrYarnOverrides) {
126-
editablePkgJson.update({
127-
__proto__: null,
128-
[RESOLUTIONS_FIELD_NAME]: overrides
126+
yarn(pkgJson: EditablePackageJson, overrides: Overrides) {
127+
pkgJson.update({
128+
[RESOLUTIONS_FIELD_NAME]: <PnpmOrYarnOverrides>overrides
129129
})
130130
}
131-
}) as Record<Agent, ModifyManifest>
132-
133-
type AddOverridesConfig = {
134-
agent: Agent
135-
isPrivate: boolean
136-
isWorkspace: boolean
137-
lockIncludes: LockIncludes
138-
lockSrc: string
139-
manifestEntries: ManifestEntry[]
140-
pkgJsonPath: string
141-
pin: boolean
142131
}
143132

144-
type AddOverridesState = {
145-
added: Set<string>
146-
updated: Set<string>
147-
}
148-
149-
async function addOverrides(
150-
{
151-
agent,
152-
isPrivate,
153-
isWorkspace,
154-
lockSrc,
155-
lockIncludes,
156-
manifestEntries,
157-
pkgJsonPath,
158-
pin
159-
}: AddOverridesConfig,
160-
state: AddOverridesState
161-
): Promise<AddOverridesState> {
162-
const editablePkgJson = await EditablePackageJson.load(
163-
path.dirname(pkgJsonPath)
164-
)
133+
function getDependencyEntries(pkgJson: PackageJsonContent) {
165134
const {
166135
dependencies,
167136
devDependencies,
168-
peerDependencies,
169-
optionalDependencies
170-
} = editablePkgJson.content
171-
const depEntries = <[string, NonNullable<typeof dependencies>][]>[
137+
optionalDependencies,
138+
peerDependencies
139+
} = pkgJson
140+
return <[string, NonNullable<typeof dependencies>][]>[
172141
[
173142
'dependencies',
174143
dependencies ? { __proto__: null, ...dependencies } : undefined
@@ -188,19 +157,107 @@ async function addOverrides(
188157
: undefined
189158
]
190159
].filter(({ 1: o }) => o)
160+
}
161+
162+
async function getWorkspaces(
163+
agent: Agent,
164+
pkgPath: string,
165+
pkgJson: PackageJsonContent
166+
): Promise<string[] | undefined> {
167+
if (agent !== 'pnpm') {
168+
return Array.isArray(pkgJson['workspaces'])
169+
? <string[]>pkgJson['workspaces'].filter(isNonEmptyString)
170+
: undefined
171+
}
172+
for (const workspacePath of [
173+
path.join(pkgPath!, `${PNPM_WORKSPACE}.yaml`),
174+
path.join(pkgPath!, `${PNPM_WORKSPACE}.yml`)
175+
]) {
176+
if (existsSync(workspacePath)) {
177+
let packages
178+
try {
179+
// eslint-disable-next-line no-await-in-loop
180+
packages = yamlParse(await fs.readFile(workspacePath, 'utf8'))?.packages
181+
} catch {}
182+
if (Array.isArray(packages)) {
183+
return packages.filter(isNonEmptyString)
184+
}
185+
}
186+
}
187+
return undefined
188+
}
189+
190+
function workspaceToGlobPattern(workspace: string): string {
191+
const { length } = workspace
192+
// If the workspace ends with "/"
193+
if (workspace.charCodeAt(length - 1) === 47 /*'/'*/) {
194+
return `${workspace}/*/package.json`
195+
}
196+
// If the workspace ends with "/**"
197+
if (
198+
workspace.charCodeAt(length - 1) === 42 /*'*'*/ &&
199+
workspace.charCodeAt(length - 2) === 42 /*'*'*/ &&
200+
workspace.charCodeAt(length - 3) === 47 /*'/'*/
201+
) {
202+
return `${workspace}/*/**/package.json`
203+
}
204+
// Things like "packages/a" or "packages/*"
205+
return `${workspace}/package.json`
206+
}
207+
208+
type AddOverridesConfig = {
209+
agent: Agent
210+
lockIncludes: LockIncludes
211+
lockSrc: string
212+
manifestEntries: ManifestEntry[]
213+
pkgJson?: EditablePackageJson | undefined
214+
pkgPath: string
215+
pin: boolean
216+
rootPath: string
217+
}
218+
219+
type AddOverridesState = {
220+
added: Set<string>
221+
updated: Set<string>
222+
}
223+
224+
async function addOverrides(
225+
{
226+
agent,
227+
lockIncludes,
228+
lockSrc,
229+
manifestEntries,
230+
pkgJson: editablePkgJson,
231+
pkgPath,
232+
pin,
233+
rootPath
234+
}: AddOverridesConfig,
235+
state: AddOverridesState = {
236+
added: new Set(),
237+
updated: new Set()
238+
}
239+
): Promise<AddOverridesState> {
240+
if (editablePkgJson === undefined) {
241+
editablePkgJson = await EditablePackageJson.load(pkgPath)
242+
}
243+
const pkgJson: Readonly<PackageJsonContent> = editablePkgJson.content
244+
const isRoot = pkgPath === rootPath
245+
const depEntries = getDependencyEntries(pkgJson)
246+
const workspaces = await getWorkspaces(agent, pkgPath, pkgJson)
247+
const isWorkspace = !!workspaces
191248
const overridesDataObjects = <GetOverridesResult[]>[]
192-
if (isPrivate || isWorkspace) {
193-
overridesDataObjects.push(
194-
getOverridesDataByAgent[agent](editablePkgJson.content)
195-
)
249+
if (pkgJson['private'] || isWorkspace) {
250+
overridesDataObjects.push(getOverridesDataByAgent[agent](pkgJson))
196251
} else {
197252
overridesDataObjects.push(
198-
getOverridesDataByAgent['npm'](editablePkgJson.content),
199-
getOverridesDataByAgent['yarn'](editablePkgJson.content)
253+
getOverridesDataByAgent['npm'](pkgJson),
254+
getOverridesDataByAgent['yarn'](pkgJson)
200255
)
201256
}
257+
const spinner = isRoot
258+
? ora('Fetching override manifests...').start()
259+
: undefined
202260
const depAliasMap = new Map<string, { id: string; version: string }>()
203-
const spinner = ora(`Fetching override manifests...`).start()
204261
// Chunk package names to process them in parallel 3 at a time.
205262
await pEach(manifestEntries, 3, async ({ 1: data }) => {
206263
const { name: regPkgName, package: origPkgName, version } = data
@@ -227,6 +284,9 @@ async function addOverrides(
227284
})
228285
}
229286
}
287+
if (!isRoot) {
288+
return
289+
}
230290
// Chunk package names to process them in parallel 3 at a time.
231291
await pEach(overridesDataObjects, 3, async ({ overrides, type }) => {
232292
const overrideExists = hasOwn(overrides, origPkgName)
@@ -263,7 +323,34 @@ async function addOverrides(
263323
}
264324
})
265325
})
266-
spinner.stop()
326+
if (workspaces) {
327+
const wsPkgJsonPaths = await tinyGlob(
328+
workspaces.map(workspaceToGlobPattern),
329+
{
330+
absolute: true,
331+
cwd: pkgPath!
332+
}
333+
)
334+
// Chunk package names to process them in parallel 3 at a time.
335+
await pEach(wsPkgJsonPaths, 3, async wsPkgJsonPath => {
336+
const { added, updated } = await addOverrides({
337+
agent,
338+
lockSrc,
339+
lockIncludes,
340+
manifestEntries,
341+
pin,
342+
pkgPath: path.dirname(wsPkgJsonPath),
343+
rootPath
344+
})
345+
for (const regPkgName of added) {
346+
state.added.add(regPkgName)
347+
}
348+
for (const regPkgName of updated) {
349+
state.updated.add(regPkgName)
350+
}
351+
})
352+
}
353+
spinner?.stop()
267354
if (state.added.size || state.updated.size) {
268355
editablePkgJson.update(<PackageJsonContent>Object.fromEntries(depEntries))
269356
for (const { overrides, type } of overridesDataObjects) {
@@ -318,13 +405,11 @@ export const optimize: CliSubcommand = {
318405
const {
319406
agent,
320407
agentExecPath,
321-
isPrivate,
322-
isWorkspace,
323408
lockSrc,
324409
lockPath,
325410
minimumNodeVersion,
326-
pkgJsonPath,
327411
pkgJson,
412+
pkgPath,
328413
supported
329414
} = await detect({
330415
cwd,
@@ -345,7 +430,7 @@ export const optimize: CliSubcommand = {
345430
console.log(`✘ ${COMMAND_TITLE}: No ${lockName} found`)
346431
return
347432
}
348-
if (pkgJson === undefined) {
433+
if (pkgPath === undefined) {
349434
console.log(`✘ ${COMMAND_TITLE}: No package.json found`)
350435
return
351436
}
@@ -354,7 +439,6 @@ export const optimize: CliSubcommand = {
354439
`⚠️ ${COMMAND_TITLE}: Package ${lockName} found at ${lockPath}`
355440
)
356441
}
357-
358442
const state: AddOverridesState = {
359443
added: new Set(),
360444
updated: new Set()
@@ -369,13 +453,13 @@ export const optimize: CliSubcommand = {
369453
await addOverrides(
370454
{
371455
agent: agent === 'bun' ? 'yarn' : agent,
372-
isPrivate,
373-
isWorkspace,
374456
lockIncludes,
375457
lockSrc,
376458
manifestEntries,
377459
pin,
378-
pkgJsonPath
460+
pkgJson,
461+
pkgPath,
462+
rootPath: pkgPath
379463
},
380464
state
381465
)

src/utils/json.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)