@starci/hfs 5.0.8 → 5.2.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 (339) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +10 -1
  3. package/cache/import-graph.mjs +6 -61
  4. package/cache/lint-family.d.mts +10 -1
  5. package/cache/lint-family.mjs +12 -5
  6. package/codemod/directive-first.d.mts +48 -0
  7. package/codemod/directive-first.mjs +136 -0
  8. package/codemod/explicit-surface-edit.d.mts +14 -0
  9. package/codemod/explicit-surface-edit.mjs +57 -0
  10. package/codemod/explicit-surface-names.d.mts +30 -0
  11. package/codemod/explicit-surface-names.mjs +135 -0
  12. package/codemod/explicit-surface-render.d.mts +29 -0
  13. package/codemod/explicit-surface-render.mjs +197 -0
  14. package/codemod/explicit-surface.d.mts +50 -0
  15. package/codemod/explicit-surface.mjs +374 -0
  16. package/codemod/main.d.mts +7 -5
  17. package/codemod/main.mjs +37 -16
  18. package/codemod/source-tree.d.mts +8 -0
  19. package/codemod/source-tree.mjs +23 -0
  20. package/db/postgres.mjs +9 -1
  21. package/lint/eslint-list.cjs +29 -0
  22. package/lint/run.d.mts +19 -5
  23. package/lint/run.mjs +47 -14
  24. package/package.json +1 -1
  25. package/runtime/knowledge/hfs/canon-pins.yaml +31 -10
  26. package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
  27. package/runtime/knowledge/hfs/fork-images.yaml +7 -0
  28. package/runtime/knowledge/hfs/rules.yaml +107 -8
  29. package/runtime/knowledge/hfs/slots.yaml +75 -24
  30. package/runtime/modules/kernel/failure-codes.yaml +62 -0
  31. package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
  32. package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
  33. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
  34. package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
  35. package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
  36. package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
  37. package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
  38. package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
  39. package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
  40. package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
  41. package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
  42. package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
  43. package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
  44. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
  45. package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
  46. package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
  47. package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
  48. package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
  49. package/runtime/scripts/hfs/secret.mjs +10 -7
  50. package/runtime/scripts/hfs/slots.mjs +1 -1
  51. package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
  52. package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
  53. package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
  54. package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
  55. package/runtime/scripts/lib/declaration-surface.mjs +112 -0
  56. package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
  57. package/runtime/scripts/lib/swarm-stack.mjs +2 -2
  58. package/scaffold/add-module.d.mts +16 -0
  59. package/scaffold/add-module.mjs +232 -0
  60. package/scaffold/add-queue-lite.mjs +49 -0
  61. package/scaffold/add.mjs +5 -0
  62. package/scaffold/app.mjs +7 -7
  63. package/scaffold/module-registry.d.mts +19 -0
  64. package/scaffold/module-registry.mjs +84 -0
  65. package/src/main.mjs +23 -2
  66. package/stack/cli.mjs +6 -1
  67. package/stack/context.d.mts +15 -0
  68. package/stack/context.mjs +1 -0
  69. package/stack/local-cli.d.mts +25 -0
  70. package/stack/local-cli.mjs +89 -0
  71. package/stack/local-config.d.mts +17 -0
  72. package/stack/local-config.mjs +37 -0
  73. package/stack/local-custody.d.mts +24 -0
  74. package/stack/local-custody.mjs +109 -0
  75. package/stack/local-doctor.d.mts +6 -0
  76. package/stack/local-doctor.mjs +49 -0
  77. package/stack/local-down.d.mts +8 -0
  78. package/stack/local-down.mjs +20 -0
  79. package/stack/local-host.d.mts +42 -0
  80. package/stack/local-host.mjs +163 -0
  81. package/stack/local-logs.d.mts +8 -0
  82. package/stack/local-logs.mjs +32 -0
  83. package/stack/local-ports.d.mts +7 -0
  84. package/stack/local-ports.mjs +39 -0
  85. package/stack/local-prepare.d.mts +22 -0
  86. package/stack/local-prepare.mjs +62 -0
  87. package/stack/local-run.d.mts +71 -0
  88. package/stack/local-run.mjs +200 -0
  89. package/stack/local-state.d.mts +35 -0
  90. package/stack/local-state.mjs +29 -0
  91. package/stack/local-status.d.mts +6 -0
  92. package/stack/local-status.mjs +36 -0
  93. package/stack/local-up.d.mts +11 -0
  94. package/stack/local-up.mjs +166 -0
  95. package/stack/local-verify.d.mts +6 -0
  96. package/stack/local-verify.mjs +21 -0
  97. package/sync/index.d.mts +13 -12
  98. package/sync/index.mjs +83 -28
  99. package/templates/app/module/jest.config.js.tpl +8 -0
  100. package/templates/app/module/src/index.ts.tpl +2 -0
  101. package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
  102. package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
  103. package/templates/app/module/src/manifest/index.ts.tpl +1 -0
  104. package/templates/app/module/src/public/index.ts.tpl +2 -0
  105. package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
  106. package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
  107. package/templates/app/module/src/template/index.ts.tpl +1 -0
  108. package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
  109. package/templates/app/module/src/workbench/index.ts.tpl +2 -0
  110. package/templates/app/package-scripts/package.json +1 -1
  111. package/templates/app/package-scripts-lite/package.json +1 -1
  112. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +76 -70
  113. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +23 -13
  114. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +25 -0
  115. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -26
  116. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -0
  117. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +2 -1
  118. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +57 -14
  119. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +11 -0
  120. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
  121. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
  122. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
  123. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
  124. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
  125. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
  126. package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
  127. package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
  128. package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
  129. package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
  130. package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
  131. package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
  132. package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
  133. package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
  134. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +13 -0
  135. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +16 -0
  136. package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
  137. package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
  138. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +13 -0
  139. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
  140. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
  141. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +90 -0
  142. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
  143. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
  144. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
  145. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +28 -0
  146. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +145 -0
  147. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +95 -0
  148. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +38 -0
  149. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +54 -0
  150. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
  151. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
  152. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
  153. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +17 -0
  154. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
  155. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
  156. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
  157. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
  158. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +78 -0
  159. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +87 -0
  160. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +44 -0
  161. package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
  162. package/templates/be/inbox/entities.index.ts.tpl +1 -1
  163. package/templates/be/inbox/module-definition.ts.tpl +8 -2
  164. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
  165. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
  166. package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
  167. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
  168. package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
  169. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
  170. package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
  171. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
  172. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
  173. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
  174. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
  175. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
  176. package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
  177. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
  178. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
  179. package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
  180. package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
  181. package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
  182. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
  183. package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
  184. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
  185. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
  186. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
  187. package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
  188. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
  189. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
  190. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
  191. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
  192. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
  193. package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
  194. package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
  195. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
  196. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
  197. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
  198. package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
  199. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
  200. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
  201. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
  202. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
  203. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
  204. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
  205. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
  206. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
  207. package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
  208. package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
  209. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
  210. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
  211. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
  212. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
  213. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
  214. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
  215. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
  216. package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
  217. package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
  218. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
  219. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
  220. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
  221. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
  222. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
  223. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
  224. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +10 -5
  225. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +34 -0
  226. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +23 -2
  227. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +47 -2
  228. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -0
  229. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +24 -0
  230. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
  231. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
  232. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
  233. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +12 -0
  234. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
  235. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
  236. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
  237. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
  238. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
  239. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
  240. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
  241. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
  242. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
  243. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
  244. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
  245. package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
  246. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
  247. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
  248. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
  249. package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
  250. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  251. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
  252. package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
  253. package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
  254. package/templates/be/table/entities.index.ts.tpl +1 -1
  255. package/templates/be/table/module-definition.ts.tpl +8 -3
  256. package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
  257. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -3
  258. package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
  259. package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
  260. package/templates/fe/common/app-layout.tsx.partial +1 -1
  261. package/templates/fe/common/site-shell.tsx.partial +3 -3
  262. package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
  263. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
  264. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
  265. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
  266. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
  267. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
  268. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
  269. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
  270. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +18 -12
  271. package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
  272. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
  273. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
  274. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
  275. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
  276. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
  277. package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
  278. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
  279. package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
  280. package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  281. package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
  282. package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
  283. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
  284. package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
  285. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
  286. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
  287. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
  288. package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
  289. package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
  290. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +20 -2
  291. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
  292. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
  293. package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
  294. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
  295. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
  296. package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
  297. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
  298. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
  299. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
  300. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
  301. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  302. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
  303. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
  304. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
  305. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
  306. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  307. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
  308. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
  309. package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
  310. package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
  311. package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
  312. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
  313. package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
  314. package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
  315. package/templates/fe/units/page/index.tsx.tpl +3 -2
  316. package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
  317. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
  318. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
  319. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
  320. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
  321. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
  322. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
  323. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
  324. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
  325. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
  326. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
  327. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
  328. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
  329. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
  330. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
  331. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
  332. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -37
  333. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -36
  334. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
  335. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
  336. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
  337. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
  338. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
  339. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
