Skip to content

Commit b87b4cc

Browse files
committed
feat(create): add selected-from breadcrumb + two more org snap-tests
When a manifest entry resolves to a downstream template (npm, github, builtin, local), print `selected '<entry.name>' from <manifest.packageName>` as an info line before dispatching. Gives users context when the downstream runner later fails — e.g. the referenced `@org/template-web` is missing — so the chain that produced the failure is visible instead of just the terminal runner's stderr. Mirrors the RFC Error Handling table's "Chosen template fails to resolve" row. Two new snap-tests: - `create-org-invalid-manifest`: mock serves a manifest with an entry missing the required `template` field. Verifies the schema error surfaces with the offending field path and the process exits 1 without silently falling through. - `create-org-monorepo-filter`: fixture is a minimal pnpm workspace (package.json + pnpm-workspace.yaml). Mock serves a manifest mixing `monorepo: true` and regular entries. Verifies the `--no-interactive` table omits the monorepo-only row and appends the "(omitted 1 monorepo-only entry because this workspace is already a monorepo)" footer. Also syncs the root README's `@org` template paragraph (the packages/cli README is regenerated from root by build-ts' syncReadmeFromRoot step). All 4 org snap-tests byte-stable across re-runs; 263 unit tests pass.
1 parent 76896d8 commit b87b4cc

13 files changed

Lines changed: 287 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ vp create
152152

153153
You can run `vp create` inside of a project to add new apps or libraries to your project.
154154

