create-template-project 0.1.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 +21 -0
- package/README.md +97 -0
- package/dist/cli.mjs +272 -0
- package/dist/config/dependencies.json +156 -0
- package/dist/generators/project.mjs +354 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +32 -0
- package/dist/templates/base/files/.github/workflows/node.js.yml +20 -0
- package/dist/templates/base/files/.prettierrc.json +6 -0
- package/dist/templates/base/files/AGENTS.md +5 -0
- package/dist/templates/base/files/CONTRIBUTING.md +31 -0
- package/dist/templates/base/files/README.md +23 -0
- package/dist/templates/base/files/_oxlint.config.ts +74 -0
- package/dist/templates/base/files/commitlint.config.js +1 -0
- package/dist/templates/base/files/package.json +32 -0
- package/dist/templates/base/files/tsconfig.json +42 -0
- package/dist/templates/base/files/vitest.config.ts +3 -0
- package/dist/templates/base/index.mjs +16 -0
- package/dist/templates/cli/files/package.json +14 -0
- package/dist/templates/cli/files/src/index.test.ts +5 -0
- package/dist/templates/cli/files/src/index.ts +13 -0
- package/dist/templates/cli/files/tsdown.config.ts +3 -0
- package/dist/templates/cli/index.mjs +16 -0
- package/dist/templates/fullstack/files/client/index.html +8 -0
- package/dist/templates/fullstack/files/client/package.json +10 -0
- package/dist/templates/fullstack/files/client/src/App.test.tsx +8 -0
- package/dist/templates/fullstack/files/client/src/App.tsx +50 -0
- package/dist/templates/fullstack/files/client/src/components/ProtectedRoute.tsx +21 -0
- package/dist/templates/fullstack/files/client/src/contexts/AuthContext.tsx +63 -0
- package/dist/templates/fullstack/files/client/src/main.tsx +9 -0
- package/dist/templates/fullstack/files/client/src/pages/Dashboard.tsx +39 -0
- package/dist/templates/fullstack/files/client/src/pages/Login.tsx +81 -0
- package/dist/templates/fullstack/files/client/src/trpc.ts +4 -0
- package/dist/templates/fullstack/files/client/tsdown.config.ts +3 -0
- package/dist/templates/fullstack/files/package.json +35 -0
- package/dist/templates/fullstack/files/server/package.json +10 -0
- package/dist/templates/fullstack/files/server/src/context.ts +24 -0
- package/dist/templates/fullstack/files/server/src/index.test.ts +7 -0
- package/dist/templates/fullstack/files/server/src/index.ts +32 -0
- package/dist/templates/fullstack/files/server/src/routers/_app.ts +8 -0
- package/dist/templates/fullstack/files/server/src/routers/auth.ts +29 -0
- package/dist/templates/fullstack/files/server/src/trpc.ts +18 -0
- package/dist/templates/fullstack/files/server/tsdown.config.ts +3 -0
- package/dist/templates/fullstack/index.mjs +42 -0
- package/dist/templates/webapp/files/backend/src/index.ts +17 -0
- package/dist/templates/webapp/files/frontend/index.html +9 -0
- package/dist/templates/webapp/files/frontend/src/index.ts +4 -0
- package/dist/templates/webapp/files/package.json +13 -0
- package/dist/templates/webapp/files/src/index.test.ts +5 -0
- package/dist/templates/webapp/files/tsdown.config.ts +10 -0
- package/dist/templates/webapp/index.mjs +16 -0
- package/dist/templates/webpage/files/index.html +8 -0
- package/dist/templates/webpage/files/package.json +8 -0
- package/dist/templates/webpage/files/src/index.test.ts +5 -0
- package/dist/templates/webpage/files/src/index.ts +1 -0
- package/dist/templates/webpage/index.mjs +16 -0
- package/dist/types.mjs +30 -0
- package/dist/utils/file.mjs +101 -0
- package/package.json +79 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { getBaseTemplate } from "../templates/base/index.mjs";
|
|
2
|
+
import { getCliTemplate } from "../templates/cli/index.mjs";
|
|
3
|
+
import { getWebpageTemplate } from "../templates/webpage/index.mjs";
|
|
4
|
+
import { getWebappTemplate } from "../templates/webapp/index.mjs";
|
|
5
|
+
import { getFullstackTemplate } from "../templates/fullstack/index.mjs";
|
|
6
|
+
import { getAllFiles, isSeedFile, mergeFile, mergePackageJson, processContent } from "../utils/file.mjs";
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import debugLib from "debug";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { execa } from "execa";
|
|
13
|
+
//#region src/generators/project.ts
|
|
14
|
+
const debug = debugLib("create-template-project:generator");
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const DEPENDENCY_CONFIG_PATH = path.resolve(__dirname, "../config/dependencies.json");
|
|
17
|
+
const pathExists = (p) => fs.access(p).then(() => true).catch(() => false);
|
|
18
|
+
const getLog = (silent) => ({
|
|
19
|
+
info: (msg) => !silent ? p.log.info(msg) : void 0,
|
|
20
|
+
success: (msg) => !silent ? p.log.success(msg) : void 0,
|
|
21
|
+
warn: (msg) => !silent ? p.log.warn(msg) : void 0,
|
|
22
|
+
error: (msg) => !silent ? p.log.error(msg) : void 0
|
|
23
|
+
});
|
|
24
|
+
const getSpinner = (silent) => {
|
|
25
|
+
const s = p.spinner();
|
|
26
|
+
return {
|
|
27
|
+
start: (msg) => !silent ? s.start(msg) : void 0,
|
|
28
|
+
stop: (msg) => !silent ? s.stop(msg) : void 0,
|
|
29
|
+
message: (msg) => !silent ? s.message(msg) : void 0
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const showNote = (msg, title, silent) => !silent ? p.note(msg, title) : void 0;
|
|
33
|
+
const generateProject = async (opts) => {
|
|
34
|
+
const { template: type, projectName, directory, update, overwrite, skipBuild, silent } = opts;
|
|
35
|
+
const isSilent = !!silent;
|
|
36
|
+
const log = getLog(isSilent);
|
|
37
|
+
const spinner = () => getSpinner(isSilent);
|
|
38
|
+
const projectDir = path.join(directory, projectName);
|
|
39
|
+
debug("Project generation started for: %s", projectName);
|
|
40
|
+
debug("Options: %O", opts);
|
|
41
|
+
debug("Project directory: %s", projectDir);
|
|
42
|
+
let isUpdate = !!update;
|
|
43
|
+
if (await pathExists(projectDir)) {
|
|
44
|
+
if (overwrite) {
|
|
45
|
+
await fs.rm(projectDir, {
|
|
46
|
+
recursive: true,
|
|
47
|
+
force: true
|
|
48
|
+
});
|
|
49
|
+
isUpdate = false;
|
|
50
|
+
} else if (!isUpdate) throw new Error(`Directory "${projectDir}" already exists. Use --overwrite to replace it or --update to update.`);
|
|
51
|
+
}
|
|
52
|
+
const templates = [getBaseTemplate(opts)];
|
|
53
|
+
debug("Applying template: base");
|
|
54
|
+
switch (type) {
|
|
55
|
+
case "cli":
|
|
56
|
+
debug("Applying template: cli");
|
|
57
|
+
templates.push(getCliTemplate(opts));
|
|
58
|
+
break;
|
|
59
|
+
case "webpage":
|
|
60
|
+
debug("Applying template: webpage");
|
|
61
|
+
templates.push(getWebpageTemplate(opts));
|
|
62
|
+
break;
|
|
63
|
+
case "webapp":
|
|
64
|
+
debug("Applying template: webapp");
|
|
65
|
+
templates.push(getWebappTemplate(opts));
|
|
66
|
+
break;
|
|
67
|
+
case "fullstack":
|
|
68
|
+
debug("Applying template: fullstack");
|
|
69
|
+
templates.push(getFullstackTemplate(opts));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
debug("Ensuring directory exists: %s", projectDir);
|
|
73
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
74
|
+
debug("Loading dependency configuration");
|
|
75
|
+
const depConfig = JSON.parse(await fs.readFile(DEPENDENCY_CONFIG_PATH, "utf8"));
|
|
76
|
+
const addedDeps = [];
|
|
77
|
+
const resolveDeps = (deps = {}) => {
|
|
78
|
+
for (const dep of Object.keys(deps)) {
|
|
79
|
+
const config = depConfig.dependencies[dep];
|
|
80
|
+
if (config) {
|
|
81
|
+
deps[dep] = config.version;
|
|
82
|
+
addedDeps.push({
|
|
83
|
+
name: dep,
|
|
84
|
+
description: config.description
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
log.warn(`Dependency "${dep}" not found in master configuration. Using empty version.`);
|
|
88
|
+
debug(`Dependency "${dep}" missing in config`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let finalPkg = {
|
|
93
|
+
name: projectName,
|
|
94
|
+
version: "0.1.0",
|
|
95
|
+
type: "module",
|
|
96
|
+
"create-template-project": { template: type },
|
|
97
|
+
scripts: {},
|
|
98
|
+
dependencies: {},
|
|
99
|
+
devDependencies: {}
|
|
100
|
+
};
|
|
101
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
102
|
+
if (isUpdate && await pathExists(pkgPath)) {
|
|
103
|
+
debug("Loading existing package.json for update");
|
|
104
|
+
const existingPkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
105
|
+
finalPkg = {
|
|
106
|
+
...finalPkg,
|
|
107
|
+
...existingPkg
|
|
108
|
+
};
|
|
109
|
+
finalPkg["create-template-project"] = {
|
|
110
|
+
...existingPkg["create-template-project"],
|
|
111
|
+
template: type
|
|
112
|
+
};
|
|
113
|
+
finalPkg.scripts = { ...existingPkg.scripts };
|
|
114
|
+
finalPkg.dependencies = { ...existingPkg.dependencies };
|
|
115
|
+
finalPkg.devDependencies = { ...existingPkg.devDependencies };
|
|
116
|
+
debug("Loaded existing package.json: %O", finalPkg);
|
|
117
|
+
}
|
|
118
|
+
for (const t of templates) {
|
|
119
|
+
debug("Collecting dependencies and scripts from template: %s", t.name);
|
|
120
|
+
const templateDeps = { ...t.dependencies };
|
|
121
|
+
const templateDevDeps = { ...t.devDependencies };
|
|
122
|
+
resolveDeps(templateDeps);
|
|
123
|
+
resolveDeps(templateDevDeps);
|
|
124
|
+
Object.assign(finalPkg.scripts, t.scripts);
|
|
125
|
+
Object.assign(finalPkg.dependencies, templateDeps);
|
|
126
|
+
Object.assign(finalPkg.devDependencies, templateDevDeps);
|
|
127
|
+
if (t.templateDir) {
|
|
128
|
+
const templatePkgPath = path.join(t.templateDir, "package.json");
|
|
129
|
+
if (await pathExists(templatePkgPath)) {
|
|
130
|
+
const pkgPart = JSON.parse(await fs.readFile(templatePkgPath, "utf8"));
|
|
131
|
+
resolveDeps(pkgPart.dependencies);
|
|
132
|
+
resolveDeps(pkgPart.devDependencies);
|
|
133
|
+
mergePackageJson(finalPkg, pkgPart);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const t of templates) {
|
|
138
|
+
debug("Processing template files for: %s", t.name);
|
|
139
|
+
if (t.templateDir) {
|
|
140
|
+
debug("Reading physical files from: %s", t.templateDir);
|
|
141
|
+
const files = await getAllFiles(t.templateDir);
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
let relativePath = path.relative(t.templateDir, file);
|
|
144
|
+
let targetPath = path.join(projectDir, relativePath);
|
|
145
|
+
if (isUpdate && isSeedFile(relativePath)) {
|
|
146
|
+
debug("Skipping seed file during update: %s", relativePath);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (relativePath === "_oxlint.config.ts") {
|
|
150
|
+
relativePath = "oxlint.config.ts";
|
|
151
|
+
targetPath = path.join(projectDir, relativePath);
|
|
152
|
+
}
|
|
153
|
+
if (relativePath === "package.json") continue;
|
|
154
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
155
|
+
debug("Reading and processing content for: %s", relativePath);
|
|
156
|
+
let content = await fs.readFile(file, "utf8");
|
|
157
|
+
content = processContent(relativePath, content, opts, addedDeps);
|
|
158
|
+
let finalTargetPath = targetPath;
|
|
159
|
+
if (type === "webpage" && skipBuild && relativePath === "src/index.ts") {
|
|
160
|
+
debug("Changing target path for webpage index.ts to .js due to skipBuild");
|
|
161
|
+
finalTargetPath = path.join(projectDir, "src/index.js");
|
|
162
|
+
}
|
|
163
|
+
if (isUpdate && await pathExists(finalTargetPath)) {
|
|
164
|
+
debug("File exists, attempting to update/merge: %s", finalTargetPath);
|
|
165
|
+
const existingContent = await fs.readFile(finalTargetPath, "utf8");
|
|
166
|
+
if (existingContent.trim() !== content.trim()) {
|
|
167
|
+
const result = await mergeFile(finalTargetPath, existingContent, content, log);
|
|
168
|
+
if (result === "merged") log.info(`ℹ Merged: ${relativePath}`);
|
|
169
|
+
else if (result === "conflict") log.warn(`⚠ Conflict: ${relativePath}`);
|
|
170
|
+
else if (result === "updated") log.info(`✔ Updated: ${relativePath}`);
|
|
171
|
+
} else debug("Content identical, skipping: %s", finalTargetPath);
|
|
172
|
+
} else {
|
|
173
|
+
debug("Writing file: %s", finalTargetPath);
|
|
174
|
+
await fs.writeFile(finalTargetPath, content);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const file of t.files) {
|
|
179
|
+
const targetPath = path.join(projectDir, file.path);
|
|
180
|
+
if (isUpdate && isSeedFile(file.path)) {
|
|
181
|
+
debug("Skipping programmatic seed file: %s", file.path);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
debug("Processing programmatic file: %s", file.path);
|
|
185
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
186
|
+
let content = typeof file.content === "function" ? file.content() : file.content;
|
|
187
|
+
content = processContent(file.path, content, opts, addedDeps);
|
|
188
|
+
if (isUpdate && await pathExists(targetPath)) {
|
|
189
|
+
debug("File exists, attempting to update/merge programmatic file: %s", targetPath);
|
|
190
|
+
const existingContent = await fs.readFile(targetPath, "utf8");
|
|
191
|
+
if (existingContent.trim() !== content.trim()) {
|
|
192
|
+
const result = await mergeFile(targetPath, existingContent, content, log);
|
|
193
|
+
if (result === "merged") log.info(`ℹ Merged: ${file.path}`);
|
|
194
|
+
else if (result === "conflict") log.warn(`⚠ Conflict: ${file.path}`);
|
|
195
|
+
else if (result === "updated") log.info(`✔ Updated: ${file.path}`);
|
|
196
|
+
} else debug("Content identical, skipping programmatic: %s", targetPath);
|
|
197
|
+
} else {
|
|
198
|
+
debug("Writing programmatic file: %s", targetPath);
|
|
199
|
+
await fs.writeFile(targetPath, content);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const pm = opts.packageManager || "npm";
|
|
204
|
+
if (pm !== "npm") {
|
|
205
|
+
for (const [key, value] of Object.entries(finalPkg.scripts)) if (typeof value === "string") finalPkg.scripts[key] = value.replaceAll("npm run ", `${pm} run `);
|
|
206
|
+
}
|
|
207
|
+
if (pm === "pnpm" && finalPkg.workspaces) {
|
|
208
|
+
debug("Creating pnpm-workspace.yaml");
|
|
209
|
+
const workspaceYaml = `packages:\n${finalPkg.workspaces.map((w) => ` - '${w}'`).join("\n")}\n`;
|
|
210
|
+
await fs.writeFile(path.join(projectDir, "pnpm-workspace.yaml"), workspaceYaml);
|
|
211
|
+
delete finalPkg.workspaces;
|
|
212
|
+
for (const [key, value] of Object.entries(finalPkg.scripts)) if (typeof value === "string" && value.includes("--workspaces")) finalPkg.scripts[key] = value.replace("run ", "-r run ").replace(" --workspaces", "");
|
|
213
|
+
}
|
|
214
|
+
if (skipBuild) {
|
|
215
|
+
debug("Applying skipBuild overrides");
|
|
216
|
+
delete finalPkg.scripts.build;
|
|
217
|
+
delete finalPkg.scripts.dev;
|
|
218
|
+
if (finalPkg.devDependencies) delete finalPkg.devDependencies.tsdown;
|
|
219
|
+
if (finalPkg.scripts.ci) finalPkg.scripts.ci = finalPkg.scripts.ci.replace(" && npm run build", "").replace(` && ${pm} run build`, "");
|
|
220
|
+
debug("Removing tsdown configs due to skipBuild");
|
|
221
|
+
await fs.rm(path.join(projectDir, "tsdown.config.ts"), { force: true });
|
|
222
|
+
await fs.rm(path.join(projectDir, "client/tsdown.config.ts"), { force: true });
|
|
223
|
+
await fs.rm(path.join(projectDir, "server/tsdown.config.ts"), { force: true });
|
|
224
|
+
}
|
|
225
|
+
debug("Writing final consolidated package.json to: %s", pkgPath);
|
|
226
|
+
await fs.writeFile(pkgPath, JSON.stringify(finalPkg, null, " "));
|
|
227
|
+
if (!await pathExists(path.join(projectDir, ".git"))) {
|
|
228
|
+
debug("Initializing Git repository");
|
|
229
|
+
try {
|
|
230
|
+
await execa("git", ["init"], {
|
|
231
|
+
cwd: projectDir,
|
|
232
|
+
preferLocal: true
|
|
233
|
+
});
|
|
234
|
+
log.success("Initialized Git repository (git init).");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
debug("Failed to initialize Git: %O", e);
|
|
237
|
+
log.error("Failed to initialize Git: " + e);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (opts.createGithubRepository && !isUpdate) {
|
|
241
|
+
debug("Creating GitHub repository");
|
|
242
|
+
try {
|
|
243
|
+
await execa("gh", [
|
|
244
|
+
"repo",
|
|
245
|
+
"create",
|
|
246
|
+
projectName,
|
|
247
|
+
"--public",
|
|
248
|
+
"--source=.",
|
|
249
|
+
"--remote=origin"
|
|
250
|
+
], {
|
|
251
|
+
cwd: projectDir,
|
|
252
|
+
preferLocal: true
|
|
253
|
+
});
|
|
254
|
+
log.success("Created GitHub repository (gh repo create).");
|
|
255
|
+
} catch (e) {
|
|
256
|
+
debug("Failed to create GitHub repository: %O", e);
|
|
257
|
+
log.warn("Failed to create GitHub repository. Ensure \"gh\" CLI is installed and authenticated.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (opts.installDependencies) {
|
|
261
|
+
debug("Installing dependencies using %s", pm);
|
|
262
|
+
const s = spinner();
|
|
263
|
+
s.start(`Installing dependencies using ${pm}...`);
|
|
264
|
+
try {
|
|
265
|
+
await execa(pm, ["install"], {
|
|
266
|
+
cwd: projectDir,
|
|
267
|
+
preferLocal: true
|
|
268
|
+
});
|
|
269
|
+
s.stop(`\x1b[1G\x1b[2K\x1b[32m◆\x1b[39m Dependencies installed (${pm} install).`);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
debug("Failed to install dependencies: %O", e);
|
|
272
|
+
s.stop("Failed to install dependencies.");
|
|
273
|
+
log.error(String(e));
|
|
274
|
+
throw new Error("Failed to install dependencies.");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (opts.build && finalPkg.scripts.ci) {
|
|
278
|
+
debug("Running CI script");
|
|
279
|
+
const s = spinner();
|
|
280
|
+
if (finalPkg.scripts["prettier-write"]) {
|
|
281
|
+
s.start(`Formatting files with Prettier (${pm} run prettier-write)...`);
|
|
282
|
+
try {
|
|
283
|
+
await execa(pm, ["run", "prettier-write"], {
|
|
284
|
+
cwd: projectDir,
|
|
285
|
+
preferLocal: true
|
|
286
|
+
});
|
|
287
|
+
s.stop(`\x1b[1G\x1b[2K\x1b[32m◆\x1b[39m Files formatted (${pm} run prettier-write).`);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
debug("Failed to format files: %O", e);
|
|
290
|
+
s.stop("Failed to format files.");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
s.start(`Running CI script (lint, build, test) (${pm} run ci)...`);
|
|
294
|
+
try {
|
|
295
|
+
await execa(pm, ["run", "ci"], {
|
|
296
|
+
cwd: projectDir,
|
|
297
|
+
preferLocal: true
|
|
298
|
+
});
|
|
299
|
+
s.stop(`\x1b[1G\x1b[2K\x1b[32m◆\x1b[39m CI script completed (${pm} run ci).`);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
debug("Failed to run CI script: %O", e);
|
|
302
|
+
s.stop("Failed to run CI script.");
|
|
303
|
+
log.error(String(e));
|
|
304
|
+
throw new Error("Failed to run CI script.");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
log.info(`Project "${projectName}" ${isUpdate ? "updated" : "scaffolded"} successfully in ${projectDir}`);
|
|
308
|
+
showSummary(opts, pm, isSilent);
|
|
309
|
+
if (opts.dev && finalPkg.scripts.dev) {
|
|
310
|
+
log.info("Starting dev server...");
|
|
311
|
+
if (opts.open) try {
|
|
312
|
+
await execa(pm, [
|
|
313
|
+
"run",
|
|
314
|
+
"dev",
|
|
315
|
+
"--",
|
|
316
|
+
"--open"
|
|
317
|
+
], {
|
|
318
|
+
cwd: projectDir,
|
|
319
|
+
stdio: "inherit",
|
|
320
|
+
preferLocal: true
|
|
321
|
+
});
|
|
322
|
+
} catch (e) {
|
|
323
|
+
log.error("Dev server failed: " + e);
|
|
324
|
+
}
|
|
325
|
+
else try {
|
|
326
|
+
await execa(pm, ["run", "dev"], {
|
|
327
|
+
cwd: projectDir,
|
|
328
|
+
stdio: "inherit",
|
|
329
|
+
preferLocal: true
|
|
330
|
+
});
|
|
331
|
+
} catch (e) {
|
|
332
|
+
log.error("Dev server failed: " + e);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
function showSummary(opts, pm, isSilent) {
|
|
337
|
+
debug("Showing summary for options: %O", opts);
|
|
338
|
+
const { projectName, template } = opts;
|
|
339
|
+
const summary = [
|
|
340
|
+
`Successfully created a new ${template} project named '${projectName}'.`,
|
|
341
|
+
"",
|
|
342
|
+
"Available Commands:"
|
|
343
|
+
];
|
|
344
|
+
const commands = [
|
|
345
|
+
`${pm} run dev - Starts the development server`,
|
|
346
|
+
`${pm} run build - Builds the project for production`,
|
|
347
|
+
`${pm} run test - Runs the unit test suite (Vitest)`,
|
|
348
|
+
`${pm} run lint - Lints and formats the codebase`,
|
|
349
|
+
`${pm} run ci - Runs lint, build, and test (used by CI/CD)`
|
|
350
|
+
];
|
|
351
|
+
showNote([...summary, ...commands.map((c) => ` ${c}`)].join("\n"), "Project ready", isSilent);
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
export { generateProject };
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "./cli.mjs";
|
|
3
|
+
import { generateProject } from "./generators/project.mjs";
|
|
4
|
+
import { cancel, intro, outro } from "@clack/prompts";
|
|
5
|
+
import debugLib from "debug";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
if (process.argv.includes("--debug")) debugLib.enable("*");
|
|
8
|
+
const debug = debugLib("create-template-project:main");
|
|
9
|
+
const main = async () => {
|
|
10
|
+
try {
|
|
11
|
+
debug("Starting CLI execution");
|
|
12
|
+
debug("Parsing arguments");
|
|
13
|
+
const options = await parseArgs();
|
|
14
|
+
if (!options) return;
|
|
15
|
+
const isSilent = !!options.silent;
|
|
16
|
+
if (!isSilent) intro("create-template-project");
|
|
17
|
+
debug("Arguments parsed: %O", options);
|
|
18
|
+
debug("Generating project");
|
|
19
|
+
await generateProject(options);
|
|
20
|
+
debug("Project generation complete");
|
|
21
|
+
if (!isSilent) outro("Done!");
|
|
22
|
+
} catch (error) {
|
|
23
|
+
debug("Execution failed: %O", error);
|
|
24
|
+
if (error.code === "PROCESS_EXIT_0" || error.code === "commander.helpDisplayed" || error.code === "commander.version") process.exit(0);
|
|
25
|
+
if (error.code === "PROCESS_EXIT_1") process.exit(1);
|
|
26
|
+
cancel(error?.message || String(error));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (import.meta.url.endsWith("src/index.ts") || import.meta.url.endsWith("dist/index.mjs")) await main();
|
|
31
|
+
//#endregion
|
|
32
|
+
export { main };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Node.js CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Use Node.js
|
|
15
|
+
uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: '22.x'
|
|
18
|
+
cache: 'npm'
|
|
19
|
+
- run: npm ci
|
|
20
|
+
- run: npm run ci
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
## Commit Message Guidelines
|
|
2
|
+
|
|
3
|
+
We follow the **Conventional Commits** specification. This is **enforced** by `commitlint` and is required for automated changelog generation.
|
|
4
|
+
|
|
5
|
+
**Format:** `type(scope): subject`
|
|
6
|
+
|
|
7
|
+
**Common Types:**
|
|
8
|
+
- `feat`: A new feature
|
|
9
|
+
- `fix`: A bug fix
|
|
10
|
+
- `docs`: Documentation only changes
|
|
11
|
+
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
|
12
|
+
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
|
13
|
+
- `perf`: A code change that improves performance
|
|
14
|
+
- `test`: Adding missing tests or correcting existing tests
|
|
15
|
+
- `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation
|
|
16
|
+
|
|
17
|
+
**Examples:**
|
|
18
|
+
- `feat(cli): add support for jsonc files`
|
|
19
|
+
- `fix(parser): handle empty input gracefully`
|
|
20
|
+
- `docs: update contributing guidelines`
|
|
21
|
+
|
|
22
|
+
## Release Process
|
|
23
|
+
|
|
24
|
+
1. **Verify**: `npm run ci`
|
|
25
|
+
2. **Bump Version**: `npm version <patch|minor|major> --no-git-tag-version`
|
|
26
|
+
3. **Update Changelog**: `npm run create-changelog`
|
|
27
|
+
4. **Commit**: `git add . && git commit -m "chore(release): $(node -p 'require("./package.json").version')"`
|
|
28
|
+
5. **Tag & Push**: `git tag v$(node -p 'require("./package.json").version') && git push && git push --tags`
|
|
29
|
+
6. **Create GitHub Release**: `gh release create v$(node -p 'require("./package.json").version') --generate-notes`
|
|
30
|
+
7. **Publish**: `npm publish`
|
|
31
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
{{description}}
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/{{projectName}})
|
|
6
|
+
[](https://www.npmjs.com/package/{{projectName}})
|
|
7
|
+
[](https://github.com/doberkofler/{{projectName}}/actions/workflows/node.js.yml)
|
|
8
|
+
|
|
9
|
+
## Generated with create-template-project.
|
|
10
|
+
|
|
11
|
+
## Templates
|
|
12
|
+
- **cli**: Node.js CLI application.
|
|
13
|
+
- **webpage**: Standalone web page.
|
|
14
|
+
- **webapp**: Web application with Express.
|
|
15
|
+
- **fullstack**: Monorepo with Express and React.
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
npm run dev # if applicable
|
|
21
|
+
npm run build
|
|
22
|
+
npm run test
|
|
23
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
import pluginRegexp from 'eslint-plugin-regexp';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
options: {
|
|
6
|
+
typeAware: true,
|
|
7
|
+
typeCheck: true
|
|
8
|
+
},
|
|
9
|
+
plugins: [
|
|
10
|
+
'unicorn',
|
|
11
|
+
'typescript',
|
|
12
|
+
'oxc',
|
|
13
|
+
'import',
|
|
14
|
+
'react',
|
|
15
|
+
'jsdoc',
|
|
16
|
+
'promise',
|
|
17
|
+
'vitest'
|
|
18
|
+
],
|
|
19
|
+
jsPlugins: [
|
|
20
|
+
'eslint-plugin-regexp'
|
|
21
|
+
],
|
|
22
|
+
categories: {
|
|
23
|
+
correctness: 'error'
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
...pluginRegexp.configs.recommended.rules,
|
|
27
|
+
'typescript/no-unused-vars': [
|
|
28
|
+
'error',
|
|
29
|
+
{
|
|
30
|
+
caughtErrors: 'none',
|
|
31
|
+
argsIgnorePattern: '^_'
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
settings: {
|
|
36
|
+
'jsx-a11y': {
|
|
37
|
+
polymorphicPropName: undefined,
|
|
38
|
+
components: {},
|
|
39
|
+
attributes: {}
|
|
40
|
+
},
|
|
41
|
+
next: {
|
|
42
|
+
rootDir: []
|
|
43
|
+
},
|
|
44
|
+
react: {
|
|
45
|
+
formComponents: [],
|
|
46
|
+
linkComponents: [],
|
|
47
|
+
version: undefined
|
|
48
|
+
},
|
|
49
|
+
jsdoc: {
|
|
50
|
+
ignorePrivate: false,
|
|
51
|
+
ignoreInternal: false,
|
|
52
|
+
ignoreReplacesDocs: true,
|
|
53
|
+
overrideReplacesDocs: true,
|
|
54
|
+
augmentsExtendsReplacesDocs: false,
|
|
55
|
+
implementsReplacesDocs: false,
|
|
56
|
+
exemptDestructuredRootsFromChecks: false,
|
|
57
|
+
tagNamePreference: {}
|
|
58
|
+
},
|
|
59
|
+
vitest: {
|
|
60
|
+
typecheck: false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
env: {
|
|
64
|
+
builtin: true
|
|
65
|
+
},
|
|
66
|
+
globals: {},
|
|
67
|
+
ignorePatterns: [
|
|
68
|
+
'**/.*',
|
|
69
|
+
'node_modules/**',
|
|
70
|
+
'dist/**',
|
|
71
|
+
'coverage/**',
|
|
72
|
+
'public/**'
|
|
73
|
+
]
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {extends: ['@commitlint/config-conventional']};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint": "tsc && oxlint -c oxlint.config.ts && npm run prettier",
|
|
7
|
+
"prettier": "prettier --check .",
|
|
8
|
+
"prettier-write": "prettier --write .",
|
|
9
|
+
"test": "vitest run --coverage",
|
|
10
|
+
"ci": "npm run lint && npm run build && npm run test",
|
|
11
|
+
"prepare": "husky"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"debug": ""
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@commitlint/cli": "",
|
|
18
|
+
"@commitlint/config-conventional": "",
|
|
19
|
+
"@types/debug": "",
|
|
20
|
+
"@types/node": "",
|
|
21
|
+
"@vitest/coverage-v8": "",
|
|
22
|
+
"conventional-changelog": "",
|
|
23
|
+
"husky": "",
|
|
24
|
+
"oxlint": "",
|
|
25
|
+
"oxlint-tsgolint": "",
|
|
26
|
+
"eslint-plugin-regexp": "",
|
|
27
|
+
"tinyexec": "",
|
|
28
|
+
"prettier": "",
|
|
29
|
+
"typescript": "",
|
|
30
|
+
"vitest": ""
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Language and Environment */
|
|
4
|
+
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
|
5
|
+
"lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
|
6
|
+
"module": "NodeNext", /* Specify what module code is generated. */
|
|
7
|
+
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
8
|
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */
|
|
9
|
+
"resolveJsonModule": true, /* Enable importing .json files. */
|
|
10
|
+
|
|
11
|
+
/* Strict Type-Checking Options */
|
|
12
|
+
"strict": true, /* Enable all strict type-checking options. */
|
|
13
|
+
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
|
14
|
+
"strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
|
15
|
+
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
|
16
|
+
"strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
|
17
|
+
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
|
18
|
+
"noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
|
19
|
+
"useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
|
20
|
+
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
|
21
|
+
"noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
|
22
|
+
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
|
23
|
+
"exactOptionalPropertyTypes": true, /* Interpret optional property types as strictly typed, preventing assignment of 'undefined'. */
|
|
24
|
+
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
|
25
|
+
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
|
26
|
+
"noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
|
27
|
+
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
|
28
|
+
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
|
29
|
+
"allowUnusedLabels": false, /* Disable error reporting for unused labels. */
|
|
30
|
+
"allowUnreachableCode": false, /* Disable error reporting for unreachable code. */
|
|
31
|
+
|
|
32
|
+
/* Emit */
|
|
33
|
+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
|
34
|
+
|
|
35
|
+
/* Completeness */
|
|
36
|
+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
37
|
+
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */
|
|
38
|
+
},
|
|
39
|
+
"include": [
|
|
40
|
+
"src/**/*"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
//#region src/templates/base/index.ts
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const getBaseTemplate = (_opts) => {
|
|
6
|
+
return {
|
|
7
|
+
name: "base",
|
|
8
|
+
dependencies: {},
|
|
9
|
+
devDependencies: { "eslint-plugin-regexp": "" },
|
|
10
|
+
scripts: {},
|
|
11
|
+
files: [],
|
|
12
|
+
templateDir: path.resolve(__dirname, "files")
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { getBaseTemplate };
|