@elmeragroup/internal 0.1.1-canary.1
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.
- package/LICENSE +21 -0
- package/NOTICE +62 -0
- package/README.md +56 -0
- package/dist/api-artifacts/model.d.mts +3 -0
- package/dist/api-artifacts/model.mjs +2 -0
- package/dist/api-extractor.d.mts +3 -0
- package/dist/api-extractor.mjs +3 -0
- package/dist/dist-CcEC-Qb_.mjs +558 -0
- package/dist/dist-_NayZK35.mjs +7760 -0
- package/dist/errors-ByXSCE94.mjs +29 -0
- package/dist/index-C9vKRif1.d.mts +815 -0
- package/dist/index.d.mts +53 -0
- package/dist/index.mjs +15 -0
- package/dist/model-1dzGdHi8.d.mts +35 -0
- package/dist/oxlint/anti-slop.d.mts +6 -0
- package/dist/oxlint/anti-slop.mjs +1588 -0
- package/dist/oxlint.d.mts +6 -0
- package/dist/oxlint.mjs +2029 -0
- package/package.json +72 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
|
|
2
|
+
import { n as ApiArtifactsError, r as ProblemLog, t as ApiArtifactsDriftError } from "./errors-ByXSCE94.mjs";
|
|
3
|
+
import { t as ProjectExtractor } from "./dist-_NayZK35.mjs";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { API, NodeBuilderFlags, SignatureKind, SymbolFlags } from "typescript/unstable/sync";
|
|
7
|
+
import { isExpressionStatement, isStringLiteral } from "typescript/unstable/ast/is";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
//#region ../api-artifacts/dist/checker.js
|
|
11
|
+
/** Opens the configured project. Callers must close the result. */
|
|
12
|
+
function openLibraryProject(tsconfigPath, projectRoot) {
|
|
13
|
+
const api = new API({ cwd: projectRoot });
|
|
14
|
+
try {
|
|
15
|
+
const project = api.updateSnapshot({ openProjects: [tsconfigPath] }).getProject(tsconfigPath);
|
|
16
|
+
if (project === void 0) throw new Error(`Could not open the project at ${tsconfigPath}`);
|
|
17
|
+
return {
|
|
18
|
+
projectRoot,
|
|
19
|
+
project,
|
|
20
|
+
checker: project.checker,
|
|
21
|
+
program: project.program,
|
|
22
|
+
close: () => api.close()
|
|
23
|
+
};
|
|
24
|
+
} catch (error) {
|
|
25
|
+
api.close();
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A `"use client"` directive only counts as one when it leads the module, so a stray
|
|
31
|
+
* string expression further down never flips the classification.
|
|
32
|
+
*/
|
|
33
|
+
function readRscStatus(sourceFile) {
|
|
34
|
+
for (const statement of sourceFile.statements) {
|
|
35
|
+
if (!isExpressionStatement(statement) || !isStringLiteral(statement.expression)) return "server";
|
|
36
|
+
const authored = statement.expression.getText(sourceFile);
|
|
37
|
+
if (authored === "\"use client\"" || authored === "'use client'") return "client";
|
|
38
|
+
}
|
|
39
|
+
return "server";
|
|
40
|
+
}
|
|
41
|
+
/** Package name a forwarded prop comes from, e.g. `@base-ui/react` or `react`. */
|
|
42
|
+
function declaringPackage(declarationPath) {
|
|
43
|
+
const last = declarationPath.lastIndexOf("/node_modules/");
|
|
44
|
+
if (last === -1) return null;
|
|
45
|
+
const segments = declarationPath.slice(last + 14).split("/");
|
|
46
|
+
const first = segments[0];
|
|
47
|
+
if (first === void 0) return null;
|
|
48
|
+
if (first.startsWith("@")) {
|
|
49
|
+
const second = segments[1];
|
|
50
|
+
return second === void 0 ? first : `${first}/${second}`;
|
|
51
|
+
}
|
|
52
|
+
return first;
|
|
53
|
+
}
|
|
54
|
+
function isOwnProp(context, symbol) {
|
|
55
|
+
return symbol.declarations.some((declaration) => {
|
|
56
|
+
const relative = path.relative(context.projectRoot.toLowerCase(), declaration.path.toLowerCase()).replaceAll("\\", "/");
|
|
57
|
+
return relative !== ".." && !relative.startsWith("../") && !path.isAbsolute(relative) && !relative.split("/").includes("node_modules");
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function isOptional(symbol) {
|
|
61
|
+
return (symbol.flags & SymbolFlags.Optional) !== 0;
|
|
62
|
+
}
|
|
63
|
+
function isRecipeAxisDeclaration(declarationPath) {
|
|
64
|
+
const normalized = declarationPath.replaceAll("\\", "/");
|
|
65
|
+
const file = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
66
|
+
return file.endsWith("-variants.ts") || file.endsWith("-variants.tsx");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Classifies a prop from facts supplied by either checker. Recipe axes are
|
|
70
|
+
* checker-synthesized in the current model and are declared in `*-variants`
|
|
71
|
+
* sources by the Effect model; neither case needs the consumer-facing JSDoc
|
|
72
|
+
* policy applied to ordinary declared props.
|
|
73
|
+
*/
|
|
74
|
+
function propOrigin(declarationPaths, synthesized) {
|
|
75
|
+
return synthesized || declarationPaths.some(isRecipeAxisDeclaration) ? "recipe-axis" : "declared";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turns one source-inspection result into artifact source metadata. Unresolved
|
|
79
|
+
* implementations become an actionable problem instead of React declaration paths.
|
|
80
|
+
* A forwarded dependency value reads its metadata from the authored module that
|
|
81
|
+
* forwards it: that module's directive decides `rsc`, and it has no defaults.
|
|
82
|
+
*/
|
|
83
|
+
function partSourceFromInspection(context, partName, result, problems) {
|
|
84
|
+
if (result.status === "unresolved") {
|
|
85
|
+
problems.add(`${partName}: could not recover the authored implementation (${result.reason})`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const sourceFile = context.program.getSourceFile(result.filePath);
|
|
89
|
+
if (sourceFile === void 0) {
|
|
90
|
+
problems.add(`${partName}: could not load the authored implementation file (${result.filePath})`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
sourcePath: path.relative(context.projectRoot, sourceFile.fileName).replaceAll("\\", "/"),
|
|
95
|
+
rsc: readRscStatus(sourceFile),
|
|
96
|
+
defaults: new Map(result.status === "resolved" ? result.defaults.map((entry) => [entry.name, entry.initializerText]) : [])
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A symbol declared in several union branches reports each branch's JSDoc in turn.
|
|
101
|
+
* Identical paragraphs are the same sentence repeated, not two facts.
|
|
102
|
+
*/
|
|
103
|
+
function dedupeDocumentation(documentation) {
|
|
104
|
+
if (documentation === void 0) return "";
|
|
105
|
+
const seen = /* @__PURE__ */ new Set();
|
|
106
|
+
const kept = [];
|
|
107
|
+
for (const paragraph of documentation.split(/\n{2,}|\n/)) {
|
|
108
|
+
const trimmed = paragraph.trim();
|
|
109
|
+
if (trimmed === "" || seen.has(trimmed)) continue;
|
|
110
|
+
seen.add(trimmed);
|
|
111
|
+
kept.push(trimmed);
|
|
112
|
+
}
|
|
113
|
+
return kept.join(" ");
|
|
114
|
+
}
|
|
115
|
+
function printType(checker, type) {
|
|
116
|
+
if (type === void 0 || type.isErrorType()) return null;
|
|
117
|
+
const printed = checker.typeToString(type, void 0, NodeBuilderFlags.NoTruncation);
|
|
118
|
+
return printed === "" ? null : printed;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The one-line type a closed reference row shows, or `null` when the printed type is
|
|
122
|
+
* short enough to show in full.
|
|
123
|
+
*
|
|
124
|
+
* A plain-string heuristic on purpose: it decides what a *collapsed* row displays, and
|
|
125
|
+
* the expanded panel always carries the real signature, so being approximate costs
|
|
126
|
+
* nothing while parsing the printed type would cost a second type model.
|
|
127
|
+
*
|
|
128
|
+
* `on`/`get` must be followed by a capital to count as the handler/accessor convention —
|
|
129
|
+
* a prop literally named `open` or `gettable` is not a function.
|
|
130
|
+
*/
|
|
131
|
+
function shortTypeOf(propName, printedType) {
|
|
132
|
+
if (/^(?:on|get)[A-Z]/.test(propName) || printedType.includes("=>")) return "function";
|
|
133
|
+
if (printedType.split("|").length - 1 >= 2 || printedType.length >= 30) return "Union";
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function callSignature(checker, type) {
|
|
137
|
+
return checker.getSignaturesOfType(type, SignatureKind.Call)[0] ?? null;
|
|
138
|
+
}
|
|
139
|
+
function isForwardedProp(context, symbol) {
|
|
140
|
+
return propOrigin(symbol.declarations.map((declaration) => declaration.path), symbol.declarations.length === 0) !== "recipe-axis" && !isOwnProp(context, symbol);
|
|
141
|
+
}
|
|
142
|
+
const emptyForwarded = {
|
|
143
|
+
count: 0,
|
|
144
|
+
from: []
|
|
145
|
+
};
|
|
146
|
+
/** A forwarded value's own declaring package joins the packages its forwarded props come from. */
|
|
147
|
+
function withForwardedValue(forwarded, result) {
|
|
148
|
+
if (result.status !== "forwarded" || forwarded.from.includes(result.packageName)) return forwarded;
|
|
149
|
+
const from = [...forwarded.from, result.packageName].sort((left, right) => left.localeCompare(right));
|
|
150
|
+
return {
|
|
151
|
+
count: forwarded.count,
|
|
152
|
+
from
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Counts props the part accepts that are neither library-declared nor recipe
|
|
157
|
+
* axes — the same omitted set the published table drops.
|
|
158
|
+
*/
|
|
159
|
+
function forwardedOfProps(context, properties) {
|
|
160
|
+
const from = /* @__PURE__ */ new Set();
|
|
161
|
+
let count = 0;
|
|
162
|
+
for (const property of properties) {
|
|
163
|
+
if (!isForwardedProp(context, property)) continue;
|
|
164
|
+
count += 1;
|
|
165
|
+
for (const declaration of property.declarations) {
|
|
166
|
+
const packageName = declaringPackage(declaration.path);
|
|
167
|
+
if (packageName !== null) from.add(packageName);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
count,
|
|
172
|
+
from: [...from].sort((left, right) => left.localeCompare(right))
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function addProblem(problems, message) {
|
|
176
|
+
problems?.add(message);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resolves the checker-backed part requests for one public component. This is
|
|
180
|
+
* the *only* walk of a component's entry: generation calls it once and
|
|
181
|
+
* every downstream fact is read from the parts it returns.
|
|
182
|
+
*/
|
|
183
|
+
function componentPartRequests(context, request, problems) {
|
|
184
|
+
const { checker, program } = context;
|
|
185
|
+
const sourceFile = program.getSourceFile(request.entryFile);
|
|
186
|
+
if (sourceFile === void 0) {
|
|
187
|
+
addProblem(problems, `${request.entryFile}: entry module is not part of the library program`);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
191
|
+
if (moduleSymbol === void 0) {
|
|
192
|
+
addProblem(problems, `${request.entryFile}: entry module has no module symbol`);
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const moduleExports = checker.getExportsOfModule(moduleSymbol);
|
|
196
|
+
const parts = [];
|
|
197
|
+
for (const exportName of request.exportNames) {
|
|
198
|
+
const rootSymbol = moduleExports.find((exported) => exported.name === exportName);
|
|
199
|
+
if (rootSymbol === void 0) {
|
|
200
|
+
addProblem(problems, `${request.entryFile}: does not export "${exportName}"`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const rootType = checker.getTypeOfSymbol(rootSymbol);
|
|
204
|
+
if (rootType === void 0 || rootType.isErrorType()) {
|
|
205
|
+
addProblem(problems, `${exportName}: exported value has an unresolvable type`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (callSignature(checker, rootType) !== null) {
|
|
209
|
+
parts.push({
|
|
210
|
+
name: exportName,
|
|
211
|
+
exportName,
|
|
212
|
+
type: rootType
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const start = parts.length;
|
|
217
|
+
for (const member of checker.getPropertiesOfType(rootType)) {
|
|
218
|
+
const memberType = checker.getTypeOfSymbol(member);
|
|
219
|
+
if (memberType === void 0 || callSignature(checker, memberType) === null) continue;
|
|
220
|
+
parts.push({
|
|
221
|
+
name: `${exportName}.${member.name}`,
|
|
222
|
+
exportName,
|
|
223
|
+
memberName: member.name,
|
|
224
|
+
type: memberType
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (parts.length === start) addProblem(problems, `${exportName}: no renderable parts were found on the exported namespace`);
|
|
228
|
+
}
|
|
229
|
+
return parts;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Prints one accepted prop's checker facts.
|
|
233
|
+
*
|
|
234
|
+
* Printing a type is the single most expensive checker call in the pass, so it is
|
|
235
|
+
* done per prop a consumer actually asks about — the dependency-enrichment merge
|
|
236
|
+
* asks for the handful of Base UI props it selects, not for all ~300 React and DOM
|
|
237
|
+
* props every part forwards.
|
|
238
|
+
*/
|
|
239
|
+
function readPartPropFact(context, part, propName) {
|
|
240
|
+
const property = part.props.get(propName);
|
|
241
|
+
if (property === void 0) return;
|
|
242
|
+
return {
|
|
243
|
+
type: printType(context.checker, context.checker.getTypeOfSymbol(property)),
|
|
244
|
+
required: !isOptional(property)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function describePart(context, request, signature, source, hasPropsParameter, propsResolved, props, forwarded, problems) {
|
|
248
|
+
const { checker } = context;
|
|
249
|
+
if (signature === null) {
|
|
250
|
+
problems.add(`${request.name}: no call signature — it does not look like a component`);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
if (source === null) return null;
|
|
254
|
+
if (!hasPropsParameter) return {
|
|
255
|
+
name: request.name,
|
|
256
|
+
rsc: source.rsc,
|
|
257
|
+
sourcePath: source.sourcePath,
|
|
258
|
+
props: [],
|
|
259
|
+
forwardedFrom: forwarded.from,
|
|
260
|
+
forwardedCount: 0
|
|
261
|
+
};
|
|
262
|
+
if (!propsResolved) {
|
|
263
|
+
problems.add(`${request.name}: props type is unresolvable`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const rows = [];
|
|
267
|
+
for (const property of props.values()) {
|
|
268
|
+
const isRecipeAxis = propOrigin(property.declarations.map((declaration) => declaration.path), property.declarations.length === 0) === "recipe-axis";
|
|
269
|
+
if (isForwardedProp(context, property)) continue;
|
|
270
|
+
const printed = printType(checker, checker.getTypeOfSymbol(property));
|
|
271
|
+
if (printed === null) {
|
|
272
|
+
problems.add(`${request.name}.${property.name}: type is unresolvable — the docs build cannot print it (${source.sourcePath})`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const description = dedupeDocumentation(checker.getDocumentationCommentOfSymbol(property));
|
|
276
|
+
if (description === "" && !isRecipeAxis) {
|
|
277
|
+
problems.add(`${request.name}.${property.name}: public prop has no JSDoc description (${source.sourcePath})`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
rows.push({
|
|
281
|
+
name: property.name,
|
|
282
|
+
origin: isRecipeAxis ? "recipe-axis" : "declared",
|
|
283
|
+
type: printed,
|
|
284
|
+
shortType: shortTypeOf(property.name, printed),
|
|
285
|
+
defaultValue: source.defaults.get(property.name) ?? null,
|
|
286
|
+
description,
|
|
287
|
+
required: !isOptional(property)
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
rows.sort((left, right) => left.name.localeCompare(right.name));
|
|
291
|
+
return {
|
|
292
|
+
name: request.name,
|
|
293
|
+
rsc: source.rsc,
|
|
294
|
+
sourcePath: source.sourcePath,
|
|
295
|
+
props: rows,
|
|
296
|
+
forwardedFrom: forwarded.from,
|
|
297
|
+
forwardedCount: forwarded.count
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Reads every fact one part yields, resolving its props type exactly once. */
|
|
301
|
+
function extractPart(context, request, sourceResult, problems) {
|
|
302
|
+
const source = partSourceFromInspection(context, request.name, sourceResult, problems);
|
|
303
|
+
const { checker } = context;
|
|
304
|
+
const signature = callSignature(checker, request.type);
|
|
305
|
+
const declarationPaths = signature?.declaration === void 0 ? [] : [signature.declaration.path];
|
|
306
|
+
const parameter = signature?.getParameters()[0];
|
|
307
|
+
const declared = parameter === void 0 ? void 0 : checker.getTypeOfSymbol(parameter);
|
|
308
|
+
const propsType = declared === void 0 || declared.isErrorType() ? null : declared;
|
|
309
|
+
const props = /* @__PURE__ */ new Map();
|
|
310
|
+
if (propsType !== null) for (const property of checker.getPropertiesOfType(propsType)) props.set(property.name, property);
|
|
311
|
+
const forwarded = withForwardedValue(props.size === 0 ? emptyForwarded : forwardedOfProps(context, props.values()), sourceResult);
|
|
312
|
+
return {
|
|
313
|
+
name: request.name,
|
|
314
|
+
declarationPaths,
|
|
315
|
+
source,
|
|
316
|
+
forwarded,
|
|
317
|
+
props,
|
|
318
|
+
part: describePart(context, request, signature, source, parameter !== void 0, propsType !== null, props, forwarded, problems)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region ../api-artifacts/dist/enrichment.js
|
|
323
|
+
function propertiesOf(type) {
|
|
324
|
+
switch (type.kind) {
|
|
325
|
+
case "component": return type.props;
|
|
326
|
+
case "object": return type.properties;
|
|
327
|
+
case "intersection": return [...type.properties, ...type.types.flatMap(propertiesOf)];
|
|
328
|
+
case "union": return type.types.flatMap(propertiesOf);
|
|
329
|
+
default: return [];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function selectedProps(result, rootName, partName) {
|
|
333
|
+
let type = result.module.exports.find((entry) => entry.name === rootName)?.type;
|
|
334
|
+
const ownerPath = [rootName];
|
|
335
|
+
if (partName !== rootName) {
|
|
336
|
+
const memberName = partName.slice(rootName.length + 1);
|
|
337
|
+
type = type?.kind === "object" ? type.properties.find((entry) => entry.name === memberName)?.type : void 0;
|
|
338
|
+
ownerPath.push("properties", memberName);
|
|
339
|
+
}
|
|
340
|
+
if (type === void 0) return void 0;
|
|
341
|
+
if (type.kind === "function") {
|
|
342
|
+
const parameter = type.callSignatures[0]?.parameters[0];
|
|
343
|
+
if (parameter === void 0) return void 0;
|
|
344
|
+
return {
|
|
345
|
+
properties: propertiesOf(parameter.type),
|
|
346
|
+
ownerPath: [
|
|
347
|
+
...ownerPath,
|
|
348
|
+
"callSignatures",
|
|
349
|
+
"0",
|
|
350
|
+
"parameters",
|
|
351
|
+
parameter.name,
|
|
352
|
+
"properties"
|
|
353
|
+
]
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
properties: propertiesOf(type),
|
|
358
|
+
ownerPath: [...ownerPath, "props"]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function enrichPart(context, component, current, result, roots, packages) {
|
|
362
|
+
const root = roots.find((name) => current.name === name || current.name.startsWith(`${name}.`));
|
|
363
|
+
const facts = component.partApis.find((part) => part.name === current.name);
|
|
364
|
+
if (root === void 0 || facts === void 0) return current;
|
|
365
|
+
const selected = selectedProps(result, root, current.name);
|
|
366
|
+
if (selected === void 0) return current;
|
|
367
|
+
const names = new Set(current.props.map((prop) => prop.name));
|
|
368
|
+
const additions = [];
|
|
369
|
+
for (const property of selected.properties) {
|
|
370
|
+
if (names.has(property.name)) continue;
|
|
371
|
+
const propPath = [...selected.ownerPath, property.name];
|
|
372
|
+
const provenance = result.provenance.find((entry) => entry.path.length === propPath.length && entry.path.every((segment, index) => segment === propPath[index]));
|
|
373
|
+
if (provenance?.synthesized === true || provenance?.declarations.some((declaration) => declaration.owner?.kind === "project")) continue;
|
|
374
|
+
const owners = [...new Set(provenance?.declarations.flatMap((declaration) => declaration.owner?.kind === "dependency" ? [declaration.owner.packageName] : []) ?? [])];
|
|
375
|
+
const packageName = owners.length === 1 ? owners[0] : void 0;
|
|
376
|
+
const description = dedupeDocumentation(property.documentation?.description);
|
|
377
|
+
if (packageName === void 0 || !packages.includes(packageName) || description === "") continue;
|
|
378
|
+
const fact = readPartPropFact(context, facts, property.name);
|
|
379
|
+
if (fact === void 0) continue;
|
|
380
|
+
if (fact.type === null) throw new Error(`${current.name}.${property.name}: selected dependency prop has an unresolvable type`);
|
|
381
|
+
additions.push({
|
|
382
|
+
name: property.name,
|
|
383
|
+
origin: { packageName },
|
|
384
|
+
type: fact.type,
|
|
385
|
+
shortType: shortTypeOf(property.name, fact.type),
|
|
386
|
+
defaultValue: facts.source?.defaults.get(property.name) ?? property.documentation?.defaultValue ?? null,
|
|
387
|
+
description,
|
|
388
|
+
required: fact.required
|
|
389
|
+
});
|
|
390
|
+
names.add(property.name);
|
|
391
|
+
}
|
|
392
|
+
additions.sort((left, right) => left.name.localeCompare(right.name));
|
|
393
|
+
if (additions.length > current.forwardedCount) throw new Error(`${current.name}: selected props exceed forwarded prop count`);
|
|
394
|
+
return {
|
|
395
|
+
...current,
|
|
396
|
+
props: [...current.props, ...additions],
|
|
397
|
+
forwardedCount: current.forwardedCount - additions.length
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function enrichComponents(context, results, model, packages) {
|
|
401
|
+
const diagnostics = [];
|
|
402
|
+
return {
|
|
403
|
+
components: model.map((component, index) => {
|
|
404
|
+
const result = results[index];
|
|
405
|
+
if (result === void 0) throw new Error(`Missing extraction for ${component.slug}`);
|
|
406
|
+
diagnostics.push(...result.warnings.map((warning) => ({
|
|
407
|
+
component: component.slug,
|
|
408
|
+
warning
|
|
409
|
+
})));
|
|
410
|
+
return {
|
|
411
|
+
...component,
|
|
412
|
+
parts: component.parts.map((part) => enrichPart(context, component, part, result, component.exportNames, packages))
|
|
413
|
+
};
|
|
414
|
+
}),
|
|
415
|
+
diagnostics
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region ../api-artifacts/dist/generate.js
|
|
420
|
+
function requestsFor(options) {
|
|
421
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
422
|
+
const outputs = /* @__PURE__ */ new Set();
|
|
423
|
+
return options.components.map((component) => {
|
|
424
|
+
const entryFile = path.resolve(options.projectRoot, component.entryFile);
|
|
425
|
+
const outputFile = path.resolve(options.projectRoot, component.outputFile);
|
|
426
|
+
if (!component.slug.trim() || slugs.has(component.slug)) throw new ApiArtifactsError([`Empty or duplicate component slug: ${component.slug}`]);
|
|
427
|
+
if (outputFile === path.resolve(options.projectRoot, options.tsconfigPath)) throw new ApiArtifactsError(["The tsconfig cannot be an artifact output"]);
|
|
428
|
+
if (outputs.has(outputFile) || !outputFile.endsWith(".json")) throw new ApiArtifactsError([`Duplicate or non-JSON output: ${outputFile}`]);
|
|
429
|
+
if (component.exportNames.length === 0 || new Set(component.exportNames).size !== component.exportNames.length || component.exportNames.some((name) => !name.trim())) throw new ApiArtifactsError([`${component.slug}: provide unique, non-empty export names`]);
|
|
430
|
+
slugs.add(component.slug);
|
|
431
|
+
outputs.add(outputFile);
|
|
432
|
+
return {
|
|
433
|
+
...component,
|
|
434
|
+
entryFile,
|
|
435
|
+
outputFile
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function existingText(file) {
|
|
440
|
+
try {
|
|
441
|
+
return await readFile(file, "utf8");
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Replaces complete files; a reader never sees a partially written JSON document. */
|
|
448
|
+
async function writeArtifact(file, text) {
|
|
449
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
450
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
451
|
+
try {
|
|
452
|
+
await writeFile(temporary, text, { flag: "wx" });
|
|
453
|
+
await rename(temporary, file);
|
|
454
|
+
} finally {
|
|
455
|
+
await rm(temporary, { force: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function describeComponent(context, request, parts, inspected, problems) {
|
|
459
|
+
if (inspected.length !== parts.length) throw new ApiArtifactsError([`${request.slug}: source inspection returned ${inspected.length} results for ${parts.length} parts`]);
|
|
460
|
+
const partApis = parts.map((part, index) => {
|
|
461
|
+
const source = inspected[index];
|
|
462
|
+
if (source === void 0) throw new ApiArtifactsError([`${part.name}: missing source inspection result`]);
|
|
463
|
+
return extractPart(context, part, source, problems);
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
slug: request.slug,
|
|
467
|
+
exportNames: request.exportNames,
|
|
468
|
+
parts: partApis.flatMap((entry) => entry.part === null ? [] : [entry.part]),
|
|
469
|
+
partApis
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/** Extracts and validates the entire inventory before writing any artifact. */
|
|
473
|
+
async function generateApiArtifacts(options) {
|
|
474
|
+
const projectRoot = path.resolve(options.projectRoot);
|
|
475
|
+
const tsconfigPath = path.resolve(projectRoot, options.tsconfigPath);
|
|
476
|
+
const requests = requestsFor({
|
|
477
|
+
...options,
|
|
478
|
+
projectRoot
|
|
479
|
+
});
|
|
480
|
+
if (requests.length === 0) return {
|
|
481
|
+
components: [],
|
|
482
|
+
diagnostics: []
|
|
483
|
+
};
|
|
484
|
+
const context = openLibraryProject(tsconfigPath, projectRoot);
|
|
485
|
+
const problems = new ProblemLog();
|
|
486
|
+
let model;
|
|
487
|
+
let diagnostics = [];
|
|
488
|
+
try {
|
|
489
|
+
const generated = await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
|
|
490
|
+
const extractor = yield* ProjectExtractor;
|
|
491
|
+
const described = yield* Effect.forEach(requests, (request) => Effect.gen(function* () {
|
|
492
|
+
const parts = componentPartRequests(context, request, problems);
|
|
493
|
+
const inspected = yield* extractor.inspectComponentSources(request.entryFile, parts);
|
|
494
|
+
return describeComponent(context, request, parts, inspected, problems);
|
|
495
|
+
}));
|
|
496
|
+
if (problems.problems.length > 0) return yield* Effect.fail(new ApiArtifactsError(problems.problems));
|
|
497
|
+
const packages = options.includeExternalTypes ?? [];
|
|
498
|
+
if (packages.length === 0) return {
|
|
499
|
+
components: described,
|
|
500
|
+
diagnostics: []
|
|
501
|
+
};
|
|
502
|
+
const extracted = yield* Effect.forEach(requests, (entry) => extractor.extractModule(entry.entryFile, { includeExternalTypes: packages }));
|
|
503
|
+
const enriched = enrichComponents(context, extracted, described, packages);
|
|
504
|
+
const rejected = enriched.diagnostics.filter((diagnostic) => !options.allowedWarningCodes?.includes(diagnostic.warning.code));
|
|
505
|
+
if (rejected.length > 0) return yield* Effect.fail(new ApiArtifactsError(rejected.map(({ component, warning }) => `${component}: ${warning.code}: ${warning.message}`)));
|
|
506
|
+
return enriched;
|
|
507
|
+
}).pipe(Effect.provide(ProjectExtractor.live({
|
|
508
|
+
tsconfigPath,
|
|
509
|
+
cwd: projectRoot
|
|
510
|
+
})))));
|
|
511
|
+
model = generated.components;
|
|
512
|
+
diagnostics = generated.diagnostics;
|
|
513
|
+
} finally {
|
|
514
|
+
context.close();
|
|
515
|
+
}
|
|
516
|
+
const components = [];
|
|
517
|
+
for (const [index, component] of model.entries()) {
|
|
518
|
+
const request = requests[index];
|
|
519
|
+
if (request === void 0) throw new Error(`Missing output for ${component.slug}`);
|
|
520
|
+
const artifact = {
|
|
521
|
+
$generated: options.generatedBy ?? "Generated from TypeScript types and JSDoc by @elmeragroup/api-artifacts. Do not edit.",
|
|
522
|
+
slug: component.slug,
|
|
523
|
+
parts: component.parts.map((part) => ({
|
|
524
|
+
name: part.name,
|
|
525
|
+
rsc: part.rsc,
|
|
526
|
+
sourcePath: part.sourcePath,
|
|
527
|
+
forwardedFrom: part.forwardedFrom,
|
|
528
|
+
forwardedCount: part.forwardedCount,
|
|
529
|
+
props: part.props.map((prop) => ({
|
|
530
|
+
name: prop.name,
|
|
531
|
+
origin: prop.origin,
|
|
532
|
+
type: prop.type,
|
|
533
|
+
shortType: prop.shortType,
|
|
534
|
+
defaultValue: prop.defaultValue,
|
|
535
|
+
description: prop.description,
|
|
536
|
+
required: prop.required
|
|
537
|
+
}))
|
|
538
|
+
}))
|
|
539
|
+
};
|
|
540
|
+
const text = `${JSON.stringify(artifact, null, 2)}\n`;
|
|
541
|
+
components.push({
|
|
542
|
+
...artifact,
|
|
543
|
+
outputFile: request.outputFile,
|
|
544
|
+
text,
|
|
545
|
+
changed: await existingText(request.outputFile) !== text
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const changed = components.filter((component) => component.changed);
|
|
549
|
+
if (options.mode === "check") {
|
|
550
|
+
if (changed.length > 0) throw new ApiArtifactsDriftError(changed.map((component) => component.outputFile));
|
|
551
|
+
} else for (const component of changed) await writeArtifact(component.outputFile, component.text);
|
|
552
|
+
return {
|
|
553
|
+
components,
|
|
554
|
+
diagnostics
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
export { generateApiArtifacts };
|