create-veap 0.1.2
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/README.md +3 -0
- package/bin/veap.js +7 -0
- package/dist/commands/docker.d.ts +1 -0
- package/dist/commands/docker.js +55 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +111 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +16 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +1 -0
- package/dist/manifests.d.ts +22 -0
- package/dist/manifests.js +32 -0
- package/dist/mutate.d.ts +46 -0
- package/dist/mutate.js +127 -0
- package/dist/package-manager.d.ts +56 -0
- package/dist/package-manager.js +222 -0
- package/dist/project-name.d.ts +22 -0
- package/dist/project-name.js +107 -0
- package/dist/prompts.d.ts +20 -0
- package/dist/prompts.js +93 -0
- package/dist/scaffold.d.ts +26 -0
- package/dist/scaffold.js +62 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +29 -0
- package/package.json +45 -0
- package/stubs/docker/.dockerignore.stub +39 -0
- package/stubs/docker/Dockerfile.bun.stub +42 -0
- package/stubs/docker/Dockerfile.npm.stub +42 -0
- package/stubs/docker/Dockerfile.stub +49 -0
- package/stubs/docker/Dockerfile.yarn.stub +44 -0
- package/stubs/docker/compose.yml.stub +58 -0
- package/stubs/overlay-full/AGENTS.md.stub +63 -0
- package/stubs/overlay-full/app/[[...catchAll]]/page.tsx.stub +47 -0
- package/stubs/overlay-full/app/api/[...catchAll]/route.ts.stub +111 -0
- package/stubs/overlay-full/app/error.tsx.stub +34 -0
- package/stubs/overlay-full/app/globals.css.stub +124 -0
- package/stubs/overlay-full/app/layout.tsx.stub +71 -0
- package/stubs/overlay-full/app/not-found.tsx.stub +29 -0
- package/stubs/overlay-full/app/storage/[...path]/route.ts.stub +61 -0
- package/stubs/overlay-full/lib/plugins.gen.ts.stub +6 -0
- package/stubs/overlay-full/lib/veap.ts.stub +26 -0
- package/stubs/overlay-full/migrations/index.ts.stub +2 -0
- package/stubs/overlay-full/next.config.ts.stub +34 -0
- package/stubs/overlay-full/proxy.ts.stub +27 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
export const PACKAGE_MANAGERS = [
|
|
6
|
+
"pnpm",
|
|
7
|
+
"npm",
|
|
8
|
+
"yarn",
|
|
9
|
+
"bun",
|
|
10
|
+
];
|
|
11
|
+
/** Versions pinned into the generated project's `packageManager` field. */
|
|
12
|
+
const PINNED_VERSIONS = {
|
|
13
|
+
pnpm: "11.9.0",
|
|
14
|
+
yarn: "4.9.2",
|
|
15
|
+
};
|
|
16
|
+
const LOCKFILES = [
|
|
17
|
+
{ file: "pnpm-lock.yaml", pm: "pnpm" },
|
|
18
|
+
{ file: "bun.lock", pm: "bun" },
|
|
19
|
+
{ file: "bun.lockb", pm: "bun" },
|
|
20
|
+
{ file: "yarn.lock", pm: "yarn" },
|
|
21
|
+
{ file: "package-lock.json", pm: "npm" },
|
|
22
|
+
];
|
|
23
|
+
const isPackageManager = (value) => PACKAGE_MANAGERS.includes(value);
|
|
24
|
+
/**
|
|
25
|
+
* Detects the package manager to default to by inspecting `dir` (usually the
|
|
26
|
+
* directory the scaffolding command was launched from).
|
|
27
|
+
*
|
|
28
|
+
* Priority: `packageManager` field in package.json > lockfiles > default (pnpm).
|
|
29
|
+
*/
|
|
30
|
+
export function detectPackageManager(dir) {
|
|
31
|
+
// 1. Explicit intent: packageManager field (corepack convention)
|
|
32
|
+
const pkgPath = path.join(dir, "package.json");
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
36
|
+
if (typeof pkg.packageManager === "string") {
|
|
37
|
+
// Handles both "pnpm@11.9.0" and "pnpm@https://..." forms
|
|
38
|
+
const name = pkg.packageManager.slice(0, pkg.packageManager.indexOf("@"));
|
|
39
|
+
if (isPackageManager(name)) {
|
|
40
|
+
return { pm: name, source: "field", detail: pkg.packageManager };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// malformed package.json - fall through to lockfiles
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 2. Lockfiles
|
|
49
|
+
const found = LOCKFILES.filter((l) => fs.existsSync(path.join(dir, l.file)));
|
|
50
|
+
if (found.length > 1) {
|
|
51
|
+
// Ambiguous project (e.g. repo migrated between managers) - prefer pnpm,
|
|
52
|
+
// the framework default, and surface the conflict in the detail.
|
|
53
|
+
return {
|
|
54
|
+
pm: "pnpm",
|
|
55
|
+
source: "lockfile",
|
|
56
|
+
detail: `multiple lockfiles found: ${found
|
|
57
|
+
.map((l) => l.file)
|
|
58
|
+
.join(", ")}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (found.length === 1) {
|
|
62
|
+
return { pm: found[0].pm, source: "lockfile", detail: found[0].file };
|
|
63
|
+
}
|
|
64
|
+
// 3. Framework default
|
|
65
|
+
return { pm: "pnpm", source: "default" };
|
|
66
|
+
}
|
|
67
|
+
function formatDetection(d) {
|
|
68
|
+
const origin = d.source === "field"
|
|
69
|
+
? `packageManager field (${d.detail})`
|
|
70
|
+
: d.source === "lockfile"
|
|
71
|
+
? d.detail
|
|
72
|
+
: "no lockfile found";
|
|
73
|
+
return `📦 Package manager detected: ${d.pm} (${origin})`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Interactive numbered-choice prompt rendered on readline (no external
|
|
77
|
+
* prompt library, matching the rest of this CLI).
|
|
78
|
+
*
|
|
79
|
+
* The detected package manager is listed first as the default; pressing
|
|
80
|
+
* Enter accepts it. Accepts a number (1-4) or a name (`pnpm`, `npm`,
|
|
81
|
+
* `yarn`, `bun`). On EOF / closed stdin (piped input, CI) resolves to the
|
|
82
|
+
* detected default instead of hanging.
|
|
83
|
+
*/
|
|
84
|
+
export async function promptPackageManager(detection) {
|
|
85
|
+
console.log(formatDetection(detection));
|
|
86
|
+
console.log("\n? Which package manager should the project use?");
|
|
87
|
+
PACKAGE_MANAGERS.forEach((pm, i) => {
|
|
88
|
+
const suffix = pm === detection.pm && detection.source === "lockfile"
|
|
89
|
+
? " (detected - press Enter)"
|
|
90
|
+
: pm === detection.pm && detection.source === "field"
|
|
91
|
+
? " (from packageManager - press Enter)"
|
|
92
|
+
: "";
|
|
93
|
+
console.log(` ${i + 1}) ${pm}${suffix}`);
|
|
94
|
+
});
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
let settled = false;
|
|
97
|
+
const finish = (pm) => {
|
|
98
|
+
if (settled)
|
|
99
|
+
return;
|
|
100
|
+
settled = true;
|
|
101
|
+
resolve(pm);
|
|
102
|
+
rl.close();
|
|
103
|
+
};
|
|
104
|
+
const rl = readline.createInterface({
|
|
105
|
+
input: process.stdin,
|
|
106
|
+
output: process.stdout,
|
|
107
|
+
});
|
|
108
|
+
rl.on("close", () => {
|
|
109
|
+
// stdin exhausted (e.g. `echo "" | veap init ...`) - fall back to default
|
|
110
|
+
if (!settled) {
|
|
111
|
+
console.log(`\n⚠ Input closed - using ${detection.pm}`);
|
|
112
|
+
finish(detection.pm);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const ask = () => {
|
|
116
|
+
if (settled)
|
|
117
|
+
return;
|
|
118
|
+
rl.question("Choose [1]: ", (input) => {
|
|
119
|
+
const value = input.trim().toLowerCase();
|
|
120
|
+
if (value === "") {
|
|
121
|
+
finish(detection.pm);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const asNumber = Number.parseInt(value, 10);
|
|
125
|
+
if (String(asNumber) === value &&
|
|
126
|
+
asNumber >= 1 &&
|
|
127
|
+
asNumber <= PACKAGE_MANAGERS.length) {
|
|
128
|
+
finish(PACKAGE_MANAGERS[asNumber - 1]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (isPackageManager(value)) {
|
|
132
|
+
finish(value);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`⚠ Unknown option "${input.trim()}" - enter 1-${PACKAGE_MANAGERS.length} or a name (${PACKAGE_MANAGERS.join(", ")}).`);
|
|
136
|
+
ask();
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
ask();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolves the package manager to use for a new project.
|
|
144
|
+
*
|
|
145
|
+
* Order: explicit `--pm <name>` flag (validated hard - scripts must not hang
|
|
146
|
+
* on a prompt) → interactive prompt (TTY only) → auto-detected default
|
|
147
|
+
* (non-TTY, CI).
|
|
148
|
+
*/
|
|
149
|
+
export async function resolvePackageManager(options) {
|
|
150
|
+
const detection = detectPackageManager(process.cwd());
|
|
151
|
+
if (options?.pm) {
|
|
152
|
+
const requested = options.pm.trim().toLowerCase();
|
|
153
|
+
if (isPackageManager(requested)) {
|
|
154
|
+
return requested;
|
|
155
|
+
}
|
|
156
|
+
console.error(`Error: Unknown package manager "${options.pm}". Allowed: ${PACKAGE_MANAGERS.join(", ")}.`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
if (!process.stdin.isTTY) {
|
|
160
|
+
console.log(`📦 Using ${detection.pm} (auto-detected${detection.detail ? `: ${detection.detail}` : ""})`);
|
|
161
|
+
return detection.pm;
|
|
162
|
+
}
|
|
163
|
+
return promptPackageManager(detection);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Applies the chosen package manager to the freshly scaffolded project:
|
|
167
|
+
*
|
|
168
|
+
* - pnpm: pins `packageManager` (corepack), writes `pnpm-workspace.yaml`
|
|
169
|
+
* only when missing (an existing one is the source of truth)
|
|
170
|
+
* - yarn: pins `packageManager`, adds `.yarnrc.yml` with `nodeLinker:
|
|
171
|
+
* node-modules` (berry's default PnP breaks Next.js)
|
|
172
|
+
* - npm/bun: removes `packageManager` (not managed by corepack), switches
|
|
173
|
+
* workspace definition to the `workspaces` field in package.json
|
|
174
|
+
*/
|
|
175
|
+
export function applyPackageManager(projectDir, pm) {
|
|
176
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
177
|
+
if (!fs.existsSync(pkgPath))
|
|
178
|
+
return;
|
|
179
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
180
|
+
if (pm === "pnpm" || pm === "yarn") {
|
|
181
|
+
pkg.packageManager = `${pm}@${PINNED_VERSIONS[pm]}`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
delete pkg.packageManager;
|
|
185
|
+
}
|
|
186
|
+
const pnpmWorkspace = path.join(projectDir, "pnpm-workspace.yaml");
|
|
187
|
+
if (pm === "pnpm") {
|
|
188
|
+
// pnpm ignores the `workspaces` field and requires its own file (kept
|
|
189
|
+
// undefined in package.json to avoid a shadowing duplicate). Written
|
|
190
|
+
// only when missing - an existing one is the source of truth.
|
|
191
|
+
if (!fs.existsSync(pnpmWorkspace)) {
|
|
192
|
+
fs.writeFileSync(pnpmWorkspace, 'packages:\n - "plugins/*"\n - "templates/*"\n', "utf-8");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
pkg.workspaces = ["plugins/*", "templates/*"];
|
|
197
|
+
if (fs.existsSync(pnpmWorkspace)) {
|
|
198
|
+
// Switching away from pnpm: the file would shadow the workspaces field.
|
|
199
|
+
fs.rmSync(pnpmWorkspace);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
203
|
+
if (pm === "yarn") {
|
|
204
|
+
const yarnrc = [
|
|
205
|
+
"# Yarn Berry configuration generated by create-veap",
|
|
206
|
+
"nodeLinker: node-modules",
|
|
207
|
+
"",
|
|
208
|
+
].join("\n");
|
|
209
|
+
fs.writeFileSync(path.join(projectDir, ".yarnrc.yml"), yarnrc, "utf-8");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Command prefix for a package-manager-agnostic npm-script invocation. */
|
|
213
|
+
export function pmRunCommand(pm, script) {
|
|
214
|
+
return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Installs dependencies with the chosen package manager (all four use the
|
|
218
|
+
* same `install` verb).
|
|
219
|
+
*/
|
|
220
|
+
export function pmInstall(pm, cwd, opts) {
|
|
221
|
+
execSync(`${pm} install`, { cwd, stdio: opts?.stdio ?? "inherit" });
|
|
222
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ParsedProjectName {
|
|
2
|
+
/** npm scope without the leading `@`, or null for unscoped names. */
|
|
3
|
+
scope: string | null;
|
|
4
|
+
/** Package name inside the scope (equals the unscoped name otherwise). */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Folder to create (base name for scoped packages). */
|
|
7
|
+
folder: string;
|
|
8
|
+
/** Full npm package name written into package.json. */
|
|
9
|
+
packageName: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parses and validates a project name. Throws an `Error` with a readable
|
|
13
|
+
* message when the name violates npm package-name rules or hits a
|
|
14
|
+
* reserved scope/name.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseProjectName(input: string): ParsedProjectName;
|
|
17
|
+
/**
|
|
18
|
+
* Full validation for user input: npm name rules (via `parseProjectName`)
|
|
19
|
+
* plus a filesystem collision check on the folder that would be created.
|
|
20
|
+
* Returns an error message, or null when the name is acceptable.
|
|
21
|
+
*/
|
|
22
|
+
export declare function projectNameError(input: string, cwd: string): string | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Project-name validation following npm package-name rules (the folder name
|
|
5
|
+
* becomes the package name via create-next-app):
|
|
6
|
+
*
|
|
7
|
+
* - lowercase only, max 214 characters
|
|
8
|
+
* - scoped names (`@scope/name`) are supported: the project folder receives
|
|
9
|
+
* the base name, `package.json` the full scoped name
|
|
10
|
+
* - reserved scopes and names (the framework's own `@veap` namespace,
|
|
11
|
+
* `next`, `react`, ...) are rejected
|
|
12
|
+
* - unscoped names must stay filesystem-safe (they become the folder)
|
|
13
|
+
*/
|
|
14
|
+
/** Scopes that may never be used for generated projects. */
|
|
15
|
+
const RESERVED_SCOPES = [
|
|
16
|
+
"veap",
|
|
17
|
+
"next",
|
|
18
|
+
"react",
|
|
19
|
+
"vercel",
|
|
20
|
+
"npm",
|
|
21
|
+
"node",
|
|
22
|
+
"typescript",
|
|
23
|
+
"types",
|
|
24
|
+
];
|
|
25
|
+
/** Unscoped package names that may never be used for generated projects. */
|
|
26
|
+
const RESERVED_NAMES = [
|
|
27
|
+
"veap",
|
|
28
|
+
"create-veap",
|
|
29
|
+
"create-next-app",
|
|
30
|
+
"next",
|
|
31
|
+
"react",
|
|
32
|
+
"react-dom",
|
|
33
|
+
"vercel",
|
|
34
|
+
];
|
|
35
|
+
const MAX_LENGTH = 214;
|
|
36
|
+
function partError(part, label) {
|
|
37
|
+
if (part !== part.toLowerCase()) {
|
|
38
|
+
return `${label} must be lowercase (npm package name rules).`;
|
|
39
|
+
}
|
|
40
|
+
if (part.startsWith(".") || part.startsWith("_")) {
|
|
41
|
+
return `${label} cannot start with "." or "_".`;
|
|
42
|
+
}
|
|
43
|
+
if (part.length > MAX_LENGTH) {
|
|
44
|
+
return `${label} is too long (npm limit is ${MAX_LENGTH} characters).`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parses and validates a project name. Throws an `Error` with a readable
|
|
50
|
+
* message when the name violates npm package-name rules or hits a
|
|
51
|
+
* reserved scope/name.
|
|
52
|
+
*/
|
|
53
|
+
export function parseProjectName(input) {
|
|
54
|
+
const value = input.trim();
|
|
55
|
+
if (!value) {
|
|
56
|
+
throw new Error("Project name cannot be empty.");
|
|
57
|
+
}
|
|
58
|
+
if (value.startsWith("@")) {
|
|
59
|
+
const match = /^@([\w.-]+)\/([\w.-]+)$/.exec(value);
|
|
60
|
+
if (!match) {
|
|
61
|
+
throw new Error('Scoped names must look like "@scope/name" (lowercase letters, digits, ".", "_", "-").');
|
|
62
|
+
}
|
|
63
|
+
const scope = match[1];
|
|
64
|
+
const name = match[2];
|
|
65
|
+
const error = partError(scope, `Scope "${scope}"`) ?? partError(name, `Name "${name}"`);
|
|
66
|
+
if (error)
|
|
67
|
+
throw new Error(error);
|
|
68
|
+
if (RESERVED_SCOPES.includes(scope)) {
|
|
69
|
+
throw new Error(`The scope "@${scope}" is reserved and cannot be used for generated projects.`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
scope,
|
|
73
|
+
name,
|
|
74
|
+
folder: name,
|
|
75
|
+
packageName: `@${scope}/${name}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Unscoped: also becomes the folder, so keep it filesystem-safe.
|
|
79
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(value)) {
|
|
80
|
+
throw new Error('Use lowercase letters, digits, ".", "_" or "-" (must start with a letter or digit).');
|
|
81
|
+
}
|
|
82
|
+
const error = partError(value, "Project name");
|
|
83
|
+
if (error)
|
|
84
|
+
throw new Error(error);
|
|
85
|
+
if (RESERVED_NAMES.includes(value)) {
|
|
86
|
+
throw new Error(`The name "${value}" is reserved and cannot be used for generated projects.`);
|
|
87
|
+
}
|
|
88
|
+
return { scope: null, name: value, folder: value, packageName: value };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Full validation for user input: npm name rules (via `parseProjectName`)
|
|
92
|
+
* plus a filesystem collision check on the folder that would be created.
|
|
93
|
+
* Returns an error message, or null when the name is acceptable.
|
|
94
|
+
*/
|
|
95
|
+
export function projectNameError(input, cwd) {
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = parseProjectName(input);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return err.message;
|
|
102
|
+
}
|
|
103
|
+
if (fs.existsSync(path.resolve(cwd, parsed.folder))) {
|
|
104
|
+
return `Folder "${parsed.folder}" already exists in ${cwd}.`;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for the scaffolder, following the same conventions as
|
|
3
|
+
* `promptPackageManager`: plain readline (no prompt library) and safe
|
|
4
|
+
* non-TTY behaviour - every prompt resolves to its default on EOF / closed
|
|
5
|
+
* stdin / non-interactive terminals, so CI never hangs.
|
|
6
|
+
*
|
|
7
|
+
* Validation (npm package-name rules, scoped names, reserved prefixes,
|
|
8
|
+
* folder collisions) lives in `project-name.ts`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Asks for a project name until a valid one is entered. Accepts scoped
|
|
12
|
+
* npm names (`@scope/name` - the folder becomes the base name). On EOF /
|
|
13
|
+
* non-TTY resolves to the default immediately.
|
|
14
|
+
*/
|
|
15
|
+
export declare function promptProjectName(defaultName: string, cwd: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Yes/no confirmation. Accepts y/yes/true (case-insensitive); empty input
|
|
18
|
+
* takes the default. On EOF / non-TTY resolves to the default.
|
|
19
|
+
*/
|
|
20
|
+
export declare function promptConfirm(question: string, defaultValue: boolean): Promise<boolean>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { projectNameError } from "./project-name.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interactive prompts for the scaffolder, following the same conventions as
|
|
5
|
+
* `promptPackageManager`: plain readline (no prompt library) and safe
|
|
6
|
+
* non-TTY behaviour - every prompt resolves to its default on EOF / closed
|
|
7
|
+
* stdin / non-interactive terminals, so CI never hangs.
|
|
8
|
+
*
|
|
9
|
+
* Validation (npm package-name rules, scoped names, reserved prefixes,
|
|
10
|
+
* folder collisions) lives in `project-name.ts`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Asks for a project name until a valid one is entered. Accepts scoped
|
|
14
|
+
* npm names (`@scope/name` - the folder becomes the base name). On EOF /
|
|
15
|
+
* non-TTY resolves to the default immediately.
|
|
16
|
+
*/
|
|
17
|
+
export function promptProjectName(defaultName, cwd) {
|
|
18
|
+
if (!process.stdin.isTTY) {
|
|
19
|
+
return Promise.resolve(defaultName);
|
|
20
|
+
}
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
let settled = false;
|
|
23
|
+
const finish = (name) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
resolve(name);
|
|
28
|
+
rl.close();
|
|
29
|
+
};
|
|
30
|
+
const rl = readline.createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
output: process.stdout,
|
|
33
|
+
});
|
|
34
|
+
rl.on("close", () => {
|
|
35
|
+
if (!settled) {
|
|
36
|
+
console.log(`\n⚠ Input closed - using "${defaultName}"`);
|
|
37
|
+
finish(defaultName);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const ask = () => {
|
|
41
|
+
if (settled)
|
|
42
|
+
return;
|
|
43
|
+
rl.question(`? Project name (npm name, e.g. \`@my-scope/my-app\`) [${defaultName}]: `, (input) => {
|
|
44
|
+
const value = input.trim() || defaultName;
|
|
45
|
+
const error = projectNameError(value, cwd);
|
|
46
|
+
if (error) {
|
|
47
|
+
console.log(`⚠ ${error}`);
|
|
48
|
+
ask();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
finish(value);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
ask();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Yes/no confirmation. Accepts y/yes/true (case-insensitive); empty input
|
|
59
|
+
* takes the default. On EOF / non-TTY resolves to the default.
|
|
60
|
+
*/
|
|
61
|
+
export function promptConfirm(question, defaultValue) {
|
|
62
|
+
if (!process.stdin.isTTY) {
|
|
63
|
+
return Promise.resolve(defaultValue);
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let settled = false;
|
|
67
|
+
const finish = (value) => {
|
|
68
|
+
if (settled)
|
|
69
|
+
return;
|
|
70
|
+
settled = true;
|
|
71
|
+
resolve(value);
|
|
72
|
+
rl.close();
|
|
73
|
+
};
|
|
74
|
+
const rl = readline.createInterface({
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
output: process.stdout,
|
|
77
|
+
});
|
|
78
|
+
rl.on("close", () => {
|
|
79
|
+
if (!settled) {
|
|
80
|
+
finish(defaultValue);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const hint = defaultValue ? "[Y/n]" : "[y/N]";
|
|
84
|
+
rl.question(`? ${question} ${hint}: `, (input) => {
|
|
85
|
+
const value = input.trim().toLowerCase();
|
|
86
|
+
if (value === "") {
|
|
87
|
+
finish(defaultValue);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
finish(value === "y" || value === "yes" || value === "true");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PackageManager } from "./package-manager.js";
|
|
2
|
+
export interface ScaffoldOptions {
|
|
3
|
+
/** Directory in which the project folder is created. */
|
|
4
|
+
cwd: string;
|
|
5
|
+
pm: PackageManager;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Builds the explicit, non-interactive CNA invocation.
|
|
9
|
+
*
|
|
10
|
+
* Every choice is passed as a flag so CNA never prompts; `--yes` additionally
|
|
11
|
+
* absorbs flags introduced by future CNA versions (they resolve to defaults
|
|
12
|
+
* instead of hanging the scaffolder). `--skip-install` keeps a single
|
|
13
|
+
* dependency install at the end of `initProject`, after veap mutations.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildCreateNextAppCommand(name: string, opts: ScaffoldOptions): string;
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes a project name into a shell-safe, npm-friendly folder name.
|
|
18
|
+
* CNA derives the package name from the folder, so the same charset applies.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sanitizeProjectName(name: string): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Runs create-next-app. Throws (with the CNA output already streamed to the
|
|
23
|
+
* terminal) when scaffolding fails - CNA removes its own partial output.
|
|
24
|
+
* Returns the (possibly sanitized) project directory name.
|
|
25
|
+
*/
|
|
26
|
+
export declare function scaffoldNextApp(name: string, opts: ScaffoldOptions): string;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Scaffolds the Next.js shell of a new project with the official
|
|
4
|
+
* `create-next-app` (CNA) instead of duplicating its boilerplate in stubs.
|
|
5
|
+
*
|
|
6
|
+
* Veap owns only the files CNA cannot know about (overlay, see
|
|
7
|
+
* `commands/init.ts`); everything else - Next version, tsconfig, postcss,
|
|
8
|
+
* fonts, README, git init - comes straight from CNA and stays current.
|
|
9
|
+
*/
|
|
10
|
+
/** How each package manager fetches and runs the CNA binary. */
|
|
11
|
+
const CNA_RUNNERS = {
|
|
12
|
+
bun: "bun create next-app@latest",
|
|
13
|
+
pnpm: "pnpm dlx create-next-app@latest",
|
|
14
|
+
yarn: "yarn dlx create-next-app@latest",
|
|
15
|
+
npm: "npx --yes create-next-app@latest",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Builds the explicit, non-interactive CNA invocation.
|
|
19
|
+
*
|
|
20
|
+
* Every choice is passed as a flag so CNA never prompts; `--yes` additionally
|
|
21
|
+
* absorbs flags introduced by future CNA versions (they resolve to defaults
|
|
22
|
+
* instead of hanging the scaffolder). `--skip-install` keeps a single
|
|
23
|
+
* dependency install at the end of `initProject`, after veap mutations.
|
|
24
|
+
*/
|
|
25
|
+
export function buildCreateNextAppCommand(name, opts) {
|
|
26
|
+
const flags = [
|
|
27
|
+
"--ts",
|
|
28
|
+
"--tailwind",
|
|
29
|
+
"--app",
|
|
30
|
+
// ESLint is the veap linter: CNA writes eslint.config.mjs, the eslint
|
|
31
|
+
// dev deps and the `lint` script (kept as-is).
|
|
32
|
+
"--eslint",
|
|
33
|
+
"--react-compiler",
|
|
34
|
+
"--import-alias",
|
|
35
|
+
'"@/*"',
|
|
36
|
+
"--skip-install",
|
|
37
|
+
];
|
|
38
|
+
return `${CNA_RUNNERS[opts.pm]} "${name}" ${flags.join(" ")}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalizes a project name into a shell-safe, npm-friendly folder name.
|
|
42
|
+
* CNA derives the package name from the folder, so the same charset applies.
|
|
43
|
+
*/
|
|
44
|
+
export function sanitizeProjectName(name) {
|
|
45
|
+
const safe = name.replace(/[^\w.-]/g, "-");
|
|
46
|
+
return safe === name ? null : safe;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Runs create-next-app. Throws (with the CNA output already streamed to the
|
|
50
|
+
* terminal) when scaffolding fails - CNA removes its own partial output.
|
|
51
|
+
* Returns the (possibly sanitized) project directory name.
|
|
52
|
+
*/
|
|
53
|
+
export function scaffoldNextApp(name, opts) {
|
|
54
|
+
const sanitized = sanitizeProjectName(name);
|
|
55
|
+
if (sanitized) {
|
|
56
|
+
console.log(`⚠ Project name sanitized: "${name}" → "${sanitized}"`);
|
|
57
|
+
}
|
|
58
|
+
const projectName = sanitized ?? name;
|
|
59
|
+
const command = buildCreateNextAppCommand(projectName, opts);
|
|
60
|
+
execSync(command, { cwd: opts.cwd, stdio: "inherit" });
|
|
61
|
+
return projectName;
|
|
62
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function processStubs(srcDir: string, destDir: string, variables: Record<string, string>): void;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function processStubs(srcDir, destDir, variables) {
|
|
4
|
+
if (!fs.existsSync(destDir)) {
|
|
5
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
let destName = entry.name;
|
|
10
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
11
|
+
destName = destName.replace(new RegExp("\\{\\{" + key + "\\}\\}", "g"), value);
|
|
12
|
+
}
|
|
13
|
+
if (destName.endsWith(".stub")) {
|
|
14
|
+
destName = destName.slice(0, -5);
|
|
15
|
+
}
|
|
16
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
17
|
+
const destPath = path.join(destDir, destName);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
processStubs(srcPath, destPath, variables);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
23
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
24
|
+
content = content.replace(new RegExp("\\{\\{" + key + "\\}\\}", "g"), value);
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(destPath, content);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-veap",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Initializer for Veap Framework applications",
|
|
5
|
+
"bugs": "https://github.com/veapjs/create-veap/issues",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/veapjs/create-veap.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/main.js",
|
|
12
|
+
"types": "./dist/main.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"create-veap": "./bin/veap.js"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/main.d.ts",
|
|
19
|
+
"default": "./dist/main.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"stubs",
|
|
26
|
+
".docs"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"clean": "rimraf dist",
|
|
30
|
+
"build": "rimraf dist && tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"cac": "^7.0.0",
|
|
37
|
+
"ora": "^9.4.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.20.2",
|
|
41
|
+
"rimraf": "^6.1.3",
|
|
42
|
+
"typescript": "^7.0.2",
|
|
43
|
+
"vitest": "^5.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.pnpm-store/
|
|
3
|
+
npm-debug.log*
|
|
4
|
+
yarn-debug.log*
|
|
5
|
+
yarn-error.log*
|
|
6
|
+
pnpm-debug.log*
|
|
7
|
+
|
|
8
|
+
.next/
|
|
9
|
+
out/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
.vercel/
|
|
13
|
+
|
|
14
|
+
coverage/
|
|
15
|
+
*.test.*
|
|
16
|
+
*.spec.*
|
|
17
|
+
|
|
18
|
+
.git/
|
|
19
|
+
.gitignore
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
23
|
+
*.log
|
|
24
|
+
|
|
25
|
+
.env
|
|
26
|
+
.env*.local
|
|
27
|
+
|
|
28
|
+
Dockerfile*
|
|
29
|
+
.dockerignore
|
|
30
|
+
compose.yaml
|
|
31
|
+
compose.yml
|
|
32
|
+
|
|
33
|
+
*.md
|
|
34
|
+
docs/
|
|
35
|
+
|
|
36
|
+
.turbo/
|
|
37
|
+
.tmp/
|
|
38
|
+
.temp/
|
|
39
|
+
.DS_Store
|