vite-plus 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/oxfmt +6 -3
  2. package/bin/oxlint +6 -3
  3. package/bin/vpr +9 -0
  4. package/binding/index.cjs +53 -53
  5. package/binding/index.d.cts +86 -144
  6. package/dist/agent-D3nZAMGI.js +9456 -0
  7. package/dist/bin.js +10 -7
  8. package/dist/{cli-truncate-CWsmbK3p.js → cli-truncate-BVOlIIPy.js} +1 -1
  9. package/dist/config/bin.js +2 -2
  10. package/dist/{constants-CrfJQIUX.js → constants-CZ7Mnq-C.js} +7 -3
  11. package/dist/create/bin.js +800 -806
  12. package/dist/{define-config-2tfJoXr1.d.ts → define-config-BuMs_LKa.d.ts} +64 -9
  13. package/dist/{define-config-DJUehepE.js → define-config-D2jo1xVA.js} +43 -18
  14. package/dist/{define-config-BGSjF6Xp.cjs → define-config-Dut6XD42.cjs} +61 -22
  15. package/dist/define-config.cjs +2 -1
  16. package/dist/define-config.d.ts +2 -2
  17. package/dist/define-config.js +2 -2
  18. package/dist/{dist-Oxo16Y0q.js → dist-CKz3vcoG.js} +4 -4
  19. package/dist/dist-CtM2JRbM.js +3 -0
  20. package/dist/editor-D1BZiU1h.js +584 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/{json-Dn87fvjk.js → json-DiRs8ceZ.js} +13 -1
  25. package/dist/{log-update-lyIiuflf.js → log-update-DHZRyJ2m.js} +2 -2
  26. package/dist/migration/bin.d.ts +1 -10
  27. package/dist/migration/bin.js +531 -170
  28. package/dist/migration/compat/worker.d.ts +1 -0
  29. package/dist/migration/compat/worker.js +35 -0
  30. package/dist/{oxlint-plugin-config-q8a5PFch.js → oxlint-plugin-config-BSbv66Qn.js} +1 -1
  31. package/dist/oxlint-plugin.js +78 -9
  32. package/dist/pack-bin.js +1 -1
  33. package/dist/{package-BHirM1_v.js → package-Cpo2ei_V.js} +52 -3
  34. package/dist/protocol-D99W10Qi.js +4 -0
  35. package/dist/{report-BHSkWqRR.js → report-BS7ULx7A.js} +2 -0
  36. package/dist/{resolve-vite-config-CmdsfQzS.js → resolve-vite-config-DX2o0SWM.js} +14 -10
  37. package/dist/{rolldown-runtime-DnnnRqeS.js → rolldown-runtime-C7HZzL1F.js} +1 -1
  38. package/dist/staged/bin.js +23 -19
  39. package/dist/{tsconfig-BWQPmGKz.js → tsconfig-D6sTl0dN.js} +439 -208
  40. package/dist/version.js +2 -5
  41. package/dist/versions.js +5 -5
  42. package/dist/{wrap-ansi-k7Dn4VtV.js → wrap-ansi-DNjkuBEp.js} +1 -1
  43. package/dist/wrap-ansi-DUi52C6W.js +2 -0
  44. package/docs/_data/team.ts +9 -0
  45. package/docs/config/check.md +35 -0
  46. package/docs/config/index.md +2 -0
  47. package/docs/config/run.md +61 -12
  48. package/docs/guide/automatic-data-tracking.md +145 -0
  49. package/docs/guide/cache.md +14 -35
  50. package/docs/guide/check.md +16 -0
  51. package/docs/guide/ci.md +4 -0
  52. package/docs/guide/docker.md +175 -0
  53. package/docs/guide/env.md +13 -1
  54. package/docs/guide/github-actions-cache.md +165 -0
  55. package/docs/guide/install.md +12 -2
  56. package/docs/guide/migrate-rules.md +347 -0
  57. package/docs/guide/migrate.md +14 -0
  58. package/docs/guide/pack.md +8 -0
  59. package/docs/guide/run.md +33 -3
  60. package/docs/guide/troubleshooting.md +3 -3
  61. package/docs/guide/upgrade.md +43 -21
  62. package/docs/package.json +3 -2
  63. package/docs/vite.config.ts +17 -0
  64. package/package.json +19 -18
  65. package/dist/agent--cKmgD_n.js +0 -3341
  66. package/dist/compat-Cql3K40m.js +0 -14
  67. package/dist/dist-DRJUd9bL.js +0 -3
  68. package/dist/workspace-D0AVy4fu.js +0 -5830
  69. package/dist/wrap-ansi-CeQuiQ31.js +0 -2
@@ -1,13 +1,13 @@
1
- import { r as __toESM, t as __commonJSMin } from "../rolldown-runtime-DnnnRqeS.js";
2
- import { o as VITE_PLUS_NAME } from "../constants-CrfJQIUX.js";
3
- import { A as select, D as log, E as intro, F as runCommand$1, I as runCommandSilently, L as require_cross_spawn, M as text, N as require_picocolors, O as multiselect, P as R, S as PackageManager, T as confirm, _ as approveBuilds, d as promptGitHooks, f as resolveGitInit, h as selectPackageManager, j as spinner, l as defaultInteractive, m as runViteInstall, p as runViteFmt, u as downloadPackageManager$1, v as detectGatedBuilds, w as cancel, x as DependencyType, y as resolveApproveBuildTargets } from "../tsconfig-BWQPmGKz.js";
1
+ import { r as __toESM, t as __commonJSMin } from "../rolldown-runtime-C7HZzL1F.js";
2
+ import { s as VITE_PLUS_NAME } from "../constants-CZ7Mnq-C.js";
3
+ import { A as intro, B as runCommandSilently, C as resolveApproveBuildTargets, E as PackageManager, F as spinner, I as text, L as require_picocolors, M as multiselect, O as cancel, P as select, R as isCancel, S as detectGatedBuilds, T as DependencyType, V as require_cross_spawn, d as downloadPackageManager$1, f as promptGitHooks, g as selectPackageManager, h as runViteInstall, j as log, k as confirm, m as runViteFmt, p as resolveGitInit, u as defaultInteractive, x as approveBuilds, z as runCommand$1 } from "../tsconfig-D6sTl0dN.js";
4
4
  import { a as printHeader, i as muted, o as success, r as log$1, t as accent } from "../terminal-uTv0ZaMr.js";
5
- import { r as readJsonFile, t as editJsonFile } from "../json-Dn87fvjk.js";
6
- import { a as resolveViteConfig, n as findWorkspaceRoot, r as hasViteConfig, t as findViteConfig } from "../resolve-vite-config-CmdsfQzS.js";
5
+ import { i as readJsonFile, n as editJsonFile } from "../json-DiRs8ceZ.js";
6
+ import { a as resolveViteConfig, n as findWorkspaceRoot, r as hasViteConfig, t as findViteConfig } from "../resolve-vite-config-DX2o0SWM.js";
7
7
  import { t as lib_default } from "../lib-L3DWSRQp.js";
8
- import { o as fetchNpmResource, s as getNpmRegistry, t as checkNpmPackageExists } from "../package-BHirM1_v.js";
9
- import { B as setPackageManager, D as injectCreateDefaultTemplate, F as promptEslintMigration, I as promptPrettierMigration, J as templatesDir, L as rewriteMonorepo, R as rewriteMonorepoProject, T as hasFrameworkShim, a as selectAgentTargets, b as detectPrettierProject, c as writeCopilotSetupWorkflow, h as detectFramework, k as installGitHooks, l as addFrameworkShim, m as detectEslintProject, q as displayRelative, r as detectExistingAgentTargetPaths, s as writeAgentInstructions, t as COPILOT_AGENT_ID, z as rewriteStandaloneProject } from "../agent--cKmgD_n.js";
10
- import { c as selectEditors, i as updateWorkspaceConfig, l as writeEditorConfigs, n as isBingoTemplate, o as detectExistingEditors, r as updatePackageJsonWithDeps, t as detectWorkspace$1 } from "../workspace-D0AVy4fu.js";
8
+ import { o as fetchNpmResource, s as getNpmRegistry, t as checkNpmPackageExists } from "../package-Cpo2ei_V.js";
9
+ import { A as detectWorkspace$1, F as detectEslintProject, M as updatePackageJsonWithDeps, N as updateWorkspaceConfig, R as promptEslintMigration, S as injectCreateDefaultTemplate, X as templatesDir, Y as displayRelative, a as selectAgentTargets, c as writeCopilotSetupWorkflow, d as rewriteStandaloneProject, g as setPackageManager, j as isBingoTemplate, l as rewriteMonorepo, r as detectExistingAgentTargetPaths, s as writeAgentInstructions, t as COPILOT_AGENT_ID, u as rewriteMonorepoProject, y as installGitHooks } from "../agent-D3nZAMGI.js";
10
+ import { a as writeEditorConfigs, c as hasFrameworkShim, f as promptPrettierMigration, i as selectEditors, n as detectExistingEditors, o as addFrameworkShim, s as detectFramework, u as detectPrettierProject } from "../editor-D1BZiU1h.js";
11
11
  import { t as renderCliDoc } from "../help-YP84FSEz.js";
12
12
  import path from "node:path";
13
13
  import { runCommand, upsertJsonConfig, vitePlusHeader } from "../../binding/index.js";
@@ -27,28 +27,6 @@ async function initGitRepository(cwd) {
27
27
  envs: process.env
28
28
  })).exitCode === 0;
29
29
  }
30
- async function createInitialCommit(cwd) {
31
- await runCommandSilently({
32
- command: "git",
33
- args: ["add", "-A"],
34
- cwd,
35
- envs: process.env
36
- });
37
- const result = await runCommandSilently({
38
- command: "git",
39
- args: [
40
- "commit",
41
- "-m",
42
- "Initial commit from Vite+"
43
- ],
44
- cwd,
45
- envs: process.env
46
- });
47
- return {
48
- success: result.exitCode === 0,
49
- output: `${result.stdout.toString()}${result.stderr.toString()}`.trim()
50
- };
51
- }
52
30
  //#endregion
53
31
  //#region src/create/command.ts
