Skip to content

Commit 31e0d46

Browse files
committed
Cleanup optimize
1 parent c4168ec commit 31e0d46

File tree

3 files changed

+306
-286
lines changed

3 files changed

+306
-286
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import path from 'node:path'
2+
3+
import npa from 'npm-package-arg'
4+
import semver from 'semver'
5+
import { glob as tinyGlob } from 'tinyglobby'
6+
7+
import { getManifestData } from '@socketsecurity/registry'
8+
import { hasOwn, toSortedObject } from '@socketsecurity/registry/lib/objects'
9+
import {
10+
fetchPackageManifest,
11+
readPackageJson
12+
} from '@socketsecurity/registry/lib/packages'
13+
import { pEach } from '@socketsecurity/registry/lib/promises'
14+
import { Spinner } from '@socketsecurity/registry/lib/spinner'
15+
16+
import { depsIncludesByAgent } from './deps-includes-by-agent'
17+
import { getDependencyEntries } from './get-dependency-entries'
18+
import { overridesDataByAgent } from './get-overrides-by-agent'
19+
import { getWorkspaceGlobs } from './get-workspace-globs'
20+
import { lockfileIncludesByAgent } from './lockfile-includes-by-agent'
21+
import { lsByAgent } from './ls-by-agent'
22+
import { updateManifestByAgent } from './update-manifest-by-agent'
23+
import constants from '../../constants'
24+
import { cmdPrefixMessage } from '../../utils/cmd'
25+
26+
import type { AgentLockIncludesFn } from './lockfile-includes-by-agent'
27+
import type {
28+
Agent,
29+
EnvDetails,
30+
StringKeyValueObject
31+
} from '../../utils/package-environment'
32+
import type { Logger } from '@socketsecurity/registry/lib/logger'
33+
34+
type AddOverridesOptions = {
35+
logger?: Logger | undefined
36+
pin?: boolean | undefined
37+
prod?: boolean | undefined
38+
spinner?: Spinner | undefined
39+
state?: AddOverridesState | undefined
40+
}
41+
type AddOverridesState = {
42+
added: Set<string>
43+
addedInWorkspaces: Set<string>
44+
updated: Set<string>
45+
updatedInWorkspaces: Set<string>
46+
warnedPnpmWorkspaceRequiresNpm: boolean
47+
}
48+
type GetOverridesResult = { type: Agent; overrides: Overrides }
49+
type NpmOverrides = { [key: string]: string | StringKeyValueObject }
50+
type PackageJson = Awaited<ReturnType<typeof readPackageJson>>
51+
type PnpmOrYarnOverrides = { [key: string]: string }
52+
type Overrides = NpmOverrides | PnpmOrYarnOverrides
53+
54+
const { NPM, PNPM, YARN_CLASSIC } = constants
55+
56+
const CMD_NAME = 'socket optimize'
57+
58+
const manifestNpmOverrides = getManifestData(NPM)
59+
60+
export async function addOverrides(
61+
pkgPath: string,
62+
pkgEnvDetails: EnvDetails,
63+
options?: AddOverridesOptions | undefined
64+
): Promise<AddOverridesState> {
65+
const {
66+
agent,
67+
agentExecPath,
68+
lockName,
69+
lockSrc,
70+
npmExecPath,
71+
pkgPath: rootPath
72+
} = pkgEnvDetails
73+
const {
74+
logger,
75+
pin,
76+
prod,
77+
spinner,
78+
state = {
79+
added: new Set(),
80+
addedInWorkspaces: new Set(),
81+
updated: new Set(),
82+
updatedInWorkspaces: new Set(),
83+
warnedPnpmWorkspaceRequiresNpm: false
84+
}
85+
} = { __proto__: null, ...options } as AddOverridesOptions
86+
let { pkgJson: editablePkgJson } = pkgEnvDetails
87+
if (editablePkgJson === undefined) {
88+
editablePkgJson = await readPackageJson(pkgPath, { editable: true })
89+
}
90+
const { content: pkgJson } = editablePkgJson
91+
92+
const workspaceName = path.relative(rootPath, pkgPath)
93+
const workspaceGlobs = await getWorkspaceGlobs(agent, pkgPath, pkgJson)
94+
const isRoot = pkgPath === rootPath
95+
const isLockScanned = isRoot && !prod
96+
const isWorkspace = !!workspaceGlobs
97+
if (
98+
isWorkspace &&
99+
agent === PNPM &&
100+
// npmExecPath will === the agent name IF it CANNOT be resolved.
101+
npmExecPath === NPM &&
102+
!state.warnedPnpmWorkspaceRequiresNpm
103+
) {
104+
state.warnedPnpmWorkspaceRequiresNpm = true
105+
logger?.warn(
106+
cmdPrefixMessage(
107+
CMD_NAME,
108+
`${agent} workspace support requires \`npm ls\`, falling back to \`${agent} list\``
109+
)
110+
)
111+
}
112+
113+
const overridesDataObjects = [] as GetOverridesResult[]
114+
if (pkgJson['private'] || isWorkspace) {
115+
overridesDataObjects.push(overridesDataByAgent.get(agent)!(pkgJson))
116+
} else {
117+
overridesDataObjects.push(
118+
overridesDataByAgent.get(NPM)!(pkgJson),
119+
overridesDataByAgent.get(YARN_CLASSIC)!(pkgJson)
120+
)
121+
}
122+
123+
spinner?.setText(
124+
`Adding overrides${workspaceName ? ` to ${workspaceName}` : ''}...`
125+
)
126+
127+
const depAliasMap = new Map<string, string>()
128+
const depEntries = getDependencyEntries(pkgJson)
129+
130+
const nodeRange = `>=${pkgEnvDetails.minimumNodeVersion}`
131+
const manifestEntries = manifestNpmOverrides.filter(({ 1: data }) =>
132+
semver.satisfies(semver.coerce(data.engines.node)!, nodeRange)
133+
)
134+
135+
// Chunk package names to process them in parallel 3 at a time.
136+
await pEach(manifestEntries, 3, async ({ 1: data }) => {
137+
const { name: sockRegPkgName, package: origPkgName, version } = data
138+
const major = semver.major(version)
139+
const sockOverridePrefix = `${NPM}:${sockRegPkgName}@`
140+
const sockOverrideSpec = `${sockOverridePrefix}${pin ? version : `^${major}`}`
141+
for (const { 1: depObj } of depEntries) {
142+
const sockSpec = hasOwn(depObj, sockRegPkgName)
143+
? depObj[sockRegPkgName]
144+
: undefined
145+
if (sockSpec) {
146+
depAliasMap.set(sockRegPkgName, sockSpec)
147+
}
148+
const origSpec = hasOwn(depObj, origPkgName)
149+
? depObj[origPkgName]
150+
: undefined
151+
if (origSpec) {
152+
let thisSpec = origSpec
153+
// Add package aliases for direct dependencies to avoid npm EOVERRIDE errors.
154+
// https://docs.npmjs.com/cli/v8/using-npm/package-spec#aliases
155+
if (
156+
!(
157+
thisSpec.startsWith(sockOverridePrefix) &&
158+
semver.coerce(npa(thisSpec).rawSpec)?.version
159+
)
160+
) {
161+
thisSpec = sockOverrideSpec
162+
depObj[origPkgName] = thisSpec
163+
state.added.add(sockRegPkgName)
164+
if (workspaceName) {
165+
state.addedInWorkspaces.add(workspaceName)
166+
}
167+
}
168+
depAliasMap.set(origPkgName, thisSpec)
169+
}
170+
}
171+
if (isRoot) {
172+
// The AgentDepsIncludesFn and AgentLockIncludesFn types overlap in their
173+
// first two parameters. AgentLockIncludesFn accepts an optional third
174+
// parameter which AgentDepsIncludesFn will ignore so we cast thingScanner
175+
// as an AgentLockIncludesFn type.
176+
const thingScanner = (
177+
isLockScanned
178+
? lockfileIncludesByAgent.get(agent)
179+
: depsIncludesByAgent.get(agent)
180+
) as AgentLockIncludesFn
181+
const thingToScan = isLockScanned
182+
? lockSrc
183+
: await lsByAgent.get(agent)!(agentExecPath, pkgPath, { npmExecPath })
184+
// Chunk package names to process them in parallel 3 at a time.
185+
await pEach(overridesDataObjects, 3, async ({ overrides, type }) => {
186+
const overrideExists = hasOwn(overrides, origPkgName)
187+
if (
188+
overrideExists ||
189+
thingScanner(thingToScan, origPkgName, lockName)
190+
) {
191+
const oldSpec = overrideExists ? overrides[origPkgName]! : undefined
192+
const origDepAlias = depAliasMap.get(origPkgName)
193+
const sockRegDepAlias = depAliasMap.get(sockRegPkgName)
194+
const depAlias = sockRegDepAlias ?? origDepAlias
195+
let newSpec = sockOverrideSpec
196+
if (type === NPM && depAlias) {
197+
// With npm one may not set an override for a package that one directly
198+
// depends on unless both the dependency and the override itself share
199+
// the exact same spec. To make this limitation easier to deal with,
200+
// overrides may also be defined as a reference to a spec for a direct
201+
// dependency by prefixing the name of the package to match the version
202+
// of with a $.
203+
// https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides
204+
newSpec = `$${sockRegDepAlias ? sockRegPkgName : origPkgName}`
205+
} else if (typeof oldSpec === 'string') {
206+
const thisSpec = oldSpec.startsWith('$')
207+
? depAlias || newSpec
208+
: oldSpec || newSpec
209+
if (thisSpec.startsWith(sockOverridePrefix)) {
210+
if (
211+
pin &&
212+
semver.major(
213+
semver.coerce(npa(thisSpec).rawSpec)?.version ?? version
214+
) !== major
215+
) {
216+
const otherVersion = (await fetchPackageManifest(thisSpec))
217+
?.version
218+
if (otherVersion && otherVersion !== version) {
219+
newSpec = `${sockOverridePrefix}${pin ? otherVersion : `^${semver.major(otherVersion)}`}`
220+
}
221+
}
222+
} else {
223+
newSpec = oldSpec
224+
}
225+
}
226+
if (newSpec !== oldSpec) {
227+
overrides[origPkgName] = newSpec
228+
const addedOrUpdated = overrideExists ? 'updated' : 'added'
229+
state[addedOrUpdated].add(sockRegPkgName)
230+
}
231+
}
232+
})
233+
}
234+
})
235+
236+
if (workspaceGlobs) {
237+
const workspacePkgJsonPaths = await tinyGlob(workspaceGlobs, {
238+
absolute: true,
239+
cwd: pkgPath!,
240+
ignore: ['**/node_modules/**', '**/bower_components/**']
241+
})
242+
// Chunk package names to process them in parallel 3 at a time.
243+
await pEach(workspacePkgJsonPaths, 3, async workspacePkgJsonPath => {
244+
const otherState = await addOverrides(
245+
path.dirname(workspacePkgJsonPath),
246+
pkgEnvDetails,
247+
{
248+
logger,
249+
pin,
250+
prod,
251+
spinner
252+
}
253+
)
254+
for (const key of [
255+
'added',
256+
'addedInWorkspaces',
257+
'updated',
258+
'updatedInWorkspaces'
259+
] satisfies
260+
// Here we're just telling TS that we're looping over key names
261+
// of the type and that they're all Set<string> props. This allows
262+
// us to do the SetA.add(setB.get) pump type-safe without casts.
263+
Array<
264+
keyof Pick<
265+
AddOverridesState,
266+
'added' | 'addedInWorkspaces' | 'updated' | 'updatedInWorkspaces'
267+
>
268+
>) {
269+
for (const value of otherState[key]) {
270+
state[key].add(value)
271+
}
272+
}
273+
})
274+
}
275+
276+
if (state.added.size > 0 || state.updated.size > 0) {
277+
editablePkgJson.update(Object.fromEntries(depEntries) as PackageJson)
278+
for (const { overrides, type } of overridesDataObjects) {
279+
updateManifestByAgent.get(type)!(
280+
editablePkgJson,
281+
toSortedObject(overrides)
282+
)
283+
}
284+
await editablePkgJson.save()
285+
}
286+
287+
return state
288+
}

0 commit comments

Comments
 (0)