birc-generator 0.9.0 → 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 (58) hide show
  1. package/PROJECT.md +24 -5
  2. package/README.md +14 -12
  3. package/bin/birc.js +10 -4
  4. package/bin/cli-util.js +30 -2
  5. package/lib/create.js +150 -0
  6. package/lib/features.js +558 -0
  7. package/lib/make.js +658 -0
  8. package/lib/project.js +234 -0
  9. package/package.json +5 -3
  10. package/plopfile.js +62 -1334
  11. package/project-docs/PROJECT.md.hbs +14 -1
  12. package/templates/aop/OperationLogAspect.hbs +10 -3
  13. package/templates/auth/AuthController.hbs +73 -0
  14. package/templates/auth/AuthFlowTest.hbs +112 -0
  15. package/templates/auth/AuthSecurityCustomizer.hbs +38 -0
  16. package/templates/auth/AuthUser.hbs +54 -0
  17. package/templates/auth/AuthUserDAO.hbs +9 -0
  18. package/templates/auth/JpaUserDetailsService.hbs +47 -0
  19. package/templates/auth/JwtAuthenticationFilter.hbs +50 -0
  20. package/templates/auth/JwtProperties.hbs +35 -0
  21. package/templates/auth/JwtSecretEnvTest.hbs +68 -0
  22. package/templates/auth/JwtSecretEnvironmentPostProcessor.hbs +102 -0
  23. package/templates/auth/JwtService.hbs +60 -0
  24. package/templates/auth/LoginFailedException.hbs +27 -0
  25. package/templates/auth/LoginRequest.hbs +16 -0
  26. package/templates/auth/V1__create_auth_users_table.sql.hbs +13 -0
  27. package/templates/auth/application-yml-block.hbs +7 -0
  28. package/templates/auth/build-gradle-dep.hbs +4 -0
  29. package/templates/auth/spring.factories.hbs +1 -0
  30. package/templates/base/CorsTest.java.hbs +93 -0
  31. package/templates/base/README.md.hbs +6 -0
  32. package/templates/base/application-test.yml.hbs +3 -0
  33. package/templates/base/application.yml.hbs +12 -0
  34. package/templates/clockin/ClockInApiException.hbs +37 -0
  35. package/templates/clockin/ClockInApiResponse.hbs +14 -0
  36. package/templates/clockin/ClockInClient.hbs +251 -0
  37. package/templates/clockin/ClockInClientConfig.hbs +41 -0
  38. package/templates/clockin/ClockInController.hbs +77 -0
  39. package/templates/clockin/ClockInPage.hbs +11 -0
  40. package/templates/clockin/ClockInProperties.hbs +34 -0
  41. package/templates/clockin/ClockInRecord.hbs +20 -0
  42. package/templates/clockin/MemberImage.hbs +7 -0
  43. package/templates/clockin/OnDutyWeek.hbs +26 -0
  44. package/templates/clockin/UnclockedMember.hbs +7 -0
  45. package/templates/clockin/UserPermission.hbs +11 -0
  46. package/templates/clockin/application-yml-block.hbs +10 -0
  47. package/templates/docker/docker-compose.prod.yml.hbs +5 -0
  48. package/templates/docker/docker-compose.yml.hbs +7 -1
  49. package/templates/docker/env.example.hbs +15 -0
  50. package/templates/file-upload/FileExtensionUtils.hbs +45 -0
  51. package/templates/file-upload/FileExtensionUtilsTest.hbs +48 -0
  52. package/templates/file-upload/FileStorageServiceImpl.hbs +6 -0
  53. package/templates/multi-module/config/SecurityConfig.java.hbs +123 -8
  54. package/templates/multi-module/config/SecurityCustomizer.java.hbs +24 -0
  55. package/templates/openapi/ApiDocsAccessTest.hbs +60 -0
  56. package/templates/openapi/application-yml-block.hbs +8 -0
  57. package/test.md +7 -4
  58. package/versions.js +1 -0
package/PROJECT.md CHANGED
@@ -16,6 +16,7 @@ anchor 就不要重寫整份設定;推測以後會用到的模組先不要做
16
16
  | 注入給產出專案的 agent 文件 | `project-docs/` | `create`(`injectDocs`)、`docs` |
17
17
  | create 的基礎骨架 | `templates/base/`、`templates/multi-module/`、`templates/gradle-wrapper/` | `create`(含給人看的 `README.md`) |
18
18
  | CLI 進入點 | `bin/birc.js`、`bin/home-install.js`、`install.sh` / `install.ps1` | `birc`(banner、版本、`update`、直接呼叫 node-plop) |
