@pikku/inspector 0.12.75 → 0.12.82

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 (223) hide show
  1. package/CHANGELOG.md +357 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-middleware.js +66 -42
  8. package/dist/add/add-wire-addon.js +7 -4
  9. package/dist/add/add-wire-remote-addon.js +1 -0
  10. package/dist/error-codes.d.ts +1 -0
  11. package/dist/error-codes.js +1 -0
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.js +3 -1
  14. package/dist/inspector.js +5 -0
  15. package/dist/surface/collect-surface.d.ts +58 -0
  16. package/dist/surface/collect-surface.js +613 -0
  17. package/dist/types.d.ts +36 -12
  18. package/dist/utils/filter-inspector-state.js +14 -0
  19. package/dist/utils/load-addon-functions-meta.js +24 -11
  20. package/dist/utils/post-process.js +15 -0
  21. package/dist/utils/read-module-specifiers.d.ts +8 -0
  22. package/dist/utils/read-module-specifiers.js +23 -0
  23. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  24. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  25. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  26. package/dist/utils/resolve-deploy-target.js +15 -7
  27. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  28. package/dist/utils/serialize-inspector-state.js +10 -0
  29. package/dist/visit.js +4 -0
  30. package/package.json +16 -6
  31. package/run-tests.sh +21 -6
  32. package/src/add/add-addon-bans.ts +0 -85
  33. package/src/add/add-agent-scorer.test.ts +0 -197
  34. package/src/add/add-agent-scorer.ts +0 -83
  35. package/src/add/add-agent.test.ts +0 -87
  36. package/src/add/add-agent.ts +0 -638
  37. package/src/add/add-approval-description.ts +0 -76
  38. package/src/add/add-auth.test.ts +0 -791
  39. package/src/add/add-auth.ts +0 -424
  40. package/src/add/add-channel.ts +0 -778
  41. package/src/add/add-cli-renderers.test.ts +0 -74
  42. package/src/add/add-cli.ts +0 -1006
  43. package/src/add/add-credential.ts +0 -186
  44. package/src/add/add-feature.test.ts +0 -306
  45. package/src/add/add-feature.ts +0 -253
  46. package/src/add/add-file-extends-core-type.ts +0 -66
  47. package/src/add/add-file-with-config.ts +0 -45
  48. package/src/add/add-file-with-factory.test.ts +0 -134
  49. package/src/add/add-file-with-factory.ts +0 -288
  50. package/src/add/add-functions.test.ts +0 -743
  51. package/src/add/add-functions.ts +0 -1548
  52. package/src/add/add-gateway.ts +0 -143
  53. package/src/add/add-http-route-ref.test.ts +0 -149
  54. package/src/add/add-http-route.ts +0 -530
  55. package/src/add/add-http-routes.ts +0 -471
  56. package/src/add/add-keyed-wiring.ts +0 -180
  57. package/src/add/add-mcp-prompt.ts +0 -157
  58. package/src/add/add-mcp-resource.ts +0 -175
  59. package/src/add/add-middleware.test.ts +0 -196
  60. package/src/add/add-middleware.ts +0 -879
  61. package/src/add/add-permission-auth.test.ts +0 -59
  62. package/src/add/add-permission.ts +0 -311
  63. package/src/add/add-personas.test.ts +0 -406
  64. package/src/add/add-personas.ts +0 -287
  65. package/src/add/add-queue-worker.ts +0 -117
  66. package/src/add/add-rpc-invocations.test.ts +0 -287
  67. package/src/add/add-rpc-invocations.ts +0 -221
  68. package/src/add/add-scenario-step.test.ts +0 -509
  69. package/src/add/add-schedule.ts +0 -120
  70. package/src/add/add-scope.test.ts +0 -419
  71. package/src/add/add-scope.ts +0 -221
  72. package/src/add/add-secret.ts +0 -10
  73. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  74. package/src/add/add-system-role.test.ts +0 -297
  75. package/src/add/add-system-role.ts +0 -162
  76. package/src/add/add-trigger.ts +0 -165
  77. package/src/add/add-variable.ts +0 -10
  78. package/src/add/add-wire-addon.test.ts +0 -185
  79. package/src/add/add-wire-addon.ts +0 -146
  80. package/src/add/add-wire-remote-addon.ts +0 -80
  81. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  82. package/src/add/add-workflow-fanout.test.ts +0 -166
  83. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  84. package/src/add/add-workflow-graph-input.test.ts +0 -94
  85. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  86. package/src/add/add-workflow-graph.ts +0 -644
  87. package/src/add/add-workflow-on-error.test.ts +0 -118
  88. package/src/add/add-workflow-return-switch.test.ts +0 -121
  89. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  90. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  91. package/src/add/add-workflow.test.ts +0 -155
  92. package/src/add/add-workflow.ts +0 -482
  93. package/src/add/addon-bans.test.ts +0 -121
  94. package/src/add/addon-contracts.test.ts +0 -221
  95. package/src/add/allow-escape-hatches.test.ts +0 -144
  96. package/src/add/audit-meta.test.ts +0 -75
  97. package/src/add/classified-sink-check.test.ts +0 -347
  98. package/src/add/config-value-metadata.test.ts +0 -263
  99. package/src/add/dynamic-import-check.test.ts +0 -119
  100. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  101. package/src/add/function-body-span.test.ts +0 -96
  102. package/src/add/inline-wiring-function.test.ts +0 -104
  103. package/src/add/pii-check.test.ts +0 -258
  104. package/src/add/rpc-type-cast.test.ts +0 -123
  105. package/src/add/scenario-instrumentation.test.ts +0 -89
  106. package/src/add/scenario-instrumentation.ts +0 -42
  107. package/src/add/scenario-marker.test.ts +0 -89
  108. package/src/add/scenario-not-extractable.test.ts +0 -159
  109. package/src/add/scenario-services.test.ts +0 -103
  110. package/src/add/scenario-skip.test.ts +0 -83
  111. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  112. package/src/add/wire-name-literal.test.ts +0 -117
  113. package/src/error-codes-emitted.test.ts +0 -76
  114. package/src/error-codes.ts +0 -145
  115. package/src/index.ts +0 -66
  116. package/src/inspector.ts +0 -575
  117. package/src/schema-generator.ts +0 -1
  118. package/src/source-file-cache.ts +0 -73
  119. package/src/tsconfig-paths.test.ts +0 -90
  120. package/src/types-map.ts +0 -141
  121. package/src/types.ts +0 -775
  122. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  123. package/src/utils/annotate-http-route-auth.ts +0 -53
  124. package/src/utils/check-classified-sinks.ts +0 -215
  125. package/src/utils/check-pii-output.ts +0 -120
  126. package/src/utils/compute-diagnostics.test.ts +0 -106
  127. package/src/utils/compute-required-schemas.ts +0 -48
  128. package/src/utils/contract-hashes.test.ts +0 -553
  129. package/src/utils/contract-hashes.ts +0 -387
  130. package/src/utils/custom-types-generator.test.ts +0 -124
  131. package/src/utils/custom-types-generator.ts +0 -150
  132. package/src/utils/detect-schema-vendor.test.ts +0 -123
  133. package/src/utils/detect-schema-vendor.ts +0 -120
  134. package/src/utils/does-type-extend-core-type.ts +0 -53
  135. package/src/utils/ensure-function-metadata.ts +0 -385
  136. package/src/utils/extract-function-name.ts +0 -612
  137. package/src/utils/extract-function-node.ts +0 -70
  138. package/src/utils/extract-node-value.test.ts +0 -117
  139. package/src/utils/extract-node-value.ts +0 -172
  140. package/src/utils/extract-secret-usage.test.ts +0 -81
  141. package/src/utils/extract-secret-usage.ts +0 -73
  142. package/src/utils/extract-services.ts +0 -70
  143. package/src/utils/extract-surface-usage.test.ts +0 -147
  144. package/src/utils/extract-surface-usage.ts +0 -121
  145. package/src/utils/filter-inspector-state.test.ts +0 -2232
  146. package/src/utils/filter-inspector-state.ts +0 -1298
  147. package/src/utils/filter-utils.test.ts +0 -843
  148. package/src/utils/filter-utils.ts +0 -161
  149. package/src/utils/find-root-dir.ts +0 -68
  150. package/src/utils/find-zod-transform.test.ts +0 -70
  151. package/src/utils/find-zod-transform.ts +0 -103
  152. package/src/utils/get-exported-variable-name.ts +0 -48
  153. package/src/utils/get-files-and-methods.ts +0 -157
  154. package/src/utils/get-property-value.test.ts +0 -141
  155. package/src/utils/get-property-value.ts +0 -331
  156. package/src/utils/hash.ts +0 -26
  157. package/src/utils/literal-properties.ts +0 -97
  158. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  159. package/src/utils/load-addon-functions-meta.ts +0 -508
  160. package/src/utils/load-addon-scopes.test.ts +0 -138
  161. package/src/utils/middleware.test.ts +0 -204
  162. package/src/utils/middleware.ts +0 -303
  163. package/src/utils/nested-project-filter.test.ts +0 -53
  164. package/src/utils/nested-project-filter.ts +0 -28
  165. package/src/utils/permissions-shorthand.test.ts +0 -79
  166. package/src/utils/permissions.test.ts +0 -125
  167. package/src/utils/permissions.ts +0 -129
  168. package/src/utils/post-process.test.ts +0 -1094
  169. package/src/utils/post-process.ts +0 -1604
  170. package/src/utils/resolve-addon-package.ts +0 -106
  171. package/src/utils/resolve-core-type.test.ts +0 -142
  172. package/src/utils/resolve-core-type.ts +0 -35
  173. package/src/utils/resolve-deploy-target.test.ts +0 -135
  174. package/src/utils/resolve-deploy-target.ts +0 -65
  175. package/src/utils/resolve-function-meta.ts +0 -25
  176. package/src/utils/resolve-function-types.ts +0 -42
  177. package/src/utils/resolve-identifier.ts +0 -46
  178. package/src/utils/resolve-ref-contract.ts +0 -71
  179. package/src/utils/resolve-versions.test.ts +0 -499
  180. package/src/utils/resolve-versions.ts +0 -228
  181. package/src/utils/schema-cache.test.ts +0 -124
  182. package/src/utils/schema-generator.test.ts +0 -272
  183. package/src/utils/schema-generator.ts +0 -780
  184. package/src/utils/secret-alias-services.test.ts +0 -146
  185. package/src/utils/secret-alias-services.ts +0 -58
  186. package/src/utils/serialize-inspector-state.ts +0 -742
  187. package/src/utils/serialize-mcp-json.ts +0 -150
  188. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  189. package/src/utils/serialize-openapi-json.ts +0 -277
  190. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  191. package/src/utils/single-declaration.ts +0 -57
  192. package/src/utils/type-utils.ts +0 -145
  193. package/src/utils/validate-auth-sessionless.ts +0 -30
  194. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  195. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  196. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  197. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  198. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  199. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  200. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  201. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  202. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  203. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  204. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  205. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  206. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  207. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  208. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  209. package/src/utils/workflow/dsl/index.ts +0 -11
  210. package/src/utils/workflow/dsl/patterns.ts +0 -440
  211. package/src/utils/workflow/dsl/validation.ts +0 -203
  212. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  213. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  214. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  215. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  216. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  217. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  218. package/src/utils/workflow/graph/index.ts +0 -28
  219. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  220. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  221. package/src/visit.ts +0 -181
  222. package/tsconfig.json +0 -19
  223. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,613 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join, relative, resolve } from 'node:path';
