@book000/create-ts 0.0.0 → 0.1.6

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>;
@@ -0,0 +1,235 @@
1
+ import { cancel, confirm, group, isCancel, log, note, select, text, } from '@clack/prompts';
2
+ import path from 'node:path';
3
+ import { validateLicense, validateOrgName, validateProjectName, validateRepoName, } from './validate.js';
4
+ /** 有効なバリアント値の一覧 */
5
+ const VALID_VARIANTS = ['base', 'config-batch', 'fastify', 'discord-bot'];
6
+ /**
7
+ * CLI フラグで渡された値をバリデーションし、不正な場合はプロセスを終了する。
8
+ */
9
+ function validateCliFlags(flags) {
10
+ if (flags.name !== undefined) {
11
+ const err = validateProjectName(flags.name);
12
+ if (err) {
13
+ log.error(`Invalid --name: ${err}`);
14
+ // eslint-disable-next-line unicorn/no-process-exit
15
+ process.exit(1);
16
+ }
17
+ }
18
+ if (flags.org !== undefined) {
19
+ const err = validateOrgName(flags.org);
20
+ if (err) {
21
+ log.error(`Invalid --org: ${err}`);
22
+ // eslint-disable-next-line unicorn/no-process-exit
23
+ process.exit(1);
24
+ }
25
+ }
26
+ if (flags.repo !== undefined) {
27
+ const err = validateRepoName(flags.repo);
28
+ if (err) {
29
+ log.error(`Invalid --repo: ${err}`);
30
+ // eslint-disable-next-line unicorn/no-process-exit
31
+ process.exit(1);
32
+ }
33
+ }
34
+ if (flags.license !== undefined) {
35
+ const err = validateLicense(flags.license);
36
+ if (err) {
37
+ log.error(`Invalid --license: ${err}`);
38
+ // eslint-disable-next-line unicorn/no-process-exit
39
+ process.exit(1);
40
+ }
41
+ }
42
+ if (flags.variant !== undefined && !VALID_VARIANTS.includes(flags.variant)) {
43
+ log.error(`Invalid --variant: "${flags.variant}". Must be one of: ${VALID_VARIANTS.join(', ')}`);
44
+ // eslint-disable-next-line unicorn/no-process-exit
45
+ process.exit(1);
46
+ }
47
+ }
48
+ /**
49
+ * 対話プロンプトで ProjectOptions を収集する。
50
+ * CLI フラグで指定済みの項目はスキップする。
51
+ */
52
+ export async function collectOptions(outDir, flags) {
53
+ // CLI フラグの値を事前検証する(不正な場合はプロセスを終了)
54
+ validateCliFlags(flags);
55
+ const resolvedDir = path.resolve(outDir);
56
+ const dirBasename = path.basename(resolvedDir);
57
+ const nameDefault = dirBasename !== '.' && /^[a-z0-9][a-z0-9\-_.]*$/.test(dirBasename)
58
+ ? dirBasename
59
+ : undefined;
60
+ const answers = await group({
61
+ // グループ 1: プロジェクト情報
62
+ name: () => flags.name === undefined
63
+ ? text({
64
+ message: 'プロジェクト名',
65
+ placeholder: 'my-app',
66
+ initialValue: nameDefault,
67
+ validate: validateProjectName,
68
+ })
69
+ : Promise.resolve(flags.name),
70
+ org: () => flags.org === undefined
71
+ ? text({
72
+ message: 'GitHub 組織 / ユーザー名',
73
+ initialValue: 'book000',
74
+ validate: validateOrgName,
75
+ })
76
+ : Promise.resolve(flags.org),
77
+ repo: ({ results }) => flags.repo === undefined
78
+ ? text({
79
+ message: 'リポジトリ名',
80
+ initialValue: results.name ?? nameDefault ?? 'my-app',
81
+ validate: validateRepoName,
82
+ })
83
+ : Promise.resolve(flags.repo),
84
+ description: () => flags.description === undefined
85
+ ? text({ message: 'プロジェクトの説明', placeholder: '' })
86
+ : Promise.resolve(flags.description),
87
+ author: ({ results }) => flags.author === undefined
88
+ ? text({
89
+ message: '作者名',
90
+ initialValue: results.org ?? 'book000',
91
+ })
92
+ : Promise.resolve(flags.author),
93
+ license: () => flags.license === undefined
94
+ ? text({
95
+ message: 'ライセンス (SPDX 識別子)',
96
+ initialValue: 'MIT',
97
+ validate: validateLicense,
98
+ })
99
+ : Promise.resolve(flags.license),
100
+ homepage: () => flags.homepage === undefined
101
+ ? text({
102
+ message: 'ホームページ URL(空白でスキップ)',
103
+ placeholder: '',
104
+ })
105
+ : Promise.resolve(flags.homepage),
106
+ bugUrl: ({ results }) => flags.bugUrl === undefined
107
+ ? text({
108
+ message: 'バグ報告 URL',
109
+ initialValue: `https://github.com/${results.org ?? 'book000'}/${results.repo ?? 'my-app'}/issues`,
110
+ })
111
+ : Promise.resolve(flags.bugUrl),
112
+ // グループ 2: テンプレート選択
113
+ variant: () => flags.variant === undefined
114
+ ? select({
115
+ message: 'バリアント',
116
+ options: [
117
+ {
118
+ value: 'base',
119
+ label: 'base',
120
+ hint: '最小構成(TypeScript + lint)',
121
+ },
122
+ {
123
+ value: 'config-batch',
124
+ label: 'config-batch',
125
+ hint: '設定ファイルありのバッチ処理',
126
+ },
127
+ {
128
+ value: 'fastify',
129
+ label: 'fastify',
130
+ hint: 'Fastify HTTP サーバー',
131
+ },
132
+ {
133
+ value: 'discord-bot',
134
+ label: 'discord-bot',
135
+ hint: 'Discord Bot',
136
+ },
137
+ ],
138
+ initialValue: 'base',
139
+ })
140
+ : Promise.resolve(flags.variant),
141
+ esm: () => flags.esm === undefined
142
+ ? select({
143
+ message: 'モジュール形式',
144
+ options: [
145
+ {
146
+ value: 'cjs',
147
+ label: 'CommonJS',
148
+ hint: '既定・現行の標準(--no-esm)',
149
+ },
150
+ { value: 'esm', label: 'ESM', hint: 'ES Modules(--esm)' },
151
+ ],
152
+ initialValue: 'cjs',
153
+ })
154
+ : Promise.resolve(flags.esm ? 'esm' : 'cjs'),
155
+ // グループ 3: オプション
156
+ test: () => flags.test === undefined
157
+ ? confirm({
158
+ message: 'Jest テストを追加しますか?',
159
+ initialValue: false,
160
+ })
161
+ : Promise.resolve(flags.test),
162
+ docker: () => flags.docker === undefined
163
+ ? confirm({
164
+ message: 'Dockerfile を追加しますか?',
165
+ initialValue: false,
166
+ })
167
+ : Promise.resolve(flags.docker),
168
+ ignoreData: () => flags.ignoreData === undefined
169
+ ? confirm({
170
+ message: '`data/` を .gitignore に追加しますか?',
171
+ initialValue: false,
172
+ })
173
+ : Promise.resolve(flags.ignoreData),
174
+ addReviewer: () => flags.addReviewer === undefined
175
+ ? confirm({
176
+ message: 'add-reviewer ワークフローを追加しますか?',
177
+ initialValue: false,
178
+ })
179
+ : Promise.resolve(flags.addReviewer),
180
+ }, {
181
+ onCancel: () => {
182
+ cancel('キャンセルしました');
183
+ // eslint-disable-next-line unicorn/no-process-exit
184
+ process.exit(0);
185
+ },
186
+ });
187
+ return {
188
+ name: answers.name,
189
+ org: answers.org,
190
+ repo: answers.repo,
191
+ description: answers.description,
192
+ author: answers.author,
193
+ license: answers.license,
194
+ homepage: answers.homepage,
195
+ bugUrl: answers.bugUrl,
196
+ variant: answers.variant,
197
+ esm: answers.esm === 'esm',
198
+ test: answers.test,
199
+ docker: answers.docker,
200
+ ignoreData: answers.ignoreData,
201
+ addReviewer: answers.addReviewer,
202
+ overwrite: flags.overwrite ?? false,
203
+ outDir,
204
+ };
205
+ }
206
+ /**
207
+ * セットアップ内容のサマリーを note() で表示する。
208
+ */
209
+ export function displaySummary(options) {
210
+ const lines = [
211
+ `プロジェクト @${options.org.toLowerCase()}/${options.name}`,
212
+ `出力先 ${options.outDir}`,
213
+ `バリアント ${options.variant}`,
214
+ `モジュール ${options.esm ? 'ESM' : 'CommonJS'}`,
215
+ `テスト ${options.test ? 'Jest' : 'なし'}`,
216
+ `Dockerfile ${options.docker ? 'あり' : 'なし'}`,
217
+ `data/無視 ${options.ignoreData ? 'あり' : 'なし'}`,
218
+ `add-reviewer ${options.addReviewer ? 'あり' : 'なし'}`,
219
+ ];
220
+ note(lines.join('\n'), 'セットアップ内容');
221
+ }
222
+ /**
223
+ * 既存ファイルの上書き確認を行う。
224
+ * キャンセル / 拒否時はプロセスを終了する。
225
+ */
226
+ export async function confirmOverwrite(outDir) {
227
+ const confirmed = await confirm({
228
+ message: `${outDir} に既存のファイルがあります。上書きしますか?`,
229
+ });
230
+ if (isCancel(confirmed) || !confirmed) {
231
+ cancel('セットアップを中断しました');
232
+ // eslint-disable-next-line unicorn/no-process-exit
233
+ process.exit(0);
234
+ }
235
+ }
@@ -0,0 +1,11 @@
1
+ import type { TemplateConfig } from './types.js';
2
+ /**
3
+ * バンドルされたテンプレートファイルを文字列として読み込む。
4
+ * @param relativePath - テンプレートルートからの相対パス
5
+ */
6
+ export declare function readTemplate(relativePath: string): string;
7
+ /**
8
+ * バリアントの template.json を読み込んでパースする。
9
+ * @param variant - バリアント名
10
+ */
11
+ export declare function fetchTemplateConfig(variant: string): TemplateConfig;
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ /** バンドルされたテンプレートのルートディレクトリ */
6
+ const TEMPLATES_DIR = path.join(__dirname, '../templates');
7
+ /**
8
+ * バンドルされたテンプレートファイルを文字列として読み込む。
9
+ * @param relativePath - テンプレートルートからの相対パス
10
+ */
11
+ export function readTemplate(relativePath) {
12
+ const filePath = path.join(TEMPLATES_DIR, relativePath);
13
+ return readFileSync(filePath, 'utf8');
14
+ }
15
+ /**
16
+ * バリアントの template.json を読み込んでパースする。
17
+ * @param variant - バリアント名
18
+ */
19
+ export function fetchTemplateConfig(variant) {
20
+ const content = readTemplate(`nodejs/${variant}/template.json`);
21
+ return JSON.parse(content);
22
+ }
@@ -0,0 +1,33 @@
1
+ /** サポートされているプロジェクトバリアント */
2
+ export type Variant = 'base' | 'config-batch' | 'fastify' | 'discord-bot';
3
+ /** プロジェクトセットアップの全オプション */
4
+ export interface ProjectOptions {
5
+ name: string;
6
+ org: string;
7
+ repo: string;
8
+ description: string;
9
+ author: string;
10
+ license: string;
11
+ homepage: string;
12
+ bugUrl: string;
13
+ variant: Variant;
14
+ esm: boolean;
15
+ test: boolean;
16
+ docker: boolean;
17
+ ignoreData: boolean;
18
+ addReviewer: boolean;
19
+ overwrite: boolean;
20
+ outDir: string;
21
+ }
22
+ /** バリアントの template.json スキーマ */
23
+ export interface TemplateConfig {
24
+ configSchema: boolean;
25
+ dependencies?: string[];
26
+ devDependencies?: string[];
27
+ scripts?: Record<string, string>;
28
+ depcheckIgnore?: string[];
29
+ /** 常にコピーする src ファイルのリスト */
30
+ src: string[];
31
+ /** --test 有効時のみコピーするテストファイルのリスト */
32
+ testSrc?: string[];
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * プロジェクト名を検証する。
3
+ * npm パッケージ名規則: 小文字英数字・ハイフン・アンダースコア・ドット、214 文字以下。
4
+ */
5
+ export declare function validateProjectName(value: string | undefined): string | undefined;
6
+ /**
7
+ * GitHub 組織 / ユーザー名を検証する。
8
+ * 英数字・ハイフン・ドットのみ許容。
9
+ */
10
+ export declare function validateOrgName(value: string | undefined): string | undefined;
11
+ /**
12
+ * リポジトリ名を検証する。
13
+ * 英数字・ハイフン・アンダースコア・ドットのみ許容。先頭はアンダースコア可。
14
+ */
15
+ export declare function validateRepoName(value: string | undefined): string | undefined;
16
+ /**
17
+ * SPDX ライセンス識別子を検証する。
18
+ * 英数字・ドット・ハイフンのみ許容。
19
+ */
20
+ export declare function validateLicense(value: string | undefined): string | undefined;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * プロジェクト名を検証する。
3
+ * npm パッケージ名規則: 小文字英数字・ハイフン・アンダースコア・ドット、214 文字以下。
4
+ */
5
+ export function validateProjectName(value) {
6
+ if (!value)
7
+ return 'プロジェクト名は必須です';
8
+ if (value.length > 214 || !/^[a-z0-9][a-z0-9\-_.]*$/.test(value)) {
9
+ return 'プロジェクト名は小文字英数字・ハイフン・アンダースコア・ドットのみ使用できます(最大 214 文字)';
10
+ }
11
+ return undefined;
12
+ }
13
+ /**
14
+ * GitHub 組織 / ユーザー名を検証する。
15
+ * 英数字・ハイフン・ドットのみ許容。
16
+ */
17
+ export function validateOrgName(value) {
18
+ if (!value)
19
+ return '組織 / ユーザー名は必須です';
20
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9\-.]*$/.test(value)) {
21
+ return '組織 / ユーザー名は英数字・ハイフン・ドットのみ使用できます';
22
+ }
23
+ return undefined;
24
+ }
25
+ /**
26
+ * リポジトリ名を検証する。
27
+ * 英数字・ハイフン・アンダースコア・ドットのみ許容。先頭はアンダースコア可。
28
+ */
29
+ export function validateRepoName(value) {
30
+ if (!value)
31
+ return 'リポジトリ名は必須です';
32
+ if (!/^[a-zA-Z0-9_][a-zA-Z0-9\-_.]*$/.test(value)) {
33
+ return 'リポジトリ名は英数字・ハイフン・アンダースコア・ドットのみ使用できます';
34
+ }
35
+ return undefined;
36
+ }
37
+ /**
38
+ * SPDX ライセンス識別子を検証する。
39
+ * 英数字・ドット・ハイフンのみ許容。
40
+ */
41
+ export function validateLicense(value) {
42
+ if (!value)
43
+ return 'ライセンス識別子は必須です';
44
+ if (!/^[a-zA-Z0-9.-]+$/.test(value)) {
45
+ return 'ライセンス識別子は英数字・ドット・ハイフンのみ使用できます(例: MIT, Apache-2.0)';
46
+ }
47
+ return undefined;
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { generateGitignore, patchDockerWorkflow, patchPackageJson, patchTsConfig, updateDepcheck, } from '../src/generate.js';
3
+ // ---- テスト用フィクスチャ ----
4
+ /** テンプレートの tsconfig.json に相当するベースオブジェクト */
5
+ const BASE_TSCONFIG = {
6
+ compilerOptions: {
7
+ target: 'es2020',
8
+ module: 'commonjs',
9
+ moduleResolution: 'bundler',
10
+ lib: ['ESNext'],
11
+ types: ['node'],
12
+ },
13
+ include: ['src/**/*'],
14
+ };
15
+ /** テンプレートの package.json に相当するベースオブジェクト */
16
+ const BASE_PACKAGE_JSON = {
17
+ name: '@book000/template',
18
+ version: '1.0.0',
19
+ description: '',
20
+ license: 'MIT',
21
+ author: 'book000',
22
+ scripts: {
23
+ preinstall: 'npx only-allow pnpm',
24
+ start: 'tsx ./src/main.ts',
25
+ test: 'jest --runInBand --passWithNoTests',
26
+ },
27
+ devDependencies: {
28
+ jest: '*',
29
+ '@types/jest': '*',
30
+ 'ts-jest': '*',
31
+ },
32
+ engines: { node: '>=24' },
33
+ repository: { type: 'git', url: 'git+https://github.com/book000/template.git' },
34
+ bugs: { url: 'https://github.com/book000/template/issues' },
35
+ jest: {
36
+ preset: 'ts-jest',
37
+ testEnvironment: 'node',
38
+ },
39
+ };
40
+ // ---- patchPackageJson ----
41
+ describe('patchPackageJson', () => {
42
+ const baseOptions = {
43
+ name: 'my-app',
44
+ org: 'book000',
45
+ repo: 'my-app',
46
+ description: 'My application',
47
+ author: 'book000',
48
+ license: 'MIT',
49
+ homepage: '',
50
+ bugUrl: 'https://github.com/book000/my-app/issues',
51
+ esm: false,
52
+ test: false,
53
+ };
54
+ it('org が大文字でも name が小文字化される', () => {
55
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, org: 'Book000' }, 20);
56
+ expect(result.name).toBe('@book000/my-app');
57
+ });
58
+ it('--esm: type が "module" になる', () => {
59
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, esm: true, test: false }, 20);
60
+ expect(result.type).toBe('module');
61
+ });
62
+ it('--esm --test: jest フィールドが ESM 用設定になる', () => {
63
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, esm: true, test: true }, 20);
64
+ expect(result.jest.preset).toBe('ts-jest/presets/default-esm');
65
+ expect(result.jest.extensionsToTreatAsEsm).toContain('.ts');
66
+ });
67
+ it('--no-test: scripts.test と jest フィールドが削除される', () => {
68
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, test: false }, 20);
69
+ expect(result.scripts.test).toBeUndefined();
70
+ expect(result.jest).toBeUndefined();
71
+ });
72
+ it('--no-test --esm: jest フィールドが追加されない', () => {
73
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, esm: true, test: false }, 20);
74
+ expect(result.jest).toBeUndefined();
75
+ });
76
+ it('homepage が空文字: homepage フィールドが追加されない', () => {
77
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, homepage: '' }, 20);
78
+ expect(result.homepage).toBeUndefined();
79
+ });
80
+ it('homepage が指定済み: homepage フィールドが追加される', () => {
81
+ const result = patchPackageJson(BASE_PACKAGE_JSON, { ...baseOptions, homepage: 'https://example.com' }, 20);
82
+ expect(result.homepage).toBe('https://example.com');
83
+ });
84
+ });
85
+ // ---- patchTsConfig ----
86
+ describe('patchTsConfig', () => {
87
+ it('CJS(デフォルト): module が "commonjs" のまま', () => {
88
+ const result = patchTsConfig(BASE_TSCONFIG, { esm: false, test: false });
89
+ const opts = result.compilerOptions;
90
+ expect(opts.module).toBe('commonjs');
91
+ expect(opts.moduleResolution).toBe('bundler');
92
+ });
93
+ it('ESM: module が "es2015" になり moduleResolution は "bundler" のまま', () => {
94
+ const result = patchTsConfig(BASE_TSCONFIG, { esm: true, test: false });
95
+ const opts = result.compilerOptions;
96
+ expect(opts.module).toBe('es2015');
97
+ expect(opts.moduleResolution).toBe('bundler');
98
+ });
99
+ it('--test: types に "jest" が追加される', () => {
100
+ const result = patchTsConfig(BASE_TSCONFIG, { esm: false, test: true });
101
+ const types = result.compilerOptions.types;
102
+ expect(types).toContain('node');
103
+ expect(types).toContain('jest');
104
+ });
105
+ it('--no-test: types が ["node"] のまま', () => {
106
+ const result = patchTsConfig(BASE_TSCONFIG, { esm: false, test: false });
107
+ const types = result.compilerOptions.types;
108
+ expect(types).toEqual(['node']);
109
+ });
110
+ });
111
+ // ---- patchDockerWorkflow ----
112
+ describe('patchDockerWorkflow', () => {
113
+ const DOCKER_YML = `
114
+ targets: >-
115
+ [
116
+ { imageName: "tomacheese/twitter-dm-memo", context: ".", file: "Dockerfile", packageName: "twitter-dm-memo" }
117
+ ]
118
+ `.trim();
119
+ it('imageName が正しく置換される', () => {
120
+ const result = patchDockerWorkflow(DOCKER_YML, 'book000', 'my-app');
121
+ expect(result).toContain('book000/my-app');
122
+ expect(result).not.toContain('tomacheese/twitter-dm-memo');
123
+ });
124
+ it('org が大文字でも GHCR パスが小文字化される', () => {
125
+ const result = patchDockerWorkflow(DOCKER_YML, 'Book000', 'my-app');
126
+ expect(result).toContain('book000/my-app');
127
+ expect(result).not.toContain('Book000');
128
+ });
129
+ it('packageName が正しく置換される', () => {
130
+ const result = patchDockerWorkflow(DOCKER_YML, 'book000', 'my-app');
131
+ expect(result).toContain('packageName: "my-app"');
132
+ expect(result).not.toContain('packageName: "twitter-dm-memo"');
133
+ });
134
+ });
135
+ // ---- generateGitignore ----
136
+ describe('generateGitignore', () => {
137
+ it('基本: pnpm セクションが含まれる', () => {
138
+ const result = generateGitignore(false);
139
+ expect(result).toContain('node_modules/');
140
+ expect(result).toContain('# pnpm');
141
+ expect(result).toContain('pnpm-debug.log*');
142
+ expect(result).not.toContain('data/');
143
+ });
144
+ it('--ignore-data: さらに data/ セクションが追記される', () => {
145
+ const result = generateGitignore(true);
146
+ expect(result).toContain('# pnpm');
147
+ expect(result).toContain('# データディレクトリ');
148
+ expect(result).toContain('data/');
149
+ });
150
+ });
151
+ // ---- updateDepcheck ----
152
+ describe('updateDepcheck', () => {
153
+ const BASE_DEPCHECK = {
154
+ ignores: ['@types/node', 'run-z'],
155
+ };
156
+ it('depcheckIgnore のパッケージが ignores に追加される', () => {
157
+ const result = updateDepcheck(BASE_DEPCHECK, ['typescript-json-schema'], false);
158
+ expect(result.ignores).toContain('typescript-json-schema');
159
+ });
160
+ it('--test: @types/jest が ignores に追加される', () => {
161
+ const result = updateDepcheck(BASE_DEPCHECK, [], true);
162
+ expect(result.ignores).toContain('@types/jest');
163
+ });
164
+ it('同一パッケージを 2 回渡しても重複しない', () => {
165
+ const result = updateDepcheck(BASE_DEPCHECK, ['typescript-json-schema', 'typescript-json-schema'], false);
166
+ const ignores = result.ignores;
167
+ const count = ignores.filter((i) => i === 'typescript-json-schema').length;
168
+ expect(count).toBe(1);
169
+ });
170
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { validateLicense, validateOrgName, validateProjectName, validateRepoName, } from '../src/validate.js';
3
+ describe('validateProjectName', () => {
4
+ it('my-app → 有効', () => {
5
+ expect(validateProjectName('my-app')).toBeUndefined();
6
+ });
7
+ it('MyApp → 無効(大文字)', () => {
8
+ expect(validateProjectName('MyApp')).toBeDefined();
9
+ });
10
+ it('-bad-start → 無効(ハイフン始まり)', () => {
11
+ expect(validateProjectName('-bad-start')).toBeDefined();
12
+ });
13
+ it('"has space" → 無効(空白)', () => {
14
+ expect(validateProjectName('has space')).toBeDefined();
15
+ });
16
+ it(`${'a'.repeat(215)} → 無効(215 文字)`, () => {
17
+ expect(validateProjectName('a'.repeat(215))).toBeDefined();
18
+ });
19
+ it(`${'a'.repeat(214)} → 有効(214 文字)`, () => {
20
+ expect(validateProjectName('a'.repeat(214))).toBeUndefined();
21
+ });
22
+ it('空文字 → 無効(必須)', () => {
23
+ expect(validateProjectName('')).toBeDefined();
24
+ });
25
+ it('undefined → 無効(必須)', () => {
26
+ expect(validateProjectName(undefined)).toBeDefined();
27
+ });
28
+ });
29
+ describe('validateOrgName', () => {
30
+ it('book000 → 有効', () => {
31
+ expect(validateOrgName('book000')).toBeUndefined();
32
+ });
33
+ it('Book.000 → 有効(大文字・ドット許容)', () => {
34
+ expect(validateOrgName('Book.000')).toBeUndefined();
35
+ });
36
+ it('"bad org!" → 無効(空白・記号)', () => {
37
+ expect(validateOrgName('bad org!')).toBeDefined();
38
+ });
39
+ it('空文字 → 無効(必須)', () => {
40
+ expect(validateOrgName('')).toBeDefined();
41
+ });
42
+ });
43
+ describe('validateRepoName', () => {
44
+ it('my-repo → 有効', () => {
45
+ expect(validateRepoName('my-repo')).toBeUndefined();
46
+ });
47
+ it('_private → 有効(アンダースコア始まり)', () => {
48
+ expect(validateRepoName('_private')).toBeUndefined();
49
+ });
50
+ it('-bad → 無効(ハイフン始まり)', () => {
51
+ expect(validateRepoName('-bad')).toBeDefined();
52
+ });
53
+ it('空文字 → 無効(必須)', () => {
54
+ expect(validateRepoName('')).toBeDefined();
55
+ });
56
+ });
57
+ describe('validateLicense', () => {
58
+ it('MIT → 有効', () => {
59
+ expect(validateLicense('MIT')).toBeUndefined();
60
+ });
61
+ it('Apache-2.0 → 有効', () => {
62
+ expect(validateLicense('Apache-2.0')).toBeUndefined();
63
+ });
64
+ it('"MIT License" → 無効(空白)', () => {
65
+ expect(validateLicense('MIT License')).toBeDefined();
66
+ });
67
+ it('空文字 → 無効(必須)', () => {
68
+ expect(validateLicense('')).toBeDefined();
69
+ });
70
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("tsdown").UserConfig;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsdown';
2
+ export default defineConfig({
3
+ entry: ['src/index.ts'],
4
+ format: 'esm',
5
+ target: 'node20',
6
+ clean: true,
7
+ });
8
+ // 注意: src/index.ts の先頭行に `#!/usr/bin/env node` を記載すること。
9
+ // tsdown がシェバンを検出し、出力ファイルへの保持と chmod +x を自動で行う(banner オプション不要)。
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ export default defineConfig({
3
+ test: {
4
+ include: ['test/**/*.test.ts'],
5
+ exclude: ['templates/**', 'dist/**', 'node_modules/**'],
6
+ },
7
+ });
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@book000/create-ts",
3
- "version": "0.0.0",
3
+ "version": "0.1.6",
4
4
  "description": "Create book000-style TypeScript projects",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/book000/create-ts"
8
+ },
5
9
  "license": "MIT",
6
10
  "author": "book000",
7
11
  "type": "module",