create-bubbles 0.1.23 → 0.1.24

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 (230) hide show
  1. package/index.js +1 -1
  2. package/package.json +25 -24
  3. package/template-taro-react-oxc/.env.development +1 -1
  4. package/template-taro-react-oxc/.env.production +1 -1
  5. package/template-taro-react-oxc/project.config.json +1 -1
  6. package/template-taro-vue-eslint/project.config.json +2 -2
  7. package/{template-vp-monorepo-react-nestjs → template-vp-monorepo-react-hono}/.spaces/server//347/231/273/345/275/225/JWT/344/270/216RefreshToken/350/275/256/346/215/242/351/211/264/346/235/203/346/236/266/346/236/204/345/256/236/347/216/260/346/214/207/345/215/227.md +588 -1133
  8. package/template-vp-monorepo-react-hono/.vite-hooks/commit-msg +2 -0
  9. package/template-vp-monorepo-react-hono/.vite-hooks/pre-commit +2 -0
  10. package/template-vp-monorepo-react-hono/.vscode/extensions.json +3 -0
  11. package/template-vp-monorepo-react-hono/.vscode/settings.json +23 -0
  12. package/template-vp-monorepo-react-hono/AGENTS.md +16 -0
  13. package/template-vp-monorepo-react-hono/README.md +54 -0
  14. package/template-vp-monorepo-react-hono/apps/server/.env.example +13 -0
  15. package/template-vp-monorepo-react-hono/apps/server/README.md +75 -0
  16. package/template-vp-monorepo-react-hono/apps/server/drizzle/0000_flaky_madame_masque.sql +18 -0
  17. package/template-vp-monorepo-react-hono/apps/server/drizzle/meta/0000_snapshot.json +153 -0
  18. package/template-vp-monorepo-react-hono/apps/server/drizzle/meta/_journal.json +13 -0
  19. package/template-vp-monorepo-react-hono/apps/server/drizzle.config.ts +19 -0
  20. package/template-vp-monorepo-react-hono/apps/server/package.json +46 -0
  21. package/template-vp-monorepo-react-hono/apps/server/src/app.test.ts +57 -0
  22. package/template-vp-monorepo-react-hono/apps/server/src/app.ts +68 -0
  23. package/template-vp-monorepo-react-hono/apps/server/src/common/errors/app-error.ts +23 -0
  24. package/template-vp-monorepo-react-hono/apps/server/src/common/http/response.ts +21 -0
  25. package/template-vp-monorepo-react-hono/apps/server/src/common/logger.ts +30 -0
  26. package/template-vp-monorepo-react-hono/apps/server/src/config/env.ts +54 -0
  27. package/template-vp-monorepo-react-hono/apps/server/src/index.bun.ts +60 -0
  28. package/template-vp-monorepo-react-hono/apps/server/src/index.ts +73 -0
  29. package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/client.ts +46 -0
  30. package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/schema/index.ts +1 -0
  31. package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/schema/job-runs.ts +42 -0
  32. package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/queue/connection.ts +6 -0
  33. package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/redis/client.ts +39 -0
  34. package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/redis/options.ts +28 -0
  35. package/template-vp-monorepo-react-hono/apps/server/src/modules/health/health.routes.ts +17 -0
  36. package/template-vp-monorepo-react-hono/apps/server/src/modules/health/health.service.ts +59 -0
  37. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.queue.ts +46 -0
  38. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.repository.ts +101 -0
  39. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.routes.ts +46 -0
  40. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.schemas.ts +9 -0
  41. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.service.ts +57 -0
  42. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.types.ts +11 -0
  43. package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.worker.ts +108 -0
  44. package/template-vp-monorepo-react-hono/apps/server/src/runtime/shutdown.ts +38 -0
  45. package/template-vp-monorepo-react-hono/apps/server/src/worker.ts +48 -0
  46. package/template-vp-monorepo-react-hono/apps/server/tsconfig.json +15 -0
  47. package/template-vp-monorepo-react-hono/apps/server/vite.config.ts +15 -0
  48. package/template-vp-monorepo-react-hono/apps/web/.env +2 -0
  49. package/template-vp-monorepo-react-hono/apps/web/.env.dev +1 -0
  50. package/template-vp-monorepo-react-hono/apps/web/.env.prod +1 -0
  51. package/template-vp-monorepo-react-hono/apps/web/index.html +13 -0
  52. package/template-vp-monorepo-react-hono/apps/web/package.json +35 -0
  53. package/template-vp-monorepo-react-hono/apps/web/public/favicon.svg +1 -0
  54. package/template-vp-monorepo-react-hono/apps/web/public/icons.svg +24 -0
  55. package/template-vp-monorepo-react-hono/apps/web/src/App.tsx +25 -0
  56. package/template-vp-monorepo-react-hono/apps/web/src/assets/hero.png +0 -0
  57. package/template-vp-monorepo-react-hono/apps/web/src/assets/react.svg +1 -0
  58. package/template-vp-monorepo-react-hono/apps/web/src/assets/vite.svg +1 -0
  59. package/template-vp-monorepo-react-hono/apps/web/src/components/Loading/PageLoading.tsx +11 -0
  60. package/template-vp-monorepo-react-hono/apps/web/src/locales/en_US.json +4 -0
  61. package/template-vp-monorepo-react-hono/apps/web/src/locales/zh_CN.json +4 -0
  62. package/template-vp-monorepo-react-hono/apps/web/src/main.tsx +5 -0
  63. package/template-vp-monorepo-react-hono/apps/web/src/pages/home/index.tsx +47 -0
  64. package/template-vp-monorepo-react-hono/apps/web/src/router/index.tsx +25 -0
  65. package/template-vp-monorepo-react-hono/apps/web/src/router/modules/.gitkeep +0 -0
  66. package/template-vp-monorepo-react-hono/apps/web/src/styles/index.css +7 -0
  67. package/template-vp-monorepo-react-hono/apps/web/src/types/auto-imports.d.ts +58 -0
  68. package/template-vp-monorepo-react-hono/apps/web/src/utils/env/index.ts +6 -0
  69. package/template-vp-monorepo-react-hono/apps/web/src/utils/request/core/index.ts +420 -0
  70. package/template-vp-monorepo-react-hono/apps/web/src/utils/request/core/utils.ts +39 -0
  71. package/template-vp-monorepo-react-hono/apps/web/src/utils/request/index.ts +61 -0
  72. package/template-vp-monorepo-react-hono/apps/web/tsconfig.json +18 -0
  73. package/template-vp-monorepo-react-hono/apps/web/vite.config.ts +45 -0
  74. package/template-vp-monorepo-react-hono/commitlint.config.js +1 -0
  75. package/template-vp-monorepo-react-hono/docker-compose.yml +41 -0
  76. package/template-vp-monorepo-react-hono/mise.toml +4 -0
  77. package/template-vp-monorepo-react-hono/package.json +37 -0
  78. package/template-vp-monorepo-react-hono/packages/i18n/core/package.json +20 -0
  79. package/template-vp-monorepo-react-hono/packages/i18n/core/src/format.ts +9 -0
  80. package/template-vp-monorepo-react-hono/packages/i18n/core/src/i18n.ts +73 -0
  81. package/template-vp-monorepo-react-hono/packages/i18n/core/src/index.ts +24 -0
  82. package/template-vp-monorepo-react-hono/packages/i18n/core/src/storage.ts +47 -0
  83. package/template-vp-monorepo-react-hono/packages/i18n/core/src/store.ts +48 -0
  84. package/template-vp-monorepo-react-hono/packages/i18n/core/src/types.ts +36 -0
  85. package/template-vp-monorepo-react-hono/packages/i18n/core/tsconfig.json +4 -0
  86. package/template-vp-monorepo-react-hono/packages/i18n/core/vite.config.ts +13 -0
  87. package/template-vp-monorepo-react-hono/packages/i18n/react/package.json +28 -0
  88. package/template-vp-monorepo-react-hono/packages/i18n/react/src/index.tsx +26 -0
  89. package/template-vp-monorepo-react-hono/packages/i18n/react/src/use-store.ts +14 -0
  90. package/template-vp-monorepo-react-hono/packages/i18n/react/tsconfig.json +9 -0
  91. package/template-vp-monorepo-react-hono/packages/i18n/react/vite.config.ts +16 -0
  92. package/template-vp-monorepo-react-hono/packages/i18n/vue/package.json +27 -0
  93. package/template-vp-monorepo-react-hono/packages/i18n/vue/src/index.ts +52 -0
  94. package/template-vp-monorepo-react-hono/packages/i18n/vue/src/use-store.ts +19 -0
  95. package/template-vp-monorepo-react-hono/packages/i18n/vue/tsconfig.json +8 -0
  96. package/template-vp-monorepo-react-hono/packages/i18n/vue/vite.config.ts +16 -0
  97. package/template-vp-monorepo-react-hono/pnpm-workspace.yaml +57 -0
  98. package/template-vp-monorepo-react-hono/scripts/clean.mjs +48 -0
  99. package/template-vp-monorepo-react-hono/tsconfig.base.json +10 -0
  100. package/template-vp-monorepo-react-hono/tsconfig.bundler.json +12 -0
  101. package/template-vp-monorepo-react-hono/tsconfig.json +11 -0
  102. package/template-vp-monorepo-react-hono/tsconfig.lib.json +10 -0
  103. package/template-vp-monorepo-react-hono/vite.config.ts +20 -0
  104. package/template-vp-monorepo-react-nestjs/.spaces/server/todo.md +991 -0
  105. package/template-vp-monorepo-react-nestjs/.vscode/launch.json +16 -0
  106. package/template-vp-monorepo-react-nestjs/.vscode/settings.json +21 -18
  107. package/template-vp-monorepo-react-nestjs/apps/server/.env.development +3 -39
  108. package/template-vp-monorepo-react-nestjs/apps/server/.swcrc +8 -0
  109. package/template-vp-monorepo-react-nestjs/apps/server/drizzle.config.ts +8 -3
  110. package/template-vp-monorepo-react-nestjs/apps/server/nest-cli.json +8 -1
  111. package/template-vp-monorepo-react-nestjs/apps/server/package.json +11 -6
  112. package/template-vp-monorepo-react-nestjs/apps/server/src/app.controller.ts +2 -0
  113. package/template-vp-monorepo-react-nestjs/apps/server/src/app.module.ts +20 -15
  114. package/template-vp-monorepo-react-nestjs/apps/server/src/common/adapters/fastify.adapter.ts +16 -0
  115. package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/auth.ts +2 -0
  116. package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/response.ts +0 -4
  117. package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/session.constants.ts +28 -0
  118. package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/current-auth.decorator.ts +16 -0
  119. package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/public.decorator.ts +4 -0
  120. package/template-vp-monorepo-react-nestjs/apps/server/src/common/dto/api-failure.dto.ts +25 -0
  121. package/template-vp-monorepo-react-nestjs/apps/server/src/common/error/common.error.ts +40 -0
  122. package/template-vp-monorepo-react-nestjs/apps/server/src/common/exceptions/app.exception.ts +55 -0
  123. package/template-vp-monorepo-react-nestjs/apps/server/src/common/filters/global-exception.filter.ts +384 -0
  124. package/template-vp-monorepo-react-nestjs/apps/server/src/common/openapi/response-headers.ts +9 -0
  125. package/template-vp-monorepo-react-nestjs/apps/server/src/config/index.ts +1 -1
  126. package/template-vp-monorepo-react-nestjs/apps/server/src/config/session.config.ts +36 -0
  127. package/template-vp-monorepo-react-nestjs/apps/server/src/database/schema.ts +4 -64
  128. package/template-vp-monorepo-react-nestjs/apps/server/src/main.ts +29 -8
  129. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.controller.ts +55 -0
  130. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.errors.ts +42 -0
  131. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.module.ts +26 -0
  132. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.repository.ts +40 -0
  133. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.service.ts +129 -0
  134. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/auth-repsponse.dto.ts +31 -0
  135. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/login.dto.ts +11 -0
  136. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/register.dto.ts +17 -0
  137. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/guards/session-auth.guard.ts +50 -0
  138. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/password.service.ts +24 -0
  139. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-store.service.ts +198 -0
  140. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-token.service.ts +29 -0
  141. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.script.ts +221 -0
  142. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.types.ts +24 -0
  143. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/db.controller.ts +1 -1
  144. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/dto/create-user.dto.ts +3 -5
  145. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/redis/redis.service.ts +1 -1
  146. package/template-vp-monorepo-react-nestjs/apps/server/src/utils/env-arr.ts +3 -0
  147. package/template-vp-monorepo-react-nestjs/apps/server/test/error-catalog.spec.ts +30 -0
  148. package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.build.json +11 -1
  149. package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.json +12 -20
  150. package/template-vp-monorepo-react-nestjs/apps/server/vite.config.ts +15 -0
  151. package/template-vp-monorepo-react-nestjs/apps/web/package.json +1 -0
  152. package/template-vp-monorepo-react-nestjs/apps/web/tsconfig.json +5 -15
  153. package/template-vp-monorepo-react-nestjs/apps/web-vue/commitlint.config.js +1 -1
  154. package/template-vp-monorepo-react-nestjs/apps/web-vue/lefthook.yml +1 -1
  155. package/template-vp-monorepo-react-nestjs/apps/web-vue/package.json +1 -3
  156. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/api/index.ts +5 -5
  157. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/components/Icon/svg-icon.vue +3 -3
  158. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/lib.ts +6 -6
  159. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/useEcharts.ts +34 -29
  160. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/i18n/instance.ts +2 -0
  161. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/layout/default/index.vue +17 -32
  162. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/locales/en_US.json +4 -1
  163. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/locales/zh_CN.json +4 -1
  164. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/index.ts +3 -3
  165. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/permissionGuard.ts +7 -7
  166. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/index.ts +7 -7
  167. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/interface.ts +6 -6
  168. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/index.ts +6 -6
  169. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/modules/user.ts +9 -9
  170. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/styles/index.scss +3 -3
  171. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/types/antdv-next.d.ts +1 -0
  172. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/env.ts +1 -1
  173. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/index.ts +63 -45
  174. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/utils.ts +12 -6
  175. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/index.ts +1 -1
  176. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/config.ts +687 -687
  177. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/index.vue +7 -7
  178. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/tree-chart.vue +2 -7
  179. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/home/index.vue +13 -3
  180. package/template-vp-monorepo-react-nestjs/apps/web-vue/tsconfig.json +5 -17
  181. package/template-vp-monorepo-react-nestjs/apps/web-vue/uno.config.ts +5 -5
  182. package/template-vp-monorepo-react-nestjs/i18n.config.ts +22 -0
  183. package/template-vp-monorepo-react-nestjs/package.json +12 -1
  184. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/README.md +113 -0
  185. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/docs/excel-sync-plan.md +618 -0
  186. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/package.json +31 -0
  187. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/cli.ts +5 -0
  188. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/command.ts +512 -0
  189. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/config.ts +15 -0
  190. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/files.ts +67 -0
  191. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/index.ts +19 -0
  192. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/load-config.ts +228 -0
  193. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/report.ts +57 -0
  194. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.test.ts +81 -0
  195. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.ts +290 -0
  196. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/sync.ts +274 -0
  197. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/command.test.ts +209 -0
  198. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/config.test.ts +19 -0
  199. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/load-config.test.ts +194 -0
  200. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/report.test.ts +63 -0
  201. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/sync.test.ts +203 -0
  202. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/temporary-directory.ts +25 -0
  203. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/tsconfig.json +9 -0
  204. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/vite.config.ts +18 -0
  205. package/template-vp-monorepo-react-nestjs/packages/i18n/core/tsconfig.json +1 -12
  206. package/template-vp-monorepo-react-nestjs/packages/i18n/react/tsconfig.json +3 -10
  207. package/template-vp-monorepo-react-nestjs/packages/i18n/vue/tsconfig.json +3 -10
  208. package/template-vp-monorepo-react-nestjs/packages/shared/package.json +40 -0
  209. package/template-vp-monorepo-react-nestjs/packages/shared/src/index.ts +13 -0
  210. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/auth.ts +35 -0
  211. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/common.ts +11 -0
  212. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/index.ts +11 -0
  213. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/account.ts +5 -0
  214. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/index.ts +2 -0
  215. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/session-terminal.ts +11 -0
  216. package/template-vp-monorepo-react-nestjs/packages/shared/tsconfig.json +4 -0
  217. package/template-vp-monorepo-react-nestjs/packages/shared/vite.config.ts +17 -0
  218. package/template-vp-monorepo-react-nestjs/pnpm-workspace.yaml +6 -6
  219. package/template-vp-monorepo-react-nestjs/scripts/clean.mjs +48 -0
  220. package/template-vp-monorepo-react-nestjs/tsconfig.base.json +10 -0
  221. package/template-vp-monorepo-react-nestjs/tsconfig.bundler.json +12 -0
  222. package/template-vp-monorepo-react-nestjs/tsconfig.json +8 -6
  223. package/template-vp-monorepo-react-nestjs/tsconfig.lib.json +10 -0
  224. package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/decorator.ts +0 -2
  225. package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/bypass.ts +0 -12
  226. package/template-vp-monorepo-react-nestjs/apps/server/src/common/interceptors/transform.ts +0 -34
  227. package/template-vp-monorepo-react-nestjs/apps/server/src/common/model/response.ts +0 -23
  228. package/template-vp-monorepo-react-nestjs/apps/server/src/config/auth.config.ts +0 -64
  229. package/template-vp-monorepo-react-nestjs/apps/web-vue/pnpm-workspace.yaml +0 -16
  230. /package/{template-vp-monorepo-react-nestjs/apps/server/src/config/security.config.ts → template-vp-monorepo-react-hono/.agents/auth.config.ts} +0 -0
