@starci/hfs 5.0.7 → 5.1.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 (204) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +7 -8
  3. package/cache/fake-session.mjs +1 -1
  4. package/cache/hfs-family.mjs +22 -9
  5. package/cache/import-graph.d.mts +32 -4
  6. package/cache/import-graph.mjs +88 -11
  7. package/cache/lint-family.d.mts +10 -3
  8. package/cache/lint-family.mjs +65 -37
  9. package/cache/records.d.mts +7 -0
  10. package/cache/records.mjs +17 -1
  11. package/cache/run.d.mts +1 -1
  12. package/cache/session.mjs +6 -2
  13. package/cache/side-key.d.mts +2 -0
  14. package/cache/side-key.mjs +5 -1
  15. package/cache/structure-memo.d.mts +34 -0
  16. package/cache/structure-memo.mjs +150 -0
  17. package/cache/types.d.mts +6 -2
  18. package/cache/wiring-family.mjs +1 -1
  19. package/codemod/directive-first.d.mts +48 -0
  20. package/codemod/directive-first.mjs +136 -0
  21. package/codemod/explicit-surface-edit.d.mts +14 -0
  22. package/codemod/explicit-surface-edit.mjs +57 -0
  23. package/codemod/explicit-surface-names.d.mts +30 -0
  24. package/codemod/explicit-surface-names.mjs +135 -0
  25. package/codemod/explicit-surface-render.d.mts +29 -0
  26. package/codemod/explicit-surface-render.mjs +197 -0
  27. package/codemod/explicit-surface.d.mts +50 -0
  28. package/codemod/explicit-surface.mjs +374 -0
  29. package/codemod/main.d.mts +7 -5
  30. package/codemod/main.mjs +37 -16
  31. package/codemod/small-renames.mjs +22 -2
  32. package/codemod/source-tree.d.mts +8 -0
  33. package/codemod/source-tree.mjs +23 -0
  34. package/lint/eslint-list.cjs +55 -0
  35. package/lint/run.d.mts +35 -61
  36. package/lint/run.mjs +91 -288
  37. package/package.json +1 -1
  38. package/release/lint-report.d.mts +1 -1
  39. package/release/lint-report.mjs +3 -3
  40. package/report/sonar.d.mts +1 -1
  41. package/report/sonar.mjs +3 -3
  42. package/report/stylelint-findings.d.mts +23 -0
  43. package/report/stylelint-findings.mjs +9 -0
  44. package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
  45. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  46. package/runtime/knowledge/hfs/rules.yaml +56 -5
  47. package/runtime/knowledge/hfs/slots.yaml +1 -1
  48. package/runtime/knowledge/sonar-gate.yaml +2 -2
  49. package/runtime/modules/kernel/failure-codes.yaml +4 -4
  50. package/runtime/scripts/api/node/run-node.d.mts +5 -0
  51. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
  52. package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
  53. package/runtime/scripts/hfs/check.d.mts +16 -1
  54. package/runtime/scripts/hfs/check.mjs +18 -7
  55. package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
  56. package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
  57. package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
  58. package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
  59. package/runtime/scripts/hfs/rule-memo.mjs +24 -0
  60. package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
  61. package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
  62. package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
  63. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
  64. package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
  65. package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
  66. package/runtime/scripts/hfs/rules/read.d.mts +5 -0
  67. package/runtime/scripts/hfs/rules/read.mjs +13 -0
  68. package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
  69. package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
  70. package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
  71. package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
  72. package/runtime/scripts/lib/declaration-surface.mjs +112 -0
  73. package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
  74. package/scaffold/app.mjs +3 -3
  75. package/src/check-cache-structure-support.d.mts +52 -0
  76. package/src/main.d.mts +3 -2
  77. package/src/main.mjs +123 -109
  78. package/sync/ci-commands.d.mts +2 -1
  79. package/sync/ci-commands.mjs +19 -20
  80. package/sync/index.mjs +2 -2
  81. package/templates/app/package-scripts/core.json +2 -2
  82. package/templates/app/task-graph/turbo.json +0 -3
  83. package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
  84. package/templates/be/inbox/entities.index.ts.tpl +1 -1
  85. package/templates/be/inbox/module-definition.ts.tpl +8 -2
  86. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
  87. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
  88. package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
  89. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
  90. package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
  91. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
  92. package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
  93. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
  94. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
  95. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
  96. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
  97. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
  98. package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
  99. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
  100. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
  101. package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
  102. package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
  103. package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
  104. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
  105. package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
  106. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
  107. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
  108. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
  109. package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
  110. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
  111. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
  112. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
  113. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
  114. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
  115. package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
  116. package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
  117. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
  118. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
  119. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
  120. package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
  121. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
  122. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
  123. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
  124. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
  125. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
  126. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
  127. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
  128. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
  129. package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
  130. package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
  131. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
  132. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
  133. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
  134. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
  135. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
  136. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
  137. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
  138. package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
  139. package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
  140. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
  141. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
  142. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
  143. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
  145. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
  146. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
  147. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
  148. package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
  149. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
  150. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
  151. package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
  152. package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
  153. package/templates/be/table/entities.index.ts.tpl +1 -1
  154. package/templates/be/table/module-definition.ts.tpl +8 -3
  155. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
  156. package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
  157. package/templates/fe/common/app-layout.tsx.partial +1 -1
  158. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
  159. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
  160. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
  161. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
  162. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
  163. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
  164. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
  165. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
  166. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
  167. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
  168. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
  169. package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
  170. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
  171. package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
  172. package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  173. package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
  174. package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
  175. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
  176. package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
  177. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
  178. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
  179. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
  180. package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
  181. package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
  182. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
  183. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
  184. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
  185. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
  186. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
  187. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
  188. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
  189. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
  190. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
  191. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
  192. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
  193. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
  194. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  195. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
  196. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
  197. package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
  198. package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
  199. package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
  200. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
  201. package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
  202. package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
  203. package/templates/fe/units/page/index.tsx.tpl +3 -2
  204. package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
