Skip to content

feat(create): support @org default templates#1398

Open
fengmk2 wants to merge 45 commits intomainfrom
vp-create-support-org
Open

feat(create): support @org default templates#1398
fengmk2 wants to merge 45 commits intomainfrom
vp-create-support-org

Conversation

@fengmk2
Copy link
Copy Markdown
Member

@fengmk2 fengmk2 commented Apr 16, 2026

Extends vp create so an organization can expose a curated set of templates through a single entry point. vp create @your-org fetches @your-org/create's vp.templates manifest from npm and renders an interactive picker over the listed entries. A create.defaultTemplate option in vite.config.ts makes an org the default for bare vp create. Zero-risk for orgs without a manifest — vp falls back to running @your-org/create as today.

Highlights

  • vp create @org → picker (with a Vite+ built-in templates escape hatch that stays reachable).
  • vp create @org/<name> → direct selection; falls back to the existing @org/create-<name> shorthand on miss.
  • vp create --no-interactive inside a repo with create.defaultTemplate set → prints the manifest as a machine-parseable table and exits 1.
  • Manifest entries accept npm specifiers, GitHub URLs, vite:* builtins, local workspace packages, or bundled subdirectories (./templates/foo extracted from the @org/create tarball with SHA-512 integrity into $VP_HOME/tmp/create-org/...).
  • monorepo: true entries are hidden in existing monorepos; direct selection refused with the same message as vite:monorepo.

Spec: rfcs/create-org-default-templates.md (4 review rounds). Docs: docs/guide/create.md § Organization Templates + new docs/config/create.md.

Testing: 264 unit tests pass, 8 snap-test fixtures under packages/cli/snap-tests/create-org-* (shared mock-registry helper at .shared/mock-npm-registry.mjs).


Note

Medium Risk
Adds new network-facing template discovery (registry fetch + optional tarball download/extraction) and new config-driven behavior for vp create, which can affect scaffolding flows and security boundaries around path/tar handling despite added validation and tests.

Overview
vp create now supports organization-scoped template catalogs: vp create @org fetches @org/create’s createConfig.templates manifest, shows an interactive picker (with a built-in-templates escape hatch), and @org:name[@version] directly selects a manifest entry.

Adds per-repo defaults via vite.config.ts (create.defaultTemplate), applies correct precedence (CLI arg > config > built-in picker), and extends --no-interactive behavior to print a stable manifest table + exit 1 when an org scope needs a selection.

Implements bundled templates referenced by relative paths (e.g. ./templates/demo) by downloading and extracting the @org/create tarball into a cache, copying the selected subdirectory, validating manifests/paths, filtering monorepo: true entries inside monorepos, and improving npm registry/auth handling via new .npmrc-aware npm-config utilities. Updates CLI help snapshots, adds extensive unit + snap-test fixtures (including a mock npm registry), updates docs/site nav, and introduces nanotar dependency.

Reviewed by Cursor Bugbot for commit 428a4b4. Configure here.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 16, 2026

Deploy Preview for viteplus-preview canceled.

Name Link
🔨 Latest commit 6f04410
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/69ec3b1d537e870008c355bb

@fengmk2 fengmk2 self-assigned this Apr 16, 2026
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 16, 2026


How to use the Graphite Merge Queue

Add the label auto-merge to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment thread rfcs/create-org-default-templates.md
fengmk2 added a commit that referenced this pull request Apr 16, 2026
When `create.defaultTemplate: '@org'` is set, bare `vp create` now drops
into the org picker with a trailing "Vite+ built-in templates" escape-hatch
entry that routes to the existing `getInitialTemplateOptions` flow. This
preserves interactive discovery of `vite:monorepo` / `vite:application` /
`vite:library` / `vite:generator` without adding a new CLI flag; explicit
specifiers (e.g. `vp create vite:application`) remain the scripted bypass.

Addresses @cpojer's review feedback on #1398.
Comment thread rfcs/create-org-default-templates.md Outdated
fengmk2 added a commit that referenced this pull request Apr 23, 2026
When `create.defaultTemplate: '@org'` is set, bare `vp create` now drops
into the org picker with a trailing "Vite+ built-in templates" escape-hatch
entry that routes to the existing `getInitialTemplateOptions` flow. This
preserves interactive discovery of `vite:monorepo` / `vite:application` /
`vite:library` / `vite:generator` without adding a new CLI flag; explicit
specifiers (e.g. `vp create vite:application`) remain the scripted bypass.