@@ -0,0 +1,374 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { declarationSurface, isEmittableSource } from '../runtime/scripts/lib/declaration-surface.mjs';
5
+ import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
6
+ import { loadPrettier } from '../sync/format.mjs';
7
+ import { applyEdits, importEdits } from './explicit-surface-edit.mjs';
8
+ import { conventionType, MAX_TYPE_LENGTH, renderType } from './explicit-surface-render.mjs';
9
+ import { GENERATED, relativePosix, sideFolders } from './source-tree.mjs';
10
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const posix = (file) => file.replaceAll('\\', '/');
12
+ /** A file a generator wrote: it is regenerated, never edited. */
13
+ const GENERATED_HEADER = /@generated|do not edit|auto-generated|autogenerated/i;
14
+ /** What to do about an export the classifier reports and the codemod does not edit, by what the classifier says to annotate. */
15
+ const SUGGESTIONS = {
16
+ 'destructured export': 'export each name as its own variable with a type: `export const name: Type = source.name`',
17
+ 'accessor type': 'write the accessor type: `get name(): Type`',
18
+ 'property type': 'write the property type: `name: Type = ...`',
19
+ 'exported value': 'declare a named type, export it and annotate the export: `export default value satisfies Type` or `export const name: Type = ...`',
20
+ };
21
+ /** The innermost node of `source` that contains `start`. */
22
+ function innermostAt(ts, source, start) {
23
+ let found = source;
24
+ const descend = (node) => {
25
+ if (start < node.getStart(source) || start >= node.end)
26
+ return;
27
+ found = node;
28
+ ts.forEachChild(node, descend);
29
+ };
30
+ ts.forEachChild(source, descend);
31
+ return found;
32
+ }
33
+ /** The declaration a diagnostic of the isolated declaration emit sits in, or undefined when the codemod has no annotation for it. */
34
+ function locate(ts, source, problem) {
35
+ for (let node = innermostAt(ts, source, problem.start); node !== undefined && node !== source; node = node.parent) {
36
+ const named = (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && node.name !== undefined && node.name.getStart(source) === problem.start;
37
+ if ((problem.code === 9007 || problem.code === 9008) && (named || ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.getStart(source) === problem.start))) {
38
+ return { kind: 'return', node: node };
39
+ }
40
+ if (problem.code === 9010 && ts.isVariableDeclaration(node) && node.name.getStart(source) === problem.start)
41
+ return { kind: 'variable', node };
42
+ if ((problem.code === 9011 || problem.code === 9025) && ts.isParameter(node))
43
+ return { kind: 'parameter', node };
44
+ if (problem.code === 9012 && ts.isPropertyDeclaration(node) && node.name.getStart(source) === problem.start)
45
+ return { kind: 'property', node };
46
+ }
47
+ return problem.annotate === 'exported value' ? declarationOfExpression(ts, source, problem.start) : undefined;
48
+ }
49
+ /** The function, variable, parameter or property whose type an expression the emit cannot read (an array, an object with a shorthand, a call) stands for. */
50
+ function declarationOfExpression(ts, source, start) {
51
+ for (let node = innermostAt(ts, source, start); node !== source; node = node.parent) {
52
+ const { parent } = node;
53
+ if (ts.isReturnStatement(node)) {
54
+ for (let owner = node; owner !== source; owner = owner.parent) {
55
+ if (ts.isFunctionDeclaration(owner) || ts.isMethodDeclaration(owner) || ts.isArrowFunction(owner) || ts.isFunctionExpression(owner))
56
+ return { kind: 'return', node: owner };
57
+ }
58
+ return undefined;
59
+ }
60
+ if (ts.isArrowFunction(parent) && parent.body === node)
61
+ return { kind: 'return', node: parent };
62
+ if (ts.isVariableDeclaration(parent) && parent.initializer === node)
63
+ return { kind: 'variable', node: parent };
64
+ if (ts.isParameter(parent) && parent.initializer === node)
65
+ return { kind: 'parameter', node: parent };
66
+ if (ts.isPropertyDeclaration(parent) && parent.initializer === node)
67
+ return { kind: 'property', node: parent };
68
+ }
69
+ return undefined;
70
+ }
71
+ /** The edits that write `typeText` on `target`. */
72
+ function annotationEdits(ts, source, target, typeText) {
73
+ const insert = (at, text) => ({ start: at, end: at, text });
74
+ if (target.kind === 'variable')
75
+ return [insert(target.node.exclamationToken?.end ?? target.node.name.end, `: ${typeText}`)];
76
+ if (target.kind === 'parameter')
77
+ return [insert(target.node.questionToken?.end ?? target.node.name.end, `: ${typeText}`)];
78
+ if (target.kind === 'property')
79
+ return [insert((target.node.questionToken ?? target.node.exclamationToken)?.end ?? target.node.name.end, `: ${typeText}`)];
80
+ const { node } = target;
81
+ const children = node.getChildren(source);
82
+ const open = children.find(child => child.kind === ts.SyntaxKind.OpenParenToken);
83
+ if (open === undefined) {
84
+ // `x => x` has no parentheses to carry a return type: `(x): T => x`.
85
+ const only = node.parameters[0];
86
+ return [insert(only.getStart(source), '('), insert(only.end, `): ${typeText}`)];
87
+ }
88
+ const close = children.find(child => child.kind === ts.SyntaxKind.CloseParenToken && child.pos >= node.parameters.end);
89
+ return [insert(close.end, `: ${typeText}`)];
90
+ }
91
+ /** The change kind of a target, as the report names it. */
92
+ const CODE = { return: 'RETURN_TYPE', variable: 'VARIABLE_TYPE', parameter: 'PARAMETER_TYPE', property: 'PROPERTY_TYPE' };
93
+ /** The type to write for `target`, from the checker, or the reason it is not written. */
94
+ function typeFor(c, target, name, maxLength) {
95
+ const { checker, ts } = c;
96
+ if (target.kind === 'return') {
97
+ const signature = checker.getSignatureFromDeclaration(target.node);
98
+ if (signature === undefined || target.node.body === undefined)
99
+ return { ok: false, code: 'NO_SIGNATURE', reason: 'the function has no body to infer from', suggestion: 'write the return type by hand' };
100
+ if (checker.getTypePredicateOfSignature(signature) !== undefined)
101
+ return { ok: false, code: 'TYPE_PREDICATE', reason: 'the return type is an inferred type predicate, which `boolean` would erase', suggestion: 'write the predicate: `value is Type`' };
102
+ const inferred = checker.getReturnTypeOfSignature(signature);
103
+ return conventionType(c, name, inferred, target.node, 'function') ?? renderType(c, inferred, target.node, maxLength);
104
+ }
105
+ const node = target.kind === 'parameter' ? target.node : target.node.name;
106
+ if (target.kind === 'parameter' && !ts.isIdentifier(target.node.name))
107
+ return { ok: false, code: 'DESTRUCTURED_PARAMETER', reason: 'the parameter is a destructuring pattern', suggestion: 'write the parameter type: `({ a, b }: Props)`' };
108
+ const inferred = checker.getTypeAtLocation(node);
109
+ return (target.kind === 'variable' ? conventionType(c, name, inferred, node, 'variable') : undefined) ?? renderType(c, inferred, node, maxLength);
110
+ }
111
+ /** Plans the annotations of one open file; every problem it does not annotate becomes a finding. */
112
+ function planFile(c, file, problems, maxLength) {
113
+ const plan = { edits: [], needs: [], changes: [], findings: [] };
114
+ const seen = new Set();
115
+ for (const problem of problems) {
116
+ const place = c.source.getLineAndCharacterOfPosition(problem.start);
117
+ const where = { file, line: place.line + 1, column: place.character + 1, name: problem.name, annotate: problem.annotate };
118
+ const target = locate(c.ts, c.source, problem);
119
+ if (target === undefined) {
120
+ plan.findings.push({ ...where, code: problem.annotate === 'destructured export' ? 'DESTRUCTURED_EXPORT' : 'NOT_ANNOTATED', reason: problem.reason, suggestion: SUGGESTIONS[problem.annotate] ?? 'annotate it by hand' });
121
+ continue;
122
+ }
123
+ const key = `${target.kind}:${target.node.pos}`;
124
+ if (seen.has(key))
125
+ continue;
126
+ seen.add(key);
127
+ const rendered = typeFor(c, target, problem.name, maxLength);
128
+ if (!rendered.ok) {
129
+ plan.findings.push({ ...where, code: rendered.code, reason: rendered.reason, suggestion: rendered.suggestion });
130
+ continue;
131
+ }
132
+ plan.edits.push(...annotationEdits(c.ts, c.source, target, rendered.text));
133
+ plan.needs.push(...rendered.needs);
134
+ plan.changes.push({ ...where, code: CODE[target.kind], from: 'inferred', to: rendered.text });
135
+ }
136
+ return plan;
137
+ }
138
+ /** The diagnostics of a file as a multiset of `code: message`, with positions left out so an edit above them does not count. */
139
+ function diagnosticsOf(ts, program, file) {
140
+ const counts = new Map();
141
+ for (const diagnostic of [...program.getSyntacticDiagnostics(file), ...program.getSemanticDiagnostics(file)]) {
142
+ const key = `${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`;
143
+ counts.set(key, (counts.get(key) ?? 0) + 1);
144
+ }
145
+ return counts;
146
+ }
147
+ /** The first diagnostic the new text has that the old text did not, or undefined. */
148
+ function addedDiagnostic(before, after) {
149
+ for (const [key, count] of after)
150
+ if (count > (before.get(key) ?? 0))
151
+ return key;
152
+ return undefined;
153
+ }
154
+ /** The tsconfig that owns `file`: the nearest `tsconfig.json` from its folder up to `top`, else undefined. */
155
+ function owningConfig(file, top) {
156
+ for (let folder = path.dirname(file); folder.length >= top.length; folder = path.dirname(folder)) {
157
+ const candidate = path.join(folder, 'tsconfig.json');
158
+ if (fs.existsSync(candidate))
159
+ return candidate;
160
+ if (folder === path.dirname(folder))
161
+ break;
162
+ }
163
+ return undefined;
164
+ }
165
+ /** The options and root files of the tsconfig at `config`, or the inferred ones when there is none. */
166
+ function groupOf(ts, config, files) {
167
+ const parsed = config === undefined ? undefined : ts.getParsedCommandLineOfConfigFile(config, {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => undefined });
168
+ const options = parsed?.options ?? { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, strict: true, noEmit: true };
169
+ const known = new Set((parsed?.fileNames ?? []).map(posix));
170
+ return { options: { ...options, noEmit: true }, rootNames: [...known, ...files.map(posix).filter(file => !known.has(file))], files: [...files] };
171
+ }
172
+ /** The source file the program holds for `file`, found by its forward-slash name, ignoring the case of the drive letter. */
173
+ function programFile(program, file) {
174
+ const normal = posix(file);
175
+ return program.getSourceFile(normal) ?? program.getSourceFile(normal.replace(/^[a-z]:/, drive => drive.toUpperCase())) ?? program.getSourceFile(normal.replace(/^[A-Z]:/, drive => drive.toLowerCase()));
176
+ }
177
+ /** A program that reads `texts` in place of the files on disk, reusing the old program's unchanged files. */
178
+ function programWith(ts, group, texts, old) {
179
+ const host = ts.createCompilerHost(group.options, true);
180
+ const original = host.getSourceFile.bind(host);
181
+ const wanted = new Map([...texts].map(([file, text]) => [posix(file).toLowerCase(), text]));
182
+ host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreate) => {
183
+ const text = wanted.get(posix(fileName).toLowerCase());
184
+ return text === undefined ? original(fileName, languageVersionOrOptions, onError, shouldCreate) : ts.createSourceFile(fileName, text, languageVersionOrOptions, true);
185
+ };
186
+ return ts.createProgram({ rootNames: group.rootNames, options: group.options, host, ...(old === undefined ? {} : { oldProgram: old }) });
187
+ }
188
+ /** The files of a side the codemod reads: those the installed lint canon selects, else every TypeScript source outside generated folders. */
189
+ async function sideSources(root, side, folder) {
190
+ let selects;
191
+ let ignores;
192
+ try {
193
+ const lint = await import('../lint/run.mjs');
194
+ const scope = lint.canonScope(await lint.installedLintConfig({ repoRoot: root, cwd: folder, side }));
195
+ selects = scope.source;
196
+ ignores = (relative) => scope.ignored(relative) || scope.ignored(`${relative}/__starci_scope_probe__`);
197
+ }
198
+ catch { /* an app without its canon installed is read by walking */ }
199
+ const files = [];
200
+ const visit = (directory) => {
201
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
202
+ const target = path.join(directory, entry.name);
203
+ const relative = relativePosix(folder, target);
204
+ if (entry.isDirectory()) {
205
+ if (!GENERATED.has(entry.name) && ignores?.(relative) !== true)
206
+ visit(target);
207
+ }
208
+ else if (entry.isFile() && isEmittableSource(entry.name) && (selects?.(relative) ?? true) && ignores?.(relative) !== true)
209
+ files.push(target);
210
+ }
211
+ };
212
+ visit(folder);
213
+ return { files: files.sort(), scope: selects === undefined ? 'walk' : 'canon' };
214
+ }
215
+ /** The app's own Prettier, from the first side or the root that installs one, or undefined. */
216
+ function prettierOf(root, folders) {
217
+ for (const folder of [...folders, root]) {
218
+ try {
219
+ return loadPrettier(folder);
220
+ }
221
+ catch { /* the next folder */ }
222
+ }
223
+ return undefined;
224
+ }
225
+ /** `text` formatted by the app's Prettier the way `app check --format` judges it, or unchanged when the file is ignored or cannot be formatted. */
226
+ async function formatted(prettier, root, file, text) {
227
+ const ignorePath = path.join(root, '.prettierignore');
228
+ try {
229
+ const info = await prettier.getFileInfo(file, { ignorePath: fs.existsSync(ignorePath) ? ignorePath : undefined, resolveConfig: true });
230
+ if (info.ignored || !info.inferredParser)
231
+ return text;
232
+ const options = (await prettier.resolveConfig(file, { editorconfig: true, useCache: true })) ?? {};
233
+ return await prettier.format(text, { ...options, filepath: file });
234
+ }
235
+ catch {
236
+ return text;
237
+ }
238
+ }
239
+ /** The side-relative name a report gives `absolute`. */
240
+ const nameOf = (root, absolute) => relativePosix(root, absolute);
241
+ /** Plans every open file of one program, checks that the rewritten files read no worse than the originals, and returns what each file becomes. */
242
+ async function migrateGroup({ ts, root, group, open, maxLength, report }) {
243
+ let program = programWith(ts, group, new Map());
244
+ const extra = ['react', 'next'].flatMap(name => {
245
+ const resolved = ts.resolveModuleName(name, open[0]?.absolute ?? root, group.options, ts.sys).resolvedModule?.resolvedFileName;
246
+ return resolved === undefined || programFile(program, resolved) !== undefined ? [] : [posix(resolved)];
247
+ });
248
+ if (extra.length) {
249
+ group.rootNames = [...group.rootNames, ...extra];
250
+ program = programWith(ts, group, new Map());
251
+ }
252
+ const checker = program.getTypeChecker();
253
+ const rewritten = new Map();
254
+ const planned = new Map();
255
+ for (const file of open) {
256
+ const source = programFile(program, file.absolute);
257
+ if (source === undefined || source.text !== file.text) {
258
+ report.findings.push(...file.problems.map(problem => ({ file: file.relative, line: 1, column: 1, code: 'NOT_IN_PROGRAM', name: problem.name, annotate: problem.annotate, reason: 'the file is not part of the side\'s TypeScript program', suggestion: 'include it in the tsconfig, then run the codemod again' })));
259
+ continue;
260
+ }
261
+ const context = { ts, program, checker, options: group.options, source, file: posix(file.absolute), text: file.text };
262
+ const plan = planFile(context, file.relative, file.problems, maxLength);
263
+ planned.set(file.absolute, plan);
264
+ if (plan.edits.length)
265
+ rewritten.set(file.absolute, applyEdits(file.text, [...plan.edits, ...importEdits(ts, source, file.text, plan.needs)]));
266
+ }
267
+ for (const [absolute, plan] of planned)
268
+ if (!rewritten.has(absolute))
269
+ report.findings.push(...plan.findings);
270
+ if (!rewritten.size)
271
+ return rewritten;
272
+ const after = programWith(ts, group, rewritten, program);
273
+ for (const [absolute, text] of [...rewritten]) {
274
+ const plan = planned.get(absolute);
275
+ const before = programFile(program, absolute);
276
+ const added = addedDiagnostic(diagnosticsOf(ts, program, before), diagnosticsOf(ts, after, programFile(after, absolute)));
277
+ if (added !== undefined) {
278
+ rewritten.delete(absolute);
279
+ report.findings.push(...plan.changes.map(change => ({ file: change.file, line: change.line, column: change.column, code: 'NOT_SOUND', name: change.name, annotate: change.code, reason: `writing \`${change.to}\` makes the compiler report: ${added}`, suggestion: 'write the type by hand' })), ...plan.findings);
280
+ continue;
281
+ }
282
+ report.changes.push(...plan.changes);
283
+ report.findings.push(...plan.findings);
284
+ const remaining = declarationSurface(ts, absolute, text, group.options.module)?.problems ?? [];
285
+ for (const problem of remaining) {
286
+ if (report.findings.some(found => found.file === nameOf(root, absolute) && found.name === problem.name && found.annotate === problem.annotate))
287
+ continue;
288
+ const place = ts.createSourceFile(absolute, text, ts.ScriptTarget.Latest).getLineAndCharacterOfPosition(problem.start);
289
+ report.findings.push({ file: nameOf(root, absolute), line: place.line + 1, column: place.character + 1, code: 'STILL_OPEN', name: problem.name, annotate: problem.annotate, reason: problem.reason, suggestion: SUGGESTIONS[problem.annotate] ?? 'annotate it by hand' });
290
+ }
291
+ }
292
+ return rewritten;
293
+ }
294
+ /**
295
+ * Preview or apply the explicit type annotations of one app: for every TypeScript source of each side that the declaration classifier counts as open
296
+ * (R368, HFS_EXPLICIT_EXPORT_SURFACE), the types the app's own checker infers are written where they are short, named and sound. Every other export is kept in the report.
297
+ */
298
+ export async function migrateExplicitSurface({ root, write = false, ts: given, maxTypeLength = MAX_TYPE_LENGTH }) {
299
+ const folders = sideFolders(root);
300
+ if (!folders.length)
301
+ throw new Error('explicit-surface runs at an app root with a be/ or fe/ directory');
302
+ const report = {
303
+ schema: 'starci/explicit-surface-report@1', root, write, maxTypeLength, filesScanned: 0, filesOpen: 0, filesChanged: [], filesStillOpen: 0, formatted: false, changes: [], findings: [],
304
+ };
305
+ const written = new Map();
306
+ for (const folder of folders) {
307
+ const ts = given ?? loadTypescript(folder, root, PACKAGE_ROOT);
308
+ if (!ts)
309
+ throw new Error('explicit-surface needs TypeScript installed in the app or @starci/hfs development checkout');
310
+ if (ts.transpileDeclaration === undefined)
311
+ throw new Error('explicit-surface needs TypeScript 5.5 or later, which writes declarations file by file');
312
+ const { files } = await sideSources(root, path.basename(folder), folder);
313
+ const groups = new Map();
314
+ for (const absolute of files) {
315
+ const text = fs.readFileSync(absolute, 'utf8');
316
+ report.filesScanned += 1;
317
+ if (GENERATED_HEADER.test(text.slice(0, 400)))
318
+ continue;
319
+ const config = owningConfig(absolute, root);
320
+ const entry = groups.get(config ?? '') ?? { config, files: [] };
321
+ entry.files.push(absolute);
322
+ groups.set(config ?? '', entry);
323
+ }
324
+ for (const { config, files: members } of groups.values()) {
325
+ const group = groupOf(ts, config, members);
326
+ const open = [];
327
+ for (const absolute of members) {
328
+ const text = fs.readFileSync(absolute, 'utf8');
329
+ const surface = declarationSurface(ts, posix(absolute), text, group.options.module);
330
+ if (surface !== null && surface.problems.length)
331
+ open.push({ absolute, relative: nameOf(root, absolute), text, problems: surface.problems });
332
+ }
333
+ report.filesOpen += open.length;
334
+ if (!open.length)
335
+ continue;
336
+ for (const [absolute, text] of await migrateGroup({ ts, root, group, open, maxLength: maxTypeLength, report }))
337
+ written.set(absolute, text);
338
+ }
339
+ }
340
+ const prettier = prettierOf(root, folders);
341
+ report.formatted = prettier !== undefined && written.size > 0;
342
+ for (const [absolute, text] of written) {
343
+ const final = prettier === undefined ? text : await formatted(prettier, root, absolute, text);
344
+ report.filesChanged.push(nameOf(root, absolute));
345
+ if (write)
346
+ fs.writeFileSync(absolute, final);
347
+ }
348
+ report.filesStillOpen = new Set(report.findings.map(finding => finding.file)).size;
349
+ report.changes.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
350
+ report.findings.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
351
+ report.filesChanged.sort();
352
+ return report;
353
+ }
354
+ const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
355
+ /** Render the annotations and what is left for a person as Markdown; JSON consumers read the same report object. */
356
+ export function explicitSurfaceMarkdown(report) {
357
+ const out = [
358
+ '# explicit-surface', '',
359
+ `Scanned ${report.filesScanned} files, ${report.filesOpen} open, ${report.filesChanged.length} ${report.write ? 'changed' : 'would change'}, ${report.changes.length} annotations, ${report.findings.length} exports in ${report.filesStillOpen} files left for a person.`, '',
360
+ ];
361
+ if (report.findings.length) {
362
+ out.push('## Left for a person', '', '| location | export | code | reason | suggested fix |', '|---|---|---|---|---|');
363
+ for (const finding of report.findings)
364
+ out.push(`| ${cell(`${finding.file}:${finding.line}:${finding.column}`)} | ${cell(finding.name)} | ${finding.code} | ${cell(finding.reason)} | ${cell(finding.suggestion)} |`);
365
+ out.push('');
366
+ }
367
+ if (report.changes.length) {
368
+ out.push('## Annotations', '', '| location | export | code | type written |', '|---|---|---|---|');
369
+ for (const item of report.changes)
370
+ out.push(`| ${cell(`${item.file}:${item.line}:${item.column}`)} | ${cell(item.name)} | ${item.code} | \`${cell(item.to)}\` |`);
371
+ out.push('');
372
+ }
373
+ return `${out.join('\n')}\n`;
374
+ }
@@ -1,16 +1,18 @@
1
1
  /** The codemods `starci app codemod` runs, by name. */
