Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ No packages with unreviewed install scripts.
**Exit code:** 1

```
note: npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. Enforcement is planned for a future npm release.
note: npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. npm 12 enforces the policy.
npm error code ENOMATCH
npm error No installed packages match: esbuild
npm error A complete log of this run can be found in: <home>/.npm/_logs/<timestamp>-debug-0.log
Expand All @@ -50,7 +50,7 @@ deny-only -> npm deny-scripts core-js (advisory note)
**Exit code:** 1

```
note: npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. Enforcement is planned for a future npm release.
note: npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. npm 12 enforces the policy.
npm error code ENOMATCH
npm error No installed packages match: core-js
npm error A complete log of this run can be found in: <home>/.npm/_logs/<timestamp>-debug-0.log
Expand Down Expand Up @@ -81,6 +81,6 @@ Pass package names as positionals (`vp pm approve-builds <pkg>...`), not after `
-> npm approve-scripts --all (advisory note)

```
note: npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. Enforcement is planned for a future npm release.
note: npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. npm 12 enforces the policy.
No packages with unreviewed install scripts.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "command-pm-approve-builds-npm12",
"version": "1.0.0",
"private": true,
"packageManager": "npm@12.0.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[case]]
name = "command_pm_approve_builds_npm12"
vp = "global"
skip-platforms = ["windows"]
steps = [
{ argv = ["vp", "pm", "approve-builds"], comment = "no args -> npm approve-scripts --allow-scripts-pending (lists pending)", continue-on-failure = true },
{ argv = ["vp", "pm", "approve-builds", "esbuild"], comment = "-> npm approve-scripts esbuild (npm 12 enforces allowScripts, so vp points at vp pm rebuild)", continue-on-failure = true },
{ argv = ["vp", "pm", "approve-builds", "!core-js"], comment = "deny-only -> npm deny-scripts core-js (denial keeps the enforced default, no note)", continue-on-failure = true },
{ argv = ["vp", "pm", "approve-builds", "esbuild", "!core-js"], comment = "mixed approve+deny -> rejected, exit non-zero", continue-on-failure = true },
{ argv = ["vp", "pm", "approve-builds", "--all"], comment = "-> npm approve-scripts --all (rebuild note)", continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# command_pm_approve_builds_npm12

## `vp pm approve-builds`

no args -> npm approve-scripts --allow-scripts-pending (lists pending)

```
No packages with unreviewed install scripts.
```

## `vp pm approve-builds esbuild`

-> npm approve-scripts esbuild (npm 12 enforces allowScripts, so vp points at vp pm rebuild)

**Exit code:** 1

```
note: npm records the approval in the `allowScripts` field of package.json but does not run scripts a previous install skipped. Run `vp pm rebuild <package>` to execute them.
npm error code ENOMATCH
npm error No installed packages match: esbuild
npm error A complete log of this run can be found in: <home>/.npm/_logs/<timestamp>-debug-0.log
```

## `vp pm approve-builds !core-js`

deny-only -> npm deny-scripts core-js (denial keeps the enforced default, no note)

**Exit code:** 1

```
npm error code ENOMATCH
npm error No installed packages match: core-js
npm error A complete log of this run can be found in: <home>/.npm/_logs/<timestamp>-debug-0.log
```

## `vp pm approve-builds esbuild !core-js`

mixed approve+deny -> rejected, exit non-zero

**Exit code:** 1

```
npm manages approvals and denials separately. Run them as two invocations, e.g. `vp pm approve-builds <approve-pkg>...` then `vp pm approve-builds !<deny-pkg>...`.
```

## `vp pm approve-builds --all`

-> npm approve-scripts --all (rebuild note)

```
note: npm records the approval in the `allowScripts` field of package.json but does not run scripts a previous install skipped. Run `vp pm rebuild <package>` to execute them.
No packages with unreviewed install scripts.
```
67 changes: 65 additions & 2 deletions crates/vite_pm_cli/src/resolution/commands/approve_builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::resolution::{
PackageManagerDialect, Pnpm, Resolve, Yarn,
};

const NPM_ADVISORY_NOTE: &str = "npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. Enforcement is planned for a future npm release.";
const NPM_ADVISORY_NOTE: &str = "npm's allowScripts policy is advisory in npm 11.x: install scripts still run; npm only warns about unreviewed packages at install time. npm 12 enforces the policy.";

const NPM_ENFORCED_NOTE: &str = "npm records the approval in the `allowScripts` field of package.json but does not run scripts a previous install skipped. Run `vp pm rebuild <package>` to execute them.";

#[pm_args]
#[derive(clap::Args, Clone, Debug, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -150,7 +152,18 @@ impl Resolve<ApproveBuildsArgs> for Npm {
}
}
if writes_policy {
diag.note(DiagnosticKind::BehaviorChange, NPM_ADVISORY_NOTE);
// npm 12 enforces allowScripts (skipped scripts stay skipped until a
// rebuild); 11.16 - 11.x only warn. An unknown version is treated as
// current, matching the version-gate default above.
if self.version().is_none_or(|version| version_satisfies(version, ">=12.0.0")) {
// An approval takes effect on the next rebuild; a denial keeps
// the enforced default and needs no follow-up.
if !has_denies {
diag.note(DiagnosticKind::BehaviorChange, NPM_ENFORCED_NOTE);
}
} else {
diag.note(DiagnosticKind::BehaviorChange, NPM_ADVISORY_NOTE);
}
}
cmd.extend(args.pass_through_args.iter());
cmd.into()
Expand Down Expand Up @@ -574,6 +587,56 @@ mod tests {
assert_eq!(resolution.diagnostics[0].message, NPM_ADVISORY_NOTE);
}

