@starci/hfs 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +116 -12
  3. package/bin/hfs.mjs +119 -15
  4. package/emit/compiler.mjs +35 -0
  5. package/emit/contracts.mjs +97 -0
  6. package/emit/operations-worker.mjs +24 -0
  7. package/emit/operations.mjs +126 -0
  8. package/emit/schema-worker.mjs +117 -0
  9. package/emit/static-graph.mjs +670 -0
  10. package/emit/type-schema.mjs +145 -0
  11. package/package.json +10 -2
  12. package/report/sonar.mjs +180 -0
  13. package/runtime/engine/admission.mjs +284 -0
  14. package/runtime/engine/digest.mjs +10 -0
  15. package/runtime/engine/ledger-db.mjs +1245 -0
  16. package/runtime/engine/machine-db.mjs +1484 -0
  17. package/runtime/engine/migrations/machine/0001-init.sql +887 -0
  18. package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
  19. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
  20. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
  21. package/runtime/engine/plain-object.mjs +5 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
  23. package/runtime/knowledge/hfs/slots.yaml +409 -140
  24. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  25. package/runtime/knowledge/sonar-gate.yaml +85 -0
  26. package/runtime/modules/kernel/failure-codes.yaml +1480 -16
  27. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  28. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  29. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  30. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  31. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  32. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  33. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  34. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  35. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  36. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  37. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  38. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  39. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  40. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  41. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  42. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  43. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  44. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  45. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  46. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  47. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  48. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  49. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  50. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  51. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  52. package/runtime/scripts/checks/architecture/index.mjs +316 -0
  53. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  54. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  55. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  56. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  57. package/runtime/scripts/checks/architecture/owners.mjs +89 -0
  58. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  59. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  60. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  61. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  62. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  63. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  64. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  65. package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
  66. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  67. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  68. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  69. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  70. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  71. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  72. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  73. package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
  74. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  75. package/runtime/scripts/checks/architecture.mjs +41 -0
  76. package/runtime/scripts/checks/common.mjs +37 -0
  77. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  78. package/runtime/scripts/lib/artifact-hold.mjs +89 -0
  79. package/runtime/scripts/lib/artifact-store.mjs +103 -0
  80. package/runtime/scripts/lib/fs-kind.mjs +10 -0
  81. package/runtime/scripts/lib/git.mjs +53 -0
  82. package/runtime/scripts/lib/hfs-allows.mjs +57 -0
  83. package/runtime/scripts/lib/hfs-check.mjs +254 -28
  84. package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
  85. package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
  86. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  87. package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
  88. package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
  89. package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
  90. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
  91. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  92. package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
  93. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  94. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
  95. package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +95 -41
  98. package/runtime/scripts/lib/hfs-tree.mjs +80 -0
  99. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  100. package/runtime/scripts/lib/json.mjs +22 -0
  101. package/runtime/scripts/lib/language.mjs +107 -0
  102. package/runtime/scripts/lib/path-key.mjs +2 -0
  103. package/runtime/scripts/lib/redact.mjs +148 -0
  104. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  105. package/runtime/scripts/lib/safe-remove.mjs +179 -0
  106. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  107. package/runtime/scripts/lib/sleep-sync.mjs +17 -0
  108. package/runtime/scripts/lib/stack-declaration.mjs +52 -0
  109. package/runtime/scripts/lib/stack-services.mjs +217 -0
  110. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  111. package/scaffold/service.mjs +333 -0
  112. package/sync/format.mjs +46 -0
  113. package/sync/hygiene.mjs +56 -24
  114. package/sync/index.mjs +126 -41
  115. package/sync/managed.mjs +170 -0
  116. package/sync/skeleton.mjs +32 -10
  117. package/sync/sonar-key.mjs +13 -0
  118. package/sync/ts-strict.mjs +48 -0
  119. package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
  120. package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
  121. package/templates/be/hooks/husky/pre-commit +13 -0
  122. package/templates/be/hooks/husky/pre-push +7 -0
  123. package/templates/be/package-scripts/package.json +21 -0
  124. package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
  125. package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
  126. package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
  127. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  128. package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
  129. package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
  130. package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
  131. package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
  132. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
  133. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
  134. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
  135. package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
  136. package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
  137. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
  138. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
  139. package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
  140. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
  141. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
  142. package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
  143. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
  144. package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
  145. package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
  146. package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
  147. package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
  148. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
  149. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
  150. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
  151. package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
  152. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
  153. package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
  154. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
  155. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
  156. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
  157. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
  158. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
  159. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
  162. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
  163. package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
  164. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
  165. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
  166. package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
  167. package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
  168. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
  169. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
  170. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
  171. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
  172. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
  173. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
  174. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
  175. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
  176. package/templates/be/tool-config/eslint.config.mjs +3 -0
  177. package/templates/be/tool-config/jest.config.js +1 -0
  178. package/templates/be/tool-config/prettierignore +8 -0
  179. package/templates/be/tool-config/prettierrc +1 -0
  180. package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
  181. package/templates/be/tool-config/tsconfig.build.json +5 -0
  182. package/templates/be/tool-config/tsconfig.json +11 -0
  183. package/templates/common/gitignore.base +1 -1
  184. package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
  185. package/templates/fe/hooks/husky/pre-commit +16 -0
  186. package/templates/fe/hooks/husky/pre-push +6 -0
  187. package/templates/fe/package-scripts/package.json +17 -0
  188. package/templates/fe/parts/api-client.ts +44 -0
  189. package/templates/fe/parts/api-outcome.ts +7 -0
  190. package/templates/fe/quality-config/sonar-project.properties +8 -0
  191. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
  192. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
  193. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
  194. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  195. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
  196. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
  197. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
  198. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
  199. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
  200. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
  201. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
  202. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
  203. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
  204. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
  205. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
  206. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
  207. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
  208. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
  209. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
  210. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
  211. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
  212. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
  213. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
  214. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
  215. package/templates/fe/tool-config/eslint.config.mjs +3 -0
  216. package/templates/fe/tool-config/prettierignore +10 -0
  217. package/templates/fe/tool-config/prettierrc +1 -0
  218. package/templates/fe/tool-config/stylelint.config.mjs +3 -0
  219. package/templates/fe/tool-config/tsconfig.json +4 -0
  220. package/templates/be/pre-commit +0 -8
  221. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
  222. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
  223. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
  224. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
  225. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
  226. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
  227. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
  228. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
  229. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
  230. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
  231. package/templates/common/codecov.yml +0 -13
  232. package/templates/common/pre-push +0 -5
  233. package/templates/fe/e2e.yml +0 -22
  234. package/templates/fe/pre-commit +0 -7
  235. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
  236. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
  237. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
  238. package/templates/fe/sonar-project.properties +0 -11
  239. /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
  240. /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
  241. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
  242. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
  243. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
  244. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
  245. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
  246. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
