cf-yoyo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.eslintrc.json +28 -0
  2. package/.github/workflows/ci.yml +96 -0
  3. package/.prettierrc.json +10 -0
  4. package/CHANGELOG.md +55 -0
  5. package/README.md +138 -0
  6. package/__tests__/cli-e2e.test.ts +145 -0
  7. package/__tests__/config.test.ts +268 -0
  8. package/__tests__/filesystem.test.ts +453 -0
  9. package/__tests__/logger.test.ts +274 -0
  10. package/__tests__/template-engine.test.ts +450 -0
  11. package/__tests__/types.test.ts +25 -0
  12. package/deep_todos.md +766 -0
  13. package/dist/cli/commands/create.d.ts +26 -0
  14. package/dist/cli/commands/create.d.ts.map +1 -0
  15. package/dist/cli/commands/create.js +308 -0
  16. package/dist/cli/commands/create.js.map +1 -0
  17. package/dist/cli/commands/git.d.ts +10 -0
  18. package/dist/cli/commands/git.d.ts.map +1 -0
  19. package/dist/cli/commands/git.js +887 -0
  20. package/dist/cli/commands/git.js.map +1 -0
  21. package/dist/cli/commands/list.d.ts +10 -0
  22. package/dist/cli/commands/list.d.ts.map +1 -0
  23. package/dist/cli/commands/list.js +90 -0
  24. package/dist/cli/commands/list.js.map +1 -0
  25. package/dist/cli/index.d.ts +15 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +62 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/core/config.d.ts +35 -0
  30. package/dist/core/config.d.ts.map +1 -0
  31. package/dist/core/config.js +260 -0
  32. package/dist/core/config.js.map +1 -0
  33. package/dist/core/filesystem.d.ts +84 -0
  34. package/dist/core/filesystem.d.ts.map +1 -0
  35. package/dist/core/filesystem.js +417 -0
  36. package/dist/core/filesystem.js.map +1 -0
  37. package/dist/core/git-token.d.ts +81 -0
  38. package/dist/core/git-token.d.ts.map +1 -0
  39. package/dist/core/git-token.js +244 -0
  40. package/dist/core/git-token.js.map +1 -0
  41. package/dist/core/git.d.ts +70 -0
  42. package/dist/core/git.d.ts.map +1 -0
  43. package/dist/core/git.js +367 -0
  44. package/dist/core/git.js.map +1 -0
  45. package/dist/core/prompt.d.ts +28 -0
  46. package/dist/core/prompt.d.ts.map +1 -0
  47. package/dist/core/prompt.js +253 -0
  48. package/dist/core/prompt.js.map +1 -0
  49. package/dist/core/template-engine.d.ts +52 -0
  50. package/dist/core/template-engine.d.ts.map +1 -0
  51. package/dist/core/template-engine.js +308 -0
  52. package/dist/core/template-engine.js.map +1 -0
  53. package/dist/core/template-manager.d.ts +54 -0
  54. package/dist/core/template-manager.d.ts.map +1 -0
  55. package/dist/core/template-manager.js +330 -0
  56. package/dist/core/template-manager.js.map +1 -0
  57. package/dist/index.d.ts +12 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +19 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/types/index.d.ts +244 -0
  62. package/dist/types/index.d.ts.map +1 -0
  63. package/dist/types/index.js +51 -0
  64. package/dist/types/index.js.map +1 -0
  65. package/dist/utils/logger.d.ts +68 -0
  66. package/dist/utils/logger.d.ts.map +1 -0
  67. package/dist/utils/logger.js +140 -0
  68. package/dist/utils/logger.js.map +1 -0
  69. package/memory.md +241 -0
  70. package/need-debug.md +395 -0
  71. package/package.json +42 -0
  72. package/src/cli/commands/create.ts +326 -0
  73. package/src/cli/commands/git.ts +1001 -0
  74. package/src/cli/commands/list.ts +97 -0
  75. package/src/cli/index.ts +71 -0
  76. package/src/core/config.ts +262 -0
  77. package/src/core/filesystem.ts +408 -0
  78. package/src/core/git-token.ts +248 -0
  79. package/src/core/git.ts +384 -0
  80. package/src/core/prompt.ts +345 -0
  81. package/src/core/template-engine.ts +324 -0
  82. package/src/core/template-manager.ts +338 -0
  83. package/src/index.ts +19 -0
  84. package/src/types/index.ts +259 -0
  85. package/src/utils/logger.ts +150 -0
  86. package/templates/pages/basic/README.md.mustache +63 -0
  87. package/templates/pages/basic/package.json.mustache +23 -0
  88. package/templates/pages/basic/public/css/style.css +199 -0
  89. package/templates/pages/basic/public/index.html.mustache +72 -0
  90. package/templates/pages/basic/public/js/main.js +103 -0
  91. package/templates/pages/basic/template.json +38 -0
  92. package/templates/pages/basic/tsconfig.json +21 -0
  93. package/templates/pages/basic/wrangler.toml.mustache +14 -0
  94. package/templates/pages/basic-js/README.md.mustache +62 -0
  95. package/templates/pages/basic-js/package.json.mustache +25 -0
  96. package/templates/pages/basic-js/public/css/style.css +212 -0
  97. package/templates/pages/basic-js/public/index.html.mustache +53 -0
  98. package/templates/pages/basic-js/public/js/main.js +134 -0
  99. package/templates/pages/basic-js/template.json +35 -0
  100. package/templates/pages/basic-js/wrangler.toml.mustache +14 -0
  101. package/templates/pages/react/README.md.mustache +97 -0
  102. package/templates/pages/react/index.html.mustache +14 -0
  103. package/templates/pages/react/package.json.mustache +34 -0
  104. package/templates/pages/react/src/App.css +168 -0
  105. package/templates/pages/react/src/App.tsx.mustache +62 -0
  106. package/templates/pages/react/src/index.css +53 -0
  107. package/templates/pages/react/src/main.tsx.mustache +10 -0
  108. package/templates/pages/react/src/vite-env.d.ts +1 -0
  109. package/templates/pages/react/template.json +54 -0
  110. package/templates/pages/react/tsconfig.json +21 -0
  111. package/templates/pages/react/tsconfig.node.json +10 -0
  112. package/templates/pages/react/vite.config.ts +16 -0
  113. package/templates/worker/basic/README.md.mustache +56 -0
  114. package/templates/worker/basic/package.json.mustache +29 -0
  115. package/templates/worker/basic/src/index.ts.mustache +125 -0
  116. package/templates/worker/basic/template.json +30 -0
  117. package/templates/worker/basic/tsconfig.json +24 -0
  118. package/templates/worker/basic/wrangler.toml.mustache +33 -0
  119. package/templates/worker/basic-js/README.md.mustache +55 -0
  120. package/templates/worker/basic-js/package.json.mustache +25 -0
  121. package/templates/worker/basic-js/src/index.js.mustache +146 -0
  122. package/templates/worker/basic-js/template.json +27 -0
  123. package/templates/worker/basic-js/wrangler.toml.mustache +33 -0
  124. package/templates/worker/hono/README.md.mustache +79 -0
  125. package/templates/worker/hono/package.json.mustache +33 -0
  126. package/templates/worker/hono/src/index.ts.mustache +64 -0
  127. package/templates/worker/hono/src/routes/index.ts.mustache +165 -0
  128. package/templates/worker/hono/template.json +34 -0
  129. package/templates/worker/hono/tsconfig.json +24 -0
  130. package/templates/worker/hono/wrangler.toml.mustache +36 -0
  131. package/templates/worker/hono-js/README.md.mustache +67 -0
  132. package/templates/worker/hono-js/package.json.mustache +29 -0
  133. package/templates/worker/hono-js/src/index.js.mustache +55 -0
  134. package/templates/worker/hono-js/src/routes/index.js.mustache +127 -0
  135. package/templates/worker/hono-js/template.json +31 -0
  136. package/templates/worker/hono-js/wrangler.toml.mustache +36 -0
  137. package/thoughts/ledgers/CONTINUITY_ses_287e.md +74 -0
  138. package/thoughts/ledgers/CONTINUITY_ses_28b5.md +85 -0
  139. package/tsconfig.json +30 -0
  140. package/vitest.config.ts +20 -0
  141. package//351/240/205/347/233/256/350/241/250.md +140 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * 模板引擎模組