#[test]
fn npm_v12_approve_notes_rebuild() {
let resolution = resolve(
&npm("12.0.2"),
ApproveBuildsArgs { packages: vec!["esbuild".to_string()], ..Default::default() },
);
let CommandResolution::Run(command) = resolution.outcome else {
panic!("expected command resolution");
};

assert_eq!(command.args, vec!["approve-scripts", "esbuild"]);
assert_eq!(resolution.diagnostics[0].message, NPM_ENFORCED_NOTE);
}

#[test]
fn npm_v12_all_notes_rebuild() {
let resolution =
resolve(&npm("12.0.2"), ApproveBuildsArgs { all: true, ..Default::default() });
let CommandResolution::Run(command) = resolution.outcome else {
panic!("expected command resolution");
};

assert_eq!(command.args, vec!["approve-scripts", "--all"]);
assert_eq!(resolution.diagnostics[0].message, NPM_ENFORCED_NOTE);
}

#[test]
fn npm_v12_deny_has_no_note() {
let resolution = resolve(
&npm("12.0.2"),
ApproveBuildsArgs { packages: vec!["!core-js".to_string()], ..Default::default() },
);
let CommandResolution::Run(command) = resolution.outcome else {
panic!("expected command resolution");
};

assert_eq!(command.args, vec!["deny-scripts", "core-js"]);
assert!(resolution.diagnostics.is_empty());
}

#[test]
fn npm_unknown_version_notes_rebuild() {
let resolution = resolve(
&Npm::unknown_version(),
ApproveBuildsArgs { packages: vec!["esbuild".to_string()], ..Default::default() },
);

assert_eq!(resolution.diagnostics[0].message, NPM_ENFORCED_NOTE);
}