@@ -0,0 +1,24 @@
1
+ // The child process of `hfs emit-contracts` for the OPERATIONS of ONE app: `node operations-worker.mjs <repoRoot> <app>`.
2
+ // Prints the OpenAPI 3.1 document of the app's typed operation table on stdout; exit 3 when the app has no `apps/<app>/src/operations.ts`.
3
+ // Loads the repository's own typescript (resolved from the repository, never from hfs). Nothing of the repository is executed.
4
+ import { createRequire } from 'node:module';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { compilerOptionsOf } from './compiler.mjs';
8
+ import { openApiText, operationsPath, readOperations } from './operations.mjs';
9
+
10
+ const [rootArgument, app] = process.argv.slice(2);
11
+ const repoRoot = path.resolve(rootArgument);
12
+ const file = path.join(repoRoot, operationsPath(app));
13
+ if (!fs.existsSync(file)) process.exit(3);
14
+ const ts = createRequire(path.join(repoRoot, 'package.json'))('typescript');
15
+ const options = compilerOptionsOf(ts, repoRoot, (message) => process.stderr.write(`stand-in ${message}\n`));
16
+ const program = ts.createProgram({ rootNames: [file], options });
17
+ try {
18
+ const { operations, components } = readOperations({ ts, program, file });
19
+ process.stdout.write(openApiText({ app, operations, components }));
20
+ } catch (error) {
21
+ process.stderr.write(`${error.message}
22
+ `);
23
+ process.exit(1);
24
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The versioned operations of an api app, read from its typed operation table and printed as OpenAPI 3.1.
3
+ *
4
+ * The table is the canon's one declaration of an app's operation surface (knowledge/patterns/be/operations.yaml, BE-OPERATIONS-1):
5
+ * `apps/<app>/src/operations.ts` exports `OPERATIONS`, made by `defineOperations({ "<id>@<version>": operation<Input, Output,
6
+ * RefusalCode>("query" | "mutation") })`. The types come from the TypeScript checker; nothing is executed. An input, an output or a
7
+ * refusal set the checker cannot express (`any`, `unknown`, `Record<string, unknown>`, an unbound generic, `string` as a refusal code)
8
+ * is an error naming the operation.
9
+ *
10
+ * The wire is one route, `POST /operations`: the body is `{ operation, requestId, input }` and the answer is `{ operation, requestId,
11
+ * outcome }`, `outcome` being `{ kind: "ok", value }` or `{ kind: "refused", code, params? }`. Each operation contributes one request
12
+ * and one reply component carrying `x-operation: "<id>@<version>"`; the route's body and answer are the oneOf over them.
13
+ */
14
+ import { createSchemaBuilder, stable } from './type-schema.mjs';
15
+
16
+ /** The export of `apps/<app>/src/operations.ts` that holds the table. */
17
+ export const OPERATIONS_EXPORT = 'OPERATIONS';
18
+
19
+ /** The repository-relative path of an app's operation table. */
20
+ export const operationsPath = (app) => `apps/${app}/src/operations.ts`;
21
+
22
+ /** The repository-relative path of an app's OpenAPI contract. */
23
+ export const openapiPath = (app) => `contracts/${app}/openapi.json`;
24
+
25
+ const ID = /^[a-z][A-Za-z0-9.]*@[1-9][0-9]*$/;
26
+
27
+ /** The component name stem of an operation id: `sales.policy@1` gives `sales.policy.v1`. */
28
+ const stemOf = (id) => id.replace('@', '.v');
29
+
30
+ /**
31
+ * Reads the operation table of a source file of a program. Answers null when the file is not in the program; throws when it
32
+ * exports no `OPERATIONS` or an operation is not decidable.
33
+ */
34
+ export function readOperations({ ts, program, file }) {
35
+ const checker = program.getTypeChecker();
36
+ const sourceFile = program.getSourceFile(file);
37
+ if (!sourceFile) return null;
38
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
39
+ const exported = moduleSymbol ? checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.getName() === OPERATIONS_EXPORT) : null;
40
+ if (!exported) throw new Error(`${file} does not export ${OPERATIONS_EXPORT}, the app's operation table`);
41
+ const builder = createSchemaBuilder({ ts, checker });
42
+ const table = checker.getTypeOfSymbol(exported);
43
+ const operations = [];
44
+ for (const prop of [...checker.getPropertiesOfType(table)].sort((a, b) => (a.getName() < b.getName() ? -1 : 1))) {
45
+ const id = prop.getName();
46
+ const fail = (why) => {
47
+ throw new Error(`operation ${id}: ${why}`);
48
+ };
49
+ if (!ID.test(id)) fail('the key must be "<name>@<positive version>", for example "sales.policy@1"');
50
+ const contract = checker.getTypeOfSymbol(prop);
51
+ const kindType = contract.getProperty('kind') ? checker.getTypeOfSymbol(contract.getProperty('kind')) : null;
52
+ const kind = kindType?.isStringLiteral() ? kindType.value : null;
53
+ if (kind !== 'query' && kind !== 'mutation') fail('kind must be the literal "query" or "mutation"');
54
+ const typesProp = contract.getProperty('types');
55
+ if (!typesProp) fail('the value is not an OperationContract (it has no types member)');
56
+ const typesType = checker.getNonNullableType(checker.getTypeOfSymbol(typesProp));
57
+ const member = (name) => {
58
+ const symbol = typesType.getProperty(name);
59
+ if (!symbol) fail(`the contract has no ${name} type`);
60
+ return checker.getTypeOfSymbol(symbol);
61
+ };
62
+ const guard = (label, type) => {
63
+ try {
64
+ return builder.schemaOf(type, label);
65
+ } catch (error) {
66
+ return fail(error.message);
67
+ }
68
+ };
69
+ const input = guard('input', member('input'));
70
+ const output = guard('output', member('output'));
71
+ const refusalType = member('refusal');
72
+ let refusal = [];
73
+ if (!(refusalType.flags & ts.TypeFlags.Never)) {
74
+ const members = refusalType.isUnion() ? refusalType.types : [refusalType];
75
+ if (!members.every((each) => each.isStringLiteral())) fail(`refusal must be a closed union of string literals, found ${checker.typeToString(refusalType)}`);
76
+ refusal = members.map((each) => each.value).sort();
77
+ }
78
+ operations.push({ id, kind, input, output, refusal });
79
+ }
80
+ if (operations.length === 0) throw new Error(`${file}: ${OPERATIONS_EXPORT} declares no operation`);
81
+ return { operations, components: builder.components() };
82
+ }
83
+
84
+ /** The OpenAPI 3.1 document of an app's operations, as sorted-key JSON text with one final newline. */
85
+ export function openApiText({ app, operations, components }) {
86
+ const schemas = { ...components };
87
+ const requests = [];
88
+ const replies = [];
89
+ for (const operation of operations) {
90
+ const stem = stemOf(operation.id);
91
+ const okOutcome = { type: 'object', required: ['kind', 'value'], properties: { kind: { const: 'ok' }, value: operation.output } };
92
+ const refusedOutcome = { type: 'object', required: ['code', 'kind'], properties: { code: { enum: operation.refusal, type: 'string' }, kind: { const: 'refused' }, params: { type: 'object', additionalProperties: { type: ['string', 'number', 'boolean'] } } } };
93
+ schemas[`Operation.${stem}.Request`] = {
94
+ type: 'object',
95
+ required: ['input', 'operation', 'requestId'],
96
+ properties: { input: operation.input, operation: { const: operation.id }, requestId: { type: 'string', minLength: 1 } },
97
+ 'x-kind': operation.kind,
98
+ 'x-operation': operation.id,
99
+ };
100
+ schemas[`Operation.${stem}.Reply`] = {
101
+ type: 'object',
102
+ required: ['operation', 'outcome', 'requestId'],
103
+ properties: { operation: { const: operation.id }, outcome: operation.refusal.length ? { oneOf: [okOutcome, refusedOutcome] } : okOutcome, requestId: { type: 'string' } },
104
+ 'x-kind': operation.kind,
105
+ 'x-operation': operation.id,
106
+ };
107
+ requests.push({ $ref: `#/components/schemas/Operation.${stem}.Request` });
108
+ replies.push({ $ref: `#/components/schemas/Operation.${stem}.Reply` });
109
+ }
110
+ const document = {
111
+ openapi: '3.1.0',
112
+ info: { title: `${app} operations`, version: '1' },
113
+ 'x-operations': operations.map((operation) => ({ id: operation.id, kind: operation.kind })),
114
+ paths: {
115
+ '/operations': {
116
+ post: {
117
+ operationId: 'invokeOperation',
118
+ requestBody: { required: true, content: { 'application/json': { schema: { oneOf: requests } } } },
119
+ responses: { 200: { description: 'The reply of the invoked operation: its outcome is ok or a declared refusal.', content: { 'application/json': { schema: { oneOf: replies } } } } },
120
+ },
121
+ },
122
+ },
123
+ components: { schemas },
124
+ };
125
+ return `${JSON.stringify(stable(document), null, 2)}\n`;
126
+ }
@@ -0,0 +1,117 @@
1
+ // The child process of `hfs emit-contracts` for ONE app: `node schema-worker.mjs <repoRoot> <app>`.
2
+ // Prints the schema the app serves on stdout; exit 3 when the app's module graph has no GraphQL server.
3
+ // Loads the repository's own typescript, @nestjs/* and graphql (resolved from the repository, never from hfs).
4
+ //
5
+ // Two steps: (1) the module graph of the app root is read from source (static-graph.mjs) and yields the resolver classes the
6
+ // GraphQL server would collect; (2) only those classes are loaded, and Nest's own GraphQLSchemaFactory builds the schema.
7
+ // Step 2 loads code through the repository's own TypeScript; a dependency of a resolver that cannot load here (configuration
8
+ // read at import time, a native addon, an ES-only package; only requires written in the repository's own source) is replaced by a stand-in and reported on stderr, because the
9
+ // schema depends on the resolver and type classes, never on what they call. A stand-in that is a GraphQL type fails the build.
10
+ import { createRequire } from 'node:module';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { aliasTarget, aliasesOf, appModulePath } from './contracts.mjs';
14
+ import { compilerOptionsOf } from './compiler.mjs';
15
+ import { createGraphReader } from './static-graph.mjs';
16
+
17
+ const [rootArgument, app] = process.argv.slice(2);
18
+ const repoRoot = path.resolve(rootArgument);
19
+ const require = createRequire(path.join(repoRoot, 'package.json'));
20
+ const ts = require('typescript');
21
+
22
+ const config = ts.readConfigFile(path.join(repoRoot, 'tsconfig.json'), ts.sys.readFile).config ?? {};
23
+ const aliases = aliasesOf(config.compilerOptions?.paths, repoRoot);
24
+
25
+ // ---- step 1: the graph, from source ----
26
+ const isFile = (candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile();
27
+ const host = {
28
+ read: (file) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null),
29
+ resolve(from, specifier) {
30
+ const aliased = aliasTarget(aliases, specifier);
31
+ if (aliased === null && !specifier.startsWith('.')) return null;
32
+ const base = (aliased ?? path.resolve(path.dirname(from), specifier)).replace(/\.js$/, '');
33
+ const found = [base, `${base}.ts`, path.join(base, 'index.ts')].find(isFile);
34
+ if (!found) throw new Error(`${from}: cannot resolve ${specifier}`);
35
+ return found;
36
+ },
37
+ };
38
+ let composition;
39
+ try {
40
+ composition = createGraphReader({ ts, host }).compose(path.join(repoRoot, appModulePath(app)));
41
+ } catch (error) {
42
+ process.stderr.write(`${error.message}
43
+ `);
44
+ process.exit(1);
45
+ }
46
+ if (composition === null) process.exit(3);
47
+ require('reflect-metadata');
48
+
49
+ // ---- step 2: the classes, loaded ----
50
+ const Module = require('node:module');
51
+ const stand = new Proxy(function standIn() {}, {
52
+ get: (_, key) => (key === '__esModule' ? true : key === 'then' ? undefined : key === Symbol.toPrimitive ? () => '' : stand),
53
+ apply: () => stand,
54
+ construct: () => stand,
55
+ });
56
+ const resolveFilename = Module._resolveFilename;
57
+ Module._resolveFilename = function resolve(request, ...rest) {
58
+ return resolveFilename.call(this, aliasTarget(aliases, request) ?? request, ...rest);
59
+ };
60
+ // The code is compiled the way `tsc` compiles it (a Program over the resolver files and what they import), not file by file:
61
+ // `emitDecoratorMetadata` writes the type of a decorated property from the type checker (`facet: Facet` with a string-literal
62
+ // union is `String`), which a per-file transpile cannot know and which decides GraphQL field types.
63
+ const options = compilerOptionsOf(ts, repoRoot, (message) => process.stderr.write(`stand-in ${message}
64
+ `));
65
+ const roots = [...composition.resolvers, ...composition.scalars, ...composition.include].map(({ file }) => file);
66
+ const program = ts.createProgram({ rootNames: [...new Set(roots)], options });
67
+ const compile = (filename) => {
68
+ const sourceFile = program.getSourceFile(filename);
69
+ if (!sourceFile) return ts.transpileModule(fs.readFileSync(filename, 'utf8'), { fileName: filename, compilerOptions: options }).outputText;
70
+ let output = '';
71
+ program.emit(sourceFile, (name, text) => { if (name.endsWith('.js')) output = text; });
72
+ return output;
73
+ };
74
+ require.extensions['.ts'] = (module, filename) => module._compile(compile(filename), filename);
75
+ let depth = 0;
76
+ const load = Module._load;
77
+ Module._load = function tolerantLoad(request, parent, isMain) {
78
+ depth += 1;
79
+ try {
80
+ return load.call(this, request, parent, isMain);
81
+ } catch (error) {
82
+ const fromSource = typeof parent?.filename === 'string' && parent.filename.startsWith(repoRoot) && !parent.filename.includes(`${path.sep}node_modules${path.sep}`);
83
+ if (depth === 1 || !fromSource) throw error;
84
+ process.stderr.write(`stand-in ${request} (from ${parent?.filename ?? '?'}): ${String(error?.message ?? error).split('\n')[0]}\n`);
85
+ return stand;
86
+ } finally {
87
+ depth -= 1;
88
+ }
89
+ };
90
+ const classOf = ({ file, name }) => {
91
+ const exported = require(file)[name];
92
+ if (typeof exported !== 'function') throw new Error(`${file} does not export the class ${name}`);
93
+ return exported;
94
+ };
95
+
96
+ // The packages of the repository under emission, resolved from it (never from hfs, which declares none of them).
97
+ const FROM_REPOSITORY = { nestGraphql: '@nestjs/graphql', nestCore: '@nestjs/core', graphql: 'graphql' };
98
+ const graphql = require(FROM_REPOSITORY.nestGraphql);
99
+ // the constants and the scalar factory are internals of the package: reached by file, its `exports` map hides them
100
+ const graphqlDist = path.dirname(require.resolve(FROM_REPOSITORY.nestGraphql));
101
+ const { SCALAR_NAME_METADATA, SCALAR_TYPE_METADATA } = require(path.join(graphqlDist, 'graphql.constants.js'));
102
+ const { createScalarType } = require(path.join(graphqlDist, 'utils', 'scalar-types.utils.js'));
103
+
104
+ const resolvers = composition.resolvers.map(classOf);
105
+ const scalarsMap = composition.scalars.map(classOf).map((cls) => {
106
+ const typeRef = Reflect.getMetadata(SCALAR_TYPE_METADATA, cls);
107
+ return { type: (typeof typeRef === 'function' && typeRef()) || cls, scalar: createScalarType(Reflect.getMetadata(SCALAR_NAME_METADATA, cls), new cls(...Array.from({ length: cls.length }, () => stand))) };
108
+ });
109
+ const includeModules = composition.include.map(classOf);
110
+
111
+ const { NestFactory } = require(FROM_REPOSITORY.nestCore);
112
+ const { GraphQLSchemaBuilderModule, GraphQLSchemaFactory } = graphql;
113
+ const { lexicographicSortSchema, printSchema } = require(FROM_REPOSITORY.graphql);
114
+ const context = await NestFactory.createApplicationContext(GraphQLSchemaBuilderModule, { logger: false });
115
+ const schema = await context.get(GraphQLSchemaFactory).create(resolvers, { scalarsMap, includeModules });
116
+ process.stdout.write(printSchema(lexicographicSortSchema(schema)));
117
+ await context.close();