Skip to content

Commit ca4e04f

Browse files
committed
feat(plugin-interactive-tools): support ignore lists
1 parent 6bdf043 commit ca4e04f

File tree

4 files changed

+140
-14
lines changed

4 files changed

+140
-14
lines changed

packages/plugin-interactive-tools/sources/commands/upgrade-interactive.tsx

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import {ItemOptions}
44
import {Pad} from '@yarnpkg/libui/sources/components/Pad';
55
import {ScrollableItems} from '@yarnpkg/libui/sources/components/ScrollableItems';
66
import {useMinistore} from '@yarnpkg/libui/sources/hooks/useMinistore';
7+
import {useKeypress} from '@yarnpkg/libui/sources/hooks/useKeypress';
78
import {renderForm, SubmitInjectedComponent} from '@yarnpkg/libui/sources/misc/renderForm';
89
import {suggestUtils} from '@yarnpkg/plugin-essentials';
910
import {Command, Usage} from 'clipanion';
1011
import {diffWords} from 'diff';
1112
import {Box, Text} from 'ink';
12-
import React, {useEffect, useRef, useState} from 'react';
13+
import React, {useCallback, useEffect, useRef, useState} from 'react';
1314
import semver from 'semver';
1415

1516
const SIMPLE_SEMVER = /^((?:[\^~]|>=?)?)([0-9]+)(\.[0-9]+)(\.[0-9]+)((?:-\S+)?)$/;
@@ -143,6 +144,46 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
143144
return suggestions;
144145
};
145146

147+
const formatAction = ({isIgnore, version}: {isIgnore: boolean; version: string}) => {
148+
return `${isIgnore ? `ignore` : `update`}__${version}`;
149+
};
150+
151+
const parseAction = (action: string) => {
152+
const [command, version] = action.split(`__`);
153+
154+
return {
155+
isIgnore: command === `ignore`,
156+
version,
157+
};
158+
};
159+
160+
const readIgnoreList = () => {
161+
return configuration.get(`upgradeInteractiveIgnoredVersions`).reduce((allVersions, versionString) => {
162+
const [packageName, versionRange = `*`] = versionString.split(`:`);
163+
164+
if (!packageName) return allVersions;
165+
166+
return {
167+
...allVersions,
168+
[packageName]: versionRange,
169+
};
170+
}, {} as Record<string, string>);
171+
};
172+
173+
const writeIgnoreList = async (ignoredDependencyUpdates: Record<string, string>) => {
174+
const currentList = readIgnoreList();
175+
176+
const newList = {
177+
...currentList,
178+
...ignoredDependencyUpdates,
179+
};
180+
181+
await Configuration.updateConfiguration(configuration.startingCwd, {
182+
upgradeInteractiveIgnoredVersions: Object.entries(newList)
183+
.map(([packageName, versionRange]) => `${packageName}:${versionRange}`),
184+
});
185+
};
186+
146187
const Prompt = () => {
147188
return (
148189
<Box flexDirection="row">
@@ -157,6 +198,11 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
157198
Press <Text bold color="cyanBright">{`<left>`}</Text>/<Text bold color="cyanBright">{`<right>`}</Text> to select versions.
158199
</Text>
159200
</Box>
201+
<Box marginLeft={1}>
202+
<Text>
203+
Press <Text bold color="cyanBright">{`<i>`}</Text> to toggle ignore mode.
204+
</Text>
205+
</Box>
160206
</Box>
161207
<Box flexDirection="column">
162208
<Box marginLeft={1}>
@@ -191,9 +237,37 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
191237

192238
const UpgradeEntry = ({active, descriptor, suggestions}: {active: boolean, descriptor: Descriptor, suggestions: Array<UpgradeSuggestion>}) => {
193239
const [action, setAction] = useMinistore<string | null>(descriptor.descriptorHash, null);
240+
const [isIgnoreMode, setIgnoreMode] = useState(false);
194241

195242
const packageIdentifier = structUtils.stringifyIdent(descriptor);
196243
const padLength = Math.max(0, 45 - packageIdentifier.length);
244+
245+
useKeypress({active}, (ch, key) => {
246+
switch (key.name) {
247+
case `i`:
248+
setIgnoreMode(prevIsIgnore => {
249+
const nextIsIgnore = !prevIsIgnore;
250+
251+
if (action !== null) {
252+
const {version} = parseAction(action);
253+
setAction(formatAction({isIgnore: nextIsIgnore, version}));
254+
}
255+
256+
return nextIsIgnore;
257+
});
258+
}
259+
}, [action, setAction, setIgnoreMode]);
260+
261+
const onItemChange = useCallback((newAction: string | null) => {
262+
if (newAction === null) {
263+
setAction(null);
264+
} else {
265+
setAction(formatAction({isIgnore: isIgnoreMode, version: newAction}));
266+
}
267+
}, [isIgnoreMode, setAction]);
268+
269+
const {version: value} = action ? parseAction(action) : {version: null};
270+
197271
return <>
198272
<Box>
199273
<Box width={45}>
@@ -203,7 +277,15 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
203277
<Pad active={active} length={padLength}/>
204278
</Box>
205279
{suggestions !== null
206-
? <ItemOptions active={active} options={suggestions} value={action} skewer={true} onChange={setAction} sizes={[17, 17, 17]} />
280+
? <ItemOptions
281+
active={active}
282+
gemColor={isIgnoreMode ? `red` : undefined /* use default */}
283+
options={suggestions}
284+
value={value}
285+
skewer={true}
286+
onChange={onItemChange}
287+
sizes={[17, 17, 17]}
288+
/>
207289
: <Box marginLeft={2}><Text color="gray">Fetching suggestions...</Text></Box>
208290
}
209291
</Box>
@@ -221,12 +303,28 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
221303
});
222304

223305
useEffect(() => {
306+
const ignoreList = readIgnoreList();
307+
224308
Promise.all(dependencies.map(descriptor => fetchSuggestions(descriptor)))
225309
.then(allSuggestions => {
226310
const mappedToSuggestions = dependencies.map((descriptor, i) => {
227311
const suggestionsForDescriptor = allSuggestions[i];
228312
return [descriptor, suggestionsForDescriptor] as const;
229-
}).filter(([_, suggestions]) => suggestions.length > 1);
313+
}).filter(([_, suggestions]) => suggestions.length > 1)
314+
.filter(([{scope, name}, suggestions]) => {
315+
const ignoredVersionRange = ignoreList[scope ? `@${scope}/${name}` : name];
316+
317+
if (!ignoredVersionRange)
318+
return true;
319+
320+
// The latest version is always the last one in the array
321+
const latestVersion = suggestions[suggestions.length - 1]?.value?.replace(`^`, ``);
322+
if (!latestVersion)
323+
return true;
324+
325+
// If the latest version satisfies the ignore range, we filter out the dependency
326+
return !semver.satisfies(latestVersion, ignoredVersionRange);
327+
});
230328

231329
if (mountedRef.current) {
232330
setSuggestions(mappedToSuggestions);
@@ -272,24 +370,38 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
272370
if (typeof updateRequests === `undefined`)
273371
return 1;
274372

275-
let hasChanged = false;
373+
let shouldInstall = false;
374+
let shouldUpdateIgnoreList = false;
375+
376+
const ignoredDependencyUpdates: Record<string, string> = {};
276377

277378
for (const workspace of project.workspaces) {
278379
for (const dependencyType of [`dependencies`, `devDependencies`] as Array<HardDependencies>) {
279380
const dependencies = workspace.manifest[dependencyType];
280381

281382
for (const descriptor of dependencies.values()) {
282-
const newRange = updateRequests.get(descriptor.descriptorHash);
283-
284-
if (typeof newRange !== `undefined` && newRange !== null) {
285-
dependencies.set(descriptor.identHash, structUtils.makeDescriptor(descriptor, newRange));
286-
hasChanged = true;
383+
const action = updateRequests.get(descriptor.descriptorHash);
384+
385+
if (typeof action !== `undefined` && action !== null) {
386+
const {isIgnore, version: newRange} = parseAction(action);
387+
388+
if (isIgnore) {
389+
const key = descriptor.scope ? `@${descriptor.scope}/${descriptor.name}` : descriptor.name;
390+
ignoredDependencyUpdates[key] = newRange;
391+
shouldUpdateIgnoreList = true;
392+
} else {
393+
dependencies.set(descriptor.identHash, structUtils.makeDescriptor(descriptor, newRange));
394+
shouldInstall = true;
395+
}
287396
}
288397
}
289398
}
290399
}
291400

292-
if (!hasChanged)
401+
if (shouldUpdateIgnoreList)
402+
await writeIgnoreList(ignoredDependencyUpdates);
403+
404+
if (!shouldInstall)
293405
return 0;
294406

295407
const installReport = await StreamReport.start({

packages/yarnpkg-core/sources/Configuration.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,14 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} =
471471
},
472472
},
473473
},
474+
475+
// Settings related to plugins
476+
upgradeInteractiveIgnoredVersions: {
477+
description: `List of versions ignored by the upgrade-interactive command`,
478+
type: SettingsType.STRING,
479+
default: [],
480+
isArray: true,
481+
},
474482
};
475483

476484
/**
@@ -546,6 +554,9 @@ export interface ConfigurationValueMap {
546554
peerDependencies?: Map<string, string>,
547555
peerDependenciesMeta?: Map<string, miscUtils.ToMapValue<{optional?: boolean}>>,
548556
}>>;
557+
558+
// Settings related to plugins
559+
upgradeInteractiveIgnoredVersions: Array<string>;
549560
}
550561

551562
export type PackageExtensionData = miscUtils.MapValueToObjectValue<miscUtils.MapValue<ConfigurationValueMap['packageExtensions']>>;
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import {Text} from 'ink';
1+
import {Text, TextProps} from 'ink';
22
import React, {memo, useMemo} from 'react';
33

44
export interface GemProps {
55
active: boolean
6+
activeColor?: TextProps['color']
67
}
7-
export const Gem: React.FC<GemProps> = memo(({active}) => {
8+
export const Gem: React.FC<GemProps> = memo(({active, activeColor = `green`}) => {
89
const text = useMemo(() => active ? `◉` : `◯`, [active]);
9-
const color = useMemo(() => active ? `green` : `yellow`, [active]);
10+
const color = useMemo(() => active ? activeColor : `yellow`, [active, activeColor]);
1011
return <Text color={color}>{text}</Text>;
1112
});

packages/yarnpkg-libui/sources/components/ItemOptions.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import {Pad} from './Pad';
88

99
export const ItemOptions = function <T>({
1010
active,
11+
gemColor,
1112
skewer,
1213
options,
1314
value,
1415
onChange,
1516
sizes = [],
1617
}: {
1718
active: boolean,
19+
gemColor: string | undefined;
1820
skewer?: boolean,
1921
options: Array<{value: T, label: string}>,
2022
value: T,
@@ -56,7 +58,7 @@ export const ItemOptions = function <T>({
5658
return (
5759
<Box key={label} width={boxWidth} marginLeft={1}>
5860
<Text wrap={`truncate`}>
59-
<Gem active={isGemActive} />{` `}{label}
61+
<Gem active={isGemActive} activeColor={gemColor} />{` `}{label}
6062
</Text>
6163
{skewer ? <Pad active={active} length={padWidth}/> : null}
6264
</Box>

0 commit comments

Comments
 (0)