4
+ import ts from 'typescript';
5
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts'];
6
+ const MODULE_EXTENSIONS = [...SOURCE_EXTENSIONS, '.js', '.jsx', '.mjs', '.cjs'];
7
+ const wildcardStem = (fileName) => {
8
+ if (fileName.endsWith('.map'))
9
+ return null;
10
+ if (/\.d\.(ts|mts|cts)$/.test(fileName))
11
+ return null;
12
+ const extension = MODULE_EXTENSIONS.find((each) => fileName.endsWith(each));
13
+ return extension ? fileName.slice(0, -extension.length) : null;
14
+ };
15
+ const subpathMap = (exports) => {
16
+ if (typeof exports === 'string')
17
+ return { '.': exports };
18
+ return Object.keys(exports).some((key) => key.startsWith('.'))
19
+ ? exports
20
+ : { '.': exports };
21
+ };
22
+ const publishesCode = (target) => !/\.(css|scss|json|svg|png|woff2?|txt|md)$/.test(target);
23
+ const sourceForTarget = (packageDir, target, outDir) => {
24
+ const withoutPrefix = target.replace(/^\.\//, '');
25
+ const isDeclaration = /\.d\.(ts|mts|cts)$/.test(withoutPrefix);
26
+ if (!isDeclaration &&
27
+ SOURCE_EXTENSIONS.some((extension) => withoutPrefix.endsWith(extension))) {
28
+ return existsSync(join(packageDir, withoutPrefix)) ? withoutPrefix : null;
29
+ }
30
+ const stripped = withoutPrefix.startsWith(`${outDir}/`)
31
+ ? withoutPrefix.slice(outDir.length + 1)
32
+ : withoutPrefix;
33
+ const base = stripped
34
+ .replace(/\.d\.(ts|mts|cts)$/, '')
35
+ .replace(/\.(js|jsx|mjs|cjs)$/, '');
36
+ const withoutSrc = base.startsWith('src/') ? base.slice(4) : base;
37
+ const candidates = [];
38
+ for (const extension of SOURCE_EXTENSIONS) {
39
+ candidates.push(join('src', `${base}${extension}`), `${base}${extension}`, join('src', `${withoutSrc}${extension}`), join('src', base, `index${extension}`), join(base, `index${extension}`));
40
+ }
41
+ for (const candidate of candidates) {
42
+ if (existsSync(join(packageDir, candidate)))
43
+ return candidate;
44
+ }
45
+ return null;
46
+ };
47
+ const expandWildcard = (packageDir, subpath, target, outDir) => {
48
+ const targetDir = target.replace(/^\.\//, '').split('*')[0] ?? '';
49
+ const suffix = target.slice(target.indexOf('*') + 1);
50
+ const suffixStem = wildcardStem(suffix) ?? suffix;
51
+ // A package that points its subpaths at the build output has nothing to walk
52
+ // until it is built, so the sources the output would be compiled from stand
53
+ // in for it. `sourceForTarget` strips the same prefix, so the target stays
54
+ // the declared one and only the directory being read changes.
55
+ const scanDir = existsSync(join(packageDir, targetDir)) ||
56
+ !targetDir.startsWith(`${outDir}/`)
57
+ ? targetDir
58
+ : targetDir.slice(outDir.length + 1);
59
+ const absolute = join(packageDir, scanDir);
60
+ if (!existsSync(absolute))
61
+ return [];
62
+ const expanded = [];
63
+ const walk = (directory, prefix) => {
64
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
65
+ if (entry.isDirectory()) {
66
+ walk(join(directory, entry.name), `${prefix}${entry.name}/`);
67
+ continue;
68
+ }
69
+ if (!entry.isFile())
70
+ continue;
71
+ if (wildcardStem(entry.name) === null)
72
+ continue;
73
+ // The suffix is matched against the path the wildcard stands in for, not
74
+ // the file name, so `#pikku/*` → `./.pikku/*/index.ts` captures the leaf
75
+ // directory rather than failing to match `index.ts` against `/index.ts`.
76
+ const relativePath = `${prefix}${entry.name}`;
77
+ // Matched without extensions, so a `*/index.js` target still finds the
78
+ // `index.ts` it would have been compiled from.
79
+ const pathStem = `${prefix}${wildcardStem(entry.name)}`;
80
+ if (suffixStem && !pathStem.endsWith(suffixStem))
81
+ continue;
82
+ const captured = suffixStem
83
+ ? pathStem.slice(0, -suffixStem.length)
84
+ : pathStem;
85
+ if (!captured)
86
+ continue;
87
+ expanded.push({
88
+ subpath: subpath.replace('*', captured),
89
+ target: `${scanDir}${relativePath}`,
90
+ });
91
+ }
92
+ };
93
+ walk(absolute, '');
94
+ return expanded;
95
+ };
96
+ const targetOf = (value) => {
97
+ if (typeof value === 'string')
98
+ return value;
99
+ // A fallback array declares the same specifier several ways; the first entry
100
+ // is the one a resolver reaches for, and the rest only matter when it misses.
101
+ if (Array.isArray(value)) {
102
+ for (const candidate of value) {
103
+ const resolved = targetOf(candidate);
104
+ if (resolved)
105
+ return resolved;
106
+ }
107
+ return null;
108
+ }
109
+ if (!value || typeof value !== 'object')
110
+ return null;
111
+ const conditions = value;
112
+ for (const key of ['types', 'import', 'default', 'require']) {
113
+ const resolved = targetOf(conditions[key]);
114
+ if (resolved)
115
+ return resolved;
116
+ }
117
+ return null;
118
+ };
119
+ const readJson = async (path) => {
120
+ try {
121
+ return JSON.parse(await readFile(path, 'utf8'));
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ };
127
+ const readCompilerOptions = async (packageDir) => {
128
+ const configPath = join(packageDir, 'tsconfig.json');
129
+ if (!existsSync(configPath))
130
+ return { outDir: 'dist', paths: {} };
131
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
132
+ if (configFile.error)
133
+ return { outDir: 'dist', paths: {} };
134
+ const { options } = ts.parseJsonConfigFileContent(configFile.config, ts.sys, packageDir);
135
+ const { baseUrl, paths, pathsBasePath } = options;
136
+ return {
137
+ outDir: options.outDir ? relative(packageDir, options.outDir) : 'dist',
138
+ paths: {
139
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
140
+ ...(paths !== undefined ? { paths } : {}),
141
+ ...(pathsBasePath !== undefined ? { pathsBasePath } : {}),
142
+ },
143
+ };
144
+ };
145
+ const KIND_BY_FLAG = [
146
+ [ts.SymbolFlags.Class, 'class'],
147
+ [ts.SymbolFlags.Interface, 'interface'],
148
+ [ts.SymbolFlags.RegularEnum | ts.SymbolFlags.ConstEnum, 'enum'],
149
+ [ts.SymbolFlags.Function, 'function'],
150
+ [ts.SymbolFlags.TypeAlias, 'type'],
151
+ [ts.SymbolFlags.Namespace, 'namespace'],
152
+ ];
153
+ /**
154
+ * A leaf re-exports through `export *`, so the symbol reached here is an alias
155
+ * that carries no kind, no documentation, no tags and no type of its own.
156
+ */
157
+ const aliasTargetOf = (symbol, checker) => symbol.flags & ts.SymbolFlags.Alias
158
+ ? checker.getAliasedSymbol(symbol)
159
+ : symbol;
160
+ const kindOf = (symbol, checker) => {
161
+ const target = aliasTargetOf(symbol, checker);
162
+ for (const [flag, kind] of KIND_BY_FLAG) {
163
+ if (target.flags & flag)
164
+ return kind;
165
+ }
166
+ if (target.flags & ts.SymbolFlags.Variable) {
167
+ const declaration = target.declarations?.[0];
168
+ if (declaration) {
169
+ const type = checker.getTypeOfSymbolAtLocation(target, declaration);
170
+ if (type.getCallSignatures().length > 0)
171
+ return 'function';
172
+ }
173
+ return 'const';
174
+ }
175
+ return 'const';
176
+ };
177
+ const jsDocTagsOf = (symbol, checker) => {
178
+ const own = symbol.getJsDocTags(checker);
179
+ if (own.length > 0)
180
+ return own;
181
+ return aliasTargetOf(symbol, checker).getJsDocTags(checker);
182
+ };
183
+ const isDeprecated = (tags) => tags.some((tag) => tag.name === 'deprecated');
184
+ const deprecationReason = (tags) => {
185
+ const tag = tags.find((each) => each.name === 'deprecated');
186
+ if (!tag)
187
+ return undefined;
188
+ const text = ts.displayPartsToString(tag.text).trim();
189
+ return text.length > 0 ? text : undefined;
190
+ };
191
+ export class UnknownSnippetError extends Error {
192
+ constructor(name, symbol, known) {
193
+ super(`The example on ${symbol} names the snippet "${name}", which no template declares. Wrap the code you want shown in "// @snippet start ${name}" / "// @snippet end ${name}". Declared: ${known.join(', ')}.`);
194
+ this.name = 'UnknownSnippetError';
195
+ }
196
+ }
197
+ const SNIPPET_REFERENCE = /^snippet:\s*(\S+)$/;
198
+ /**
199
+ * The `@example` blocks. `getDocumentationComment` returns the description only,
200
+ * so an example an author wrote is dropped unless the tags are read separately.
201
+ * An `@example snippet: name` names a region of real template source instead of
202
+ * restating it, and is resolved here so the shipped doc holds code that compiles.
203
+ */
204
+ const examplesOf = (tags, symbolName, snippets) => tags
205
+ .filter((tag) => tag.name === 'example')
206
+ .map((tag) => ts.displayPartsToString(tag.text).trim())
207
+ .filter((text) => text.length > 0)
208
+ .map((text) => {
209
+ const reference = text.match(SNIPPET_REFERENCE);
210
+ if (!reference?.[1] || !snippets)
211
+ return text;
212
+ const snippet = snippets.get(reference[1]);
213
+ if (snippet === undefined) {
214
+ throw new UnknownSnippetError(reference[1], symbolName, [
215
+ ...snippets.keys(),
216
+ ]);
217
+ }
218
+ return snippet;
219
+ });
220
+ /**
221
+ * The doc comment as written. A leaf re-exports through `export *`, so the
222
+ * symbol reached here is an alias carrying no documentation of its own, and an
223
+ * overloaded function documents whichever overload the author chose to explain
224
+ * — hence the walk through the aliased symbol and then every declaration.
225
+ */
226
+ const documentationOf = (symbol, checker) => {
227
+ const targets = [symbol];
228
+ if (symbol.flags & ts.SymbolFlags.Alias) {
229
+ targets.push(checker.getAliasedSymbol(symbol));
230
+ }
231
+ for (const target of targets) {
232
+ const documentation = ts
233
+ .displayPartsToString(target.getDocumentationComment(checker))
234
+ .trim();
235
+ if (documentation.length > 0)
236
+ return documentation;
237
+ }
238
+ return exportDeclarationDocumentationOf(symbol);
239
+ };
240
+ /**
241
+ * A re-export the generator documents (`export type { Services }`) carries its
242
+ * JSDoc on the export statement, which TypeScript attaches to no symbol at all
243
+ * — so it is read off the node.
244
+ */
245
+ const exportDeclarationDocumentationOf = (symbol) => {
246
+ for (const declaration of symbol.declarations ?? []) {
247
+ if (!ts.isExportSpecifier(declaration))
248
+ continue;
249
+ const statement = declaration.parent.parent;
250
+ const [comment] = ts.getJSDocCommentsAndTags(statement);
251
+ if (!comment || !ts.isJSDoc(comment))
252
+ continue;
253
+ const documentation = ts.getTextOfJSDocComment(comment.comment)?.trim();
254
+ if (documentation && documentation.length > 0)
255
+ return documentation;
256
+ }
257
+ return undefined;
258
+ };
259
+ /**
260
+ * The first paragraph of the doc comment. A symbol's JSDoc regularly runs to
261
+ * several paragraphs of examples, and a list row shows one line.
262
+ */
263
+ const summaryOf = (documentation) => {
264
+ if (!documentation)
265
+ return undefined;
266
+ return documentation
267
+ .split(/\n\s*\n/)[0]
268
+ .replace(/\s+/g, ' ')
269
+ .trim();
270
+ };
271
+ const declarationFileOf = (symbol, checker) => {
272
+ const target = aliasTargetOf(symbol, checker);
273
+ const declaration = target.declarations?.[0] ?? symbol.declarations?.[0];
274
+ return declaration?.getSourceFile().fileName ?? null;
275
+ };
276
+ /**
277
+ * `NoTruncation` is the load-bearing one: the default cuts a type off at ~160
278
+ * characters, which lands in the middle of the generics of every wiring helper.
279
+ * `UseAliasDefinedOutsideCurrentScope` keeps a named alias named instead of
280
+ * inlining its definition, which is what keeps the result readable.
281
+ */
282
+ const SIGNATURE_FLAGS = ts.TypeFormatFlags.NoTruncation |
283
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
284
+ ts.TypeFormatFlags.WriteTypeArgumentsOfSignature;
285
+ const DECLARED_TYPE_KINDS = new Set(['interface', 'type']);
286
+ const typeOfSymbol = (symbol, kind, checker) => {
287
+ if (DECLARED_TYPE_KINDS.has(kind))
288
+ return checker.getDeclaredTypeOfSymbol(symbol);
289
+ const declaration = symbol.declarations?.[0];
290
+ return declaration
291
+ ? checker.getTypeOfSymbolAtLocation(symbol, declaration)
292
+ : undefined;
293
+ };
294
+ const printType = (type, location, checker) => checker.typeToString(type, location, SIGNATURE_FLAGS).replace(/\s+/g, ' ');
295
+ /**
296
+ * The widest an expanded type may print before its name is the better answer.
297
+ * A method verb or a content type says everything inline; a wiring object does
298
+ * not, and expanding it buries the keys that matter in the ones that do not.
299
+ */
300
+ const INLINE_LIMIT = 72;
301
+ const EXPANDED_FLAGS = SIGNATURE_FLAGS & ~ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
302
+ const NOT_TERMINAL = /[{}]|=>/;
303
+ const collapse = (constituents) => {
304
+ const collapsed = constituents.includes('true') && constituents.includes('false')
305
+ ? [
306
+ 'boolean',
307
+ ...constituents.filter((c) => c !== 'true' && c !== 'false'),
308
+ ]
309
+ : constituents;
310
+ const isAbsent = (c) => c === 'undefined' || c === 'null';
311
+ return [
312
+ ...collapsed.filter((c) => !isAbsent(c)),
313
+ ...collapsed.filter(isAbsent),
314
+ ];
315
+ };
316
+ /**
317
+ * A named type whose definition is a short union or an index signature costs a
318
+ * reader a second lookup to learn something that fits on the line they are
319
+ * already reading, so it is printed as its values rather than its name.
320
+ */
321
+ const printResolved = (type, location, checker, optional = false) => {
322
+ const constraint = type.flags & ts.TypeFlags.TypeParameter
323
+ ? checker.getBaseConstraintOfType(type)
324
+ : undefined;
325
+ if (constraint) {
326
+ return printResolved(constraint, location, checker, optional);
327
+ }
328
+ const named = printType(type, location, checker);
329
+ const expanded = type.isUnion()
330
+ ? collapse(type.types.map((constituent) => checker.typeToString(constituent, location, EXPANDED_FLAGS))).join(' | ')
331
+ : checker.typeToString(type, location, EXPANDED_FLAGS).replace(/\s+/g, ' ');
332
+ const shed = optional ? / \| undefined$/ : /(?!)/;
333
+ if (expanded === named || expanded.length > INLINE_LIMIT) {
334
+ return named.replace(shed, '');
335
+ }
336
+ return (NOT_TERMINAL.test(expanded) ? named : expanded).replace(shed, '');
337
+ };
338
+ /**
339
+ * A class prints as `typeof Foo`, which says nothing. Its construct signatures
340
+ * are what a caller needs, so they stand in for it.
341
+ */
342
+ const constructorSignature = (name, type, checker) => {
343
+ const signatures = type.getConstructSignatures();
344
+ if (signatures.length === 0)
345
+ return undefined;
346
+ return signatures
347
+ .map((signature) => `new ${name}(${signature
348
+ .getParameters()
349
+ .map((parameter) => {
350
+ const declaration = parameter.valueDeclaration;
351
+ const parameterType = declaration
352
+ ? checker.getTypeOfSymbolAtLocation(parameter, declaration)
353
+ : checker.getTypeOfSymbol(parameter);
354
+ const optional = declaration &&
355
+ ts.isParameter(declaration) &&
356
+ declaration.questionToken
357
+ ? '?'
358
+ : '';
359
+ return `${parameter.getName()}${optional}: ${printResolved(parameterType, declaration, checker, optional === '?')}`;
360
+ })
361
+ .join(', ')})`)
362
+ .join(' | ');
363
+ };
364
+ const signatureOf = (name, type, kind, location, checker) => {
365
+ const printed = kind === 'class'
366
+ ? constructorSignature(name, type, checker)
367
+ : printType(type, location, checker);
368
+ if (!printed || printed === 'any' || printed === 'error' || printed === name)
369
+ return undefined;
370
+ return collapseRuns(printed);
371
+ };
372
+ /**
373
+ * A union parameterised by the project — every function name, every route —
374
+ * is unbounded and belongs to `pikku meta`, not to a framework doc that is the
375
+ * same for every project. Printed in full it buried one export under 4,000
376
+ * tokens of another project's identifiers.
377
+ */
378
+ const LITERAL_RUN = /("[^"]*"(?: \| "[^"]*"){7,})/g;
379
+ const collapseRuns = (line) => line.replace(LITERAL_RUN, (run) => `${run.split(' | ').length} names (pikku meta)`);
380
+ /**
381
+ * A key declared by the project rather than by the framework — the services in
382
+ * its own `SingletonServices`, a type it wrote — is not part of the surface
383
+ * every project shares. Left in, the doc built from the sample project told
384
+ * every reader their singleton services hold a `todoStore`.
385
+ */
386
+ const isProjectOwned = (declaration, root) => {
387
+ const file = declaration.getSourceFile().fileName;
388
+ return file.startsWith(root) && !file.includes('node_modules');
389
+ };
390
+ const memberLine = (property, checker, program, root) => {
391
+ if (property.getName().startsWith('__'))
392
+ return undefined;
393
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
394
+ if (declaration &&
395
+ (program.isSourceFileDefaultLibrary(declaration.getSourceFile()) ||
396
+ isProjectOwned(declaration, root))) {
397
+ return undefined;
398
+ }
399
+ const type = declaration
400
+ ? checker.getTypeOfSymbolAtLocation(property, declaration)
401
+ : checker.getTypeOfSymbol(property);
402
+ const optional = property.flags & ts.SymbolFlags.Optional ? '?' : '';
403
+ const doc = ts
404
+ .displayPartsToString(property.getDocumentationComment(checker))
405
+ .replace(/\s+/g, ' ')
406
+ .trim();
407
+ return {
408
+ line: collapseRuns(`${property.getName()}${optional}: ${printResolved(type, declaration, checker, optional === '?')}`),
409
+ ...(doc ? { doc } : {}),
410
+ };
411
+ };
412
+ /**
413
+ * Every wiring and function helper takes one options object, so its signature
414
+ * can only name that object's type and the keys someone has to write sit one
415
+ * level below it. Anything else describes itself.
416
+ */
417
+ const shapeOf = (type, kind, checker) => {
418
+ if (kind === 'class') {
419
+ return type.getConstructSignatures()[0]?.getReturnType();
420
+ }
421
+ if (kind !== 'function' && kind !== 'const')
422
+ return type;
423
+ const signature = type.getCallSignatures()[0];
424
+ if (!signature)
425
+ return type;
426
+ for (const parameter of signature.getParameters()) {
427
+ const declaration = parameter.valueDeclaration;
428
+ const parameterType = declaration
429
+ ? checker.getTypeOfSymbolAtLocation(parameter, declaration)
430
+ : checker.getTypeOfSymbol(parameter);
431
+ if (checker.getPropertiesOfType(parameterType).length > 0)
432
+ return parameterType;
433
+ }
434
+ return undefined;
435
+ };
436
+ const membersOf = (type, kind, checker, program, root) => {
437
+ const shape = shapeOf(type, kind, checker);
438
+ if (!shape)
439
+ return [];
440
+ return checker
441
+ .getPropertiesOfType(shape)
442
+ .map((property) => memberLine(property, checker, program, root))
443
+ .filter((member) => member !== undefined)
444
+ .sort((a, b) => a.line.localeCompare(b.line));
445
+ };
446
+ /**
447
+ * `addError(SomeError, { status })` is a runtime registration, so nothing about
448
+ * a class says what it maps to. The calls are top-level statements next to the
449
+ * classes they register, which makes the mapping readable by parsing rather
450
+ * than by importing and booting the runtime.
451
+ *
452
+ * A surveyed project consumes pikku as `.d.ts`, and a declaration file carries
453
+ * no statements — the registrations survive only in the `.js` beside it, which
454
+ * is why the emitted sibling is parsed too.
455
+ */
456
+ const collectErrorStatuses = (program) => {
457
+ const statuses = new Map();
458
+ const record = (name, details) => {
459
+ if (!ts.isIdentifier(name) || !ts.isObjectLiteralExpression(details))
460
+ return;
461
+ for (const property of details.properties) {
462
+ if (!ts.isPropertyAssignment(property))
463
+ continue;
464
+ if (property.name.getText() !== 'status')
465
+ continue;
466
+ const status = Number(property.initializer.getText());
467
+ if (Number.isFinite(status))
468
+ statuses.set(name.text, status);
469
+ }
470
+ };
471
+ const scan = (sourceFile) => {
472
+ for (const statement of sourceFile.statements) {
473
+ if (!ts.isExpressionStatement(statement))
474
+ continue;
475
+ const call = statement.expression;
476
+ if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression))
477
+ continue;
478
+ if (call.expression.text === 'addError') {
479
+ const [name, details] = call.arguments;
480
+ if (name && details)
481
+ record(name, details);
482
+ continue;
483
+ }
484
+ if (call.expression.text !== 'addErrors')
485
+ continue;
486
+ const [list] = call.arguments;
487
+ if (!list || !ts.isArrayLiteralExpression(list))
488
+ continue;
489
+ for (const entry of list.elements) {
490
+ if (!ts.isArrayLiteralExpression(entry))
491
+ continue;
492
+ const [name, details] = entry.elements;
493
+ if (name && details)
494
+ record(name, details);
495
+ }
496
+ }
497
+ };
498
+ for (const sourceFile of program.getSourceFiles()) {
499
+ if (!sourceFile.isDeclarationFile) {
500
+ scan(sourceFile);
501
+ continue;
502
+ }
503
+ if (!sourceFile.text.includes('PikkuError'))
504
+ continue;
505
+ const emitted = sourceFile.fileName.replace(/\.d\.([cm]?ts)$/, '.$1');
506
+ if (emitted === sourceFile.fileName)
507
+ continue;
508
+ const source = ts.sys.readFile(emitted.replace(/\.ts$/, '.js'));
509
+ if (source === undefined)
510
+ continue;
511
+ scan(ts.createSourceFile(emitted, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS));
512
+ }
513
+ return statuses;
514
+ };
515
+ export const collectSurface = async (packageDir, { importsSubpath, subpaths, snippets } = {}) => {
516
+ const root = resolve(packageDir);
517
+ const packageJson = await readJson(join(root, 'package.json'));
518
+ if (!packageJson)
519
+ return [];
520
+ const declaredMap = importsSubpath
521
+ ? packageJson.imports?.[importsSubpath] !== undefined
522
+ ? { [importsSubpath]: packageJson.imports[importsSubpath] }
523
+ : {}
524
+ : packageJson.exports
525
+ ? subpathMap(packageJson.exports)
526
+ : {};
527
+ if (Object.keys(declaredMap).length === 0)
528
+ return [];
529
+ const { outDir, paths } = await readCompilerOptions(root);
530
+ const packageName = packageJson.name ?? '';
531
+ const entries = [];
532
+ const seenEntryFiles = new Set();
533
+ for (const [subpath, value] of Object.entries(declaredMap)) {
534
+ if (!importsSubpath && !subpath.startsWith('.'))
535
+ continue;
536
+ const target = targetOf(value);
537
+ if (!target || !publishesCode(target))
538
+ continue;
539
+ const declared = target.includes('*')
540
+ ? expandWildcard(root, subpath, target, outDir)
541
+ : [{ subpath, target }];
542
+ for (const each of declared) {
543
+ if (subpaths && !subpaths.includes(each.subpath))
544
+ continue;
545
+ const entryFile = sourceForTarget(root, each.target, outDir);
546
+ if (!entryFile || seenEntryFiles.has(entryFile))
547
+ continue;
548
+ seenEntryFiles.add(entryFile);
549
+ entries.push({
550
+ subpath: each.subpath,
551
+ specifier: importsSubpath
552
+ ? each.subpath
553
+ : each.subpath === '.'
554
+ ? packageName
555
+ : `${packageName}${each.subpath.slice(1)}`,
556
+ entryFile,
557
+ });
558
+ }
559
+ }
560
+ if (entries.length === 0)
561
+ return [];
562
+ const program = ts.createProgram(entries.map((entry) => join(root, entry.entryFile)), {
563
+ ...paths,
564
+ target: ts.ScriptTarget.ESNext,
565
+ module: ts.ModuleKind.Node16,
566
+ moduleResolution: ts.ModuleResolutionKind.Node16,
567
+ skipLibCheck: true,
568
+ skipDefaultLibCheck: true,
569
+ types: [],
570
+ allowJs: false,
571
+ checkJs: false,
572
+ });
573
+ const checker = program.getTypeChecker();
574
+ const errorStatuses = collectErrorStatuses(program);
575
+ return entries.map((entry) => {
576
+ const sourceFile = program.getSourceFile(join(root, entry.entryFile));
577
+ const moduleSymbol = sourceFile
578
+ ? checker.getSymbolAtLocation(sourceFile)
579
+ : undefined;
580
+ const symbols = [];
581
+ for (const symbol of moduleSymbol
582
+ ? checker.getExportsOfModule(moduleSymbol)
583
+ : []) {
584
+ const file = declarationFileOf(symbol, checker);
585
+ const kind = kindOf(symbol, checker);
586
+ const docs = documentationOf(symbol, checker);
587
+ const tags = jsDocTagsOf(symbol, checker);
588
+ const name = symbol.getName();
589
+ const target = aliasTargetOf(symbol, checker);
590
+ const type = typeOfSymbol(target, kind, checker);
591
+ const location = target.declarations?.[0];
592
+ const examples = examplesOf(tags, name, snippets);
593
+ const members = type ? membersOf(type, kind, checker, program, root) : [];
594
+ symbols.push({
595
+ name,
596
+ kind,
597
+ declaredAt: file ? relative(root, file) : entry.entryFile,
598
+ declaredIn: file,
599
+ deprecated: isDeprecated(tags),
600
+ deprecatedReason: deprecationReason(tags),
601
+ summary: summaryOf(docs),
602
+ docs,
603
+ signature: type
604
+ ? signatureOf(name, type, kind, location, checker)
605
+ : undefined,
606
+ members: members.length > 0 ? members : undefined,
607
+ examples: examples.length > 0 ? examples : undefined,
608
+ status: errorStatuses.get(name),
609
+ });
610
+ }
611
+ return { ...entry, symbols };
612
+ });
613
+ };