54
32
  async function runCommandAndDetectProjectDir(options, parentDir) {
@@ -127,829 +105,861 @@ function prependToPathToEnvs(extraPath, envs) {
127
105
  return envs;
128
106
  }
129
107
  //#endregion
130
- //#region src/create/org-manifest.ts
131
- /**
132
- * Parse the org picker specifier: `@scope` (scope only → picker) or
133
- * `@scope:name` (direct manifest-entry selection). Colon mirrors the
134
- * existing `vite:monorepo` / `vite:library` builtin-template syntax and
135
- * keeps manifest entries syntactically distinct from real
136
- * `@scope/package-name` npm specifiers.
137
- *
138
- * Returns `null` for anything else — including the plain `@scope/name`
139
- * form, which routes to the existing `@scope/create-name` shorthand as
140
- * it did before the org-manifest feature.
141
- *
142
- * The optional `version` suffix (`@scope@1.2.3`, `@scope:name@1.2.3`)
143
- * pins `@scope/create` to a specific release rather than `dist-tags.latest`.
144
- */
145
- function parseOrgScopedSpec(spec) {
146
- if (!spec.startsWith("@")) return null;
147
- if (spec.includes("/")) return null;
148
- const colonIndex = spec.indexOf(":");
149
- if (colonIndex === -1) {
150
- const atIndex = spec.indexOf("@", 1);
151
- if (atIndex === -1) return { scope: spec };
152
- const version = spec.slice(atIndex + 1);
153
- return version ? {
154
- scope: spec.slice(0, atIndex),
155
- version
156
- } : { scope: spec.slice(0, atIndex) };
108
+ //#region ../../node_modules/.pnpm/nanotar@0.3.0/node_modules/nanotar/dist/index.mjs
109
+ const tarItemTypeMap = {
110
+ "0": "file",
111
+ "1": "hardLink",
112
+ "2": "symbolicLink",
113
+ "3": "characterDevice",
114
+ "4": "blockDevice",
115
+ "5": "directory",
116
+ "6": "fifo",
117
+ "7": "contiguousFile",
118
+ "g": "globalExtendedHeader",
119
+ "x": "extendedHeader",
120
+ "D": "gnuDirectory",
121
+ "I": "gnuInodeMetadata",
122
+ "K": "gnuLongLinkName",
123
+ "L": "gnuLongFileName",
124
+ "N": "gnuOldLongFileName",
125
+ "M": "gnuMultiVolume",
126
+ "S": "gnuSparseFile",
127
+ "E": "gnuExtendedSparse",
128
+ "A": "solarisAcl",
129
+ "V": "solarisVolumeLabel",
130
+ "X": "solarisOldExtendedHeader"
131
+ };
132
+ function parseTar(data, opts) {
133
+ const buffer = data.buffer || data;
134
+ const files = [];
135
+ let offset = 0;
136
+ let nextExtendedHeader;
137
+ let globalExtendedHeader;
138
+ while (offset < buffer.byteLength - 512) {
139
+ let name = _readString(buffer, offset, 100);
140
+ if (name.length === 0) break;
141
+ if (nextExtendedHeader) {
142
+ const longName = nextExtendedHeader.path || nextExtendedHeader.linkpath;
143
+ if (longName) name = longName;
144
+ }
145
+ const mode = _readString(buffer, offset + 100, 8).trim();
146
+ const uid = Number.parseInt(_readString(buffer, offset + 108, 8));
147
+ const gid = Number.parseInt(_readString(buffer, offset + 116, 8));
148
+ const size = _readNumber(buffer, offset + 124, 12);
149
+ const seek = 512 + 512 * Math.trunc(size / 512) + (size % 512 ? 512 : 0);
150
+ const mtime = _readNumber(buffer, offset + 136, 12);
151
+ const _type = _readString(buffer, offset + 156, 1) || "0";
152
+ const type = tarItemTypeMap[_type] || _type;
153
+ switch (type) {
154
+ case "extendedHeader":
155
+ case "globalExtendedHeader": {
156
+ const headers = _parseExtendedHeaders(new Uint8Array(buffer, offset + 512, size));
157
+ if (type === "extendedHeader") nextExtendedHeader = headers;
158
+ else {
159
+ nextExtendedHeader = void 0;
160
+ globalExtendedHeader = {
161
+ ...globalExtendedHeader,
162
+ ...headers
163
+ };
164
+ }
165
+ offset += seek;
166
+ continue;
167
+ }
168
+ case "gnuLongFileName":
169
+ case "gnuOldLongFileName":
170
+ case "gnuLongLinkName":
171
+ nextExtendedHeader = { path: _readString(buffer, offset + 512, size) };
172
+ offset += seek;
173
+ continue;
174
+ }
175
+ const user = _readString(buffer, offset + 265, 32);
176
+ const group = _readString(buffer, offset + 297, 32);
177
+ name = _sanitizePath(name);
178
+ const meta = {
179
+ name,
180
+ type,
181
+ size,
182
+ attrs: {
183
+ ...globalExtendedHeader,
184
+ ...nextExtendedHeader,
185
+ mode,
186
+ uid,
187
+ gid,
188
+ mtime,
189
+ user,
190
+ group
191
+ }
192
+ };
193
+ nextExtendedHeader = void 0;
194
+ if (opts?.filter && !opts.filter(meta)) {
195
+ offset += seek;
196
+ continue;
197
+ }
198
+ if (opts?.metaOnly) {
199
+ files.push(meta);
200
+ offset += seek;
201
+ continue;
202
+ }
203
+ const data2 = size === 0 ? void 0 : new Uint8Array(buffer, offset + 512, size);
204
+ files.push({
205
+ ...meta,
206
+ data: data2,
207
+ get text() {
208
+ return new TextDecoder().decode(this.data);
209
+ }
210
+ });
211
+ offset += seek;
157
212
  }
158
- const scope = spec.slice(0, colonIndex);
159
- const rest = spec.slice(colonIndex + 1);
160
- const atIndex = rest.indexOf("@");
161
- const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
162
- const version = atIndex === -1 ? "" : rest.slice(atIndex + 1);
163
- if (!name) return version ? {
164
- scope,
165
- version
166
- } : { scope };
167
- return version ? {
168
- scope,
169
- name,
170
- version
171
- } : {
172
- scope,
173
- name
174
- };
213
+ return files;
175
214
  }
176
- /**
177
- * Schema-level failure. Never falls through silently — a maintainer who
178
- * shipped an invalid manifest should see the offending field.
179
- */
180
- var OrgManifestSchemaError = class extends Error {
181
- packageName;
182
- constructor(message, packageName) {
183
- super(`${packageName}: ${message}`);
184
- this.packageName = packageName;
185
- this.name = "OrgManifestSchemaError";
215
+ async function parseTarGzip(data, opts = {}) {
216
+ const stream = new ReadableStream({ start(controller) {
217
+ controller.enqueue(new Uint8Array(data));
218
+ controller.close();
219
+ } }).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));
220
+ return parseTar(await new Response(stream).arrayBuffer(), opts);
221
+ }
222
+ function _sanitizePath(path) {
223
+ let normalized = path.replace(/\\/g, "/");
224
+ normalized = normalized.replace(/^[a-zA-Z]:\//, "");
225
+ normalized = normalized.replace(/^\/+/, "");
226
+ const hasLeadingDotSlash = normalized.startsWith("./");
227
+ const parts = normalized.split("/");
228
+ const resolved = [];
229
+ for (const part of parts) if (part === "..") resolved.pop();
230
+ else if (part !== "." && part !== "") resolved.push(part);
231
+ let result = resolved.join("/");
232
+ if (hasLeadingDotSlash && !result.startsWith("./")) result = "./" + result;
233
+ if (path.endsWith("/") && !result.endsWith("/")) result += "/";
234
+ return result;
235
+ }
236
+ function _readString(buffer, offset, size) {
237
+ const view = new Uint8Array(buffer, offset, size);
238
+ const i = view.indexOf(0);
239
+ return new TextDecoder().decode(i === -1 ? view : view.slice(0, i));
240
+ }
241
+ function _readNumber(buffer, offset, size) {
242
+ const view = new Uint8Array(buffer, offset, size);
243
+ let str = "";
244
+ for (let i = 0; i < size; i++) str += String.fromCodePoint(view[i]);
245
+ return Number.parseInt(str, 8);
246
+ }
247
+ function _parseExtendedHeaders(data) {
248
+ const dataStr = new TextDecoder().decode(data);
249
+ const headers = {};
250
+ for (const line of dataStr.split("\n")) {
251
+ const s = line.split(" ")[1]?.split("=");
252
+ if (s) headers[s[0]] = s[1];
186
253
  }
187
- };
188
- function isRelativePath(spec) {
189
- return spec.startsWith("./") || spec.startsWith("../");
254
+ return headers;
255
+ }
256
+ //#endregion
257
+ //#region src/create/org-tarball.ts
258
+ function getCacheRoot() {
259
+ const home = process.env.VP_HOME || path.join(os.homedir(), ".vite-plus");
260
+ return path.join(home, "tmp", "create-org");
190
261
  }
191
262
  /**
192
- * Validate the `{ name, description, template }` fields shared by org manifest
193
- * entries and local `create.templates` entries. `label` is the config path
194
- * used in error messages (e.g. `createConfig.templates` or `create.templates`)
195
- * and `makeError` builds the thrown error so each source uses its own type.
263
+ * Replace characters that are illegal in Windows path segments
264
+ * (`\ / : * ? " < > |` plus the IPv6 bracket pair `[ ]`). The host
265
+ * comes from `new URL(...).host` which can carry a port (`:4873`) or
266
+ * IPv6 literal (`[::1]`); both end up in the cache path otherwise.
196
267
  */
197
- function validateTemplateEntry(entry, index, label, makeError) {
198
- if (!entry || typeof entry !== "object") throw makeError(`${label}[${index}] must be an object`);
199
- const raw = entry;
200
- const requireString = (field) => {
201
- const value = raw[field];
202
- if (typeof value !== "string" || value.length === 0) throw makeError(`${label}[${index}].${field} must be a non-empty string`);
203
- return value;
204
- };
205
- const name = requireString("name");
206
- if (name.startsWith("__vp_")) throw makeError(`${label}[${index}].name uses the reserved \`__vp_\` prefix`);
207
- const description = requireString("description");
208
- const template = requireString("template");
209
- if (isRelativePath(template)) {
210
- const resolved = path.posix.resolve("/root", template.replaceAll("\\", "/"));
211
- if (resolved !== "/root" && !resolved.startsWith("/root/")) throw makeError(`${label}[${index}].template escapes the package root: ${template}`);
212
- }
213
- return {
214
- name,
215
- description,
216
- template
217
- };
268
+ function sanitizeHostForPath(host) {
269
+ return host.replaceAll(/[\\/:*?"<>|[\]]/g, "_");
218
270
  }
219
271
  /**
220
- * Validate a list of entries, rejecting duplicate `name`s. Shared by org
221
- * manifests and local `create.templates`.
222
- */
223
- function validateTemplateEntries(templates, label, makeError, validateOne) {
224
- const entries = [];
225
- const seen = /* @__PURE__ */ new Set();
226
- for (let index = 0; index < templates.length; index += 1) {
227
- const entry = validateOne(templates[index], index);
228
- if (seen.has(entry.name)) throw makeError(`${label}[${index}].name duplicates an earlier entry: "${entry.name}"`);
229
- seen.add(entry.name);
230
- entries.push(entry);
231
- }
232
- return entries;
272
+ * Cache extracted tarballs under `<host>/<scope>/create/<version>` so two
273
+ * repos resolving the same `<scope>@<version>` through different registries
274
+ * (via `.npmrc` scope mappings) don't share a cache slot. The registry
275
+ * guarantees `manifest.tarballUrl` is a valid URL, so any parse failure
276
+ * here is a real bug worth surfacing.
277
+ */
278
+ function getExtractionDir(manifest) {
279
+ const { host } = new URL(manifest.tarballUrl);
280
+ return path.join(getCacheRoot(), sanitizeHostForPath(host), manifest.scope, "create", manifest.version);
233
281
  }
234
- function validateEntry(entry, index, packageName) {
235
- const makeError = (message) => new OrgManifestSchemaError(message, packageName);
236
- const base = validateTemplateEntry(entry, index, "createConfig.templates", makeError);
237
- let monorepo;
238
- const raw = entry;
239
- if (raw.monorepo !== void 0) {
240
- if (typeof raw.monorepo !== "boolean") throw makeError(`createConfig.templates[${index}].monorepo must be a boolean`);
241
- monorepo = raw.monorepo;
242
- }
282
+ function parseIntegrity(integrity) {
283
+ const match = integrity.split(/\s+/)[0].match(/^(sha\d+)-(.+)$/);
284
+ if (!match) return null;
243
285
  return {
244
- ...base,
245
- ...monorepo !== void 0 ? { monorepo } : {}
286
+ algorithm: match[1],
287
+ expected: match[2]
246
288
  };
247
289
  }
248
- function validateManifest(raw, packageName) {
249
- if (!raw || typeof raw !== "object") return null;
250
- const createConfig = raw.createConfig;
251
- if (!createConfig || typeof createConfig !== "object") return null;
252
- const templates = createConfig.templates;
253
- if (templates === void 0) return null;
254
- if (!Array.isArray(templates)) throw new OrgManifestSchemaError("createConfig.templates must be an array", packageName);
255
- if (templates.length === 0) return null;
256
- return validateTemplateEntries(templates, "createConfig.templates", (message) => new OrgManifestSchemaError(message, packageName), (entry, index) => validateEntry(entry, index, packageName));
290
+ function verifyIntegrity(bytes, integrity) {
291
+ if (!integrity) return;
292
+ const parsed = parseIntegrity(integrity);
293
+ if (!parsed) return;
294
+ const hash = createHash(parsed.algorithm);
295
+ hash.update(bytes);
296
+ const actual = hash.digest("base64");
297
+ if (actual !== parsed.expected) throw new Error(`integrity check failed: expected ${integrity}, got ${parsed.algorithm}-${actual}`);
257
298
  }
258
- /**
259
- * Schema-level failure for `create.templates` in `vite.config.ts`. A misconfigured
260
- * local template should surface clearly rather than silently disappear.
261
- */
262
- var CreateConfigSchemaError = class extends Error {
263
- constructor(message) {
264
- super(message);
265
- this.name = "CreateConfigSchemaError";
299
+ const MAX_TARBALL_BYTES = 50 * 1024 * 1024;
300
+ async function downloadTarball(url) {
301
+ const response = await fetchNpmResource(url, { timeoutMs: 3e4 });
302
+ if (!response.ok) throw new Error(`failed to download tarball (${response.status}): ${url}`);
303
+ const contentLength = Number(response.headers.get("content-length"));
304
+ if (Number.isFinite(contentLength) && contentLength > MAX_TARBALL_BYTES) throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
305
+ const reader = response.body?.getReader();
306
+ if (!reader) throw new Error(`tarball response has no body: ${url}`);
307
+ const chunks = [];
308
+ let total = 0;
309
+ while (true) {
310
+ const { done, value } = await reader.read();
311
+ if (done) break;
312
+ total += value.byteLength;
313
+ if (total > MAX_TARBALL_BYTES) {
314
+ await reader.cancel();
315
+ throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
316
+ }
317
+ chunks.push(value);
266
318
  }
267
- };
268
- /**
269
- * Validate `create.templates` from `vite.config.ts`. Returns `[]` when the field
270
- * is absent or an empty array; throws {@link CreateConfigSchemaError} when present
271
- * but malformed.
272
- */
273
- function validateCreateTemplates(templates) {
274
- if (templates === void 0) return [];
275
- if (!Array.isArray(templates)) throw new CreateConfigSchemaError("create.templates must be an array");
276
- const makeError = (message) => new CreateConfigSchemaError(message);
277
- return validateTemplateEntries(templates, "create.templates", makeError, (entry, index) => {
278
- const validated = validateTemplateEntry(entry, index, "create.templates", makeError);
279
- if (validated.name.startsWith("vite:")) throw makeError(`create.templates[${index}].name uses the reserved \`vite:\` prefix`);
280
- return validated;
281
- });
282
- }
283
- async function fetchPackument(scope, packageName) {
284
- const response = await fetchNpmResource(`${getNpmRegistry(scope)}/${packageName}`, {
285
- headers: { accept: "application/json" },
286
- timeoutMs: 5e3
287
- });
288
- if (response.status === 404) return null;
289
- if (!response.ok) throw new Error(`npm registry responded with ${response.status} for ${packageName}`);
290
- return await response.json();
319
+ const bytes = new Uint8Array(total);
320
+ let offset = 0;
321
+ for (const chunk of chunks) {
322
+ bytes.set(chunk, offset);
323
+ offset += chunk.byteLength;
324
+ }
325
+ return bytes;
291
326
  }
292
327
  /**
293
- * Fetch `@scope/create` from the npm registry and parse its `createConfig.templates`
294
- * manifest.
295
- *
296
- * Returns `null` when:
297
- * - the package does not exist on the registry (404), or
298
- * - the package exists but has no `createConfig.templates` field
328
+ * Download a package tarball and parse its `package/package.json`.
299
329
  *
300
- * Throws when:
301
- * - the `createConfig.templates` field is present but malformed (`OrgManifestSchemaError`), or
302
- * - the registry request fails for any non-404 reason
330
+ * Some registries (GitHub Packages among them) strip fields they don't
331
+ * recognize including `createConfig` from packument *version metadata*
332
+ * while preserving the published tarball byte-for-byte. This gives
333
+ * `readOrgManifest` a fallback source of truth for those registries.
303
334
  *
304
- * `requestedVersion` pins the lookup to a specific `versions[...]` entry
305
- * (equivalent to `vp create @scope@1.2.3`); omit it to resolve `dist-tags.latest`.
335
+ * Returns `null` when the archive contains no `package/package.json`.
336
+ * Throws on download/integrity failures or unparsable JSON.
306
337
  */
307
- async function readOrgManifest(scope, requestedVersion) {
308
- if (!scope.startsWith("@")) return null;
309
- const packageName = `${scope}/create`;
310
- const packument = await fetchPackument(scope, packageName);
311
- if (!packument) return null;
312
- let resolvedVersion;
313
- if (requestedVersion) {
314
- resolvedVersion = packument["dist-tags"]?.[requestedVersion] ?? (packument.versions?.[requestedVersion] ? requestedVersion : void 0);
315
- if (!resolvedVersion) throw new OrgManifestSchemaError(`version "${requestedVersion}" not found (known tags: ${Object.keys(packument["dist-tags"] ?? {}).join(", ") || "none"})`, packageName);
316
- } else {
317
- resolvedVersion = packument["dist-tags"]?.latest;
318
- if (!resolvedVersion) return null;
338
+ async function readPackageJsonFromTarball(tarballUrl, integrity) {
339
+ const bytes = await downloadTarball(tarballUrl);
340
+ verifyIntegrity(bytes, integrity);
341
+ const entries = await parseTarGzip(bytes);
342
+ for (const entry of entries) {
343
+ if (normalizeEntryName(entry.name) !== "package.json" || !entry.data) continue;
344
+ const text = new TextDecoder().decode(entry.data);
345
+ try {
346
+ return JSON.parse(text);
347
+ } catch {
348
+ throw new Error(`invalid package.json in tarball: ${tarballUrl}`);
349
+ }
319
350
  }
320
- const meta = packument.versions?.[resolvedVersion];
321
- if (!meta) return null;
322
- const templates = validateManifest(meta, packageName);
323
- if (!templates) return null;
324
- if (!meta.dist?.tarball) throw new OrgManifestSchemaError(`missing dist.tarball for ${resolvedVersion}`, packageName);
325
- return {
326
- scope,
327
- packageName,
328
- version: resolvedVersion,
329
- tarballUrl: meta.dist.tarball,
330
- integrity: meta.dist.integrity,
331
- templates
332
- };
351
+ return null;
333
352
  }
353
+ const STAGING_SUFFIX_PREFIX = ".tmp-";
334
354
  /**
335
- * Apply the in-monorepo filter rule from the RFC: entries with
336
- * `monorepo: true` are hidden when the command is invoked inside an
337
- * existing monorepo, mirroring `initial-template-options.ts:9-31`.
355
+ * Parse a tar entry's stored mode (always octal) into the numeric
356
+ * permission bits (low 9 bits `rwxrwxrwx`). Returns `undefined` when
357
+ * the mode is missing or unparsable so the caller leaves the file with
358
+ * its default (umask-derived) permissions instead of downgrading.
338
359
  */
339
- function filterManifestForContext(templates, isMonorepo) {
340
- if (!isMonorepo) return [...templates];
341
- return templates.filter((entry) => !entry.monorepo);
342
- }
343
- //#endregion
344
- //#region src/create/templates/types.ts
345
- const LibraryTemplateRepo = "github:sxzz/tsdown-templates/vite-plus";
346
- const BuiltinTemplate = {
347
- generator: "vite:generator",
348
- monorepo: "vite:monorepo",
349
- application: "vite:application",
350
- library: "vite:library"
351
- };
352
- const TemplateType = {
353
- builtin: "builtin",
354
- bingo: "bingo",
355
- remote: "remote",
356
- bundled: "bundled"
357
- };
358
- //#endregion
359
- //#region src/create/discovery.ts
360
- function isGitHubUrl(templateName) {
361
- return templateName.startsWith("https://github.com/") || templateName.startsWith("github:") || templateName.includes("github.com/");
362
- }
363
- function parseGitHubUrl(url) {
364
- if (url.startsWith("github:")) return url.slice(7);
365
- const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
366
- if (match) return match[1].replace(/\.git$/, "");
367
- return null;
360
+ function parseEntryMode(raw) {
361
+ if (!raw) return;
362
+ const parsed = Number.parseInt(raw, 8);
363
+ if (!Number.isFinite(parsed)) return;
364
+ return parsed & 511;
368
365
  }
369
- function inferGitHubRepoName(templateName) {
370
- const degitPath = parseGitHubUrl(templateName);
371
- if (!degitPath) return null;
372
- return degitPath.split("/").pop() || null;
366
+ /**
367
+ * Strip the `package/` prefix from an `npm pack` tarball entry. Returns
368
+ * `null` for entries to skip (root dir, PaxHeader, anything outside
369
+ * `package/`).
370
+ */
371
+ function normalizeEntryName(rawName) {
372
+ const name = rawName.replace(/^\.\//, "").replace(/\\/g, "/");
373
+ if (!name || name === "package" || name === "package/") return null;
374
+ if (name.startsWith("PaxHeader/") || name.includes("/PaxHeader/")) return null;
375
+ if (!name.startsWith("package/")) return null;
376
+ return name.slice(8);
373
377
  }
374
- function localTemplateDir(workspaceInfo, templateName) {
375
- if (isRelativePath(templateName)) return templateName.replace(/^\.\//, "");
376
- return workspaceInfo.packages.find((pkg) => pkg.name === templateName)?.path;
378
+ async function extractTarballTo(bytes, destDir) {
379
+ const entries = await parseTarGzip(bytes);
380
+ const stagingDir = `${destDir}${STAGING_SUFFIX_PREFIX}${process.pid}-${Date.now()}`;
381
+ await fs.promises.mkdir(stagingDir, { recursive: true });
382
+ const resolvedStaging = path.resolve(stagingDir);
383
+ try {
384
+ for (const entry of entries) {
385
+ const relativeName = normalizeEntryName(entry.name);
386
+ if (relativeName === null) continue;
387
+ const targetPath = path.join(stagingDir, relativeName);
388
+ const resolvedTarget = path.resolve(targetPath);
389
+ if (resolvedTarget !== resolvedStaging && !resolvedTarget.startsWith(`${resolvedStaging}${path.sep}`)) throw new Error(`tarball entry escapes extraction root: ${entry.name}`);
390
+ if (entry.type === "directory" || relativeName.endsWith("/")) {
391
+ await fs.promises.mkdir(targetPath, { recursive: true });
392
+ continue;
393
+ }
394
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
395
+ const data = entry.data ?? /* @__PURE__ */ new Uint8Array(0);
396
+ await fs.promises.writeFile(targetPath, data);
397
+ const mode = parseEntryMode(entry.attrs?.mode);
398
+ if (mode !== void 0) await fs.promises.chmod(targetPath, mode);
399
+ }
400
+ try {
401
+ await fs.promises.rename(stagingDir, destDir);
402
+ } catch (error) {
403
+ const code = error.code;
404
+ if ((code === "ENOTEMPTY" || code === "EEXIST") && fs.existsSync(path.join(destDir, "package.json"))) {
405
+ await fs.promises.rm(stagingDir, {
406
+ recursive: true,
407
+ force: true
408
+ }).catch(() => {});
409
+ return;
410
+ }
411
+ throw error;
412
+ }
413
+ } catch (error) {
414
+ await fs.promises.rm(stagingDir, {
415
+ recursive: true,
416
+ force: true
417
+ }).catch(() => {});
418
+ throw error;
419
+ }
377
420
  }
378
- function resolveLocalBinPath(localPackagePath, packageName, bin) {
379
- if (!bin) return;
380
- if (typeof bin === "string") return path.join(localPackagePath, bin);
381
- const entries = Object.entries(bin);
382
- if (entries.length === 0) return;
383
- if (entries.length === 1) return path.join(localPackagePath, entries[0][1]);
384
- const unscopedName = packageName.slice(packageName.lastIndexOf("/") + 1);
385
- const preferred = bin[packageName] ?? bin[unscopedName];
386
- if (preferred) return path.join(localPackagePath, preferred);
387
- throw new Error(`Local template package "${packageName}" defines multiple "bin" entries (${entries.map(([name]) => name).join(", ")}); add a "bin" entry named "${packageName}" so the template entry is unambiguous`);
388
- }
389
- function discoverTemplate(templateName, templateArgs, workspaceInfo, interactive, bundledLocalPath, skipShorthand, localTemplate) {
390
- const envs = prependToPathToEnvs(workspaceInfo.downloadPackageManager.binPrefix, { ...process.env });
391
- const parentDir = inferParentDir(templateName, workspaceInfo, localTemplate);
392
- if (bundledLocalPath) return {
393
- command: "",
394
- args: [...templateArgs],
395
- envs,
396
- type: TemplateType.bundled,
397
- parentDir,
398
- interactive,
399
- localPath: bundledLocalPath
400
- };
401
- if (templateName.startsWith("vite:")) return {
402
- command: templateName,
403
- args: [...templateArgs],
404
- envs,
405
- type: TemplateType.builtin,
406
- parentDir,
407
- interactive
408
- };
409
- if (isGitHubUrl(templateName)) {
410
- const degitPath = parseGitHubUrl(templateName);
411
- if (degitPath) return {
412
- command: "degit",
413
- args: [degitPath, ...templateArgs],
414
- envs,
415
- type: TemplateType.remote,
416
- parentDir,
417
- interactive
418
- };
419
- }
420
- if (localTemplate) {
421
- const localDir = localTemplateDir(workspaceInfo, templateName);
422
- if (!localDir) throw new Error(`Local template "${templateName}" does not match any workspace package; update the \`create.templates\` entry in vite.config.ts`);
423
- const localPackagePath = path.join(workspaceInfo.rootDir, localDir);
424
- const packageJsonPath = path.join(localPackagePath, "package.json");
425
- if (!fs.existsSync(packageJsonPath)) throw new Error(`Local template "${templateName}" has no package.json, so it cannot be run as a template`);
426
- const pkg = readJsonFile(packageJsonPath);
427
- const binPath = resolveLocalBinPath(localPackagePath, pkg.name ?? templateName, pkg.bin);
428
- if (!binPath) throw new Error(`Local template "${templateName}" has no "bin" entry in its package.json, so it cannot be run as a template`);
429
- const args = [binPath, ...templateArgs];
430
- let type = TemplateType.remote;
431
- if (isBingoTemplate(pkg)) {
432
- type = TemplateType.bingo;
433
- args.push("--skip-requests");
434
- }
435
- return {
436
- command: "node",
437
- args,
438
- envs,
439
- type,
440
- parentDir,
441
- interactive
442
- };
421
+ const STAGING_STALE_MS = 1440 * 60 * 1e3;
422
+ /**
423
+ * Remove `<destDir>.tmp-*` siblings left behind by a previous crash so
424
+ * repeated aborts don't accumulate orphaned staging trees. Only deletes
425
+ * entries whose mtime is older than 24 hours — a concurrent `vp create`
426
+ * that's still actively extracting will always be younger than that, so
427
+ * the age gate keeps this safe to run at the top of every extract.
428
+ */
429
+ async function cleanupStaleStagingDirs(destDir) {
430
+ const parent = path.dirname(destDir);
431
+ const prefix = `${path.basename(destDir)}${STAGING_SUFFIX_PREFIX}`;
432
+ let entries;
433
+ try {
434
+ entries = await fs.promises.readdir(parent);
435
+ } catch {
436
+ return;
443
437
  }
444
- return {
445
- command: skipShorthand ? templateName : expandCreateShorthand(templateName),
446
- args: [...templateArgs],
447
- envs,
448
- type: TemplateType.remote,
449
- parentDir,
450
- interactive
451
- };
438
+ const cutoff = Date.now() - STAGING_STALE_MS;
439
+ await Promise.all(entries.filter((name) => name.startsWith(prefix)).map(async (name) => {
440
+ const fullPath = path.join(parent, name);
441
+ try {
442
+ if ((await fs.promises.stat(fullPath)).mtimeMs < cutoff) await fs.promises.rm(fullPath, {
443
+ recursive: true,
444
+ force: true
445
+ });
446
+ } catch {}
447
+ }));
452
448
  }
453
449
  /**
454
- * Expand shorthand template names to their full `create-*` package names.
455
- *
456
- * This follows the same convention as `npm create` / `pnpm create`:
457
- * - `vite` → `create-vite`
458
- * - `vite@latest` → `create-vite@latest`
459
- *
460
- * Special cases for packages where the convention doesn't work:
461
- * - `nitro` → `create-nitro-app` (create-nitro is abandoned)
462
- * - `svelte` → `sv`
463
- * - `@tanstack/start` → `@tanstack/cli` (@tanstack/create-start is deprecated)
450
+ * Ensure the `@org/create` package tarball for the given manifest has been
451
+ * downloaded and extracted locally. Returns the absolute path to the
452
+ * extracted package root (i.e. the directory that contains
453
+ * `package.json`).
464
454
  *
465
- * Skips expansion for:
466
- * - Builtin templates (`vite:*`)
467
- * - GitHub URLs
468
- * - Local paths (`./`, `../`, `/`)
469
- * - Names already starting with `create-` (or `@scope/create-`)
455
+ * Idempotent: subsequent calls for the same `<scope, version>` reuse the
456
+ * cached extraction. Concurrent calls race on the final rename; the loser
457
+ * cleans up and returns the existing directory.
470
458
  */
471
- function expandCreateShorthand(templateName) {
472
- if (templateName.includes(":")) return templateName;
473
- if (isGitHubUrl(templateName)) return templateName;
474
- if (templateName.startsWith("./") || templateName.startsWith("../") || templateName.startsWith("/")) return templateName;
475
- if (templateName.startsWith("@")) {
476
- const slashIndex = templateName.indexOf("/");
477
- if (slashIndex === -1) {
478
- const atIndex = templateName.indexOf("@", 1);
479
- return `${atIndex === -1 ? templateName : templateName.slice(0, atIndex)}/create${atIndex === -1 ? "" : templateName.slice(atIndex)}`;
480
- }
481
- const scope = templateName.slice(0, slashIndex);
482
- const rest = templateName.slice(slashIndex + 1);
483
- const atIndex = rest.indexOf("@");
484
- const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
485
- const version = atIndex === -1 ? "" : rest.slice(atIndex);
486
- if (name.startsWith("create-")) return templateName;
487
- if (scope === "@tanstack" && name === "start") return `@tanstack/cli${version}`;
488
- return `${scope}/create-${name}${version}`;
489
- }
490
- const atIndex = templateName.indexOf("@");
491
- const name = atIndex === -1 ? templateName : templateName.slice(0, atIndex);
492
- const version = atIndex === -1 ? "" : templateName.slice(atIndex);
493
- if (name.startsWith("create-")) return templateName;
494
- if (name === "nitro") return `create-nitro-app${version}`;
495
- if (name === "svelte") return `sv${version}`;
496
- return `create-${name}${version}`;
497
- }
498
- function inferParentDir(templateName, workspaceInfo, localTemplate = false) {
499
- if (workspaceInfo.parentDirs.length === 0) return;
500
- const localDir = localTemplate ? localTemplateDir(workspaceInfo, templateName) : void 0;
501
- if (localDir) {
502
- const ownParentDir = path.dirname(localDir);
503
- if (workspaceInfo.parentDirs.includes(ownParentDir)) return ownParentDir;
504
- }
505
- let rule = /app/i;
506
- if (templateName === BuiltinTemplate.library) rule = /lib|component|package/i;
507
- else if (templateName === BuiltinTemplate.generator) rule = /generator|tool/i;
508
- for (const parentDir of workspaceInfo.parentDirs) if (rule.test(parentDir)) return parentDir;
459
+ async function ensureOrgPackageExtracted(manifest) {
460
+ const extractedRoot = getExtractionDir(manifest);
461
+ if (fs.existsSync(path.join(extractedRoot, "package.json"))) return extractedRoot;
462
+ const parent = path.dirname(extractedRoot);
463
+ await fs.promises.mkdir(parent, { recursive: true });
464
+ await cleanupStaleStagingDirs(extractedRoot);
465
+ const bytes = await downloadTarball(manifest.tarballUrl);
466
+ verifyIntegrity(bytes, manifest.integrity);
467
+ await extractTarballTo(bytes, extractedRoot);
468
+ return extractedRoot;
509
469
  }
510
- //#endregion
511
- //#region src/create/initial-template-options.ts
512
- function getInitialTemplateOptions(isMonorepo, templates = []) {
513
- return [
514
- ...!isMonorepo ? [{
515
- label: "Vite+ Monorepo",
516
- value: BuiltinTemplate.monorepo,
517
- hint: "Create a new Vite+ monorepo project"
518
- }] : [],
519
- {
520
- label: "Vite+ Application",
521
- value: BuiltinTemplate.application,
522
- hint: "Create vite applications"
523
- },
524
- {
525
- label: "Vite+ Library",
526
- value: BuiltinTemplate.library,
527
- hint: "Create vite libraries"
528
- },
529
- ...isMonorepo ? templates.map((entry) => ({
530
- label: entry.name,
531
- value: entry.name,
532
- hint: entry.description
533
- })) : []
534
- ];
470
+ /**
471
+ * Resolve a manifest entry's relative `./...` path against an already-
472
+ * extracted package root, rejecting any path that escapes the root (via
473
+ * `..` walks or an absolute specifier).
474
+ *
475
+ * Existence is NOT checked here — the subsequent `copyDir` surfaces any
476
+ * missing-directory error with a clearer errno.
477
+ */
478
+ function resolveBundledPath(extractedRoot, relativePath) {
479
+ if (path.isAbsolute(relativePath)) throw new Error(`bundled template path must be relative, got ${relativePath}`);
480
+ const resolvedRoot = path.resolve(extractedRoot);
481
+ const resolvedTarget = path.resolve(extractedRoot, relativePath);
482
+ if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) throw new Error(`bundled template path escapes the package root: ${relativePath}`);
483
+ return resolvedTarget;
535
484
  }
536
485
  //#endregion
537
- //#region src/create/org-picker.ts
538
- const ORG_PICKER_CANCEL = Symbol("org-picker-cancel");
539
- const ORG_PICKER_BUILTIN_ESCAPE = Symbol("org-picker-builtin-escape");
540
- const ESCAPE_HATCH = Symbol("builtin-escape");
486
+ //#region src/create/org-manifest.ts
541
487
  /**
542
- * Render the interactive picker for an org manifest. Always appends a
543
- * trailing "Vite+ built-in templates" escape-hatch entry.
488
+ * Parse the org picker specifier: `@scope` (scope only picker) or
489
+ * `@scope:name` (direct manifest-entry selection). Colon mirrors the
490
+ * existing `vite:monorepo` / `vite:library` builtin-template syntax and
491
+ * keeps manifest entries syntactically distinct from real
492
+ * `@scope/package-name` npm specifiers.
544
493
  *
545
- * Context-filters entries with `monorepo: true` when running inside an
546
- * existing monorepo, mirroring `initial-template-options.ts:9-31`.
494
+ * Returns `null` for anything else including the plain `@scope/name`
495
+ * form, which routes to the existing `@scope/create-name` shorthand as
496
+ * it did before the org-manifest feature.
547
497
  *
548
- * Returns `ORG_PICKER_BUILTIN_ESCAPE` when the escape hatch is selected,
549
- * or `ORG_PICKER_CANCEL` when the user hits Ctrl-C.
498
+ * The optional `version` suffix (`@scope@1.2.3`, `@scope:name@1.2.3`)
499
+ * pins `@scope/create` to a specific release rather than `dist-tags.latest`.
550
500
  */
551
- async function pickOrgTemplate(manifest, opts) {
552
- const filtered = filterManifestForContext(manifest.templates, opts.isMonorepo);
553
- if (filtered.length === 0) return ORG_PICKER_BUILTIN_ESCAPE;
554
- const escapeValue = `__vp_builtin_escape__::${randomUUID()}`;
555
- const lookup = /* @__PURE__ */ new Map();
556
- const options = filtered.map((entry) => {
557
- lookup.set(entry.name, entry);
558
- return {
559
- value: entry.name,
560
- label: entry.name,
561
- hint: entry.description
562
- };
563
- });
564
- lookup.set(escapeValue, ESCAPE_HATCH);
565
- const builtinHint = opts.isMonorepo ? "Use defaults (application / library)" : "Use defaults (monorepo / application / library)";
566
- options.push({
567
- value: escapeValue,
568
- label: "Vite+ built-in templates",
569
- hint: builtinHint
570
- });
571
- const picked = await select({
572
- message: `Pick a template from ${manifest.scope}`,
573
- options
574
- });
575
- if (R(picked)) return ORG_PICKER_CANCEL;
576
- const found = lookup.get(picked);
577
- if (found === ESCAPE_HATCH) return ORG_PICKER_BUILTIN_ESCAPE;
578
- if (!found) throw new Error(`org-picker: prompts.select returned an unregistered value: ${picked}`);
579
- return {
580
- kind: "entry",
581
- entry: found
501
+ function parseOrgScopedSpec(spec) {
502
+ if (!spec.startsWith("@")) return null;
503
+ if (spec.includes("/")) return null;
504
+ const colonIndex = spec.indexOf(":");
505
+ if (colonIndex === -1) {
506
+ const atIndex = spec.indexOf("@", 1);
507
+ if (atIndex === -1) return { scope: spec };
508
+ const version = spec.slice(atIndex + 1);
509
+ return version ? {
510
+ scope: spec.slice(0, atIndex),
511
+ version
512
+ } : { scope: spec.slice(0, atIndex) };
513
+ }
514
+ const scope = spec.slice(0, colonIndex);
515
+ const rest = spec.slice(colonIndex + 1);
516
+ const atIndex = rest.indexOf("@");
517
+ const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
518
+ const version = atIndex === -1 ? "" : rest.slice(atIndex + 1);
519
+ if (!name) return version ? {
520
+ scope,
521
+ version
522
+ } : { scope };
523
+ return version ? {
524
+ scope,
525
+ name,
526
+ version
527
+ } : {
528
+ scope,
529
+ name
582
530
  };
583
531
  }
584
532
  /**
585
- * Render the manifest as a plain-text table for the `--no-interactive`
586
- * error output. Fixed column order so AI agents and scripts can recover
587
- * available template names without a `--json` flag.
533
+ * Schema-level failure. Never falls through silently a maintainer who
534
+ * shipped an invalid manifest should see the offending field.
588
535
  */
589
- function formatManifestTable(manifest, isMonorepo) {
590
- const visible = filterManifestForContext(manifest.templates, isMonorepo);
591
- const filteredCount = manifest.templates.length - visible.length;
592
- const nameWidth = Math.max(4, ...visible.map((entry) => entry.name.length));
593
- const descWidth = Math.max(11, ...visible.map((entry) => entry.description.length));
594
- const lines = [];
595
- lines.push(` ${"NAME".padEnd(nameWidth)} ${"DESCRIPTION".padEnd(descWidth)} TEMPLATE`);
596
- for (const entry of visible) lines.push(` ${entry.name.padEnd(nameWidth)} ${entry.description.padEnd(descWidth)} ${entry.template}`);
536
+ var OrgManifestSchemaError = class extends Error {
537
+ packageName;
538
+ constructor(message, packageName) {
539
+ super(`${packageName}: ${message}`);
540
+ this.packageName = packageName;
541
+ this.name = "OrgManifestSchemaError";
542
+ }
543
+ };
544
+ function isRelativePath(spec) {
545
+ return spec.startsWith("./") || spec.startsWith("../");
546
+ }
547
+ /**
548
+ * Validate the `{ name, description, template }` fields shared by org manifest
549
+ * entries and local `create.templates` entries. `label` is the config path
550
+ * used in error messages (e.g. `createConfig.templates` or `create.templates`)
551
+ * and `makeError` builds the thrown error so each source uses its own type.
552
+ */
553
+ function validateTemplateEntry(entry, index, label, makeError) {
554
+ if (!entry || typeof entry !== "object") throw makeError(`${label}[${index}] must be an object`);
555
+ const raw = entry;
556
+ const requireString = (field) => {
557
+ const value = raw[field];
558
+ if (typeof value !== "string" || value.length === 0) throw makeError(`${label}[${index}].${field} must be a non-empty string`);
559
+ return value;
560
+ };
561
+ const name = requireString("name");
562
+ if (name.startsWith("__vp_")) throw makeError(`${label}[${index}].name uses the reserved \`__vp_\` prefix`);
563
+ const description = requireString("description");
564
+ const template = requireString("template");
565
+ if (isRelativePath(template)) {
566
+ const resolved = path.posix.resolve("/root", template.replaceAll("\\", "/"));
567
+ if (resolved !== "/root" && !resolved.startsWith("/root/")) throw makeError(`${label}[${index}].template escapes the package root: ${template}`);
568
+ }
597
569
  return {
598
- lines,
599
- filteredCount
570
+ name,
571
+ description,
572
+ template
600
573
  };
601
574
  }
602
- //#endregion
603
- //#region ../../node_modules/.pnpm/nanotar@0.3.0/node_modules/nanotar/dist/index.mjs
604
- const tarItemTypeMap = {
605
- "0": "file",
606
- "1": "hardLink",
607
- "2": "symbolicLink",
608
- "3": "characterDevice",
609
- "4": "blockDevice",
610
- "5": "directory",
611
- "6": "fifo",
612
- "7": "contiguousFile",
613
- "g": "globalExtendedHeader",
614
- "x": "extendedHeader",
615
- "D": "gnuDirectory",
616
- "I": "gnuInodeMetadata",
617
- "K": "gnuLongLinkName",
618
- "L": "gnuLongFileName",
619
- "N": "gnuOldLongFileName",
620
- "M": "gnuMultiVolume",
621
- "S": "gnuSparseFile",
622
- "E": "gnuExtendedSparse",
623
- "A": "solarisAcl",
624
- "V": "solarisVolumeLabel",
625
- "X": "solarisOldExtendedHeader"
626
- };
627
- function parseTar(data, opts) {
628
- const buffer = data.buffer || data;
629
- const files = [];
630
- let offset = 0;
631
- let nextExtendedHeader;
632
- let globalExtendedHeader;
633
- while (offset < buffer.byteLength - 512) {
634
- let name = _readString(buffer, offset, 100);
635
- if (name.length === 0) break;
636
- if (nextExtendedHeader) {
637
- const longName = nextExtendedHeader.path || nextExtendedHeader.linkpath;
638
- if (longName) name = longName;
639
- }
640
- const mode = _readString(buffer, offset + 100, 8).trim();
641
- const uid = Number.parseInt(_readString(buffer, offset + 108, 8));
642
- const gid = Number.parseInt(_readString(buffer, offset + 116, 8));
643
- const size = _readNumber(buffer, offset + 124, 12);
644
- const seek = 512 + 512 * Math.trunc(size / 512) + (size % 512 ? 512 : 0);
645
- const mtime = _readNumber(buffer, offset + 136, 12);
646
- const _type = _readString(buffer, offset + 156, 1) || "0";
647
- const type = tarItemTypeMap[_type] || _type;
648
- switch (type) {
649
- case "extendedHeader":
650
- case "globalExtendedHeader": {
651
- const headers = _parseExtendedHeaders(new Uint8Array(buffer, offset + 512, size));
652
- if (type === "extendedHeader") nextExtendedHeader = headers;
653
- else {
654
- nextExtendedHeader = void 0;
655
- globalExtendedHeader = {
656
- ...globalExtendedHeader,
657
- ...headers
658
- };
659
- }
660
- offset += seek;
661
- continue;
662
- }
663
- case "gnuLongFileName":
664
- case "gnuOldLongFileName":
665
- case "gnuLongLinkName":
666
- nextExtendedHeader = { path: _readString(buffer, offset + 512, size) };
667
- offset += seek;
668
- continue;
669
- }
670
- const user = _readString(buffer, offset + 265, 32);
671
- const group = _readString(buffer, offset + 297, 32);
672
- name = _sanitizePath(name);
673
- const meta = {
674
- name,
675
- type,
676
- size,
677
- attrs: {
678
- ...globalExtendedHeader,
679
- ...nextExtendedHeader,
680
- mode,
681
- uid,
682
- gid,
683
- mtime,
684
- user,
685
- group
686
- }
687
- };
688
- nextExtendedHeader = void 0;
689
- if (opts?.filter && !opts.filter(meta)) {
690
- offset += seek;
691
- continue;
692
- }
693
- if (opts?.metaOnly) {
694
- files.push(meta);
695
- offset += seek;
696
- continue;
697
- }
698
- const data2 = size === 0 ? void 0 : new Uint8Array(buffer, offset + 512, size);
699
- files.push({
700
- ...meta,
701
- data: data2,
702
- get text() {
703
- return new TextDecoder().decode(this.data);
704
- }
705
- });
706
- offset += seek;
575
+ /**
576
+ * Validate a list of entries, rejecting duplicate `name`s. Shared by org
577
+ * manifests and local `create.templates`.
578
+ */
579
+ function validateTemplateEntries(templates, label, makeError, validateOne) {
580
+ const entries = [];
581
+ const seen = /* @__PURE__ */ new Set();
582
+ for (let index = 0; index < templates.length; index += 1) {
583
+ const entry = validateOne(templates[index], index);
584
+ if (seen.has(entry.name)) throw makeError(`${label}[${index}].name duplicates an earlier entry: "${entry.name}"`);
585
+ seen.add(entry.name);
586
+ entries.push(entry);
707
587
  }
708
- return files;
709
- }
710
- async function parseTarGzip(data, opts = {}) {
711
- const stream = new ReadableStream({ start(controller) {
712
- controller.enqueue(new Uint8Array(data));
713
- controller.close();
714
- } }).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));
715
- return parseTar(await new Response(stream).arrayBuffer(), opts);
716
- }
717
- function _sanitizePath(path) {
718
- let normalized = path.replace(/\\/g, "/");
719
- normalized = normalized.replace(/^[a-zA-Z]:\//, "");
720
- normalized = normalized.replace(/^\/+/, "");
721
- const hasLeadingDotSlash = normalized.startsWith("./");
722
- const parts = normalized.split("/");
723
- const resolved = [];
724
- for (const part of parts) if (part === "..") resolved.pop();
725
- else if (part !== "." && part !== "") resolved.push(part);
726
- let result = resolved.join("/");
727
- if (hasLeadingDotSlash && !result.startsWith("./")) result = "./" + result;
728
- if (path.endsWith("/") && !result.endsWith("/")) result += "/";
729
- return result;
588
+ return entries;
730
589
  }
731
- function _readString(buffer, offset, size) {
732
- const view = new Uint8Array(buffer, offset, size);
733
- const i = view.indexOf(0);
734
- return new TextDecoder().decode(i === -1 ? view : view.slice(0, i));
590
+ function validateEntry(entry, index, packageName) {
591
+ const makeError = (message) => new OrgManifestSchemaError(message, packageName);
592
+ const base = validateTemplateEntry(entry, index, "createConfig.templates", makeError);
593
+ let monorepo;
594
+ const raw = entry;
595
+ if (raw.monorepo !== void 0) {
596
+ if (typeof raw.monorepo !== "boolean") throw makeError(`createConfig.templates[${index}].monorepo must be a boolean`);
597
+ monorepo = raw.monorepo;
598
+ }
599
+ return {
600
+ ...base,
601
+ ...monorepo !== void 0 ? { monorepo } : {}
602
+ };
735
603
  }
736
- function _readNumber(buffer, offset, size) {
737
- const view = new Uint8Array(buffer, offset, size);
738
- let str = "";
739
- for (let i = 0; i < size; i++) str += String.fromCodePoint(view[i]);
740
- return Number.parseInt(str, 8);
604
+ function validateManifest(raw, packageName) {
605
+ if (!raw || typeof raw !== "object") return null;
606
+ const createConfig = raw.createConfig;
607
+ if (!createConfig || typeof createConfig !== "object") return null;
608
+ const templates = createConfig.templates;
609
+ if (templates === void 0) return null;
610
+ if (!Array.isArray(templates)) throw new OrgManifestSchemaError("createConfig.templates must be an array", packageName);
611
+ if (templates.length === 0) return null;
612
+ return validateTemplateEntries(templates, "createConfig.templates", (message) => new OrgManifestSchemaError(message, packageName), (entry, index) => validateEntry(entry, index, packageName));
741
613
  }
742
- function _parseExtendedHeaders(data) {
743
- const dataStr = new TextDecoder().decode(data);
744
- const headers = {};
745
- for (const line of dataStr.split("\n")) {
746
- const s = line.split(" ")[1]?.split("=");
747
- if (s) headers[s[0]] = s[1];
614
+ /**
615
+ * Schema-level failure for `create.templates` in `vite.config.ts`. A misconfigured
616
+ * local template should surface clearly rather than silently disappear.
617
+ */
618
+ var CreateConfigSchemaError = class extends Error {
619
+ constructor(message) {
620
+ super(message);
621
+ this.name = "CreateConfigSchemaError";
748
622
  }
749
- return headers;
623
+ };
624
+ /**
625
+ * Validate `create.templates` from `vite.config.ts`. Returns `[]` when the field
626
+ * is absent or an empty array; throws {@link CreateConfigSchemaError} when present
627
+ * but malformed.
628
+ */
629
+ function validateCreateTemplates(templates) {
630
+ if (templates === void 0) return [];
631
+ if (!Array.isArray(templates)) throw new CreateConfigSchemaError("create.templates must be an array");
632
+ const makeError = (message) => new CreateConfigSchemaError(message);
633
+ return validateTemplateEntries(templates, "create.templates", makeError, (entry, index) => {
634
+ const validated = validateTemplateEntry(entry, index, "create.templates", makeError);
635
+ if (validated.name.startsWith("vite:")) throw makeError(`create.templates[${index}].name uses the reserved \`vite:\` prefix`);
636
+ return validated;
637
+ });
750
638
  }
751
- //#endregion
752
- //#region src/create/org-tarball.ts
753
- function getCacheRoot() {
754
- const home = process.env.VP_HOME || path.join(os.homedir(), ".vite-plus");
755
- return path.join(home, "tmp", "create-org");
639
+ async function fetchPackument(scope, packageName) {
640
+ const response = await fetchNpmResource(`${getNpmRegistry(scope)}/${packageName}`, {
641
+ headers: { accept: "application/json" },
642
+ timeoutMs: 5e3
643
+ });
644
+ if (response.status === 404) return null;
645
+ if (!response.ok) throw new Error(`npm registry responded with ${response.status} for ${packageName}`);
646
+ return await response.json();
756
647
  }
757
648
  /**
758
- * Replace characters that are illegal in Windows path segments
759
- * (`\ / : * ? " < > |` plus the IPv6 bracket pair `[ ]`). The host
760
- * comes from `new URL(...).host` which can carry a port (`:4873`) or
761
- * IPv6 literal (`[::1]`); both end up in the cache path otherwise.
649
+ * Fetch `@scope/create` from the npm registry and parse its `createConfig.templates`
650
+ * manifest.
651
+ *
652
+ * Returns `null` when:
653
+ * - the package does not exist on the registry (404), or
654
+ * - the package exists but has no `createConfig.templates` field
655
+ *
656
+ * When the packument version metadata lacks `createConfig` entirely, the
657
+ * published tarball's package.json is consulted before giving up — some
658
+ * registries (GitHub Packages among them) strip custom fields from version
659
+ * metadata while preserving the tarball byte-for-byte.
660
+ *
661
+ * Throws when:
662
+ * - the `createConfig.templates` field is present but malformed (`OrgManifestSchemaError`), or
663
+ * - the registry request fails for any non-404 reason
664
+ *
665
+ * `requestedVersion` pins the lookup to a specific `versions[...]` entry
666
+ * (equivalent to `vp create @scope@1.2.3`); omit it to resolve `dist-tags.latest`.
762
667
  */
763
- function sanitizeHostForPath(host) {
764
- return host.replaceAll(/[\\/:*?"<>|[\]]/g, "_");
668
+ async function readOrgManifest(scope, requestedVersion) {
669
+ if (!scope.startsWith("@")) return null;
670
+ const packageName = `${scope}/create`;
671
+ const packument = await fetchPackument(scope, packageName);
672
+ if (!packument) return null;
673
+ let resolvedVersion;
674
+ if (requestedVersion) {
675
+ resolvedVersion = packument["dist-tags"]?.[requestedVersion] ?? (packument.versions?.[requestedVersion] ? requestedVersion : void 0);
676
+ if (!resolvedVersion) throw new OrgManifestSchemaError(`version "${requestedVersion}" not found (known tags: ${Object.keys(packument["dist-tags"] ?? {}).join(", ") || "none"})`, packageName);
677
+ } else {
678
+ resolvedVersion = packument["dist-tags"]?.latest;
679
+ if (!resolvedVersion) return null;
680
+ }
681
+ const meta = packument.versions?.[resolvedVersion];
682
+ if (!meta) return null;
683
+ let templates = validateManifest(meta, packageName);
684
+ if (!templates && meta.createConfig === void 0 && meta.dist?.tarball) templates = validateManifest(await readPackageJsonFromTarball(meta.dist.tarball, meta.dist.integrity).catch(() => null), packageName);
685
+ if (!templates) return null;
686
+ if (!meta.dist?.tarball) throw new OrgManifestSchemaError(`missing dist.tarball for ${resolvedVersion}`, packageName);
687
+ return {
688
+ scope,
689
+ packageName,
690
+ version: resolvedVersion,
691
+ tarballUrl: meta.dist.tarball,
692
+ integrity: meta.dist.integrity,
693
+ templates
694
+ };
765
695
  }
766
696
  /**
767
- * Cache extracted tarballs under `<host>/<scope>/create/<version>` so two
768
- * repos resolving the same `<scope>@<version>` through different registries
769
- * (via `.npmrc` scope mappings) don't share a cache slot. The registry
770
- * guarantees `manifest.tarballUrl` is a valid URL, so any parse failure
771
- * here is a real bug worth surfacing.
697
+ * Apply the in-monorepo filter rule from the RFC: entries with
698
+ * `monorepo: true` are hidden when the command is invoked inside an
699
+ * existing monorepo, mirroring `initial-template-options.ts:9-31`.
772
700
  */
773
- function getExtractionDir(manifest) {
774
- const { host } = new URL(manifest.tarballUrl);
775
- return path.join(getCacheRoot(), sanitizeHostForPath(host), manifest.scope, "create", manifest.version);
701
+ function filterManifestForContext(templates, isMonorepo) {
702
+ if (!isMonorepo) return [...templates];
703
+ return templates.filter((entry) => !entry.monorepo);
776
704
  }
777
- function parseIntegrity(integrity) {
778
- const match = integrity.split(/\s+/)[0].match(/^(sha\d+)-(.+)$/);
779
- if (!match) return null;
780
- return {
781
- algorithm: match[1],
782
- expected: match[2]
783
- };
705
+ //#endregion
706
+ //#region src/create/templates/types.ts
707
+ const LibraryTemplateRepo = "github:sxzz/tsdown-templates/vite-plus";
708
+ const BuiltinTemplate = {
709
+ generator: "vite:generator",
710
+ monorepo: "vite:monorepo",
711
+ application: "vite:application",
712
+ library: "vite:library"
713
+ };
714
+ const TemplateType = {
715
+ builtin: "builtin",
716
+ bingo: "bingo",
717
+ remote: "remote",
718
+ bundled: "bundled"
719
+ };
720
+ //#endregion
721
+ //#region src/create/discovery.ts
722
+ function isGitHubUrl(templateName) {
723
+ return templateName.startsWith("https://github.com/") || templateName.startsWith("github:") || templateName.includes("github.com/");
784
724
  }
785
- function verifyIntegrity(bytes, integrity) {
786
- if (!integrity) return;
787
- const parsed = parseIntegrity(integrity);
788
- if (!parsed) return;
789
- const hash = createHash(parsed.algorithm);
790
- hash.update(bytes);
791
- const actual = hash.digest("base64");
792
- if (actual !== parsed.expected) throw new Error(`integrity check failed: expected ${integrity}, got ${parsed.algorithm}-${actual}`);
725
+ function parseGitHubUrl(url) {
726
+ if (url.startsWith("github:")) return url.slice(7);
727
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
728
+ if (match) return match[1].replace(/\.git$/, "");
729
+ return null;
793
730
  }
794
- const MAX_TARBALL_BYTES = 50 * 1024 * 1024;
795
- async function downloadTarball(url) {
796
- const response = await fetchNpmResource(url, { timeoutMs: 3e4 });
797
- if (!response.ok) throw new Error(`failed to download tarball (${response.status}): ${url}`);
798
- const contentLength = Number(response.headers.get("content-length"));
799
- if (Number.isFinite(contentLength) && contentLength > MAX_TARBALL_BYTES) throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
800
- const reader = response.body?.getReader();
801
- if (!reader) throw new Error(`tarball response has no body: ${url}`);
802
- const chunks = [];
803
- let total = 0;
804
- while (true) {
805
- const { done, value } = await reader.read();
806
- if (done) break;
807
- total += value.byteLength;
808
- if (total > MAX_TARBALL_BYTES) {
809
- await reader.cancel();
810
- throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
811
- }
812
- chunks.push(value);
813
- }
814
- const bytes = new Uint8Array(total);
815
- let offset = 0;
816
- for (const chunk of chunks) {
817
- bytes.set(chunk, offset);
818
- offset += chunk.byteLength;
819
- }
820
- return bytes;
731
+ function inferGitHubRepoName(templateName) {
732
+ const degitPath = parseGitHubUrl(templateName);
733
+ if (!degitPath) return null;
734
+ return degitPath.split("/").pop() || null;
821
735
  }
822
- const STAGING_SUFFIX_PREFIX = ".tmp-";
823
- /**
824
- * Parse a tar entry's stored mode (always octal) into the numeric
825
- * permission bits (low 9 bits — `rwxrwxrwx`). Returns `undefined` when
826
- * the mode is missing or unparsable so the caller leaves the file with
827
- * its default (umask-derived) permissions instead of downgrading.
828
- */
829
- function parseEntryMode(raw) {
830
- if (!raw) return;
831
- const parsed = Number.parseInt(raw, 8);
832
- if (!Number.isFinite(parsed)) return;
833
- return parsed & 511;
736
+ function localTemplateDir(workspaceInfo, templateName) {
737
+ if (isRelativePath(templateName)) return templateName.replace(/^\.\//, "");
738
+ return workspaceInfo.packages.find((pkg) => pkg.name === templateName)?.path;
739
+ }
740
+ function resolveLocalBinPath(localPackagePath, packageName, bin) {
741
+ if (!bin) return;
742
+ if (typeof bin === "string") return path.join(localPackagePath, bin);
743
+ const entries = Object.entries(bin);
744
+ if (entries.length === 0) return;
745
+ if (entries.length === 1) return path.join(localPackagePath, entries[0][1]);
746
+ const unscopedName = packageName.slice(packageName.lastIndexOf("/") + 1);
747
+ const preferred = bin[packageName] ?? bin[unscopedName];
748
+ if (preferred) return path.join(localPackagePath, preferred);
749
+ throw new Error(`Local template package "${packageName}" defines multiple "bin" entries (${entries.map(([name]) => name).join(", ")}); add a "bin" entry named "${packageName}" so the template entry is unambiguous`);
750
+ }
751
+ function discoverTemplate(templateName, templateArgs, workspaceInfo, interactive, bundledLocalPath, skipShorthand, localTemplate) {
752
+ const envs = prependToPathToEnvs(workspaceInfo.downloadPackageManager.binPrefix, { ...process.env });
753
+ const parentDir = inferParentDir(templateName, workspaceInfo, localTemplate);
754
+ if (bundledLocalPath) return {
755
+ command: "",
756
+ args: [...templateArgs],
757
+ envs,
758
+ type: TemplateType.bundled,
759
+ parentDir,
760
+ interactive,
761
+ localPath: bundledLocalPath
762
+ };
763
+ if (templateName.startsWith("vite:")) return {
764
+ command: templateName,
765
+ args: [...templateArgs],
766
+ envs,
767
+ type: TemplateType.builtin,
768
+ parentDir,
769
+ interactive
770
+ };
771
+ if (isGitHubUrl(templateName)) {
772
+ const degitPath = parseGitHubUrl(templateName);
773
+ if (degitPath) return {
774
+ command: "degit",
775
+ args: [degitPath, ...templateArgs],
776
+ envs,
777
+ type: TemplateType.remote,
778
+ parentDir,
779
+ interactive
780
+ };
781
+ }
782
+ if (localTemplate) {
783
+ const localDir = localTemplateDir(workspaceInfo, templateName);
784
+ if (!localDir) throw new Error(`Local template "${templateName}" does not match any workspace package; update the \`create.templates\` entry in vite.config.ts`);
785
+ const localPackagePath = path.join(workspaceInfo.rootDir, localDir);
786
+ const packageJsonPath = path.join(localPackagePath, "package.json");
787
+ if (!fs.existsSync(packageJsonPath)) throw new Error(`Local template "${templateName}" has no package.json, so it cannot be run as a template`);
788
+ const pkg = readJsonFile(packageJsonPath);
789
+ const binPath = resolveLocalBinPath(localPackagePath, pkg.name ?? templateName, pkg.bin);
790
+ if (!binPath) throw new Error(`Local template "${templateName}" has no "bin" entry in its package.json, so it cannot be run as a template`);
791
+ const args = [binPath, ...templateArgs];
792
+ let type = TemplateType.remote;
793
+ if (isBingoTemplate(pkg)) {
794
+ type = TemplateType.bingo;
795
+ args.push("--skip-requests");
796
+ }
797
+ return {
798
+ command: "node",
799
+ args,
800
+ envs,
801
+ type,
802
+ parentDir,
803
+ interactive
804
+ };
805
+ }
806
+ return {
807
+ command: skipShorthand ? templateName : expandCreateShorthand(templateName),
808
+ args: [...templateArgs],
809
+ envs,
810
+ type: TemplateType.remote,
811
+ parentDir,
812
+ interactive
813
+ };
834
814
  }
835
815
  /**
836
- * Strip the `package/` prefix from an `npm pack` tarball entry. Returns
837
- * `null` for entries to skip (root dir, PaxHeader, anything outside
838
- * `package/`).
816
+ * Expand shorthand template names to their full `create-*` package names.
817
+ *
818
+ * This follows the same convention as `npm create` / `pnpm create`:
819
+ * - `vite` → `create-vite`
820
+ * - `vite@latest` → `create-vite@latest`
821
+ *
822
+ * Special cases for packages where the convention doesn't work:
823
+ * - `nitro` → `create-nitro-app` (create-nitro is abandoned)
824
+ * - `svelte` → `sv`
825
+ * - `@tanstack/start` → `@tanstack/cli` (@tanstack/create-start is deprecated)
826
+ *
827
+ * Skips expansion for:
828
+ * - Builtin templates (`vite:*`)
829
+ * - GitHub URLs
830
+ * - Local paths (`./`, `../`, `/`)
831
+ * - Names already starting with `create-` (or `@scope/create-`)
839
832
  */
840
- function normalizeEntryName(rawName) {
841
- const name = rawName.replace(/^\.\//, "").replace(/\\/g, "/");
842
- if (!name || name === "package" || name === "package/") return null;
843
- if (name.startsWith("PaxHeader/") || name.includes("/PaxHeader/")) return null;
844
- if (!name.startsWith("package/")) return null;
845
- return name.slice(8);
846
- }
847
- async function extractTarballTo(bytes, destDir) {
848
- const entries = await parseTarGzip(bytes);
849
- const stagingDir = `${destDir}${STAGING_SUFFIX_PREFIX}${process.pid}-${Date.now()}`;
850
- await fs.promises.mkdir(stagingDir, { recursive: true });
851
- const resolvedStaging = path.resolve(stagingDir);
852
- try {
853
- for (const entry of entries) {
854
- const relativeName = normalizeEntryName(entry.name);
855
- if (relativeName === null) continue;
856
- const targetPath = path.join(stagingDir, relativeName);
857
- const resolvedTarget = path.resolve(targetPath);
858
- if (resolvedTarget !== resolvedStaging && !resolvedTarget.startsWith(`${resolvedStaging}${path.sep}`)) throw new Error(`tarball entry escapes extraction root: ${entry.name}`);
859
- if (entry.type === "directory" || relativeName.endsWith("/")) {
860
- await fs.promises.mkdir(targetPath, { recursive: true });
861
- continue;
862
- }
863
- await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
864
- const data = entry.data ?? new Uint8Array(0);
865
- await fs.promises.writeFile(targetPath, data);
866
- const mode = parseEntryMode(entry.attrs?.mode);
867
- if (mode !== void 0) await fs.promises.chmod(targetPath, mode);
868
- }
869
- try {
870
- await fs.promises.rename(stagingDir, destDir);
871
- } catch (error) {
872
- const code = error.code;
873
- if ((code === "ENOTEMPTY" || code === "EEXIST") && fs.existsSync(path.join(destDir, "package.json"))) {
874
- await fs.promises.rm(stagingDir, {
875
- recursive: true,
876
- force: true
877
- }).catch(() => {});
878
- return;
879
- }
880
- throw error;
833
+ function expandCreateShorthand(templateName) {
834
+ if (templateName.includes(":")) return templateName;
835
+ if (isGitHubUrl(templateName)) return templateName;
836
+ if (templateName.startsWith("./") || templateName.startsWith("../") || templateName.startsWith("/")) return templateName;
837
+ if (templateName.startsWith("@")) {
838
+ const slashIndex = templateName.indexOf("/");
839
+ if (slashIndex === -1) {
840
+ const atIndex = templateName.indexOf("@", 1);
841
+ return `${atIndex === -1 ? templateName : templateName.slice(0, atIndex)}/create${atIndex === -1 ? "" : templateName.slice(atIndex)}`;
881
842
  }
882
- } catch (error) {
883
- await fs.promises.rm(stagingDir, {
884
- recursive: true,
885
- force: true
886
- }).catch(() => {});
887
- throw error;
843
+ const scope = templateName.slice(0, slashIndex);
844
+ const rest = templateName.slice(slashIndex + 1);
845
+ const atIndex = rest.indexOf("@");
846
+ const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
847
+ const version = atIndex === -1 ? "" : rest.slice(atIndex);
848
+ if (name.startsWith("create-")) return templateName;
849
+ if (scope === "@tanstack" && name === "start") return `@tanstack/cli${version}`;
850
+ return `${scope}/create-${name}${version}`;
888
851
  }
852
+ const atIndex = templateName.indexOf("@");
853
+ const name = atIndex === -1 ? templateName : templateName.slice(0, atIndex);
854
+ const version = atIndex === -1 ? "" : templateName.slice(atIndex);
855
+ if (name.startsWith("create-")) return templateName;
856
+ if (name === "nitro") return `create-nitro-app${version}`;
857
+ if (name === "svelte") return `sv${version}`;
858
+ return `create-${name}${version}`;
889
859
  }
890
- const STAGING_STALE_MS = 1440 * 60 * 1e3;
891
- /**
892
- * Remove `<destDir>.tmp-*` siblings left behind by a previous crash so
893
- * repeated aborts don't accumulate orphaned staging trees. Only deletes
894
- * entries whose mtime is older than 24 hours — a concurrent `vp create`
895
- * that's still actively extracting will always be younger than that, so
896
- * the age gate keeps this safe to run at the top of every extract.
897
- */
898
- async function cleanupStaleStagingDirs(destDir) {
899
- const parent = path.dirname(destDir);
900
- const prefix = `${path.basename(destDir)}${STAGING_SUFFIX_PREFIX}`;
901
- let entries;
902
- try {
903
- entries = await fs.promises.readdir(parent);
904
- } catch {
905
- return;
860
+ function inferParentDir(templateName, workspaceInfo, localTemplate = false) {
861
+ if (workspaceInfo.parentDirs.length === 0) return;
862
+ const localDir = localTemplate ? localTemplateDir(workspaceInfo, templateName) : void 0;
863
+ if (localDir) {
864
+ const ownParentDir = path.dirname(localDir);
865
+ if (workspaceInfo.parentDirs.includes(ownParentDir)) return ownParentDir;
906
866
  }
907
- const cutoff = Date.now() - STAGING_STALE_MS;
908
- await Promise.all(entries.filter((name) => name.startsWith(prefix)).map(async (name) => {
909
- const fullPath = path.join(parent, name);
910
- try {
911
- if ((await fs.promises.stat(fullPath)).mtimeMs < cutoff) await fs.promises.rm(fullPath, {
912
- recursive: true,
913
- force: true
914
- });
915
- } catch {}
916
- }));
867
+ let rule = /app/i;
868
+ if (templateName === BuiltinTemplate.library) rule = /lib|component|package/i;
869
+ else if (templateName === BuiltinTemplate.generator) rule = /generator|tool/i;
870
+ for (const parentDir of workspaceInfo.parentDirs) if (rule.test(parentDir)) return parentDir;
871
+ }
872
+ //#endregion
873
+ //#region src/create/initial-template-options.ts
874
+ function getInitialTemplateOptions(isMonorepo, templates = []) {
875
+ return [
876
+ ...!isMonorepo ? [{
877
+ label: "Vite+ Monorepo",
878
+ value: BuiltinTemplate.monorepo,
879
+ hint: "Create a new Vite+ monorepo project"
880
+ }] : [],
881
+ {
882
+ label: "Vite+ Application",
883
+ value: BuiltinTemplate.application,
884
+ hint: "Create vite applications"
885
+ },
886
+ {
887
+ label: "Vite+ Library",
888
+ value: BuiltinTemplate.library,
889
+ hint: "Create vite libraries"
890
+ },
891
+ ...isMonorepo ? templates.map((entry) => ({
892
+ label: entry.name,
893
+ value: entry.name,
894
+ hint: entry.description
895
+ })) : []
896
+ ];
917
897
  }
898
+ //#endregion
899
+ //#region src/create/org-picker.ts
900
+ const ORG_PICKER_CANCEL = Symbol("org-picker-cancel");
901
+ const ORG_PICKER_BUILTIN_ESCAPE = Symbol("org-picker-builtin-escape");
902
+ const ESCAPE_HATCH = Symbol("builtin-escape");
918
903
  /**
919
- * Ensure the `@org/create` package tarball for the given manifest has been
920
- * downloaded and extracted locally. Returns the absolute path to the
921
- * extracted package root (i.e. the directory that contains
922
- * `package.json`).
904
+ * Render the interactive picker for an org manifest. Always appends a
905
+ * trailing "Vite+ built-in templates" escape-hatch entry.
923
906
  *
924
- * Idempotent: subsequent calls for the same `<scope, version>` reuse the
925
- * cached extraction. Concurrent calls race on the final rename; the loser
926
- * cleans up and returns the existing directory.
907
+ * Context-filters entries with `monorepo: true` when running inside an
908
+ * existing monorepo, mirroring `initial-template-options.ts:9-31`.
909
+ *
910
+ * Returns `ORG_PICKER_BUILTIN_ESCAPE` when the escape hatch is selected,
911
+ * or `ORG_PICKER_CANCEL` when the user hits Ctrl-C.
927
912
  */
928
- async function ensureOrgPackageExtracted(manifest) {
929
- const extractedRoot = getExtractionDir(manifest);
930
- if (fs.existsSync(path.join(extractedRoot, "package.json"))) return extractedRoot;
931
- const parent = path.dirname(extractedRoot);
932
- await fs.promises.mkdir(parent, { recursive: true });
933
- await cleanupStaleStagingDirs(extractedRoot);
934
- const bytes = await downloadTarball(manifest.tarballUrl);
935
- verifyIntegrity(bytes, manifest.integrity);
936
- await extractTarballTo(bytes, extractedRoot);
937
- return extractedRoot;
913
+ async function pickOrgTemplate(manifest, opts) {
914
+ const filtered = filterManifestForContext(manifest.templates, opts.isMonorepo);
915
+ if (filtered.length === 0) return ORG_PICKER_BUILTIN_ESCAPE;
916
+ const escapeValue = `__vp_builtin_escape__::${randomUUID()}`;
917
+ const lookup = /* @__PURE__ */ new Map();
918
+ const options = filtered.map((entry) => {
919
+ lookup.set(entry.name, entry);
920
+ return {
921
+ value: entry.name,
922
+ label: entry.name,
923
+ hint: entry.description
924
+ };
925
+ });
926
+ lookup.set(escapeValue, ESCAPE_HATCH);
927
+ const builtinHint = opts.isMonorepo ? "Use defaults (application / library)" : "Use defaults (monorepo / application / library)";
928
+ options.push({
929
+ value: escapeValue,
930
+ label: "Vite+ built-in templates",
931
+ hint: builtinHint
932
+ });
933
+ const picked = await select({
934
+ message: `Pick a template from ${manifest.scope}`,
935
+ options
936
+ });
937
+ if (isCancel(picked)) return ORG_PICKER_CANCEL;
938
+ const found = lookup.get(picked);
939
+ if (found === ESCAPE_HATCH) return ORG_PICKER_BUILTIN_ESCAPE;
940
+ if (!found) throw new Error(`org-picker: prompts.select returned an unregistered value: ${picked}`);
941
+ return {
942
+ kind: "entry",
943
+ entry: found
944
+ };
938
945
  }
939
946
  /**
940
- * Resolve a manifest entry's relative `./...` path against an already-
941
- * extracted package root, rejecting any path that escapes the root (via
942
- * `..` walks or an absolute specifier).
943
- *
944
- * Existence is NOT checked here — the subsequent `copyDir` surfaces any
945
- * missing-directory error with a clearer errno.
947
+ * Render the manifest as a plain-text table for the `--no-interactive`
948
+ * error output. Fixed column order so AI agents and scripts can recover
949
+ * available template names without a `--json` flag.
946
950
  */
947
- function resolveBundledPath(extractedRoot, relativePath) {
948
- if (path.isAbsolute(relativePath)) throw new Error(`bundled template path must be relative, got ${relativePath}`);
949
- const resolvedRoot = path.resolve(extractedRoot);
950
- const resolvedTarget = path.resolve(extractedRoot, relativePath);
951
- if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) throw new Error(`bundled template path escapes the package root: ${relativePath}`);
952
- return resolvedTarget;
951
+ function formatManifestTable(manifest, isMonorepo) {
952
+ const visible = filterManifestForContext(manifest.templates, isMonorepo);
953
+ const filteredCount = manifest.templates.length - visible.length;
954
+ const nameWidth = Math.max(4, ...visible.map((entry) => entry.name.length));
955
+ const descWidth = Math.max(11, ...visible.map((entry) => entry.description.length));
956
+ const lines = [];
957
+ lines.push(` ${"NAME".padEnd(nameWidth)} ${"DESCRIPTION".padEnd(descWidth)} TEMPLATE`);
958
+ for (const entry of visible) lines.push(` ${entry.name.padEnd(nameWidth)} ${entry.description.padEnd(descWidth)} ${entry.template}`);
959
+ return {
960
+ lines,
961
+ filteredCount
962
+ };
953
963
  }
954
964
  //#endregion
955
965
  //#region ../../node_modules/.pnpm/validate-npm-package-name@7.0.2/node_modules/validate-npm-package-name/lib/builtin-modules.json
@@ -4019,7 +4029,7 @@ async function promptPackageNameAndTargetDir(defaultPackageName, interactive) {
4019
4029
  return result?.errors?.[0] ?? result?.warnings?.[0] ?? "Invalid package name";
4020
4030
  }
4021
4031
  });
4022
- if (R(selected)) cancelAndExit();
4032
+ if (isCancel(selected)) cancelAndExit();
4023
4033
  packageName = selected;
4024
4034
  targetDir = getProjectDirFromPackageName(packageName);
4025
4035
  } else {
@@ -4041,7 +4051,7 @@ async function promptTargetDir(defaultTargetDir, interactive, options) {
4041
4051
  defaultValue: defaultTargetDir,
4042
4052
  validate: (value) => validateTargetDir(value ?? defaultTargetDir, options?.cwd).error
4043
4053
  });
4044
- if (R(selected)) cancelAndExit();
4054
+ if (isCancel(selected)) cancelAndExit();
4045
4055
  targetDir = validateTargetDir(selected ?? defaultTargetDir, options?.cwd).directory;
4046
4056
  } else {
4047
4057
  targetDir = validateTargetDir(defaultTargetDir, options?.cwd).directory;
@@ -4074,7 +4084,7 @@ async function checkProjectDirExists(projectDirFullPath, interactive) {
4074
4084
  value: "yes"
4075
4085
  }]
4076
4086
  });
4077
- if (R(overwrite)) cancelAndExit();
4087
+ if (isCancel(overwrite)) cancelAndExit();
4078
4088
  switch (overwrite) {
4079
4089
  case "yes":
4080
4090
  emptyDir(projectDirFullPath);
@@ -4352,7 +4362,7 @@ async function executeGeneratorScaffold(workspaceInfo, templateInfo, options) {
4352
4362
  placeholder: defaultDescription,
4353
4363
  defaultValue: defaultDescription
4354
4364
  });
4355
- if (!R(descPrompt)) description = descPrompt;
4365
+ if (!isCancel(descPrompt)) description = descPrompt;
4356
4366
  }
4357
4367
  const fullPath = path.join(workspaceInfo.rootDir, templateInfo.targetDir);
4358
4368
  copyDir(path.join(templatesDir, "generator"), fullPath);
@@ -4665,7 +4675,7 @@ const helpMessage = renderCliDoc({
4665
4675
  },
4666
4676
  {
4667
4677
  label: "--git",
4668
- description: "Initialize a git repository with an initial commit"
4678
+ description: "Initialize a git repository"
4669
4679
  },
4670
4680
  {
4671
4681
  label: "--no-git",
@@ -5025,7 +5035,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5025
5035
  message: "",
5026
5036
  options: getInitialTemplateOptions(isMonorepo, localTemplates)
5027
5037
  });
5028
- if (R(template)) cancelAndExit();
5038
+ if (isCancel(template)) cancelAndExit();
5029
5039
  selectedTemplateName = template;
5030
5040
  }
5031
5041
  const matchedLocalTemplate = resolvedByOrg ? void 0 : localTemplates.find((entry) => entry.name === selectedTemplateName);
@@ -5073,7 +5083,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5073
5083
  options: dirOptions,
5074
5084
  initialValue: shouldOfferCwdOption ? cwdRelativeToRoot : inferParentDir(selectedTemplateName, workspaceInfoOptional, isLocalTemplate) ?? workspaceInfoOptional.parentDirs[0]
5075
5085
  });
5076
- if (R(selected)) cancelAndExit();
5086
+ if (isCancel(selected)) cancelAndExit();
5077
5087
  if (selected !== "other") parentDir = selected;
5078
5088
  }
5079
5089
  if (!parentDir) {
@@ -5084,7 +5094,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5084
5094
  return value ? formatTargetDir(value).error : "Target directory is required";
5085
5095
  }
5086
5096
  });
5087
- if (R(customTargetDir)) cancelAndExit();
5097
+ if (isCancel(customTargetDir)) cancelAndExit();
5088
5098
  parentDir = customTargetDir;
5089
5099
  }
5090
5100
  selectedParentDir = parentDir;
@@ -5199,7 +5209,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5199
5209
  let migratePendingBuilds = [];
5200
5210
  const handleIgnoredBuilds = async (projectPath, installCwd, summary) => {
5201
5211
  if (summary?.status !== "installed") return;
5202
- const reportedBuilds = [...new Set([...summary.pendingBuilds ?? [], ...migratePendingBuilds])];
5212
+ const reportedBuilds = [.../* @__PURE__ */ new Set([...summary.pendingBuilds ?? [], ...migratePendingBuilds])];
5203
5213
  const targets = resolveApproveBuildTargets(projectPath, await detectGatedBuilds(installCwd, workspaceInfo.packageManager, reportedBuilds), workspaceInfo.packageManager);
5204
5214
  if (targets.length === 0) return;
5205
5215
  pauseCreateProgress();
@@ -5235,7 +5245,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5235
5245
  initialValue: true
5236
5246
  });
5237
5247
  resumeCreateProgress();
5238
- if (R(selected)) {
5248
+ if (isCancel(selected)) {
5239
5249
  log.info("Operation cancelled. Skipping git initialization");
5240
5250
  shouldInitGit = false;
5241
5251
  } else shouldInitGit = selected;
@@ -5313,14 +5323,6 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5313
5323
  await handleIgnoredBuilds(fullPath, fullPath, installSummary);
5314
5324
  updateCreateProgress("Formatting code");
5315
5325
  await runViteFmt(fullPath, options.interactive, void 0, { silent: compactOutput });
5316
- if (shouldSetupGit) {
5317
- updateCreateProgress("Creating initial commit");
5318
- const commitResult = await createInitialCommit(fullPath);
5319
- if (!commitResult.success) {
5320
- log.warn("Initial commit failed");
5321
- if (commitResult.output) log.info(commitResult.output);
5322
- }
5323
- }
5324
5326
  clearCreateProgress();
5325
5327
  showCreateSummary({
5326
5328
  description: describeScaffold(selectedTemplateName, selectedTemplateArgs),
@@ -5459,7 +5461,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5459
5461
  required: false
5460
5462
  });
5461
5463
  let selectedDepTypes = [];
5462
- if (!R(selectedDepTypeOptions)) selectedDepTypes = selectedDepTypeOptions;
5464
+ if (!isCancel(selectedDepTypeOptions)) selectedDepTypes = selectedDepTypeOptions;
5463
5465
  for (const selectedDepType of selectedDepTypes) {
5464
5466
  const selected = await multiselect({
5465
5467
  message: `Which packages should be added as ${selectedDepType} to ${success(projectDir)}?`,
@@ -5470,7 +5472,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5470
5472
  required: false
5471
5473
  });
5472
5474
  let selectedDeps = [];
5473
- if (!R(selected)) selectedDeps = selected;
5475
+ if (!isCancel(selected)) selectedDeps = selected;
5474
5476
  if (selectedDeps.length > 0) updatePackageJsonWithDeps(workspaceInfo.rootDir, projectDir, selectedDeps, selectedDepType);
5475
5477
  }
5476
5478
  resumeCreateProgress();
@@ -5512,14 +5514,6 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
5512
5514
  await handleIgnoredBuilds(fullPath, fullPath, installSummary);
5513
5515
  updateCreateProgress("Formatting code");
5514
5516
  await runViteFmt(fullPath, options.interactive, void 0, { silent: compactOutput });
5515
- if (shouldSetupGit) {
5516
- updateCreateProgress("Creating initial commit");
5517
- const commitResult = await createInitialCommit(fullPath);
5518
- if (!commitResult.success) {
5519
- log.warn("Initial commit failed");
5520
- if (commitResult.output) log.info(commitResult.output);
5521
- }
5522
- }
5523
5517
  }
5524
5518
  clearCreateProgress();
5525
5519
  showCreateSummary({