#[test]
fn npm_v11_16_mixed_rejected() {
let resolution = resolve(
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ Run `vp create --list` to see the built-in templates and the common shorthand te

### Dependency build scripts

For security, pnpm, bun, and yarn (Berry) do not run a dependency's build scripts (`install` / `postinstall`, e.g. native builds like `better-sqlite3`) until you approve them. When a template adds such a dependency directly, `vp create` surfaces it after installing instead of leaving the project in a half-built state:
For security, pnpm, bun, yarn (Berry), and npm (v12+) do not run a dependency's build scripts (`install` / `postinstall`, e.g. native builds like `better-sqlite3`) until you approve them. When a template adds such a dependency directly, `vp create` surfaces it after installing instead of leaving the project in a half-built state:

- Interactive: you are asked which of those dependencies to approve and build (nothing is selected by default).
- Non-interactive: a note lists them and points at `vp pm approve-builds`.
- `--approve-builds`: approves and builds them automatically, so non-interactive runs (CI) can produce a ready-to-use project.

Approval is recorded the way each package manager expects: pnpm's `allowBuilds`, bun's `trustedDependencies`, or yarn's `dependenciesMeta.<pkg>.built` (in the workspace root manifest). Transitive build scripts you did not choose (e.g. `esbuild` pulled in by Vite) are left at the package manager's defaults and are not surfaced. npm runs build scripts by default, so there is nothing to approve there.
Approval is recorded the way each package manager expects: pnpm's `allowBuilds`, bun's `trustedDependencies`, npm's `allowScripts`, or yarn's `dependenciesMeta.<pkg>.built` (in the workspace root manifest). Transitive build scripts you did not choose (e.g. `esbuild` pulled in by Vite) are left at the package manager's defaults and are not surfaced. npm 11 and older run build scripts during install, so there is nothing to approve there.

## Template Options

Expand Down
13 changes: 13 additions & 0 deletions docs/guide/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ vp rebuild -- --update-binary

With pnpm v10+, bare `vp rebuild` only rebuilds packages whose build scripts are listed in `onlyBuiltDependencies` (or approved via `pnpm approve-builds`); name the package explicitly to force a rebuild that bypasses the approval gate.

#### Dependency build scripts (npm v12+)

npm v12 skips dependency install scripts (`preinstall` / `install` / `postinstall`, including implicit `node-gyp` builds) unless the `allowScripts` field in package.json covers them; the install succeeds and npm warns about what it skipped. `vp pm approve-builds` manages that allowlist:

- `vp pm approve-builds <pkg...>` approves the named packages (`npm approve-scripts`)
- `vp pm approve-builds !<pkg...>` denies them (`npm deny-scripts`)
- `vp pm approve-builds --all` approves everything currently pending
- `vp pm approve-builds` lists the packages whose scripts are not yet covered

Approval only records the allowlist: scripts an earlier install skipped do not run until you run `vp rebuild <pkg>`. With npm 11.16 - 11.x the same commands work, but npm treats the allowlist as advisory and still runs scripts.

npm v12 also stops resolving git dependencies (`github:`, `git+https:`) and remote tarball URLs by default; such installs fail with `EALLOWGIT` / `EALLOWREMOTE`. Opt back in per project with npm's `allow-git` / `allow-remote` config.

#### Advanced

Use these when you need lower-level package-manager behavior.
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/create/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,8 +947,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
};

// After a successful install, surface gated build scripts (native builds like
// better-sqlite3 the template added as a direct dependency) that pnpm or bun
// blocked, and let the user approve them. `projectPath` is the created package
// better-sqlite3 the template added as a direct dependency) that the package
// manager blocked, and let the user approve them. `projectPath` is the created package
// whose direct deps decide what is worth prompting for; `installCwd` is where
// the package manager (and `node_modules`) lives.
// Gated builds reported by the ESLint/Prettier migration pre-install. yarn
Expand Down
81 changes: 77 additions & 4 deletions packages/cli/src/utils/__tests__/approve-builds.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseBunUntrusted,
parseIgnoredBuilds,
parseInstallGatedBuilds,
parseNpmBlockedScripts,
parseYarnDisabledBuilds,
pnpmSupportsPositionalApprove,
resolveApproveBuildTargets,
Expand Down Expand Up @@ -179,6 +180,62 @@ describe('parseYarnDisabledBuilds', () => {
});
});

