@book000/create-ts 0.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/dist/index.d.mts +1 -0
- package/dist/index.mjs +543 -0
- package/package.json +50 -0
- package/templates/gitignore/Node.gitignore +147 -0
- package/templates/nodejs/base/package.json +54 -0
- package/templates/nodejs/base/pnpm-lock.yaml +5639 -0
- package/templates/nodejs/base/pnpm-workspace.yaml +5 -0
- package/templates/nodejs/base/src/main.ts +13 -0
- package/templates/nodejs/base/template.json +8 -0
- package/templates/nodejs/base/test/smoke.test.ts +32 -0
- package/templates/nodejs/base/tsconfig.test.json +7 -0
- package/templates/nodejs/common/.depcheckrc.json +3 -0
- package/templates/nodejs/common/.devcontainer/devcontainer.json +9 -0
- package/templates/nodejs/common/.fixpackrc +4 -0
- package/templates/nodejs/common/.prettierrc.yml +7 -0
- package/templates/nodejs/common/Dockerfile +31 -0
- package/templates/nodejs/common/entrypoint.sh +4 -0
- package/templates/nodejs/common/eslint.config.mjs +1 -0
- package/templates/nodejs/common/package.json +29 -0
- package/templates/nodejs/common/pnpm-workspace.yaml +5 -0
- package/templates/nodejs/common/renovate.json +4 -0
- package/templates/nodejs/common/tsconfig.json +22 -0
- package/templates/nodejs/config-batch/package.json +59 -0
- package/templates/nodejs/config-batch/pnpm-lock.yaml +6128 -0
- package/templates/nodejs/config-batch/pnpm-workspace.yaml +5 -0
- package/templates/nodejs/config-batch/src/config.ts +7 -0
- package/templates/nodejs/config-batch/src/main.ts +38 -0
- package/templates/nodejs/config-batch/template.json +11 -0
- package/templates/nodejs/config-batch/test/config.test.ts +76 -0
- package/templates/nodejs/config-batch/tsconfig.test.json +7 -0
- package/templates/nodejs/discord-bot/package.json +60 -0
- package/templates/nodejs/discord-bot/pnpm-lock.yaml +6315 -0
- package/templates/nodejs/discord-bot/pnpm-workspace.yaml +5 -0
- package/templates/nodejs/discord-bot/src/config.ts +8 -0
- package/templates/nodejs/discord-bot/src/discord.ts +35 -0
- package/templates/nodejs/discord-bot/src/main.ts +53 -0
- package/templates/nodejs/discord-bot/template.json +11 -0
- package/templates/nodejs/discord-bot/test/discord.test.ts +68 -0
- package/templates/nodejs/discord-bot/tsconfig.test.json +7 -0
- package/templates/nodejs/fastify/package.json +59 -0
- package/templates/nodejs/fastify/pnpm-lock.yaml +6077 -0
- package/templates/nodejs/fastify/pnpm-workspace.yaml +5 -0
- package/templates/nodejs/fastify/src/main.ts +34 -0
- package/templates/nodejs/fastify/template.json +9 -0
- package/templates/nodejs/fastify/test/app.test.ts +49 -0
- package/templates/nodejs/fastify/tsconfig.test.json +7 -0
- package/templates/workflows/add-reviewer.yml +15 -0
- package/templates/workflows/docker.yml +92 -0
- package/templates/workflows/nodejs-ci-pnpm.yml +79 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cancel, confirm, group, intro, isCancel, log, note, outro, select, spinner, text } from "@clack/prompts";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execa } from "execa";
|
|
8
|
+
//#region src/template.ts
|
|
9
|
+
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/** バンドルされたテンプレートのルートディレクトリ */
|
|
11
|
+
const TEMPLATES_DIR = path.join(__dirname$1, "../templates");
|
|
12
|
+
/**
|
|
13
|
+
* バンドルされたテンプレートファイルを文字列として読み込む。
|
|
14
|
+
* @param relativePath - テンプレートルートからの相対パス
|
|
15
|
+
*/
|
|
16
|
+
function readTemplate(relativePath) {
|
|
17
|
+
return readFileSync(path.join(TEMPLATES_DIR, relativePath), "utf8");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* バリアントの template.json を読み込んでパースする。
|
|
21
|
+
* @param variant - バリアント名
|
|
22
|
+
*/
|
|
23
|
+
function fetchTemplateConfig(variant) {
|
|
24
|
+
const content = readTemplate(`nodejs/${variant}/template.json`);
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/generate.ts
|
|
29
|
+
/**
|
|
30
|
+
* バリアントの package.json にプロジェクト固有の値を適用する。
|
|
31
|
+
* テンプレートのプレースホルダーを実際の値で上書きする。
|
|
32
|
+
*/
|
|
33
|
+
function patchPackageJson(packageJson, options, nodeMajor) {
|
|
34
|
+
const { name, org, repo, description, author, license, homepage, bugUrl, esm, test } = options;
|
|
35
|
+
const result = structuredClone(packageJson);
|
|
36
|
+
result.name = `@${org.toLowerCase()}/${name}`;
|
|
37
|
+
result.description = description;
|
|
38
|
+
result.license = license;
|
|
39
|
+
result.author = author;
|
|
40
|
+
result.engines.node = `>=${nodeMajor}`;
|
|
41
|
+
result.repository.url = `git+https://github.com/${org}/${repo}.git`;
|
|
42
|
+
result.bugs.url = bugUrl;
|
|
43
|
+
if (homepage) result.homepage = homepage;
|
|
44
|
+
if (esm) {
|
|
45
|
+
result.type = "module";
|
|
46
|
+
if (test) result.jest = {
|
|
47
|
+
preset: "ts-jest/presets/default-esm",
|
|
48
|
+
extensionsToTreatAsEsm: [".ts"],
|
|
49
|
+
transform: { "^.+\\.tsx?$": ["ts-jest", { useESM: true }] }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!test) {
|
|
53
|
+
delete result.scripts.test;
|
|
54
|
+
delete result.jest;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* tsconfig.json にモジュール形式・テスト設定のパッチを適用する。
|
|
60
|
+
*/
|
|
61
|
+
function patchTsConfig(tsconfig, options) {
|
|
62
|
+
const result = structuredClone(tsconfig);
|
|
63
|
+
const compilerOptions = result.compilerOptions;
|
|
64
|
+
if (options.esm) compilerOptions.module = "es2015";
|
|
65
|
+
if (options.test) {
|
|
66
|
+
compilerOptions.types ??= ["node"];
|
|
67
|
+
if (!compilerOptions.types.includes("jest")) compilerOptions.types.push("jest");
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* docker.yml のプレースホルダーをプロジェクト固有の値に置換する。
|
|
73
|
+
*/
|
|
74
|
+
function patchDockerWorkflow(content, org, repo) {
|
|
75
|
+
let result = content;
|
|
76
|
+
result = result.replaceAll("tomacheese/twitter-dm-memo", `${org.toLowerCase()}/${repo.toLowerCase()}`);
|
|
77
|
+
result = result.replaceAll("packageName: \"twitter-dm-memo\"", `packageName: "${repo}"`);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* .gitignore を生成する。
|
|
82
|
+
* バンドルされた Node.gitignore(pnpm セクション付き)をベースに、
|
|
83
|
+
* 任意で data/ セクションを追記する。
|
|
84
|
+
*/
|
|
85
|
+
function generateGitignore(ignoreData) {
|
|
86
|
+
let result = readTemplate("gitignore/Node.gitignore");
|
|
87
|
+
if (ignoreData) result += "\n\n# データディレクトリ\ndata/";
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* .depcheckrc.json の ignores 配列を更新する。
|
|
92
|
+
* テンプレートの depcheckIgnore と Jest 関連パッケージを追加する(重複除去)。
|
|
93
|
+
*/
|
|
94
|
+
function updateDepcheck(depcheckJson, depcheckIgnore, test) {
|
|
95
|
+
const result = structuredClone(depcheckJson);
|
|
96
|
+
const ignores = result.ignores ?? [];
|
|
97
|
+
result.ignores = ignores;
|
|
98
|
+
for (const pkg of depcheckIgnore) if (!ignores.includes(pkg)) ignores.push(pkg);
|
|
99
|
+
if (test && !ignores.includes("@types/jest")) ignores.push("@types/jest");
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/validate.ts
|
|
104
|
+
/**
|
|
105
|
+
* プロジェクト名を検証する。
|
|
106
|
+
* npm パッケージ名規則: 小文字英数字・ハイフン・アンダースコア・ドット、214 文字以下。
|
|
107
|
+
*/
|
|
108
|
+
function validateProjectName(value) {
|
|
109
|
+
if (!value) return "プロジェクト名は必須です";
|
|
110
|
+
if (value.length > 214 || !/^[a-z0-9][a-z0-9\-_.]*$/.test(value)) return "プロジェクト名は小文字英数字・ハイフン・アンダースコア・ドットのみ使用できます(最大 214 文字)";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* GitHub 組織 / ユーザー名を検証する。
|
|
114
|
+
* 英数字・ハイフン・ドットのみ許容。
|
|
115
|
+
*/
|
|
116
|
+
function validateOrgName(value) {
|
|
117
|
+
if (!value) return "組織 / ユーザー名は必須です";
|
|
118
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9\-.]*$/.test(value)) return "組織 / ユーザー名は英数字・ハイフン・ドットのみ使用できます";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* リポジトリ名を検証する。
|
|
122
|
+
* 英数字・ハイフン・アンダースコア・ドットのみ許容。先頭はアンダースコア可。
|
|
123
|
+
*/
|
|
124
|
+
function validateRepoName(value) {
|
|
125
|
+
if (!value) return "リポジトリ名は必須です";
|
|
126
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9\-_.]*$/.test(value)) return "リポジトリ名は英数字・ハイフン・アンダースコア・ドットのみ使用できます";
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* SPDX ライセンス識別子を検証する。
|
|
130
|
+
* 英数字・ドット・ハイフンのみ許容。
|
|
131
|
+
*/
|
|
132
|
+
function validateLicense(value) {
|
|
133
|
+
if (!value) return "ライセンス識別子は必須です";
|
|
134
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(value)) return "ライセンス識別子は英数字・ドット・ハイフンのみ使用できます(例: MIT, Apache-2.0)";
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/prompts.ts
|
|
138
|
+
/** 有効なバリアント値の一覧 */
|
|
139
|
+
const VALID_VARIANTS = [
|
|
140
|
+
"base",
|
|
141
|
+
"config-batch",
|
|
142
|
+
"fastify",
|
|
143
|
+
"discord-bot"
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* CLI フラグで渡された値をバリデーションし、不正な場合はプロセスを終了する。
|
|
147
|
+
*/
|
|
148
|
+
function validateCliFlags(flags) {
|
|
149
|
+
if (flags.name !== void 0) {
|
|
150
|
+
const err = validateProjectName(flags.name);
|
|
151
|
+
if (err) {
|
|
152
|
+
log.error(`Invalid --name: ${err}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (flags.org !== void 0) {
|
|
157
|
+
const err = validateOrgName(flags.org);
|
|
158
|
+
if (err) {
|
|
159
|
+
log.error(`Invalid --org: ${err}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (flags.repo !== void 0) {
|
|
164
|
+
const err = validateRepoName(flags.repo);
|
|
165
|
+
if (err) {
|
|
166
|
+
log.error(`Invalid --repo: ${err}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (flags.license !== void 0) {
|
|
171
|
+
const err = validateLicense(flags.license);
|
|
172
|
+
if (err) {
|
|
173
|
+
log.error(`Invalid --license: ${err}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (flags.variant !== void 0 && !VALID_VARIANTS.includes(flags.variant)) {
|
|
178
|
+
log.error(`Invalid --variant: "${flags.variant}". Must be one of: ${VALID_VARIANTS.join(", ")}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 対話プロンプトで ProjectOptions を収集する。
|
|
184
|
+
* CLI フラグで指定済みの項目はスキップする。
|
|
185
|
+
*/
|
|
186
|
+
async function collectOptions(outDir, flags) {
|
|
187
|
+
validateCliFlags(flags);
|
|
188
|
+
const resolvedDir = path.resolve(outDir);
|
|
189
|
+
const dirBasename = path.basename(resolvedDir);
|
|
190
|
+
const nameDefault = dirBasename !== "." && /^[a-z0-9][a-z0-9\-_.]*$/.test(dirBasename) ? dirBasename : void 0;
|
|
191
|
+
const answers = await group({
|
|
192
|
+
name: () => flags.name === void 0 ? text({
|
|
193
|
+
message: "プロジェクト名",
|
|
194
|
+
placeholder: "my-app",
|
|
195
|
+
initialValue: nameDefault,
|
|
196
|
+
validate: validateProjectName
|
|
197
|
+
}) : Promise.resolve(flags.name),
|
|
198
|
+
org: () => flags.org === void 0 ? text({
|
|
199
|
+
message: "GitHub 組織 / ユーザー名",
|
|
200
|
+
initialValue: "book000",
|
|
201
|
+
validate: validateOrgName
|
|
202
|
+
}) : Promise.resolve(flags.org),
|
|
203
|
+
repo: ({ results }) => flags.repo === void 0 ? text({
|
|
204
|
+
message: "リポジトリ名",
|
|
205
|
+
initialValue: results.name ?? nameDefault ?? "my-app",
|
|
206
|
+
validate: validateRepoName
|
|
207
|
+
}) : Promise.resolve(flags.repo),
|
|
208
|
+
description: () => flags.description === void 0 ? text({
|
|
209
|
+
message: "プロジェクトの説明",
|
|
210
|
+
placeholder: ""
|
|
211
|
+
}) : Promise.resolve(flags.description),
|
|
212
|
+
author: ({ results }) => flags.author === void 0 ? text({
|
|
213
|
+
message: "作者名",
|
|
214
|
+
initialValue: results.org ?? "book000"
|
|
215
|
+
}) : Promise.resolve(flags.author),
|
|
216
|
+
license: () => flags.license === void 0 ? text({
|
|
217
|
+
message: "ライセンス (SPDX 識別子)",
|
|
218
|
+
initialValue: "MIT",
|
|
219
|
+
validate: validateLicense
|
|
220
|
+
}) : Promise.resolve(flags.license),
|
|
221
|
+
homepage: () => flags.homepage === void 0 ? text({
|
|
222
|
+
message: "ホームページ URL(空白でスキップ)",
|
|
223
|
+
placeholder: ""
|
|
224
|
+
}) : Promise.resolve(flags.homepage),
|
|
225
|
+
bugUrl: ({ results }) => flags.bugUrl === void 0 ? text({
|
|
226
|
+
message: "バグ報告 URL",
|
|
227
|
+
initialValue: `https://github.com/${results.org ?? "book000"}/${results.repo ?? "my-app"}/issues`
|
|
228
|
+
}) : Promise.resolve(flags.bugUrl),
|
|
229
|
+
variant: () => flags.variant === void 0 ? select({
|
|
230
|
+
message: "バリアント",
|
|
231
|
+
options: [
|
|
232
|
+
{
|
|
233
|
+
value: "base",
|
|
234
|
+
label: "base",
|
|
235
|
+
hint: "最小構成(TypeScript + lint)"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
value: "config-batch",
|
|
239
|
+
label: "config-batch",
|
|
240
|
+
hint: "設定ファイルありのバッチ処理"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
value: "fastify",
|
|
244
|
+
label: "fastify",
|
|
245
|
+
hint: "Fastify HTTP サーバー"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
value: "discord-bot",
|
|
249
|
+
label: "discord-bot",
|
|
250
|
+
hint: "Discord Bot"
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
initialValue: "base"
|
|
254
|
+
}) : Promise.resolve(flags.variant),
|
|
255
|
+
esm: () => flags.esm === void 0 ? select({
|
|
256
|
+
message: "モジュール形式",
|
|
257
|
+
options: [{
|
|
258
|
+
value: "cjs",
|
|
259
|
+
label: "CommonJS",
|
|
260
|
+
hint: "既定・現行の標準(--no-esm)"
|
|
261
|
+
}, {
|
|
262
|
+
value: "esm",
|
|
263
|
+
label: "ESM",
|
|
264
|
+
hint: "ES Modules(--esm)"
|
|
265
|
+
}],
|
|
266
|
+
initialValue: "cjs"
|
|
267
|
+
}) : Promise.resolve(flags.esm ? "esm" : "cjs"),
|
|
268
|
+
test: () => flags.test === void 0 ? confirm({
|
|
269
|
+
message: "Jest テストを追加しますか?",
|
|
270
|
+
initialValue: false
|
|
271
|
+
}) : Promise.resolve(flags.test),
|
|
272
|
+
docker: () => flags.docker === void 0 ? confirm({
|
|
273
|
+
message: "Dockerfile を追加しますか?",
|
|
274
|
+
initialValue: false
|
|
275
|
+
}) : Promise.resolve(flags.docker),
|
|
276
|
+
ignoreData: () => flags.ignoreData === void 0 ? confirm({
|
|
277
|
+
message: "`data/` を .gitignore に追加しますか?",
|
|
278
|
+
initialValue: false
|
|
279
|
+
}) : Promise.resolve(flags.ignoreData),
|
|
280
|
+
addReviewer: () => flags.addReviewer === void 0 ? confirm({
|
|
281
|
+
message: "add-reviewer ワークフローを追加しますか?",
|
|
282
|
+
initialValue: false
|
|
283
|
+
}) : Promise.resolve(flags.addReviewer)
|
|
284
|
+
}, { onCancel: () => {
|
|
285
|
+
cancel("キャンセルしました");
|
|
286
|
+
process.exit(0);
|
|
287
|
+
} });
|
|
288
|
+
return {
|
|
289
|
+
name: answers.name,
|
|
290
|
+
org: answers.org,
|
|
291
|
+
repo: answers.repo,
|
|
292
|
+
description: answers.description,
|
|
293
|
+
author: answers.author,
|
|
294
|
+
license: answers.license,
|
|
295
|
+
homepage: answers.homepage,
|
|
296
|
+
bugUrl: answers.bugUrl,
|
|
297
|
+
variant: answers.variant,
|
|
298
|
+
esm: answers.esm === "esm",
|
|
299
|
+
test: answers.test,
|
|
300
|
+
docker: answers.docker,
|
|
301
|
+
ignoreData: answers.ignoreData,
|
|
302
|
+
addReviewer: answers.addReviewer,
|
|
303
|
+
overwrite: flags.overwrite ?? false,
|
|
304
|
+
outDir
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* セットアップ内容のサマリーを note() で表示する。
|
|
309
|
+
*/
|
|
310
|
+
function displaySummary(options) {
|
|
311
|
+
note([
|
|
312
|
+
`プロジェクト @${options.org.toLowerCase()}/${options.name}`,
|
|
313
|
+
`出力先 ${options.outDir}`,
|
|
314
|
+
`バリアント ${options.variant}`,
|
|
315
|
+
`モジュール ${options.esm ? "ESM" : "CommonJS"}`,
|
|
316
|
+
`テスト ${options.test ? "Jest" : "なし"}`,
|
|
317
|
+
`Dockerfile ${options.docker ? "あり" : "なし"}`,
|
|
318
|
+
`data/無視 ${options.ignoreData ? "あり" : "なし"}`,
|
|
319
|
+
`add-reviewer ${options.addReviewer ? "あり" : "なし"}`
|
|
320
|
+
].join("\n"), "セットアップ内容");
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 既存ファイルの上書き確認を行う。
|
|
324
|
+
* キャンセル / 拒否時はプロセスを終了する。
|
|
325
|
+
*/
|
|
326
|
+
async function confirmOverwrite(outDir) {
|
|
327
|
+
const confirmed = await confirm({ message: `${outDir} に既存のファイルがあります。上書きしますか?` });
|
|
328
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
329
|
+
cancel("セットアップを中断しました");
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/index.ts
|
|
335
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
336
|
+
const { version } = JSON.parse(readFileSync(path.join(__dirname, "../package.json"), "utf8"));
|
|
337
|
+
/** ファイルを書き込む。親ディレクトリが存在しない場合は作成する。 */
|
|
338
|
+
function writeFile(filePath, content) {
|
|
339
|
+
const dir = path.dirname(filePath);
|
|
340
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
341
|
+
writeFileSync(filePath, content, "utf8");
|
|
342
|
+
}
|
|
343
|
+
/** node / pnpm の存在と Node.js バージョンを確認する。メジャーバージョンを返す。 */
|
|
344
|
+
async function checkPrerequisites() {
|
|
345
|
+
let nodeMajor;
|
|
346
|
+
try {
|
|
347
|
+
const { stdout: nodeVer } = await execa("node", ["--version"]);
|
|
348
|
+
nodeMajor = Number.parseInt(nodeVer.trim().replace("v", "").split(".", 1)[0], 10);
|
|
349
|
+
} catch {
|
|
350
|
+
log.error("Error: node not found. Please install Node.js v20 or later.");
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
if (nodeMajor < 20) {
|
|
354
|
+
log.error(`Error: Node.js v${nodeMajor} is too old. Please install Node.js v20 or later.`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
await execa("pnpm", ["--version"]);
|
|
359
|
+
} catch {
|
|
360
|
+
log.error("Error: pnpm not found. Run \"corepack enable\" or \"npm install -g pnpm\".");
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
return nodeMajor;
|
|
364
|
+
}
|
|
365
|
+
async function main() {
|
|
366
|
+
const program = new Command();
|
|
367
|
+
program.name("create-ts").description("Create book000-style TypeScript projects").version(version).argument("[outdir]", "出力ディレクトリ", ".").option("--name <name>", "プロジェクト名(npm パッケージ名規則)").option("--org <org>", "GitHub 組織 / ユーザー名").option("--repo <repo>", "リポジトリ名").option("--description <desc>", "プロジェクトの説明").option("--author <author>", "作者名").option("--license <spdx>", "SPDX ライセンス識別子").option("--homepage <url>", "ホームページ URL").option("--bug-url <url>", "バグ報告 URL").option("--variant <variant>", "バリアント (base/config-batch/fastify/discord-bot)").option("--esm", "ESM モジュール形式を有効にする").option("--no-esm", "CommonJS モジュール形式(デフォルト)").option("--test", "Jest テストを追加する").option("--no-test", "Jest テストを追加しない(デフォルト)").option("--docker", "Dockerfile を追加する").option("--no-docker", "Dockerfile を追加しない(デフォルト)").option("--ignore-data", "data/ を .gitignore に追加する").option("--no-ignore-data", "data/ を .gitignore に追加しない(デフォルト)").option("--add-reviewer", "add-reviewer ワークフローを追加する").option("--no-add-reviewer", "add-reviewer ワークフローを追加しない(デフォルト)").option("--overwrite", "既存ファイルを確認なしで上書きする");
|
|
368
|
+
program.parse();
|
|
369
|
+
const outDirArg = program.args[0] ?? ".";
|
|
370
|
+
const outDir = path.resolve(outDirArg);
|
|
371
|
+
const opts = program.opts();
|
|
372
|
+
/** CLI で明示的に指定されたかどうか確認して boolean | undefined を返す */
|
|
373
|
+
const getCLIBoolFlag = (name) => {
|
|
374
|
+
const source = program.getOptionValueSource(name);
|
|
375
|
+
if (source === "cli" || source === "env") return opts[name];
|
|
376
|
+
};
|
|
377
|
+
intro("create-ts");
|
|
378
|
+
const s = spinner();
|
|
379
|
+
s.start("前提条件を確認しています...");
|
|
380
|
+
const nodeMajor = await checkPrerequisites();
|
|
381
|
+
s.stop("前提条件を確認しました");
|
|
382
|
+
const options = await collectOptions(outDir, {
|
|
383
|
+
name: opts.name,
|
|
384
|
+
org: opts.org,
|
|
385
|
+
repo: opts.repo,
|
|
386
|
+
description: opts.description,
|
|
387
|
+
author: opts.author,
|
|
388
|
+
license: opts.license,
|
|
389
|
+
homepage: opts.homepage,
|
|
390
|
+
bugUrl: opts.bugUrl,
|
|
391
|
+
variant: opts.variant,
|
|
392
|
+
esm: getCLIBoolFlag("esm"),
|
|
393
|
+
test: getCLIBoolFlag("test"),
|
|
394
|
+
docker: getCLIBoolFlag("docker"),
|
|
395
|
+
ignoreData: getCLIBoolFlag("ignoreData"),
|
|
396
|
+
addReviewer: getCLIBoolFlag("addReviewer"),
|
|
397
|
+
overwrite: opts.overwrite
|
|
398
|
+
});
|
|
399
|
+
if (!options.overwrite) {
|
|
400
|
+
if (existsSync(path.join(outDir, "package.json")) || existsSync(path.join(outDir, "tsconfig.json")) || existsSync(path.join(outDir, "src"))) await confirmOverwrite(outDir);
|
|
401
|
+
}
|
|
402
|
+
displaySummary(options);
|
|
403
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
404
|
+
try {
|
|
405
|
+
s.start("テンプレート設定を読み込んでいます...");
|
|
406
|
+
const templateConfig = fetchTemplateConfig(options.variant);
|
|
407
|
+
s.stop("テンプレート設定を読み込みました");
|
|
408
|
+
const commonFiles = [
|
|
409
|
+
"tsconfig.json",
|
|
410
|
+
".prettierrc.yml",
|
|
411
|
+
"eslint.config.mjs",
|
|
412
|
+
"renovate.json",
|
|
413
|
+
".depcheckrc.json",
|
|
414
|
+
".fixpackrc",
|
|
415
|
+
"pnpm-workspace.yaml",
|
|
416
|
+
".devcontainer/devcontainer.json"
|
|
417
|
+
];
|
|
418
|
+
if (options.docker) commonFiles.push("Dockerfile", "entrypoint.sh");
|
|
419
|
+
s.start("共通ファイルをコピーしています...");
|
|
420
|
+
for (const file of commonFiles) {
|
|
421
|
+
const content = readTemplate(`nodejs/common/${file}`);
|
|
422
|
+
writeFile(path.join(outDir, file), content);
|
|
423
|
+
}
|
|
424
|
+
s.stop(`共通ファイルをコピーしました (${commonFiles.length} ファイル)`);
|
|
425
|
+
const filesToCopy = [...templateConfig.src];
|
|
426
|
+
if (options.test && templateConfig.testSrc) filesToCopy.push(...templateConfig.testSrc);
|
|
427
|
+
s.start("src ファイルをコピーしています...");
|
|
428
|
+
for (const srcFile of filesToCopy) {
|
|
429
|
+
const content = readTemplate(`nodejs/${options.variant}/${srcFile}`);
|
|
430
|
+
writeFile(path.join(outDir, srcFile), content);
|
|
431
|
+
}
|
|
432
|
+
s.stop(`src ファイルをコピーしました (${filesToCopy.length} ファイル)`);
|
|
433
|
+
const tsconfigPath = path.join(outDir, "tsconfig.json");
|
|
434
|
+
const patchedTsConfig = patchTsConfig(JSON.parse(readFileSync(tsconfigPath, "utf8")), options);
|
|
435
|
+
writeFileSync(tsconfigPath, JSON.stringify(patchedTsConfig, null, 2) + "\n", "utf8");
|
|
436
|
+
s.start(".gitignore を生成しています...");
|
|
437
|
+
const gitignoreContent = generateGitignore(options.ignoreData);
|
|
438
|
+
writeFileSync(path.join(outDir, ".gitignore"), gitignoreContent, "utf8");
|
|
439
|
+
const { stdout: nodeVersion } = await execa("node", ["--version"]);
|
|
440
|
+
const nodeVersionNumber = nodeVersion.trim().replace("v", "");
|
|
441
|
+
writeFileSync(path.join(outDir, ".node-version"), `${nodeVersionNumber}\n`, "utf8");
|
|
442
|
+
s.stop(".gitignore と .node-version を生成しました");
|
|
443
|
+
s.start("LICENSE を生成しています...");
|
|
444
|
+
try {
|
|
445
|
+
const licenseRes = await fetch(`https://api.github.com/licenses/${options.license.toLowerCase()}`);
|
|
446
|
+
if (!licenseRes.ok) throw new Error(`HTTP ${licenseRes.status}`);
|
|
447
|
+
const licenseText = (await licenseRes.json()).body.replaceAll("[year]", String((/* @__PURE__ */ new Date()).getFullYear())).replaceAll("[fullname]", options.author);
|
|
448
|
+
writeFileSync(path.join(outDir, "LICENSE"), licenseText, "utf8");
|
|
449
|
+
s.stop("LICENSE を生成しました");
|
|
450
|
+
} catch {
|
|
451
|
+
s.stop("LICENSE の取得をスキップしました(取得失敗)");
|
|
452
|
+
log.warn("警告: LICENSE の取得に失敗しました。後で手動で追加してください。");
|
|
453
|
+
}
|
|
454
|
+
s.start("ワークフローファイルをコピーしています...");
|
|
455
|
+
const workflowDir = path.join(outDir, ".github", "workflows");
|
|
456
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
457
|
+
const ciYml = readTemplate("workflows/nodejs-ci-pnpm.yml");
|
|
458
|
+
writeFileSync(path.join(workflowDir, "nodejs-ci-pnpm.yml"), ciYml, "utf8");
|
|
459
|
+
if (options.docker) {
|
|
460
|
+
const patchedDockerYml = patchDockerWorkflow(readTemplate("workflows/docker.yml"), options.org, options.repo);
|
|
461
|
+
const expected = `${options.org.toLowerCase()}/${options.repo.toLowerCase()}`;
|
|
462
|
+
if (!patchedDockerYml.includes(expected)) log.warn("警告: docker.yml の文字列置換が正しく行われなかった可能性があります。");
|
|
463
|
+
writeFileSync(path.join(workflowDir, "docker.yml"), patchedDockerYml, "utf8");
|
|
464
|
+
}
|
|
465
|
+
if (options.addReviewer) {
|
|
466
|
+
const addReviewerYml = readTemplate("workflows/add-reviewer.yml");
|
|
467
|
+
writeFileSync(path.join(workflowDir, "add-reviewer.yml"), addReviewerYml, "utf8");
|
|
468
|
+
}
|
|
469
|
+
s.stop("ワークフローファイルをコピーしました");
|
|
470
|
+
s.start("package.json を生成しています...");
|
|
471
|
+
const variantPkgText = readTemplate(`nodejs/${options.variant}/package.json`);
|
|
472
|
+
const patchedPkgJson = patchPackageJson(JSON.parse(variantPkgText), options, nodeMajor);
|
|
473
|
+
const depcheckIgnore = templateConfig.depcheckIgnore ?? [];
|
|
474
|
+
if (depcheckIgnore.length > 0 || options.test) {
|
|
475
|
+
const depcheckPath = path.join(outDir, ".depcheckrc.json");
|
|
476
|
+
const updatedDepcheck = updateDepcheck(JSON.parse(readFileSync(depcheckPath, "utf8")), depcheckIgnore, options.test);
|
|
477
|
+
writeFileSync(depcheckPath, JSON.stringify(updatedDepcheck, null, 2) + "\n", "utf8");
|
|
478
|
+
}
|
|
479
|
+
if (templateConfig.configSchema) mkdirSync(path.join(outDir, "schema"), { recursive: true });
|
|
480
|
+
writeFileSync(path.join(outDir, "package.json"), JSON.stringify(patchedPkgJson, null, 2) + "\n", "utf8");
|
|
481
|
+
s.stop("package.json を生成しました");
|
|
482
|
+
s.start("pnpm-lock.yaml をコピーしています...");
|
|
483
|
+
const pnpmLock = readTemplate(`nodejs/${options.variant}/pnpm-lock.yaml`);
|
|
484
|
+
writeFileSync(path.join(outDir, "pnpm-lock.yaml"), pnpmLock, "utf8");
|
|
485
|
+
s.stop("pnpm-lock.yaml をコピーしました");
|
|
486
|
+
s.start("pnpm install を実行しています...");
|
|
487
|
+
await execa("pnpm", ["install", "--frozen-lockfile"], {
|
|
488
|
+
cwd: outDir,
|
|
489
|
+
stdio: "inherit"
|
|
490
|
+
});
|
|
491
|
+
s.stop("依存パッケージをインストールしました");
|
|
492
|
+
if (!options.test) {
|
|
493
|
+
s.start("Jest 関連パッケージを削除しています...");
|
|
494
|
+
await execa("pnpm", [
|
|
495
|
+
"remove",
|
|
496
|
+
"jest",
|
|
497
|
+
"@types/jest",
|
|
498
|
+
"ts-jest"
|
|
499
|
+
], { cwd: outDir });
|
|
500
|
+
s.stop("Jest 関連パッケージを削除しました");
|
|
501
|
+
}
|
|
502
|
+
s.start("fixpack を実行しています...");
|
|
503
|
+
try {
|
|
504
|
+
await execa("npx", ["--yes", "fixpack"], { cwd: outDir });
|
|
505
|
+
s.stop("fixpack を実行しました");
|
|
506
|
+
} catch {
|
|
507
|
+
s.stop("fixpack をスキップしました(失敗)");
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
await execa("npx", ["--yes", "fixdevcontainer"], { cwd: outDir });
|
|
511
|
+
log.success("fixdevcontainer を実行しました");
|
|
512
|
+
} catch {
|
|
513
|
+
log.warn("fixdevcontainer をスキップしました(失敗)");
|
|
514
|
+
}
|
|
515
|
+
const completionLines = [
|
|
516
|
+
"=== セットアップ完了! ===",
|
|
517
|
+
"",
|
|
518
|
+
`プロジェクト : @${options.org.toLowerCase()}/${options.name}`,
|
|
519
|
+
`バリアント : ${options.variant}`,
|
|
520
|
+
`モジュール : ${options.esm ? "ESM" : "CommonJS"}`,
|
|
521
|
+
"",
|
|
522
|
+
"次のステップ:",
|
|
523
|
+
" 1. git init && git add . && git commit -m \"feat: 初期コミット\"",
|
|
524
|
+
" 2. pnpm run lint"
|
|
525
|
+
];
|
|
526
|
+
let stepNum = 3;
|
|
527
|
+
if (templateConfig.configSchema) {
|
|
528
|
+
completionLines.push(` ${stepNum}. pnpm run generate-schema`);
|
|
529
|
+
stepNum++;
|
|
530
|
+
}
|
|
531
|
+
if (options.test) completionLines.push(` ${stepNum}. pnpm run test`);
|
|
532
|
+
outro(completionLines.join("\n"));
|
|
533
|
+
} catch (error) {
|
|
534
|
+
log.error(`エラーが発生しました: ${error.message}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
main().catch((error) => {
|
|
539
|
+
log.error(`予期しないエラーが発生しました: ${error.message}`);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
});
|
|
542
|
+
//#endregion
|
|
543
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@book000/create-ts",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Create book000-style TypeScript projects",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "book000",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-ts": "dist/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.mjs",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"templates"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@clack/prompts": "^1.5.1",
|
|
24
|
+
"commander": "^15.0.0",
|
|
25
|
+
"execa": "^9.6.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@book000/eslint-config": "^1.15.3",
|
|
29
|
+
"@types/node": "^25.9.3",
|
|
30
|
+
"eslint": "^10.5.0",
|
|
31
|
+
"prettier": "^3.8.4",
|
|
32
|
+
"run-z": "^2.1.0",
|
|
33
|
+
"tsdown": "^0.22.2",
|
|
34
|
+
"typescript": "^6.0.3",
|
|
35
|
+
"vitest": "^4.1.9"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"preinstall": "npx only-allow pnpm",
|
|
39
|
+
"build": "tsdown",
|
|
40
|
+
"dev": "tsdown --watch",
|
|
41
|
+
"lint": "run-z lint:prettier,lint:eslint,lint:tsc",
|
|
42
|
+
"lint:prettier": "prettier --check src",
|
|
43
|
+
"lint:eslint": "eslint . -c eslint.config.mjs",
|
|
44
|
+
"lint:tsc": "tsc",
|
|
45
|
+
"fix": "run-z fix:prettier fix:eslint",
|
|
46
|
+
"fix:eslint": "eslint . -c eslint.config.mjs --fix",
|
|
47
|
+
"fix:prettier": "prettier --write src",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
}
|
|
50
|
+
}
|