19
+ | generator 實作 | `plopfile.js`(登記)+ `lib/features.js` / `lib/make.js` / `lib/create.js` / `lib/project.js` | `create`、`add`、`sync`、`make:*`、`docs` |
19
20
 
20
21
  完成條件:能說出這次只動哪一個 seam、哪幾個 generator 會吃到。
21
22
 
@@ -32,23 +33,41 @@ anchor 就不要重寫整份設定;推測以後會用到的模組先不要做
32
33
 
33
34
  ## 改 feature
34
35
 
35
- 1. 在 `FEATURES` 加一個 key:`label`、`files` / `resourceFiles` / `rootFiles`、`ymlBlock`、`gradleDep`、`gradlePlugin`、`gradleAllprojects`。
36
+ 1. 在 `FEATURES` 加一個 key:`label`、`files` / `testFiles` / `entityFiles` / `daoFiles` / `resourceFiles` / `rootFiles`、`ymlBlock`、`gradleDep`、`gradlePlugin`、`gradleAllprojects`。
36
37
  完成條件:key 對得上 `templates/<key>/`;用不到的插入欄位是 `null`,`rootFiles` 只在有 repo 根檔時才出現。
37
38
  2. 補對應的 `.hbs`。Java 用 `{{basePackage}}`;路徑層級跟既有 feature 一樣(`service/`、`config/`、`client/`)。
38
- 完成條件:`FEATURES` 列出的每個 `templateFile` 都在,dest 落在對的 root(Java → `root`,resources → `resourcesRoot`,repo 根檔 → `projectRoot`)。
39
+ 完成條件:`FEATURES` 列出的每個 `templateFile` 都在,dest 落在對的 root(Java → `root`,測試 → `testRoot`,
40
+ Entity → `entityRoot`,DAO → `daoRoot`,resources → `resourcesRoot`,repo 根檔 → `projectRoot`)。
41
+
42
+ Entity / DAO 多模組時落在 `databaseconfig` 模組而不是 `root`,所以要用 `entityFiles` / `daoFiles`,
43
+ 套件名用 `{{entityPackage}}` / `{{daoPackage}}`,不要自己拼 `{{basePackage}}.entity`。
44
+
45
+ feature 自帶 Entity 時,名字不要用專案很可能自己 `make` 的(`User`、`Order` 這種),
46
+ 否則 `birc make User` 會撞檔。`auth` 用 `AuthUser` / `auth_users` 就是為了讓出 `User`。
47
+
48
+ 安全相關的判斷(擋什麼、放什麼)用 `testFiles` 讓 feature 帶一份自己的測試進產出專案。
49
+ generator 這邊的正規表達式斷言只證明程式碼長什麼樣,不會執行到那段邏輯。
50
+
51
+ 要動 `SecurityFilterChain` 的 feature,寫一個實作 `SecurityCustomizer` 的 `@Component`,
52
+ 不要改寫 `SecurityConfig`——那個檔每個專案都會自己改,覆寫它等於 `add --force` 會吃掉專案的修改。
53
+ 放行路徑走 `publicPaths()`,掛 filter 走 `customize()`。`auth` 的 `AuthSecurityCustomizer` 是範例。
39
54
  3. 要寫進 `application.yml` / `build.gradle` 時,做 append fragment,接在 **anchor** 後面:
40
55
  - yml:`# birc-generator:config-anchor`
41
56
  - gradle 依賴:`// birc-generator:dependency-anchor`(`gradleDep`,在 `dependencies {}`)
42
57
  - gradle plugin:`// birc-generator:plugin-anchor`(`gradlePlugin`,在 `plugins {}`)
43
58
  - gradle allprojects:`// birc-generator:allprojects-anchor`(`gradleAllprojects`,在 `allprojects {}`)
44
- 完成條件:產出檔仍留著同一行 anchor,新內容在它後面。yml fragment 若再寫一個 top-level `spring:`,用 `---` 開新 YAML document(SnakeYAML 同一 document 重複 key 是後蓋前,不會 merge)。gradle 依賴 fragment 維持 `dependencies {}` 內的縮排;plugin / allprojects 不要塞進 `dependencies {}`。
59
+ 插進去的每段 fragment 都會被 `birc:begin:<marker>` / `birc:end:<marker>` 包住(yml `#`,gradle `//`),
60
+ marker 就是 feature key,同一個 feature 插到 plugins / allprojects 的另外兩段是 `<key>-plugin` / `<key>-allprojects`。
61
+ 冪等看 marker 在不在;marker 出現前產出的舊專案沒有 marker,改用整段文字比對,兩條都要留著。
62
+
63
+ 完成條件:產出檔仍留著同一行 anchor,新內容在它後面,而且被成對 marker 包住。yml fragment 若再寫一個 top-level `spring:`,用 `---` 開新 YAML document(SnakeYAML 同一 document 重複 key 是後蓋前,不會 merge)。gradle 依賴 fragment 維持 `dependencies {}` 內的縮排;plugin / allprojects 不要塞進 `dependencies {}`。
45
64
  4. 用 `npx plop create` 與 `npx plop add` 各走一次,再重跑一次 `add`。
