Skip to content

Commit 82cb5af

Browse files
authored
Merge branch 'main' into deps/upstream-update
2 parents 099b3b1 + b8b73de commit 82cb5af

27 files changed

Lines changed: 1695 additions & 48 deletions

File tree

docs/guide/create.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,21 @@ Run `vp create --list` to see the built-in templates and the common shorthand te
5252
- `--hooks` enables pre-commit hook setup
5353
- `--no-hooks` skips hook setup
5454
- `--package-manager <name>` uses a specified package manager (`pnpm`, `npm`, `yarn`, or `bun`)
55+
- `--approve-builds` approves and runs gated dependency build scripts without prompting
5556
- `--no-interactive` runs without prompts
5657
- `--verbose` shows detailed scaffolding output
5758
- `--list` prints the available built-in and popular templates
5859

60+
### Dependency build scripts
61+
62+
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:
63+
64+
- Interactive: you are asked which of those dependencies to approve and build (nothing is selected by default).
65+
- Non-interactive: a note lists them and points at `vp pm approve-builds`.
66+
- `--approve-builds`: approves and builds them automatically, so non-interactive runs (CI) can produce a ready-to-use project.
67+
68+
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.
69+
5970
## Template Options
6071

6172
Arguments after `--` are passed directly to the selected template.

packages/cli/snap-tests-global/command-create-help/snap.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Options:
2424
--hooks Set up pre-commit hooks (default in non-interactive mode)
2525
--no-hooks Skip pre-commit hooks setup
2626
--package-manager NAME Use specified package manager (pnpm, npm, yarn, bun)
27+
--approve-builds Approve and run gated dependency build scripts without prompting
2728
--verbose Show detailed scaffolding output
2829
--no-interactive Run in non-interactive mode
2930
--list List all available templates
@@ -89,6 +90,7 @@ Options:
8990
--hooks Set up pre-commit hooks (default in non-interactive mode)
9091
--no-hooks Skip pre-commit hooks setup
9192
--package-manager NAME Use specified package manager (pnpm, npm, yarn, bun)
93+
--approve-builds Approve and run gated dependency build scripts without prompting
9294
--verbose Show detailed scaffolding output
9395
--no-interactive Run in non-interactive mode
9496
--list List all available templates
@@ -154,6 +156,7 @@ Options:
154156
--hooks Set up pre-commit hooks (default in non-interactive mode)
155157
--no-hooks Skip pre-commit hooks setup
156158
--package-manager NAME Use specified package manager (pnpm, npm, yarn, bun)
159+
--approve-builds Approve and run gated dependency build scripts without prompting
157160
--verbose Show detailed scaffolding output
158161
--no-interactive Run in non-interactive mode
159162
--list List all available templates

packages/cli/snap-tests-global/new-check/snap.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Options:
2424
--hooks Set up pre-commit hooks (default in non-interactive mode)
2525
--no-hooks Skip pre-commit hooks setup
2626
--package-manager NAME Use specified package manager (pnpm, npm, yarn, bun)
27+
--approve-builds Approve and run gated dependency build scripts without prompting
2728
--verbose Show detailed scaffolding output
2829
--no-interactive Run in non-interactive mode
2930
--list List all available templates

packages/cli/snap-tests/.shared/mock-npm-registry.mjs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { spawn } from 'node:child_process';
1111
import { readFileSync } from 'node:fs';
1212
import { createServer } from 'node:http';
13+
import { get as httpsGet } from 'node:https';
1314
import path from 'node:path';
1415

1516
const manifest = JSON.parse(readFileSync('./mock-manifest.json', 'utf-8'));
@@ -19,21 +20,52 @@ function rewriteRegistry(value, registry) {
1920
return JSON.parse(JSON.stringify(value).replaceAll('{REGISTRY}', registry));
2021
}
2122