3
+ * 負責模板的渲染、變數替換與條件渲染
4
+ *
5
+ * 使用 Mustache 語法:
6
+ * - {{variable}} - 變數替換
7
+ * - {{#condition}}...{{/condition}} - 條件渲染(條件為 truthy 時顯示)
8
+ * - {{^condition}}...{{/condition}} - 反向條件渲染(條件為 falsy 時顯示)
9
+ * - {{!comment}} - 註解(不輸出)
10
+ */
11
+
12
+ import * as path from 'path';
13
+ import {
14
+ TemplateContext,
15
+ TemplateConfig,
16
+ TemplateFile,
17
+ TemplateRenderResult,
18
+ RenderOptions,
19
+ } from '../types';
20
+ import { readFile, fileExists, createDirectory, writeFile } from './filesystem';
21
+ import { logger } from '../utils/logger';
22
+
23
+ /**
24
+ * 變數替換正則表達式
25
+ * 支援:{{variable}}、{{#condition}}、{{^condition}}、{{!comment}}
26
+ */
27
+ const VARIABLE_REGEX = /\{\{([#^!]?)\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}\}/g;
28
+
29
+ /**
30
+ * 檔案名稱中的變數替換正則
31
+ * 例如:__projectName__.ts
32
+ */
33
+ const FILENAME_VARIABLE_REGEX = /__([a-zA-Z_$][a-zA-Z0-9_$]*)__/g;
34
+
35
+ /**
36
+ * 渲染模板字符串
37
+ * 支援變數替換與條件渲染
38
+ */
39
+ export function renderTemplate(
40
+ template: string,
41
+ context: TemplateContext,
42
+ options?: RenderOptions
43
+ ): string {
44
+ const { escapeHtml = true } = options || {};
45
+
46
+ if (!template) {
47
+ return '';
48
+ }
49
+
50
+ let result = template;
51
+
52
+ // 處理條件區塊(支援巢狀)
53
+ // 先處理正向條件 {{#condition}}content{{/condition}}
54
+ result = renderSections(result, context, true);
55
+
56
+ // 處理反向條件 {{^condition}}content{{/condition}}
57
+ result = renderSections(result, context, false);
58
+
59
+ // 處理普通變數替換
60
+ result = result.replace(VARIABLE_REGEX, (match, prefix, name) => {
61
+ // 跳過條件和註解前綴
62
+ if (prefix === '#' || prefix === '^' || prefix === '!') {
63
+ return match;
64
+ }
65
+
66
+ const value = getNestedValue(context, name);
67
+ const stringValue = value !== undefined && value !== null ? String(value) : '';
68
+
69
+ return escapeHtml ? escapeHtmlEntities(stringValue) : stringValue;
70
+ });
71
+
72
+ return result;
73
+ }
74
+
75
+ /**
76
+ * 渲染條件區塊
77
+ * @param isSection - true 表示正向條件({{#var}}),false 表示反向條件({{^var}})
78
+ */
79
+ function renderSections(template: string, context: TemplateContext, isSection: boolean): string {
80
+ let result = template;
81
+ const regex = isSection
82
+ ? /\{\{#\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}\}([\s\S]*?)\{\{\/\s*\1\s*\}\}/g
83
+ : /\{\{\^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}\}([\s\S]*?)\{\{\/\s*\1\s*\}\}/g;
84
+
85
+ // 需要重複處理,因為可能存在巢狀
86
+ let lastResult = '';
87
+ let iterations = 0;
88
+ const MAX_ITERATIONS = 100; // 防止無限循環
89
+
90
+ while (result !== lastResult && iterations < MAX_ITERATIONS) {
91
+ lastResult = result;
92
+ iterations++;
93
+
94
+ result = result.replace(regex, (match, name, content) => {
95
+ const value = getNestedValue(context, name);
96
+ const isTruthy = isTruthyValue(value);
97
+
98
+ if (isSection) {
99
+ // 正向條件:值為 truthy 時顯示內容
100
+ return isTruthy ? content : '';
101
+ } else {
102
+ // 反向條件:值為 falsy 時顯示內容
103
+ return !isTruthy ? content : '';
104
+ }
105
+ });
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * 檢查值是否為 truthy
113
+ * 陣列會檢查長度,物件會檢查是否有鍵
114
+ */
115
+ function isTruthyValue(value: unknown): boolean {
116
+ if (value === undefined || value === null) {
117
+ return false;
118
+ }
119
+ if (typeof value === 'boolean') {
120
+ return value;
121
+ }
122
+ if (typeof value === 'number') {
123
+ return value !== 0;
124
+ }
125
+ if (typeof value === 'string') {
126
+ return value.length > 0;
127
+ }
128
+ if (Array.isArray(value)) {
129
+ return value.length > 0;
130
+ }
131
+ if (typeof value === 'object' && value !== null) {
132
+ return Object.keys(value).length > 0;
133
+ }
134
+ return Boolean(value);
135
+ }
136
+
137
+ /**
138
+ * 獲取巢狀物件值
139
+ * 支援點號表示法,例如:user.name
140
+ */
141
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
142
+ const keys = path.split('.');
143
+ let value: unknown = obj;
144
+
145
+ for (const key of keys) {
146
+ if (value === null || value === undefined) {
147
+ return undefined;
148
+ }
149
+ if (typeof value === 'object') {
150
+ value = (value as Record<string, unknown>)[key];
151
+ } else {
152
+ return undefined;
153
+ }
154
+ }
155
+
156
+ return value;
157
+ }
158
+
159
+ /**
160
+ * 轉義 HTML 實體
161
+ */
162
+ function escapeHtmlEntities(text: string): string {
163
+ const htmlEntities: Record<string, string> = {
164
+ '&': '&amp;',
165
+ '<': '&lt;',
166
+ '>': '&gt;',
167
+ '"': '&quot;',
168
+ "'": '&#x27;',
169
+ };
170
+
171
+ return text.replace(/[&<>"']/g, (char) => htmlEntities[char] || char);
172
+ }
173
+
174
+ /**
175
+ * 處理檔案名稱中的變數替換
176
+ * 將 __variable__ 替換為實際值
177
+ */
178
+ export function renderFileName(fileName: string, context: TemplateContext): string {
179
+ return fileName.replace(FILENAME_VARIABLE_REGEX, (match, name) => {
180
+ const value = getNestedValue(context, name);
181
+ return value !== undefined && value !== null ? String(value) : match;
182
+ });
183
+ }
184
+
185
+ /**
186
+ * 從模板配置生成完整的渲染結果
187
+ */
188
+ export async function renderTemplateToFiles(
189
+ templateConfig: TemplateConfig,
190
+ targetDir: string,
191
+ context: TemplateContext
192
+ ): Promise<TemplateRenderResult> {
193
+ const result: TemplateRenderResult = {
194
+ success: true,
195
+ files: [],
196
+ errors: [],
197
+ };
198
+
199
+ logger.debug(`開始渲染模板:${templateConfig.name}`);
200
+ logger.debug(`目標目錄:${targetDir}`);
201
+ logger.debug(`渲染上下文:${JSON.stringify(context, null, 2)}`);
202
+
203
+ for (const file of templateConfig.files) {
204
+ try {
205
+ // 渲染目標路徑
206
+ const renderedPath = renderFileName(file.targetPath, context);
207
+ const fullTargetPath = path.join(targetDir, renderedPath);
208
+
209
+ // 確保目標目錄存在
210
+ const targetFileDir = path.dirname(fullTargetPath);
211
+ createDirectory(targetFileDir);
212
+
213
+ let content: string;
214
+
215
+ if (file.sourcePath) {
216
+ // 從檔案讀取模板
217
+ const templateContent = readFile(file.sourcePath);
218
+ if (templateContent === null) {
219
+ throw new Error(`無法讀取模板檔案:${file.sourcePath}`);
220
+ }
221
+ content = renderTemplate(templateContent, context, {
222
+ escapeHtml: false,
223
+ });
224
+ } else if (file.content) {
225
+ // 使用內嵌內容
226
+ content = renderTemplate(file.content, context, { escapeHtml: false });
227
+ } else {
228
+ throw new Error(`模板檔案 ${file.targetPath} 沒有來源內容`);
229
+ }
230
+
231
+ // 寫入檔案
232
+ if (writeFile(fullTargetPath, content, { overwrite: true })) {
233
+ result.files.push(fullTargetPath);
234
+ logger.debug(`已生成檔案:${fullTargetPath}`);
235
+ } else {
236
+ throw new Error(`無法寫入檔案:${fullTargetPath}`);
237
+ }
238
+ } catch (error: unknown) {
239
+ const errorMessage = error instanceof Error ? error.message : String(error);
240
+ result.errors.push({
241
+ file: file.targetPath,
242
+ error: errorMessage,
243
+ });
244
+ logger.error(`渲染檔案失敗 ${file.targetPath}:${errorMessage}`);
245
+ }
246
+ }
247
+
248
+ if (result.errors.length > 0 && result.files.length === 0) {
249
+ result.success = false;
250
+ }
251
+
252
+ return result;
253
+ }
254
+
255
+ /**
256
+ * 驗證模板上下文
257
+ * 檢查是否包含所有必需的變數
258
+ */
259
+ export function validateContext(
260
+ context: TemplateContext,
261
+ requiredVars: string[]
262
+ ): { valid: boolean; missing: string[] } {
263
+ const missing: string[] = [];
264
+
265
+ for (const varName of requiredVars) {
266
+ const value = getNestedValue(context, varName);
267
+ if (value === undefined || value === null) {
268
+ missing.push(varName);
269
+ }
270
+ }
271
+
272
+ return {
273
+ valid: missing.length === 0,
274
+ missing,
275
+ };
276
+ }
277
+
278
+ /**
279
+ * 生成預設的模板上下文
280
+ * 基於專案配置生成預設值
281
+ */
282
+ export function generateDefaultContext(projectConfig: {
283
+ projectName: string;
284
+ projectType: string;
285
+ template: string;
286
+ language?: string;
287
+ }): TemplateContext {
288
+ const now = new Date();
289
+
290
+ return {
291
+ projectName: projectConfig.projectName,
292
+ projectType: projectConfig.projectType,
293
+ template: projectConfig.template,
294
+ // 基本資訊
295
+ author: '', // 可由用戶提供或從 git 配置讀取
296
+ email: '',
297
+ description: `${projectConfig.projectName} - Cloudflare ${projectConfig.projectType} 專案`,
298
+ version: '1.0.0',
299
+ license: 'MIT',
300
+ // 時間戳
301
+ year: now.getFullYear(),
302
+ date: now.toISOString().split('T')[0] || '',
303
+ timestamp: now.toISOString(),
304
+ // Cloudflare 相關
305
+ isWorker: projectConfig.projectType === 'worker' || projectConfig.projectType === 'hono',
306
+ isPages: projectConfig.projectType === 'pages',
307
+ isD1: projectConfig.projectType === 'd1',
308
+ isKV: projectConfig.projectType === 'kv',
309
+ isR2: projectConfig.projectType === 'r2',
310
+ // 框架相關
311
+ isHono: projectConfig.template === 'hono',
312
+ isReact: projectConfig.template === 'react',
313
+ isVue: projectConfig.template === 'vue',
314
+ isNextjs: projectConfig.template === 'nextjs',
315
+ };
316
+ }
317
+
318
+ export default {
319
+ renderTemplate,
320
+ renderFileName,
321
+ renderTemplateToFiles,
322
+ validateContext,
323
+ generateDefaultContext,
324
+ };
@@ -0,0 +1,338 @@
1
+ /**
2
+ * 模板管理器模組
3
+ * 負責模板的載入、查找與驗證
4
+ */
5
+
6
+ import * as path from 'path';
7
+ import {
8
+ TemplateConfig,
9
+ TemplateInfo,
10
+ ProjectType,
11
+ TemplateType,
12
+ TemplateFile,
13
+ Language,
14
+ } from '../types';
15
+ import { directoryExists, readFile, listDirectory, fileExists } from './filesystem';
16
+ import { logger } from '../utils/logger';
17
+
18
+ /**
19
+ * 模板基礎路徑
20
+ * 指向專案中的 templates 目錄
21
+ */
22
+ const TEMPLATES_BASE_PATH = path.join(__dirname, '../../templates');
23
+
24
+ /**
25
+ * 模板配置檔名稱
26
+ */
27
+ const TEMPLATE_CONFIG_FILE = 'template.json';
28
+
29
+ /**
30
+ * 可用的模板類型映射
31
+ * 根據專案類型和語言分類
32
+ */
33
+ const AVAILABLE_TEMPLATES: Record<
34
+ ProjectType,
35
+ Record<Language, Array<{ type: TemplateType; name: string; description: string }>>
36
+ > = {
37
+ [ProjectType.WORKER]: {
38
+ [Language.TYPESCRIPT]: [
39
+ {
40
+ type: TemplateType.BASIC,
41
+ name: '基礎 Worker',
42
+ description: '最簡單的 Cloudflare Worker 專案,使用原生 API',
43
+ },
44
+ {
45
+ type: TemplateType.HONO,
46
+ name: 'Hono Worker',
47
+ description: '使用 Hono 框架的 Cloudflare Worker 專案',
48
+ },
49
+ {
50
+ type: TemplateType.ITTY_ROUTER,
51
+ name: 'itty-router Worker',
52
+ description: '使用 itty-router 路由庫的 Worker 專案',
53
+ },
54
+ ],
55
+ [Language.JAVASCRIPT]: [
56
+ {
57
+ type: TemplateType.BASIC,
58
+ name: '基礎 Worker (JavaScript)',
59
+ description: '最簡單的 Cloudflare Worker 專案,使用原生 API',
60
+ },
61
+ {
62
+ type: TemplateType.HONO,
63
+ name: 'Hono Worker (JavaScript)',
64
+ description: '使用 Hono 框架的 Cloudflare Worker 專案',
65
+ },
66
+ ],
67
+ },
68
+ [ProjectType.PAGES]: {
69
+ [Language.TYPESCRIPT]: [
70
+ {
71
+ type: TemplateType.STATIC,
72
+ name: '靜態網站',
73
+ description: '純靜態 HTML/CSS/JS 的 Pages 專案',
74
+ },
75
+ {
76
+ type: TemplateType.REACT,
77
+ name: 'React Pages',
78
+ description: '使用 React 的 Pages 專案',
79
+ },
80
+ {
81
+ type: TemplateType.VUE,
82
+ name: 'Vue Pages',
83
+ description: '使用 Vue 的 Pages 專案',
84
+ },
85
+ {
86
+ type: TemplateType.NEXTJS,
87
+ name: 'Next.js Pages',
88
+ description: '使用 Next.js 的 Pages 專案',
89
+ },
90
+ ],
91
+ [Language.JAVASCRIPT]: [
92
+ {
93
+ type: TemplateType.STATIC,
94
+ name: '靜態網站 (JavaScript)',
95
+ description: '純靜態 HTML/CSS/JS 的 Pages 專案',
96
+ },
97
+ ],
98
+ },
99
+ [ProjectType.D1]: {
100
+ [Language.TYPESCRIPT]: [
101
+ {
102
+ type: TemplateType.BASIC,
103
+ name: 'D1 基礎模板',
104
+ description: '整合 Cloudflare D1 資料庫的 Worker 專案',
105
+ },
106
+ ],
107
+ [Language.JAVASCRIPT]: [
108
+ {
109
+ type: TemplateType.BASIC,
110
+ name: 'D1 基礎模板 (JavaScript)',
111
+ description: '整合 Cloudflare D1 資料庫的 Worker 專案',
112
+ },
113
+ ],
114
+ },
115
+ [ProjectType.KV]: {
116
+ [Language.TYPESCRIPT]: [
117
+ {
118
+ type: TemplateType.BASIC,
119
+ name: 'KV 基礎模板',
120
+ description: '整合 Cloudflare KV 存儲的 Worker 專案',
121
+ },
122
+ ],
123
+ [Language.JAVASCRIPT]: [
124
+ {
125
+ type: TemplateType.BASIC,
126
+ name: 'KV 基礎模板 (JavaScript)',
127
+ description: '整合 Cloudflare KV 存儲的 Worker 專案',
128
+ },
129
+ ],
130
+ },
131
+ [ProjectType.R2]: {
132
+ [Language.TYPESCRIPT]: [
133
+ {
134
+ type: TemplateType.BASIC,
135
+ name: 'R2 基礎模板',
136
+ description: '整合 Cloudflare R2 存儲的 Worker 專案',
137
+ },
138
+ ],
139
+ [Language.JAVASCRIPT]: [
140
+ {
141
+ type: TemplateType.BASIC,
142
+ name: 'R2 基礎模板 (JavaScript)',
143
+ description: '整合 Cloudflare R2 存儲的 Worker 專案',
144
+ },
145
+ ],
146
+ },
147
+ };
148
+
149
+ /**
150
+ * 獲取指定專案類型和語言的可用模板
151
+ */
152
+ export function getTemplatesForProjectType(
153
+ projectType: ProjectType,
154
+ language: Language = Language.TYPESCRIPT
155
+ ): Array<{ type: TemplateType; name: string; description: string }> {
156
+ return AVAILABLE_TEMPLATES[projectType]?.[language] || [];
157
+ }
158
+
159
+ /**
160
+ * 獲取所有可用的模板資訊
161
+ * 包含所有語言和專案類型的模板
162
+ */
163
+ export function getAllTemplates(): TemplateInfo[] {
164
+ const templates: TemplateInfo[] = [];
165
+
166
+ for (const [projectType, langTemplates] of Object.entries(AVAILABLE_TEMPLATES)) {
167
+ for (const [language, templateList] of Object.entries(langTemplates)) {
168
+ for (const template of templateList) {
169
+ templates.push({
170
+ id: `${projectType}-${language}-${template.type}`,
171
+ name: template.name,
172
+ description: template.description,
173
+ projectType: projectType as ProjectType,
174
+ language: language as Language,
175
+ path: getTemplatePath(projectType as ProjectType, template.type, language as Language),
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ return templates;
182
+ }
183
+
184
+ /**
185
+ * 獲取模板路徑
186
+ * 根據專案類型、模板類型和語言構建路徑
187
+ */
188
+ export function getTemplatePath(
189
+ projectType: ProjectType,
190
+ templateType: TemplateType,
191
+ language: Language = Language.TYPESCRIPT
192
+ ): string {
193
+ // JS 模板使用 -js 後綴區分
194
+ const langSuffix = language === Language.JAVASCRIPT ? '-js' : '';
195
+ return path.join(TEMPLATES_BASE_PATH, projectType, `${templateType}${langSuffix}`);
196
+ }
197
+
198
+ /**
199
+ * 驗證模板是否存在且完整
200
+ */
201
+ export function validateTemplate(
202
+ projectType: ProjectType,
203
+ templateType: TemplateType,
204
+ language: Language = Language.TYPESCRIPT
205
+ ): { valid: boolean; error?: string } {
206
+ const templatePath = getTemplatePath(projectType, templateType, language);
207
+
208
+ // 檢查模板目錄是否存在
209
+ if (!directoryExists(templatePath)) {
210
+ return {
211
+ valid: false,
212
+ error: `模板目錄不存在:${templatePath}`,
213
+ };
214
+ }
215
+
216
+ // 檢查模板配置檔
217
+ const configPath = path.join(templatePath, TEMPLATE_CONFIG_FILE);
218
+ if (!fileExists(configPath)) {
219
+ return {
220
+ valid: false,
221
+ error: `模板配置檔不存在:${configPath}`,
222
+ };
223
+ }
224
+
225
+ // 嘗試解析配置
226
+ try {
227
+ const configContent = readFile(configPath);
228
+ if (!configContent) {
229
+ return {
230
+ valid: false,
231
+ error: `無法讀取模板配置檔:${configPath}`,
232
+ };
233
+ }
234
+
235
+ const config = JSON.parse(configContent) as TemplateConfig;
236
+
237
+ // 檢查必需的檔案是否存在
238
+ for (const file of config.files) {
239
+ if (file.sourcePath) {
240
+ const fullSourcePath = path.join(templatePath, file.sourcePath);
241
+ if (!fileExists(fullSourcePath)) {
242
+ return {
243
+ valid: false,
244
+ error: `模板檔案不存在:${fullSourcePath}`,
245
+ };
246
+ }
247
+ }
248
+ }
249
+
250
+ return { valid: true };
251
+ } catch (error: unknown) {
252
+ const errorMessage = error instanceof Error ? error.message : String(error);
253
+ return {
254
+ valid: false,
255
+ error: `模板配置檔解析失敗:${errorMessage}`,
256
+ };
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 載入模板配置
262
+ */
263
+ export function loadTemplate(
264
+ projectType: ProjectType,
265
+ templateType: TemplateType,
266
+ language: Language = Language.TYPESCRIPT
267
+ ): TemplateConfig | null {
268
+ const templatePath = getTemplatePath(projectType, templateType, language);
269
+ const configPath = path.join(templatePath, TEMPLATE_CONFIG_FILE);
270
+
271
+ try {
272
+ if (!fileExists(configPath)) {
273
+ logger.error(`模板配置檔不存在:${configPath}`);
274
+ return null;
275
+ }
276
+
277
+ const configContent = readFile(configPath);
278
+ if (!configContent) {
279
+ logger.error(`無法讀取模板配置檔:${configPath}`);
280
+ return null;
281
+ }
282
+
283
+ const config = JSON.parse(configContent) as TemplateConfig;
284
+
285
+ // 解析檔案的完整路徑
286
+ config.files = config.files.map((file: TemplateFile) => ({
287
+ ...file,
288
+ sourcePath: file.sourcePath ? path.join(templatePath, file.sourcePath) : undefined,
289
+ }));
290
+
291
+ logger.debug(`已載入模板:${config.name}`);
292
+ return config;
293
+ } catch (error: unknown) {
294
+ const errorMessage = error instanceof Error ? error.message : String(error);
295
+ logger.error(`載入模板失敗:${errorMessage}`);
296
+ return null;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * 獲取預設模板類型
302
+ * 根據專案類型返回預設的模板
303
+ */
304
+ export function getDefaultTemplateType(projectType: ProjectType): TemplateType {
305
+ switch (projectType) {
306
+ case ProjectType.WORKER:
307
+ return TemplateType.BASIC;
308
+ case ProjectType.PAGES:
309
+ return TemplateType.STATIC;
310
+ case ProjectType.D1:
311
+ case ProjectType.KV:
312
+ case ProjectType.R2:
313
+ return TemplateType.BASIC;
314
+ default:
315
+ return TemplateType.BASIC;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * 檢查模板類型是否適用於指定專案類型
321
+ */
322
+ export function isTemplateCompatible(
323
+ projectType: ProjectType,
324
+ templateType: TemplateType
325
+ ): boolean {
326
+ const templates = getTemplatesForProjectType(projectType);
327
+ return templates.some((t) => t.type === templateType);
328
+ }
329
+
330
+ export default {
331
+ getTemplatesForProjectType,
332
+ getAllTemplates,
333
+ getTemplatePath,
334
+ validateTemplate,
335
+ loadTemplate,
336
+ getDefaultTemplateType,
337
+ isTemplateCompatible,
338
+ };
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-cf-project CLI 工具主入口
5
+ *
6
+ * 使用方式:
7
+ * npx create-cf-project create [project-name] [options]
8
+ * npx create-cf-project list
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ import { runCLI } from './cli';
14
+
15
+ // 執行 CLI
16
+ runCLI().catch((error) => {
17
+ console.error('執行 CLI 時發生錯誤:', error);
18
+ process.exit(1);
19
+ });