46
65
  完成條件:兩邊產出同一組檔與同一段 yml/gradle,anchor 行還在;第二次 `add`
47
66
  不改檔,`.bircrc.json.features` 只有一個同名 key。
48
67
 
49
68
  ## 慣例
50
69
 
51
- **make 一次整組。** `make` 串 `makeStackActions`:Entity + DAO + Mapper + DTO + Service + Controller。`--migration` 生 `create_{table}_table`(Flyway SQL)。`--seed` 生 `XxxSeeder`(Java,走 DAO;`birc seed` 才執行)。單層指令共用同一組 actions,不另寫一份。`make:entity` 產出 Entity、`XxxDAO extends BaseDAO`。`--migration` / `--seed` 同 `make:model`;已有 Entity 就跳過,只補 SQL / Seeder。`make:model` 同 Entity + DAO;`--migration` / `--seed` 同上,`--controller` 生 Controller。`make:seeder BookSeeder` 只生 Seeder(寫 `Book` 也可以,結尾 `Seeder` 會剝掉再對 Entity)。`--soft-delete` 在 Entity 加 Hibernate `@SoftDelete(columnName = "deleted_at", strategy = TIMESTAMP)`,migration 加 `deleted_at TIMESTAMP NULL`;`deleteById` 不用改。不做 restore / withTrashed。`make:entity --dto` 與 `make:entity --mapper` 一併生 Mapper 與 Request / Response DTO(同一組檔)。`make:mapper` 連 Request / Response DTO,並把 MapStruct 依賴接到 `build.gradle` 的 dependency-anchor 後面(已有則跳過)。`make:service` 的 `XxxService extends BaseService`,CRUD 在 `BaseServiceImpl`;多出來的業務方法寫在 `XxxService` / `XxxServiceImpl`。指令是 `make:entity`,不是 `make::entity`。預設只生引入與初稿;加 `--example` 或 `--fields` 才生完整範例(CRUD、欄位、`@Column`)。`--example` 的 Entity 與 `make:migration` 一樣帶 `created_at` / `updated_at`(DB 填,JPA `insertable = false`),不進 CreateRequest。多模組時 Entity / DAO 落在 `databaseconfig`。路徑以 `.bircrc.json` 的 `entityPath` / `daoPath` 為準。
70
+ **make 一次整組。** `make` 串 `makeStackActions`:Entity + DAO + Mapper + DTO + Service + Controller。`--migration` 生 `create_{table}_table`(Flyway SQL)。`--seed` 生 `XxxSeeder`(Java,走 DAO;`birc seed` 才執行)。加 `--migration` / `--seed` 時已有 Java 就跳過,只補缺的檔與 SQL / Seeder。單層指令共用同一組 actions,不另寫一份。`make:entity` 產出 Entity、`XxxDAO extends BaseDAO`。`--migration` / `--seed` 同 `make:model`;已有 Entity 就跳過,只補 SQL / Seeder。`make:model` 同 Entity + DAO;`--migration` / `--seed` 同上,`--controller` 生 Controller。`make:seeder BookSeeder` 只生 Seeder(寫 `Book` 也可以,結尾 `Seeder` 會剝掉再對 Entity)。`--soft-delete` 在 Entity 加 Hibernate `@SoftDelete(columnName = "deleted_at", strategy = TIMESTAMP)`,migration 加 `deleted_at TIMESTAMP NULL`;`deleteById` 不用改。不做 restore / withTrashed。`make:entity --dto` 與 `make:entity --mapper` 一併生 Mapper 與 Request / Response DTO(同一組檔)。`make:mapper` 連 Request / Response DTO,並把 MapStruct 依賴接到 `build.gradle` 的 dependency-anchor 後面(已有則跳過)。`make:service` 的 `XxxService extends BaseService`,CRUD 在 `BaseServiceImpl`;多出來的業務方法寫在 `XxxService` / `XxxServiceImpl`。指令是 `make:entity`,不是 `make::entity`。預設只生引入與初稿;加 `--example` 或 `--fields` 才生完整範例(CRUD、欄位、`@Column`)。`--example` 的 Entity 與 `make:migration` 一樣帶 `created_at` / `updated_at`(DB 填,JPA `insertable = false`),不進 CreateRequest。多模組時 Entity / DAO 落在 `databaseconfig`。路徑以 `.bircrc.json` 的 `entityPath` / `daoPath` 為準。
52
71
 
