feat: skip package manager prompt when nothing needs installing - #1195
feat: skip package manager prompt when nothing needs installing#1195jycouet wants to merge 3 commits into
Conversation
|
Install the latest version of pnpm add https://pkg.svelte.dev/sv/c/57085d36e0863c0b5711cc05237e2355f1285307Open in |
🦋 Changeset detectedLatest commit: 57085d3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
e072bab to
a883eb7
Compare
|
Darn, I was working on the issue too, but yours look better. |
| let packageManager: AgentName | null | undefined = null; | ||
| if (options.install !== false && installNeeded) { | ||
| packageManager = | ||
| options.install === true ? await packageManagerPrompt(options.cwd) : options.install; | ||
| } |
There was a problem hiding this comment.
| let packageManager: AgentName | null | undefined = null; | |
| if (options.install !== false && installNeeded) { | |
| packageManager = | |
| options.install === true ? await packageManagerPrompt(options.cwd) : options.install; | |
| } | |
| let packageManager: AgentName | null | undefined = null; | |
| if (options.install === true) { | |
| // prompt user when `package.json` has changed | |
| if (installNeeded) { | |
| packageManager = await packageManagerPrompt(options.cwd); | |
| } | |
| } else if (options.install === false) { | |
| // user choose not to install | |
| } else { | |
| packageManager = options.install; | |
| } |
| // still format when the install was skipped only because no new dependency was added | ||
| if (packageManager || (!installNeeded && options.install !== false)) { |
There was a problem hiding this comment.
really hard to understand this condition
| /** | ||
| * Returns `true` when every version matching `range` also matches `target`, | ||
| * e.g. `^9.2.0` is within `^9.0.0` but `^8.0.0` is not. | ||
| * Unparseable inputs (`latest`, `workspace:*`, ...) return `false`. | ||
| */ | ||
| export function isRangeWithin(range: string, target: string): boolean { | ||
| try { | ||
| return isRangeSubset(range, target); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
the param names are a bit too ambiguous. i think sticking with isRangeSubset's naming makes the most sense.
| /** | |
| * Returns `true` when every version matching `range` also matches `target`, | |
| * e.g. `^9.2.0` is within `^9.0.0` but `^8.0.0` is not. | |
| * Unparseable inputs (`latest`, `workspace:*`, ...) return `false`. | |
| */ | |
| export function isRangeWithin(range: string, target: string): boolean { | |
| try { | |
| return isRangeSubset(range, target); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Returns `true` when every version matching `subset` also matches `superset`, | |
| * e.g. `^9.2.0` is within `^9.0.0` but `^8.0.0` is not. | |
| * Unparseable inputs (`latest`, `workspace:*`, ...) return `false`. | |
| */ | |
| export function isRangeWithin(subset: string, superset: string): boolean { | |
| try { | |
| return isRangeSubset(subset, superset); | |
| } catch { | |
| return false; | |
| } | |
| } |
| export function isVersionUnsupportedBelow( | ||
| versionStr: string, | ||
| belowStr: string |
There was a problem hiding this comment.
drive-by change
| export function isVersionUnsupportedBelow( | |
| versionStr: string, | |
| belowStr: string | |
| export function isVersionUnsupportedBelow( | |
| version: string, | |
| below: string |
| describe('isRangeWithin', () => { | ||
| const combinations = [ | ||
| { range: '^9.0.0', target: '^9.0.0', expected: true }, | ||
| { range: '^9.2.0', target: '^9.0.0', expected: true }, | ||
| { range: '9.2.0', target: '^9.0.0', expected: true }, | ||
| { range: '^9.0.0', target: '^9.2.0', expected: false }, | ||
| { range: '^8.0.0', target: '^9.0.0', expected: false }, | ||
| { range: '*', target: '^9.0.0', expected: false }, | ||
| { range: 'latest', target: '^9.0.0', expected: false }, | ||
| { range: 'workspace:^9.0.0', target: '^9.0.0', expected: false } | ||
| ] as const; | ||
| it.each(combinations)( | ||
| '($range within $target) should be $expected', | ||
| ({ range, target, expected }) => { | ||
| expect(isRangeWithin(range, target)).toEqual(expected); | ||
| } | ||
| ); | ||
| }); |
There was a problem hiding this comment.
...continues the optional name change
| describe('isRangeWithin', () => { | |
| const combinations = [ | |
| { range: '^9.0.0', target: '^9.0.0', expected: true }, | |
| { range: '^9.2.0', target: '^9.0.0', expected: true }, | |
| { range: '9.2.0', target: '^9.0.0', expected: true }, | |
| { range: '^9.0.0', target: '^9.2.0', expected: false }, | |
| { range: '^8.0.0', target: '^9.0.0', expected: false }, | |
| { range: '*', target: '^9.0.0', expected: false }, | |
| { range: 'latest', target: '^9.0.0', expected: false }, | |
| { range: 'workspace:^9.0.0', target: '^9.0.0', expected: false } | |
| ] as const; | |
| it.each(combinations)( | |
| '($range within $target) should be $expected', | |
| ({ range, target, expected }) => { | |
| expect(isRangeWithin(range, target)).toEqual(expected); | |
| } | |
| ); | |
| }); | |
| describe('isRangeWithin', () => { | |
| const combinations = [ | |
| { subset: '^9.0.0', superset: '^9.0.0', expected: true }, | |
| { subset: '^9.2.0', superset: '^9.0.0', expected: true }, | |
| { subset: '9.2.0', superset: '^9.0.0', expected: true }, | |
| { subset: '^9.0.0', superset: '^9.2.0', expected: false }, | |
| { subset: '^8.0.0', superset: '^9.0.0', expected: false }, | |
| { subset: '*', superset: '^9.0.0', expected: false }, | |
| { subset: 'latest', superset: '^9.0.0', expected: false }, | |
| { subset: 'workspace:^9.0.0', superset: '^9.0.0', expected: false } | |
| ] as const; | |
| it.each(combinations)( | |
| '($subset within $superset) should be $expected', | |
| ({ subset, superset, expected }) => { | |
| expect(isRangeWithin(subset, superset)).toEqual(expected); | |
| } | |
| ); | |
| }); |
| /** Returns `true` when `package.json` gained or widened a dependency, i.e. an install is required. */ | ||
| function updatePackages( | ||
| dependencies: Array<{ pkg: string; version: string; dev: boolean }>, | ||
| sv: SvApi | ||
| ) { | ||
| if (dependencies.length === 0) return; | ||
| ): boolean { | ||
| if (dependencies.length === 0) return false; | ||
|
|
||
| let installNeeded = false; | ||
| const pkgPath = filePaths.packageJson; |
There was a problem hiding this comment.
this is very much a nitpick, but returning an object should remove the need for the comment here
| /** Returns `true` when `package.json` gained or widened a dependency, i.e. an install is required. */ | |
| function updatePackages( | |
| dependencies: Array<{ pkg: string; version: string; dev: boolean }>, | |
| sv: SvApi | |
| ) { | |
| if (dependencies.length === 0) return; | |
| ): boolean { | |
| if (dependencies.length === 0) return false; | |
| let installNeeded = false; | |
| const pkgPath = filePaths.packageJson; | |
| /** Returns `true` when `package.json` gained or widened a dependency, i.e. an install is required. */ | |
| function updatePackages( | |
| dependencies: Array<{ pkg: string; version: string; dev: boolean }>, | |
| sv: SvApi | |
| ): { installNeeded: boolean } { | |
| let installNeeded = false; | |
| if (dependencies.length === 0) return { installNeeded }; | |
| const pkgPath = filePaths.packageJson; |
| }) | ||
| ); | ||
|
|
||
| return installNeeded; |
There was a problem hiding this comment.
a follow-up from the previous suggestion
| return installNeeded; | |
| return { installNeeded }; |
| sv, | ||
| finalize: () => { | ||
| updatePackages(dependencies, sv); | ||
| const installNeeded = updatePackages(dependencies, sv); |
There was a problem hiding this comment.
another follow-up
| const installNeeded = updatePackages(dependencies, sv); | |
| const { installNeeded } = updatePackages(dependencies, sv); |
| if (packageManager) { | ||
| workspace.packageManager = packageManager; | ||
| await installDependencies(packageManager, options.cwd); | ||
| } | ||
|
|
||
| // still format when the install was skipped only because no new dependency was added | ||
| if (packageManager || (!installNeeded && options.install !== false)) { | ||
| await formatFiles({ | ||
| packageManager, | ||
| packageManager: packageManager ?? workspace.packageManager, | ||
| cwd: options.cwd, | ||
| filesToFormat, | ||
| strategy: 'files-only' |
There was a problem hiding this comment.
im not exactly sure when this changed, but iirc, we used to guard formatting files behind whether we ran the deps install ourselves because we couldn't guarantee that the formatter was installed locally. if it wasn't, then it would result in an error when attempting to format.
I think that seems to be the case now. for example:
- create a new project
- install prettier and eslint with their respective add-ons
- delete node_modules
- try running the eslint add-on
Closes #1088
sv addnow only asks which package manager to use when an add-on actually adds or widens a dependency inpackage.json(e.g.sv add mcp=ide:claude-code+setup:remoteneeds nothing installed). Files are still formatted when the install is skipped for that reason.Adds
isRangeWithinto@sveltejs/sv-utils, so an existing stricter range (^9.2.0vs a requested^9.0.0) is kept and doesn't trigger an install.