155+
Organizations can expose a curated set of templates under their npm scope by
156+
publishing `@org/create` with a `vp.templates` manifest in its `package.json`.
157+
Once published, `vp create @org` opens an interactive picker over those
158+
templates, and setting `create: { defaultTemplate: '@org' }` in
159+
`vite.config.ts` makes it the default for bare `vp create`. See the
160+
[Organization Templates guide](https://viteplus.dev/guide/create#organization-templates)
161+
for the authoring workflow and
162+
[`create.defaultTemplate`](https://viteplus.dev/config/create) for the
163+
config reference.
164+
155165
### Migrating an existing project
156166

157167
You can migrate an existing project to Vite+:

packages/cli/snap-tests/create-org-bundled/snap.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
> node mock-server.mjs -- vp create @nkzw/demo --no-interactive --directory my-demo-app # bundled template: extract tarball, copy subdir
2+
3+
selected 'demo' from @nkzw/create
24
◇ Scaffolded my-demo-app
35
• Node <semver> pnpm <semver>
46
→ Next: cd my-demo-app && vp run
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"@nkzw/create": {
3+
"name": "@nkzw/create",
4+
"dist-tags": { "latest": "1.0.0" },
5+
"versions": {
6+
"1.0.0": {
7+
"version": "1.0.0",
8+
"dist": {
9+
"tarball": "{REGISTRY}/@nkzw/create/-/create-1.0.0.tgz",
10+
"integrity": "sha512-fake"
11+
},
12+
"vp": {
13+
"templates": [
14+
{
15+
"name": "broken-entry",
16+
"description": "Entry missing the required template field"
17+
}
18+
]
19+
}
20+
}
21+
}
22+
}
23+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Minimal mock npm registry used by `create-org-*` snap-tests.
2+
//
3+
// Reads `./mock-manifest.json` (keyed by URL path, e.g. `"@nkzw/create"`) and
4+
// optionally serves `.tgz` tarballs from `./tarballs/<name>`. Picks an
5+
// ephemeral port, sets `NPM_CONFIG_REGISTRY` on the child environment, spawns
6+
// the wrapped command, and tears down when the child exits.
7+
//
8+
// Usage: node mock-server.mjs -- <command> [args...]
9+
10+
import { spawn } from 'node:child_process';
11+
import { readFileSync } from 'node:fs';
12+
import { createServer } from 'node:http';
13+
import path from 'node:path';
14+
15+
const manifest = JSON.parse(readFileSync('./mock-manifest.json', 'utf-8'));
16+
const UPSTREAM_REGISTRY = 'https://registry.npmjs.org';
17+
18+
function rewriteRegistry(value, registry) {
19+
return JSON.parse(JSON.stringify(value).replaceAll('{REGISTRY}', registry));
20+
}
21+
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);
35+
res.end(`proxy error: ${error.message}`);
36+
}
37+
}
38+
39+
const server = createServer(async (req, res) => {
40+
const key = decodeURIComponent(req.url ?? '/').replace(/^\/+/, '');
41+
if (Object.hasOwn(manifest, key)) {
42+
const address = server.address();
43+
const registry =
44+
address && typeof address !== 'string' ? `http://127.0.0.1:${address.port}` : '';
45+
res.writeHead(200, { 'content-type': 'application/json' });
46+
res.end(JSON.stringify(rewriteRegistry(manifest[key], registry)));
47+
return;
48+
}
49+
const tarMatch = key.match(/\/-\/([^/]+\.tgz)$/);
50+
if (tarMatch) {
51+
try {
52+
const bytes = readFileSync(path.resolve('./tarballs', tarMatch[1]));
53+
res.writeHead(200, { 'content-type': 'application/octet-stream' });
54+
res.end(bytes);
55+
return;
56+
} catch {
57+
// fall through to proxy
58+
}
59+
}
60+
// Proxy anything we don't mock (pnpm/latest, tarball downloads for real
61+
// packages, etc.) to the upstream registry. Keeps the fixture scoped to
62+
// just the @org/create manifest while letting vp's other startup work
63+
// (package-manager download) succeed normally.
64+
await proxyToUpstream(req, res);
65+
});
66+
67+
server.listen(0, '127.0.0.1', () => {
68+
const address = server.address();
69+
if (!address || typeof address === 'string') {
70+
console.error('mock-server: failed to bind');
71+
process.exit(1);
72+
}
73+
const registry = `http://127.0.0.1:${address.port}`;
74+
const separatorIndex = process.argv.indexOf('--');
75+
if (separatorIndex === -1) {
76+
console.error('usage: node mock-server.mjs -- <command> [args...]');
77+
server.close(() => process.exit(2));
78+
return;
79+
}
80+
const [cmd, ...args] = process.argv.slice(separatorIndex + 1);
81+
const child = spawn(cmd, args, {
82+
env: { ...process.env, NPM_CONFIG_REGISTRY: registry },
83+
stdio: 'inherit',
84+
});
85+
child.on('exit', (code, signal) => {
86+
const exitCode = code ?? (signal ? 128 : 0);
87+
server.close(() => process.exit(exitCode));
88+
});
89+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[1]> node mock-server.mjs -- vp create @nkzw --no-interactive # invalid manifest -> schema error
2+
3+
@nkzw/create: vp.templates[0].template must be a non-empty string
4+
Failed to resolve org template manifest
5+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"commands": [
3+
"node mock-server.mjs -- vp create @nkzw --no-interactive # invalid manifest -> schema error"
4+
]
5+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"@nkzw/create": {
3+
"name": "@nkzw/create",
4+
"dist-tags": { "latest": "1.0.0" },
5+
"versions": {
6+
"1.0.0": {
7+
"version": "1.0.0",
8+
"dist": {
9+
"tarball": "{REGISTRY}/@nkzw/create/-/create-1.0.0.tgz",
10+
"integrity": "sha512-fake"
11+
},
12+
"vp": {
13+
"templates": [
14+
{
15+
"name": "monorepo",
16+
"description": "Full Nakazawa monorepo scaffold",
17+
"template": "@nkzw/template-monorepo",
18+
"monorepo": true
19+
},
20+
{
21+
"name": "web",
22+
"description": "Web app template (Vite + React)",
23+
"template": "@nkzw/template-web"
24+
},
25+
{
26+
"name": "library",
27+
"description": "TypeScript library template",
28+
"template": "@nkzw/template-library"
29+
}
30+
]
31+
}
32+
}
33+
}
34+
}
35+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Minimal mock npm registry used by `create-org-*` snap-tests.
2+
//
3+
// Reads `./mock-manifest.json` (keyed by URL path, e.g. `"@nkzw/create"`) and
4+
// optionally serves `.tgz` tarballs from `./tarballs/<name>`. Picks an
5+
// ephemeral port, sets `NPM_CONFIG_REGISTRY` on the child environment, spawns
6+
// the wrapped command, and tears down when the child exits.
7+
//
8+
// Usage: node mock-server.mjs -- <command> [args...]
9+
10+
import { spawn } from 'node:child_process';
11+
import { readFileSync } from 'node:fs';
12+
import { createServer } from 'node:http';
13+
import path from 'node:path';
14+
15+
const manifest = JSON.parse(readFileSync('./mock-manifest.json', 'utf-8'));
16+
const UPSTREAM_REGISTRY = 'https://registry.npmjs.org';
17+
18+
function rewriteRegistry(value, registry) {
19+
return JSON.parse(JSON.stringify(value).replaceAll('{REGISTRY}', registry));
20+
}
21+
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);
35+
res.end(`proxy error: ${error.message}`);
36+
}
37+
}
38+
39+
const server = createServer(async (req, res) => {
40+
const key = decodeURIComponent(req.url ?? '/').replace(/^\/+/, '');
41+
if (Object.hasOwn(manifest, key)) {
42+
const address = server.address();
43+
const registry =
44+
address && typeof address !== 'string' ? `http://127.0.0.1:${address.port}` : '';
45+
res.writeHead(200, { 'content-type': 'application/json' });
46+
res.end(JSON.stringify(rewriteRegistry(manifest[key], registry)));
47+
return;
48+
}
49+
const tarMatch = key.match(/\/-\/([^/]+\.tgz)$/);
50+
if (tarMatch) {
51+
try {
52+
const bytes = readFileSync(path.resolve('./tarballs', tarMatch[1]));
53+
res.writeHead(200, { 'content-type': 'application/octet-stream' });
54+
res.end(bytes);
55+
return;
56+
} catch {
57+
// fall through to proxy
58+
}
59+
}
60+
// Proxy anything we don't mock (pnpm/latest, tarball downloads for real
61+
// packages, etc.) to the upstream registry. Keeps the fixture scoped to
62+
// just the @org/create manifest while letting vp's other startup work
63+
// (package-manager download) succeed normally.
64+
await proxyToUpstream(req, res);
65+
});
66+
67+
server.listen(0, '127.0.0.1', () => {
68+
const address = server.address();
69+
if (!address || typeof address === 'string') {
70+
console.error('mock-server: failed to bind');
71+
process.exit(1);
72+
}
73+
const registry = `http://127.0.0.1:${address.port}`;
74+
const separatorIndex = process.argv.indexOf('--');
75+
if (separatorIndex === -1) {
76+
console.error('usage: node mock-server.mjs -- <command> [args...]');
77+
server.close(() => process.exit(2));
78+
return;
79+
}
80+
const [cmd, ...args] = process.argv.slice(separatorIndex + 1);
81+
const child = spawn(cmd, args, {
82+
env: { ...process.env, NPM_CONFIG_REGISTRY: registry },
83+
stdio: 'inherit',
84+
});
85+
child.on('exit', (code, signal) => {
86+
const exitCode = code ?? (signal ? 128 : 0);
87+
server.close(() => process.exit(exitCode));
88+
});
89+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "monorepo-filter-fixture",
3+
"private": true,
4+
"packageManager": "pnpm@10.0.0"
5+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- packages/*

0 commit comments

Comments
 (0)