53
72
  **例外走 ProjectException。** 基底是專案內的 `ProjectException`(`getErrorCode` / `getHttpStatus`),不是 starter 的 `BusinessException`。`create` 會生 `ProjectException`、`NotFoundException`、`ExceptionHandleController`、`web/Result`。`make:exception` 生的類別一律 `extends ProjectException`。Controller 回傳專案內 `Result<T>`,不引用 `tw.edu.ntub.birc.starter`。
54
73
 
@@ -67,7 +86,7 @@ anchor 就不要重寫整份設定;推測以後會用到的模組先不要做
67
86
  **版本只改一處。** Spring Boot、Flyway、Sentry、MapStruct、Spotless、dependency-management
68
87
  plugin 與 starter 版本都在 `versions.js`;`create` / `add` / `make:mapper` 將它們注入 template。
69
88
 
70
- **`birc` 的 dest。** `bin/birc.js` 把 plop 的 cwd 釘在這個 repo(找 `plopfile.js` 與 `templates/`),dest 與 `.bircrc.json` 走呼叫者目錄(`BIRC_PROJECT_ROOT` + `--dest`)。`create` / `update` 用目前目錄;目前目錄已有 `.bircrc.json`、或目標資料夾已在,`create` 會先失敗。`birc create --yes` 其餘用預設、不要問。`add` / `make:*` / `migrate` / `seed` / `db:wipe` / `docs` 從目前目錄往上找 `.bircrc.json`,檔案落在找到的專案根。`birc add email sentry` 可帶 feature 名(也接受 `file-upload` 這種 kebab);沒帶參數仍互動勾選。打錯功能名會給近似建議。未知旗標會失敗並提示正確名稱。`docs` 用 `.bircrc.json` 的專案名,不再問。`make` 撞到已有檔會提示 `--force`;`--force` 覆寫那組 Java。`--fields` 有帶就當完整產出,不必再加 `--example`。
89
+ **`birc` 的 dest。** `bin/birc.js` 把 plop 的 cwd 釘在這個 repo(找 `plopfile.js` 與 `templates/`),dest 與 `.bircrc.json` 走呼叫者目錄(`BIRC_PROJECT_ROOT` + `--dest`)。`create` / `update` 用目前目錄;目前目錄已有 `.bircrc.json`、或目標資料夾已在,`create` 會先失敗。`birc create --yes` 其餘用預設、不要問。`add` / `make:*` / `migrate` / `seed` / `db:wipe` / `docs` 從目前目錄往上找 `.bircrc.json`,檔案落在找到的專案根。`birc add email sentry` 可帶 feature 名(也接受 `file-upload` 這種 kebab);沒帶參數仍互動勾選。打錯功能名會給近似建議。未知旗標會失敗並提示正確名稱。`docs` 用 `.bircrc.json` 的專案名,不再問。`make` 撞到已有檔只印 `Error:` 與那一個檔,並提示只要表用 `make:migration`、只要 Seeder 用 `make:seeder`;不印後續 aborted。`--force` 覆寫那組 Java。`--fields` 有帶就當完整產出,不必再加 `--example`。
71
90
 
72
91
  **`birc update`。** 先 `detectInstallKind`:`npm` 跑 `npm install -g birc-generator@latest`。curl / PowerShell / clone 走 `bin/home-install.js`(不要再把 Windows 暫存路徑丟給 bash)。`~/.birc-generator` 的 kind 是 `home`,跟用 bash 還是 `irm | iex` 裝無關。
73
92
 
package/README.md CHANGED
@@ -16,7 +16,7 @@ DAO 繼承寫錯、例外類從 starter 抓來用,之後還要再修。
16
16
  CRUD 用 `birc make`。多模組目錄、`BaseDAO`、`ProjectException` 會先寫好。
17
17
 
18
18
  完整文件在
19
- [GitLab wiki](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/home)
19
+ [GitLab wiki](https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/wikis/home)
20
20
  (開始使用、add / make / migrate)。這份 README 只留安裝與第一次用。
21
21
 
22
22
  ## 安裝
@@ -122,7 +122,7 @@ ValidGroup、Spotless、注入 agent 文件),不再問。
122
122
  | `birc update` | 更新本機 `birc`(`npm install -g birc-generator@latest`) |
123
123
  | `birc add` | 往既有專案加模組。沒帶名稱會互動勾選;`birc add email sentry` 直接加。`--force` 覆寫已選的已安裝 feature 檔 |
124
124
  | `birc sync` | 用目前 template 覆寫全部已安裝 feature 的檔(不動 yml / gradle) |