2
2
  export declare const CODEMODS: readonly string[];
3
- /** The arguments of `starci app codemod spacing-roles`, `theme-tokens`, `small-renames`, `page-navigation`, `section-frame`, `surface-card-kinds`, `tier-binding` and `loading-states`: --execute, --json, --format and --out; any other is refused. */
3
+ /** The arguments of every report-style `starci app codemod`: --execute, --json, --format and --out; any other is refused. */
4
4
  export declare function spacingRolesArguments(argv: readonly string[]): {
5
5
  write: boolean;
6
6
  format: 'json' | 'markdown';
7
7
  out?: string;
8
8
  };
9
+ type Io = {
10
+ cwd: string;
11
+ stdout: (text: string) => unknown;
12
+ };
9
13
  /**
10
14
  * CLI boundary of `starci app codemod <name>`. Dry run by default; --execute writes only the safe rewrites. A codemod
11
15
  * that leaves a case for a person exits 1 with the complete report.
12
16
  */
13
- export declare function codemodMain(argv: string[], io: {
14
- cwd: string;
15
- stdout: (text: string) => unknown;
16
- }): number;
17
+ export declare function codemodMain(argv: string[], io: Io): number | Promise<number>;
18
+ export {};
package/codemod/main.mjs CHANGED
@@ -7,11 +7,13 @@ import { migrateSectionFrame, sectionFrameMarkdown } from './section-frame.mjs';
7
7
  import { migrateSmallRenames, smallRenamesMarkdown } from './small-renames.mjs';
