@book000/create-ts 0.0.0 → 0.1.7

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.
@@ -0,0 +1,25 @@
1
+ import type { ProjectOptions } from './types.js';
2
+ /**
3
+ * バリアントの package.json にプロジェクト固有の値を適用する。
4
+ * テンプレートのプレースホルダーを実際の値で上書きする。
5
+ */
6
+ export declare function patchPackageJson(packageJson: Record<string, unknown>, options: Pick<ProjectOptions, 'name' | 'org' | 'repo' | 'description' | 'author' | 'license' | 'homepage' | 'bugUrl' | 'esm' | 'test'>, nodeMajor: number): Record<string, unknown>;
7
+ /**
8
+ * tsconfig.json にモジュール形式・テスト設定のパッチを適用する。
9
+ */
10
+ export declare function patchTsConfig(tsconfig: Record<string, unknown>, options: Pick<ProjectOptions, 'esm' | 'test'>): Record<string, unknown>;
11
+ /**
12
+ * docker.yml のプレースホルダーをプロジェクト固有の値に置換する。
13
+ */
14
+ export declare function patchDockerWorkflow(content: string, org: string, repo: string): string;
15
+ /**
16
+ * .gitignore を生成する。
17
+ * バンドルされた Node.gitignore(pnpm セクション付き)をベースに、
18
+ * 任意で data/ セクションを追記する。
19
+ */
20
+ export declare function generateGitignore(ignoreData: boolean): string;
21
+ /**
22
+ * .depcheckrc.json の ignores 配列を更新する。
23
+ * テンプレートの depcheckIgnore と Jest 関連パッケージを追加する(重複除去)。
24
+ */
25
+ export declare function updateDepcheck(depcheckJson: Record<string, unknown>, depcheckIgnore: string[], test: boolean): Record<string, unknown>;
@@ -0,0 +1,93 @@
1
+ import { readTemplate } from './template.js';
2
+ /**
3
+ * バリアントの package.json にプロジェクト固有の値を適用する。
4
+ * テンプレートのプレースホルダーを実際の値で上書きする。
5
+ */
6
+ export function patchPackageJson(packageJson, options, nodeMajor) {
7
+ const { name, org, repo, description, author, license, homepage, bugUrl, esm, test, } = options;
8
+ const result = structuredClone(packageJson);
9
+ result.name = `@${org.toLowerCase()}/${name}`;
10
+ result.description = description;
11
+ result.license = license;
12
+ result.author = author;
13
+ result.engines.node = `>=${nodeMajor}`;
14
+ result.repository.url =
15
+ `git+https://github.com/${org}/${repo}.git`;
16
+ result.bugs.url = bugUrl;
17
+ if (homepage) {
18
+ result.homepage = homepage;
19
+ }
20
+ if (esm) {
21
+ result.type = 'module';
22
+ if (test) {
23
+ result.jest = {
24
+ preset: 'ts-jest/presets/default-esm',
25
+ extensionsToTreatAsEsm: ['.ts'],
26
+ transform: {
27
+ '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
28
+ },
29
+ };
30
+ }
31
+ }
32
+ if (!test) {
33
+ delete result.scripts.test;
34
+ delete result.jest;
35
+ }
36
+ return result;
37
+ }
38
+ /**
39
+ * tsconfig.json にモジュール形式・テスト設定のパッチを適用する。
40
+ */
41
+ export function patchTsConfig(tsconfig, options) {
42
+ const result = structuredClone(tsconfig);
43
+ const compilerOptions = result.compilerOptions;
44
+ if (options.esm) {
45
+ compilerOptions.module = 'es2015';
46
+ }
47
+ if (options.test) {
48
+ compilerOptions.types ??= ['node'];
49
+ if (!compilerOptions.types.includes('jest')) {
50
+ compilerOptions.types.push('jest');
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+ /**
56
+ * docker.yml のプレースホルダーをプロジェクト固有の値に置換する。
57
+ */
58
+ export function patchDockerWorkflow(content, org, repo) {
59
+ let result = content;
60
+ result = result.replaceAll('tomacheese/twitter-dm-memo', `${org.toLowerCase()}/${repo.toLowerCase()}`);
61
+ result = result.replaceAll('packageName: "twitter-dm-memo"', `packageName: "${repo}"`);
62
+ return result;
63
+ }
64
+ /**
65
+ * .gitignore を生成する。
66
+ * バンドルされた Node.gitignore(pnpm セクション付き)をベースに、
67
+ * 任意で data/ セクションを追記する。
68
+ */
69
+ export function generateGitignore(ignoreData) {
70
+ let result = readTemplate('gitignore/Node.gitignore');
71
+ if (ignoreData) {
72
+ result += '\n\n# データディレクトリ\ndata/';
73
+ }
74
+ return result;
75
+ }
76
+ /**
77
+ * .depcheckrc.json の ignores 配列を更新する。
78
+ * テンプレートの depcheckIgnore と Jest 関連パッケージを追加する(重複除去)。
79
+ */
80
+ export function updateDepcheck(depcheckJson, depcheckIgnore, test) {
81
+ const result = structuredClone(depcheckJson);
82
+ const ignores = result.ignores ?? [];
83
+ result.ignores = ignores;
84
+ for (const pkg of depcheckIgnore) {
85
+ if (!ignores.includes(pkg)) {
86
+ ignores.push(pkg);
87
+ }
88
+ }
89
+ if (test && !ignores.includes('@types/jest')) {
90
+ ignores.push('@types/jest');
91
+ }
92
+ return result;
93
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ import { intro, log, outro, spinner } 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
+ import { generateGitignore, patchDockerWorkflow, patchPackageJson, patchTsConfig, updateDepcheck, } from './generate.js';
9
+ import { collectOptions, confirmOverwrite, displaySummary } from './prompts.js';
10
+ import { fetchTemplateConfig, readTemplate } from './template.js';
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const { version } = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
13
+ /** ファイルを書き込む。親ディレクトリが存在しない場合は作成する。 */
14
+ function writeFile(filePath, content) {
15
+ const dir = path.dirname(filePath);
16
+ if (!existsSync(dir)) {
17
+ mkdirSync(dir, { recursive: true });
18
+ }
19
+ writeFileSync(filePath, content, 'utf8');
20
+ }
21
+ /** node / pnpm の存在と Node.js バージョンを確認する。メジャーバージョンを返す。 */
22
+ async function checkPrerequisites() {
23
+ let nodeMajor;
24
+ try {
25
+ const { stdout: nodeVer } = await execa('node', ['--version']);
26
+ nodeMajor = Number.parseInt(nodeVer.trim().replace('v', '').split('.', 1)[0], 10);
27
+ }
28
+ catch {
29
+ log.error('Error: node not found. Please install Node.js v20 or later.');
30
+ process.exit(1);
31
+ }
32
+ if (nodeMajor < 20) {
33
+ log.error(`Error: Node.js v${nodeMajor} is too old. Please install Node.js v20 or later.`);
34
+ process.exit(1);
35
+ }
36
+ try {
37
+ await execa('pnpm', ['--version']);
38
+ }
39
+ catch {
40
+ log.error('Error: pnpm not found. Run "corepack enable" or "npm install -g pnpm".');
41
+ process.exit(1);
42
+ }
43
+ return nodeMajor;
44
+ }
45
+ async function main() {
46
+ const program = new Command();
47
+ program
48
+ .name('create-ts')
49
+ .description('Create book000-style TypeScript projects')
50
+ .version(version)
51
+ .argument('[outdir]', '出力ディレクトリ', '.')
52
+ .option('--name <name>', 'プロジェクト名(npm パッケージ名規則)')
53
+ .option('--org <org>', 'GitHub 組織 / ユーザー名')
54
+ .option('--repo <repo>', 'リポジトリ名')
55
+ .option('--description <desc>', 'プロジェクトの説明')
56
+ .option('--author <author>', '作者名')
57
+ .option('--license <spdx>', 'SPDX ライセンス識別子')
58
+ .option('--homepage <url>', 'ホームページ URL')
59
+ .option('--bug-url <url>', 'バグ報告 URL')
60
+ .option('--variant <variant>', 'バリアント (base/config-batch/fastify/discord-bot)')
61
+ .option('--esm', 'ESM モジュール形式を有効にする')
62
+ .option('--no-esm', 'CommonJS モジュール形式(デフォルト)')
63
+ .option('--test', 'Jest テストを追加する')
64
+ .option('--no-test', 'Jest テストを追加しない(デフォルト)')
65
+ .option('--docker', 'Dockerfile を追加する')
66
+ .option('--no-docker', 'Dockerfile を追加しない(デフォルト)')
67
+ .option('--ignore-data', 'data/ を .gitignore に追加する')
68
+ .option('--no-ignore-data', 'data/ を .gitignore に追加しない(デフォルト)')
69
+ .option('--add-reviewer', 'add-reviewer ワークフローを追加する')
70
+ .option('--no-add-reviewer', 'add-reviewer ワークフローを追加しない(デフォルト)')
71
+ .option('--overwrite', '既存ファイルを確認なしで上書きする');
72
+ program.parse();
73
+ const outDirArg = program.args[0] ?? '.';
74
+ const outDir = path.resolve(outDirArg);
75
+ const opts = program.opts();
76
+ /** CLI で明示的に指定されたかどうか確認して boolean | undefined を返す */
77
+ const getCLIBoolFlag = (name) => {
78
+ const source = program.getOptionValueSource(name);
79
+ if (source === 'cli' || source === 'env') {
80
+ return opts[name];
81
+ }
82
+ return undefined;
83
+ };
84
+ intro('create-ts');
85
+ // ステップ 1: 前提チェック
86
+ const s = spinner();
87
+ s.start('前提条件を確認しています...');
88
+ const nodeMajor = await checkPrerequisites();
89
+ s.stop('前提条件を確認しました');
90
+ // 対話プロンプトでオプション収集
91
+ const options = await collectOptions(outDir, {
92
+ name: opts.name,
93
+ org: opts.org,
94
+ repo: opts.repo,
95
+ description: opts.description,
96
+ author: opts.author,
97
+ license: opts.license,
98
+ homepage: opts.homepage,
99
+ bugUrl: opts.bugUrl,
100
+ variant: opts.variant,
101
+ esm: getCLIBoolFlag('esm'),
102
+ test: getCLIBoolFlag('test'),
103
+ docker: getCLIBoolFlag('docker'),
104
+ ignoreData: getCLIBoolFlag('ignoreData'),
105
+ addReviewer: getCLIBoolFlag('addReviewer'),
106
+ overwrite: opts.overwrite,
107
+ });
108
+ // 既存ファイルの確認
109
+ if (!options.overwrite) {
110
+ const hasExisting = existsSync(path.join(outDir, 'package.json')) ||
111
+ existsSync(path.join(outDir, 'tsconfig.json')) ||
112
+ existsSync(path.join(outDir, 'src'));
113
+ if (hasExisting) {
114
+ await confirmOverwrite(outDir);
115
+ }
116
+ }
117
+ // サマリー表示
118
+ displaySummary(options);
119
+ // 出力ディレクトリを作成
120
+ if (!existsSync(outDir)) {
121
+ mkdirSync(outDir, { recursive: true });
122
+ }
123
+ try {
124
+ // ステップ 2: template.json の読み込み
125
+ s.start('テンプレート設定を読み込んでいます...');
126
+ const templateConfig = fetchTemplateConfig(options.variant);
127
+ s.stop('テンプレート設定を読み込みました');
128
+ // ステップ 3: 共通テンプレートファイルのコピー
129
+ const commonFiles = [
130
+ 'tsconfig.json',
131
+ '.prettierrc.yml',
132
+ 'eslint.config.mjs',
133
+ 'renovate.json',
134
+ '.depcheckrc.json',
135
+ '.fixpackrc',
136
+ 'pnpm-workspace.yaml',
137
+ '.devcontainer/devcontainer.json',
138
+ ];
139
+ if (options.docker) {
140
+ commonFiles.push('Dockerfile', 'entrypoint.sh');
141
+ }
142
+ s.start('共通ファイルをコピーしています...');
143
+ for (const file of commonFiles) {
144
+ const content = readTemplate(`nodejs/common/${file}`);
145
+ writeFile(path.join(outDir, file), content);
146
+ }
147
+ s.stop(`共通ファイルをコピーしました (${commonFiles.length} ファイル)`);
148
+ // ステップ 4: バリアント src ファイルのコピー
149
+ const filesToCopy = [...templateConfig.src];
150
+ if (options.test && templateConfig.testSrc) {
151
+ filesToCopy.push(...templateConfig.testSrc);
152
+ }
153
+ s.start('src ファイルをコピーしています...');
154
+ for (const srcFile of filesToCopy) {
155
+ const content = readTemplate(`nodejs/${options.variant}/${srcFile}`);
156
+ writeFile(path.join(outDir, srcFile), content);
157
+ }
158
+ s.stop(`src ファイルをコピーしました (${filesToCopy.length} ファイル)`);
159
+ // ステップ 5: tsconfig.json のパッチ
160
+ const tsconfigPath = path.join(outDir, 'tsconfig.json');
161
+ const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf8'));
162
+ const patchedTsConfig = patchTsConfig(tsconfig, options);
163
+ writeFileSync(tsconfigPath, JSON.stringify(patchedTsConfig, null, 2) + '\n', 'utf8');
164
+ // ステップ 6: .gitignore / .node-version の生成
165
+ s.start('.gitignore を生成しています...');
166
+ const gitignoreContent = generateGitignore(options.ignoreData);
167
+ writeFileSync(path.join(outDir, '.gitignore'), gitignoreContent, 'utf8');
168
+ const { stdout: nodeVersion } = await execa('node', ['--version']);
169
+ const nodeVersionNumber = nodeVersion.trim().replace('v', '');
170
+ writeFileSync(path.join(outDir, '.node-version'), `${nodeVersionNumber}\n`, 'utf8');
171
+ s.stop('.gitignore と .node-version を生成しました');
172
+ // ステップ 7: LICENSE の生成
173
+ s.start('LICENSE を生成しています...');
174
+ try {
175
+ const licenseRes = await fetch(`https://api.github.com/licenses/${options.license.toLowerCase()}`);
176
+ if (!licenseRes.ok)
177
+ throw new Error(`HTTP ${licenseRes.status}`);
178
+ const licenseJson = (await licenseRes.json());
179
+ const licenseText = licenseJson.body
180
+ .replaceAll('[year]', String(new Date().getFullYear()))
181
+ .replaceAll('[fullname]', options.author);
182
+ writeFileSync(path.join(outDir, 'LICENSE'), licenseText, 'utf8');
183
+ s.stop('LICENSE を生成しました');
184
+ }
185
+ catch {
186
+ s.stop('LICENSE の取得をスキップしました(取得失敗)');
187
+ log.warn('警告: LICENSE の取得に失敗しました。後で手動で追加してください。');
188
+ }
189
+ // ステップ 8: ワークフローファイルのコピー
190
+ s.start('ワークフローファイルをコピーしています...');
191
+ const workflowDir = path.join(outDir, '.github', 'workflows');
192
+ mkdirSync(workflowDir, { recursive: true });
193
+ const ciYml = readTemplate('workflows/nodejs-ci-pnpm.yml');
194
+ writeFileSync(path.join(workflowDir, 'nodejs-ci-pnpm.yml'), ciYml, 'utf8');
195
+ if (options.docker) {
196
+ const dockerYml = readTemplate('workflows/docker.yml');
197
+ const patchedDockerYml = patchDockerWorkflow(dockerYml, options.org, options.repo);
198
+ const expected = `${options.org.toLowerCase()}/${options.repo.toLowerCase()}`;
199
+ if (!patchedDockerYml.includes(expected)) {
200
+ log.warn('警告: docker.yml の文字列置換が正しく行われなかった可能性があります。');
201
+ }
202
+ writeFileSync(path.join(workflowDir, 'docker.yml'), patchedDockerYml, 'utf8');
203
+ }
204
+ if (options.addReviewer) {
205
+ const addReviewerYml = readTemplate('workflows/add-reviewer.yml');
206
+ writeFileSync(path.join(workflowDir, 'add-reviewer.yml'), addReviewerYml, 'utf8');
207
+ }
208
+ s.stop('ワークフローファイルをコピーしました');
209
+ // ステップ 9: package.json の生成
210
+ s.start('package.json を生成しています...');
211
+ // 9-1: バリアントの package.json を読み込み
212
+ const variantPkgText = readTemplate(`nodejs/${options.variant}/package.json`);
213
+ const variantPkgJson = JSON.parse(variantPkgText);
214
+ // 9-2〜9-4: プロジェクト固有の値を適用
215
+ const patchedPkgJson = patchPackageJson(variantPkgJson, options, nodeMajor);
216
+ // 9-5: .depcheckrc.json の更新
217
+ const depcheckIgnore = templateConfig.depcheckIgnore ?? [];
218
+ if (depcheckIgnore.length > 0 || options.test) {
219
+ const depcheckPath = path.join(outDir, '.depcheckrc.json');
220
+ const depcheckJson = JSON.parse(readFileSync(depcheckPath, 'utf8'));
221
+ const updatedDepcheck = updateDepcheck(depcheckJson, depcheckIgnore, options.test);
222
+ writeFileSync(depcheckPath, JSON.stringify(updatedDepcheck, null, 2) + '\n', 'utf8');
223
+ }
224
+ // 9-6: schema/ ディレクトリの作成
225
+ if (templateConfig.configSchema) {
226
+ mkdirSync(path.join(outDir, 'schema'), { recursive: true });
227
+ }
228
+ // 9-7: package.json の保存
229
+ writeFileSync(path.join(outDir, 'package.json'), JSON.stringify(patchedPkgJson, null, 2) + '\n', 'utf8');
230
+ s.stop('package.json を生成しました');
231
+ // ステップ 10: pnpm-lock.yaml のコピー
232
+ s.start('pnpm-lock.yaml をコピーしています...');
233
+ const pnpmLock = readTemplate(`nodejs/${options.variant}/pnpm-lock.yaml`);
234
+ writeFileSync(path.join(outDir, 'pnpm-lock.yaml'), pnpmLock, 'utf8');
235
+ s.stop('pnpm-lock.yaml をコピーしました');
236
+ // ステップ 11: pnpm install
237
+ s.start('pnpm install を実行しています...');
238
+ await execa('pnpm', ['install', '--frozen-lockfile'], {
239
+ cwd: outDir,
240
+ stdio: 'inherit',
241
+ });
242
+ s.stop('依存パッケージをインストールしました');
243
+ // ステップ 12: jest 関連の除去(テストなし時)
244
+ if (!options.test) {
245
+ s.start('Jest 関連パッケージを削除しています...');
246
+ await execa('pnpm', ['remove', 'jest', '@types/jest', 'ts-jest'], {
247
+ cwd: outDir,
248
+ });
249
+ s.stop('Jest 関連パッケージを削除しました');
250
+ }
251
+ // ステップ 13: fixpack / fixdevcontainer
252
+ s.start('fixpack を実行しています...');
253
+ try {
254
+ await execa('npx', ['--yes', 'fixpack'], { cwd: outDir });
255
+ s.stop('fixpack を実行しました');
256
+ }
257
+ catch {
258
+ s.stop('fixpack をスキップしました(失敗)');
259
+ }
260
+ try {
261
+ await execa('npx', ['--yes', 'fixdevcontainer'], { cwd: outDir });
262
+ log.success('fixdevcontainer を実行しました');
263
+ }
264
+ catch {
265
+ log.warn('fixdevcontainer をスキップしました(失敗)');
266
+ }
267
+ // ステップ 14: 完了メッセージ
268
+ const completionLines = [
269
+ '=== セットアップ完了! ===',
270
+ '',
271
+ `プロジェクト : @${options.org.toLowerCase()}/${options.name}`,
272
+ `バリアント : ${options.variant}`,
273
+ `モジュール : ${options.esm ? 'ESM' : 'CommonJS'}`,
274
+ '',
275
+ '次のステップ:',
276
+ ' 1. git init && git add . && git commit -m "feat: 初期コミット"',
277
+ ' 2. pnpm run lint',
278
+ ];
279
+ let stepNum = 3;
280
+ if (templateConfig.configSchema) {
281
+ completionLines.push(` ${stepNum}. pnpm run generate-schema`);
282
+ stepNum++;
283
+ }
284
+ if (options.test) {
285
+ completionLines.push(` ${stepNum}. pnpm run test`);
286
+ }
287
+ outro(completionLines.join('\n'));
288
+ }
289
+ catch (error) {
290
+ log.error(`エラーが発生しました: ${error.message}`);
291
+ process.exit(1);
292
+ }
293
+ }
294
+ main().catch((error) => {
295
+ log.error(`予期しないエラーが発生しました: ${error.message}`);
296
+ process.exit(1);
297
+ });
@@ -0,0 +1,33 @@
1
+ import type { ProjectOptions } from './types.js';
2
+ /** CLI フラグから渡される部分的なオプション */
3
+ export interface CliFlags {
4
+ name?: string;
5
+ org?: string;
6
+ repo?: string;
7
+ description?: string;
8
+ author?: string;
9
+ license?: string;
10
+ homepage?: string;
11
+ bugUrl?: string;
12
+ variant?: string;
13
+ esm?: boolean;
14
+ test?: boolean;
15
+ docker?: boolean;
16
+ ignoreData?: boolean;
17
+ addReviewer?: boolean;
18
+ overwrite?: boolean;
19
+ }
20
+ /**
21
+ * 対話プロンプトで ProjectOptions を収集する。
22
+ * CLI フラグで指定済みの項目はスキップする。
23
+ */
24
+ export declare function collectOptions(outDir: string, flags: CliFlags): Promise<ProjectOptions>;
25
+ /**
26
+ * セットアップ内容のサマリーを note() で表示する。
27
+ */
28
+ export declare function displaySummary(options: ProjectOptions): void;
29
+ /**
30
+ * 既存ファイルの上書き確認を行う。
31
+ * キャンセル / 拒否時はプロセスを終了する。
32
+ */
33
+ export declare function confirmOverwrite(outDir: string): Promise<void>;