describe('parseNpmBlockedScripts', () => {
it('parses the npm 12 blocked-scripts warning from real install output', () => {
// Captured from `npm install esbuild core-js` with npm 12.0.2.
const output = [
'',
'added 3 packages in 466ms',
'npm warn install-scripts 2 packages had install scripts blocked because they are not covered by allowScripts:',
'npm warn install-scripts core-js@3.49.0 (postinstall: node -e "try{require(\'./postinstall\')}catch(e){}")',
'npm warn install-scripts esbuild@0.28.1 (postinstall: node install.js)',
'npm warn install-scripts',
'npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.',
].join('\n');
expect(parseNpmBlockedScripts(output)).toEqual(['core-js', 'esbuild']);
});

it('parses the singular one-package form', () => {
const output = [
'npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:',
'npm warn install-scripts fsevents@2.3.3 (install: node-gyp rebuild)',
].join('\n');
expect(parseNpmBlockedScripts(output)).toEqual(['fsevents']);
});

it('parses scoped packages and dedupes multiple versions', () => {
const output = [
'npm warn install-scripts 3 packages had install scripts blocked because they are not covered by allowScripts:',
'npm warn install-scripts @scope/native@1.0.0 (preinstall: node setup.js)',
'npm warn install-scripts esbuild@0.25.0 (postinstall: node install.js)',
'npm warn install-scripts esbuild@0.28.1 (postinstall: node install.js)',
].join('\n');
expect(parseNpmBlockedScripts(output)).toEqual(['@scope/native', 'esbuild']);
});

it('returns [] for the npm 11.x advisory warning (scripts still ran)', () => {
// npm 11.16 - 11.x warn about unreviewed packages but run their scripts.
const output = [
'npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:',
'npm warn install-scripts esbuild@0.28.1 (postinstall: node install.js)',
].join('\n');
expect(parseNpmBlockedScripts(output)).toEqual([]);
});

it('returns [] for the npm rebuild summary warning (no package list)', () => {
expect(
parseNpmBlockedScripts(
'npm warn rebuild 1 package had install scripts blocked because they are not covered by allowScripts. Run `npm install-scripts ls` to review.',
),
).toEqual([]);
});

it('returns [] for a clean install log', () => {
expect(parseNpmBlockedScripts('added 87 packages in 39s')).toEqual([]);
expect(parseNpmBlockedScripts('')).toEqual([]);
});
});

describe('parseInstallGatedBuilds', () => {
it('dispatches to the pnpm parser for pnpm', () => {
expect(
Expand All @@ -198,9 +255,18 @@ describe('parseInstallGatedBuilds', () => {
).toEqual(['core-js']);
});

it('returns [] for bun/npm (not parsed from install output)', () => {
it('dispatches to the npm parser for npm', () => {
expect(
parseInstallGatedBuilds(
'npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:\n' +
'npm warn install-scripts better-sqlite3@11.0.0 (install: node-gyp rebuild)',
PackageManager.npm,
),
).toEqual(['better-sqlite3']);
});

it('returns [] for bun (not parsed from install output)', () => {
expect(parseInstallGatedBuilds('whatever', PackageManager.bun)).toEqual([]);
expect(parseInstallGatedBuilds('whatever', PackageManager.npm)).toEqual([]);
expect(parseInstallGatedBuilds('whatever', undefined)).toEqual([]);
});
});
Expand Down Expand Up @@ -299,9 +365,16 @@ describe('resolveApproveBuildTargets', () => {
]);
});

it('returns [] for package managers that do not gate builds', () => {
it('returns direct-dep build targets for npm', () => {
writePkg({ dependencies: { 'better-sqlite3': '^11.0.0' } });
expect(
resolveApproveBuildTargets(dir, ['better-sqlite3', 'esbuild'], PackageManager.npm),
).toEqual(['better-sqlite3']);
});

it('returns [] when the package manager is unknown', () => {
writePkg({ dependencies: { 'better-sqlite3': '^11.0.0' } });
expect(resolveApproveBuildTargets(dir, ['better-sqlite3'], PackageManager.npm)).toEqual([]);
expect(resolveApproveBuildTargets(dir, ['better-sqlite3'], undefined)).toEqual([]);
});

it('returns [] when there are no pending builds', () => {
Expand Down
Loading
Loading