Addresses @cpojer's review feedback on #1398.
fengmk2 added a commit that referenced this pull request Apr 23, 2026
Let manifest entries use relative paths (`./templates/demo`) that resolve
against the enclosing @org/create package root, so orgs can ship one
package containing N templates instead of publishing N @org/template-*
packages — the pattern already used by create-vite, create-next-app, and
most enterprise scaffolding kits.

Vite+ fetches the tarball URL already returned by the registry manifest,
extracts it once per version into `$VP_HOME/tmp/create-org/<scope>/<name>/<version>/`,
and scaffolds by directory copy. Paths that escape the package root are
rejected at schema-validation time.

Addresses @fengmk2's review feedback on #1398 asking whether
`./templates/demo`-style bundled paths would be supported.
@fengmk2 fengmk2 force-pushed the vp-create-support-org branch from d3c56db to 6d1e8f3 Compare April 23, 2026 07:11
Comment thread docs/guide/create.md Outdated
Comment thread docs/guide/create.md Outdated
fengmk2 added a commit that referenced this pull request Apr 23, 2026
Two inline review findings from @fengmk2 on PR #1398:

- Bundled subdirectory section was leaking implementation details
  (tarball download mechanics, SHA-512 integrity verification, cache
  path under $VP_HOME, NPM_CONFIG_REGISTRY handling). Users just need
  to know that `./...` paths resolve against the @org/create package
  root and that the directory is copied verbatim. The rest belongs in
  the RFC.