22-
async function proxyToUpstream(req, res) {
23-
try {
24-
const upstream = await fetch(`${UPSTREAM_REGISTRY}${req.url ?? '/'}`, {
25-
method: req.method,
26-
headers: { accept: req.headers.accept ?? 'application/json' },
27-
});
28-
const body = Buffer.from(await upstream.arrayBuffer());
29-
res.writeHead(upstream.status, {
30-
'content-type': upstream.headers.get('content-type') ?? 'application/octet-stream',
31-
});
32-
res.end(body);
33-
} catch (error) {
34-
res.writeHead(502);
23+
// Stream the upstream response byte-for-byte. Unlike `fetch`, `https` does not
24+
// auto-decompress, so the tarball reaches the client exactly as the registry
25+
// served it (content-encoding and all). bun verifies tarball integrity and
26+
// rejects any re-encoded body, so faithful streaming is what lets bun installs
27+
// work through the proxy (pnpm fetches tarballs from the upstream URL directly,
28+
// so it was never affected).
29+
function proxyToUpstream(req, res) {
30+
// Stream errors can fire after headers are already sent (e.g. the upstream
31+
// connection resets mid-tarball), so guard against a second writeHead.
32+
const fail = (error) => {
33+
if (!res.headersSent) {
34+
res.writeHead(502);
35+
}
3536
res.end(`proxy error: ${error.message}`);
36-
}
37+
};
38+
const fetchUrl = (url, redirectsLeft) => {
39+
httpsGet(url, { headers: { accept: req.headers.accept ?? 'application/json' } }, (upstream) => {
40+
const status = upstream.statusCode ?? 502;
41+
if (status >= 300 && status < 400 && upstream.headers.location && redirectsLeft > 0) {
42+
// Draining can still emit 'error' (e.g. the socket resets mid-redirect),
43+
// so guard it here too — otherwise it's uncaught and crashes the server.
44+
upstream.on('error', fail);
45+
upstream.resume();
46+
fetchUrl(new URL(upstream.headers.location, url).toString(), redirectsLeft - 1);
47+
return;
48+
}
49+
const headers = {};
50+
// Forward `location` too, so a 3xx we stop following (or one with no
51+
// location to follow) still reaches the client with its redirect target.
52+
for (const name of ['content-type', 'content-encoding', 'content-length', 'location']) {
53+
if (upstream.headers[name] !== undefined) {
54+
headers[name] = upstream.headers[name];
55+
}
56+
}
57+
// Default a missing content-type (parity with the prior fetch-based proxy)
58+
// so clients that key off it still recognize a proxied tarball.
59+
headers['content-type'] ??= 'application/octet-stream';
60+
res.writeHead(status, headers);
61+
// `pipe` does not forward source errors, so listen on the response stream
62+
// directly; otherwise a mid-stream upstream error is uncaught and crashes
63+
// the mock server.
64+
upstream.on('error', fail);
65+
upstream.pipe(res);
66+
}).on('error', fail);
67+
};
68+
fetchUrl(`${UPSTREAM_REGISTRY}${req.url ?? '/'}`, 5);
3769
}
3870

3971
const server = createServer(async (req, res) => {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"@your-org/create": {
3+
"name": "@your-org/create",
4+
"dist-tags": {
5+
"latest": "1.0.0"
6+
},
7+
"versions": {
8+
"1.0.0": {
9+
"version": "1.0.0",
10+
"dist": {
11+
"tarball": "{REGISTRY}/@your-org/create/-/create-1.0.0.tgz",
12+
"integrity": "sha512-ixybtmdasQGKVYm7m33nUnA2N37/CLV5T1u2nXqCx63bFvQHBMh4Ql5vhvB6usf7TlM9v6qEimRnyEckO5jZ2w=="
13+
},
14+
"createConfig": {
15+
"templates": [
16+
{
17+
"name": "with-build-dep",
18+
"description": "Template with a dependency that has a gated build script",
19+
"template": "./templates/demo"
20+
}
21+
]
22+
}
23+
}
24+
}
25+
}
26+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
> node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:with-build-dep --no-interactive --approve-builds --package-manager bun --directory approved-app # --approve-builds runs `bun pm trust` for the gated build script (core-js)
2+
◇ Scaffolded approved-app
3+
• Node <semver> bun <semver>
4+
✓ Dependencies installed in <variable>ms
5+
→ Next: cd approved-app && vp run
6+
7+
> cat approved-app/package.json # core-js recorded under trustedDependencies
8+
{
9+
"name": "approved-app",
10+
"version": "0.0.0",
11+
"private": true,
12+
"scripts": {
13+
"prepare": "vp config"
14+
},
15+
"dependencies": {
16+
"core-js": "3.39.0"
17+
},
18+
"devDependencies": {
19+
"vite-plus": "latest"
20+
},
21+
"overrides": {
22+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
23+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
24+
},
25+
"devEngines": {
26+
"packageManager": {
27+
"name": "bun",
28+
"version": "<semver>",
29+
"onFail": "download"
30+
}
31+
},
32+
"trustedDependencies": [
33+
"core-js"
34+
]
35+
}
36+
37+
> node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:with-build-dep --no-interactive --package-manager bun --directory default-app # default run surfaces the gated build with guidance, leaving it untrusted
38+
39+
Build scripts were not run for: core-js.
40+
41+
These dependencies may not work until built. Run vp pm approve-builds core-js in the project to approve them, or re-create with --approve-builds.
42+
◇ Scaffolded default-app
43+
• Node <semver> bun <semver>
44+
✓ Dependencies installed in <variable>ms
45+
→ Next: cd default-app && vp run
46+
47+
> cat default-app/package.json # no trustedDependencies, the build was not run
48+
{
49+
"name": "default-app",
50+
"version": "0.0.0",
51+
"private": true,
52+
"scripts": {
53+
"prepare": "vp config"
54+
},
55+
"dependencies": {
56+
"core-js": "3.39.0"
57+
},
58+
"devDependencies": {
59+
"vite-plus": "latest"
60+
},
61+
"overrides": {
62+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
63+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
64+
},
65+
"devEngines": {
66+
"packageManager": {
67+
"name": "bun",
68+
"version": "<semver>",
69+
"onFail": "download"
70+
}
71+
}
72+
}
73+
74+
> cd default-app && vp pm approve-builds core-js # the guidance's `vp pm approve-builds` command approves the gated build
75+
bun pm trust v<semver> (<hash>)
76+
77+
./node_modules/core-js @<semver>
78+
✓ [postinstall]: node -e "try{require('./postinstall')}catch(e){}"
79+
80+
1 script ran across 1 package [<variable>ms]
81+
82+
> cat default-app/package.json # core-js is now recorded under trustedDependencies
83+
{
84+
"name": "default-app",
85+
"version": "0.0.0",
86+
"private": true,
87+
"scripts": {
88+
"prepare": "vp config"
89+
},
90+
"dependencies": {
91+
"core-js": "3.39.0"
92+
},
93+
"devDependencies": {
94+
"vite-plus": "latest"
95+
},
96+
"overrides": {
97+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
98+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
99+
},
100+
"devEngines": {
101+
"packageManager": {
102+
"name": "bun",
103+
"version": "<semver>",
104+
"onFail": "download"
105+
}
106+
},
107+
"trustedDependencies": [
108+
"core-js"
109+
]
110+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"ignoredPlatforms": ["win32"],
3+
"env": {
4+
"VP_SKIP_INSTALL": "",
5+
"CI": "",
6+
"ADBLOCK": "1"
7+
},
8+
"commands": [
9+
"node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:with-build-dep --no-interactive --approve-builds --package-manager bun --directory approved-app # --approve-builds runs `bun pm trust` for the gated build script (core-js)",
10+
"cat approved-app/package.json # core-js recorded under trustedDependencies",
11+
"node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:with-build-dep --no-interactive --package-manager bun --directory default-app # default run surfaces the gated build with guidance, leaving it untrusted",
12+
"cat default-app/package.json # no trustedDependencies, the build was not run",
13+
"cd default-app && vp pm approve-builds core-js # the guidance's `vp pm approve-builds` command approves the gated build",
14+
"cat default-app/package.json # core-js is now recorded under trustedDependencies"
15+
]
16+
}
Binary file not shown.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"@your-org/create": {
3+
"name": "@your-org/create",
4+
"dist-tags": { "latest": "1.0.0" },
5+
"versions": {
6+
"1.0.0": {
7+
"version": "1.0.0",
8+
"dist": {
9+
"tarball": "{REGISTRY}/@your-org/create/-/create-1.0.0.tgz",
10+
"integrity": "sha512-kS9rnZeli5CMs19BqGc+rxj1Tpb+an06y4gzorf7DOWNOPVTSLMiSl4V7LlFN9c1GApQ9ysu9ZHqOG3x1D8LfA=="
11+
},
12+
"createConfig": {
13+
"templates": [
14+
{
15+
"name": "with-build-dep",
16+
"description": "Template that ships prettier and a gated build dep",
17+
"template": "./templates/demo"
18+
}
19+
]
20+
}
21+
}
22+
}
23+
}
24+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
> node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:with-build-dep --no-interactive --approve-builds --directory approved-app # template ships Prettier, so create installs+migrates before the main install; the gated build (core-js) must still be surfaced and approved
2+
3+
Prettier detected in workspace packages but no root config found. Package-level Prettier must be migrated manually.
4+
◇ Scaffolded approved-app
5+
• Node <semver> pnpm <semver>
6+
✓ Dependencies installed in <variable>ms
7+
→ Next: cd approved-app && vp run
8+
9+
> cat approved-app/pnpm-workspace.yaml # approval recorded under allowBuilds despite the migration pre-install
10+
allowBuilds:
11+
core-js: true
12+
catalog:
13+
vite: npm:@voidzero-dev/vite-plus-core@latest
14+
vitest: npm:@voidzero-dev/vite-plus-test@latest
15+
vite-plus: latest
16+
overrides:
17+
vite: "catalog:"
18+
vitest: "catalog:"
19+
peerDependencyRules:
20+
allowAny:
21+
- vite
22+
- vitest
23+
allowedVersions:
24+
vite: "*"
25+
vitest: "*"
26+
27+
> node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:with-build-dep --no-interactive --directory default-app # default run surfaces the gated build with guidance, leaving it unapproved
28+
29+
Prettier detected in workspace packages but no root config found. Package-level Prettier must be migrated manually.
30+
31+
Build scripts were not run for: core-js.
32+
33+
These dependencies may not work until built. Run vp pm approve-builds in the project to approve them, or re-create with --approve-builds.
34+
◇ Scaffolded default-app
35+
• Node <semver> pnpm <semver>
36+
✓ Dependencies installed in <variable>ms
37+
→ Next: cd default-app && vp run
38+
39+
> cat default-app/pnpm-workspace.yaml # no allowBuilds, the build was not run
40+
allowBuilds:
41+
core-js: set this to true or false
42+
catalog:
43+
vite: npm:@voidzero-dev/vite-plus-core@latest
44+
vitest: npm:@voidzero-dev/vite-plus-test@latest
45+
vite-plus: latest
46+
overrides:
47+
vite: "catalog:"
48+
vitest: "catalog:"
49+
peerDependencyRules:
50+
allowAny:
51+
- vite
52+
- vitest
53+
allowedVersions:
54+
vite: "*"
55+
vitest: "*"
56+
57+
> cd default-app && vp pm approve-builds core-js # the guidance's `vp pm approve-builds` command approves the gated build
58+
.../core-js@<semver>/node_modules/core-js postinstall$ node -e "try{require('./postinstall')}catch(e){}"
59+
.../core-js@<semver>/node_modules/core-js postinstall: Done
60+
61+
> cat default-app/pnpm-workspace.yaml # core-js is now allowed under allowBuilds
62+
allowBuilds:
63+
core-js: true
64+
catalog:
65+
vite: npm:@voidzero-dev/vite-plus-core@latest
66+
vitest: npm:@voidzero-dev/vite-plus-test@latest
67+
vite-plus: latest
68+
overrides:
69+
vite: "catalog:"
70+
vitest: "catalog:"
71+
peerDependencyRules:
72+
allowAny:
73+
- vite
74+
- vitest
75+
allowedVersions:
76+
vite: "*"
77+
vitest: "*"

0 commit comments

Comments
 (0)