125
- | `birc make` | 一次生 Entity + DAO + Mapper + DTO + Service + Controller;加 `--migration` / `--seed` |
125
+ | `birc make` | 一次生 Entity + DAO + Mapper + DTO + Service + Controller;加 `--migration` / `--seed`。已有 Java 時加這兩個旗標會跳過,只補缺的檔與 SQL / Seeder |
126
126
  | `birc make:entity` | Entity、`XxxDAO`;加 `--migration` / `--seed` / `--dto`、`--mapper`、`--soft-delete`。已有檔要覆寫加 `--force`;只要表或 Seeder 可 `--migration` / `--seed`,不會覆寫 Entity |
127
127
  | `birc make:model` | 同 `make:entity`;加 `--migration` / `--seed` / `--controller` / `--soft-delete` |
128
128
  | `birc make:mapper` | Mapper 和 CreateRequest / Response |
@@ -170,31 +170,33 @@ birc migrate
170
170
  `birc add` 沒帶名稱時會問你要加哪些模組;也可以寫 `birc add docker` 或 `birc add file-upload`。
171
171
  `make:migration create_book_table` 會讀已有的 Book Entity 把欄位寫進 SQL。`birc make Book --fields title:String --migration --seed` 同一指令生骨架、CREATE TABLE 與 `BookSeeder`,再 `birc migrate`、`birc seed`。只補 Seeder 用 `birc make:seeder BookSeeder`。清空資料庫用 `birc db:wipe`。`add_votes_to_users_table` 裡的 `add_votes_...` 仍只是檔名。
172
172
 
173
- 各指令的細節見 wiki:[make](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/make)、
174
- [add](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/add)、
175
- [migrate](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/migrate)。
173
+ 各指令的細節見 wiki:[make](https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/wikis/基礎/make)、
174
+ [add](https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/wikis/基礎/add)、
175
+ [migrate](https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/wikis/基礎/migrate)。
176
176
 
177
177
  ## 建立專案時可以勾的功能
178
178
 
179
179
  勾了就會寫進專案裡。之後也能用 `birc add` 再補。完整對照見
180
- [功能模組](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/開發人員/功能模組)。
180
+ [功能模組](https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/wikis/開發人員/功能模組)。
181
181
 
182
182
  | 畫面上的名稱 | 會多出什麼 |
183
183
  | --- | --- |
184
184
  | Docker | `Dockerfile`、`docker-compose.yml`、`docker-compose.prod.yml`、`.env.example`(建立時預設勾選,並複製成 `.env`) |
185
185
  | Email | JavaMailSender、Thymeleaf 範本、`EmailService` |
186
- | SSO | 校內單一登入(`SsoProperties` + `SsoAutoConfiguration` 原始碼,無外部依賴) |
187
- | OAuth2 登入 | OAuth2 登入(範例接 Google) |
186
+ | 帳號登入 | `auth_users` 表、`POST /api/auth/login`(回應標頭 `X-Auth-Token` JWT)、`GET /api/auth/me`、驗章 filter。`JWT_SECRET` 寫在 `.env`,沒有就啟動時產生 |
187
+ | SSO | 校內單一登入的設定骨架(`SsoProperties` + `SsoAutoConfiguration`)。只有設定檔,驗票與發證要自己接 |
188
+ | OAuth2 登入 | OAuth2 登入(範例接 Google)。`OAuth2LoginSuccessHandler` 要自己掛上 `SecurityCustomizer` 才會生效 |
188
189
  | 排程 | `@EnableScheduling` 和一個範例 Job |
189
190
  | AOP 事件記錄 | `@OperationLog` 和對應的 Aspect |
190
191
  | Client | 呼叫外部 API 的 RestClient 封裝 |
192
+ | 簽到 | 接中心簽到系統的 `ClockInClient`(登入拿 JWT、簽到、簽退、改工作內容、查紀錄、查權限、值班表)與 `ClockInController`;帳密放 `BIRC_CLOCKIN_ACCOUNT` / `BIRC_CLOCKIN_PASSWORD` |
191
193
  | Sentry | 錯誤追蹤設定與客戶端例外過濾 |
192
194
  | Log4j2 | 取代預設 Logback,加入主控台、rolling file 與 MDC 格式(建立時預設勾選) |
193
195
  | OpenAPI | springdoc、Swagger UI 與 Bearer JWT security scheme |
194
196
  | 檔案上傳 | 本機磁碟上傳、`util/file` 檢查、上傳例外、`FileUploadController` |
195
197
  | 分頁查詢 | `PageRequest` / `PageResponse`、`Pager`、`GenericSpecification` |
196
198
  | 驗證群組 | `ValidGroup`(Create / Update / Delete / Submit),給 `@Validated` 與欄位 `groups`(建立時預設勾選) |