5
+ import { sourceLocation } from '../runtime/scripts/lib/ts-ast.mjs';
6
+ import { filesUnder, relativePosix, sideFolders } from './source-tree.mjs';
7
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ /** The directives Next.js reads from the prologue of a module (R369, FE_DIRECTIVE_FIRST). */
9
+ const DIRECTIVES = new Set(['use client', 'use server']);
10
+ const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
11
+ const DECLARATION = /\.d\.[cm]?ts$/i;
12
+ /** The string literal a statement is made of, through the parentheses Prettier wraps it in, or null. */
13
+ function stringStatement(ts, statement) {
14
+ if (!ts.isExpressionStatement(statement))
15
+ return null;
16
+ let expression = statement.expression;
17
+ while (ts.isParenthesizedExpression(expression))
18
+ expression = expression.expression;
19
+ return ts.isStringLiteral(expression) ? expression : null;
20
+ }
21
+ /** The span to delete for `statement`: its own line when nothing else is on it (with a `;` Prettier put before a parenthesized statement), else the statement alone. */
22
+ function removalSpan(text, statement, source) {
23
+ let start = statement.getStart(source);
24
+ let end = statement.end;
25
+ let from = start;
26
+ while (from > 0 && (text[from - 1] === ' ' || text[from - 1] === '\t'))
27
+ from -= 1;
28
+ if (text[from - 1] === ';' && (from - 1 === 0 || text[from - 2] === '\n'))
29
+ from -= 1;
30
+ const startsLine = from === 0 || text[from - 1] === '\n';
31
+ let to = end;
32
+ while (to < text.length && (text[to] === ' ' || text[to] === '\t'))
33
+ to += 1;
34
+ const endsLine = to >= text.length || text[to] === '\n' || text[to] === '\r';
35
+ if (startsLine && endsLine) {
36
+ start = from;
37
+ end = text[to] === '\r' ? to + 2 : to + 1;
38
+ }
39
+ return { start, end: Math.min(end, text.length) };
40
+ }
41
+ /** Move every `"use client"` / `"use server"` that is not in the prologue of the module to the top of it, as its first statement, with one blank line after. */
42
+ export function transformDirectiveFirst({ ts, text, file = 'module.tsx' }) {
43
+ const none = { text, changes: [], findings: [] };
44
+ if (!text.includes('use client') && !text.includes('use server'))
45
+ return none;
46
+ const kind = /\.tsx$/i.test(file) ? ts.ScriptKind.TSX : /\.jsx$/i.test(file) ? ts.ScriptKind.JSX : /\.[cm]?js$/i.test(file) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
47
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
48
+ const first = source.statements[0];
49
+ if (first === undefined)
50
+ return none;
51
+ // The prologue is the run of plain string statements at the top; a parenthesized one is an expression, not a directive.
52
+ let prologueEnd = 0;
53
+ const kept = new Set();
54
+ while (prologueEnd < source.statements.length) {
55
+ const statement = source.statements[prologueEnd];
56
+ if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
57
+ break;
58
+ kept.add(statement.expression.text);
59
+ prologueEnd += 1;
60
+ }
61
+ const misplaced = source.statements.slice(prologueEnd).flatMap((statement, offset) => {
62
+ const literal = stringStatement(ts, statement);
63
+ return literal !== null && DIRECTIVES.has(literal.text) ? [{ statement, literal, before: prologueEnd + offset }] : [];
64
+ });
65
+ if (!misplaced.length)
66
+ return none;
67
+ const changes = [];
68
+ const findings = [];
69
+ const opposite = (directive) => (directive === 'use client' ? 'use server' : 'use client');
70
+ const wanted = new Set(misplaced.map(entry => entry.literal.text));
71
+ const edits = [];
72
+ const toTop = [];
73
+ const placed = new Set(kept);
74
+ for (const { statement, literal, before } of misplaced) {
75
+ const place = sourceLocation(source, statement);
76
+ const directive = literal.text;
77
+ if (kept.has(opposite(directive)) || wanted.has(opposite(directive))) {
78
+ findings.push({ file, ...place, code: 'CONFLICTING_DIRECTIVES', reason: `"use client" and "use server" cannot share a module: split it, then keep the one directive that stays at the top` });
79
+ continue;
80
+ }
81
+ edits.push({ ...removalSpan(text, statement, source), text: '' });
82
+ if (!placed.has(directive)) {
83
+ toTop.push(literal.getText(source) + (statement.getText(source).endsWith(';') ? ';' : ''));
84
+ placed.add(directive);
85
+ }
86
+ const mangled = ts.isExpressionStatement(statement) && ts.isParenthesizedExpression(statement.expression);
87
+ changes.push({ file, ...place, code: mangled ? 'DIRECTIVE_PARENTHESIZED' : 'DIRECTIVE_BELOW_STATEMENT', from: `${mangled ? 'parenthesized, ' : ''}after ${before} statement${before === 1 ? '' : 's'}`, to: 'first statement' });
88
+ }
89
+ if (toTop.length)
90
+ edits.push({ start: first.getStart(source), end: first.getStart(source), text: `${toTop.join('\n')}\n\n` });
91
+ let output = text;
92
+ for (const edit of edits.sort((a, b) => b.start - a.start || b.end - a.end))
93
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
94
+ return { text: output, changes, findings };
95
+ }
96
+ /** Preview or apply the moves in one app (`be/`, `fe/`, or `src/` of a single-side app); a conflicting module is kept in the returned report. */
97
+ export function migrateDirectiveFirst({ root, write = false, ts = loadTypescript(root, PACKAGE_ROOT) }) {
98
+ if (!ts)
99
+ throw new Error('directive-first needs TypeScript installed in the app or @starci/hfs development checkout');
100
+ const folders = sideFolders(root);
101
+ if (!folders.length)
102
+ throw new Error('directive-first runs at an app root with a fe/ directory');
103
+ const report = { schema: 'starci/directive-first-report@1', root, write, filesScanned: 0, filesChanged: [], changes: [], findings: [] };
104
+ for (const target of folders.flatMap(folder => filesUnder(folder, SOURCE)).filter(target => !DECLARATION.test(target))) {
105
+ const file = relativePosix(root, target);
106
+ const original = fs.readFileSync(target, 'utf8');
107
+ const result = transformDirectiveFirst({ ts, text: original, file });
108
+ report.filesScanned += 1;
109
+ report.changes.push(...result.changes);
110
+ report.findings.push(...result.findings);
111
+ if (result.text !== original) {
112
+ report.filesChanged.push(file);
113
+ if (write)
114
+ fs.writeFileSync(target, result.text);
115
+ }
116
+ }
117
+ return report;
118
+ }
119
+ const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
120
+ /** Render the moves and what is left for a person as Markdown; JSON consumers read the same report object. */
121
+ export function directiveFirstMarkdown(report) {
122
+ const out = ['# directive-first', '', `Scanned ${report.filesScanned} files, ${report.filesChanged.length} ${report.write ? 'changed' : 'would change'}, ${report.changes.length} directives moved, ${report.findings.length} left for a person.`, ''];
123
+ if (report.findings.length) {
124
+ out.push('## Left for a person', '', '| location | code | reason |', '|---|---|---|');
125
+ for (const finding of report.findings)
126
+ out.push(`| ${cell(`${finding.file}:${finding.line}:${finding.column}`)} | ${finding.code} | ${cell(finding.reason)} |`);
127
+ out.push('');
128
+ }
129
+ if (report.changes.length) {
130
+ out.push('## Moves', '', '| location | code | from | to |', '|---|---|---|---|');
131
+ for (const item of report.changes)
132
+ out.push(`| ${cell(`${item.file}:${item.line}:${item.column}`)} | ${item.code} | ${cell(item.from)} | ${cell(item.to)} |`);
133
+ out.push('');
134
+ }
135
+ return `${out.join('\n')}\n`;
136
+ }
@@ -0,0 +1,14 @@
1
+ import type { ImportNeed } from './explicit-surface-render.mjs';
2
+ import type * as TsTypes from 'typescript';
3
+ type Ts = typeof import('typescript');
4
+ /** One text edit of the source: the span `start`..`end` becomes `text`. */
5
+ export interface Edit {
6
+ start: number;
7
+ end: number;
8
+ text: string;
9
+ }
10
+ /** The edits that give a file the imports `needs` names: a specifier added to a named import of the same module, else one `import type` line per module. */
11
+ export declare function importEdits(ts: Ts, source: TsTypes.SourceFile, text: string, needs: readonly ImportNeed[]): Edit[];
12
+ /** `text` with `edits` applied, last position first so every offset stays true; edits at one position keep their order. */
13
+ export declare function applyEdits(text: string, edits: readonly Edit[]): string;
14
+ export {};
@@ -0,0 +1,57 @@
1
+ /** The edits that give a file the imports `needs` names: a specifier added to a named import of the same module, else one `import type` line per module. */
2
+ export function importEdits(ts, source, text, needs) {
3
+ if (!needs.length)
4
+ return [];
5
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
6
+ const imports = source.statements.filter(ts.isImportDeclaration);
7
+ const first = imports[0];
8
+ const quote = first !== undefined && ts.isStringLiteral(first.moduleSpecifier) ? first.moduleSpecifier.getText(source).slice(0, 1) : '"';
9
+ const semicolon = first !== undefined && first.getText(source).endsWith(';') ? ';' : '';
10
+ const edits = [];
11
+ const lines = [];
12
+ const bySpecifier = new Map();
13
+ for (const need of needs)
14
+ bySpecifier.set(need.specifier, [...(bySpecifier.get(need.specifier) ?? []), need.name]);
15
+ for (const [specifier, names] of bySpecifier) {
16
+ const sorted = [...new Set(names)].sort();
17
+ const home = imports.find(declaration => ts.isStringLiteral(declaration.moduleSpecifier) && declaration.moduleSpecifier.text === specifier
18
+ && declaration.importClause?.namedBindings !== undefined && ts.isNamedImports(declaration.importClause.namedBindings) && declaration.importClause.namedBindings.elements.length > 0);
19
+ const clause = home?.importClause;
20
+ const bindings = clause?.namedBindings;
21
+ if (clause !== undefined && bindings !== undefined && ts.isNamedImports(bindings)) {
22
+ const last = bindings.elements[bindings.elements.length - 1];
23
+ edits.push({ start: last.end, end: last.end, text: sorted.map(name => `, ${clause.isTypeOnly ? '' : 'type '}${name}`).join('') });
24
+ }
25
+ else
26
+ lines.push(`import type { ${sorted.join(', ')} } from ${quote}${specifier}${quote}${semicolon}`);
27
+ }
28
+ if (lines.length)
29
+ edits.push(newImports(ts, source, text, lines, eol));
30
+ return edits;
31
+ }
32
+ /** The edit that adds whole import lines: after the last import, else after the directive prologue, else above the first statement. */
33
+ function newImports(ts, source, text, lines, eol) {
34
+ const block = lines.join(eol);
35
+ const last = source.statements.filter(ts.isImportDeclaration).at(-1);
36
+ if (last !== undefined)
37
+ return { start: last.end, end: last.end, text: `${eol}${block}` };
38
+ let prologue;
39
+ for (const statement of source.statements) {
40
+ if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
41
+ break;
42
+ prologue = statement;
43
+ }
44
+ if (prologue !== undefined)
45
+ return { start: prologue.end, end: prologue.end, text: `${eol}${eol}${block}` };
46
+ const first = source.statements[0];
47
+ const at = first === undefined ? text.length : first.getStart(source);
48
+ return { start: at, end: at, text: `${block}${eol}${eol}` };
49
+ }
50
+ /** `text` with `edits` applied, last position first so every offset stays true; edits at one position keep their order. */
51
+ export function applyEdits(text, edits) {
52
+ const ordered = edits.map((edit, at) => ({ edit, at })).sort((a, b) => b.edit.start - a.edit.start || b.edit.end - a.edit.end || b.at - a.at);
53
+ let output = text;
54
+ for (const { edit } of ordered)
55
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
56
+ return output;
57
+ }
@@ -0,0 +1,30 @@
1
+ import type * as TsTypes from 'typescript';
2
+ /** The TypeScript compiler API of the app's own install, typed by the version this package develops against. */
3
+ type Ts = typeof import('typescript');
4
+ /** What naming a type reads of the app: its compiler, program and one source file. */
5
+ export interface NamingContext {
6
+ ts: Ts;
7
+ program: TsTypes.Program;
8
+ checker: TsTypes.TypeChecker;
9
+ options: TsTypes.CompilerOptions;
10
+ source: TsTypes.SourceFile;
11
+ /** The absolute, forward-slash name of the file being annotated. */
12
+ file: string;
13
+ text: string;
14
+ }
15
+ /** The source file of the program a module path (absolute, without its extension) names, or undefined. */
16
+ export declare function moduleFile(c: NamingContext, modulePath: string): TsTypes.SourceFile | undefined;
17
+ /** The symbol a module exports under `name`, with aliases resolved, or undefined. */
18
+ export declare function exportedSymbol(c: NamingContext, file: TsTypes.SourceFile, name: string): TsTypes.Symbol | undefined;
19
+ /** The declaration `symbol` stands for: an import resolved to what it imports, a local export to its export symbol, so two names of one declaration compare equal. */
20
+ export declare function declarationOf(c: NamingContext, symbol: TsTypes.Symbol): TsTypes.Symbol;
21
+ /** The names that can be written at `enclosing` without an import, as symbols; what a function body declares is not visible from its signature. */
22
+ export declare function namesInScope(c: NamingContext, enclosing: TsTypes.Node): Map<string, TsTypes.Symbol>;
23
+ /**
24
+ * `node` (the type node the checker wrote for `type`) with every anonymous object type that an exported type of the app stands for replaced by a
25
+ * reference to that type. It follows type arguments (`Promise<{...}>`), arrays and unions; any other anonymous part stays for the caller to refuse.
26
+ */
27
+ export declare function nameAnonymousParts(c: NamingContext, node: TsTypes.TypeNode, type: TsTypes.Type, enclosing: TsTypes.Node): TsTypes.TypeNode;
28
+ /** The directory-relative form of `target` from the folder of `from`, without extension, the way a relative import is written. */
29
+ export declare function relativeSpecifier(from: string, target: string): string;
30
+ export {};
@@ -0,0 +1,135 @@
1
+ import path from 'node:path';
2
+ const EXTENSIONS = ['', '.ts', '.tsx', '.d.ts', '.mts', '.d.mts', '.cts', '.d.cts', '/index.ts', '/index.tsx', '/index.d.ts'];
3
+ const posix = (file) => file.replaceAll('\\', '/');
4
+ /** The source file of the program a module path (absolute, without its extension) names, or undefined. */
5
+ export function moduleFile(c, modulePath) {
6
+ for (const extension of EXTENSIONS) {
7
+ const found = c.program.getSourceFile(posix(modulePath) + extension);
8
+ if (found !== undefined)
9
+ return found;
10
+ }
11
+ return undefined;
12
+ }
13
+ /** The symbol a module exports under `name`, with aliases resolved, or undefined. */
14
+ export function exportedSymbol(c, file, name) {
15
+ const module = c.checker.getSymbolAtLocation(file);
16
+ if (module === undefined)
17
+ return undefined;
18
+ const found = c.checker.getExportsOfModule(module).find(symbol => symbol.name === name);
19
+ if (found === undefined)
20
+ return undefined;
21
+ return (found.flags & c.ts.SymbolFlags.Alias) === 0 ? found : c.checker.getAliasedSymbol(found);
22
+ }
23
+ /** The declaration `symbol` stands for: an import resolved to what it imports, a local export to its export symbol, so two names of one declaration compare equal. */
24
+ export function declarationOf(c, symbol) {
25
+ const target = (symbol.flags & c.ts.SymbolFlags.Alias) === 0 ? symbol : c.checker.getAliasedSymbol(symbol);
26
+ return c.checker.getExportSymbolOfSymbol(target);
27
+ }
28
+ /** The key that groups candidate types before the expensive assignability test: the sorted property names. */
29
+ const shapeKey = (names) => [...names].sort().join(',');
30
+ const indexes = new WeakMap();
31
+ /** The exported, non-generic object types of the app's own files (never installed packages or declaration files), by their property names. */
32
+ function namedTypes(c) {
33
+ const known = indexes.get(c.program);
34
+ if (known !== undefined)
35
+ return known;
36
+ const found = new Map();
37
+ for (const file of c.program.getSourceFiles()) {
38
+ if (file.isDeclarationFile || c.program.isSourceFileFromExternalLibrary(file) || file.fileName.includes('/node_modules/'))
39
+ continue;
40
+ const module = c.checker.getSymbolAtLocation(file);
41
+ if (module === undefined)
42
+ continue;
43
+ for (const symbol of c.checker.getExportsOfModule(module)) {
44
+ const target = (symbol.flags & c.ts.SymbolFlags.Alias) === 0 ? symbol : c.checker.getAliasedSymbol(symbol);
45
+ if ((target.flags & (c.ts.SymbolFlags.TypeAlias | c.ts.SymbolFlags.Interface)) === 0)
46
+ continue;
47
+ const type = c.checker.getDeclaredTypeOfSymbol(target);
48
+ const generic = type.typeParameters?.length ?? type.aliasTypeArguments?.length ?? 0;
49
+ if (generic > 0 || (type.flags & c.ts.TypeFlags.Object) === 0)
50
+ continue;
51
+ const key = shapeKey(c.checker.getPropertiesOfType(type).map(property => property.name));
52
+ if (key === '')
53
+ continue;
54
+ found.set(key, [...(found.get(key) ?? []), { name: symbol.name, type, file: file.fileName }]);
55
+ }
56
+ }
57
+ indexes.set(c.program, found);
58
+ return found;
59
+ }
60
+ /** The names that can be written at `enclosing` without an import, as symbols; what a function body declares is not visible from its signature. */
61
+ export function namesInScope(c, enclosing) {
62
+ const flags = c.ts.SymbolFlags.Type | c.ts.SymbolFlags.Namespace | c.ts.SymbolFlags.Alias | c.ts.SymbolFlags.Value;
63
+ const body = enclosing.body;
64
+ const inBody = (symbol) => body !== undefined && symbol.declarations?.length !== 0 && symbol.declarations?.every(declaration => declaration.pos >= body.pos && declaration.end <= body.end) === true;
65
+ return new Map(c.checker.getSymbolsInScope(enclosing, flags).filter(symbol => !inBody(symbol)).map(symbol => [symbol.name, symbol]));
66
+ }
67
+ /**
68
+ * The one exported type of the app that is interchangeable with the inferred object type `type` (each assignable to the other and the same property names),
69
+ * preferring a name the file already has in scope; undefined when none or more than one fits, so a guess never stands for the inference.
70
+ */
71
+ function matchingName(c, type, enclosing) {
72
+ if ((type.flags & c.ts.TypeFlags.Object) === 0)
73
+ return undefined;
74
+ const key = shapeKey(c.checker.getPropertiesOfType(type).map(property => property.name));
75
+ const fits = (namedTypes(c).get(key) ?? []).filter(named => c.checker.isTypeAssignableTo(type, named.type) && c.checker.isTypeAssignableTo(named.type, type));
76
+ if (fits.length <= 1)
77
+ return fits[0];
78
+ const scope = namesInScope(c, enclosing);
79
+ const local = fits.filter(named => scope.has(named.name));
80
+ return local.length === 1 ? local[0] : undefined;
81
+ }
82
+ /** The type argument nodes of `node` paired with the type arguments the checker holds for `type`. */
83
+ function argumentsOf(c, type) {
84
+ if ((type.flags & c.ts.TypeFlags.Object) !== 0 && (type.objectFlags & c.ts.ObjectFlags.Reference) !== 0)
85
+ return c.checker.getTypeArguments(type);
86
+ return type.aliasTypeArguments ?? [];
87
+ }
88
+ /**
89
+ * `node` (the type node the checker wrote for `type`) with every anonymous object type that an exported type of the app stands for replaced by a
90
+ * reference to that type. It follows type arguments (`Promise<{...}>`), arrays and unions; any other anonymous part stays for the caller to refuse.
91
+ */
92
+ export function nameAnonymousParts(c, node, type, enclosing) {
93
+ const { ts } = c;
94
+ const reference = (named) => c.checker.typeToTypeNode(named.type, enclosing, ts.NodeBuilderFlags.NoTruncation);
95
+ if (ts.isTypeLiteralNode(node) && node.members.length > 0) {
96
+ const named = matchingName(c, type, enclosing);
97
+ return (named === undefined ? undefined : reference(named)) ?? node;
98
+ }
99
+ if (ts.isArrayTypeNode(node) || ts.isTypeReferenceNode(node) || ts.isImportTypeNode(node)) {
100
+ const given = ts.isArrayTypeNode(node) ? [node.elementType] : node.typeArguments ?? [];
101
+ const held = argumentsOf(c, type);
102
+ if (!given.length || held.length < given.length)
103
+ return node;
104
+ const named = given.map((child, at) => nameAnonymousParts(c, child, held[at], enclosing));
105
+ if (named.every((child, at) => child === given[at]))
106
+ return node;
107
+ const { factory } = ts;
108
+ if (ts.isArrayTypeNode(node))
109
+ return factory.updateArrayTypeNode(node, named[0]);
110
+ if (ts.isImportTypeNode(node))
111
+ return factory.updateImportTypeNode(node, node.argument, node.attributes, node.qualifier, named, node.isTypeOf);
112
+ return factory.updateTypeReferenceNode(node, node.typeName, factory.createNodeArray(named));
113
+ }
114
+ if (ts.isUnionTypeNode(node) && type.isUnion()) {
115
+ const members = type.types.filter(member => (member.flags & ts.TypeFlags.Object) !== 0);
116
+ const used = new Set();
117
+ const named = node.types.map(child => {
118
+ if (!ts.isTypeLiteralNode(child))
119
+ return child;
120
+ const key = shapeKey(child.members.map(member => (member.name === undefined ? '' : ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) ? member.name.text : '')));
121
+ const match = members.find(member => !used.has(member) && shapeKey(c.checker.getPropertiesOfType(member).map(property => property.name)) === key);
122
+ if (match === undefined)
123
+ return child;
124
+ used.add(match);
125
+ return nameAnonymousParts(c, child, match, enclosing);
126
+ });
127
+ return named.every((child, at) => child === node.types[at]) ? node : ts.factory.updateUnionTypeNode(node, ts.factory.createNodeArray(named));
128
+ }
129
+ return node;
130
+ }
131
+ /** The directory-relative form of `target` from the folder of `from`, without extension, the way a relative import is written. */
132
+ export function relativeSpecifier(from, target) {
133
+ const relative = posix(path.relative(path.dirname(from), target)).replace(/\.(?:d\.)?[cm]?tsx?$/, '');
134
+ return relative.startsWith('.') ? relative : `./${relative}`;
135
+ }
@@ -0,0 +1,29 @@
1
+ import { type NamingContext } from './explicit-surface-names.mjs';
2
+ import type * as TsTypes from 'typescript';
3
+ /** An import the written type needs: `name` exported by the module `specifier`. */
4
+ export interface ImportNeed {
5
+ name: string;
6
+ specifier: string;
7
+ }
8
+ /** A type written as source text, with the imports it needs. */
9
+ export interface Rendered {
10
+ ok: true;
11
+ text: string;
12
+ needs: ImportNeed[];
13
+ }
14
+ /** Why a type is not written: a code of the report, the reason, and what a person does about it. */
15
+ export interface Refused {
16
+ ok: false;
17
+ code: string;
18
+ reason: string;
19
+ suggestion: string;
20
+ }
21
+ /** The longest type the codemod writes on one line; a longer one is a type a person names. */
22
+ export declare const MAX_TYPE_LENGTH = 100;
23
+ /**
24
+ * The type Next.js names for a route export (`generateMetadata`: `Promise<Metadata>`, `metadata`: `Metadata`, and the viewport pair), when `next` is installed and the inferred
25
+ * type is assignable to it; undefined for any other export, so the inferred type is written instead.
26
+ */
27
+ export declare function conventionType(c: NamingContext, name: string, inferred: TsTypes.Type, enclosing: TsTypes.Node, kind: 'function' | 'variable'): Rendered | undefined;
28
+ /** Writes `type`, as it is seen from `enclosing`, as source text: named types by name, the imports they need, or the reason it is left to a person. */
29
+ export declare function renderType(c: NamingContext, type: TsTypes.Type, enclosing: TsTypes.Node, maxLength?: number): Rendered | Refused;
@@ -0,0 +1,197 @@
1
+ import path from 'node:path';
2
+ import { declarationOf, exportedSymbol, moduleFile, nameAnonymousParts, namesInScope, relativeSpecifier } from './explicit-surface-names.mjs';
3
+ /** The longest type the codemod writes on one line; a longer one is a type a person names. */
4
+ export const MAX_TYPE_LENGTH = 100;
5
+ const refuse = (code, reason, suggestion) => ({ ok: false, code, reason, suggestion });
6
+ const posix = (file) => file.replaceAll('\\', '/');
7
+ const ABSOLUTE = /^(?:[A-Za-z]:)?\//;
8
+ const JSX_ELEMENT = new Set(['JSX.Element', 'React.JSX.Element']);
9
+ /** The text of an entity name (`A.B.C`). */
10
+ function entityText(ts, name) {
11
+ return ts.isIdentifier(name) ? name.text : `${entityText(ts, name.left)}.${name.right.text}`;
12
+ }
13
+ /** The leftmost identifier of an entity name. */
14
+ function rootIdentifier(ts, name) {
15
+ return ts.isIdentifier(name) ? name : rootIdentifier(ts, name.left);
16
+ }
17
+ /** The package specifier of a file under `node_modules` (`@types/react` is `react`, `@types/a__b` is `@a/b`), and the path inside it. */
18
+ function packageOf(file) {
19
+ const at = file.lastIndexOf('/node_modules/');
20
+ if (at < 0)
21
+ return undefined;
22
+ const parts = file.slice(at + '/node_modules/'.length).split('/');
23
+ const scoped = parts[0]?.startsWith('@') === true;
24
+ let name = parts.slice(0, scoped ? 2 : 1).join('/');
25
+ const typed = /^@types\/(.+)$/.exec(name);
26
+ if (typed?.[1] !== undefined)
27
+ name = typed[1].includes('__') ? `@${typed[1].replace('__', '/')}` : typed[1];
28
+ return { name, inner: parts.slice(scoped ? 2 : 1).join('/').replace(/\.(?:d\.)?[cm]?tsx?$/, '').replace(/\/?index$/, '') };
29
+ }
30
+ /** The specifiers an import of `target` from `c.file` can be written with, best first: a package name, a path alias the file already uses, a relative path. */
31
+ function specifierCandidates(c, target) {
32
+ const installed = packageOf(posix(target.fileName));
33
+ if (installed !== undefined)
34
+ return [installed.name, ...(installed.inner ? [`${installed.name}/${installed.inner}`] : [])];
35
+ const relative = relativeSpecifier(c.file, target.fileName);
36
+ const nodeNext = c.options.moduleResolution === c.ts.ModuleResolutionKind.NodeNext || c.options.moduleResolution === c.ts.ModuleResolutionKind.Node16;
37
+ const extension = /\.([cm]?)tsx?$/.exec(target.fileName);
38
+ const written = nodeNext ? `${relative}.${extension?.[1] === 'm' ? 'mjs' : extension?.[1] === 'c' ? 'cjs' : 'js'}` : relative;
39
+ const aliases = [];
40
+ const base = c.options.baseUrl ?? c.options['pathsBasePath'] ?? path.dirname(c.file);
41
+ const bare = posix(target.fileName).replace(/\.(?:d\.)?[cm]?tsx?$/, '');
42
+ for (const [pattern, targets] of Object.entries(c.options.paths ?? {})) {
43
+ for (const to of targets) {
44
+ const [head = '', tail = ''] = to.split('*');
45
+ const prefix = posix(path.resolve(base, head)) + (head.endsWith('/') ? '/' : '');
46
+ if (to.includes('*') && bare.startsWith(prefix) && bare.endsWith(tail))
47
+ aliases.push(pattern.replace('*', bare.slice(prefix.length, bare.length - tail.length)));
48
+ else if (!to.includes('*') && bare === posix(path.resolve(base, to)).replace(/\.(?:d\.)?[cm]?tsx?$/, ''))
49
+ aliases.push(pattern);
50
+ }
51
+ }
52
+ const usesAlias = c.source.statements.some(statement => c.ts.isImportDeclaration(statement) && c.ts.isStringLiteral(statement.moduleSpecifier)
53
+ && Object.keys(c.options.paths ?? {}).some(pattern => statement.moduleSpecifier.getText(c.source).slice(1, -1).startsWith(pattern.replace('*', ''))));
54
+ return usesAlias ? [...aliases, written] : [written, ...aliases];
55
+ }
56
+ /** Whether `specifier`, written in `c.file`, reaches the same declaration of `name` that `target` holds. */
57
+ function reaches(c, specifier, name, target) {
58
+ const resolved = c.ts.resolveModuleName(specifier, c.file, c.options, c.ts.sys).resolvedModule?.resolvedFileName;
59
+ const file = resolved === undefined ? undefined : c.program.getSourceFile(posix(resolved));
60
+ return file !== undefined && exportedSymbol(c, file, name) === target;
61
+ }
62
+ /** The module file a raw specifier of the checker's import type names: an absolute path without extension, or a module name. */
63
+ function targetModule(c, raw) {
64
+ if (ABSOLUTE.test(raw))
65
+ return moduleFile(c, raw);
66
+ const resolved = c.ts.resolveModuleName(raw, c.file, c.options, c.ts.sys).resolvedModule?.resolvedFileName;
67
+ return resolved === undefined ? undefined : c.program.getSourceFile(posix(resolved));
68
+ }
69
+ /**
70
+ * How `name`, exported by the module the checker named with `raw`, is written in the file: an import specifier to add, `null` when the file already has the
71
+ * name bound to that very declaration, or a refusal when the name is taken by something else or no specifier of the file reaches it.
72
+ */
73
+ function resolveNeed(c, scope, name, raw) {
74
+ const file = targetModule(c, raw);
75
+ const target = file === undefined ? undefined : exportedSymbol(c, file, name);
76
+ if (file === undefined || target === undefined)
77
+ return refuse('TYPE_UNNAMEABLE', `\`${name}\` is not exported by a module the file can import`, `export ${name} from its module, or declare a named type in this file`);
78
+ const taken = scope.get(name);
79
+ if (taken !== undefined) {
80
+ if (declarationOf(c, taken) === declarationOf(c, target))
81
+ return { specifier: null };
82
+ const global = taken.declarations?.every(declaration => !c.ts.isExternalModule(declaration.getSourceFile())) === true;
83
+ if (!global || new RegExp(`\\b${name}\\b`).test(c.text))
84
+ return refuse('TYPE_UNNAMEABLE', `\`${name}\` is already another name in this file`, `rename one of the two, or declare a named type in this file`);
85
+ }
86
+ const specifier = specifierCandidates(c, file).find(candidate => reaches(c, candidate, name, target));
87
+ return specifier === undefined ? refuse('TYPE_UNNAMEABLE', `no import path of this file reaches \`${name}\` (${posix(file.fileName)})`, `export ${name} from a module this file can import, or declare a named type in this file`) : { specifier };
88
+ }
89
+ /** Writes the checker's type node as source: import types become names plus imports, `JSX.Element` becomes `ReactElement`, and anonymous, `any` and non-nameable parts are refused. */
90
+ function rewrite(c, node, scope, needs) {
91
+ const { ts } = c;
92
+ let refused;
93
+ const need = (name, raw) => {
94
+ const resolved = resolveNeed(c, scope, name, raw);
95
+ if ('ok' in resolved)
96
+ refused ??= resolved;
97
+ else if (resolved.specifier !== null && !needs.some(entry => entry.name === name && entry.specifier === resolved.specifier))
98
+ needs.push({ name, specifier: resolved.specifier });
99
+ };
100
+ // The checker names a type by an identifier that carries its symbol: a name the file cannot write is imported from the module that exports it.
101
+ const named = (id) => {
102
+ const symbol = id.symbol;
103
+ if (symbol === undefined)
104
+ return;
105
+ if ((symbol.flags & ts.SymbolFlags.TypeParameter) !== 0)
106
+ return;
107
+ const taken = scope.get(id.text);
108
+ if (taken !== undefined && declarationOf(c, taken) === declarationOf(c, symbol))
109
+ return;
110
+ const file = symbol.declarations?.[0]?.getSourceFile();
111
+ if (file !== undefined && ts.isExternalModule(file))
112
+ need(id.text, file.fileName);
113
+ };
114
+ const result = ts.transform(node, [context => root => {
115
+ const visit = (child) => {
116
+ if (ts.isImportTypeNode(child)) {
117
+ const literal = ts.isLiteralTypeNode(child.argument) && ts.isStringLiteral(child.argument.literal) ? child.argument.literal.text : undefined;
118
+ if (child.isTypeOf || child.qualifier === undefined || literal === undefined) {
119
+ refused ??= refuse('TYPE_UNNAMEABLE', 'the type is a module or a `typeof import(...)`', 'declare a named type and annotate with it');
120
+ return child;
121
+ }
122
+ const args = child.typeArguments?.map(argument => visit(argument));
123
+ if (JSX_ELEMENT.has(entityText(ts, child.qualifier))) {
124
+ need('ReactElement', 'react');
125
+ return context.factory.createTypeReferenceNode('ReactElement');
126
+ }
127
+ need(rootIdentifier(ts, child.qualifier).text, literal);
128
+ return context.factory.createTypeReferenceNode(child.qualifier, args);
129
+ }
130
+ if (ts.isTypeReferenceNode(child) && JSX_ELEMENT.has(entityText(ts, child.typeName))) {
131
+ need('ReactElement', 'react');
132
+ return context.factory.createTypeReferenceNode('ReactElement');
133
+ }
134
+ if (ts.isTypeReferenceNode(child))
135
+ named(rootIdentifier(ts, child.typeName));
136
+ if (ts.isTypeQueryNode(child))
137
+ named(rootIdentifier(ts, child.exprName));
138
+ if (child.kind === ts.SyntaxKind.AnyKeyword) {
139
+ refused ??= refuse('TYPE_ANY', 'the inferred type contains `any`', 'write the type the value really has; an `any` on a surface hides the contract');
140
+ return child;
141
+ }
142
+ if (ts.isTypeLiteralNode(child) && child.members.length > 0) {
143
+ refused ??= refuse('TYPE_ANONYMOUS', 'the inferred type is an anonymous object type', 'declare a named type (`type X = ...`), export it and annotate with it');
144
+ return child;
145
+ }
146
+ if (ts.isMappedTypeNode(child) || ts.isConditionalTypeNode(child) || (ts.isTypeOperatorNode(child) && child.operator === ts.SyntaxKind.UniqueKeyword)) {
147
+ refused ??= refuse('TYPE_ANONYMOUS', 'the inferred type is computed (mapped, conditional or `unique symbol`)', 'declare a named type for it and annotate with it');
148
+ return child;
149
+ }
150
+ return ts.visitEachChild(child, visit, context);
151
+ };
152
+ return visit(root);
153
+ }]);
154
+ return refused ?? { node: result.transformed[0] };
155
+ }
156
+ /** The Next.js export conventions whose type the framework names: the export, its type in `next`, and whether it is a function. */
157
+ const CONVENTIONS = new Map([
158
+ ['generateMetadata', { type: 'Metadata', kind: 'function' }], ['metadata', { type: 'Metadata', kind: 'variable' }],
159
+ ['generateViewport', { type: 'Viewport', kind: 'function' }], ['viewport', { type: 'Viewport', kind: 'variable' }],
160
+ ]);
161
+ /**
162
+ * The type Next.js names for a route export (`generateMetadata`: `Promise<Metadata>`, `metadata`: `Metadata`, and the viewport pair), when `next` is installed and the inferred
163
+ * type is assignable to it; undefined for any other export, so the inferred type is written instead.
164
+ */
165
+ export function conventionType(c, name, inferred, enclosing, kind) {
166
+ const row = CONVENTIONS.get(name);
167
+ const file = row?.kind === kind ? targetModule(c, 'next') : undefined;
168
+ const symbol = row === undefined || file === undefined ? undefined : exportedSymbol(c, file, row.type);
169
+ if (row === undefined || symbol === undefined)
170
+ return undefined;
171
+ const awaited = kind === 'function' ? c.checker.getAwaitedType(inferred) : inferred;
172
+ if (awaited === undefined || !c.checker.isTypeAssignableTo(awaited, c.checker.getDeclaredTypeOfSymbol(symbol)))
173
+ return undefined;
174
+ const resolved = resolveNeed(c, namesInScope(c, enclosing), row.type, 'next');
175
+ if ('ok' in resolved)
176
+ return undefined;
177
+ const written = awaited === inferred ? row.type : `Promise<${row.type}>`;
178
+ return { ok: true, text: written, needs: resolved.specifier === null ? [] : [{ name: row.type, specifier: resolved.specifier }] };
179
+ }
180
+ /** Writes `type`, as it is seen from `enclosing`, as source text: named types by name, the imports they need, or the reason it is left to a person. */
181
+ export function renderType(c, type, enclosing, maxLength = MAX_TYPE_LENGTH) {
182
+ const { ts } = c;
183
+ if ((type.flags & ts.TypeFlags.Any) !== 0)
184
+ return refuse('TYPE_ANY', 'the inferred type is `any`', 'write the type the value really has; an `any` on a surface hides the contract');
185
+ const flags = ts.NodeBuilderFlags.NoTruncation;
186
+ const drawn = c.checker.typeToTypeNode(type, enclosing, flags | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) ?? c.checker.typeToTypeNode(type, enclosing, flags);
187
+ if (drawn === undefined)
188
+ return refuse('TYPE_UNNAMEABLE', 'the compiler cannot write the type', 'declare a named type and annotate with it');
189
+ const needs = [];
190
+ const rewritten = rewrite(c, nameAnonymousParts(c, drawn, type, enclosing), namesInScope(c, enclosing), needs);
191
+ if ('ok' in rewritten)
192
+ return rewritten;
193
+ const text = ts.createPrinter({ removeComments: true }).printNode(ts.EmitHint.Unspecified, rewritten.node, c.source);
194
+ if (text.length > maxLength)
195
+ return refuse('TYPE_TOO_LONG', `the type is ${text.length} characters (${text.slice(0, 60)}...)`, 'declare a named type (`type X = ...`), export it and annotate with it');
196
+ return { ok: true, text, needs };
197
+ }