vite-plus 0.2.2 → 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.
- package/binding/index.cjs +52 -52
- package/dist/{agent-D7O7mSeO.js → agent-D3nZAMGI.js} +4 -4
- package/dist/bin.js +4 -4
- package/dist/config/bin.js +2 -2
- package/dist/{constants-NeTOxrzV.js → constants-CZ7Mnq-C.js} +1 -1
- package/dist/create/bin.js +793 -761
- package/dist/{define-config-CSgy0zML.js → define-config-D2jo1xVA.js} +2 -2
- package/dist/{define-config-CrA3lZhQ.cjs → define-config-Dut6XD42.cjs} +1 -1
- package/dist/define-config.cjs +1 -1
- package/dist/define-config.js +1 -1
- package/dist/{editor-CPzssglc.js → editor-D1BZiU1h.js} +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/migration/bin.js +5 -5
- package/dist/{oxlint-plugin-config-C2Rqc_WQ.js → oxlint-plugin-config-BSbv66Qn.js} +1 -1
- package/dist/oxlint-plugin.js +1 -1
- package/dist/pack-bin.js +1 -1
- package/dist/{package-CU2g7URl.js → package-Cpo2ei_V.js} +1 -1
- package/dist/{resolve-vite-config-r91rIaPs.js → resolve-vite-config-DX2o0SWM.js} +1 -1
- package/dist/staged/bin.js +1 -1
- package/dist/{tsconfig-fvpxgUq2.js → tsconfig-D6sTl0dN.js} +48 -12
- package/dist/version.js +2 -2
- package/dist/versions.js +1 -1
- package/docs/guide/docker.md +3 -3
- package/docs/guide/env.md +1 -1
- package/docs/guide/install.md +1 -1
- package/docs/guide/troubleshooting.md +2 -2
- package/docs/guide/upgrade.md +1 -1
- package/docs/package.json +2 -1
- package/docs/vite.config.ts +17 -0
- package/package.json +12 -12
package/dist/create/bin.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "../rolldown-runtime-C7HZzL1F.js";
|
|
2
|
-
import { s as VITE_PLUS_NAME } from "../constants-
|
|
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-
|
|
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
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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";
|
|
@@ -105,829 +105,861 @@ function prependToPathToEnvs(extraPath, envs) {
|
|
|
105
105
|
return envs;
|
|
106
106
|
}
|
|
107
107
|
//#endregion
|
|
108
|
-
//#region
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return version ? {
|
|
132
|
-
scope: spec.slice(0, atIndex),
|
|
133
|
-
version
|
|
134
|
-
} : { scope: spec.slice(0, atIndex) };
|
|
135
|
-
}
|
|
136
|
-
const scope = spec.slice(0, colonIndex);
|
|
137
|
-
const rest = spec.slice(colonIndex + 1);
|
|
138
|
-
const atIndex = rest.indexOf("@");
|
|
139
|
-
const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
|
|
140
|
-
const version = atIndex === -1 ? "" : rest.slice(atIndex + 1);
|
|
141
|
-
if (!name) return version ? {
|
|
142
|
-
scope,
|
|
143
|
-
version
|
|
144
|
-
} : { scope };
|
|
145
|
-
return version ? {
|
|
146
|
-
scope,
|
|
147
|
-
name,
|
|
148
|
-
version
|
|
149
|
-
} : {
|
|
150
|
-
scope,
|
|
151
|
-
name
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Schema-level failure. Never falls through silently — a maintainer who
|
|
156
|
-
* shipped an invalid manifest should see the offending field.
|
|
157
|
-
*/
|
|
158
|
-
var OrgManifestSchemaError = class extends Error {
|
|
159
|
-
packageName;
|
|
160
|
-
constructor(message, packageName) {
|
|
161
|
-
super(`${packageName}: ${message}`);
|
|
162
|
-
this.packageName = packageName;
|
|
163
|
-
this.name = "OrgManifestSchemaError";
|
|
164
|
-
}
|
|
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"
|
|
165
131
|
};
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
190
212
|
}
|
|
191
|
-
return
|
|
192
|
-
name,
|
|
193
|
-
description,
|
|
194
|
-
template
|
|
195
|
-
};
|
|
213
|
+
return files;
|
|
196
214
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const seen = /* @__PURE__ */ new Set();
|
|
204
|
-
for (let index = 0; index < templates.length; index += 1) {
|
|
205
|
-
const entry = validateOne(templates[index], index);
|
|
206
|
-
if (seen.has(entry.name)) throw makeError(`${label}[${index}].name duplicates an earlier entry: "${entry.name}"`);
|
|
207
|
-
seen.add(entry.name);
|
|
208
|
-
entries.push(entry);
|
|
209
|
-
}
|
|
210
|
-
return entries;
|
|
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);
|
|
211
221
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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;
|
|
225
235
|
}
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
const templates = createConfig.templates;
|
|
231
|
-
if (templates === void 0) return null;
|
|
232
|
-
if (!Array.isArray(templates)) throw new OrgManifestSchemaError("createConfig.templates must be an array", packageName);
|
|
233
|
-
if (templates.length === 0) return null;
|
|
234
|
-
return validateTemplateEntries(templates, "createConfig.templates", (message) => new OrgManifestSchemaError(message, packageName), (entry, index) => validateEntry(entry, index, packageName));
|
|
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));
|
|
235
240
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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];
|
|
244
253
|
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Validate `create.templates` from `vite.config.ts`. Returns `[]` when the field
|
|
248
|
-
* is absent or an empty array; throws {@link CreateConfigSchemaError} when present
|
|
249
|
-
* but malformed.
|
|
250
|
-
*/
|
|
251
|
-
function validateCreateTemplates(templates) {
|
|
252
|
-
if (templates === void 0) return [];
|
|
253
|
-
if (!Array.isArray(templates)) throw new CreateConfigSchemaError("create.templates must be an array");
|
|
254
|
-
const makeError = (message) => new CreateConfigSchemaError(message);
|
|
255
|
-
return validateTemplateEntries(templates, "create.templates", makeError, (entry, index) => {
|
|
256
|
-
const validated = validateTemplateEntry(entry, index, "create.templates", makeError);
|
|
257
|
-
if (validated.name.startsWith("vite:")) throw makeError(`create.templates[${index}].name uses the reserved \`vite:\` prefix`);
|
|
258
|
-
return validated;
|
|
259
|
-
});
|
|
254
|
+
return headers;
|
|
260
255
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (response.status === 404) return null;
|
|
267
|
-
if (!response.ok) throw new Error(`npm registry responded with ${response.status} for ${packageName}`);
|
|
268
|
-
return await response.json();
|
|
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");
|
|
269
261
|
}
|
|
270
262
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* - the package does not exist on the registry (404), or
|
|
276
|
-
* - the package exists but has no `createConfig.templates` field
|
|
277
|
-
*
|
|
278
|
-
* Throws when:
|
|
279
|
-
* - the `createConfig.templates` field is present but malformed (`OrgManifestSchemaError`), or
|
|
280
|
-
* - the registry request fails for any non-404 reason
|
|
281
|
-
*
|
|
282
|
-
* `requestedVersion` pins the lookup to a specific `versions[...]` entry
|
|
283
|
-
* (equivalent to `vp create @scope@1.2.3`); omit it to resolve `dist-tags.latest`.
|
|
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.
|
|
284
267
|
*/
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const packageName = `${scope}/create`;
|
|
288
|
-
const packument = await fetchPackument(scope, packageName);
|
|
289
|
-
if (!packument) return null;
|
|
290
|
-
let resolvedVersion;
|
|
291
|
-
if (requestedVersion) {
|
|
292
|
-
resolvedVersion = packument["dist-tags"]?.[requestedVersion] ?? (packument.versions?.[requestedVersion] ? requestedVersion : void 0);
|
|
293
|
-
if (!resolvedVersion) throw new OrgManifestSchemaError(`version "${requestedVersion}" not found (known tags: ${Object.keys(packument["dist-tags"] ?? {}).join(", ") || "none"})`, packageName);
|
|
294
|
-
} else {
|
|
295
|
-
resolvedVersion = packument["dist-tags"]?.latest;
|
|
296
|
-
if (!resolvedVersion) return null;
|
|
297
|
-
}
|
|
298
|
-
const meta = packument.versions?.[resolvedVersion];
|
|
299
|
-
if (!meta) return null;
|
|
300
|
-
const templates = validateManifest(meta, packageName);
|
|
301
|
-
if (!templates) return null;
|
|
302
|
-
if (!meta.dist?.tarball) throw new OrgManifestSchemaError(`missing dist.tarball for ${resolvedVersion}`, packageName);
|
|
303
|
-
return {
|
|
304
|
-
scope,
|
|
305
|
-
packageName,
|
|
306
|
-
version: resolvedVersion,
|
|
307
|
-
tarballUrl: meta.dist.tarball,
|
|
308
|
-
integrity: meta.dist.integrity,
|
|
309
|
-
templates
|
|
310
|
-
};
|
|
268
|
+
function sanitizeHostForPath(host) {
|
|
269
|
+
return host.replaceAll(/[\\/:*?"<>|[\]]/g, "_");
|
|
311
270
|
}
|
|
312
271
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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.
|
|
316
277
|
*/
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
//#endregion
|
|
322
|
-
//#region src/create/templates/types.ts
|
|
323
|
-
const LibraryTemplateRepo = "github:sxzz/tsdown-templates/vite-plus";
|
|
324
|
-
const BuiltinTemplate = {
|
|
325
|
-
generator: "vite:generator",
|
|
326
|
-
monorepo: "vite:monorepo",
|
|
327
|
-
application: "vite:application",
|
|
328
|
-
library: "vite:library"
|
|
329
|
-
};
|
|
330
|
-
const TemplateType = {
|
|
331
|
-
builtin: "builtin",
|
|
332
|
-
bingo: "bingo",
|
|
333
|
-
remote: "remote",
|
|
334
|
-
bundled: "bundled"
|
|
335
|
-
};
|
|
336
|
-
//#endregion
|
|
337
|
-
//#region src/create/discovery.ts
|
|
338
|
-
function isGitHubUrl(templateName) {
|
|
339
|
-
return templateName.startsWith("https://github.com/") || templateName.startsWith("github:") || templateName.includes("github.com/");
|
|
340
|
-
}
|
|
341
|
-
function parseGitHubUrl(url) {
|
|
342
|
-
if (url.startsWith("github:")) return url.slice(7);
|
|
343
|
-
const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
344
|
-
if (match) return match[1].replace(/\.git$/, "");
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
function inferGitHubRepoName(templateName) {
|
|
348
|
-
const degitPath = parseGitHubUrl(templateName);
|
|
349
|
-
if (!degitPath) return null;
|
|
350
|
-
return degitPath.split("/").pop() || null;
|
|
278
|
+
function getExtractionDir(manifest) {
|
|
279
|
+
const { host } = new URL(manifest.tarballUrl);
|
|
280
|
+
return path.join(getCacheRoot(), sanitizeHostForPath(host), manifest.scope, "create", manifest.version);
|
|
351
281
|
}
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
282
|
+
function parseIntegrity(integrity) {
|
|
283
|
+
const match = integrity.split(/\s+/)[0].match(/^(sha\d+)-(.+)$/);
|
|
284
|
+
if (!match) return null;
|
|
285
|
+
return {
|
|
286
|
+
algorithm: match[1],
|
|
287
|
+
expected: match[2]
|
|
288
|
+
};
|
|
355
289
|
}
|
|
356
|
-
function
|
|
357
|
-
if (!
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
if (preferred) return path.join(localPackagePath, preferred);
|
|
365
|
-
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`);
|
|
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}`);
|
|
366
298
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
parentDir,
|
|
385
|
-
interactive
|
|
386
|
-
};
|
|
387
|
-
if (isGitHubUrl(templateName)) {
|
|
388
|
-
const degitPath = parseGitHubUrl(templateName);
|
|
389
|
-
if (degitPath) return {
|
|
390
|
-
command: "degit",
|
|
391
|
-
args: [degitPath, ...templateArgs],
|
|
392
|
-
envs,
|
|
393
|
-
type: TemplateType.remote,
|
|
394
|
-
parentDir,
|
|
395
|
-
interactive
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
if (localTemplate) {
|
|
399
|
-
const localDir = localTemplateDir(workspaceInfo, templateName);
|
|
400
|
-
if (!localDir) throw new Error(`Local template "${templateName}" does not match any workspace package; update the \`create.templates\` entry in vite.config.ts`);
|
|
401
|
-
const localPackagePath = path.join(workspaceInfo.rootDir, localDir);
|
|
402
|
-
const packageJsonPath = path.join(localPackagePath, "package.json");
|
|
403
|
-
if (!fs.existsSync(packageJsonPath)) throw new Error(`Local template "${templateName}" has no package.json, so it cannot be run as a template`);
|
|
404
|
-
const pkg = readJsonFile(packageJsonPath);
|
|
405
|
-
const binPath = resolveLocalBinPath(localPackagePath, pkg.name ?? templateName, pkg.bin);
|
|
406
|
-
if (!binPath) throw new Error(`Local template "${templateName}" has no "bin" entry in its package.json, so it cannot be run as a template`);
|
|
407
|
-
const args = [binPath, ...templateArgs];
|
|
408
|
-
let type = TemplateType.remote;
|
|
409
|
-
if (isBingoTemplate(pkg)) {
|
|
410
|
-
type = TemplateType.bingo;
|
|
411
|
-
args.push("--skip-requests");
|
|
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}`);
|
|
412
316
|
}
|
|
413
|
-
|
|
414
|
-
command: "node",
|
|
415
|
-
args,
|
|
416
|
-
envs,
|
|
417
|
-
type,
|
|
418
|
-
parentDir,
|
|
419
|
-
interactive
|
|
420
|
-
};
|
|
317
|
+
chunks.push(value);
|
|
421
318
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
};
|
|
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;
|
|
430
326
|
}
|
|
431
327
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* This follows the same convention as `npm create` / `pnpm create`:
|
|
435
|
-
* - `vite` → `create-vite`
|
|
436
|
-
* - `vite@latest` → `create-vite@latest`
|
|
328
|
+
* Download a package tarball and parse its `package/package.json`.
|
|
437
329
|
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
* -
|
|
441
|
-
*
|
|
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.
|
|
442
334
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* - GitHub URLs
|
|
446
|
-
* - Local paths (`./`, `../`, `/`)
|
|
447
|
-
* - Names already starting with `create-` (or `@scope/create-`)
|
|
335
|
+
* Returns `null` when the archive contains no `package/package.json`.
|
|
336
|
+
* Throws on download/integrity failures or unparsable JSON.
|
|
448
337
|
*/
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return
|
|
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}`);
|
|
458
349
|
}
|
|
459
|
-
const scope = templateName.slice(0, slashIndex);
|
|
460
|
-
const rest = templateName.slice(slashIndex + 1);
|
|
461
|
-
const atIndex = rest.indexOf("@");
|
|
462
|
-
const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
|
|
463
|
-
const version = atIndex === -1 ? "" : rest.slice(atIndex);
|
|
464
|
-
if (name.startsWith("create-")) return templateName;
|
|
465
|
-
if (scope === "@tanstack" && name === "start") return `@tanstack/cli${version}`;
|
|
466
|
-
return `${scope}/create-${name}${version}`;
|
|
467
350
|
}
|
|
468
|
-
|
|
469
|
-
const name = atIndex === -1 ? templateName : templateName.slice(0, atIndex);
|
|
470
|
-
const version = atIndex === -1 ? "" : templateName.slice(atIndex);
|
|
471
|
-
if (name.startsWith("create-")) return templateName;
|
|
472
|
-
if (name === "nitro") return `create-nitro-app${version}`;
|
|
473
|
-
if (name === "svelte") return `sv${version}`;
|
|
474
|
-
return `create-${name}${version}`;
|
|
351
|
+
return null;
|
|
475
352
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
353
|
+
const STAGING_SUFFIX_PREFIX = ".tmp-";
|
|
354
|
+
/**
|
|
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.
|
|
359
|
+
*/
|
|
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;
|
|
365
|
+
}
|
|
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);
|
|
377
|
+
}
|
|
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;
|
|
482
419
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
420
|
+
}
|
|
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;
|
|
437
|
+
}
|
|
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
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
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`).
|
|
454
|
+
*
|
|
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.
|
|
458
|
+
*/
|
|
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;
|
|
469
|
+
}
|
|
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;
|
|
487
484
|
}
|
|
488
485
|
//#endregion
|
|
489
|
-
//#region src/create/
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
486
|
+
//#region src/create/org-manifest.ts
|
|
487
|
+
/**
|
|
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.
|
|
493
|
+
*
|
|
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.
|
|
497
|
+
*
|
|
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`.
|
|
500
|
+
*/
|
|
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
|
|
530
|
+
};
|
|
513
531
|
}
|
|
514
|
-
//#endregion
|
|
515
|
-
//#region src/create/org-picker.ts
|
|
516
|
-
const ORG_PICKER_CANCEL = Symbol("org-picker-cancel");
|
|
517
|
-
const ORG_PICKER_BUILTIN_ESCAPE = Symbol("org-picker-builtin-escape");
|
|
518
|
-
const ESCAPE_HATCH = Symbol("builtin-escape");
|
|
519
532
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* Context-filters entries with `monorepo: true` when running inside an
|
|
524
|
-
* existing monorepo, mirroring `initial-template-options.ts:9-31`.
|
|
525
|
-
*
|
|
526
|
-
* Returns `ORG_PICKER_BUILTIN_ESCAPE` when the escape hatch is selected,
|
|
527
|
-
* or `ORG_PICKER_CANCEL` when the user hits Ctrl-C.
|
|
533
|
+
* Schema-level failure. Never falls through silently — a maintainer who
|
|
534
|
+
* shipped an invalid manifest should see the offending field.
|
|
528
535
|
*/
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
hint: entry.description
|
|
540
|
-
};
|
|
541
|
-
});
|
|
542
|
-
lookup.set(escapeValue, ESCAPE_HATCH);
|
|
543
|
-
const builtinHint = opts.isMonorepo ? "Use defaults (application / library)" : "Use defaults (monorepo / application / library)";
|
|
544
|
-
options.push({
|
|
545
|
-
value: escapeValue,
|
|
546
|
-
label: "Vite+ built-in templates",
|
|
547
|
-
hint: builtinHint
|
|
548
|
-
});
|
|
549
|
-
const picked = await select({
|
|
550
|
-
message: `Pick a template from ${manifest.scope}`,
|
|
551
|
-
options
|
|
552
|
-
});
|
|
553
|
-
if (isCancel(picked)) return ORG_PICKER_CANCEL;
|
|
554
|
-
const found = lookup.get(picked);
|
|
555
|
-
if (found === ESCAPE_HATCH) return ORG_PICKER_BUILTIN_ESCAPE;
|
|
556
|
-
if (!found) throw new Error(`org-picker: prompts.select returned an unregistered value: ${picked}`);
|
|
557
|
-
return {
|
|
558
|
-
kind: "entry",
|
|
559
|
-
entry: found
|
|
560
|
-
};
|
|
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("../");
|
|
561
546
|
}
|
|
562
547
|
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
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.
|
|
566
552
|
*/
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
+
}
|
|
575
569
|
return {
|
|
576
|
-
|
|
577
|
-
|
|
570
|
+
name,
|
|
571
|
+
description,
|
|
572
|
+
template
|
|
578
573
|
};
|
|
579
574
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
"x": "extendedHeader",
|
|
593
|
-
"D": "gnuDirectory",
|
|
594
|
-
"I": "gnuInodeMetadata",
|
|
595
|
-
"K": "gnuLongLinkName",
|
|
596
|
-
"L": "gnuLongFileName",
|
|
597
|
-
"N": "gnuOldLongFileName",
|
|
598
|
-
"M": "gnuMultiVolume",
|
|
599
|
-
"S": "gnuSparseFile",
|
|
600
|
-
"E": "gnuExtendedSparse",
|
|
601
|
-
"A": "solarisAcl",
|
|
602
|
-
"V": "solarisVolumeLabel",
|
|
603
|
-
"X": "solarisOldExtendedHeader"
|
|
604
|
-
};
|
|
605
|
-
function parseTar(data, opts) {
|
|
606
|
-
const buffer = data.buffer || data;
|
|
607
|
-
const files = [];
|
|
608
|
-
let offset = 0;
|
|
609
|
-
let nextExtendedHeader;
|
|
610
|
-
let globalExtendedHeader;
|
|
611
|
-
while (offset < buffer.byteLength - 512) {
|
|
612
|
-
let name = _readString(buffer, offset, 100);
|
|
613
|
-
if (name.length === 0) break;
|
|
614
|
-
if (nextExtendedHeader) {
|
|
615
|
-
const longName = nextExtendedHeader.path || nextExtendedHeader.linkpath;
|
|
616
|
-
if (longName) name = longName;
|
|
617
|
-
}
|
|
618
|
-
const mode = _readString(buffer, offset + 100, 8).trim();
|
|
619
|
-
const uid = Number.parseInt(_readString(buffer, offset + 108, 8));
|
|
620
|
-
const gid = Number.parseInt(_readString(buffer, offset + 116, 8));
|
|
621
|
-
const size = _readNumber(buffer, offset + 124, 12);
|
|
622
|
-
const seek = 512 + 512 * Math.trunc(size / 512) + (size % 512 ? 512 : 0);
|
|
623
|
-
const mtime = _readNumber(buffer, offset + 136, 12);
|
|
624
|
-
const _type = _readString(buffer, offset + 156, 1) || "0";
|
|
625
|
-
const type = tarItemTypeMap[_type] || _type;
|
|
626
|
-
switch (type) {
|
|
627
|
-
case "extendedHeader":
|
|
628
|
-
case "globalExtendedHeader": {
|
|
629
|
-
const headers = _parseExtendedHeaders(new Uint8Array(buffer, offset + 512, size));
|
|
630
|
-
if (type === "extendedHeader") nextExtendedHeader = headers;
|
|
631
|
-
else {
|
|
632
|
-
nextExtendedHeader = void 0;
|
|
633
|
-
globalExtendedHeader = {
|
|
634
|
-
...globalExtendedHeader,
|
|
635
|
-
...headers
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
offset += seek;
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
case "gnuLongFileName":
|
|
642
|
-
case "gnuOldLongFileName":
|
|
643
|
-
case "gnuLongLinkName":
|
|
644
|
-
nextExtendedHeader = { path: _readString(buffer, offset + 512, size) };
|
|
645
|
-
offset += seek;
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
const user = _readString(buffer, offset + 265, 32);
|
|
649
|
-
const group = _readString(buffer, offset + 297, 32);
|
|
650
|
-
name = _sanitizePath(name);
|
|
651
|
-
const meta = {
|
|
652
|
-
name,
|
|
653
|
-
type,
|
|
654
|
-
size,
|
|
655
|
-
attrs: {
|
|
656
|
-
...globalExtendedHeader,
|
|
657
|
-
...nextExtendedHeader,
|
|
658
|
-
mode,
|
|
659
|
-
uid,
|
|
660
|
-
gid,
|
|
661
|
-
mtime,
|
|
662
|
-
user,
|
|
663
|
-
group
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
nextExtendedHeader = void 0;
|
|
667
|
-
if (opts?.filter && !opts.filter(meta)) {
|
|
668
|
-
offset += seek;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
if (opts?.metaOnly) {
|
|
672
|
-
files.push(meta);
|
|
673
|
-
offset += seek;
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
const data2 = size === 0 ? void 0 : new Uint8Array(buffer, offset + 512, size);
|
|
677
|
-
files.push({
|
|
678
|
-
...meta,
|
|
679
|
-
data: data2,
|
|
680
|
-
get text() {
|
|
681
|
-
return new TextDecoder().decode(this.data);
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
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);
|
|
685
587
|
}
|
|
686
|
-
return
|
|
687
|
-
}
|
|
688
|
-
async function parseTarGzip(data, opts = {}) {
|
|
689
|
-
const stream = new ReadableStream({ start(controller) {
|
|
690
|
-
controller.enqueue(new Uint8Array(data));
|
|
691
|
-
controller.close();
|
|
692
|
-
} }).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));
|
|
693
|
-
return parseTar(await new Response(stream).arrayBuffer(), opts);
|
|
694
|
-
}
|
|
695
|
-
function _sanitizePath(path) {
|
|
696
|
-
let normalized = path.replace(/\\/g, "/");
|
|
697
|
-
normalized = normalized.replace(/^[a-zA-Z]:\//, "");
|
|
698
|
-
normalized = normalized.replace(/^\/+/, "");
|
|
699
|
-
const hasLeadingDotSlash = normalized.startsWith("./");
|
|
700
|
-
const parts = normalized.split("/");
|
|
701
|
-
const resolved = [];
|
|
702
|
-
for (const part of parts) if (part === "..") resolved.pop();
|
|
703
|
-
else if (part !== "." && part !== "") resolved.push(part);
|
|
704
|
-
let result = resolved.join("/");
|
|
705
|
-
if (hasLeadingDotSlash && !result.startsWith("./")) result = "./" + result;
|
|
706
|
-
if (path.endsWith("/") && !result.endsWith("/")) result += "/";
|
|
707
|
-
return result;
|
|
708
|
-
}
|
|
709
|
-
function _readString(buffer, offset, size) {
|
|
710
|
-
const view = new Uint8Array(buffer, offset, size);
|
|
711
|
-
const i = view.indexOf(0);
|
|
712
|
-
return new TextDecoder().decode(i === -1 ? view : view.slice(0, i));
|
|
713
|
-
}
|
|
714
|
-
function _readNumber(buffer, offset, size) {
|
|
715
|
-
const view = new Uint8Array(buffer, offset, size);
|
|
716
|
-
let str = "";
|
|
717
|
-
for (let i = 0; i < size; i++) str += String.fromCodePoint(view[i]);
|
|
718
|
-
return Number.parseInt(str, 8);
|
|
588
|
+
return entries;
|
|
719
589
|
}
|
|
720
|
-
function
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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;
|
|
726
598
|
}
|
|
727
|
-
return
|
|
599
|
+
return {
|
|
600
|
+
...base,
|
|
601
|
+
...monorepo !== void 0 ? { monorepo } : {}
|
|
602
|
+
};
|
|
728
603
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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));
|
|
734
613
|
}
|
|
735
614
|
/**
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* comes from `new URL(...).host` which can carry a port (`:4873`) or
|
|
739
|
-
* IPv6 literal (`[::1]`); both end up in the cache path otherwise.
|
|
615
|
+
* Schema-level failure for `create.templates` in `vite.config.ts`. A misconfigured
|
|
616
|
+
* local template should surface clearly rather than silently disappear.
|
|
740
617
|
*/
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
618
|
+
var CreateConfigSchemaError = class extends Error {
|
|
619
|
+
constructor(message) {
|
|
620
|
+
super(message);
|
|
621
|
+
this.name = "CreateConfigSchemaError";
|
|
622
|
+
}
|
|
623
|
+
};
|
|
744
624
|
/**
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* guarantees `manifest.tarballUrl` is a valid URL, so any parse failure
|
|
749
|
-
* here is a real bug worth surfacing.
|
|
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.
|
|
750
628
|
*/
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
expected: match[2]
|
|
761
|
-
};
|
|
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
|
+
});
|
|
762
638
|
}
|
|
763
|
-
function
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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();
|
|
771
647
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
648
|
+
/**
|
|
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`.
|
|
667
|
+
*/
|
|
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;
|
|
797
680
|
}
|
|
798
|
-
|
|
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
|
+
};
|
|
799
695
|
}
|
|
800
|
-
const STAGING_SUFFIX_PREFIX = ".tmp-";
|
|
801
696
|
/**
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
* its default (umask-derived) permissions instead of downgrading.
|
|
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`.
|
|
806
700
|
*/
|
|
807
|
-
function
|
|
808
|
-
if (!
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
701
|
+
function filterManifestForContext(templates, isMonorepo) {
|
|
702
|
+
if (!isMonorepo) return [...templates];
|
|
703
|
+
return templates.filter((entry) => !entry.monorepo);
|
|
704
|
+
}
|
|
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/");
|
|
724
|
+
}
|
|
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;
|
|
730
|
+
}
|
|
731
|
+
function inferGitHubRepoName(templateName) {
|
|
732
|
+
const degitPath = parseGitHubUrl(templateName);
|
|
733
|
+
if (!degitPath) return null;
|
|
734
|
+
return degitPath.split("/").pop() || null;
|
|
735
|
+
}
|
|
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
|
+
};
|
|
812
814
|
}
|
|
813
815
|
/**
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
* `
|
|
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-`)
|
|
817
832
|
*/
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
if (
|
|
822
|
-
if (
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const stagingDir = `${destDir}${STAGING_SUFFIX_PREFIX}${process.pid}-${Date.now()}`;
|
|
828
|
-
await fs.promises.mkdir(stagingDir, { recursive: true });
|
|
829
|
-
const resolvedStaging = path.resolve(stagingDir);
|
|
830
|
-
try {
|
|
831
|
-
for (const entry of entries) {
|
|
832
|
-
const relativeName = normalizeEntryName(entry.name);
|
|
833
|
-
if (relativeName === null) continue;
|
|
834
|
-
const targetPath = path.join(stagingDir, relativeName);
|
|
835
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
836
|
-
if (resolvedTarget !== resolvedStaging && !resolvedTarget.startsWith(`${resolvedStaging}${path.sep}`)) throw new Error(`tarball entry escapes extraction root: ${entry.name}`);
|
|
837
|
-
if (entry.type === "directory" || relativeName.endsWith("/")) {
|
|
838
|
-
await fs.promises.mkdir(targetPath, { recursive: true });
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
842
|
-
const data = entry.data ?? /* @__PURE__ */ new Uint8Array(0);
|
|
843
|
-
await fs.promises.writeFile(targetPath, data);
|
|
844
|
-
const mode = parseEntryMode(entry.attrs?.mode);
|
|
845
|
-
if (mode !== void 0) await fs.promises.chmod(targetPath, mode);
|
|
846
|
-
}
|
|
847
|
-
try {
|
|
848
|
-
await fs.promises.rename(stagingDir, destDir);
|
|
849
|
-
} catch (error) {
|
|
850
|
-
const code = error.code;
|
|
851
|
-
if ((code === "ENOTEMPTY" || code === "EEXIST") && fs.existsSync(path.join(destDir, "package.json"))) {
|
|
852
|
-
await fs.promises.rm(stagingDir, {
|
|
853
|
-
recursive: true,
|
|
854
|
-
force: true
|
|
855
|
-
}).catch(() => {});
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
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)}`;
|
|
859
842
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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}`;
|
|
866
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}`;
|
|
867
859
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
* the age gate keeps this safe to run at the top of every extract.
|
|
875
|
-
*/
|
|
876
|
-
async function cleanupStaleStagingDirs(destDir) {
|
|
877
|
-
const parent = path.dirname(destDir);
|
|
878
|
-
const prefix = `${path.basename(destDir)}${STAGING_SUFFIX_PREFIX}`;
|
|
879
|
-
let entries;
|
|
880
|
-
try {
|
|
881
|
-
entries = await fs.promises.readdir(parent);
|
|
882
|
-
} catch {
|
|
883
|
-
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;
|
|
884
866
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
+
];
|
|
895
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");
|
|
896
903
|
/**
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
* extracted package root (i.e. the directory that contains
|
|
900
|
-
* `package.json`).
|
|
904
|
+
* Render the interactive picker for an org manifest. Always appends a
|
|
905
|
+
* trailing "Vite+ built-in templates" escape-hatch entry.
|
|
901
906
|
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
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.
|
|
905
912
|
*/
|
|
906
|
-
async function
|
|
907
|
-
const
|
|
908
|
-
if (
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
+
};
|
|
916
945
|
}
|
|
917
946
|
/**
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
* Existence is NOT checked here — the subsequent `copyDir` surfaces any
|
|
923
|
-
* 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.
|
|
924
950
|
*/
|
|
925
|
-
function
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
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
|
+
};
|
|
931
963
|
}
|
|
932
964
|
//#endregion
|
|
933
965
|
//#region ../../node_modules/.pnpm/validate-npm-package-name@7.0.2/node_modules/validate-npm-package-name/lib/builtin-modules.json
|