8
8
  import { migrateSpacingRoles, spacingRolesMarkdown } from './spacing-roles.mjs';
9
9
  import { migrateSurfaceCardKinds, surfaceCardKindsMarkdown } from './surface-card-kinds.mjs';
10
+ import { explicitSurfaceMarkdown, migrateExplicitSurface } from './explicit-surface.mjs';
11
+ import { directiveFirstMarkdown, migrateDirectiveFirst } from './directive-first.mjs';
10
12
  import { migrateThemeTokens, themeTokensMarkdown } from './theme-tokens.mjs';
11
13
  import { migrateTierBinding, tierBindingMarkdown } from './tier-binding.mjs';
12
14
  /** The codemods `starci app codemod` runs, by name. */
13
- export const CODEMODS = Object.freeze(['button-intent', 'spacing-roles', 'theme-tokens', 'small-renames', 'page-navigation', 'section-frame', 'surface-card-kinds', 'tier-binding', 'loading-states']);
14
- /** The arguments of `starci app codemod spacing-roles`, `theme-tokens`, `small-renames`, `page-navigation`, `section-frame`, `surface-card-kinds`, `tier-binding` and `loading-states`: --execute, --json, --format and --out; any other is refused. */
15
+ export const CODEMODS = Object.freeze(['button-intent', 'spacing-roles', 'theme-tokens', 'small-renames', 'page-navigation', 'section-frame', 'surface-card-kinds', 'tier-binding', 'loading-states', 'explicit-surface', 'directive-first']);
16
+ /** The arguments of every report-style `starci app codemod`: --execute, --json, --format and --out; any other is refused. */
15
17
  export function spacingRolesArguments(argv) {
16
18
  const options = { write: false, format: 'markdown' };
17
19
  const args = argv.values();
@@ -44,6 +46,10 @@ export function spacingRolesArguments(argv) {
44
46
  }
45
47
  /** One of the report-style codemods run at an app root: its report object and the Markdown of it. */
46
48
  function runCodemod(name, root, write) {
49
+ if (name === 'directive-first') {
50
+ const report = migrateDirectiveFirst({ root, write });
51
+ return { report, markdown: directiveFirstMarkdown(report) };
52
+ }
47
53
  if (name === 'theme-tokens') {
48
54
  const report = migrateThemeTokens({ root, write });
49
55
  return { report, markdown: themeTokensMarkdown(report) };
@@ -75,6 +81,30 @@ function runCodemod(name, root, write) {
75
81
  const report = migrateSpacingRoles({ root, write });
76
82
  return { report, markdown: spacingRolesMarkdown(report) };
77
83
  }
84
+ /** The report written where the options say (`--out`) and to stdout; the exit code is 1 when a case is left for a person. */
85
+ function emit(options, result, io) {
86
+ const output = options.format === 'json' ? `${JSON.stringify(result.report, null, 2)}\n` : result.markdown;
87
+ if (options.out) {
88
+ const target = path.resolve(io.cwd, options.out);
89
+ fs.mkdirSync(path.dirname(target), { recursive: true });
90
+ fs.writeFileSync(target, output);
91
+ }
92
+ io.stdout(output);
93
+ return result.report.findings.length ? 1 : 0;
94
+ }
95
+ /** The arguments of a report-style codemod: spacingRolesArguments, and a report file that is JSON or Markdown. */
96
+ function reportArguments(argv) {
97
+ const options = spacingRolesArguments(argv);
98
+ if (options.out && !/\.(?:json|md)$/i.test(options.out))
99
+ throw new Error('--out needs a .json or .md report file');
100
+ return options;
101
+ }
102
+ /** `starci app codemod explicit-surface`: it reads the app's types through the compiler and formats with its Prettier, both asynchronous. */
103
+ async function explicitSurfaceMain(argv, io) {
104
+ const options = reportArguments(argv);
105
+ const report = await migrateExplicitSurface({ root: path.resolve(io.cwd), write: options.write });
106
+ return emit(options, { report, markdown: explicitSurfaceMarkdown(report) }, io);
107
+ }
78
108
  /**
79
109
  * CLI boundary of `starci app codemod <name>`. Dry run by default; --execute writes only the safe rewrites. A codemod
80
110
  * that leaves a case for a person exits 1 with the complete report.
@@ -83,19 +113,10 @@ export function codemodMain(argv, io) {
83
113
  const [name, ...rest] = argv;
84
114
  if (name === 'button-intent')
85
115
  return buttonIntentMain(argv, io);
86
- if (name !== 'spacing-roles' && name !== 'theme-tokens' && name !== 'small-renames' && name !== 'page-navigation' && name !== 'section-frame' && name !== 'surface-card-kinds' && name !== 'tier-binding' && name !== 'loading-states')
116
+ if (name === 'explicit-surface')
117
+ return explicitSurfaceMain(rest, io);
118
+ if (name === undefined || !CODEMODS.includes(name))
87
119
  throw new Error(`starci app codemod takes ${CODEMODS.join(' or ')}`);
88
- const options = spacingRolesArguments(rest);
89
- if (options.out && !/\.(?:json|md)$/i.test(options.out))
90
- throw new Error('--out needs a .json or .md report file');
91
- const root = path.resolve(io.cwd);
92
- const { report, markdown } = runCodemod(name, root, options.write);
93
- const output = options.format === 'json' ? `${JSON.stringify(report, null, 2)}\n` : markdown;
94
- if (options.out) {
95
- const target = path.resolve(io.cwd, options.out);
96
- fs.mkdirSync(path.dirname(target), { recursive: true });
97
- fs.writeFileSync(target, output);
98
- }
99
- io.stdout(output);
100
- return report.findings.length ? 1 : 0;
120
+ const options = reportArguments(rest);
121
+ return emit(options, runCodemod(name, path.resolve(io.cwd), options.write), io);
101
122
  }
@@ -0,0 +1,8 @@
1
+ /** Folders no codemod enters: installed packages, build output and caches. */
2
+ export declare const GENERATED: Set<string>;
3
+ /** The folders of an app a codemod reads source from: the `be` and `fe` sides, else `src` of a single-side app, as absolute paths. */
4
+ export declare function sideFolders(root: string): string[];
5
+ /** Every file under `directory` whose name matches `pattern`, sorted, without descending into generated folders. */
6
+ export declare function filesUnder(directory: string, pattern: RegExp): string[];
7
+ /** The posix path of `target` relative to `root`, the way a report names a file. */
8
+ export declare const relativePosix: (root: string, target: string) => string;
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Folders no codemod enters: installed packages, build output and caches. */
4
+ export const GENERATED = new Set(['node_modules', '.next', '.turbo', 'dist', 'build', 'out', 'coverage', '.git', 'storybook-static']);
5
+ /** The folders of an app a codemod reads source from: the `be` and `fe` sides, else `src` of a single-side app, as absolute paths. */
6
+ export function sideFolders(root) {
7
+ const isFolder = (folder) => fs.statSync(folder, { throwIfNoEntry: false })?.isDirectory() === true;
8
+ const sides = ['be', 'fe'].map(name => path.join(root, name)).filter(isFolder);
9
+ if (sides.length)
10
+ return sides;
11
+ return ['src'].map(name => path.join(root, name)).filter(isFolder);
12
+ }
13
+ /** Every file under `directory` whose name matches `pattern`, sorted, without descending into generated folders. */
14
+ export function filesUnder(directory, pattern) {
15
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
16
+ const target = path.join(directory, entry.name);
17
+ if (entry.isDirectory())
18
+ return GENERATED.has(entry.name) ? [] : filesUnder(target, pattern);
19
+ return entry.isFile() && pattern.test(entry.name) ? [target] : [];
20
+ }).sort();
21
+ }
22
+ /** The posix path of `target` relative to `root`, the way a report names a file. */
23
+ export const relativePosix = (root, target) => path.relative(root, target).split(path.sep).join('/');
package/db/postgres.mjs CHANGED
@@ -35,7 +35,15 @@ export function dbTarget(ctx, name = DEFAULT_DB_URL_ENV) {
35
35
  const parsed = parseConnection(value);
36
36
  if (!parsed)
37
37
  throw refusal('DB_URL_INVALID', `${name} is not a postgres connection URL`);
38
- return { kind: 'url', how: `connection string from ${name}`, urlEnv: name, url: parsed.url, env: parsed.password ? { PGPASSWORD: parsed.password } : {}, secrets: [value, parsed.password].filter(Boolean) };
38
+ const target = { kind: 'url', how: `connection string from ${name}`, urlEnv: name, url: parsed.url, env: parsed.password ? { PGPASSWORD: parsed.password } : {}, secrets: [value, parsed.password].filter(Boolean) };
39
+ // A dev environment's database runs in its own container (starci app stack up): STARCI_DB_CONTAINER runs the client tools inside it, so the host needs no PostgreSQL client.
40
+ const container = ctx.env.STARCI_DB_CONTAINER;
41
+ if (!container)
42
+ return target;
43
+ const inside = inContainer(target, container);
44
+ const url = new URL(inside.url);
45
+ url.port = '5432';
46
+ return { ...target, ...inside, kind: 'url', urlEnv: name, url: url.toString() };
39
47
  }
40
48
  /** Where a `-f` file of an in-container target is copied before the client reads it. */
41
49
  const CONTAINER_DIR = '/tmp/starci-db';
@@ -1,4 +1,12 @@
1
1
  "use strict";
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
2
10
  Object.defineProperty(exports, "__esModule", { value: true });
3
11
  // eslint-list.cts - ESLint over a list of files in ONE process, through the app's own install (the Node API of the ESLint the side's folder resolves).
4
12
  //
@@ -7,9 +15,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
15
  // argument ({ files, fix }), lints it once from its working directory (the side folder) and prints the results as the `--format json` of the CLI
8
16
  // does (the list of result objects), so the launcher validates them the same way. Its exit code is the CLI's: 0 clean of errors, 1 at least one
9
17
  // error, 2 the run could not complete (the message is on stderr). It stays CommonJS like bound-sys.cts, which may be preloaded in front of it.
18
+ //
19
+ // The project graph the canon's graph rules read is built in this process, once per side. When the job names the canons (`graph.canons`, the entry file
20
+ // of each installed canon) and a file (`graph.out`), the runner asks each canon for the snapshot of the graph it built (`projectGraphSnapshot`) after
21
+ // the lint and writes `{ snapshots }` there, so the cache keeps the graph findings without building the graph a second time in the CLI. A canon that
22
+ // cannot answer leaves its snapshot out; the report and the exit code are never affected.
10
23
  const fs = require('node:fs');
11
24
  const path = require('node:path');
12
25
  const { createRequire } = require('node:module');
26
+ const { pathToFileURL } = require('node:url');
27
+ /** Writes the snapshot of the graph each named canon built in this process; a canon that cannot answer is left out. */
28
+ async function handOutGraph(handoff, repoRoot) {
29
+ const snapshots = {};
30
+ for (const { name, entry } of handoff.canons) {
31
+ try {
32
+ const canon = await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href));
33
+ if (typeof canon.projectGraphSnapshot === 'function')
34
+ snapshots[name] = canon.projectGraphSnapshot({ repoRoot });
35
+ }
36
+ catch { /* the cache builds this canon's graph itself */ }
37
+ }
38
+ fs.writeFileSync(handoff.out, JSON.stringify({ snapshots }));
39
+ }
13
40
  async function main() {
14
41
  const job = JSON.parse(fs.readFileSync(String(process.argv[2]), 'utf8'));
15
42
  const cwd = process.cwd();
@@ -17,6 +44,8 @@ async function main() {
17
44
  const results = await new ESLint({ cwd, fix: job.fix }).lintFiles(job.files);
18
45
  if (job.fix)
19
46
  await ESLint.outputFixes(results);
47
+ if (job.graph !== undefined)
48
+ await handOutGraph(job.graph, cwd);
20
49
  process.stdout.write(JSON.stringify(results));
21
50
  return results.some((result) => result.errorCount > 0 || result.fatalErrorCount > 0) ? 1 : 0;
22
51
  }