197
- | 權限控管 | Spring Security 和 `@RequirePermission` |
199
+ | 權限控管 | Spring Security 和 `@RequirePermission`。比對的是 `Authentication` 裡的權限代碼,所以要搭配「帳號登入」或自己的登入機制才有作用 |
198
200
  | .gitlab-ci.yml | Harbor build/push 與 SSH deploy(對齊 teaching-platform;帳密放 GitLab CI/CD Variables) |
199
201
  | Spotless | 根目錄 `spotless_formatter.xml`、Eclipse 4.31、`./gradlew spotlessApply`(建立時預設勾選) |
200
202
 
@@ -236,7 +238,7 @@ Flyway Community 沒有 Laravel 的 rollback。`birc migrate:rollback` 只會
236
238
  clone 下來之後,在 repo 裡跑才會吃到你改的 template:
237
239
 
238
240
  ```bash
239
- git clone https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator.git
241
+ git clone https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator.git
240
242
  cd birc-generator
241
243
  npm install
242
244
  node bin/birc.js --help
@@ -245,9 +247,9 @@ npm run test:coverage
245
247
  npm run test:mutation
246
248
  ```
247
249
 
248
- 改 template 或 `plopfile.js` 之前,先讀 [AGENTS.md](./AGENTS.md) 的目錄,
250
+ 改 template、`plopfile.js` 或 `lib/` 之前,先讀 [AGENTS.md](./AGENTS.md) 的目錄,
249
251
  規範在 [PROJECT.md](./PROJECT.md)。wiki 的
250
- [改產生器](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/開發人員/改產生器)
252
+ [改產生器](https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/wikis/開發人員/改產生器)
251
253
  是給人看的精簡版。
252
254
 
253
255
  發版順序:先把改動推進 GitLab `main`,再改 `package.json` 的 `version`,
package/bin/birc.js CHANGED
@@ -336,15 +336,21 @@ async function runGenerator(name, bypassArr, flags) {
336
336
  },
337
337
  onFailure(fail) {
338
338
  failed = true;
339
- const { alreadyExistsMessage } = require('./cli-util');
340
- const rewritten = alreadyExistsMessage(fail);
339
+ const { isAbortedFollowOn, formatAlreadyExistsHelp } = require('./cli-util');
340
+ if (isAbortedFollowOn(fail)) {
341
+ return;
342
+ }
343
+ const rewritten = formatAlreadyExistsHelp(fail, {
344
+ command: name,
345
+ entityName: answers.entityName
346
+ });
341
347
  if (rewritten) {
342
- process.stderr.write(` ${rewritten}\n`);
348
+ process.stderr.write(`${rewritten.map((line) => ` ${line}`).join('\n')}\n`);
343
349
  return;
344
350
  }
345
351
  const errMsg = fail.error || fail.message || '';
346
352
  const bits = [fail.type, fail.path, errMsg].filter(Boolean);
347
- process.stderr.write(` ${bits.join(' ')}\n`);
353
+ process.stderr.write(` Error: ${bits.join(' ')}\n`);
348
354
  }
349
355
  });
350
356
  if (failed) process.exit(1);
package/bin/cli-util.js CHANGED
@@ -206,9 +206,35 @@ function alreadyExistsMessage(fail = {}) {
206
206
  const rawPath = fail.path || (fromError && fromError[1].trim()) || '';
207
207
  const fileName = path.basename(String(rawPath).trim());
208
208
  if (!fileName) {
209
- return '檔案已在,要覆寫用 --force';
209
+ return 'Error: 檔案已在,要覆寫用 --force';
210
210
  }
211
- return `${fileName} 已在,要覆寫用 --force`;
211
+ return `Error: ${fileName} 已在,要覆寫用 --force`;
212
+ }
213
+
214
+ function isAbortedFollowOn(fail = {}) {
215
+ return /aborted due to previous action failure/i.test(
216
+ String(fail.error || fail.message || '')
217
+ );
218
+ }
219
+
220
+ function formatAlreadyExistsHelp(fail = {}, context = {}) {
221
+ const headline = alreadyExistsMessage(fail);
222
+ if (!headline) return null;
223
+ const command = String(context.command || '');
224
+ const entityName = toPascalCaseName(context.entityName || '');
225
+ const showExtras = entityName
226
+ && (command === 'make' || command === 'make:entity' || command === 'make:model');
227
+ if (!showExtras) {
228
+ return [headline];
229
+ }
230
+ const table = entityName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
231
+ return [
232
+ headline,
233
+ '只要建表,不要動 Java:',
234
+ ` birc make:migration create_${table}_table`,
235
+ '只要示範資料:',
236
+ ` birc make:seeder ${entityName}Seeder`
237
+ ];
212
238
  }