- Non-interactive inspection section explained the design rationale
  for keeping the output machine-parseable ("so scripts and AI agents
  can recover the list without a separate `--list` flag"). That's an
  RFC-level decision; users just need to see the shape of the output.
@fengmk2
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 23, 2026

@cursor review

Comment thread packages/cli/src/create/bin.ts
fengmk2 added a commit that referenced this pull request Apr 23, 2026
The early-exit "template name is required" guard fired before
`getConfiguredDefaultTemplate` ran, so `vp create --no-interactive`
in a repo with `create.defaultTemplate: '@your-org'` exited with a
generic "template name required" message instead of reading the
configured default. The documented precedence (CLI arg >
create.defaultTemplate > interactive picker) was silently broken for
all non-interactive / CI usage.

Fix: move the guard to after the full resolution chain
(CLI arg → create.defaultTemplate → @org manifest). Only error if
`selectedTemplateName` is still empty once every input source has had
a chance to fill it. If the configured default is an @org scope, the
manifest resolver's own `--no-interactive` branch prints the manifest
table — exactly what a user expects when the config was supposed to
pick the org.

New snap-test `create-org-config-default` locks in the fix: a fixture
with `create: { defaultTemplate: '@your-org' }` in vite.config.ts
runs bare `vp create --no-interactive` and verifies the @your-org
manifest table prints instead of the generic missing-argument error.

Reported by Cursor Bugbot on #1398.
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 23, 2026

@cursor review

Comment thread packages/cli/src/create/org-resolve.ts Outdated
@fengmk2
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 23, 2026

Running effect

image image image

Inside a monorepo
image

fengmk2 added a commit that referenced this pull request Apr 23, 2026
When `create.defaultTemplate: '@org'` is set, bare `vp create` now drops
into the org picker with a trailing "Vite+ built-in templates" escape-hatch
entry that routes to the existing `getInitialTemplateOptions` flow. This
preserves interactive discovery of `vite:monorepo` / `vite:application` /
`vite:library` / `vite:generator` without adding a new CLI flag; explicit
specifiers (e.g. `vp create vite:application`) remain the scripted bypass.

Addresses @cpojer's review feedback on #1398.
fengmk2 added a commit that referenced this pull request Apr 23, 2026
Let manifest entries use relative paths (`./templates/demo`) that resolve
against the enclosing @org/create package root, so orgs can ship one
package containing N templates instead of publishing N @org/template-*
packages — the pattern already used by create-vite, create-next-app, and
most enterprise scaffolding kits.

Vite+ fetches the tarball URL already returned by the registry manifest,
extracts it once per version into `$VP_HOME/tmp/create-org/<scope>/<name>/<version>/`,
and scaffolds by directory copy. Paths that escape the package root are
rejected at schema-validation time.

Addresses @fengmk2's review feedback on #1398 asking whether
`./templates/demo`-style bundled paths would be supported.
fengmk2 added a commit that referenced this pull request Apr 23, 2026
Two inline review findings from @fengmk2 on PR #1398:

- Bundled subdirectory section was leaking implementation details
  (tarball download mechanics, SHA-512 integrity verification, cache
  path under $VP_HOME, NPM_CONFIG_REGISTRY handling). Users just need
  to know that `./...` paths resolve against the @org/create package
  root and that the directory is copied verbatim. The rest belongs in
  the RFC.

- Non-interactive inspection section explained the design rationale
  for keeping the output machine-parseable ("so scripts and AI agents
  can recover the list without a separate `--list` flag"). That's an
  RFC-level decision; users just need to see the shape of the output.
fengmk2 added a commit that referenced this pull request Apr 23, 2026
The early-exit "template name is required" guard fired before
`getConfiguredDefaultTemplate` ran, so `vp create --no-interactive`
in a repo with `create.defaultTemplate: '@your-org'` exited with a
generic "template name required" message instead of reading the
configured default. The documented precedence (CLI arg >
create.defaultTemplate > interactive picker) was silently broken for
all non-interactive / CI usage.

Fix: move the guard to after the full resolution chain
(CLI arg → create.defaultTemplate → @org manifest). Only error if
`selectedTemplateName` is still empty once every input source has had
a chance to fill it. If the configured default is an @org scope, the
manifest resolver's own `--no-interactive` branch prints the manifest
table — exactly what a user expects when the config was supposed to
pick the org.

New snap-test `create-org-config-default` locks in the fix: a fixture
with `create: { defaultTemplate: '@your-org' }` in vite.config.ts
runs bare `vp create --no-interactive` and verifies the @your-org
manifest table prints instead of the generic missing-argument error.

Reported by Cursor Bugbot on #1398.
fengmk2 added a commit that referenced this pull request Apr 23, 2026
When a user types `vp create @org` expecting the picker but
`@org/create` has no `vp.templates` manifest, emit a one-line
`info:` note before falling through to run `@org/create` as a normal
package:

  No `vp.templates` manifest in @org/create — running it as a normal package.

Without this, a later `ERR_PNPM_DLX_NO_BIN` (the typical failure when
`@org/create` is data-only, as with `@eggjs/create`) looks mysterious
— the user has no way to know the picker was attempted and skipped.

Triggered only for scope-only input (`vp create @org`). The
per-entry form (`vp create @org/name`) stays silent because it's
ambiguous — the user might have genuinely wanted `@org/create-name`
via the existing shorthand.

Also helps diagnose registry-mirror sync lag: if a user's mirror
hasn't synced a just-published manifest, they now see "no manifest"
instead of the downstream dlx failure.

Reported by @fengmk2 on #1398 after `vp create @eggjs` in a monorepo
showed the monorepo parent-dir prompt and then `pnpm dlx @eggjs/create`
without ever running the picker.
fengmk2 added a commit that referenced this pull request Apr 23, 2026
Manifest entries like `{ template: '@your-org/template-web' }` were
passing through `discoverTemplate` → `expandCreateShorthand` and being
rewritten into `@your-org/create-template-web` — because the shorthand
rule targets any `@scope/name` that doesn't already start with `create-`.
The manifest author's literal package name was silently transformed,
causing vp to fetch the wrong package.

Fix: thread a `skipShorthand` flag through `discoverTemplate` that short-
circuits the final `expandCreateShorthand` call. Set it when
`resolveOrgManifestForCreate` returns `kind: 'replaced'` (the path
responsible for manifest-resolved specifiers). The earlier branches in
`discoverTemplate` (`vite:`, GitHub URLs, local workspace packages)
still match first, so manifest entries using those specifier kinds
behave unchanged.

Unit test added to `discovery.spec.ts` pinning both halves of the
contract: `@your-org/template-web` stays literal with the flag set,
and still expands via shorthand without it (the existing behavior
for non-manifest inputs).

Reported by Cursor Bugbot on #1398.
@fengmk2 fengmk2 force-pushed the vp-create-support-org branch from 764dd2d to f4ed12f Compare April 23, 2026 14:32
@fengmk2
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 23, 2026

@cursor review

@fengmk2 fengmk2 changed the title docs(rfc): add RFC for @org default templates in vp create feat(create): support @org default templates via vp.templates manifest Apr 23, 2026
Comment thread packages/cli/src/create/bin.ts
@cpojer
Copy link
Copy Markdown
Member

cpojer commented Apr 24, 2026

It works:

CleanShot 2026-04-24 at 10 12 42@2x

When an entry is selected, it prints "selected 'web' from ankzw/create", but it shows the selection above. Can we get rid of that line?
CleanShot 2026-04-24 at 10 12 32@2x

I also noticed something I missed previously, this PR now supports something like vp create @nkzw/web which will pick the web template from @nkzw/create. I don't think we should support this "fake npm package" syntax, that's confusing and might create issues, like what if a real @nkzw/web package exists. If we want to support this shorthand, we need to come up with an unambiguous syntax, maybe @nkzw#web or @nkzw:web or similar.

@fengmk2
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 24, 2026

I also noticed something I missed previously, this PR now supports something like vp create @nkzw/web which will pick the web template from @nkzw/create. I don't think we should support this "fake npm package" syntax, that's confusing and might create issues, like what if a real @nkzw/web package exists. If we want to support this shorthand, we need to come up with an unambiguous syntax, maybe @nkzw#web or @nkzw:web or similar.

I would also be confused about this. Let's change it to the @nkzw:web syntax instead. This way the final command will be vp create @nkzw:web, which is consistent with the current built-in template syntax vp create vite:monorepo. @cpojer

fengmk2 added 21 commits April 25, 2026 11:08
Manifest entries like `{ template: '@your-org/template-web' }` were
passing through `discoverTemplate` → `expandCreateShorthand` and being
rewritten into `@your-org/create-template-web` — because the shorthand
rule targets any `@scope/name` that doesn't already start with `create-`.
The manifest author's literal package name was silently transformed,
causing vp to fetch the wrong package.

Fix: thread a `skipShorthand` flag through `discoverTemplate` that short-
circuits the final `expandCreateShorthand` call. Set it when
`resolveOrgManifestForCreate` returns `kind: 'replaced'` (the path
responsible for manifest-resolved specifiers). The earlier branches in
`discoverTemplate` (`vite:`, GitHub URLs, local workspace packages)
still match first, so manifest entries using those specifier kinds
behave unchanged.

Unit test added to `discovery.spec.ts` pinning both halves of the
contract: `@your-org/template-web` stays literal with the flag set,
and still expands via shorthand without it (the existing behavior
for non-manifest inputs).

Reported by Cursor Bugbot on #1398.
When the `--directory` guard was widened from `!isBuiltinTemplate` to
`!isDirectScaffoldTemplate` (to accept bundled @org templates alongside
builtins), the error text was left behind and still said "only available
for builtin templates". Users hitting it today are told --directory
doesn't work with bundled templates, even though it does.

Now reads: "only available for builtin and bundled @org templates".

Reported by Cursor Bugbot on #1398.
BREAKING (unpublished feature): the org-create manifest now lives at
`createConfig.templates` in `@org/create`'s package.json, replacing the
Vite+-specific `vp.templates` location.

Rationale (fengmk2 + cpojer, PR #1398 review): the manifest metadata is
tool-neutral (names, descriptions, template specifiers) and belongs under
a neutral field name. Following the precedent of npm's `publishConfig`,
`createConfig` reads as "configuration used specifically when this
package is consumed as a create initializer", leaving room for other
scaffolding tools to adopt the same convention later. Vite+-specific
behavior, if/when it's needed, can sit at `createConfig.vp.*` without
further schema changes.

Schema shape unchanged (name / description / template / keywords /
monorepo). Validation error messages now reference
`createConfig.templates[...]` instead of `vp.templates[...]`.

Updated: org-manifest reader and schema validator, RegistryVersionMeta
type, org-resolve info note, bin.ts help text, define-config JSDoc,
RFC manifest schema + field reference + resolution flow sections,
docs guide + config pages, root README, all 7 snap-test mock manifests
(and the bundled-fixture tarball + integrity hash), and the unit-test
regex that asserts the error path name.
BREAKING (unpublished feature): the manifest-entry selector uses `:`
instead of `/`. `vp create @your-org:web` now picks the `web` entry from
`@your-org/create`'s `createConfig.templates` manifest. The old
`@your-org/web` form is no longer a manifest lookup — it reverts to the
pre-feature `expandCreateShorthand` path that turns it into
`@your-org/create-web`, exactly as it worked before this feature.

Rationale (cpojer + fengmk2, PR #1398 review): the `/` form collides
with real npm packages. If `@your-org/web` exists on npm as a genuine
package, `vp create @your-org/web` would silently prefer the manifest
entry over the real package — confusing, and fragile. The `:` separator
mirrors the existing `vite:monorepo` / `vite:library` builtin-template
syntax, which users already read as "namespace:entry". It also keeps
manifest entries lexically distinct from any real npm package path.

Miss behavior: `vp create @org:foo` where `foo` isn't in the manifest
(or the manifest is absent) is a HARD ERROR listing the available
entries. No silent fall-through — the `:` form is an explicit manifest
lookup, and the fallback shorthand is reserved for the `/` form.

Updated: `parseOrgScopedSpec` parses only `@scope` and `@scope:name`
(returns `null` for `@scope/anything`), `resolveOrgManifestForCreate`
errors on direct-selection miss, CLI help text, RFC command matrix +
examples + error-handling table + compatibility section, docs guide +
config page, snap-test fixture commands (`create-org-bundled`,
`create-org-monorepo-direct-in-monorepo`), `--no-interactive` hint line,
and unit tests for `parseOrgScopedSpec` (new test pinning the null
return for the `/` form).
Removed the `prompts.log.info('selected ... from ...')` line that used
to precede every manifest-driven template resolution. Rationale
(cpojer, PR #1398): the clack picker already prints the selected entry
above in its completed-prompt line, so the info note was a redundant
restatement of what the user just picked a moment before.

Impact on downstream failure context (the original reason the
breadcrumb was added, via the Cursor "chosen template fails to resolve
with context" finding): for the interactive path, the picker's own
completed-prompt line still shows the selection. For the
non-interactive `@org:name` path, the error surfaces from the
downstream runner — no vp-side framing. If that proves confusing in
practice, we can add the breadcrumb back only in failure paths.

Snap regenerated for `create-org-bundled` (the only fixture that
captured the line). 265 unit tests still pass.
- org-resolve: replace `prompts.log.error + process.exit(1)` pairs with
  `cancelAndExit(msg, 1)` for a consistent exit style; route the inline
  `templates.filter((t) => !(t.monorepo && isMonorepo))` sites through the
  existing `filterManifestForContext` helper; drop the pointless dynamic
  `await import('../resolve-vite-config.ts')` since `findViteConfigUp` was
  already statically imported from the same module.
- OrgResolution: drop the unused `entry` field from `replaced`/`bundled`
  variants.
- org-tarball: stream the response body and enforce the 50 MB cap during
  read; a malicious 1 GB tarball is now rejected before being fully
  buffered. Short-circuit early via Content-Length when the server
  advertises one.
- bin / discovery: trim narrating comments that restated the code; keep
  the `@org/template-web` vs `create-template-web` note since it
  explains a non-obvious invariant.
…dirs

PR review follow-ups for the org tarball extractor:

- bugbot TOCTOU race (#3135845042): the final
  `rename(stagingDir → destDir)` now catches ENOTEMPTY/EEXIST/EPERM. A
  losing concurrent `vp create @org:<name>` drops its staging tree and
  returns the already-populated cache dir instead of surfacing an
  unhandled error.
- staging-dir accumulation (#3135903176): `cleanupStaleStagingDirs`
  runs before each fresh extract and removes sibling `<destDir>.tmp-*`
  trees older than 24 hours. The mtime gate keeps concurrent live
  extractions safe — a staging dir that's currently being written is
  always younger than the cutoff.
- hoist `path.resolve(stagingDir)` out of the per-entry loop
  (#3135922205) — no behavior change, one resolve per extract.
- document why non-`package/` tar roots are accepted (#3135908121)
  with a source reference to the npm-pack convention.
`@org/create` is a new feature — we only need to support the tarball
layout produced by `npm pack`, which always wraps every entry under
`package/`. Drop the accept-anything fallback; entries outside
`package/` are silently ignored alongside the existing PaxHeader /
root-directory skips.
Simplify-round follow-ups:

- drop `EPERM` from the rename-race allowlist and require
  `destDir/package.json` to actually exist before treating the rename
  failure as a won-race: permission errors (SIP, read-only FS,
  cross-device) now surface instead of being silently swallowed.
- trim the `normalizeEntryName` JSDoc to one sentence; export it and
  `cleanupStaleStagingDirs` for direct unit testing.
- add 9 tests covering the `package/` prefix strip, skip paths
  (PaxHeader, root dir, outside `package/`), and the 24-hour mtime
  gate in `cleanupStaleStagingDirs` (stale deleted, fresh preserved,
  unrelated siblings untouched, missing parent tolerated).
…orepo

Address Codex review follow-ups for the org-template flow (exec-bit
finding ignored per the user):

- Pinned versions: parseOrgScopedSpec now returns the optional `version`
  suffix and readOrgManifest resolves it against `versions[...]` /
  `dist-tags[...]`, throwing loudly when the requested version is unknown.
  `vp create @your-org@1.2.3` and `vp create @your-org:web@next` no
  longer silently scaffold `dist-tags.latest`.
- npm registry / auth: getNpmRegistry(scope?) reads cwd-walked and user
  `.npmrc` files plus `npm_config_*` env vars, honoring
  `@your-org:registry=...` scope mappings. getNpmAuthHeader parses
  `//host[/path]/:_authToken`, `:_auth`, and username/password pairs.
  The packument fetch and tarball download retry with auth only when
  the server returns 401/403 — public registries never see a token.
- Bundled monorepo entries: `OrgResolution.bundled` now carries the
  `monorepo` flag. bin.ts routes bundled monorepo templates through the
  same post-processing as the `vite:monorepo` builtin (rewriteMonorepo,
  agent instructions, hooks, install, fmt, summary) instead of the
  standalone path.
- Docs: mention version pinning and `.npmrc` support in the guide.
- Tests: +5 new scenarios for version pinning, scope-specific registry,
  auth retry, and no-auth-on-first-request behavior.
Apply each tar entry's stored mode (masked to the low 9 permission
bits) after writing the file. Files shipped with 0755 — shell scripts,
gradlew/mvnw wrappers, release helpers — are now executable in the
scaffolded project instead of arriving as plain files.

setuid / setgid / sticky bits are stripped on purpose: those have no
place in a user-land scaffold and could surprise reviewers later.

Also export `parseEntryMode` and cover it with tests (parses octal,
masks elevated mode bits, tolerates missing / unparseable input).
Post-simplify cleanup for the org-template feature:

- Move `getNpmRegistry`, `getNpmAuthHeader`, `.npmrc` parsing, and the
  new `fetchNpmResource` wrapper into `utils/npm-config.ts` so the
  concerns stay out of the local-package-metadata file.
- `fetchNpmResource` centralizes the 401/403 retry-with-auth pattern
  previously copy-pasted in `fetchPackument` and `fetchTarball`; future
  tweaks (WWW-Authenticate parsing, observability) land in one place.
- `checkNpmPackageExists` now honors the scope-specific registry and
  flows through the same auth retry — probes against private registries
  no longer miss because of a hardcoded public-registry URL.
- `OrgResolution.bundled.monorepo` flips to `monorepo?: true` to match
  `OrgTemplateEntry.monorepo?: boolean`; non-monorepo entries drop the
  redundant `false` field entirely.
PR review (#3136050751): the bundled scaffold-fallback name was
hardcoded to `vite-plus-template`, so two bundled templates from the
same org produced identical default project names. Propagate the
manifest entry's `name` through `OrgResolution.bundled.entryName` and
use it in the fallback, so `@your-org:demo` defaults to `vite-plus-demo`
and the picker case (where `selectedTemplateName` is scope-only) still
gets a descriptive name.
PR review (#3136678759): the cwd walk-up in `getNpmConfig` passes
through `$HOME` when the project lives under it, re-loading
`~/.npmrc` AFTER the project `.npmrc` was loaded. The last
`loadFileInto` wins, so user-level registry/auth silently clobbered
project-level settings — inverting the documented precedence.

Fix: collect project `.npmrc` paths while walking up (skipping the
already-loaded `~/.npmrc` duplicate), then apply them in reverse
(root → cwd) so the innermost file wins. Add a dedicated test suite
covering the project-vs-user precedence, `@scope:registry`
resolution, and `_authToken` matching.
`getConfiguredDefaultTemplate` used to inspect only the directory it
was handed — which, for standalone repos, was reset to `cwd` earlier
in `bin.ts`. Running `vp create` from `repo/apps/web/` therefore never
saw `repo/vite.config.ts` and its configured default.

Walk up from `startDir` to find the nearest project root first
(monorepo marker or `.git`) and read the config from there. Both
monorepo and standalone-repo layouts now pick up the root config
regardless of which subdirectory the command was invoked from.

- `resolve-vite-config.ts`: extend `findWorkspaceRoot` with a `.git`
  branch so standalone git repos resolve to the repo root. Export it
  so the create flow can reuse it.
- `org-resolve.ts`: `getConfiguredDefaultTemplate` now walks via
  `findWorkspaceRoot` before looking for `vite.config.ts`.
- Add `__tests__/org-resolve.spec.ts` covering the root case, the
  walk-up-from-subdir case, no-config, and no-defaultTemplate.
A subproject (git submodule, nested clone) can have its own `.git`
directory. If that inner `.git` is matched during the primary walk-up,
it preempts a monorepo marker higher up the tree and `vp create`
resolves to the subproject root instead of the monorepo root.

Split `findWorkspaceRoot` into two passes:

- Primary: walk up looking only for authoritative monorepo markers
  (`pnpm-workspace.yaml`, `workspaces` in `package.json`, `lerna.json`).
- Fallback: only if no marker was found anywhere in the ancestry, walk
  again looking for `.git` as the standalone repo boundary.

Add a test covering the subproject case (inner `.git` must not shadow
an outer `pnpm-workspace.yaml`).
…anups

Post-simplify cleanup:

- `findWorkspaceRoot` now uses a single walk with a deferred `.git`
  match. The two-pass version walked the entire ancestry twice in the
  no-marker case; now we record the first `.git` seen and return it
  only after the walk exhausts without finding a monorepo marker.
  Same "markers win globally" semantics, half the I/O.
- Export `hasViteConfig` and use it in `getConfiguredDefaultTemplate`
  instead of the degenerate `findViteConfigUp(x, x)` (start == stop
  meant it only checked the single directory anyway).
- Drop the dead `?? 'template'` fallback in `bin.ts` —
  `bundledEntryName` is always set whenever `isBundledTemplate` is true
  (both come from the same `resolved.kind === 'bundled'` branch).
- `fetchNpmResource` init type is now `Omit<RequestInit, 'signal'>`
  so callers can't accidentally pass a `signal` that the
  `AbortSignal.timeout(timeoutMs)` inside would silently override.
- Tighten `expandNpmrcValue` JSDoc to list the four `.npmrc` features
  we do NOT handle (backslash escapes, `${VAR-default}`, inline `;`
  comments, `key[]=` lists).
…ty comment

Two PR review follow-ups:

- `discovery.spec.ts` (#3137604410, #3137608041): the `@acme-corp`
  placeholder rename inadvertently touched pre-existing GitHub-URL
  tests that have nothing to do with the `@org` feature. Revert those
  four lines back to `acme-corp`; the new manifest-specific tests
  below keep `@your-org`.
- `org-tarball.ts` (#3137690878): the `verifyIntegrity` comment
  claimed "don't silently accept" but the unknown-format branch does
  exactly that. Rewrite the comment to match reality — we skip
  verification for unknown integrity formats because `sha512-<base64>`
  is the universal case and erroring out on anything else would break
  extracts for no benefit.
Per PR review: extending `findWorkspaceRoot` to recognize `.git` as a
project boundary is out of scope for this PR. Restore it to the
original monorepo-marker-only behavior (`pnpm-workspace.yaml`,
`workspaces` in `package.json`, `lerna.json`).

`getConfiguredDefaultTemplate` still walks up via `findWorkspaceRoot`
so monorepo layouts continue to pick up the root `vite.config.ts` from
any subdirectory. Standalone git repos (no monorepo marker) now only
see a config that sits at the cwd.

Update `org-resolve.spec.ts` to exercise the walk-up via
`pnpm-workspace.yaml` instead of `.git`, and remove the nested-`.git`-
subproject test since there's nothing for that case to assert against
anymore.
PR review (#3138196923, #3138197820, #3138198930): the earlier revert
went half-way — the pre-feature GitHub-URL parsing tests should keep
their original `nkzw-tech/fate-template` placeholder, not `acme-corp`.
These tests are unrelated to the `@org` feature and should never have
been touched by the placeholder refactor.
@fengmk2 fengmk2 force-pushed the vp-create-support-org branch from 428a4b4 to a5f36e0 Compare April 25, 2026 03:11
@fengmk2 fengmk2 marked this pull request as ready for review April 25, 2026 03:11
@fengmk2 fengmk2 changed the title feat(create): support @org default templates via vp.templates manifest feat(create): support @org default templates Apr 25, 2026
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 25, 2026

Upgrading to the alpha version allows early access to this feature:

vp upgrade --tag alpha

vp create @eggjs

fengmk2 added 3 commits April 25, 2026 11:47
Four findings from a Codex review of the feature, addressed together:

1. **High — registry-aware tarball cache key**: `getExtractionDir`
   now namespaces by the tarball URL's host, so two repos resolving
   the same `<scope>@<version>` through different registries don't
   share a cache slot. Layout: `<host>/<scope>/create/<version>` (e.g.
   `registry.npmjs.org/@your-org/create/1.0.0`). Falls back to a
   `_unknown` segment if the URL parse fails.
2. **Medium — picker escape-hatch sentinel**: replace the static
   `__vp_builtin_escape__` value with a per-invocation
   `__vp_builtin_escape__::<uuid>` value plus a `Map<string, entry |
   ESCAPE_HATCH>` lookup so a manifest entry can never collide with
   the sentinel. Also enforce the reservation at the schema level —
   any entry `name` starting with `__vp_` is rejected as malformed.
3. **Medium — drop unused `keywords` field**: parsed and validated
   but never consumed. Remove from `OrgTemplateEntry`,
   `validateEntry`, and the docs `Each entry supports` table.
4. **Low — JSDoc on `getConfiguredDefaultTemplate`**: the comment
   referenced `.git`-based discovery that was reverted. Trim it to
   match current behavior (workspace markers only).

Tests: +1 covering the reserved `__vp_` prefix; existing 160 still
pass.
- `org-tarball.ts` — `new URL('https:/typo').host` parses successfully
  but yields `''`, which would join into an empty cache-path segment
  and silently bucket all such manifests together. Fall back to
  `_unknown` for empty hosts the same way we do for hard parse
  failures.
- `org-picker.ts` — the "should never happen" branch in
  `pickOrgTemplate` used to return `ORG_PICKER_CANCEL`, masquerading
  an internal invariant violation as a user-initiated cancel. Throw
  loudly instead so a real bug surfaces.
- `org-manifest.ts` — add a one-liner explaining why the `__vp_`
  name prefix is reserved (internal sentinel namespace, e.g. the
  picker's escape-hatch nonce) so the next maintainer doesn't have
  to reverse-engineer the link.
`manifest.tarballUrl` comes from the npm registry, which guarantees a
valid URL. The `_unknown` fallback was defensive against a hypothetical
malformed input, but if the URL ever fails to parse it's a real bug —
let it throw rather than silently bucket the extraction under a
fallback path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants