Skip to content

Commit 88b3334

Browse files
committed
feat(plugin-interactive-tools): support ignore lists
1 parent 63a77b5 commit 88b3334

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+)?)$/;
@@ -140,6 +141,46 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
140141
return suggestions;
141142
};
142143

144+
const formatAction = ({isIgnore, version}: {isIgnore: boolean; version: string}) => {
145+
return `${isIgnore ? `ignore` : `update`}__${version}`;
146+
};
147+
148+
const parseAction = (action: string) => {
149+
const [command, version] = action.split(`__`);
150+
151+
return {
152+
isIgnore: command === `ignore`,
153+
version,
154+
};
155+
};
156+
157+
const readIgnoreList = () => {
158+
return configuration.get(`upgradeInteractiveIgnoredVersions`).reduce((allVersions, versionString) => {
159+
const [packageName, versionRange = `*`] = versionString.split(`:`);
160+
161+
if (!packageName) return allVersions;
162+
163+
return {
164+
...allVersions,
165+
[packageName]: versionRange,
166+
};
167+
}, {} as Record<string, string>);
168+
};
169+
170+
const writeIgnoreList = async (ignoredDependencyUpdates: Record<string, string>) => {
171+
const currentList = readIgnoreList();
172+
173+
const newList = {
174+
...currentList,
175+
...ignoredDependencyUpdates,
176+
};
177+
178+
await Configuration.updateConfiguration(configuration.startingCwd, {
179+
upgradeInteractiveIgnoredVersions: Object.entries(newList)
180+
.map(([packageName, versionRange]) => `${packageName}:${versionRange}`),
181+
});
182+
};
183+
143184
const Prompt = () => {
144185
return (
145186
<Box flexDirection="row">
@@ -154,6 +195,11 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
154195
Press <Text bold color="cyanBright">{`<left>`}</Text>/<Text bold color="cyanBright">{`<right>`}</Text> to select versions.
155196
</Text>
156197
</Box>
198+
<Box marginLeft={1}>
199+
<Text>
200+
Press <Text bold color="cyanBright">{`<i>`}</Text> to toggle ignore mode.
201+
</Text>
202+
</Box>
157203
</Box>
158204
<Box flexDirection="column">
159205
<Box marginLeft={1}>
@@ -188,9 +234,37 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
188234

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

192239
const packageIdentifier = structUtils.stringifyIdent(descriptor);
193240
const padLength = Math.max(0, 45 - packageIdentifier.length);
241+
242+
useKeypress({active}, (ch, key) => {
243+
switch (key.name) {
244+
case `i`:
245+
setIgnoreMode(prevIsIgnore => {
246+
const nextIsIgnore = !prevIsIgnore;
247+
248+
if (action !== null) {
249+
const {version} = parseAction(action);
250+
setAction(formatAction({isIgnore: nextIsIgnore, version}));
251+
}
252+
253+
return nextIsIgnore;
254+
});
255+
}
256+
}, [action, setAction, setIgnoreMode]);
257+
258+
const onItemChange = useCallback((newAction: string | null) => {
259+
if (newAction === null) {
260+
setAction(null);
261+
} else {
262+
setAction(formatAction({isIgnore: isIgnoreMode, version: newAction}));
263+
}
264+
}, [isIgnoreMode, setAction]);
265+
266+
const {version: value} = action ? parseAction(action) : {version: null};
267+
194268
return <>
195269
<Box>
196270
<Box width={45}>
@@ -200,7 +274,15 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
200274
<Pad active={active} length={padLength}/>
201275
</Box>
202276
{suggestions !== null
203-
? <ItemOptions active={active} options={suggestions} value={action} skewer={true} onChange={setAction} sizes={[17, 17, 17]} />
277+
? <ItemOptions
278+
active={active}
279+
gemColor={isIgnoreMode ? `red` : undefined /* use default */}
280+
options={suggestions}
281+
value={value}
282+
skewer={true}
283+
onChange={onItemChange}
284+
sizes={[17, 17, 17]}
285+
/>
204286
: <Box marginLeft={2}><Text color="gray">Fetching suggestions...</Text></Box>
205287
}
206288
</Box>
@@ -218,12 +300,28 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
218300
});
219301

220302
useEffect(() => {
303+
const ignoreList = readIgnoreList();
304+
221305
Promise.all(dependencies.map(descriptor => fetchSuggestions(descriptor)))
222306
.then(allSuggestions => {
223307
const mappedToSuggestions = dependencies.map((descriptor, i) => {
224308
const suggestionsForDescriptor = allSuggestions[i];
225309
return [descriptor, suggestionsForDescriptor] as const;
226-
}).filter(([_, suggestions]) => suggestions.length > 1);
310+
}).filter(([_, suggestions]) => suggestions.length > 1)
311+
.filter(([{scope, name}, suggestions]) => {
312+
const ignoredVersionRange = ignoreList[scope ? `@${scope}/${name}` : name];
313+
314+
if (!ignoredVersionRange)
315+
return true;
316+
317+
// The latest version is always the last one in the array
318+
const latestVersion = suggestions[suggestions.length - 1]?.value?.replace(`^`, ``);
319+
if (!latestVersion)
320+
return true;
321+
322+
// If the latest version satisfies the ignore range, we filter out the dependency
323+
return !semver.satisfies(latestVersion, ignoredVersionRange);
324+
});
227325

228326
if (mountedRef.current) {
229327
setSuggestions(mappedToSuggestions);
@@ -269,24 +367,38 @@ export default class UpgradeInteractiveCommand extends BaseCommand {
269367
if (typeof updateRequests === `undefined`)
270368
return 1;
271369

272-
let hasChanged = false;
370+
let shouldInstall = false;
371+
let shouldUpdateIgnoreList = false;
372+
373+
const ignoredDependencyUpdates: Record<string, string> = {};
273374

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

278379
for (const descriptor of dependencies.values()) {
279-
const newRange = updateRequests.get(descriptor.descriptorHash);
280-
281-
if (typeof newRange !== `undefined` && newRange !== null) {
282-
dependencies.set(descriptor.identHash, structUtils.makeDescriptor(descriptor, newRange));
283-
hasChanged = true;
380+
const action = updateRequests.get(descriptor.descriptorHash);
381+
382+
if (typeof action !== `undefined` && action !== null) {
383+
const {isIgnore, version: newRange} = parseAction(action);
384+
385+
if (isIgnore) {
386+
const key = descriptor.scope ? `@${descriptor.scope}/${descriptor.name}` : descriptor.name;
387+
ignoredDependencyUpdates[key] = newRange;
388+
shouldUpdateIgnoreList = true;
389+
} else {
390+
dependencies.set(descriptor.identHash, structUtils.makeDescriptor(descriptor, newRange));
391+
shouldInstall = true;
392+
}
284393
}
285394
}
286395
}
287396
}
288397

289-
if (!hasChanged)
398+
if (shouldUpdateIgnoreList)
399+
await writeIgnoreList(ignoredDependencyUpdates);
400+
401+
if (!shouldInstall)
290402
return 0;
291403

292404
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
/**
@@ -547,6 +555,9 @@ export interface ConfigurationValueMap {
547555
peerDependencies?: Map<string, string>,
548556
peerDependenciesMeta?: Map<string, miscUtils.ToMapValue<{optional?: boolean}>>,
549557
}>>;
558+
559+
// Settings related to plugins
560+
upgradeInteractiveIgnoredVersions: Array<string>;
550561
}
551562

552563
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)