@@ -0,0 +1,274 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
3
+ import { basename, dirname, isAbsolute, join } from 'node:path'
4
+
5
+ import type { SyncFileReport } from './report.ts'
6
+
7
+ export interface SyncCatalogOptions {
8
+ project: string
9
+ locale: string
10
+ path: string
11
+ keys: Iterable<string>
12
+ clean?: boolean
13
+ dryRun?: boolean
14
+ allowEmpty?: boolean
15
+ }
16
+
17
+ export interface SyncProjectOptions {
18
+ project: string
19
+ catalogs: Readonly<Record<string, string>>
20
+ keys: Iterable<string>
21
+ clean?: boolean
22
+ dryRun?: boolean
23
+ allowEmpty?: boolean
24
+ }
25
+
26
+ export class CatalogValidationError extends Error {
27
+ override name = 'CatalogValidationError'
28
+ }
29
+
30
+ export class EmptyScanCleanError extends Error {
31
+ override name = 'EmptyScanCleanError'
32
+ }
33
+
34
+ interface CatalogFormat {
35
+ bom: boolean
36
+ eol: '\n' | '\r\n'
37
+ indent: string | number | undefined
38
+ trailingEol: boolean
39
+ }
40
+
41
+ interface CatalogState {
42
+ exists: boolean
43
+ messages: Record<string, string>
44
+ format: CatalogFormat
45
+ }
46
+
47
+ interface CatalogPlan {
48
+ path: string
49
+ content: string
50
+ shouldWrite: boolean
51
+ report: SyncFileReport
52
+ }
53
+
54
+ const defaultFormat: CatalogFormat = {
55
+ bom: false,
56
+ eol: '\n',
57
+ indent: 2,
58
+ trailingEol: true,
59
+ }
60
+
61
+ export async function syncCatalog(options: SyncCatalogOptions): Promise<SyncFileReport> {
62
+ const keys = normalizeKeys(options.keys)
63
+ const plan = await planCatalog(options, keys)
64
+
65
+ if (!(options.dryRun ?? false) && plan.shouldWrite) {
66
+ await writeAtomic(plan.path, plan.content)
67
+ }
68
+
69
+ return plan.report
70
+ }
71
+
72
+ export async function syncProject(options: SyncProjectOptions): Promise<SyncFileReport[]> {
73
+ const keys = normalizeKeys(options.keys)
74
+ const plans = await Promise.all(
75
+ Object.entries(options.catalogs).map(([locale, path]) =>
76
+ planCatalog(
77
+ {
78
+ ...options,
79
+ locale,
80
+ path,
81
+ },
82
+ keys,
83
+ ),
84
+ ),
85
+ )
86
+
87
+ if (!(options.dryRun ?? false)) {
88
+ await Promise.all(
89
+ plans.map(async (plan) => {
90
+ if (plan.shouldWrite) {
91
+ await writeAtomic(plan.path, plan.content)
92
+ }
93
+ }),
94
+ )
95
+ }
96
+
97
+ return plans.map((plan) => plan.report)
98
+ }
99
+
100
+ async function planCatalog(
101
+ options: SyncCatalogOptions,
102
+ keys: ReadonlySet<string>,
103
+ ): Promise<CatalogPlan> {
104
+ if (!isAbsolute(options.path)) {
105
+ throw new TypeError(`Catalog path must be absolute: "${options.path}"`)
106
+ }
107
+
108
+ const state = await readCatalog(options.path)
109
+ const existingKeys = Object.keys(state.messages)
110
+ const added = sortKeys(
111
+ [...keys].filter((key) => !Object.prototype.hasOwnProperty.call(state.messages, key)),
112
+ )
113
+ const stale = sortKeys(existingKeys.filter((key) => !keys.has(key)))
114
+ const clean = options.clean ?? false
115
+
116
+ if (clean && keys.size === 0 && existingKeys.length > 0 && !(options.allowEmpty ?? false)) {
117
+ throw new EmptyScanCleanError(
118
+ `Refusing to clean non-empty catalog "${options.path}" because the source scan found no keys. ` +
119
+ 'Pass allowEmpty: true only when clearing the catalog is intentional.',
120
+ )
121
+ }
122
+
123
+ const deleted = clean ? stale : []
124
+ const unused = clean ? [] : stale
125
+ const retainedEntries = Object.entries(state.messages).filter(([key]) => !deleted.includes(key))
126
+ const nextMessages = Object.fromEntries([
127
+ ...retainedEntries,
128
+ ...added.map((key): [string, string] => [key, key]),
129
+ ])
130
+ const shouldWrite = !state.exists || added.length > 0 || deleted.length > 0
131
+
132
+ return {
133
+ path: options.path,
134
+ content: serializeCatalog(nextMessages, state.format),
135
+ shouldWrite,
136
+ report: {
137
+ project: options.project,
138
+ path: options.path,
139
+ locale: options.locale,
140
+ added,
141
+ unused,
142
+ deleted,
143
+ unchangedCount: existingKeys.filter((key) => keys.has(key)).length,
144
+ },
145
+ }
146
+ }
147
+
148
+ async function readCatalog(path: string): Promise<CatalogState> {
149
+ let source: string
150
+
151
+ try {
152
+ source = await readFile(path, 'utf8')
153
+ } catch (error) {
154
+ if (isNodeError(error) && error.code === 'ENOENT') {
155
+ return {
156
+ exists: false,
157
+ messages: {},
158
+ format: defaultFormat,
159
+ }
160
+ }
161
+
162
+ throw error
163
+ }
164
+
165
+ const format = detectFormat(source)
166
+ const json = format.bom ? source.slice(1) : source
167
+ let parsed: unknown
168
+
169
+ try {
170
+ parsed = JSON.parse(json)
171
+ } catch (error) {
172
+ throw new CatalogValidationError(
173
+ `Catalog "${path}" contains invalid JSON: ${errorMessage(error)}`,
174
+ )
175
+ }
176
+
177
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
178
+ throw new CatalogValidationError(
179
+ `Catalog "${path}" must contain a flat JSON object with string values.`,
180
+ )
181
+ }
182
+
183
+ const entries = Object.entries(parsed as Record<string, unknown>)
184
+
185
+ for (const [key, value] of entries) {
186
+ if (typeof value !== 'string') {
187
+ throw new CatalogValidationError(
188
+ `Catalog "${path}" must contain only string values; key "${key}" is ${valueType(value)}.`,
189
+ )
190
+ }
191
+ }
192
+
193
+ return {
194
+ exists: true,
195
+ messages: Object.fromEntries(entries) as Record<string, string>,
196
+ format,
197
+ }
198
+ }
199
+
200
+ function detectFormat(source: string): CatalogFormat {
201
+ const bom = source.startsWith('\uFEFF')
202
+ const json = bom ? source.slice(1) : source
203
+ const eol = json.includes('\r\n') ? '\r\n' : '\n'
204
+ const indentation = json.match(/\r?\n([\t ]+)"/u)?.[1]
205
+ const hasLineBreak = json.includes('\n')
206
+
207
+ return {
208
+ bom,
209
+ eol,
210
+ indent: indentation?.slice(0, 10) ?? (hasLineBreak ? 2 : undefined),
211
+ trailingEol: json.endsWith('\n'),
212
+ }
213
+ }
214
+
215
+ function serializeCatalog(messages: Record<string, string>, format: CatalogFormat): string {
216
+ const serialized = JSON.stringify(messages, undefined, format.indent) ?? '{}'
217
+ const withEol = format.eol === '\n' ? serialized : serialized.split('\n').join(format.eol)
218
+ const withTrailingEol = format.trailingEol ? `${withEol}${format.eol}` : withEol
219
+
220
+ return format.bom ? `\uFEFF${withTrailingEol}` : withTrailingEol
221
+ }
222
+
223
+ async function writeAtomic(path: string, content: string): Promise<void> {
224
+ const directory = dirname(path)
225
+ const temporaryPath = join(directory, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`)
226
+
227
+ await mkdir(directory, { recursive: true })
228
+
229
+ try {
230
+ await writeFile(temporaryPath, content, 'utf8')
231
+ await rename(temporaryPath, path)
232
+ } catch (error) {
233
+ await unlink(temporaryPath).catch(() => undefined)
234
+ throw error
235
+ }
236
+ }
237
+
238
+ function normalizeKeys(keys: Iterable<string>): ReadonlySet<string> {
239
+ const normalized = new Set<string>()
240
+
241
+ for (const key of keys) {
242
+ if (typeof key !== 'string') {
243
+ throw new TypeError('Scanned message keys must be strings.')
244
+ }
245
+
246
+ normalized.add(key)
247
+ }
248
+
249
+ return normalized
250
+ }
251
+
252
+ function sortKeys(keys: string[]): string[] {
253
+ return keys.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))
254
+ }
255
+
256
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
257
+ return error instanceof Error && 'code' in error
258
+ }
259
+
260
+ function errorMessage(error: unknown): string {
261
+ return error instanceof Error ? error.message : String(error)
262
+ }
263
+
264
+ function valueType(value: unknown): string {
265
+ if (value === null) {
266
+ return 'null'
267
+ }
268
+
269
+ if (Array.isArray(value)) {
270
+ return 'an array'
271
+ }
272
+
273
+ return `a ${typeof value}`
274
+ }
@@ -0,0 +1,209 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ import { describe, expect, it } from 'vite-plus/test'
5
+
6
+ import { CliUsageError, runCli } from '../src/command.ts'
7
+ import { useTemporaryDirectory } from './temporary-directory.ts'
8
+
9
+ describe('runCli', () => {
10
+ const temporaryDirectory = useTemporaryDirectory()
11
+
12
+ it('prints Chinese help', async () => {
13
+ const output: string[] = []
14
+
15
+ await expect(runCli(['--help'], { stdout: (message) => output.push(message) })).resolves.toBe(0)
16
+ expect(output.join('\n')).toContain('用法:')
17
+ expect(output.join('\n')).toContain('安全保护:')
18
+ expect(output.join('\n')).toContain('显示帮助信息')
19
+ })
20
+
21
+ it('discovers config, syncs catalogs and writes relative report paths', async () => {
22
+ const root = temporaryDirectory.path
23
+ await mkdir(join(root, 'src'), { recursive: true })
24
+ await mkdir(join(root, 'locales'), { recursive: true })
25
+ await writeFile(
26
+ join(root, 'src', 'app.ts'),
27
+ "tr('保存')\ntr (\n `English`\n)\ntr('ignored-' + value)\n",
28
+ 'utf8',
29
+ )
30
+ await writeFile(
31
+ join(root, 'locales', 'zh_CN.json'),
32
+ '{\n "保存": "保存成功",\n "旧文案": "旧翻译"\n}\n',
33
+ 'utf8',
34
+ )
35
+ await writeConfig(root)
36
+
37
+ const output: string[] = []
38
+ const exitCode = await runCli(['sync'], {
39
+ cwd: root,
40
+ stdout: (message) => output.push(message),
41
+ })
42
+
43
+ expect(exitCode).toBe(0)
44
+ expect(JSON.parse(await readFile(join(root, 'locales', 'zh_CN.json'), 'utf8'))).toEqual({
45
+ 保存: '保存成功',
46
+ 旧文案: '旧翻译',
47
+ English: 'English',
48
+ })
49
+ expect(JSON.parse(await readFile(join(root, 'locales', 'en_US.json'), 'utf8'))).toEqual({
50
+ English: 'English',
51
+ 保存: '保存',
52
+ })
53
+
54
+ const report = JSON.parse(await readFile(join(root, '.reports', 'i18n.json'), 'utf8'))
55
+ expect(report.command).toBe('sync')
56
+ expect(report.files).toEqual(
57
+ expect.arrayContaining([
58
+ expect.objectContaining({ path: 'locales/zh_CN.json', unused: ['旧文案'] }),
59
+ expect.objectContaining({ path: 'locales/en_US.json' }),
60
+ ]),
61
+ )
62
+ expect(output.join('\n')).toContain('Project web: 1 source files, 2 keys.')
63
+ })
64
+
65
+ it('checks without writing and can fail on unused keys', async () => {
66
+ const root = temporaryDirectory.path
67
+ await mkdir(join(root, 'src'), { recursive: true })
68
+ await mkdir(join(root, 'locales'), { recursive: true })
69
+ await writeFile(join(root, 'src', 'app.ts'), "tr('new')\n", 'utf8')
70
+ await writeFile(join(root, 'locales', 'en_US.json'), '{"old":"Translated"}\n', 'utf8')
71
+ await writeFile(join(root, 'locales', 'zh_CN.json'), '{}\n', 'utf8')
72
+ await writeConfig(root)
73
+
74
+ const before = await readFile(join(root, 'locales', 'en_US.json'), 'utf8')
75
+ const exitCode = await runCli(['check', '--fail-on-stale'], {
76
+ cwd: join(root, 'src'),
77
+ stdout: () => undefined,
78
+ })
79
+
80
+ expect(exitCode).toBe(1)
81
+ expect(await readFile(join(root, 'locales', 'en_US.json'), 'utf8')).toBe(before)
82
+ const report = JSON.parse(await readFile(join(root, '.reports', 'i18n.json'), 'utf8'))
83
+ expect(report).toMatchObject({ command: 'check', dryRun: true })
84
+ })
85
+
86
+ it('refuses clean when include patterns match no source files', async () => {
87
+ const root = temporaryDirectory.path
88
+ await mkdir(join(root, 'locales'), { recursive: true })
89
+ await writeFile(join(root, 'locales', 'en_US.json'), '{"old":"Translation"}\n', 'utf8')
90
+ await writeFile(join(root, 'locales', 'zh_CN.json'), '{}\n', 'utf8')
91
+ await writeConfig(root)
92
+
93
+ await expect(
94
+ runCli(['sync', '--clean', '--allow-empty'], { cwd: root, stdout: () => undefined }),
95
+ ).rejects.toBeInstanceOf(CliUsageError)
96
+ expect(JSON.parse(await readFile(join(root, 'locales', 'en_US.json'), 'utf8'))).toEqual({
97
+ old: 'Translation',
98
+ })
99
+ })
100
+
101
+ it('validates catalog ownership across unselected projects', async () => {
102
+ const root = temporaryDirectory.path
103
+ await mkdir(join(root, 'src'), { recursive: true })
104
+ await mkdir(join(root, 'locales'), { recursive: true })
105
+ await writeFile(join(root, 'src', 'app.ts'), "tr('new')\n", 'utf8')
106
+ const sharedCatalog = join(root, 'locales', 'shared.json')
107
+ await writeFile(sharedCatalog, '{"old":"Translation"}\n', 'utf8')
108
+ await writeFile(
109
+ join(root, 'i18n.config.mjs'),
110
+ `export default {
111
+ projects: {
112
+ web: { include: ['src/**/*.ts'], catalogs: { en_US: 'locales/shared.json' } },
113
+ admin: { include: ['src/**/*.ts'], catalogs: { zh_CN: 'locales/shared.json' } }
114
+ }
115
+ }\n`,
116
+ 'utf8',
117
+ )
118
+
119
+ await expect(
120
+ runCli(['sync', '--project', 'web'], { cwd: root, stdout: () => undefined }),
121
+ ).rejects.toThrow('configured by both web/en_US and admin/zh_CN')
122
+ expect(await readFile(sharedCatalog, 'utf8')).toBe('{"old":"Translation"}\n')
123
+ })
124
+
125
+ it('refuses report paths that overwrite config or existing non-report files', async () => {
126
+ const root = temporaryDirectory.path
127
+ await mkdir(join(root, 'src'), { recursive: true })
128
+ await writeFile(join(root, 'src', 'app.ts'), "tr('new')\n", 'utf8')
129
+ const configPath = join(root, 'i18n.config.mjs')
130
+ const configSource = `export default {
131
+ projects: {
132
+ web: { include: ['src/**/*.ts'], catalogs: { en_US: 'locales/en_US.json' } }
133
+ },
134
+ report: 'i18n.config.mjs'
135
+ }\n`
136
+ await writeFile(configPath, configSource, 'utf8')
137
+
138
+ await expect(runCli(['sync'], { cwd: root, stdout: () => undefined })).rejects.toThrow(
139
+ 'Report path must not overwrite the config file',
140
+ )
141
+ expect(await readFile(configPath, 'utf8')).toBe(configSource)
142
+
143
+ const existingPath = join(root, 'notes.json')
144
+ const existingSource = '{"notes":"keep me"}\n'
145
+ await writeFile(existingPath, existingSource, 'utf8')
146
+ const secondConfigPath = join(root, 'other.config.mjs')
147
+ await writeFile(secondConfigPath, configSource.replace('i18n.config.mjs', 'notes.json'), 'utf8')
148
+
149
+ await expect(
150
+ runCli(['sync', '--config', secondConfigPath], {
151
+ cwd: root,
152
+ stdout: () => undefined,
153
+ }),
154
+ ).rejects.toThrow('Refusing to overwrite existing non-report file')
155
+ expect(await readFile(existingPath, 'utf8')).toBe(existingSource)
156
+ })
157
+
158
+ it('treats prototype names as unknown projects', async () => {
159
+ const root = temporaryDirectory.path
160
+ await writeConfig(root)
161
+
162
+ await expect(
163
+ runCli(['sync', '--project', 'toString'], { cwd: root, stdout: () => undefined }),
164
+ ).rejects.toThrow('Unknown project "toString"')
165
+ })
166
+
167
+ it('allows stale keys by default in check and fails with --fail-on-stale', async () => {
168
+ const root = temporaryDirectory.path
169
+ await mkdir(join(root, 'src'), { recursive: true })
170
+ await mkdir(join(root, 'locales'), { recursive: true })
171
+ await writeFile(join(root, 'src', 'app.ts'), "tr('used')\n", 'utf8')
172
+ await writeFile(join(root, 'locales', 'en_US.json'), '{"used":"Used","old":"Old"}\n', 'utf8')
173
+ await writeFile(join(root, 'locales', 'zh_CN.json'), '{"used":"使用","old":"旧文案"}\n', 'utf8')
174
+ await writeConfig(root)
175
+
176
+ await expect(runCli(['check'], { cwd: root, stdout: () => undefined })).resolves.toBe(0)
177
+ await expect(
178
+ runCli(['check', '--fail-on-stale'], { cwd: root, stdout: () => undefined }),
179
+ ).resolves.toBe(1)
180
+ })
181
+
182
+ it('rejects unsafe option combinations', async () => {
183
+ await expect(
184
+ runCli(['sync', '--allow-empty'], {
185
+ cwd: temporaryDirectory.path,
186
+ stdout: () => undefined,
187
+ }),
188
+ ).rejects.toThrow('--allow-empty requires --clean')
189
+ })
190
+ })
191
+
192
+ async function writeConfig(root: string): Promise<void> {
193
+ await writeFile(
194
+ join(root, 'i18n.config.mjs'),
195
+ `export default {
196
+ projects: {
197
+ web: {
198
+ include: ['src/**/*.ts'],
199
+ catalogs: {
200
+ zh_CN: 'locales/zh_CN.json',
201
+ en_US: 'locales/en_US.json'
202
+ }
203
+ }
204
+ },
205
+ report: '.reports/i18n.json'
206
+ }\n`,
207
+ 'utf8',
208
+ )
209
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vite-plus/test'
2
+
3
+ import { defineConfig } from '../src/config.ts'
4
+
5
+ describe('defineConfig', () => {
6
+ it('returns the same config object', () => {
7
+ const config = {
8
+ callNames: ['tr'],
9
+ projects: {
10
+ web: {
11
+ include: ['apps/web/src/**/*.ts'],
12
+ catalogs: { en_US: 'apps/web/src/locales/en_US.json' },
13
+ },
14
+ },
15
+ }
16
+
17
+ expect(defineConfig(config)).toBe(config)
18
+ })
19
+ })
@@ -0,0 +1,194 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ import { describe, expect, it } from 'vite-plus/test'
5
+
6
+ import {
7
+ CONFIG_FILE_NAMES,
8
+ ConfigLoadError,
9
+ ConfigNotFoundError,
10
+ ConfigValidationError,
11
+ loadConfig,
12
+ validateConfig,
13
+ } from '../src/load-config.ts'
14
+ import { useTemporaryDirectory } from './temporary-directory.ts'
15
+
16
+ const validConfig = {
17
+ callNames: ['tr'],
18
+ projects: {
19
+ web: {
20
+ include: ['src/**/*.{ts,tsx}'],
21
+ exclude: [],
22
+ catalogs: {
23
+ zh_CN: 'locales/zh_CN.json',
24
+ },
25
+ },
26
+ },
27
+ report: '.bubbles-i18n/report.json',
28
+ }
29
+
30
+ describe('loadConfig', () => {
31
+ const temporaryDirectory = useTemporaryDirectory()
32
+
33
+ it.each([
34
+ ['i18n.config.ts', createEsmConfigSource()],
35
+ ['i18n.config.mts', createEsmConfigSource()],
36
+ ['i18n.config.js', createCommonJsConfigSource()],
37
+ ['i18n.config.mjs', createEsmConfigSource()],
38
+ ['i18n.config.cts', createCommonJsConfigSource()],
39
+ ['i18n.config.cjs', createCommonJsConfigSource()],
40
+ ])('discovers %s while walking upward', async (fileName, source) => {
41
+ const configPath = join(temporaryDirectory.path, fileName)
42
+ const cwd = join(temporaryDirectory.path, 'apps', 'web', 'src')
43
+ await mkdir(cwd, { recursive: true })
44
+ await writeFile(configPath, source, 'utf8')
45
+
46
+ const loaded = await loadConfig({ cwd })
47
+
48
+ expect(loaded).toEqual({
49
+ configPath,
50
+ rootDir: temporaryDirectory.path,
51
+ config: validConfig,
52
+ })
53
+ })
54
+
55
+ it('uses the first candidate when multiple config formats exist in one directory', async () => {
56
+ const tsPath = join(temporaryDirectory.path, CONFIG_FILE_NAMES[0])
57
+ await writeFile(tsPath, createEsmConfigSource(), 'utf8')
58
+ await writeFile(
59
+ join(temporaryDirectory.path, 'i18n.config.mjs'),
60
+ 'export default { projects: {} }',
61
+ 'utf8',
62
+ )
63
+
64
+ const loaded = await loadConfig({ cwd: temporaryDirectory.path })
65
+
66
+ expect(loaded.configPath).toBe(tsPath)
67
+ })
68
+
69
+ it('resolves an explicit config path relative to cwd', async () => {
70
+ const cwd = join(temporaryDirectory.path, 'workspace')
71
+ const configPath = join(cwd, 'config', 'custom-i18n.ts')
72
+ await mkdir(dirname(configPath), { recursive: true })
73
+ await writeFile(configPath, createEsmConfigSource(), 'utf8')
74
+
75
+ const loaded = await loadConfig({ cwd, configPath: './config/custom-i18n.ts' })
76
+
77
+ expect(loaded.configPath).toBe(configPath)
78
+ expect(loaded.rootDir).toBe(dirname(configPath))
79
+ expect(loaded.config).toEqual(validConfig)
80
+ })
81
+
82
+ it('reports the searched directory and supported names when discovery fails', async () => {
83
+ await expect(loadConfig({ cwd: temporaryDirectory.path })).rejects.toThrowError(
84
+ ConfigNotFoundError,
85
+ )
86
+ await expect(loadConfig({ cwd: temporaryDirectory.path })).rejects.toThrow(
87
+ `Could not find an i18n config from "${resolve(temporaryDirectory.path)}" upward`,
88
+ )
89
+ })
90
+
91
+ it('reports the resolved path when an explicit config is missing', async () => {
92
+ const expectedPath = join(temporaryDirectory.path, 'missing.ts')
93
+
94
+ await expect(
95
+ loadConfig({ cwd: temporaryDirectory.path, configPath: './missing.ts' }),
96
+ ).rejects.toThrow(`Could not find the explicit i18n config at "${expectedPath}"`)
97
+ })
98
+
99
+ it('requires a default export', async () => {
100
+ const configPath = join(temporaryDirectory.path, 'i18n.config.mjs')
101
+ await writeFile(configPath, 'export const config = {}', 'utf8')
102
+
103
+ await expect(loadConfig({ cwd: temporaryDirectory.path })).rejects.toThrowError(
104
+ ConfigValidationError,
105
+ )
106
+ await expect(loadConfig({ cwd: temporaryDirectory.path })).rejects.toThrow(
107
+ 'the module must have a default export',
108
+ )
109
+ })
110
+
111
+ it('wraps module evaluation failures with the config path and preserves the cause', async () => {
112
+ const configPath = join(temporaryDirectory.path, 'i18n.config.mjs')
113
+ await writeFile(configPath, 'throw new Error("module failed")', 'utf8')
114
+
115
+ const error = await loadConfig({ cwd: temporaryDirectory.path }).catch(
116
+ (caught: unknown) => caught,
117
+ )
118
+
119
+ expect(error).toBeInstanceOf(ConfigLoadError)
120
+ expect(error).toMatchObject({
121
+ message: `Failed to load i18n config at "${configPath}"`,
122
+ cause: expect.objectContaining({ message: 'module failed' }),
123
+ })
124
+ })
125
+ })
126
+
127
+ describe('validateConfig', () => {
128
+ it('accepts a complete config and returns the original object', () => {
129
+ expect(validateConfig(validConfig)).toBe(validConfig)
130
+ })
131
+
132
+ it.each([
133
+ [undefined, 'the default export must be an object'],
134
+ [{}, 'projects must be a non-empty object'],
135
+ [{ projects: {} }, 'projects must be a non-empty object'],
136
+ [{ projects: { web: null } }, 'projects["web"] must be an object'],
137
+ [
138
+ { projects: { web: { include: [], catalogs: { en_US: 'en.json' } } } },
139
+ 'projects["web"].include must be a non-empty array of strings',
140
+ ],
141
+ [
142
+ { projects: { web: { include: [''], catalogs: { en_US: 'en.json' } } } },
143
+ 'projects["web"].include[0] must be a non-empty string',
144
+ ],
145
+ [
146
+ {
147
+ projects: {
148
+ web: { include: ['src/**/*.ts'], exclude: 'dist', catalogs: { en_US: 'en.json' } },
149
+ },
150
+ },
151
+ 'projects["web"].exclude must be an array of non-empty strings',
152
+ ],
153
+ [
154
+ { projects: { web: { include: ['src/**/*.ts'], catalogs: {} } } },
155
+ 'projects["web"].catalogs must be a non-empty object',
156
+ ],
157
+ [
158
+ { projects: { web: { include: ['src/**/*.ts'], catalogs: { en_US: '' } } } },
159
+ 'projects["web"].catalogs["en_US"] must be a non-empty string',
160
+ ],
161
+ [
162
+ {
163
+ callNames: [],
164
+ projects: { web: { include: ['src/**/*.ts'], catalogs: { en_US: 'en.json' } } },
165
+ },
166
+ 'callNames must be a non-empty array of strings',
167
+ ],
168
+ [
169
+ {
170
+ callNames: [' '],
171
+ projects: { web: { include: ['src/**/*.ts'], catalogs: { en_US: 'en.json' } } },
172
+ },
173
+ 'callNames[0] must be a non-empty string',
174
+ ],
175
+ [
176
+ {
177
+ projects: { web: { include: ['src/**/*.ts'], catalogs: { en_US: 'en.json' } } },
178
+ report: '',
179
+ },
180
+ 'report must be a non-empty string',
181
+ ],
182
+ ])('rejects invalid config %#', (config, message) => {
183
+ expect(() => validateConfig(config, 'test.config.ts')).toThrowError(ConfigValidationError)
184
+ expect(() => validateConfig(config, 'test.config.ts')).toThrow(message)
185
+ })
186
+ })
187
+
188
+ function createEsmConfigSource(): string {
189
+ return `export default ${JSON.stringify(validConfig)}`
190
+ }
191
+
192
+ function createCommonJsConfigSource(): string {
193
+ return `module.exports = ${JSON.stringify(validConfig)}`
194
+ }