213
239
 
214
240
  function levenshtein(left, right) {
@@ -419,6 +445,8 @@ module.exports = {
419
445
  applyCreatePromptDefaults,
420
446
  applyCreateYes,
421
447
  alreadyExistsMessage,
448
+ isAbortedFollowOn,
449
+ formatAlreadyExistsHelp,
422
450
  suggestCommand,
423
451
  shouldPrintBanner,
424
452
  formatAddNextSteps,
package/lib/create.js ADDED
@@ -0,0 +1,150 @@
1
+ const VERSIONS = require('../versions');
2
+ const {
3
+ toKebab,
4
+ persistenceLayout,
5
+ randomDbPassword
6
+ } = require('./project');
7
+ const { featureActions } = require('./features');
8
+ const {
9
+ exceptionSkeletonActions,
10
+ serviceSkeletonActions,
11
+ seederSkeletonActions
12
+ } = require('./make');
13
+
14
+ function buildCreateActions(data) {
15
+ const projectDir = toKebab(data.projectName);
16
+ const basePackagePath = data.basePackage.replace(/\./g, '/');
17
+
18
+ const layout = persistenceLayout(data.basePackage, {
19
+ multiModule: true,
20
+ projectNameKebab: projectDir
21
+ });
22
+
23
+ const templateData = {
24
+ ...VERSIONS,
25
+ basePackage: data.basePackage,
26
+ projectName: data.projectName,
27
+ projectNameKebab: projectDir,
28
+ multiModule: true,
29
+ entityPackage: layout.entityPackage,
30
+ daoPackage: layout.daoPackage,
31
+ entityPath: layout.entityRoot,
32
+ daoPath: layout.daoRoot,
33
+ gitlabProjectPath: data.gitlabProjectPath || '',
34
+ dbRootPassword: randomDbPassword(),
35
+ dbPassword: randomDbPassword(),
36
+ hasDocker: (data.features || []).includes('docker')
37
+ };
38
+
39
+ const srcPath = 'src/main/java';
40
+ const testSrcPath = 'src/test/java';
41
+ const resourcesPath = 'src/main/resources';
42
+ const root = `${projectDir}/${srcPath}/${basePackagePath}`;
43
+ const testRoot = `${projectDir}/${testSrcPath}/${basePackagePath}`;
44
+ const resourcesRoot = `${projectDir}/${resourcesPath}`;
45
+ const ymlPath = `${resourcesRoot}/application.yml`;
46
+ const gradlePath = `${projectDir}/build.gradle`;
47
+
48
+ let actions = [
49
+ { type: 'assertCreateDestFree', abortOnFail: true },
50
+ { type: 'add', path: `${root}/Application.java`, templateFile: 'templates/base/Application.hbs', data: templateData },
51
+ { type: 'add', path: `${testRoot}/ApplicationTests.java`, templateFile: 'templates/base/ApplicationTests.java.hbs', data: templateData },
52
+ { type: 'add', path: `${testRoot}/CorsTest.java`, templateFile: 'templates/base/CorsTest.java.hbs', data: templateData },
53
+ { type: 'add', path: `${projectDir}/src/test/resources/application.yml`, templateFile: 'templates/base/application-test.yml.hbs', data: templateData },
54
+ { type: 'add', path: gradlePath, templateFile: 'templates/multi-module/build.gradle.hbs', data: templateData },
55
+ { type: 'add', path: `${projectDir}/settings.gradle`, templateFile: 'templates/multi-module/settings.gradle.hbs', data: templateData },
56
+ { type: 'add', path: `${resourcesRoot}/application.yml`, templateFile: 'templates/base/application.yml.hbs', data: templateData },
57
+ { type: 'add', path: `${projectDir}/.gitignore`, templateFile: 'templates/base/gitignore.hbs', data: templateData },
58
+ { type: 'copyFile', from: 'templates/gradle-wrapper/gradlew', path: `${projectDir}/gradlew`, executable: true },
59
+ { type: 'copyFile', from: 'templates/gradle-wrapper/gradlew.bat', path: `${projectDir}/gradlew.bat` },
60
+ {
61
+ type: 'copyFile',
62
+ from: 'templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.jar',
63
+ path: `${projectDir}/gradle/wrapper/gradle-wrapper.jar`
64
+ },
65
+ {
66
+ type: 'copyFile',
67
+ from: 'templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.properties',
68
+ path: `${projectDir}/gradle/wrapper/gradle-wrapper.properties`
69
+ },
70
+ {
71
+ type: 'add',
72
+ path: `${projectDir}/.bircrc.json`,
73
+ templateFile: 'templates/base/bircrc.hbs',
74
+ data: { ...templateData, featuresJson: JSON.stringify(data.features) }
75
+ }
76
+ ];
77
+ actions = actions.concat(exceptionSkeletonActions(root, templateData));
78
+ actions = actions.concat(serviceSkeletonActions(root, templateData));
79
+ actions = actions.concat(seederSkeletonActions(root, templateData));
80
+
81
+ const configRoot = `${projectDir}/modules/${projectDir}-config/src/main/java/${basePackagePath}/config`;
82
+ actions.push(
83
+ { type: 'add', path: `${projectDir}/modules/${projectDir}-config/build.gradle`, templateFile: 'templates/multi-module/config/build.gradle.hbs', data: templateData },
84
+ { type: 'add', path: `${configRoot}/Config.java`, templateFile: 'templates/multi-module/config/Config.java.hbs', data: templateData },
85
+ { type: 'add', path: `${configRoot}/ApplicationConfig.java`, templateFile: 'templates/multi-module/config/ApplicationConfig.java.hbs', data: templateData },
86
+ { type: 'add', path: `${configRoot}/SecurityConfig.java`, templateFile: 'templates/multi-module/config/SecurityConfig.java.hbs', data: templateData },
87
+ { type: 'add', path: `${configRoot}/SecurityCustomizer.java`, templateFile: 'templates/multi-module/config/SecurityCustomizer.java.hbs', data: templateData }
88
+ );
89
+
90
+ const dbConfigRoot = `${projectDir}/modules/${projectDir}-database-config/src/main/java/${basePackagePath}/databaseconfig`;
91
+ actions.push(
92
+ { type: 'add', path: `${projectDir}/modules/${projectDir}-database-config/build.gradle`, templateFile: 'templates/multi-module/database-config/build.gradle.hbs', data: templateData },
93
+ { type: 'add', path: `${dbConfigRoot}/Config.java`, templateFile: 'templates/multi-module/database-config/Config.java.hbs', data: templateData },
94
+ { type: 'add', path: `${dbConfigRoot}/JpaConfig.java`, templateFile: 'templates/multi-module/database-config/JpaConfig.java.hbs', data: templateData },
95
+ { type: 'add', path: `${projectDir}/${layout.daoRoot}/BaseViewDAO.java`, templateFile: 'templates/multi-module/database-config/BaseViewDAO.java.hbs', data: templateData },
96
+ { type: 'add', path: `${projectDir}/${layout.daoRoot}/BaseDAO.java`, templateFile: 'templates/multi-module/database-config/BaseDAO.java.hbs', data: templateData }
97
+ );
98
+
99
+ for (const featureKey of data.features) {
100
+ actions = actions.concat(
101
+ featureActions(featureKey, {
102
+ root,
103
+ testRoot,
104
+ entityRoot: `${projectDir}/${layout.entityRoot}`,
105
+ daoRoot: `${projectDir}/${layout.daoRoot}`,
106
+ resourcesRoot,
107
+ projectRoot: projectDir,
108
+ ymlPath,
109
+ gradlePath,
110
+ templateData
111
+ })
112
+ );
113
+ }
114
+
115
+ actions.push({
116
+ type: 'add',
117
+ path: `${projectDir}/README.md`,
118
+ templateFile: 'templates/base/README.md.hbs',
119
+ data: templateData,
120
+ skipIfExists: true
121
+ });
122
+
123
+ if (data.injectDocs) {
124
+ actions.push(
125
+ { type: 'add', path: `${projectDir}/AGENTS.md`, templateFile: 'project-docs/AGENTS.md.hbs', data: templateData },
126
+ { type: 'add', path: `${projectDir}/PROJECT.md`, templateFile: 'project-docs/PROJECT.md.hbs', data: templateData },
127
+ { type: 'add', path: `${projectDir}/implement.md`, templateFile: 'project-docs/implement.hbs', data: templateData },
128
+ { type: 'add', path: `${projectDir}/test.md`, templateFile: 'project-docs/test.hbs', data: templateData }
129
+ );
130
+ }
131
+
132
+ if ((data.features || []).includes('docker')) {
133
+ actions.push({
134
+ type: 'copyIfExists',
135
+ from: `${projectDir}/.env.example`,
136
+ path: `${projectDir}/.env`
137
+ });
138
+ }
139
+
140
+ if ((data.features || []).includes('auth')) {
141
+ actions.push({
142
+ type: 'ensureJwtSecret',
143
+ path: `${projectDir}/.env`
144
+ });
145
+ }
146
+
147
+ return actions;
148
+ }
149
+
150
+ module.exports = { buildCreateActions };