birc-generator 0.5.5
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/.bircrc.example.json +14 -0
- package/AGENTS.md +20 -0
- package/PROJECT.md +83 -0
- package/README.md +300 -0
- package/bin/birc.js +645 -0
- package/bin/cli-util.js +428 -0
- package/bin/home-install.js +346 -0
- package/bin/update-check.js +153 -0
- package/implement.md +76 -0
- package/install.ps1 +156 -0
- package/install.sh +201 -0
- package/package.json +68 -0
- package/plopfile.js +1358 -0
- package/project-docs/AGENTS.md.hbs +12 -0
- package/project-docs/PROJECT.md.hbs +128 -0
- package/project-docs/implement.hbs +54 -0
- package/project-docs/test.hbs +31 -0
- package/scripts/install-bin.sh +72 -0
- package/scripts/update.sh +11 -0
- package/templates/aop/OperationLog.hbs +18 -0
- package/templates/aop/OperationLogAspect.hbs +95 -0
- package/templates/aop/build-gradle-dep.hbs +1 -0
- package/templates/base/Application.hbs +11 -0
- package/templates/base/ApplicationTests.java.hbs +11 -0
- package/templates/base/BaseService.hbs +15 -0
- package/templates/base/BaseServiceImpl.hbs +53 -0
- package/templates/base/EntityMapper.hbs +12 -0
- package/templates/base/README.md.hbs +56 -0
- package/templates/base/Result.hbs +48 -0
- package/templates/base/application-test.yml.hbs +27 -0
- package/templates/base/application.yml.hbs +18 -0
- package/templates/base/bircrc.hbs +14 -0
- package/templates/base/build.gradle.hbs +33 -0
- package/templates/base/gitignore.hbs +15 -0
- package/templates/client/ExternalApiClient.hbs +28 -0
- package/templates/client/ExternalApiClientConfig.hbs +26 -0
- package/templates/client/ExternalApiProperties.hbs +30 -0
- package/templates/client/application-yml-block.hbs +7 -0
- package/templates/controller.hbs +55 -0
- package/templates/dao.hbs +11 -0
- package/templates/docker/Dockerfile.hbs +29 -0
- package/templates/docker/docker-compose.prod.yml.hbs +65 -0
- package/templates/docker/docker-compose.yml.hbs +52 -0
- package/templates/docker/dockerignore.hbs +6 -0
- package/templates/docker/env.example.hbs +24 -0
- package/templates/dto-request.hbs +19 -0
- package/templates/dto-response.hbs +13 -0
- package/templates/email/EmailAsyncConfig.hbs +28 -0
- package/templates/email/EmailService.hbs +26 -0
- package/templates/email/EmailServiceImpl.hbs +49 -0
- package/templates/email/application-yml-block.hbs +19 -0
- package/templates/email/build-gradle-dep.hbs +2 -0
- package/templates/email/sample-template.hbs +16 -0
- package/templates/entity.hbs +46 -0
- package/templates/exception/ExceptionHandleController.hbs +16 -0
- package/templates/exception/NotFoundException.hbs +28 -0
- package/templates/exception/ProjectException.hbs +20 -0
- package/templates/exception.hbs +30 -0
- package/templates/file-upload/EmptyFileException.hbs +25 -0
- package/templates/file-upload/FileExtensionIllegalException.hbs +21 -0
- package/templates/file-upload/FileExtensionUtils.hbs +20 -0
- package/templates/file-upload/FileStorageProperties.hbs +37 -0
- package/templates/file-upload/FileStorageService.hbs +15 -0
- package/templates/file-upload/FileStorageServiceImpl.hbs +78 -0
- package/templates/file-upload/FileTooLargeException.hbs +21 -0
- package/templates/file-upload/FileUploadController.hbs +57 -0
- package/templates/file-upload/FileUtils.hbs +39 -0
- package/templates/file-upload/InvalidStoredFileException.hbs +21 -0
- package/templates/file-upload/application-yml-block.hbs +12 -0
- package/templates/gitlab-ci/gitlab-ci.hbs +158 -0
- package/templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/templates/gradle-wrapper/gradlew +248 -0
- package/templates/gradle-wrapper/gradlew.bat +93 -0
- package/templates/log4j2/build-gradle-dep.hbs +6 -0
- package/templates/log4j2/log4j2-spring.xml.hbs +34 -0
- package/templates/mapper-gradle-dep.hbs +3 -0
- package/templates/mapper.hbs +11 -0
- package/templates/migration/add-column.hbs +4 -0
- package/templates/migration/create-table.hbs +9 -0
- package/templates/migration/custom.hbs +2 -0
- package/templates/migration/remove-column.hbs +4 -0
- package/templates/migration/seed.hbs +7 -0
- package/templates/multi-module/build.gradle.hbs +86 -0
- package/templates/multi-module/config/ApplicationConfig.java.hbs +33 -0
- package/templates/multi-module/config/Config.java.hbs +19 -0
- package/templates/multi-module/config/SecurityConfig.java.hbs +48 -0
- package/templates/multi-module/config/build.gradle.hbs +10 -0
- package/templates/multi-module/database-config/BaseDAO.java.hbs +31 -0
- package/templates/multi-module/database-config/BaseViewDAO.java.hbs +48 -0
- package/templates/multi-module/database-config/Config.java.hbs +14 -0
- package/templates/multi-module/database-config/JpaConfig.java.hbs +9 -0
- package/templates/multi-module/database-config/build.gradle.hbs +17 -0
- package/templates/multi-module/settings.gradle.hbs +9 -0
- package/templates/oauth/OAuth2LoginSuccessHandler.hbs +31 -0
- package/templates/oauth/application-yml-block.hbs +11 -0
- package/templates/oauth/build-gradle-dep.hbs +1 -0
- package/templates/openapi/OpenApiConfig.hbs +23 -0
- package/templates/openapi/build-gradle-dep.hbs +1 -0
- package/templates/pagination/GenericSpecification.hbs +76 -0
- package/templates/pagination/PageInfo.hbs +20 -0
- package/templates/pagination/PageRequest.hbs +27 -0
- package/templates/pagination/PageResponse.hbs +24 -0
- package/templates/pagination/Pager.hbs +37 -0
- package/templates/pagination/SearchCriteria.hbs +26 -0
- package/templates/pagination/SpecificationSupport.hbs +34 -0
- package/templates/permission/PermissionAspect.hbs +41 -0
- package/templates/permission/RequirePermission.hbs +18 -0
- package/templates/permission/build-gradle-dep.hbs +2 -0
- package/templates/scheduling/SampleSchedule.hbs +22 -0
- package/templates/scheduling/SchedulingConfig.hbs +13 -0
- package/templates/scheduling/application-yml-block.hbs +5 -0
- package/templates/sentry/SentryConfig.hbs +74 -0
- package/templates/sentry/application-yml-block.hbs +15 -0
- package/templates/sentry/build-gradle-dep.hbs +1 -0
- package/templates/service.hbs +9 -0
- package/templates/serviceImpl.hbs +23 -0
- package/templates/spotless/build-gradle-allprojects.hbs +16 -0
- package/templates/spotless/build-gradle-plugin.hbs +1 -0
- package/templates/spotless/spotless_formatter.xml +83 -0
- package/templates/sso/SsoAutoConfiguration.hbs +17 -0
- package/templates/sso/SsoProperties.hbs +27 -0
- package/templates/sso/application-yml-block.hbs +7 -0
- package/templates/valid-group/ValidGroup.hbs +20 -0
- package/test.md +56 -0
- package/uninstall.ps1 +60 -0
- package/uninstall.sh +76 -0
- package/versions.js +10 -0
package/bin/birc.js
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const BANNER = `
|
|
8
|
+
██████╗ ██╗██████╗ ██████╗
|
|
9
|
+
██╔══██╗██║██╔══██╗██╔════╝
|
|
10
|
+
██████╔╝██║██████╔╝██║
|
|
11
|
+
██╔══██╗██║██╔══██╗██║
|
|
12
|
+
██████╔╝██║██║ ██║╚██████╗
|
|
13
|
+
╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═════╝
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const HELP_SECTIONS = [
|
|
17
|
+
{
|
|
18
|
+
title: '專案',
|
|
19
|
+
commands: [
|
|
20
|
+
['create', '開多模組 Spring Boot 4 專案(預設 Practice)'],
|
|
21
|
+
['add', '往既有專案加模組;可帶名稱如 birc add email sentry'],
|
|
22
|
+
['sync', '用目前 template 覆寫已安裝 feature 的檔'],
|
|
23
|
+
['update', '更新本機 birc(npm 走 npm,其餘拉 GitLab main)']
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: '骨架',
|
|
28
|
+
note: '寫法是 make:種類 [名稱]。預設只生引入與初稿;加 --example 才有完整範例',
|
|
29
|
+
commands: [
|
|
30
|
+
['make', '一次生 Entity + DAO + Mapper + DTO + Service + Controller'],
|
|
31
|
+
['make:entity', 'Entity、XxxDAO(--dto / --mapper / --soft-delete)'],
|
|
32
|
+
['make:model', '同 make:entity;--migration / --seed / --controller / --soft-delete'],
|
|
33
|
+
['make:mapper', 'Mapper、CreateRequest、Response'],
|
|
34
|
+
['make:service', 'XxxService extends BaseService'],
|
|
35
|
+
['make:controller', 'Controller'],
|
|
36
|
+
['make:exception', '繼承 ProjectException 的例外'],
|
|
37
|
+
['make:migration', 'Flyway SQL(檔名如 create_users_table)'],
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: '資料庫',
|
|
42
|
+
commands: [
|
|
43
|
+
['migrate', '套用待執行的遷移(flywayMigrate)'],
|
|
44
|
+
['migrate:reset', '清 schema 再重跑(flywayClean + migrate)'],
|
|
45
|
+
['migrate:rollback', 'Community 做不到 rollback,會印替代作法']
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: '文件',
|
|
50
|
+
commands: [
|
|
51
|
+
['docs', '補 AGENTS.md / PROJECT.md / implement.md / test.md']
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const FLYWAY_COMMANDS = new Set(['migrate', 'migrate:rollback', 'migrate:reset']);
|
|
57
|
+
|
|
58
|
+
function allCommandNames() {
|
|
59
|
+
return HELP_SECTIONS.flatMap((section) => section.commands.map(([name]) => name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const generatorRoot = path.join(__dirname, '..');
|
|
63
|
+
let projectRoot = process.cwd();
|
|
64
|
+
const pkg = require(path.join(generatorRoot, 'package.json'));
|
|
65
|
+
|
|
66
|
+
function authorLine() {
|
|
67
|
+
const author = pkg.author;
|
|
68
|
+
if (!author) return '';
|
|
69
|
+
if (typeof author === 'string') return author;
|
|
70
|
+
if (author.email) return `${author.name} <${author.email}>`;
|
|
71
|
+
return author.name || '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatDate(iso) {
|
|
75
|
+
const d = new Date(iso);
|
|
76
|
+
if (Number.isNaN(d.getTime())) return String(iso);
|
|
77
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
78
|
+
timeZone: 'Asia/Taipei',
|
|
79
|
+
year: 'numeric',
|
|
80
|
+
month: '2-digit',
|
|
81
|
+
day: '2-digit',
|
|
82
|
+
hour: '2-digit',
|
|
83
|
+
minute: '2-digit',
|
|
84
|
+
hour12: false
|
|
85
|
+
}).formatToParts(d);
|
|
86
|
+
const get = (type) => parts.find((part) => part.type === type)?.value;
|
|
87
|
+
return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lastUpdated() {
|
|
91
|
+
const gitDir = path.join(generatorRoot, '.git');
|
|
92
|
+
if (fs.existsSync(gitDir)) {
|
|
93
|
+
const result = spawnSync('git', ['log', '-1', '--format=%cI'], {
|
|
94
|
+
cwd: generatorRoot,
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
timeout: 300
|
|
97
|
+
});
|
|
98
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
99
|
+
return formatDate(result.stdout.trim());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (pkg.bircUpdatedAt) return formatDate(pkg.bircUpdatedAt);
|
|
103
|
+
return formatDate(fs.statSync(path.join(generatorRoot, 'package.json')).mtime.toISOString());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function displayWidth(text) {
|
|
107
|
+
let width = 0;
|
|
108
|
+
for (const ch of String(text)) {
|
|
109
|
+
const code = ch.codePointAt(0);
|
|
110
|
+
width += code <= 0x1f || (code >= 0x7f && code <= 0x9f)
|
|
111
|
+
? 0
|
|
112
|
+
: code >= 0x1100 && (
|
|
113
|
+
code <= 0x115f
|
|
114
|
+
|| code === 0x2329
|
|
115
|
+
|| code === 0x232a
|
|
116
|
+
|| (code >= 0x2e80 && code <= 0xa4cf)
|
|
117
|
+
|| (code >= 0xac00 && code <= 0xd7a3)
|
|
118
|
+
|| (code >= 0xf900 && code <= 0xfaff)
|
|
119
|
+
|| (code >= 0xfe10 && code <= 0xfe19)
|
|
120
|
+
|| (code >= 0xfe30 && code <= 0xfe6f)
|
|
121
|
+
|| (code >= 0xff00 && code <= 0xff60)
|
|
122
|
+
|| (code >= 0xffe0 && code <= 0xffe6)
|
|
123
|
+
)
|
|
124
|
+
? 2
|
|
125
|
+
: 1;
|
|
126
|
+
}
|
|
127
|
+
return width;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function padDisplay(text, width) {
|
|
131
|
+
const extra = width - displayWidth(text);
|
|
132
|
+
return extra > 0 ? `${text}${' '.repeat(extra)}` : text;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printBanner() {
|
|
136
|
+
const art = BANNER.trim().split('\n').map((line) => line.trimEnd());
|
|
137
|
+
const artWidth = Math.max(...art.map(displayWidth));
|
|
138
|
+
const info = [
|
|
139
|
+
'BIRC Spring Boot 4 骨架產生器',
|
|
140
|
+
`作者 ${authorLine()}`,
|
|
141
|
+
`版本 ${pkg.version}`,
|
|
142
|
+
`更新 ${lastUpdated()}`
|
|
143
|
+
];
|
|
144
|
+
const lines = art.map((line, i) => {
|
|
145
|
+
const left = padDisplay(line, artWidth);
|
|
146
|
+
return info[i] ? `${left} ${info[i]}` : left;
|
|
147
|
+
});
|
|
148
|
+
process.stdout.write(`${lines.join('\n')}\n\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printHelp() {
|
|
152
|
+
const names = HELP_SECTIONS.flatMap((section) => section.commands.map(([name]) => name));
|
|
153
|
+
const nameWidth = Math.max(...names.map((name) => name.length), 16);
|
|
154
|
+
const sections = HELP_SECTIONS.flatMap((section) => {
|
|
155
|
+
const rows = section.commands.map(
|
|
156
|
+
([name, desc]) => ` ${name.padEnd(nameWidth + 2)}${desc}`
|
|
157
|
+
);
|
|
158
|
+
return ['', section.title, ...(section.note ? [` ${section.note}`] : []), ...rows];
|
|
159
|
+
});
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
[
|
|
162
|
+
'用法:',
|
|
163
|
+
' birc <指令> [名稱] [選項]',
|
|
164
|
+
' 子指令接在冒號後面,例如 make:entity、migrate:reset。',
|
|
165
|
+
...sections,
|
|
166
|
+
'',
|
|
167
|
+
'選項',
|
|
168
|
+
` ${'-h, --help'.padEnd(nameWidth + 2)}顯示這份說明`,
|
|
169
|
+
` ${'-v, --version'.padEnd(nameWidth + 2)}顯示版本`,
|
|
170
|
+
` ${'--example'.padEnd(nameWidth + 2)}make:* 產出完整範例`,
|
|
171
|
+
` ${'--dto'.padEnd(nameWidth + 2)}make:entity 順便生 Mapper 與 DTO`,
|
|
172
|
+
` ${'--mapper'.padEnd(nameWidth + 2)}同 --dto`,
|
|
173
|
+
` ${'--migration'.padEnd(nameWidth + 2)}make:model 順便生 create_{table}_table`,
|
|
174
|
+
` ${'--seed'.padEnd(nameWidth + 2)}make:model 順便生 seed_{table}`,
|
|
175
|
+
` ${'--controller'.padEnd(nameWidth + 2)}make:model 順便生 Controller`,
|
|
176
|
+
` ${'--soft-delete'.padEnd(nameWidth + 2)}make:entity / make:model 加 deleted_at 軟刪`,
|
|
177
|
+
` ${'--fields'.padEnd(nameWidth + 2)}make --example 的欄位,例如 title:String,startTime:LocalDateTime`,
|
|
178
|
+
` ${'--force'.padEnd(nameWidth + 2)}migrate:reset 略過確認;add 覆寫已選的已安裝 feature;make 覆寫已有 Java`,
|
|
179
|
+
` ${'--yes'.padEnd(nameWidth + 2)}create 其餘用預設、不要問`,
|
|
180
|
+
'',
|
|
181
|
+
'例子',
|
|
182
|
+
' birc create',
|
|
183
|
+
' birc create Practice tw.edu.ntub.birc.practice',
|
|
184
|
+
' birc create Practice --yes',
|
|
185
|
+
' birc update',
|
|
186
|
+
' birc add',
|
|
187
|
+
' birc add email',
|
|
188
|
+
' birc add email sentry',
|
|
189
|
+
' birc add file-upload',
|
|
190
|
+
' birc add --force',
|
|
191
|
+
' birc sync',
|
|
192
|
+
' birc make:entity User',
|
|
193
|
+
' birc make:entity User --dto',
|
|
194
|
+
' birc make:entity User --mapper',
|
|
195
|
+
' birc make:entity User --example',
|
|
196
|
+
' birc make:entity User --example --fields title:String,startTime:LocalDateTime',
|
|
197
|
+
' birc make:entity User --force',
|
|
198
|
+
' birc make:model Flight --migration',
|
|
199
|
+
' birc make:model Flight --seed',
|
|
200
|
+
' birc make:model Flight --controller',
|
|
201
|
+
' birc make:model Flight --soft-delete',
|
|
202
|
+
' birc make:migration create_users_table',
|
|
203
|
+
' birc make:migration add_votes_to_users_table',
|
|
204
|
+
' birc migrate',
|
|
205
|
+
'',
|
|
206
|
+
'create / update 以外的指令會從目前目錄往上找 .bircrc.json。',
|
|
207
|
+
''
|
|
208
|
+
].join('\n')
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printUnknownCommand(name) {
|
|
213
|
+
const { suggestCommand } = require('./cli-util');
|
|
214
|
+
const known = allCommandNames();
|
|
215
|
+
process.stderr.write(`找不到指令: ${name}\n`);
|
|
216
|
+
const suggestion = suggestCommand(name, known);
|
|
217
|
+
if (suggestion) {
|
|
218
|
+
if (String(name).includes('::')) {
|
|
219
|
+
process.stderr.write(`你要的大概是 ${suggestion}(中間一條冒號)。\n`);
|
|
220
|
+
} else {
|
|
221
|
+
process.stderr.write(`你要的大概是 ${suggestion}。\n`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
process.stderr.write('看全部指令:birc --help\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runGenerator(name, bypassArr, flags) {
|
|
228
|
+
const { default: nodePlop } = await import('node-plop');
|
|
229
|
+
const plop = await nodePlop(path.join(generatorRoot, 'plopfile.js'), {
|
|
230
|
+
destBasePath: projectRoot,
|
|
231
|
+
force: Boolean(flags.force) || name === 'sync'
|
|
232
|
+
});
|
|
233
|
+
const names = plop.getGeneratorList().map((generator) => generator.name);
|
|
234
|
+
if (!names.includes(name)) {
|
|
235
|
+
printUnknownCommand(name);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (flags.example) {
|
|
240
|
+
process.env.BIRC_EXAMPLE = '1';
|
|
241
|
+
}
|
|
242
|
+
if (flags.dto) {
|
|
243
|
+
process.env.BIRC_MAKE_DTO = '1';
|
|
244
|
+
}
|
|
245
|
+
if (flags.mapper) {
|
|
246
|
+
process.env.BIRC_MAKE_MAPPER = '1';
|
|
247
|
+
}
|
|
248
|
+
if (flags.migration) {
|
|
249
|
+
process.env.BIRC_MAKE_MIGRATION = '1';
|
|
250
|
+
}
|
|
251
|
+
if (flags.seed) {
|
|
252
|
+
process.env.BIRC_MAKE_SEED = '1';
|
|
253
|
+
}
|
|
254
|
+
if (flags.controller) {
|
|
255
|
+
process.env.BIRC_MAKE_CONTROLLER = '1';
|
|
256
|
+
}
|
|
257
|
+
if (flags.softDelete) {
|
|
258
|
+
process.env.BIRC_MAKE_SOFT_DELETE = '1';
|
|
259
|
+
}
|
|
260
|
+
if (name === 'add' && flags.force) {
|
|
261
|
+
process.env.BIRC_ADD_FORCE = '1';
|
|
262
|
+
}
|
|
263
|
+
if (name === 'sync') {
|
|
264
|
+
process.env.BIRC_ADD_FORCE = '1';
|
|
265
|
+
process.env.BIRC_ADD_SYNC = '1';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const generator = plop.getGenerator(name);
|
|
269
|
+
let promptBypass = bypassArr;
|
|
270
|
+
let addFeatures;
|
|
271
|
+
let createYes;
|
|
272
|
+
if (name === 'create') {
|
|
273
|
+
const { applyCreatePromptDefaults, applyCreateYes } = require('./cli-util');
|
|
274
|
+
applyCreatePromptDefaults(generator.prompts, bypassArr);
|
|
275
|
+
promptBypass = [];
|
|
276
|
+
if (flags.yes) {
|
|
277
|
+
const { DEFAULT_FEATURES } = require('../plopfile');
|
|
278
|
+
createYes = applyCreateYes(generator.prompts, bypassArr, [...DEFAULT_FEATURES]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (name === 'add') {
|
|
282
|
+
const { applyAddFeatureArgs } = require('./cli-util');
|
|
283
|
+
const { FEATURES } = require('../plopfile');
|
|
284
|
+
const resolved = applyAddFeatureArgs(generator.prompts, bypassArr, Object.keys(FEATURES));
|
|
285
|
+
if (resolved.error) {
|
|
286
|
+
process.stderr.write(`${resolved.error}\n`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
if (resolved.keys) {
|
|
290
|
+
addFeatures = resolved.keys;
|
|
291
|
+
promptBypass = [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (flags.fields) {
|
|
295
|
+
process.env.BIRC_MAKE_FIELDS = flags.fields;
|
|
296
|
+
const { applyMakeFieldsFlag, fieldsWithoutExampleMessage } = require('./cli-util');
|
|
297
|
+
applyMakeFieldsFlag(generator.prompts, flags.fields);
|
|
298
|
+
if (name === 'make' || String(name).startsWith('make:')) {
|
|
299
|
+
const warn = fieldsWithoutExampleMessage(flags);
|
|
300
|
+
if (warn) process.stderr.write(`${warn}\n`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const answers = await generator.runPrompts(promptBypass);
|
|
304
|
+
if (createYes) {
|
|
305
|
+
Object.assign(answers, createYes);
|
|
306
|
+
}
|
|
307
|
+
if (addFeatures) {
|
|
308
|
+
answers.features = addFeatures;
|
|
309
|
+
}
|
|
310
|
+
if (flags.fields) {
|
|
311
|
+
answers.fields = flags.fields;
|
|
312
|
+
}
|
|
313
|
+
answers.example = Boolean(answers.example) || flags.example;
|
|
314
|
+
answers.dto = Boolean(answers.dto) || flags.dto;
|
|
315
|
+
answers.mapper = Boolean(answers.mapper) || flags.mapper;
|
|
316
|
+
answers.migration = Boolean(answers.migration) || flags.migration;
|
|
317
|
+
answers.seed = Boolean(answers.seed) || flags.seed;
|
|
318
|
+
answers.controller = Boolean(answers.controller) || flags.controller;
|
|
319
|
+
answers.softDelete = Boolean(answers.softDelete) || flags.softDelete;
|
|
320
|
+
answers.force = Boolean(answers.force) || flags.force || name === 'sync';
|
|
321
|
+
answers.sync = Boolean(answers.sync) || name === 'sync';
|
|
322
|
+
let failed = false;
|
|
323
|
+
let changeCount = 0;
|
|
324
|
+
await generator.runActions(answers, {
|
|
325
|
+
onSuccess(change) {
|
|
326
|
+
changeCount += 1;
|
|
327
|
+
const bits = [change.type, change.path].filter(Boolean);
|
|
328
|
+
if (bits.length) process.stdout.write(` ${bits.join(' ')}\n`);
|
|
329
|
+
},
|
|
330
|
+
onFailure(fail) {
|
|
331
|
+
failed = true;
|
|
332
|
+
const { alreadyExistsMessage } = require('./cli-util');
|
|
333
|
+
const rewritten = alreadyExistsMessage(fail);
|
|
334
|
+
if (rewritten) {
|
|
335
|
+
process.stderr.write(` ${rewritten}\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const errMsg = fail.error || fail.message || '';
|
|
339
|
+
const bits = [fail.type, fail.path, errMsg].filter(Boolean);
|
|
340
|
+
process.stderr.write(` ${bits.join(' ')}\n`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
if (failed) process.exit(1);
|
|
344
|
+
if (name === 'create') {
|
|
345
|
+
printCreateNextSteps(answers);
|
|
346
|
+
}
|
|
347
|
+
if (name === 'add') {
|
|
348
|
+
const { formatAddNextSteps } = require('./cli-util');
|
|
349
|
+
process.stdout.write(`${formatAddNextSteps(answers, { changed: changeCount > 0 }).join('\n')}\n`);
|
|
350
|
+
}
|
|
351
|
+
if (name === 'make' || String(name).startsWith('make:') || name === 'migration') {
|
|
352
|
+
const { formatMakeNextSteps } = require('./cli-util');
|
|
353
|
+
const lines = formatMakeNextSteps(name, answers);
|
|
354
|
+
if (lines.length) process.stdout.write(`${lines.join('\n')}\n`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function printCreateNextSteps(answers) {
|
|
359
|
+
const { toKebabName } = require('./cli-util');
|
|
360
|
+
const dir = toKebabName(answers.projectName);
|
|
361
|
+
const hasDocker = (answers.features || []).includes('docker');
|
|
362
|
+
const lines = [
|
|
363
|
+
'',
|
|
364
|
+
`專案已建立在 ${dir}/。接下來:`,
|
|
365
|
+
'',
|
|
366
|
+
` cd ${dir}`
|
|
367
|
+
];
|
|
368
|
+
if (hasDocker) {
|
|
369
|
+
lines.push(
|
|
370
|
+
' docker compose up -d db # 啟動 MySQL(已從 .env.example 複製成 .env)'
|
|
371
|
+
);
|
|
372
|
+
if (process.platform === 'win32') {
|
|
373
|
+
lines.push(
|
|
374
|
+
' # PowerShell:把 .env 載入目前 session 後再跑',
|
|
375
|
+
' Get-Content .env | ForEach-Object { if ($_ -match "^\\s*([^#][^=]*)=(.*)$") { Set-Item -Path "Env:$($matches[1].Trim())" -Value $matches[2].Trim().Trim(\'"\') } }',
|
|
376
|
+
' .\\gradlew.bat bootRun'
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
lines.push(
|
|
380
|
+
' set -a && source .env && set +a',
|
|
381
|
+
' ./gradlew bootRun # 啟動 Spring Boot'
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
lines.push(
|
|
386
|
+
' # 先準備 MySQL,並設定 src/main/resources/application.yml 的連線',
|
|
387
|
+
process.platform === 'win32' ? ' .\\gradlew.bat bootRun' : ' ./gradlew bootRun'
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
lines.push(
|
|
391
|
+
'',
|
|
392
|
+
'生 CRUD 骨架(會從目前目錄往上找 .bircrc.json):',
|
|
393
|
+
' birc make # Entity + DAO + Mapper + DTO + Service + Controller',
|
|
394
|
+
' birc make:entity # Entity / DAO',
|
|
395
|
+
' birc make:entity User --dto',
|
|
396
|
+
' birc make:model Flight --migration --seed --controller',
|
|
397
|
+
' birc make:entity User --example',
|
|
398
|
+
' birc make:migration create_users_table',
|
|
399
|
+
' birc add # 之後再補 docker / email 等功能',
|
|
400
|
+
'',
|
|
401
|
+
'打包:',
|
|
402
|
+
process.platform === 'win32' ? ' .\\gradlew.bat build -x test' : ' ./gradlew build -x test',
|
|
403
|
+
''
|
|
404
|
+
);
|
|
405
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function confirmYes(question) {
|
|
409
|
+
const readline = require('readline/promises');
|
|
410
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
411
|
+
try {
|
|
412
|
+
const answer = await rl.question(question);
|
|
413
|
+
return /^y(es)?$/i.test(String(answer || '').trim());
|
|
414
|
+
} finally {
|
|
415
|
+
rl.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function confirmDefaultYes(question) {
|
|
420
|
+
const readline = require('readline/promises');
|
|
421
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
422
|
+
try {
|
|
423
|
+
const answer = await rl.question(question);
|
|
424
|
+
const text = String(answer || '').trim();
|
|
425
|
+
if (!text) return true;
|
|
426
|
+
return /^y(es)?$/i.test(text);
|
|
427
|
+
} finally {
|
|
428
|
+
rl.close();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function offerUpdateIfNeeded(command) {
|
|
433
|
+
const {
|
|
434
|
+
compareSemver,
|
|
435
|
+
detectInstallKind,
|
|
436
|
+
shouldSkipPrompt,
|
|
437
|
+
readSnooze,
|
|
438
|
+
writeSnooze,
|
|
439
|
+
fetchLatestVersion
|
|
440
|
+
} = require('./update-check');
|
|
441
|
+
|
|
442
|
+
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
443
|
+
if (shouldSkipPrompt({ env: process.env, tty, command })) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const kind = detectInstallKind(generatorRoot);
|
|
448
|
+
const latest = await fetchLatestVersion(kind);
|
|
449
|
+
if (!latest || compareSemver(latest, pkg.version) <= 0) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (shouldSkipPrompt({ env: process.env, tty, command, latest, snooze: readSnooze() })) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
process.stdout.write(`發現新版本 ${latest}(目前 ${pkg.version})。\n`);
|
|
457
|
+
const ok = await confirmDefaultYes('要現在更新嗎?(Y/n): ');
|
|
458
|
+
if (!ok) {
|
|
459
|
+
writeSnooze(latest);
|
|
460
|
+
process.stdout.write('已略過。24 小時內不會再問同一版本。\n\n');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (await applyInstalledUpdate(kind, { continueOnFailure: true }) === 0) {
|
|
465
|
+
process.stdout.write('已更新。請再執行一次剛才的指令。\n');
|
|
466
|
+
process.exit(0);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function runGradle(args) {
|
|
471
|
+
const {
|
|
472
|
+
resolveGradleWrapper,
|
|
473
|
+
gradleWrapperMissingMessage
|
|
474
|
+
} = require('./cli-util');
|
|
475
|
+
const wrapper = resolveGradleWrapper(projectRoot);
|
|
476
|
+
if (!wrapper) {
|
|
477
|
+
throw new Error(gradleWrapperMissingMessage());
|
|
478
|
+
}
|
|
479
|
+
const result = spawnSync(wrapper.command, args, {
|
|
480
|
+
stdio: 'inherit',
|
|
481
|
+
cwd: projectRoot,
|
|
482
|
+
shell: wrapper.shell,
|
|
483
|
+
windowsHide: true
|
|
484
|
+
});
|
|
485
|
+
if (result.error) {
|
|
486
|
+
throw new Error(`執行 Gradle 失敗:${result.error.message}`);
|
|
487
|
+
}
|
|
488
|
+
return result.status === null ? 1 : result.status;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function runFlyway(command, flags) {
|
|
492
|
+
if (command === 'migrate:rollback') {
|
|
493
|
+
process.stdout.write(
|
|
494
|
+
[
|
|
495
|
+
'Flyway Community 沒有 Laravel 的 migrate:rollback。',
|
|
496
|
+
'undo 只有 Flyway Teams(./gradlew flywayUndo),而且要另寫 U{version}__*.sql。',
|
|
497
|
+
'Community 請再寫一筆往前的遷移來反做,例如:',
|
|
498
|
+
' birc make:migration remove_votes_from_users_table',
|
|
499
|
+
' birc migrate',
|
|
500
|
+
''
|
|
501
|
+
].join('\n')
|
|
502
|
+
);
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (command === 'migrate:reset') {
|
|
507
|
+
if (!flags.force) {
|
|
508
|
+
const ok = await confirmYes('這會清掉 schema 再重跑全部遷移。確定? [y/N] ');
|
|
509
|
+
if (!ok) {
|
|
510
|
+
process.stdout.write('已取消。\n');
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const cleanStatus = runGradle(['flywayClean', '-Dflyway.cleanDisabled=false']);
|
|
515
|
+
if (cleanStatus !== 0) {
|
|
516
|
+
process.exit(cleanStatus);
|
|
517
|
+
}
|
|
518
|
+
process.exit(runGradle(['flywayMigrate']));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
process.exit(runGradle(['flywayMigrate']));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function updateViaNpm() {
|
|
525
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
526
|
+
process.stdout.write(`這份 birc 是 npm 裝的,改跑 npm install -g ${pkg.name}@latest。\n`);
|
|
527
|
+
const result = spawnSync(npmCmd, ['install', '-g', `${pkg.name}@latest`], {
|
|
528
|
+
stdio: 'inherit'
|
|
529
|
+
});
|
|
530
|
+
if (result.error) {
|
|
531
|
+
throw new Error(`更新失敗:${result.error.message}`);
|
|
532
|
+
}
|
|
533
|
+
return result.status === null ? 1 : result.status;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function applyInstalledUpdate(kind, { continueOnFailure = false } = {}) {
|
|
537
|
+
const { resolveUpdateMethod } = require('./update-check');
|
|
538
|
+
if (resolveUpdateMethod(kind) === 'npm') {
|
|
539
|
+
const status = updateViaNpm();
|
|
540
|
+
if (status === 0) {
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
if (continueOnFailure) {
|
|
544
|
+
process.stderr.write('更新失敗,先繼續用目前版本。\n\n');
|
|
545
|
+
return status;
|
|
546
|
+
}
|
|
547
|
+
throw new Error(`npm 更新失敗。也可改跑 npm update -g ${pkg.name}。`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const homeInstall = require('./home-install');
|
|
551
|
+
process.stdout.write('這份 birc 不是 npm 裝的,改拉 GitLab main 到 ~/.birc-generator(curl / PowerShell 同一處)。\n');
|
|
552
|
+
try {
|
|
553
|
+
return await homeInstall.runUpdate({
|
|
554
|
+
currentRoot: generatorRoot,
|
|
555
|
+
installDir: homeInstall.defaultInstallDir(),
|
|
556
|
+
baseUrl: homeInstall.defaultBaseUrl()
|
|
557
|
+
});
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (continueOnFailure) {
|
|
560
|
+
process.stderr.write(`更新失敗:${err.message || err}\n先繼續用目前版本。\n\n`);
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
throw err;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function commandNeedsProject(command) {
|
|
568
|
+
return Boolean(command) && command !== 'create' && command !== 'update' && command !== 'help';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function main() {
|
|
572
|
+
const { findProjectRoot, projectConfigMissingMessage, shouldPrintBanner, parseArgv, createDestConflict } = require('./cli-util');
|
|
573
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
574
|
+
if (parsed.error) {
|
|
575
|
+
process.stderr.write(`${parsed.error}\n`);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
const flags = parsed.flags;
|
|
579
|
+
const command = flags.positionals[0];
|
|
580
|
+
|
|
581
|
+
if (command === 'create' && !flags.help) {
|
|
582
|
+
const conflict = createDestConflict({
|
|
583
|
+
cwd: process.cwd(),
|
|
584
|
+
projectName: flags.positionals[1]
|
|
585
|
+
});
|
|
586
|
+
if (conflict) {
|
|
587
|
+
process.stderr.write(`${conflict}\n`);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (
|
|
593
|
+
!flags.help
|
|
594
|
+
&& !flags.version
|
|
595
|
+
&& commandNeedsProject(command)
|
|
596
|
+
&& (allCommandNames().includes(command) || FLYWAY_COMMANDS.has(command))
|
|
597
|
+
) {
|
|
598
|
+
const found = findProjectRoot(process.cwd());
|
|
599
|
+
if (!found) {
|
|
600
|
+
process.stderr.write(`${projectConfigMissingMessage()}\n`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
projectRoot = found;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
process.env.BIRC_PROJECT_ROOT = projectRoot;
|
|
607
|
+
|
|
608
|
+
if (shouldPrintBanner(command, flags)) {
|
|
609
|
+
printBanner();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (flags.version && !flags.positionals.length) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
await offerUpdateIfNeeded(command);
|
|
617
|
+
|
|
618
|
+
if (flags.help || flags.positionals.length === 0 || command === 'help') {
|
|
619
|
+
printHelp();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!allCommandNames().includes(command) && !FLYWAY_COMMANDS.has(command)) {
|
|
624
|
+
printUnknownCommand(command);
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
if (command === 'update') {
|
|
628
|
+
const { detectInstallKind } = require('./update-check');
|
|
629
|
+
if (await applyInstalledUpdate(detectInstallKind(generatorRoot)) === 0) {
|
|
630
|
+
process.stdout.write('已更新。\n');
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (FLYWAY_COMMANDS.has(command)) {
|
|
635
|
+
await runFlyway(command, flags);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
await runGenerator(command, flags.positionals.slice(1), flags);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
main().catch((err) => {
|
|
643
|
+
console.error(err.message || err);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
});
|