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.
Files changed (44) hide show
  1. package/README.md +3 -0
  2. package/bin/veap.js +7 -0
  3. package/dist/commands/docker.d.ts +1 -0
  4. package/dist/commands/docker.js +55 -0
  5. package/dist/commands/init.d.ts +5 -0
  6. package/dist/commands/init.js +111 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +16 -0
  9. package/dist/main.d.ts +2 -0
  10. package/dist/main.js +1 -0
  11. package/dist/manifests.d.ts +22 -0
  12. package/dist/manifests.js +32 -0
  13. package/dist/mutate.d.ts +46 -0
  14. package/dist/mutate.js +127 -0
  15. package/dist/package-manager.d.ts +56 -0
  16. package/dist/package-manager.js +222 -0
  17. package/dist/project-name.d.ts +22 -0
  18. package/dist/project-name.js +107 -0
  19. package/dist/prompts.d.ts +20 -0
  20. package/dist/prompts.js +93 -0
  21. package/dist/scaffold.d.ts +26 -0
  22. package/dist/scaffold.js +62 -0
  23. package/dist/utils.d.ts +1 -0
  24. package/dist/utils.js +29 -0
  25. package/package.json +45 -0
  26. package/stubs/docker/.dockerignore.stub +39 -0
  27. package/stubs/docker/Dockerfile.bun.stub +42 -0
  28. package/stubs/docker/Dockerfile.npm.stub +42 -0
  29. package/stubs/docker/Dockerfile.stub +49 -0
  30. package/stubs/docker/Dockerfile.yarn.stub +44 -0
  31. package/stubs/docker/compose.yml.stub +58 -0
  32. package/stubs/overlay-full/AGENTS.md.stub +63 -0
  33. package/stubs/overlay-full/app/[[...catchAll]]/page.tsx.stub +47 -0
  34. package/stubs/overlay-full/app/api/[...catchAll]/route.ts.stub +111 -0
  35. package/stubs/overlay-full/app/error.tsx.stub +34 -0
  36. package/stubs/overlay-full/app/globals.css.stub +124 -0
  37. package/stubs/overlay-full/app/layout.tsx.stub +71 -0
  38. package/stubs/overlay-full/app/not-found.tsx.stub +29 -0
  39. package/stubs/overlay-full/app/storage/[...path]/route.ts.stub +61 -0
  40. package/stubs/overlay-full/lib/plugins.gen.ts.stub +6 -0
  41. package/stubs/overlay-full/lib/veap.ts.stub +26 -0
  42. package/stubs/overlay-full/migrations/index.ts.stub +2 -0
  43. package/stubs/overlay-full/next.config.ts.stub +34 -0
  44. 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>;
@@ -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;
@@ -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
+ }
@@ -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