create-template-project 0.4.0 โ 1.0.0
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/LICENSE +1 -1
- package/README.md +57 -12
- package/dist/config/dependencies.json +13 -13
- package/dist/index.js +553 -183
- package/dist/templates/base/files/.github/workflows/node.js.yml +38 -10
- package/dist/templates/base/files/AGENTS.md +6 -5
- package/dist/templates/base/files/CONTRIBUTING.md +4 -4
- package/dist/templates/base/files/LICENSE +21 -0
- package/dist/templates/base/files/README.md +16 -14
- package/dist/templates/base/files/_oxc.config.ts +95 -0
- package/dist/templates/base/files/_oxfmt.config.ts +2 -0
- package/dist/templates/base/files/_oxlint.config.ts +2 -58
- package/dist/templates/base/files/package.json +30 -30
- package/dist/templates/base/files/vitest.config.ts +1 -1
- package/dist/templates/cli/files/package.json +2 -1
- package/dist/templates/cli/files/vite.config.ts +1 -1
- package/dist/templates/web-app/files/vite.config.ts +1 -1
- package/dist/templates/web-fullstack/files/client/vite.config.ts +1 -1
- package/dist/templates/web-fullstack/files/package.json +1 -1
- package/dist/templates/web-fullstack/files/server/vite.config.ts +1 -1
- package/dist/templates/web-vanilla/files/package.json +1 -1
- package/dist/templates/web-vanilla/files/vite.config.ts +1 -1
- package/package.json +93 -75
- package/dist/templates/base/files/.prettierignore +0 -53
- package/dist/templates/base/files/.prettierrc.json +0 -8
package/dist/index.js
CHANGED
|
@@ -3,12 +3,12 @@ import { Command } from "commander";
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import { cancel, intro, outro } from "@clack/prompts";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { execa } from "execa";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import fs from "node:fs/promises";
|
|
8
9
|
import debugLib from "debug";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { existsSync } from "node:fs";
|
|
11
|
-
import { execa } from "execa";
|
|
12
12
|
//#region src/types.ts
|
|
13
13
|
var TemplateTypeSchema = z.enum([
|
|
14
14
|
"cli",
|
|
@@ -24,18 +24,120 @@ var PackageManagerSchema = z.enum([
|
|
|
24
24
|
var ProjectOptionsSchema = z.object({
|
|
25
25
|
template: TemplateTypeSchema,
|
|
26
26
|
projectName: z.string().min(1, "Project name is required"),
|
|
27
|
+
description: z.string().optional(),
|
|
28
|
+
keywords: z.string().optional(),
|
|
29
|
+
author: z.string().min(1, "Author name is required"),
|
|
30
|
+
githubUsername: z.string().min(1, "GitHub username is required"),
|
|
27
31
|
packageManager: PackageManagerSchema.optional().default("npm"),
|
|
28
32
|
createGithubRepository: z.boolean().optional().default(false),
|
|
29
33
|
directory: z.string(),
|
|
30
|
-
overwrite: z.boolean().optional().default(false),
|
|
31
34
|
update: z.boolean().optional().default(false),
|
|
32
35
|
installDependencies: z.boolean().optional().default(false),
|
|
33
36
|
build: z.boolean().optional().default(false),
|
|
34
|
-
dev: z.boolean().optional().default(false),
|
|
35
|
-
open: z.boolean().optional().default(false),
|
|
36
37
|
progress: z.boolean().optional().default(true)
|
|
37
38
|
});
|
|
38
39
|
//#endregion
|
|
40
|
+
//#region src/utils/templating/generic.ts
|
|
41
|
+
var genericProcessor = (content, { opts }) => {
|
|
42
|
+
const { projectName, template, author, githubUsername } = opts;
|
|
43
|
+
let description = opts.description || "";
|
|
44
|
+
if (!description) switch (template) {
|
|
45
|
+
case "cli":
|
|
46
|
+
description = "A modern Node.js CLI application with TypeScript and automated tooling.";
|
|
47
|
+
break;
|
|
48
|
+
case "web-vanilla":
|
|
49
|
+
description = "A standalone web page/application for modern browsers.";
|
|
50
|
+
break;
|
|
51
|
+
case "web-fullstack":
|
|
52
|
+
description = "A full-stack monorepo with an Express server and a React/MUI client.";
|
|
53
|
+
break;
|
|
54
|
+
case "web-app":
|
|
55
|
+
description = "A React application with MUI and TanStack Query.";
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const pm = opts.packageManager || "npm";
|
|
59
|
+
const lockfileRules = pm === "pnpm" ? "package-lock.json\nyarn.lock" : pm === "yarn" ? "package-lock.json\npnpm-lock.yaml" : "yarn.lock\npnpm-lock.yaml";
|
|
60
|
+
return content.replaceAll("{{projectName}}", projectName).replaceAll("{{description}}", description).replaceAll("{{packageManager}}", pm).replaceAll("{{author}}", author || "").replaceAll("{{githubUsername}}", githubUsername || "").replaceAll("{{year}}", (/* @__PURE__ */ new Date()).getFullYear().toString()).replaceAll("{{lockfileRules}}", lockfileRules);
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/utils/templating/github-workflow.ts
|
|
64
|
+
var WORKFLOW_PNPM_SETUP = ` - name: Setup pnpm
|
|
65
|
+
uses: pnpm/action-setup@v4
|
|
66
|
+
with:
|
|
67
|
+
version: 9
|
|
68
|
+
run_install: false`;
|
|
69
|
+
var WORKFLOW_PLAYWRIGHT_SETUP = ` - name: Install Playwright Browsers & Deps
|
|
70
|
+
run: npx playwright install --with-deps chromium`;
|
|
71
|
+
var githubWorkflowProcessor = (content, { filePath, opts }) => {
|
|
72
|
+
if (!filePath.includes(".github/workflows/node.js.yml")) return content;
|
|
73
|
+
const { template, packageManager: pm = "npm" } = opts;
|
|
74
|
+
let installCommand = "npm ci";
|
|
75
|
+
let pmSetup = "";
|
|
76
|
+
if (pm === "pnpm") {
|
|
77
|
+
installCommand = "pnpm install --frozen-lockfile";
|
|
78
|
+
pmSetup = WORKFLOW_PNPM_SETUP;
|
|
79
|
+
} else if (pm === "yarn") installCommand = "yarn install --frozen-lockfile";
|
|
80
|
+
let playwrightSetup = "";
|
|
81
|
+
if (template === "web-fullstack" || template === "web-app" || template === "web-vanilla") playwrightSetup = WORKFLOW_PLAYWRIGHT_SETUP;
|
|
82
|
+
let processed = content.replaceAll("{{installCommand}}", installCommand).replaceAll("# [PM_SETUP]", pmSetup).replaceAll("# [PLAYWRIGHT_SETUP]", playwrightSetup);
|
|
83
|
+
processed = processed.replace(/^\s*# \[PM_SETUP\]\s*\n/m, "");
|
|
84
|
+
processed = processed.replace(/^\s*# \[PLAYWRIGHT_SETUP\]\s*\n/m, "");
|
|
85
|
+
return processed;
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/utils/templating/tsconfig.ts
|
|
89
|
+
var WEB_ENV = `/* Language and Environment */
|
|
90
|
+
"target": "ES2023" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
|
91
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
|
92
|
+
"module": "ESNext" /* Specify what module code is generated. */,
|
|
93
|
+
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
|
94
|
+
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. */,
|
|
95
|
+
"resolveJsonModule": true /* Enable importing .json files. */,
|
|
96
|
+
"allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. */,
|
|
97
|
+
"noEmit": true /* Disable emitting files from a compilation. */,
|
|
98
|
+
"jsx": "react-jsx" /* Specify what JSX code is generated. */,`;
|
|
99
|
+
var tsconfigProcessor = (content, { filePath, opts }) => {
|
|
100
|
+
if (filePath !== "tsconfig.json") return content;
|
|
101
|
+
const { template } = opts;
|
|
102
|
+
let processed = content;
|
|
103
|
+
if (template === "web-fullstack" || template === "web-vanilla" || template === "web-app") processed = processed.replace(/\/\* Language and Environment \*\/[\s\S]*?\/\* Strict Type-Checking Options \*\//, WEB_ENV + "\n\n /* Strict Type-Checking Options */");
|
|
104
|
+
if (template === "web-fullstack") processed = processed.replace(/"include":\s*\[\s*"src\/\*\*\/\*"\s*\]/, "\"include\": [\"client/src/**/*\", \"server/src/**/*\"]");
|
|
105
|
+
return processed;
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/utils/templating/contributing.ts
|
|
109
|
+
var contributingProcessor = (content, { filePath, addedDeps }) => {
|
|
110
|
+
if (filePath !== "CONTRIBUTING.md" || addedDeps.length === 0) return content;
|
|
111
|
+
let processed = content;
|
|
112
|
+
processed += "\n## Dependencies\n\n";
|
|
113
|
+
const uniqueDeps = Array.from(new Set(addedDeps.map((d) => JSON.stringify(d)))).map((s) => JSON.parse(s));
|
|
114
|
+
for (const dep of uniqueDeps) processed += `- **${dep.name}**: ${dep.description}\n`;
|
|
115
|
+
return processed;
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/utils/templating/web-vanilla-html.ts
|
|
119
|
+
var webVanillaHtmlProcessor = (content, { filePath, opts }) => {
|
|
120
|
+
if (opts.template === "web-vanilla" && filePath === "index.html") return content.replace("{{scriptSrc}}", "/src/index.ts");
|
|
121
|
+
return content;
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/utils/templating/index.ts
|
|
125
|
+
var processors = [
|
|
126
|
+
genericProcessor,
|
|
127
|
+
githubWorkflowProcessor,
|
|
128
|
+
tsconfigProcessor,
|
|
129
|
+
contributingProcessor,
|
|
130
|
+
webVanillaHtmlProcessor
|
|
131
|
+
];
|
|
132
|
+
function processContent$1(filePath, content, opts, addedDeps) {
|
|
133
|
+
const context = {
|
|
134
|
+
filePath,
|
|
135
|
+
opts,
|
|
136
|
+
addedDeps
|
|
137
|
+
};
|
|
138
|
+
return processors.reduce((acc, processor) => processor(acc, context), content);
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
39
141
|
//#region src/utils/file.ts
|
|
40
142
|
var debug$3 = debugLib("create-template-project:utils:file");
|
|
41
143
|
function getTemplateDir(dirname, templateName) {
|
|
@@ -53,46 +155,7 @@ async function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
|
53
155
|
return arrayOfFiles;
|
|
54
156
|
}
|
|
55
157
|
function processContent(filePath, content, opts, addedDeps) {
|
|
56
|
-
|
|
57
|
-
let description = "";
|
|
58
|
-
switch (template) {
|
|
59
|
-
case "cli":
|
|
60
|
-
description = "A modern Node.js CLI application with TypeScript and automated tooling.";
|
|
61
|
-
break;
|
|
62
|
-
case "web-vanilla":
|
|
63
|
-
description = "A standalone web page/application for modern browsers.";
|
|
64
|
-
break;
|
|
65
|
-
case "web-fullstack":
|
|
66
|
-
description = "A full-stack monorepo with an Express server and a React/MUI client.";
|
|
67
|
-
break;
|
|
68
|
-
case "web-app":
|
|
69
|
-
description = "A React application with MUI and TanStack Query.";
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
const pm = opts.packageManager || "npm";
|
|
73
|
-
let processed = content.replaceAll("{{projectName}}", projectName).replaceAll("{{description}}", description).replaceAll("{{packageManager}}", pm);
|
|
74
|
-
if (filePath.includes(".github/workflows/node.js.yml")) {
|
|
75
|
-
let installCommand = "npm ci";
|
|
76
|
-
let pmSetup = "";
|
|
77
|
-
if (pm === "pnpm") {
|
|
78
|
-
installCommand = "pnpm install --frozen-lockfile";
|
|
79
|
-
pmSetup = "- uses: pnpm/action-setup@v4\n with:\n version: 9";
|
|
80
|
-
} else if (pm === "yarn") installCommand = "yarn install --frozen-lockfile";
|
|
81
|
-
let playwrightSetup = "";
|
|
82
|
-
if (template === "web-fullstack" || template === "web-app" || template === "web-vanilla") playwrightSetup = "- name: Install Playwright Browsers & Deps\n run: npx playwright install --with-deps chromium";
|
|
83
|
-
processed = processed.replaceAll("{{installCommand}}", installCommand).replaceAll("# [PM_SETUP]", pmSetup).replaceAll("# [PLAYWRIGHT_SETUP]", playwrightSetup);
|
|
84
|
-
processed = processed.replace(/^\s*# \[PM_SETUP\]\s*\n/m, "");
|
|
85
|
-
processed = processed.replace(/^\s*# \[PLAYWRIGHT_SETUP\]\s*\n/m, "");
|
|
86
|
-
}
|
|
87
|
-
if (template === "web-vanilla" && filePath === "index.html") processed = processed.replace("{{scriptSrc}}", "/src/index.ts");
|
|
88
|
-
if (filePath === "CONTRIBUTING.md" && addedDeps.length > 0) {
|
|
89
|
-
processed += "\n## Dependencies\n\n";
|
|
90
|
-
const uniqueDeps = Array.from(new Set(addedDeps.map((d) => JSON.stringify(d)))).map((s) => JSON.parse(s));
|
|
91
|
-
for (const dep of uniqueDeps) processed += `- **${dep.name}**: ${dep.description}\n`;
|
|
92
|
-
}
|
|
93
|
-
if ((template === "web-fullstack" || template === "web-vanilla" || template === "web-app") && filePath === "tsconfig.json") processed = processed.replace(/\/\* Language and Environment \*\/[\s\S]*?\/\* Strict Type-Checking Options \*\//, "/* Language and Environment */\n \"target\": \"ES2023\" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,\n \"module\": \"ESNext\" /* Specify what module code is generated. */,\n \"moduleResolution\": \"bundler\" /* Specify how TypeScript looks up a file from a given module specifier. */,\n \"esModuleInterop\": true /* Emit additional JavaScript to ease support for importing CommonJS modules. */,\n \"resolveJsonModule\": true /* Enable importing .json files. */,\n \"allowImportingTsExtensions\": true /* Allow imports to include TypeScript file extensions. */,\n \"noEmit\": true /* Disable emitting files from a compilation. */,\n \"jsx\": \"react-jsx\" /* Specify what JSX code is generated. */,\n\n /* Strict Type-Checking Options */");
|
|
94
|
-
if (template === "web-fullstack" && filePath === "tsconfig.json") processed = processed.replace(/"include":\s*\[\s*"src\/\*\*\/\*"\s*\]/, "\"include\": [\"client/src/**/*\", \"server/src/**/*\"]");
|
|
95
|
-
return processed;
|
|
158
|
+
return processContent$1(filePath, content, opts, addedDeps);
|
|
96
159
|
}
|
|
97
160
|
function mergePackageJson(target, source) {
|
|
98
161
|
if (source.scripts) target.scripts = {
|
|
@@ -121,8 +184,9 @@ function isSeedFile(filePath) {
|
|
|
121
184
|
"index.html",
|
|
122
185
|
"App.tsx",
|
|
123
186
|
"main.tsx",
|
|
124
|
-
"index.tsx"
|
|
125
|
-
|
|
187
|
+
"index.tsx",
|
|
188
|
+
"LICENSE"
|
|
189
|
+
].some((file) => filePath === file) || filePath.toLowerCase().endsWith(".md");
|
|
126
190
|
}
|
|
127
191
|
async function mergeFile(filePath, existing, template, log) {
|
|
128
192
|
debug$3("Merging file: %s", filePath);
|
|
@@ -145,7 +209,7 @@ async function mergeFile(filePath, existing, template, log) {
|
|
|
145
209
|
});
|
|
146
210
|
return (await fs.readFile(filePath, "utf8")).trim() !== template.trim() ? "merged" : "updated";
|
|
147
211
|
} catch (e) {
|
|
148
|
-
if (e.exitCode
|
|
212
|
+
if (e.exitCode >= 1 && e.exitCode < 128) return "conflict";
|
|
149
213
|
else {
|
|
150
214
|
debug$3("Git merge-file failed: %O", e);
|
|
151
215
|
const detail = e.stdout || e.stderr ? `\n\nOutput:\n${e.stdout}\n${e.stderr}` : "";
|
|
@@ -175,8 +239,8 @@ var getBaseTemplate = (_opts) => {
|
|
|
175
239
|
description: "Ultra-fast Rust-based linter."
|
|
176
240
|
},
|
|
177
241
|
{
|
|
178
|
-
name: "
|
|
179
|
-
description: "
|
|
242
|
+
name: "oxfmt",
|
|
243
|
+
description: "High performance code formatting."
|
|
180
244
|
},
|
|
181
245
|
{
|
|
182
246
|
name: "Vitest",
|
|
@@ -274,7 +338,7 @@ var getWebVanillaTemplate = (_opts) => {
|
|
|
274
338
|
preview: "vite preview",
|
|
275
339
|
test: "vitest run",
|
|
276
340
|
"test:ui": "vitest",
|
|
277
|
-
"test
|
|
341
|
+
"integration-test": "playwright test"
|
|
278
342
|
},
|
|
279
343
|
files: [],
|
|
280
344
|
templateDir: getTemplateDir(__dirname$3, "web-vanilla")
|
|
@@ -340,7 +404,7 @@ var getWebAppTemplate = (_opts) => {
|
|
|
340
404
|
preview: "vite preview",
|
|
341
405
|
test: "vitest run",
|
|
342
406
|
"test:ui": "vitest",
|
|
343
|
-
"test
|
|
407
|
+
"integration-test": "playwright test",
|
|
344
408
|
start: "vite preview"
|
|
345
409
|
},
|
|
346
410
|
files: [],
|
|
@@ -417,7 +481,7 @@ var getWebFullstackTemplate = (_opts) => {
|
|
|
417
481
|
build: "npm run build --workspaces",
|
|
418
482
|
dev: "npm run dev --workspaces",
|
|
419
483
|
test: "npm run test --workspaces",
|
|
420
|
-
"test
|
|
484
|
+
"integration-test": "playwright test"
|
|
421
485
|
},
|
|
422
486
|
files: [],
|
|
423
487
|
templateDir: getTemplateDir(__dirname$1, "web-fullstack")
|
|
@@ -428,14 +492,13 @@ var getWebFullstackTemplate = (_opts) => {
|
|
|
428
492
|
var MOCK_OPTS = {
|
|
429
493
|
template: "cli",
|
|
430
494
|
projectName: "mock",
|
|
495
|
+
author: "mock",
|
|
496
|
+
githubUsername: "mock",
|
|
431
497
|
directory: ".",
|
|
432
498
|
packageManager: "npm",
|
|
433
|
-
overwrite: false,
|
|
434
499
|
update: false,
|
|
435
500
|
installDependencies: false,
|
|
436
501
|
build: false,
|
|
437
|
-
dev: false,
|
|
438
|
-
open: false,
|
|
439
502
|
progress: true,
|
|
440
503
|
createGithubRepository: false
|
|
441
504
|
};
|
|
@@ -477,6 +540,22 @@ var getAllTemplatesInfo = () => {
|
|
|
477
540
|
//#endregion
|
|
478
541
|
//#region src/cli.ts
|
|
479
542
|
var pathExists$1 = (p) => fs.access(p).then(() => true).catch(() => false);
|
|
543
|
+
var getDefaultAuthor = async () => {
|
|
544
|
+
try {
|
|
545
|
+
const { stdout } = await execa("git", ["config", "user.name"]);
|
|
546
|
+
return stdout.trim();
|
|
547
|
+
} catch (e) {
|
|
548
|
+
return "";
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
var getDefaultGithubUsername = async () => {
|
|
552
|
+
try {
|
|
553
|
+
const { stdout } = await execa("git", ["config", "github.user"]);
|
|
554
|
+
return stdout.trim();
|
|
555
|
+
} catch (e) {
|
|
556
|
+
return "";
|
|
557
|
+
}
|
|
558
|
+
};
|
|
480
559
|
var debug$2 = debugLib("create-template-project:cli");
|
|
481
560
|
var parseArgs = async () => {
|
|
482
561
|
debug$2("Parsing CLI arguments: %O", process.argv);
|
|
@@ -530,17 +609,20 @@ Templates:
|
|
|
530
609
|
p.outro("Use \"create\" to scaffold a new project.");
|
|
531
610
|
process.exit(0);
|
|
532
611
|
});
|
|
533
|
-
program.command("create").description("Create a new project from a template").option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").option("-n, --name <name>", "Project name").option("-
|
|
612
|
+
program.command("create").description("Create a new project from a template").option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").option("-n, --name <name>", "Project name").option("--description <description>", "Project description").option("-k, --keywords <keywords>", "Project keywords (comma separated)").option("-a, --author <author>", "Author name (defaults to 'git config user.name')").option("--github-username <username>", "GitHub username (defaults to 'git config github.user')").option("-p, --package-manager <pm>", "Package manager (npm, pnpm, yarn)", "pnpm").option("--create-github-repository", "Create GitHub project").requiredOption("--path <path>", "Output directory").option("--install-dependencies", "Install dependencies after scaffolding", false).option("--build", "Run the CI script (lint, build, test) after scaffolding", false).option("--no-progress", "Do not show progress indicators").action(async (opts) => {
|
|
534
613
|
debug$2("Executing \"create\" command with options: %O", opts);
|
|
535
614
|
commandResult = {
|
|
536
615
|
...opts,
|
|
537
616
|
update: false,
|
|
538
617
|
template: opts.template,
|
|
539
618
|
projectName: opts.name,
|
|
619
|
+
description: opts.description,
|
|
620
|
+
keywords: opts.keywords,
|
|
621
|
+
author: opts.author || await getDefaultAuthor(),
|
|
622
|
+
githubUsername: opts.githubUsername || await getDefaultGithubUsername(),
|
|
540
623
|
packageManager: opts.packageManager,
|
|
541
|
-
directory: path.resolve(opts.
|
|
624
|
+
directory: path.resolve(opts.path),
|
|
542
625
|
createGithubRepository: !!opts.createGithubRepository,
|
|
543
|
-
overwrite: !!opts.overwrite,
|
|
544
626
|
progress: !!opts.progress
|
|
545
627
|
};
|
|
546
628
|
debug$2("Processed \"create\" options: %O", commandResult);
|
|
@@ -551,21 +633,45 @@ Details:
|
|
|
551
633
|
It intelligently merges changes into your existing files using 'git merge-file'.
|
|
552
634
|
|
|
553
635
|
Restrictions & Behavior:
|
|
554
|
-
- Seed Files: Files in 'src/', 'client/src/', etc., are considered "seed" files and are NEVER overwritten or modified during an update to protect your application logic.
|
|
636
|
+
- Seed Files: Files in 'src/', 'client/src/', etc., and ALL markdown files (*.md) are considered "seed" files and are NEVER overwritten or modified during an update to protect your application logic and documentation.
|
|
555
637
|
- package.json: Dependencies and scripts are merged. Existing versions are preserved unless they are missing.
|
|
556
638
|
- Merging: For non-seed files, the tool attempts to merge template changes. If a conflict occurs, it will be marked with standard git conflict markers.
|
|
557
639
|
- Confirmation: The command will always show a summary of proposed changes (ADD, MODIFY) and ask for your confirmation before applying them.
|
|
558
|
-
`).option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").option("-
|
|
640
|
+
`).option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").option("--description <description>", "Project description").option("-k, --keywords <keywords>", "Project keywords (comma separated)").option("-a, --author <author>", "Author name (defaults to 'git config user.name')").option("--github-username <username>", "GitHub username (defaults to 'git config github.user')").option("-p, --package-manager <pm>", "Package manager (npm, pnpm, yarn)", "pnpm").option("--create-github-repository", "Create GitHub project").option("-d, --directory <path>", "Output directory", ".").option("--install-dependencies", "Install dependencies after scaffolding", false).option("--build", "Run the CI script (lint, build, test) after updating", false).option("--dev", "Run the dev server after scaffolding", false).option("--open", "Open the browser after scaffolding", false).option("--no-progress", "Do not show progress indicators").action(async (opts) => {
|
|
559
641
|
debug$2("Executing \"update\" command with options: %O", opts);
|
|
642
|
+
const directory = path.resolve(opts.directory);
|
|
643
|
+
const pkgPath = path.join(directory, "package.json");
|
|
644
|
+
if (!await pathExists$1(pkgPath)) {
|
|
645
|
+
p.log.error(`No package.json found in ${directory}. The update command must be run in a project directory.`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
let pkg;
|
|
649
|
+
try {
|
|
650
|
+
pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
651
|
+
} catch (e) {
|
|
652
|
+
p.log.error(`Failed to read or parse package.json at ${pkgPath}: ${e.message}`);
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
if (!pkg.name) {
|
|
656
|
+
p.log.error(`No name property found in ${pkgPath}.`);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
if (!pkg["create-template-project"]?.template) {
|
|
660
|
+
p.log.error(`No "create-template-project" configuration found in ${pkgPath}. The update command can only be used on projects created with this tool.`);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
560
663
|
commandResult = {
|
|
561
664
|
...opts,
|
|
562
665
|
update: true,
|
|
563
|
-
template: opts.template,
|
|
564
|
-
projectName:
|
|
666
|
+
template: opts.template || pkg["create-template-project"]?.template,
|
|
667
|
+
projectName: pkg.name,
|
|
668
|
+
description: opts.description || pkg.description,
|
|
669
|
+
keywords: opts.keywords || (pkg.keywords ? pkg.keywords.join(", ") : void 0),
|
|
670
|
+
author: opts.author || pkg.author || await getDefaultAuthor(),
|
|
671
|
+
githubUsername: opts.githubUsername || pkg["create-template-project"]?.githubUsername || await getDefaultGithubUsername(),
|
|
565
672
|
packageManager: opts.packageManager,
|
|
566
|
-
directory
|
|
673
|
+
directory,
|
|
567
674
|
createGithubRepository: !!opts.createGithubRepository,
|
|
568
|
-
overwrite: !!opts.overwrite,
|
|
569
675
|
progress: !!opts.progress
|
|
570
676
|
};
|
|
571
677
|
debug$2("Processed \"update\" options: %O", commandResult);
|
|
@@ -595,38 +701,84 @@ Restrictions & Behavior:
|
|
|
595
701
|
const pkgPath = path.join(projectDir, "package.json");
|
|
596
702
|
const pkgExists = await pathExists$1(pkgPath);
|
|
597
703
|
let existingConfig = {};
|
|
704
|
+
let existingAuthor = "";
|
|
705
|
+
let existingGithubUsername = "";
|
|
706
|
+
let existingDescription = "";
|
|
707
|
+
let existingKeywords = [];
|
|
598
708
|
if (pkgExists) try {
|
|
599
|
-
|
|
709
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
710
|
+
existingConfig = pkg["create-template-project"] || {};
|
|
711
|
+
existingAuthor = pkg.author;
|
|
712
|
+
existingGithubUsername = existingConfig.githubUsername;
|
|
713
|
+
existingDescription = pkg.description;
|
|
714
|
+
existingKeywords = pkg.keywords || [];
|
|
600
715
|
debug$2("Found existing project config: %O", existingConfig);
|
|
601
716
|
} catch (e) {
|
|
602
717
|
debug$2("Failed to read existing package.json: %O", e);
|
|
603
718
|
}
|
|
719
|
+
const projectDescription = await p.text({
|
|
720
|
+
message: "Project description:",
|
|
721
|
+
placeholder: "A new project",
|
|
722
|
+
defaultValue: existingDescription || ""
|
|
723
|
+
});
|
|
724
|
+
if (p.isCancel(projectDescription)) {
|
|
725
|
+
p.cancel("Operation cancelled.");
|
|
726
|
+
process.exit(0);
|
|
727
|
+
}
|
|
728
|
+
const projectKeywords = await p.text({
|
|
729
|
+
message: "Project keywords (comma separated):",
|
|
730
|
+
placeholder: "cli, nodejs, typescript",
|
|
731
|
+
defaultValue: existingKeywords ? existingKeywords.join(", ") : ""
|
|
732
|
+
});
|
|
733
|
+
if (p.isCancel(projectKeywords)) {
|
|
734
|
+
p.cancel("Operation cancelled.");
|
|
735
|
+
process.exit(0);
|
|
736
|
+
}
|
|
737
|
+
const defaultAuthor = await getDefaultAuthor();
|
|
738
|
+
const author = await p.text({
|
|
739
|
+
message: "Author name:",
|
|
740
|
+
placeholder: "Your Name",
|
|
741
|
+
defaultValue: existingAuthor || existingConfig.author || defaultAuthor,
|
|
742
|
+
validate: (value) => value && value.length > 0 ? void 0 : "Author name is required"
|
|
743
|
+
});
|
|
744
|
+
if (p.isCancel(author)) {
|
|
745
|
+
p.cancel("Operation cancelled.");
|
|
746
|
+
process.exit(0);
|
|
747
|
+
}
|
|
748
|
+
const defaultGithubUsername = await getDefaultGithubUsername();
|
|
749
|
+
const githubUsername = await p.text({
|
|
750
|
+
message: "GitHub username:",
|
|
751
|
+
placeholder: "your-github-username",
|
|
752
|
+
defaultValue: existingGithubUsername || defaultGithubUsername,
|
|
753
|
+
validate: (value) => value && value.length > 0 ? void 0 : "GitHub username is required"
|
|
754
|
+
});
|
|
755
|
+
if (p.isCancel(githubUsername)) {
|
|
756
|
+
p.cancel("Operation cancelled.");
|
|
757
|
+
process.exit(0);
|
|
758
|
+
}
|
|
604
759
|
let update = false;
|
|
605
|
-
let overwrite = false;
|
|
606
760
|
if (exists) {
|
|
607
761
|
const action = await p.select({
|
|
608
762
|
message: `Directory "${projectDir}" already exists. What would you like to do?`,
|
|
609
|
-
options: [
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
value: "overwrite"
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
label: "Cancel",
|
|
620
|
-
value: "cancel"
|
|
621
|
-
}
|
|
622
|
-
]
|
|
763
|
+
options: [{
|
|
764
|
+
label: "Run an update",
|
|
765
|
+
value: "update"
|
|
766
|
+
}, {
|
|
767
|
+
label: "Cancel",
|
|
768
|
+
value: "cancel"
|
|
769
|
+
}]
|
|
623
770
|
});
|
|
624
771
|
if (p.isCancel(action) || action === "cancel") {
|
|
625
772
|
p.cancel("Operation cancelled.");
|
|
626
773
|
process.exit(0);
|
|
627
774
|
}
|
|
628
|
-
if (action === "update")
|
|
629
|
-
|
|
775
|
+
if (action === "update") {
|
|
776
|
+
if (!existingConfig.template) {
|
|
777
|
+
p.log.error(`No "create-template-project" configuration found in ${pkgPath}. The update command can only be used on projects created with this tool.`);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
update = true;
|
|
781
|
+
}
|
|
630
782
|
}
|
|
631
783
|
let template = existingConfig.template;
|
|
632
784
|
if (!update || !template) {
|
|
@@ -657,11 +809,11 @@ Restrictions & Behavior:
|
|
|
657
809
|
process.exit(0);
|
|
658
810
|
}
|
|
659
811
|
} else p.log.info(`Using existing template type: ${template}`);
|
|
660
|
-
let packageManager = "
|
|
812
|
+
let packageManager = "pnpm";
|
|
661
813
|
if (!update) {
|
|
662
814
|
packageManager = await p.select({
|
|
663
815
|
message: "Select package manager:",
|
|
664
|
-
initialValue: "
|
|
816
|
+
initialValue: "pnpm",
|
|
665
817
|
options: [
|
|
666
818
|
{
|
|
667
819
|
label: "npm",
|
|
@@ -714,15 +866,16 @@ Restrictions & Behavior:
|
|
|
714
866
|
commandResult = {
|
|
715
867
|
template,
|
|
716
868
|
projectName,
|
|
869
|
+
description: projectDescription,
|
|
870
|
+
keywords: projectKeywords,
|
|
871
|
+
author,
|
|
872
|
+
githubUsername,
|
|
717
873
|
packageManager,
|
|
718
874
|
createGithubRepository: createGithubRepositoryRes,
|
|
719
|
-
directory:
|
|
875
|
+
directory: projectDir,
|
|
720
876
|
update,
|
|
721
|
-
overwrite,
|
|
722
877
|
installDependencies,
|
|
723
878
|
build,
|
|
724
|
-
dev: false,
|
|
725
|
-
open: false,
|
|
726
879
|
progress: true
|
|
727
880
|
};
|
|
728
881
|
});
|
|
@@ -751,16 +904,12 @@ Restrictions & Behavior:
|
|
|
751
904
|
process.exit(1);
|
|
752
905
|
}
|
|
753
906
|
commandResult = validationResult.data;
|
|
754
|
-
const projectDir =
|
|
755
|
-
if (await pathExists$1(projectDir) && !commandResult.update
|
|
756
|
-
p.cancel(`Directory "${projectDir}" already exists. Use
|
|
907
|
+
const projectDir = commandResult.directory;
|
|
908
|
+
if (await pathExists$1(projectDir) && !commandResult.update) {
|
|
909
|
+
p.cancel(`Directory "${projectDir}" already exists. Use the "update" command to update.`);
|
|
757
910
|
process.exit(1);
|
|
758
911
|
}
|
|
759
|
-
if (commandResult.
|
|
760
|
-
commandResult.dev = true;
|
|
761
|
-
commandResult.installDependencies = true;
|
|
762
|
-
}
|
|
763
|
-
if (commandResult.dev || commandResult.build) commandResult.installDependencies = true;
|
|
912
|
+
if (commandResult.build) commandResult.installDependencies = true;
|
|
764
913
|
return commandResult;
|
|
765
914
|
};
|
|
766
915
|
//#endregion
|
|
@@ -786,11 +935,6 @@ var getSpinner = (progress) => {
|
|
|
786
935
|
stop: (msg) => progress ? s.stop(msg) : void 0
|
|
787
936
|
};
|
|
788
937
|
};
|
|
789
|
-
var showNote = (msg, title, progress) => {
|
|
790
|
-
if (progress !== void 0 && !progress) return;
|
|
791
|
-
if (title) p.log.success(title);
|
|
792
|
-
p.note(msg);
|
|
793
|
-
};
|
|
794
938
|
var isFileRequired = (relativePath, type) => {
|
|
795
939
|
if (relativePath === "vitest.config.ts") return ![
|
|
796
940
|
"cli",
|
|
@@ -801,23 +945,17 @@ var isFileRequired = (relativePath, type) => {
|
|
|
801
945
|
return true;
|
|
802
946
|
};
|
|
803
947
|
var generateProject = async (opts) => {
|
|
804
|
-
const { template: type, projectName, directory, update,
|
|
948
|
+
const { template: type, projectName, author, githubUsername, directory, update, progress } = opts;
|
|
805
949
|
const isProgress = progress !== false;
|
|
806
950
|
const log = getLog(isProgress);
|
|
807
951
|
const spinner = () => getSpinner(isProgress);
|
|
808
|
-
const projectDir =
|
|
952
|
+
const projectDir = directory;
|
|
809
953
|
debug$1("Project generation started for: %s", projectName);
|
|
810
954
|
debug$1("Options: %O", opts);
|
|
811
955
|
debug$1("Project directory: %s", projectDir);
|
|
812
956
|
let isUpdate = !!update;
|
|
813
957
|
if (await pathExists(projectDir)) {
|
|
814
|
-
if (
|
|
815
|
-
await fs.rm(projectDir, {
|
|
816
|
-
recursive: true,
|
|
817
|
-
force: true
|
|
818
|
-
});
|
|
819
|
-
isUpdate = false;
|
|
820
|
-
} else if (!isUpdate) throw new Error(`Directory "${projectDir}" already exists. Use --overwrite to replace it or --update to update.`);
|
|
958
|
+
if (!isUpdate) throw new Error(`Directory "${projectDir}" already exists. Use the "update" command to update.`);
|
|
821
959
|
}
|
|
822
960
|
const templates = [getBaseTemplate(opts)];
|
|
823
961
|
debug$1("Applying template: base");
|
|
@@ -863,6 +1001,16 @@ var generateProject = async (opts) => {
|
|
|
863
1001
|
let finalPkg = {
|
|
864
1002
|
name: projectName,
|
|
865
1003
|
version: "0.1.0",
|
|
1004
|
+
description: opts.description || "TODO: Add project description",
|
|
1005
|
+
keywords: opts.keywords ? opts.keywords.split(",").map((k) => k.trim()) : ["TODO: Add keywords"],
|
|
1006
|
+
homepage: `https://github.com/${githubUsername}/${projectName}#readme`,
|
|
1007
|
+
bugs: { url: `https://github.com/${githubUsername}/${projectName}/issues` },
|
|
1008
|
+
license: "MIT",
|
|
1009
|
+
author: author || "",
|
|
1010
|
+
repository: {
|
|
1011
|
+
type: "git",
|
|
1012
|
+
url: `https://github.com/${githubUsername}/${projectName}.git`
|
|
1013
|
+
},
|
|
866
1014
|
type: "module",
|
|
867
1015
|
"create-template-project": { template: type },
|
|
868
1016
|
scripts: {},
|
|
@@ -873,6 +1021,7 @@ var generateProject = async (opts) => {
|
|
|
873
1021
|
if (isUpdate && await pathExists(pkgPath)) {
|
|
874
1022
|
debug$1("Loading existing package.json for update");
|
|
875
1023
|
const existingPkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
1024
|
+
if (!existingPkg["create-template-project"]?.template) throw new Error(`No "create-template-project" configuration found in ${pkgPath}. The update command can only be used on projects created with this tool.`);
|
|
876
1025
|
finalPkg = {
|
|
877
1026
|
...finalPkg,
|
|
878
1027
|
...existingPkg
|
|
@@ -919,7 +1068,8 @@ var generateProject = async (opts) => {
|
|
|
919
1068
|
if (isUpdate && isSeedFile(relativePath)) {
|
|
920
1069
|
actions.push({
|
|
921
1070
|
type: "SKIP",
|
|
922
|
-
path: relativePath
|
|
1071
|
+
path: relativePath,
|
|
1072
|
+
reason: "Seed file - skipped during update to preserve manual changes"
|
|
923
1073
|
});
|
|
924
1074
|
continue;
|
|
925
1075
|
}
|
|
@@ -927,7 +1077,8 @@ var generateProject = async (opts) => {
|
|
|
927
1077
|
if (isUpdate && await pathExists(targetPath)) {
|
|
928
1078
|
actions.push({
|
|
929
1079
|
type: "DELETE",
|
|
930
|
-
path: relativePath
|
|
1080
|
+
path: relativePath,
|
|
1081
|
+
reason: "File no longer required for this template type"
|
|
931
1082
|
});
|
|
932
1083
|
pendingOperations.push(async () => {
|
|
933
1084
|
await fs.rm(targetPath, { force: true });
|
|
@@ -935,8 +1086,8 @@ var generateProject = async (opts) => {
|
|
|
935
1086
|
}
|
|
936
1087
|
continue;
|
|
937
1088
|
}
|
|
938
|
-
if (relativePath
|
|
939
|
-
relativePath =
|
|
1089
|
+
if (relativePath.startsWith("_") && relativePath.endsWith(".config.ts")) {
|
|
1090
|
+
relativePath = relativePath.substring(1);
|
|
940
1091
|
targetPath = path.join(projectDir, relativePath);
|
|
941
1092
|
}
|
|
942
1093
|
if (relativePath === "package.json") continue;
|
|
@@ -947,21 +1098,36 @@ var generateProject = async (opts) => {
|
|
|
947
1098
|
if (isUpdate && exists) {
|
|
948
1099
|
const existingContent = await fs.readFile(finalTargetPath, "utf8");
|
|
949
1100
|
if (existingContent.trim() !== content.trim()) {
|
|
950
|
-
|
|
1101
|
+
const action = {
|
|
951
1102
|
type: "MODIFY",
|
|
952
|
-
path: finalRelativePath
|
|
953
|
-
|
|
1103
|
+
path: finalRelativePath,
|
|
1104
|
+
reason: "Template tooling or configuration update"
|
|
1105
|
+
};
|
|
1106
|
+
actions.push(action);
|
|
954
1107
|
pendingOperations.push(async () => {
|
|
955
1108
|
const result = await mergeFile(finalTargetPath, existingContent, content, log);
|
|
956
|
-
if (result === "merged")
|
|
957
|
-
|
|
958
|
-
|
|
1109
|
+
if (result === "merged") {
|
|
1110
|
+
action.type = "MERGE";
|
|
1111
|
+
action.reason = "Merged template updates with your manual changes";
|
|
1112
|
+
action.recommendedAction = "Review changes for correct integration";
|
|
1113
|
+
log.info(`โน Merged: ${finalRelativePath}`);
|
|
1114
|
+
} else if (result === "conflict") {
|
|
1115
|
+
action.type = "CONFLICT";
|
|
1116
|
+
action.reason = "Conflicting changes between template and your code";
|
|
1117
|
+
action.recommendedAction = "Resolve git conflict markers in this file";
|
|
1118
|
+
log.warn(`โ Conflict: ${finalRelativePath}`);
|
|
1119
|
+
} else if (result === "updated") {
|
|
1120
|
+
action.type = "UPDATED";
|
|
1121
|
+
action.reason = "File was updated to the latest template version";
|
|
1122
|
+
log.info(`โ Updated: ${finalRelativePath}`);
|
|
1123
|
+
}
|
|
959
1124
|
});
|
|
960
1125
|
}
|
|
961
1126
|
} else if (!exists) {
|
|
962
1127
|
actions.push({
|
|
963
1128
|
type: "ADD",
|
|
964
|
-
path: finalRelativePath
|
|
1129
|
+
path: finalRelativePath,
|
|
1130
|
+
reason: "New template file added"
|
|
965
1131
|
});
|
|
966
1132
|
pendingOperations.push(async () => {
|
|
967
1133
|
await fs.mkdir(path.dirname(finalTargetPath), { recursive: true });
|
|
@@ -977,7 +1143,8 @@ var generateProject = async (opts) => {
|
|
|
977
1143
|
if (isUpdate && isSeedFile(file.path)) {
|
|
978
1144
|
actions.push({
|
|
979
1145
|
type: "SKIP",
|
|
980
|
-
path: file.path
|
|
1146
|
+
path: file.path,
|
|
1147
|
+
reason: "Seed file - skipped during update to preserve manual changes"
|
|
981
1148
|
});
|
|
982
1149
|
continue;
|
|
983
1150
|
}
|
|
@@ -985,7 +1152,8 @@ var generateProject = async (opts) => {
|
|
|
985
1152
|
if (isUpdate && await pathExists(targetPath)) {
|
|
986
1153
|
actions.push({
|
|
987
1154
|
type: "DELETE",
|
|
988
|
-
path: file.path
|
|
1155
|
+
path: file.path,
|
|
1156
|
+
reason: "File no longer required for this template type"
|
|
989
1157
|
});
|
|
990
1158
|
pendingOperations.push(async () => {
|
|
991
1159
|
await fs.rm(targetPath, { force: true });
|
|
@@ -999,21 +1167,36 @@ var generateProject = async (opts) => {
|
|
|
999
1167
|
if (isUpdate && exists) {
|
|
1000
1168
|
const existingContent = await fs.readFile(targetPath, "utf8");
|
|
1001
1169
|
if (existingContent.trim() !== content.trim()) {
|
|
1002
|
-
|
|
1170
|
+
const action = {
|
|
1003
1171
|
type: "MODIFY",
|
|
1004
|
-
path: file.path
|
|
1005
|
-
|
|
1172
|
+
path: file.path,
|
|
1173
|
+
reason: "Template configuration update"
|
|
1174
|
+
};
|
|
1175
|
+
actions.push(action);
|
|
1006
1176
|
pendingOperations.push(async () => {
|
|
1007
1177
|
const result = await mergeFile(targetPath, existingContent, content, log);
|
|
1008
|
-
if (result === "merged")
|
|
1009
|
-
|
|
1010
|
-
|
|
1178
|
+
if (result === "merged") {
|
|
1179
|
+
action.type = "MERGE";
|
|
1180
|
+
action.reason = "Merged template updates with your manual changes";
|
|
1181
|
+
action.recommendedAction = "Review changes for correct integration";
|
|
1182
|
+
log.info(`โน Merged: ${file.path}`);
|
|
1183
|
+
} else if (result === "conflict") {
|
|
1184
|
+
action.type = "CONFLICT";
|
|
1185
|
+
action.reason = "Conflicting changes between template and your code";
|
|
1186
|
+
action.recommendedAction = "Resolve git conflict markers in this file";
|
|
1187
|
+
log.warn(`โ Conflict: ${file.path}`);
|
|
1188
|
+
} else if (result === "updated") {
|
|
1189
|
+
action.type = "UPDATED";
|
|
1190
|
+
action.reason = "File was updated to the latest template version";
|
|
1191
|
+
log.info(`โ Updated: ${file.path}`);
|
|
1192
|
+
}
|
|
1011
1193
|
});
|
|
1012
1194
|
}
|
|
1013
1195
|
} else if (!exists) {
|
|
1014
1196
|
actions.push({
|
|
1015
1197
|
type: "ADD",
|
|
1016
|
-
path: file.path
|
|
1198
|
+
path: file.path,
|
|
1199
|
+
reason: "New template file added"
|
|
1017
1200
|
});
|
|
1018
1201
|
pendingOperations.push(async () => {
|
|
1019
1202
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
@@ -1036,7 +1219,8 @@ var generateProject = async (opts) => {
|
|
|
1036
1219
|
if (workspaceChanged) {
|
|
1037
1220
|
actions.push({
|
|
1038
1221
|
type: workspaceExists ? "MODIFY" : "ADD",
|
|
1039
|
-
path: "pnpm-workspace.yaml"
|
|
1222
|
+
path: "pnpm-workspace.yaml",
|
|
1223
|
+
reason: "Updated workspace configuration for pnpm"
|
|
1040
1224
|
});
|
|
1041
1225
|
pendingOperations.push(async () => {
|
|
1042
1226
|
await fs.writeFile(workspacePath, workspaceYaml);
|
|
@@ -1054,11 +1238,13 @@ var generateProject = async (opts) => {
|
|
|
1054
1238
|
if (pkgChanged) {
|
|
1055
1239
|
if (isUpdate) actions.push({
|
|
1056
1240
|
type: "MODIFY",
|
|
1057
|
-
path: "package.json"
|
|
1241
|
+
path: "package.json",
|
|
1242
|
+
reason: "Updated dependencies and scripts to match latest template"
|
|
1058
1243
|
});
|
|
1059
1244
|
else actions.push({
|
|
1060
1245
|
type: "ADD",
|
|
1061
|
-
path: "package.json"
|
|
1246
|
+
path: "package.json",
|
|
1247
|
+
reason: "Initial project configuration"
|
|
1062
1248
|
});
|
|
1063
1249
|
pendingOperations.push(async () => {
|
|
1064
1250
|
debug$1("Writing final consolidated package.json to: %s", pkgPath);
|
|
@@ -1081,6 +1267,16 @@ var generateProject = async (opts) => {
|
|
|
1081
1267
|
} else log.info("No changes detected.");
|
|
1082
1268
|
}
|
|
1083
1269
|
for (const op of pendingOperations) await op();
|
|
1270
|
+
const states = {
|
|
1271
|
+
gitInitialized: false,
|
|
1272
|
+
githubCreated: false,
|
|
1273
|
+
githubSkipped: !opts.createGithubRepository || isUpdate,
|
|
1274
|
+
githubError: "",
|
|
1275
|
+
depsInstalled: false,
|
|
1276
|
+
depsSkipped: !opts.installDependencies,
|
|
1277
|
+
ciRun: false,
|
|
1278
|
+
ciSkipped: !opts.build || !finalPkg.scripts.ci
|
|
1279
|
+
};
|
|
1084
1280
|
const stdio = debug$1.enabled ? "inherit" : "pipe";
|
|
1085
1281
|
if (!await pathExists(path.join(projectDir, ".git"))) {
|
|
1086
1282
|
debug$1("Initializing Git repository");
|
|
@@ -1092,12 +1288,13 @@ var generateProject = async (opts) => {
|
|
|
1092
1288
|
preferLocal: true
|
|
1093
1289
|
});
|
|
1094
1290
|
log.success("Initialized Git repository (git init).");
|
|
1291
|
+
states.gitInitialized = true;
|
|
1095
1292
|
} catch (e) {
|
|
1096
1293
|
debug$1("Failed to initialize Git: %O", e);
|
|
1097
1294
|
const detail = e.stdout || e.stderr ? `\n\nOutput:\n${e.stdout}\n${e.stderr}` : "";
|
|
1098
1295
|
log.error(`Failed to initialize Git: ${e.message}${detail}`);
|
|
1099
1296
|
}
|
|
1100
|
-
}
|
|
1297
|
+
} else states.gitInitialized = true;
|
|
1101
1298
|
if (opts.createGithubRepository && !isUpdate) {
|
|
1102
1299
|
debug$1("Creating GitHub repository");
|
|
1103
1300
|
try {
|
|
@@ -1115,10 +1312,12 @@ var generateProject = async (opts) => {
|
|
|
1115
1312
|
preferLocal: true
|
|
1116
1313
|
});
|
|
1117
1314
|
log.success("Created GitHub repository (gh repo create).");
|
|
1315
|
+
states.githubCreated = true;
|
|
1118
1316
|
} catch (e) {
|
|
1119
1317
|
debug$1("Failed to create GitHub repository: %O", e);
|
|
1120
1318
|
const detail = e.stdout || e.stderr ? `\n\nOutput:\n${e.stdout}\n${e.stderr}` : "";
|
|
1121
1319
|
log.warn(`Failed to create GitHub repository: ${e.message}${detail}\nEnsure "gh" CLI is installed and authenticated.`);
|
|
1320
|
+
states.githubError = e.message;
|
|
1122
1321
|
}
|
|
1123
1322
|
}
|
|
1124
1323
|
if (opts.installDependencies) {
|
|
@@ -1133,6 +1332,7 @@ var generateProject = async (opts) => {
|
|
|
1133
1332
|
preferLocal: true
|
|
1134
1333
|
});
|
|
1135
1334
|
s.stop(`\x1b[1G\x1b[2K\x1b[32mโ\x1b[39m Dependencies installed (${pm} install).`);
|
|
1335
|
+
states.depsInstalled = true;
|
|
1136
1336
|
} catch (e) {
|
|
1137
1337
|
debug$1("Failed to install dependencies: %O", e);
|
|
1138
1338
|
s.stop("Failed to install dependencies.");
|
|
@@ -1144,16 +1344,16 @@ var generateProject = async (opts) => {
|
|
|
1144
1344
|
if (opts.build && finalPkg.scripts.ci) {
|
|
1145
1345
|
debug$1("Running CI script");
|
|
1146
1346
|
const s = spinner();
|
|
1147
|
-
if (finalPkg.scripts
|
|
1148
|
-
s.start(`Formatting files with
|
|
1347
|
+
if (finalPkg.scripts.format) {
|
|
1348
|
+
s.start(`Formatting files with oxfmt (${pm} run format)...`);
|
|
1149
1349
|
try {
|
|
1150
|
-
debug$1("Executing: %s run
|
|
1151
|
-
await execa(pm, ["run", "
|
|
1350
|
+
debug$1("Executing: %s run format", pm);
|
|
1351
|
+
await execa(pm, ["run", "format"], {
|
|
1152
1352
|
cwd: projectDir,
|
|
1153
1353
|
stdio,
|
|
1154
1354
|
preferLocal: true
|
|
1155
1355
|
});
|
|
1156
|
-
s.stop(`\x1b[1G\x1b[2K\x1b[32mโ\x1b[39m Files formatted (${pm} run
|
|
1356
|
+
s.stop(`\x1b[1G\x1b[2K\x1b[32mโ\x1b[39m Files formatted (${pm} run format).`);
|
|
1157
1357
|
} catch (e) {
|
|
1158
1358
|
debug$1("Failed to format files: %O", e);
|
|
1159
1359
|
s.stop("Failed to format files.");
|
|
@@ -1170,6 +1370,7 @@ var generateProject = async (opts) => {
|
|
|
1170
1370
|
preferLocal: true
|
|
1171
1371
|
});
|
|
1172
1372
|
s.stop(`\x1b[1G\x1b[2K\x1b[32mโ\x1b[39m CI script completed (${pm} run ci).`);
|
|
1373
|
+
states.ciRun = true;
|
|
1173
1374
|
} catch (e) {
|
|
1174
1375
|
debug$1("Failed to run CI script: %O", e);
|
|
1175
1376
|
s.stop("Failed to run CI script.");
|
|
@@ -1178,53 +1379,222 @@ var generateProject = async (opts) => {
|
|
|
1178
1379
|
throw new Error(`Failed to run CI script: ${e.message}${detail}`);
|
|
1179
1380
|
}
|
|
1180
1381
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
await execa(pm, [
|
|
1188
|
-
"run",
|
|
1189
|
-
"dev",
|
|
1190
|
-
"--",
|
|
1191
|
-
"--open"
|
|
1192
|
-
], {
|
|
1193
|
-
cwd: projectDir,
|
|
1194
|
-
stdio: "inherit",
|
|
1195
|
-
preferLocal: true
|
|
1196
|
-
});
|
|
1197
|
-
} catch (e) {
|
|
1198
|
-
log.error("Dev server failed: " + e);
|
|
1199
|
-
}
|
|
1200
|
-
else try {
|
|
1201
|
-
debug$1("Executing: %s run dev", pm);
|
|
1202
|
-
await execa(pm, ["run", "dev"], {
|
|
1203
|
-
cwd: projectDir,
|
|
1204
|
-
stdio: "inherit",
|
|
1205
|
-
preferLocal: true
|
|
1206
|
-
});
|
|
1207
|
-
} catch (e) {
|
|
1208
|
-
log.error("Dev server failed: " + e);
|
|
1209
|
-
}
|
|
1382
|
+
let hasErrors = false;
|
|
1383
|
+
let hasWarnings = false;
|
|
1384
|
+
const errorMessages = [];
|
|
1385
|
+
if (states.githubError) {
|
|
1386
|
+
hasWarnings = true;
|
|
1387
|
+
errorMessages.push(`GitHub repository creation failed: ${states.githubError}`);
|
|
1210
1388
|
}
|
|
1389
|
+
await generateGeneratedMd(projectDir, opts, pm, states, isUpdate, {
|
|
1390
|
+
hasErrors,
|
|
1391
|
+
hasWarnings,
|
|
1392
|
+
errorMessages
|
|
1393
|
+
}, actions);
|
|
1394
|
+
const successMsg = `Project "${projectName}" ${isUpdate ? "updated" : "scaffolded"} successfully in ${projectDir}. A detailed setup guide has been generated at GENERATED.md`;
|
|
1395
|
+
if (hasWarnings) log.warn(`${successMsg} (completed with warnings)`);
|
|
1396
|
+
else log.success(successMsg);
|
|
1211
1397
|
};
|
|
1212
|
-
function
|
|
1213
|
-
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1398
|
+
async function generateGeneratedMd(projectDir, opts, pm, states, isUpdate, status, actions) {
|
|
1399
|
+
const statusBadge = status.hasErrors ? "๐ด **Completed with Errors**" : status.hasWarnings ? "๐ก **Completed with Warnings**" : "๐ข **Successfully Completed**";
|
|
1400
|
+
const md = [
|
|
1401
|
+
`# ๐ Project Setup Guide: ${opts.projectName}`,
|
|
1402
|
+
"",
|
|
1403
|
+
`Welcome to your newly ${isUpdate ? "updated" : "generated"} **${opts.template}** project! This document outlines what was scaffolded, the automated steps already completed, and the remaining manual adjustments required to finalize your setup.`,
|
|
1404
|
+
"",
|
|
1405
|
+
`**Status:** ${statusBadge}`,
|
|
1406
|
+
"",
|
|
1407
|
+
...status.errorMessages.length > 0 ? [
|
|
1408
|
+
"### โ ๏ธ Issues Encountered",
|
|
1409
|
+
...status.errorMessages.map((msg) => `- ${msg}`),
|
|
1410
|
+
""
|
|
1411
|
+
] : [],
|
|
1412
|
+
"## ๐ฆ What Was Generated",
|
|
1413
|
+
`* **Project Name:** \`${opts.projectName}\``,
|
|
1414
|
+
`* **Template Used:** \`${opts.template}\``,
|
|
1415
|
+
`* **Package Manager:** \`${pm}\``,
|
|
1416
|
+
"",
|
|
1417
|
+
"---",
|
|
1418
|
+
"",
|
|
1419
|
+
"## ๐ Initialization Checklist",
|
|
1420
|
+
isUpdate ? "The project was updated with the latest template changes:" : "The following tasks were executed during the generation process:",
|
|
1421
|
+
`- [x] Scaffold project files and directories`,
|
|
1422
|
+
`- [x] Configure \`package.json\` with appropriate dependencies`,
|
|
1423
|
+
`- [${states.depsInstalled ? "x" : " "}] Install dependencies using \`${pm}\`${states.depsSkipped ? " *(Skipped)*" : ""}`,
|
|
1424
|
+
`- [${states.gitInitialized ? "x" : " "}] Initialize Git repository`,
|
|
1425
|
+
`- [${states.githubCreated ? "x" : " "}] Create GitHub repository${states.githubSkipped ? " *(Skipped)*" : states.githubError ? " *(Failed)*" : ""}`,
|
|
1426
|
+
`- [${states.ciRun ? "x" : " "}] Run initial CI pipeline (lint, build, test)${states.ciSkipped ? " *(Skipped)*" : ""}`,
|
|
1427
|
+
"",
|
|
1428
|
+
...isUpdate ? [
|
|
1429
|
+
"### ๐ ๏ธ Upgrade Details",
|
|
1430
|
+
"The following files were affected by this update:",
|
|
1431
|
+
"",
|
|
1432
|
+
"| File Path | Action | Reason | Next Steps |",
|
|
1433
|
+
"| :--- | :--- | :--- | :--- |",
|
|
1434
|
+
...actions.filter((a) => a.type !== "SKIP").map((a) => {
|
|
1435
|
+
const actionIcon = {
|
|
1436
|
+
ADD: "โ ADD",
|
|
1437
|
+
MODIFY: "๐ MODIFY",
|
|
1438
|
+
MERGE: "๐ MERGE",
|
|
1439
|
+
CONFLICT: "๐ฅ CONFLICT",
|
|
1440
|
+
DELETE: "๐๏ธ DELETE",
|
|
1441
|
+
UPDATED: "โจ UPDATED"
|
|
1442
|
+
}[a.type] || a.type;
|
|
1443
|
+
return `| \`${a.path}\` | ${actionIcon} | ${a.reason || "-"} | ${a.recommendedAction || (a.type === "CONFLICT" ? "**Resolve conflicts**" : "Review changes")} |`;
|
|
1444
|
+
}),
|
|
1445
|
+
""
|
|
1446
|
+
] : [],
|
|
1447
|
+
"---",
|
|
1448
|
+
"",
|
|
1449
|
+
"## ๐ ๏ธ Manual Adjustments Needed",
|
|
1450
|
+
"To complete your project setup, please review and manually update the following:",
|
|
1451
|
+
"- [ ] **`LICENSE`**: Verify the copyright year and author name.",
|
|
1452
|
+
"- [ ] **`package.json`**: Review the description, keywords, author, and repository links.",
|
|
1453
|
+
"- [ ] **`README.md`**: Update with project-specific instructions, architecture details, and contribution guidelines.",
|
|
1454
|
+
"- [ ] **`.gitignore`**: Note that there is a **`# Custom`** section at the end of the file for your own ignores.",
|
|
1455
|
+
"",
|
|
1456
|
+
"---",
|
|
1457
|
+
"",
|
|
1458
|
+
"## ๐ก Next Steps",
|
|
1459
|
+
"1. Review the generated codebase to familiarize yourself with the structure.",
|
|
1460
|
+
`2. Start the development server using \`${pm} run dev\`.`,
|
|
1461
|
+
"3. Make your first commit and push to your remote repository.",
|
|
1462
|
+
"",
|
|
1463
|
+
"---",
|
|
1464
|
+
"",
|
|
1465
|
+
"## ๐ป Available Commands",
|
|
1466
|
+
`You can run these commands from the project root using \`${pm} run <command>\`:`,
|
|
1467
|
+
"",
|
|
1468
|
+
"| Command | Description |",
|
|
1469
|
+
"| :--- | :--- |",
|
|
1470
|
+
"| `dev` | Starts the development server |",
|
|
1471
|
+
"| `build` | Builds the project for production |",
|
|
1472
|
+
"| `test` | Runs the unit test suite (Vitest) |",
|
|
1473
|
+
"| `lint` | Lints and formats the codebase |",
|
|
1474
|
+
"| `ci` | Runs lint, build, and test (used by CI/CD) |",
|
|
1475
|
+
"",
|
|
1476
|
+
"---",
|
|
1217
1477
|
"",
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1478
|
+
...getTemplateArchitectureSection(opts.template),
|
|
1479
|
+
"",
|
|
1480
|
+
"---",
|
|
1481
|
+
"",
|
|
1482
|
+
"## ๐งช Testing Strategy",
|
|
1483
|
+
"",
|
|
1484
|
+
"### Unit Testing (Vitest)",
|
|
1485
|
+
"This project is pre-configured with **Vitest** for blazing-fast unit testing and coverage reporting.",
|
|
1486
|
+
`- **Where to put tests**: Create files with the \`.test.ts\` or \`.spec.ts\` extension next to your source files (e.g., \`src/main.test.ts\`).`,
|
|
1487
|
+
`- **How to run**: \`${pm} run test\``,
|
|
1488
|
+
`- **Watch mode**: \`${pm} exec vitest\` to automatically re-run tests on file changes.`,
|
|
1489
|
+
`- **Coverage**: Coverage is generated automatically during the test run. Aim for high coverage on core logic!`,
|
|
1490
|
+
"",
|
|
1491
|
+
"### End-to-End (E2E) Testing",
|
|
1492
|
+
"Currently, only unit tests are scaffolded by default. To enhance your project's reliability, we highly recommend adding E2E testing:",
|
|
1493
|
+
`- **For Web Apps**: Consider installing [Playwright](https://playwright.dev/) (\`${pm} create playwright\`) to simulate real user interactions in the browser.`,
|
|
1494
|
+
`- **For CLIs**: Consider using \`execa\` within your Vitest suite to invoke your compiled CLI binary and assert its \`stdout\`/\`stderr\` outputs.`,
|
|
1495
|
+
"",
|
|
1496
|
+
"---",
|
|
1497
|
+
"",
|
|
1498
|
+
"## ๐ Key Git Commands",
|
|
1499
|
+
"Here are the most common Git operations you will use to manage your codebase:",
|
|
1500
|
+
"",
|
|
1501
|
+
"| Command | Description |",
|
|
1502
|
+
"| :--- | :--- |",
|
|
1503
|
+
"| `git status` | Check the current state of your working directory |",
|
|
1504
|
+
"| `git add .` | Stage all your changes for the next commit |",
|
|
1505
|
+
"| `git commit -m \"feat: your feature\"` | Create a new commit (following Conventional Commits) |",
|
|
1506
|
+
"| `git push` | Push your committed changes to the remote repository |",
|
|
1507
|
+
"| `git pull` | Fetch and merge changes from the remote repository |",
|
|
1508
|
+
"| `git checkout -b <branch>` | Create and switch to a new branch |",
|
|
1509
|
+
"",
|
|
1510
|
+
"---",
|
|
1511
|
+
"",
|
|
1512
|
+
"## ๐ Key GitHub (`gh`) Commands",
|
|
1513
|
+
"The `gh` CLI provides powerful tools to interact with GitHub right from your terminal:",
|
|
1514
|
+
"",
|
|
1515
|
+
"| Command | Description |",
|
|
1516
|
+
"| :--- | :--- |",
|
|
1517
|
+
"| `gh repo view --web` | Open the repository in your default web browser |",
|
|
1518
|
+
"| `gh pr create` | Create a new Pull Request |",
|
|
1519
|
+
"| `gh pr checkout <pr-number>` | Checkout a Pull Request branch locally |",
|
|
1520
|
+
"| `gh issue create` | Create a new Issue |",
|
|
1521
|
+
"| `gh issue list` | List all open Issues |",
|
|
1522
|
+
"| `gh repo delete <owner>/<repo> --confirm` | Dangerously delete a repository completely (use with caution!) |",
|
|
1523
|
+
"",
|
|
1524
|
+
"---",
|
|
1525
|
+
"",
|
|
1526
|
+
"## ๐ Creating a Release",
|
|
1527
|
+
"This project uses Conventional Commits and automated changelogs. To create a new release:",
|
|
1528
|
+
"1. **Verify build/tests:** `pnpm run ci`",
|
|
1529
|
+
"2. **Bump version:** `pnpm version <patch|minor|major> --no-git-tag-version`",
|
|
1530
|
+
"3. **Update changelog:** `pnpm run create-changelog`",
|
|
1531
|
+
"4. **Commit changes:** `git add . && git commit -m \"chore(release): $(node -p 'require(\"./package.json\").version')\"`",
|
|
1532
|
+
"5. **Tag & Push:** `git tag v$(node -p 'require(\"./package.json\").version') && git push && git push --tags`",
|
|
1533
|
+
"6. **Create GitHub Release:** `gh release create v$(node -p 'require(\"./package.json\").version') --generate-notes`",
|
|
1534
|
+
"7. **Publish (if applicable):** `pnpm publish`",
|
|
1535
|
+
"",
|
|
1536
|
+
"---",
|
|
1537
|
+
"",
|
|
1538
|
+
"<br>",
|
|
1539
|
+
"<p align=\"center\"><i>This file was auto-generated by <b>create-template-project</b>.</i></p>"
|
|
1540
|
+
].join("\n");
|
|
1541
|
+
await fs.writeFile(path.join(projectDir, "GENERATED.md"), md);
|
|
1542
|
+
}
|
|
1543
|
+
function getTemplateArchitectureSection(template) {
|
|
1544
|
+
switch (template) {
|
|
1545
|
+
case "cli": return [
|
|
1546
|
+
"## ๐๏ธ CLI Architecture",
|
|
1547
|
+
"This project uses `commander` for argument parsing and `@clack/prompts` for interactive CLI interfaces.",
|
|
1548
|
+
"",
|
|
1549
|
+
"### Source Files Generated",
|
|
1550
|
+
"- **`src/index.ts`**: The main execution entry point. Handles top-level errors and bootstraps the CLI application.",
|
|
1551
|
+
"- **`src/cli.ts`**: Parses command-line arguments and orchestrates your user prompts.",
|
|
1552
|
+
"",
|
|
1553
|
+
"### How to Enhance",
|
|
1554
|
+
"- Add new sub-commands directly in `src/cli.ts`.",
|
|
1555
|
+
"- Extract logic into a new `src/commands/` directory as your application scales."
|
|
1556
|
+
];
|
|
1557
|
+
case "web-vanilla": return [
|
|
1558
|
+
"## ๐๏ธ Web Vanilla Architecture",
|
|
1559
|
+
"A standalone, blazing fast web application scaffolded with Vite.",
|
|
1560
|
+
"",
|
|
1561
|
+
"### Source Files Generated",
|
|
1562
|
+
"- **`index.html`**: The main HTML entry point that loads your application scripts.",
|
|
1563
|
+
"- **`src/main.ts`**: The core TypeScript application logic where you can start adding DOM manipulation.",
|
|
1564
|
+
"",
|
|
1565
|
+
"### How to Enhance",
|
|
1566
|
+
"- Add new UI logic or Web Components inside the `src/` directory.",
|
|
1567
|
+
"- Create styling (`.css` or `.scss`) and import them directly into `main.ts`."
|
|
1568
|
+
];
|
|
1569
|
+
case "web-app": return [
|
|
1570
|
+
"## ๐๏ธ Web App Architecture",
|
|
1571
|
+
"A robust React SPA configured with MUI components and TanStack Query.",
|
|
1572
|
+
"",
|
|
1573
|
+
"### Source Files Generated",
|
|
1574
|
+
"- **`index.html`**: The HTML entry point hosting the React root element.",
|
|
1575
|
+
"- **`src/main.tsx`**: Bootstraps the React application and mounts all necessary providers (QueryClient, Theme).",
|
|
1576
|
+
"- **`src/App.tsx`**: The root application component. Start building your UI here.",
|
|
1577
|
+
"",
|
|
1578
|
+
"### How to Enhance",
|
|
1579
|
+
"- Add new components to a `src/components/` directory.",
|
|
1580
|
+
"- Set up React Router for client-side routing.",
|
|
1581
|
+
"- Manage complex global state with a store like Zustand if needed."
|
|
1582
|
+
];
|
|
1583
|
+
case "web-fullstack": return [
|
|
1584
|
+
"## ๐๏ธ Fullstack Monorepo Architecture",
|
|
1585
|
+
"A modern monorepo combining an Express server with a React client, seamlessly integrated using workspaces.",
|
|
1586
|
+
"",
|
|
1587
|
+
"### Source Files Generated",
|
|
1588
|
+
"- **`client/`**: The frontend React application (similar to the `web-app` template).",
|
|
1589
|
+
"- **`server/`**: The backend Express/Node application delivering API endpoints.",
|
|
1590
|
+
"",
|
|
1591
|
+
"### How to Enhance",
|
|
1592
|
+
"- Add new API routes in the `server` package.",
|
|
1593
|
+
"- Consume those routes in the `client` package via TanStack Query.",
|
|
1594
|
+
"- **Tip**: Create a `shared/` workspace package to share types across both frontend and backend for end-to-end type safety."
|
|
1595
|
+
];
|
|
1596
|
+
default: return [];
|
|
1597
|
+
}
|
|
1228
1598
|
}
|
|
1229
1599
|
//#endregion
|
|
1230
1600
|
//#region src/index.ts
|