@brew-lang/sdk 0.0.0-stage → 0.3.0-alpha.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/build/fetch.js ADDED
@@ -0,0 +1,1245 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BASE_IMPORT = exports.BASE_TYPE = void 0;
37
+ exports.tsType = tsType;
38
+ exports.stripTypeParams = stripTypeParams;
39
+ exports.splitParams = splitParams;
40
+ exports.refNames = refNames;
41
+ exports.refQuals = refQuals;
42
+ exports.createMemberCollector = createMemberCollector;
43
+ exports.addMethodMember = addMethodMember;
44
+ exports.addFieldMember = addFieldMember;
45
+ exports.assembleInterface = assembleInterface;
46
+ exports.apiDocsToInterface = apiDocsToInterface;
47
+ exports.extractClassLinks = extractClassLinks;
48
+ exports.extractPackageLinks = extractPackageLinks;
49
+ exports.isNestedHref = isNestedHref;
50
+ exports.indexLinks = indexLinks;
51
+ exports.makeResolver = makeResolver;
52
+ exports.mirrorDirs = mirrorDirs;
53
+ exports.writeMirrorIndexes = writeMirrorIndexes;
54
+ exports.writeMirrorPackage = writeMirrorPackage;
55
+ exports.writeBrewJson = writeBrewJson;
56
+ exports.loadBrewMap = loadBrewMap;
57
+ exports.resolveExternalRef = resolveExternalRef;
58
+ exports.buildClassIndex = buildClassIndex;
59
+ exports.importPathFor = importPathFor;
60
+ exports.importBlockFor = importBlockFor;
61
+ exports.withImports = withImports;
62
+ exports.fileExportLine = fileExportLine;
63
+ exports.buildIndex = buildIndex;
64
+ exports.declaredNames = declaredNames;
65
+ exports.replaceImports = replaceImports;
66
+ exports.brewPackageName = brewPackageName;
67
+ exports.parseBrewPackageName = parseBrewPackageName;
68
+ exports.baseImportFor = baseImportFor;
69
+ exports.discoverJavadocRoots = discoverJavadocRoots;
70
+ exports.fetchAllApiDocs = fetchAllApiDocs;
71
+ exports.fetchApiDocs = fetchApiDocs;
72
+ exports.writeFetched = writeFetched;
73
+ /**
74
+ * brewc fetch — turn a Spigot/Bukkit javadoc page into a TypeScript interface.
75
+ *
76
+ * Fetches the page, extracts the class's public instance fields and methods
77
+ * from the javadoc member signatures, and generates an interface extending
78
+ * `JavaObject` (so the transpiler's type dispatcher treats values of the
79
+ * type with skript-Reflect syntax). Methods following the JavaBean
80
+ * convention become TS accessors:
81
+ *
82
+ * Location getLocation() -> get location(): Location;
83
+ * void setLocation(Location loc) -> set location(loc: Location);
84
+ */
85
+ const fs = __importStar(require("fs"));
86
+ const path = __importStar(require("path"));
87
+ const JAVA_TO_TS = {
88
+ void: "void",
89
+ int: "number", long: "number", short: "number", byte: "number",
90
+ float: "number", double: "number", boolean: "boolean", char: "string",
91
+ Integer: "number", Long: "number", Short: "number", Byte: "number",
92
+ Float: "number", Double: "number", Boolean: "boolean", Character: "string",
93
+ String: "string", CharSequence: "string", Object: "object",
94
+ };
95
+ /**
96
+ * Classes outside a Bukkit-style mirrored tree (JDK, Guava, Gson,
97
+ * bungeecord-chat, snakeyaml, JOML) can only map to a plain TS type. When
98
+ * the mirrored tree itself declares such a class (fetch-javadoc of the JDK),
99
+ * `isTreeClass` lets it bind to its interface instead.
100
+ */
101
+ const EXTERNAL_TYPES = {
102
+ Class: "object", Number: "number", Iterator: "object", UUID: "string",
103
+ File: "object", URL: "string", URI: "string", InetAddress: "object",
104
+ SocketAddress: "object", Logger: "object", LogRecord: "object",
105
+ Random: "object", Thread: "object", Runnable: "object", Future: "object",
106
+ Callable: "object", Stream: "object", Predicate: "object",
107
+ Supplier: "object", Multimap: "object", HashMap: "object",
108
+ ListIterator: "object", TimeZone: "object", JsonElement: "object",
109
+ BaseComponent: "object", Image: "object", Throwable: "object",
110
+ Node: "object", MappingNode: "object", Optional: "object",
111
+ InetSocketAddress: "object", CompletableFuture: "object",
112
+ InputStream: "object", OutputStream: "object", Writer: "object",
113
+ Reader: "object", Matrix4f: "object", Vector3f: "object",
114
+ Quaternionf: "object", TCommand: "any", // javadoc type variable
115
+ Vector2d: "object", Vector2dc: "object", Vector2f: "object", Vector2fc: "object",
116
+ Vector2i: "object", Vector2ic: "object", Vector3d: "object", Vector3dc: "object",
117
+ Vector3fc: "object", Vector3i: "object", Vector3ic: "object", // JOML
118
+ // java.time / java.util.concurrent / java.awt and friends
119
+ Instant: "object", Duration: "object", Period: "object", LocalDate: "object",
120
+ LocalTime: "object", LocalDateTime: "object", ZonedDateTime: "object",
121
+ OffsetDateTime: "object", OffsetTime: "object", ZoneId: "object",
122
+ ZoneOffset: "object", DayOfWeek: "object", Month: "object", Year: "object",
123
+ YearMonth: "object", MonthDay: "object", Clock: "object", TimeUnit: "object",
124
+ ChronoUnit: "object", DateTimeFormatter: "object", Color: "object",
125
+ BufferedImage: "object", Graphics2D: "object", RenderingHints: "object",
126
+ // java.util / java.util.function extras
127
+ Scanner: "object", BitSet: "object", StringTokenizer: "object",
128
+ Consumer: "object", BiConsumer: "object", Function: "object",
129
+ BiFunction: "object", UnaryOperator: "object", BinaryOperator: "object",
130
+ IntFunction: "object", ToIntFunction: "object", BooleanSupplier: "object",
131
+ IntConsumer: "object", DoubleConsumer: "object", LongConsumer: "object",
132
+ ObjIntConsumer: "object", Properties: "object", StringJoiner: "string",
133
+ StringBuilder: "string", Formatter: "object", Currency: "object",
134
+ Locale: "object", Date: "object", Calendar: "object", Timer: "object",
135
+ TimerTask: "object", // java.util
136
+ // java.net / java.nio extras
137
+ Socket: "object", ServerSocket: "object", HttpURLConnection: "object",
138
+ Path: "object", Files: "object", Charset: "object", ByteBuffer: "object",
139
+ CharBuffer: "object", Buffer: "object",
140
+ // common library types (NOT API classes like org.bukkit.Plugin — those
141
+ // must stay linkable to their mirrored pack via brew.json)
142
+ JsonObject: "object", JsonArray: "object", Gson: "object",
143
+ // API classes the javadoc does not document (page 404s), so no mirror can
144
+ // declare them: map to object instead of emitting an unresolvable name
145
+ ChatMessageType: "object", Dialog: "object",
146
+ };
147
+ /** Best-effort Java type -> TypeScript type. */
148
+ function tsType(raw, isTreeClass) {
149
+ let t = raw.trim();
150
+ if (t.endsWith("..."))
151
+ t = t.slice(0, -3) + "[]"; // varargs are arrays
152
+ t = t.replace(/\?\s*(?:extends|super)\s+/g, ""); // wildcard bounds
153
+ if (t === "?")
154
+ return "any";
155
+ // strip package prefixes (java.util.List<X> -> List<X>, common in Java
156
+ // sources); anchored so capitalized segments (Villager.Profession,
157
+ // MarkerOptions.Builder) stay — they are nested-class references
158
+ t = t.replace(/^(?:[a-z_$][\w$]*\.)+/, "");
159
+ const list = /^(?:List|Set|Collection|Queue|Iterable|Iterator)<(.+)>$/.exec(t);
160
+ if (list)
161
+ return `${tsType(list[1], isTreeClass)}[]`;
162
+ if (/^Map<.+>$/.test(t))
163
+ return "object";
164
+ if (t.endsWith("[]"))
165
+ return `${tsType(t.slice(0, -2), isTreeClass)}[]`;
166
+ const core = t.split("<")[0].split(".").pop() ?? t;
167
+ if (/^[A-Z][A-Z0-9_]*$/.test(core))
168
+ return "any"; // type variable (T, E, T_CONS, ...)
169
+ // String, the boxed types, Object, ... are Skript natives: always plain TS
170
+ if (JAVA_TO_TS[core] !== undefined)
171
+ return JAVA_TO_TS[core];
172
+ if (isTreeClass?.(core))
173
+ return core; // declared/linkable by the mirror
174
+ const ext = EXTERNAL_TYPES[core];
175
+ if (ext !== undefined)
176
+ return ext;
177
+ // mirror mode (isTreeClass given): an unknown, unlinked class gets no
178
+ // import, so map it to object instead of emitting an unresolvable name;
179
+ // single-page fetch (no isTreeClass) keeps the bare name as a plain ref
180
+ return isTreeClass ? "object" : (core || "any");
181
+ }
182
+ /** Strip tags/entities/annotations from a javadoc signature block. */
183
+ function cleanSignature(html) {
184
+ return html
185
+ .replace(/\u200B/g, "") // javadoc zero-width spaces after identifiers
186
+ // javadoc PREVIEW marks (<sup class="preview-mark">PREVIEW</sup>) must
187
+ // go before the tags, or their text glues onto the type name
188
+ .replace(/<sup class="preview-mark">[\s\S]*?<\/sup>/g, " ")
189
+ .replace(/<[^>]+>/g, "")
190
+ .replace(/&nbsp;/g, " ")
191
+ .replace(/&lt;/g, "<")
192
+ .replace(/&gt;/g, ">")
193
+ .replace(/&amp;/g, "&")
194
+ .replace(/&quot;/g, '"')
195
+ .replace(/@\w+\s*\([^)]*\)/g, "") // annotations with arguments
196
+ .replace(/@\w+/g, "") // bare annotations
197
+ .replace(/\s+/g, " ")
198
+ .trim();
199
+ }
200
+ /** Lowercase the first character: getLocation -> location. */
201
+ function propertyName(getterSuffix) {
202
+ if (!getterSuffix)
203
+ return "";
204
+ return getterSuffix[0].toLowerCase() + getterSuffix.slice(1);
205
+ }
206
+ /**
207
+ * Strip a leading method type-parameter clause (`<T extends Comparable<T>>`)
208
+ * from a signature header, at any nesting depth. The clause must directly
209
+ * follow whitespace-separated modifier words, so a generic RETURN type
210
+ * (`List<Gadget> foo()`) is kept: "List<" has no space before "<".
211
+ */
212
+ function stripTypeParams(header) {
213
+ const m = /^((?:\w+\s+)*?)</.exec(header);
214
+ if (!m)
215
+ return header;
216
+ const open = m[1].length; // index of '<'
217
+ let depth = 0;
218
+ for (let i = open; i < header.length; i++) {
219
+ if (header[i] === "<")
220
+ depth++;
221
+ else if (header[i] === ">" && --depth === 0)
222
+ return m[1] + header.slice(i + 1);
223
+ }
224
+ return header;
225
+ }
226
+ /** Parameter names that collide with TypeScript keywords (Java allows them). */
227
+ const TS_RESERVED = new Set([
228
+ "function", "class", "interface", "enum", "constructor", "get", "set",
229
+ "static", "public", "private", "protected", "readonly", "declare",
230
+ "module", "namespace", "type", "new", "delete", "in", "of", "var", "let",
231
+ "const", "return", "if", "else", "for", "while", "do", "switch", "case",
232
+ "break", "continue", "default", "extends", "implements", "import",
233
+ "export", "await", "yield", "async", "arguments",
234
+ ]);
235
+ /** Split a Java parameter list on top-level commas. */
236
+ function splitParams(params, isTreeClass) {
237
+ const raw = [];
238
+ let depth = 0;
239
+ let current = "";
240
+ for (const ch of params) {
241
+ if (ch === "<")
242
+ depth++;
243
+ if (ch === ">")
244
+ depth--;
245
+ if (ch === "," && depth === 0) {
246
+ raw.push(current);
247
+ current = "";
248
+ }
249
+ else {
250
+ current += ch;
251
+ }
252
+ }
253
+ if (current.trim())
254
+ raw.push(current);
255
+ return raw.map((p) => {
256
+ // drop parameter modifiers ("final World w" -> type "World", name "w")
257
+ const parts = p.trim().split(/\s+/).filter((w) => w !== "final");
258
+ let name = parts[parts.length - 1];
259
+ if (TS_RESERVED.has(name))
260
+ name += "_"; // e.g. a Consumer parameter named "function"
261
+ const type = parts.slice(0, -1).join(" ");
262
+ return { name, type: tsType(type, isTreeClass), raw: type };
263
+ });
264
+ }
265
+ /** Class-like names referenced by a Java type expression ("List<org.bukkit.entity.Entity>" -> ["Entity"]). */
266
+ function refNames(rawType, isTreeClass) {
267
+ return [...refQuals(rawType, isTreeClass).keys()];
268
+ }
269
+ /**
270
+ * Like refNames, but also keeps the package-qualified form of each ref
271
+ * ("org.bukkit.World"), so a reference to a class the mirrored tree does
272
+ * not declare can be linked to the brew package providing it (see
273
+ * resolveExternalRef). Map: ref ("World", "Villager.Profession") ->
274
+ * qualified name ("org.bukkit.World", "Villager.Profession").
275
+ */
276
+ function refQuals(rawType, isTreeClass) {
277
+ // JDK collections are structural, not cross-references to the API's classes
278
+ const CONTAINERS = new Set(["List", "Set", "Map", "Collection", "Iterable", "Queue", "Iterator"]);
279
+ const quals = new Map();
280
+ for (const m of rawType.matchAll(/[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/g)) {
281
+ const full = m[0];
282
+ // strip package segments (lowercase): org.bukkit.World -> World,
283
+ // Villager.Profession -> Villager.Profession (a nested class reference)
284
+ const segments = full.split(".");
285
+ while (segments.length > 1 && /^[a-z_$]/.test(segments[0]))
286
+ segments.shift();
287
+ const ref = segments.join(".");
288
+ const core = segments[segments.length - 1];
289
+ if (!/^[A-Z]/.test(core))
290
+ continue; // primitives and type parameters
291
+ if (CONTAINERS.has(core))
292
+ continue;
293
+ if (JAVA_TO_TS[core] !== undefined || core === "void")
294
+ continue;
295
+ // external classes are references only when the tree declares them
296
+ if (EXTERNAL_TYPES[core] !== undefined && !isTreeClass?.(ref))
297
+ continue;
298
+ if (!quals.has(ref))
299
+ quals.set(ref, full);
300
+ }
301
+ return quals;
302
+ }
303
+ /**
304
+ * Collects interface members, resolving accessor/method name clashes the
305
+ * same way for javadoc pages and Java sources (shared by both parsers).
306
+ *
307
+ * TS allows method overloads and getter/setter pairs, but not two
308
+ * accessors of the same kind (setTypes(Tag) / setTypes(Collection)), nor
309
+ * a method or field sharing a property's name (header()/getHeader()).
310
+ */
311
+ function createMemberCollector() {
312
+ const members = [];
313
+ const seen = new Map(); // accessor overload detection
314
+ const memberKinds = new Map(); // name -> get/set/method/field
315
+ const addMember = (member) => {
316
+ if (members.includes(member))
317
+ return; // exact duplicate signature
318
+ const bare = member.replace(/^static\s+/, ""); // parse without the static prefix
319
+ const prop = bare.match(/^(?:get|set) ([A-Za-z_$][\w$]*)/)?.[1];
320
+ const kind = prop ? bare.slice(0, 3).trim() : bare.includes("(") ? "method" : "field";
321
+ const memberName = prop ?? bare.match(/^([A-Za-z_$][\w$]*)/)?.[1] ?? "";
322
+ const kinds = memberKinds.get(memberName) ?? new Set();
323
+ const isAccessor = kind === "get" || kind === "set";
324
+ // methods overload freely; clashes are accessors vs methods/fields
325
+ // (a method may not share a property's name) and field vs anything
326
+ const clash = kinds.size > 0 && (isAccessor
327
+ ? [...kinds].some((k) => k !== "get" && k !== "set")
328
+ : [...kinds].some((k) => k !== "method"));
329
+ if (clash)
330
+ return;
331
+ const key = isAccessor ? `${kind} ${memberName}` : member;
332
+ const dup = seen.get(key) ?? 0;
333
+ seen.set(key, dup + 1);
334
+ kinds.add(kind);
335
+ memberKinds.set(memberName, kinds);
336
+ members.push(dup === 0 ? member : member.replace(/^(\s*(?:static\s+)?(?:get|set )?)([A-Za-z_]\w*)/, (_s, pre, n2) => `${pre}${n2}_${dup + 1}`));
337
+ };
338
+ return { members, addMember };
339
+ }
340
+ /** Turn a parsed Java method signature into an interface member (shared by
341
+ * the javadoc and Java-source parsers). */
342
+ function addMethodMember(sig, ctx) {
343
+ let mName = sig.name;
344
+ // javadoc often omits the public modifier entirely; only members
345
+ // explicitly marked otherwise are skipped
346
+ if (/\b(private|protected)\b/.test(sig.mods))
347
+ return;
348
+ if (!ctx.includeStatic && /\bstatic\b/.test(sig.mods))
349
+ return;
350
+ if (mName === ctx.className)
351
+ return; // constructor
352
+ const plist = splitParams(sig.params, ctx.isTreeClass);
353
+ const retT = tsType(sig.ret, ctx.isTreeClass);
354
+ ctx.addRefs(sig.ret);
355
+ for (const p of plist)
356
+ ctx.addRefs(p.raw);
357
+ const isGetter = /^get[A-Z]/.test(mName) && plist.length === 0 && retT !== "void";
358
+ const isSetter = /^set[A-Z]/.test(mName) && plist.length === 1 && retT === "void";
359
+ let member;
360
+ // java.lang.Object's public instance methods: a subclass override
361
+ // with a different parameter count cannot legally extend the base
362
+ // interface (Certificate.toString(boolean) vs toString()), so those
363
+ // rare incompatible overloads get a distinguishing suffix
364
+ const BASE_PARAMS = {
365
+ hashCode: [0], toString: [0], equals: [1], notify: [0], notifyAll: [0],
366
+ };
367
+ if (Object.hasOwn(BASE_PARAMS, mName) && !BASE_PARAMS[mName].includes(plist.length)) {
368
+ mName = `${mName}_2`;
369
+ }
370
+ if (isGetter && propertyName(mName.slice(3))) {
371
+ member = `get ${propertyName(mName.slice(3))}(): ${retT};`;
372
+ }
373
+ else if (isSetter && propertyName(mName.slice(3))) {
374
+ member = `set ${propertyName(mName.slice(3))}(${plist[0].name}: ${plist[0].type});`;
375
+ }
376
+ else {
377
+ const args = plist.map((p) => `${p.name}: ${p.type}`).join(", ");
378
+ member = `${mName}(${args}): ${retT};`;
379
+ }
380
+ // declare-class members carry their static modifier, so the class's
381
+ // static side resolves in editors (BukkitAdapter.worldIdentifier)
382
+ if (ctx.includeStatic && /\bstatic\b/.test(sig.mods))
383
+ member = `static ${member}`;
384
+ ctx.addMember(member);
385
+ }
386
+ /** Turn a parsed Java field signature into an interface member (shared by
387
+ * the javadoc and Java-source parsers). */
388
+ function addFieldMember(sig, ctx) {
389
+ if (/\b(private|protected)\b/.test(sig.mods))
390
+ return;
391
+ if (!ctx.includeStatic && /\bstatic\b/.test(sig.mods))
392
+ return;
393
+ ctx.addRefs(sig.type);
394
+ const field = `${sig.name}: ${tsType(sig.type, ctx.isTreeClass)};`;
395
+ ctx.addMember(ctx.includeStatic && /\bstatic\b/.test(sig.mods) ? `static ${field}` : field);
396
+ }
397
+ /** Assemble the generated declaration from collected members (shared by the
398
+ * javadoc and Java-source generators). Mirrors emit `export declare class` —
399
+ * not `interface` — because only classes carry a static side, so static
400
+ * members (emitted with the `static` modifier) resolve as `Class.member` in
401
+ * editors and `declare class` is a drop-in for the interface shape. */
402
+ function assembleInterface(opts) {
403
+ const body = opts.members.length
404
+ ? opts.members.map((x) => ` ${x}`).join("\n")
405
+ : " // no public members found";
406
+ // the full Java class location for the transpiler's import section
407
+ const docBlock = opts.pkg ? `/**\n * @classloc ${opts.pkg}.${opts.fullName}\n */\n` : "";
408
+ // java.lang.Object itself is the root: it cannot extend or import itself
409
+ const isBase = opts.name === exports.BASE_TYPE.name;
410
+ const ts = `${opts.sourceLine}\n` +
411
+ `// Extends ${exports.BASE_TYPE.name} (java.lang.Object) from ${exports.BASE_TYPE.module}.\n` +
412
+ "\n" +
413
+ docBlock +
414
+ `export declare class ${opts.name}${isBase ? "" : ` extends ${exports.BASE_TYPE.name}`} {\n${body}\n}\n`;
415
+ return { name: opts.name, ts, refs: [...opts.refs].filter((r) => r !== opts.name) };
416
+ }
417
+ /**
418
+ * Generate the interface source from javadoc HTML (pure, testable).
419
+ * Targets the modern javadoc layout (`<div class="member-signature">`).
420
+ *
421
+ * `refs` lists the class-like names the members reference — candidates for
422
+ * imports; the fetch-javadoc mirror turns them into `import type` statements
423
+ * so the tree cross-references itself. `opts.name` / `opts.fullName` override
424
+ * the derived class name (nested classes declare as their last name segment,
425
+ * but keep the dotted name for @classloc).
426
+ */
427
+ function apiDocsToInterface(html, url, opts = {}) {
428
+ // Class name: the javadoc header ("Class Location" -> Location), else the
429
+ // page title, else the URL basename (Location.html -> Location). Dotted
430
+ // header names belong to nested classes (Enum Class AbstractArrow.PickupStatus)
431
+ // and javadoc's PREVIEW types (StructuredTaskScope.SubtaskPREVIEW): the
432
+ // interface keeps the last segment, @classloc keeps the dots.
433
+ const h1 = /<h1[^>]*>\s*(?:(?:Class|Interface|Enum(?:\s+Class)?|Record|Annotation(?:\s+Interface)?)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/.exec(html)?.[1] ?? "";
434
+ const title = /<title>\s*(?:(?:Class|Interface|Enum|Record)\s+)?([A-Za-z_]\w*)/.exec(html)?.[1] ?? "";
435
+ const urlName = decodeURIComponent((url.split("/").pop() ?? "").replace(/\.html.*$/, ""));
436
+ const keyword = /^(Class|Interface|Enum|Record|Annotation)$/;
437
+ const name = opts.name ||
438
+ (h1 && !keyword.test(h1) ? h1.split(".").pop() : "") ||
439
+ (title && !keyword.test(title) ? title : "") ||
440
+ urlName.split(".").pop() || "FetchedApi";
441
+ const fullName = opts.fullName ||
442
+ (h1 && !keyword.test(h1) && h1.includes(".") ? h1 : "") ||
443
+ urlName || name;
444
+ const collector = createMemberCollector();
445
+ const refs = new Set();
446
+ const ctx = {
447
+ className: name,
448
+ isTreeClass: opts.isTreeClass,
449
+ addRefs: (rawType) => {
450
+ for (const ref of refNames(rawType, opts.isTreeClass))
451
+ refs.add(ref);
452
+ },
453
+ addMember: collector.addMember,
454
+ };
455
+ const sigRe = /<div class="member-signature">([\s\S]*?)<\/div>/g;
456
+ let m;
457
+ while ((m = sigRe.exec(html))) {
458
+ // strip the generic type-parameter marker ([modifiers] <T extends X> Ret name(...))
459
+ // and the throws clause (…() throws IOException) — the method regex
460
+ // anchors on the closing paren
461
+ const sig = stripTypeParams(cleanSignature(m[1])).replace(/\s*throws\s+[\w\s.,$]+$/, "");
462
+ // Method: [modifiers] ReturnType name(params)
463
+ const method = /^((?:public|protected|private|static|final|abstract|synchronized|native|default|strictfp|\s)*)\s*([^()\s][^()]*?)\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*$/.exec(sig);
464
+ if (method) {
465
+ addMethodMember({ mods: method[1], ret: method[2], name: method[3], params: method[4] }, ctx);
466
+ continue;
467
+ }
468
+ // Field: [modifiers] Type name
469
+ const field = /^((?:public|protected|private|static|final|transient|volatile|\s)*)\s*([^()\s][^()]*?)\s+([A-Za-z_]\w*)$/.exec(sig);
470
+ if (field) {
471
+ addFieldMember({ mods: field[1], type: field[2], name: field[3] }, ctx);
472
+ }
473
+ }
474
+ const pkg = /package-summary\.html[^>]*>([A-Za-z0-9_.]+)</.exec(html)?.[1] ?? "";
475
+ return assembleInterface({
476
+ name,
477
+ fullName,
478
+ pkg,
479
+ sourceLine: `// Generated by brewc fetch from ${url}`,
480
+ members: collector.members,
481
+ refs,
482
+ });
483
+ }
484
+ /** Extract class page paths from an allclasses-index/allclasses page (pure). */
485
+ function extractClassLinks(html, opts = {}) {
486
+ const links = new Set();
487
+ const re = /<a[^>]+href="([^"]+\.html)"/g;
488
+ let m;
489
+ while ((m = re.exec(html))) {
490
+ let href = m[1].replace(/^\.\//, "").split("#")[0];
491
+ if (/^[a-z][\w+.-]*:/i.test(href) || href.startsWith("/") || href.startsWith("//"))
492
+ continue; // off-site link
493
+ // package-summary pages link their classes as bare files: resolve them
494
+ // against the package directory (String.html -> java/lang/String.html)
495
+ if (opts.resolveFrom)
496
+ href = path.posix.join(opts.resolveFrom, href);
497
+ if (href.startsWith("../"))
498
+ continue; // escaped the root (another module)
499
+ // module-qualified hrefs (java.base/java/lang/String.html) drop the
500
+ // module segment: a package belongs to exactly one module, so the
501
+ // remaining tree paths stay unique
502
+ if (/^[^/]*\.[^/]*\//.test(href))
503
+ href = href.slice(href.indexOf("/") + 1);
504
+ const parts = href.split("/");
505
+ if (parts.length < 2)
506
+ continue; // not inside a package
507
+ if (parts.slice(0, -1).some((seg) => !/^[A-Za-z_$][\w$]*$/.test(seg)))
508
+ continue; // not a package path
509
+ const file = parts[parts.length - 1];
510
+ const first = parts[0];
511
+ // nested classes (AbstractArrow.PickupStatus.html) would collide with
512
+ // their outer class file, so they are excluded unless requested
513
+ if (!opts.includeNested && /\./.test(file.replace(/\.html$/, "")))
514
+ continue;
515
+ if (file.startsWith("package-") || file.startsWith("module-") ||
516
+ file.startsWith("index-") || file.startsWith("allclasses"))
517
+ continue;
518
+ if (first.startsWith("index-files") || first.startsWith("jquery") ||
519
+ first.startsWith("script") || first.startsWith("search"))
520
+ continue;
521
+ if (parts.some((seg) => seg === "doc-files"))
522
+ continue; // package doc resources
523
+ links.add(href);
524
+ }
525
+ return [...links];
526
+ }
527
+ /**
528
+ * Packages listed on a module-summary page (pure), e.g. the Oracle javadocs
529
+ * (docs.oracle.com/.../api/java.base) whose modules have no allclasses page:
530
+ * java/io/package-summary.html, java/lang/package-summary.html, ...
531
+ */
532
+ function extractPackageLinks(html) {
533
+ const pkgs = new Set();
534
+ const re = /<a[^>]+href="([^"]*package-summary\.html)"/g;
535
+ let m;
536
+ while ((m = re.exec(html))) {
537
+ let href = m[1].replace(/^\.\//, "").split("#")[0];
538
+ if (/^[a-z][\w+.-]*:/i.test(href) || href.startsWith("/") || href.startsWith("//"))
539
+ continue;
540
+ href = path.posix.join(href);
541
+ if (href.startsWith("../") || path.posix.dirname(href) === ".")
542
+ continue; // not inside a package
543
+ const pkgParts = href.split("/").slice(0, -1);
544
+ if (pkgParts.some((seg) => !/^[A-Za-z_$][\w$]*$/.test(seg)))
545
+ continue; // module or doc path
546
+ pkgs.add(href);
547
+ }
548
+ return [...pkgs].sort();
549
+ }
550
+ /** True when an href is a nested class page (Outer.Inner.html / .java). */
551
+ function isNestedHref(href) {
552
+ return /\./.test(path.posix.basename(href).replace(/\.[^.]*$/, ""));
553
+ }
554
+ /**
555
+ * Index every class link (nested included) and pick the pages to mirror as
556
+ * files. `limit` caps the mirror and prunes the index so imports never
557
+ * point at pages that were not written.
558
+ */
559
+ function indexLinks(allLinks, limit) {
560
+ const { classIndex, nestedIndex, nestedOwners, nested } = buildClassIndex(allLinks);
561
+ const links = limit ? allLinks.filter((l) => !isNestedHref(l)).slice(0, limit) : allLinks.filter((l) => !isNestedHref(l));
562
+ // with a limit only part of the tree is mirrored: importing classes whose
563
+ // pages were not written would leave the tree with unresolved modules
564
+ if (limit) {
565
+ const written = new Set(links);
566
+ for (const [cls, hrefs] of classIndex) {
567
+ if (!hrefs.some((h) => written.has(h)))
568
+ classIndex.delete(cls);
569
+ }
570
+ for (const [fullName, outer] of nestedIndex) {
571
+ if (!written.has(outer))
572
+ nestedIndex.delete(fullName);
573
+ }
574
+ for (const [cls, hrefs] of nestedOwners) {
575
+ const kept = hrefs.filter((h) => written.has(h));
576
+ if (kept.length)
577
+ nestedOwners.set(cls, kept);
578
+ else
579
+ nestedOwners.delete(cls);
580
+ }
581
+ }
582
+ return { links, classIndex, nestedIndex, nestedOwners, nested };
583
+ }
584
+ /**
585
+ * Bind a reference to the mirrored page declaring it (shared by the javadoc
586
+ * and git mirrors): top-level names resolve to their declaring page, dotted
587
+ * refs (Villager.Profession) to their outer class's page, and a name claimed
588
+ * by several classes binds the one whose package is nearest to this file's.
589
+ */
590
+ function makeResolver(index, href, localNames) {
591
+ return (ref) => {
592
+ if (localNames.has(ref.split(".").pop()))
593
+ return null;
594
+ let target = null;
595
+ if (ref.includes(".")) {
596
+ // nested class reference (Villager.Profession)
597
+ target = index.nestedIndex.get(ref) ?? null;
598
+ }
599
+ else {
600
+ // top-level names win; a bare nested name (ConditionObject,
601
+ // package-private in a sibling class) binds the nested interface
602
+ // exported by its outer class's file
603
+ const owners = index.classIndex.get(ref) ?? index.nestedOwners.get(ref) ?? [];
604
+ // a name claimed by several classes (org.bukkit.block.Jukebox vs
605
+ // org.bukkit.block.data.type.Jukebox) binds the one whose package
606
+ // is nearest to this file's
607
+ const fromDir = path.posix.dirname(href).split("/");
608
+ const dist = (o) => {
609
+ const toDir = path.posix.dirname(o).split("/");
610
+ let i = 0;
611
+ while (i < fromDir.length && i < toDir.length && fromDir[i] === toDir[i])
612
+ i++;
613
+ return (fromDir.length - i) + (toDir.length - i); // hops up + down
614
+ };
615
+ target = owners.length === 1 ? owners[0]
616
+ : [...owners].sort((a, b) => dist(a) - dist(b) || a.localeCompare(b))[0] ?? null;
617
+ }
618
+ if (!target || target === href)
619
+ return null; // unknown or this file
620
+ return importPathFor(path.posix.dirname(href), target);
621
+ };
622
+ }
623
+ /** Every directory of a mirror that needs an index.ts (mirrorDirs of the
624
+ * javadoc roots and the git clone alike). */
625
+ function mirrorDirs(outRoot, targets) {
626
+ const dirs = new Set([outRoot]);
627
+ for (const { rootDir, href } of targets) {
628
+ const treeRoot = rootDir ? path.join(outRoot, rootDir) : outRoot;
629
+ dirs.add(treeRoot);
630
+ let d = path.join(treeRoot, path.dirname(href));
631
+ while (true) {
632
+ dirs.add(d);
633
+ if (d === treeRoot)
634
+ break;
635
+ d = path.dirname(d);
636
+ }
637
+ }
638
+ return dirs;
639
+ }
640
+ /**
641
+ * Write an index (.ts, or .d.ts for a types-only package) for every
642
+ * directory of a mirror: class files re-export directly, subpackages as
643
+ * namespaces — except at a tree root with a single top package, which
644
+ * re-exports it flat so `import * as org from "..."` works.
645
+ */
646
+ function writeMirrorIndexes(dirs, treeRoots, ext) {
647
+ for (const d of dirs) {
648
+ const entries = fs.readdirSync(d, { withFileTypes: true });
649
+ const files = entries
650
+ .filter((e) => e.isFile() && e.name.endsWith(`.${ext}`) && e.name !== `index.${ext}`)
651
+ .map((e) => e.name.slice(0, -(ext.length + 1)))
652
+ .sort();
653
+ const subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
654
+ // star-exporting every class file would clash on same-named nested
655
+ // classes, so names are claimed file by file
656
+ const claimed = new Set();
657
+ const fileExports = files.flatMap((f) => {
658
+ const src = fs.readFileSync(path.join(d, `${f}.${ext}`), "utf8");
659
+ const names = [...src.matchAll(/^export (?:declare )?(?:interface|class|type|function) (\w+)/gm)].map((m) => m[1]);
660
+ const line = fileExportLine(f, names, claimed);
661
+ return line ? [line] : [];
662
+ });
663
+ // the API root (the outDir itself, or each tree root of a multi-root
664
+ // mirror) exposes its single top-level package directly, so
665
+ // `import * as org from ".../spigot-api"` gives org.bukkit.entity.Player;
666
+ // with several top packages (java.base: java + javax) it wraps them,
667
+ // or their same-named subpackages (java.net, javax.net) would collide
668
+ const flat = treeRoots.has(d) && fileExports.length === 0 && subdirs.length === 1;
669
+ fs.writeFileSync(path.join(d, `index.${ext}`), buildIndex(fileExports, subdirs, { flat }), "utf8");
670
+ }
671
+ }
672
+ /** Write the package.json of a types-only mirror (`--package`). */
673
+ function writeMirrorPackage(outDir, packageName, ext) {
674
+ // a mirror of java.base itself provides java.lang.Object at the package
675
+ // root (mirrors import `{ Object } from` the base module): its "types"
676
+ // points at the java.lang re-export index; every other package root
677
+ // exposes the whole mirrored tree
678
+ const types = fs.existsSync(path.join(outDir, "java", "lang", `index.${ext}`))
679
+ ? `java/lang/index.${ext}`
680
+ : `index.${ext}`;
681
+ fs.writeFileSync(path.join(outDir, "package.json"), JSON.stringify({
682
+ name: packageName,
683
+ version: "0.0.0",
684
+ types,
685
+ private: true,
686
+ }, null, 2) + "\n", "utf8");
687
+ writeBrewJson(outDir, packageName, ext);
688
+ }
689
+ /**
690
+ * Write brew.json next to package.json: a map from every Java package this
691
+ * mirror covers to the npm package name, so other mirrors can link their
692
+ * external references into this package (see loadBrewMap/resolveExternalRef):
693
+ *
694
+ * { "java": { "org.bukkit": "@brew-types/java__org.spigotmc__spigot-api" } }
695
+ *
696
+ * Package names follow the @brew-types/<languageFileExtension>__<groupId>__<artifactId>
697
+ * standard (see brewPackageName), e.g. @brew-types/java__java.base for the
698
+ * java.base module (java.lang) and @brew-types/java__org.spigotmc__spigot-api
699
+ * for org.spigotmc:spigot-api (org.bukkit).
700
+ */
701
+ function writeBrewJson(outDir, packageName, ext) {
702
+ const java = {};
703
+ const walk = (dir) => {
704
+ let entries;
705
+ try {
706
+ entries = fs.readdirSync(dir, { withFileTypes: true });
707
+ }
708
+ catch {
709
+ return;
710
+ }
711
+ const hasClasses = entries.some((e) => e.isFile() && e.name.endsWith(`.${ext}`) && e.name !== `index.${ext}`);
712
+ if (hasClasses && dir !== outDir) {
713
+ java[path.relative(outDir, dir).split(path.sep).join(".")] = packageName;
714
+ }
715
+ for (const e of entries) {
716
+ if (e.isDirectory() && !e.name.startsWith("."))
717
+ walk(path.join(dir, e.name));
718
+ }
719
+ };
720
+ walk(outDir);
721
+ fs.writeFileSync(path.join(outDir, "brew.json"), JSON.stringify({ java }, null, 2) + "\n", "utf8");
722
+ }
723
+ /**
724
+ * Load the union of the brew.json maps found in the given directories (each
725
+ * dir's own brew.json plus one level of subdirectories, so both
726
+ * node_modules/@brew-types and a packages/ dir work): java package prefix
727
+ * -> npm package name.
728
+ */
729
+ function loadBrewMap(searchDirs) {
730
+ const map = new Map();
731
+ const read = (file) => {
732
+ try {
733
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
734
+ if (parsed?.java && typeof parsed.java === "object") {
735
+ for (const [prefix, pkg] of Object.entries(parsed.java)) {
736
+ if (typeof pkg === "string" && prefix)
737
+ map.set(prefix, pkg);
738
+ }
739
+ }
740
+ }
741
+ catch {
742
+ // not a brew package (no brew.json / unreadable) — skip
743
+ }
744
+ };
745
+ for (const dir of searchDirs) {
746
+ read(path.join(dir, "brew.json"));
747
+ let entries;
748
+ try {
749
+ entries = fs.readdirSync(dir, { withFileTypes: true });
750
+ }
751
+ catch {
752
+ continue;
753
+ }
754
+ for (const e of entries) {
755
+ if (e.isDirectory())
756
+ read(path.join(dir, e.name, "brew.json"));
757
+ }
758
+ }
759
+ return map;
760
+ }
761
+ /**
762
+ * Resolve a reference the mirrored tree does not declare to an import path
763
+ * in the brew package providing it (pure): with brew map
764
+ * { "org.bukkit": "@brew-types/java__org.spigotmc__spigot-api" }, the
765
+ * qualified name "org.bukkit.World" resolves to
766
+ * "@brew-types/java__org.spigotmc__spigot-api/org/bukkit/World". Nested
767
+ * classes (Outer.Inner) import from their outer class's file. The longest
768
+ * matching package prefix wins.
769
+ */
770
+ function resolveExternalRef(ref, quals, brewMap) {
771
+ const prefixes = [...brewMap.keys()].filter(Boolean).sort((a, b) => b.length - a.length);
772
+ if (!prefixes.length)
773
+ return null;
774
+ for (const q of quals) {
775
+ const segs = q.split(".");
776
+ // package segments start lowercase, class segments uppercase
777
+ let i = 0;
778
+ while (i < segs.length - 1 && /^[a-z_$]/.test(segs[i]))
779
+ i++;
780
+ const pkg = segs.slice(0, i).join(".");
781
+ const cls = segs.slice(i);
782
+ if (!cls.length || !/^[A-Z]/.test(cls[0]))
783
+ continue;
784
+ for (const prefix of prefixes) {
785
+ if (pkg === prefix || pkg.startsWith(prefix + ".")) {
786
+ return `${brewMap.get(prefix)}/${[...segs.slice(0, i), cls[0]].join("/")}`;
787
+ }
788
+ }
789
+ }
790
+ return null;
791
+ }
792
+ /**
793
+ * Map every class of a javadoc tree to the page that declares it (pure,
794
+ * testable). Nested classes live in their outer class's page; they do not
795
+ * claim their simple name, which would shadow same-named top-level classes
796
+ * (TropicalFish.Pattern vs org.bukkit.block.banner.Pattern). Names claimed
797
+ * by more than one page are left out (an ambiguous import would silently
798
+ * bind the wrong class).
799
+ */
800
+ function buildClassIndex(links) {
801
+ const owners = new Map();
802
+ const claim = (cls, href) => {
803
+ if (!owners.has(cls))
804
+ owners.set(cls, new Set());
805
+ owners.get(cls).add(href);
806
+ };
807
+ const nested = new Map();
808
+ const nestedIndex = new Map();
809
+ const nestedOwners = new Map();
810
+ for (const href of links) {
811
+ const dir = path.posix.dirname(href);
812
+ // strip any extension (.html javadoc pages, .java source files alike)
813
+ const ext = path.posix.extname(href);
814
+ const base = decodeURIComponent(path.posix.basename(href).replace(/\.[^.]*$/, ""));
815
+ if (isNestedHref(href)) {
816
+ const outer = path.posix.join(dir, base.slice(0, base.indexOf(".")) + ext);
817
+ if (!nested.has(outer))
818
+ nested.set(outer, []);
819
+ nested.get(outer).push(href);
820
+ nestedIndex.set(base, outer);
821
+ // simple names of nested classes claim nothing (they would shadow
822
+ // top-level classes) but remember their pages for bare references
823
+ const simple = base.slice(base.lastIndexOf(".") + 1);
824
+ if (!nestedOwners.has(simple))
825
+ nestedOwners.set(simple, new Set());
826
+ nestedOwners.get(simple).add(outer);
827
+ }
828
+ else {
829
+ claim(base, href);
830
+ }
831
+ }
832
+ const classIndex = new Map();
833
+ for (const [cls, hrefs] of owners)
834
+ classIndex.set(cls, [...hrefs]);
835
+ const nestedOwnerList = new Map();
836
+ for (const [cls, hrefs] of nestedOwners)
837
+ nestedOwnerList.set(cls, [...hrefs]);
838
+ return { classIndex, nestedIndex, nestedOwners: nestedOwnerList, nested };
839
+ }
840
+ /** Module path importing toHref's class from a file living in fromDir (pure).
841
+ * Works for any href extension (.html javadoc pages, .java source files). */
842
+ function importPathFor(fromDir, toHref) {
843
+ let rel = path.posix.relative(fromDir, path.posix.dirname(toHref)) || ".";
844
+ if (!rel.startsWith("."))
845
+ rel = "./" + rel; // keep the specifier relative
846
+ const base = decodeURIComponent(path.posix.basename(toHref)).replace(/\.[^.]*$/, "");
847
+ return `${rel}/${base}`;
848
+ }
849
+ /** Build the `import type` block for referenced classes (pure). */
850
+ function importBlockFor(refs, resolveImport) {
851
+ const chosen = new Map();
852
+ // dotted refs (Villager.Profession) are more specific than plain names:
853
+ // when both bind the same import name, the dotted one wins
854
+ const sorted = [...new Set(refs)].sort((a, b) => b.split(".").length - a.split(".").length || a.localeCompare(b));
855
+ for (const ref of sorted) {
856
+ const mod = resolveImport(ref);
857
+ if (mod === null)
858
+ continue;
859
+ const name = ref.split(".").pop();
860
+ if (!chosen.has(name))
861
+ chosen.set(name, { name, mod });
862
+ }
863
+ return [...chosen.values()].sort((a, b) => a.name.localeCompare(b.name))
864
+ .map((x) => `import type { ${x.name} } from "${x.mod}";`)
865
+ .join("\n");
866
+ }
867
+ /** Insert an import block after the generated header comments (pure). */
868
+ function withImports(ts, importBlock) {
869
+ if (!importBlock)
870
+ return ts;
871
+ const secondNl = ts.indexOf("\n", ts.indexOf("\n") + 1) + 1;
872
+ return ts.slice(0, secondNl) + importBlock + "\n\n" + ts.slice(secondNl).replace(/^\n/, "");
873
+ }
874
+ /** index.ts content for one directory: re-export files and subdirectories. */
875
+ /**
876
+ * Re-export statement for one class file (pure), or null when every name it
877
+ * declares was already claimed by an earlier sibling: two nested classes of
878
+ * the same name in one directory (Crafter.Orientation, Jigsaw.Orientation)
879
+ * cannot both be star-exported from its index.ts.
880
+ */
881
+ function fileExportLine(file, names, claimed) {
882
+ const fresh = names.filter((n) => !claimed.has(n));
883
+ for (const n of fresh)
884
+ claimed.add(n);
885
+ if (!fresh.length)
886
+ return null;
887
+ return fresh.length === names.length
888
+ ? `export * from "./${file}";`
889
+ : `export { ${fresh.join(", ")} } from "./${file}";`;
890
+ }
891
+ /**
892
+ * index.ts content for one directory (pure). Class files re-export directly;
893
+ * subpackages re-export as namespaces named after their directory, so a tree
894
+ * mirrors Java paths: org/bukkit/index.ts gives org.bukkit.entity.Player.
895
+ * `opts.flat` re-exports subdirectories directly instead — used for the API
896
+ * root, so `import * as org from ".../spigot-api"` binds its top packages.
897
+ */
898
+ function buildIndex(fileExports, subdirs, opts = {}) {
899
+ const lines = [
900
+ ...fileExports,
901
+ ...subdirs.map((d) => opts.flat ? `export * from "./${d}";` : `export * as ${d} from "./${d}";`),
902
+ ];
903
+ return lines.length ? lines.join("\n") + "\n" : "";
904
+ }
905
+ /** Names of the interfaces a generated file declares (its class + nested). */
906
+ function declaredNames(ts) {
907
+ return new Set(ts.match(/^export (?:declare )?(?:interface|class|type|function) ([A-Za-z_$][\w$]*)/gm)
908
+ ?.map((m) => m.replace(/^export (?:declare )?(?:interface|class|type|function) /, "")) ?? []);
909
+ }
910
+ /** Swap the import block of an already-generated file (pure). */
911
+ function replaceImports(ts, importBlock) {
912
+ const lines = ts.split("\n");
913
+ let i = 2; // past the two header comment lines
914
+ while (i < lines.length && lines[i].startsWith("import type "))
915
+ i++;
916
+ while (i < lines.length && lines[i] === "")
917
+ i++;
918
+ return lines.slice(0, 2).join("\n") + "\n" + (importBlock ? importBlock + "\n\n" : "") + lines.slice(i).join("\n");
919
+ }
920
+ /**
921
+ * @brew-types package names are `<languageFileExtension>__<groupId>__<artifactId>`,
922
+ * double-underscore delimited — e.g. @brew-types/java__org.spigotmc__spigot-api —
923
+ * so snakecase groupIds/artifactIds of future languages stay readable and the
924
+ * name parses unambiguously. JDK modules put the module name in place of
925
+ * groupId/artifactId: @brew-types/java__java.base.
926
+ */
927
+ function brewPackageName(language, ...rest) {
928
+ return `@brew-types/${[language, ...rest].join("__")}`;
929
+ }
930
+ /** Inverse of brewPackageName: split on the double-underscore delimiter.
931
+ * Two components => <language>__<module> (JDK module), three =>
932
+ * <language>__<groupId>__<artifactId> (Maven coordinates). */
933
+ function parseBrewPackageName(name) {
934
+ if (!name.startsWith("@brew-types/"))
935
+ return null;
936
+ const parts = name.slice("@brew-types/".length).split("__");
937
+ if (parts.some((p) => !p))
938
+ return null;
939
+ if (parts.length === 2)
940
+ return { language: parts[0], module: parts[1] };
941
+ if (parts.length === 3)
942
+ return { language: parts[0], groupId: parts[1], artifactId: parts[2] };
943
+ return null;
944
+ }
945
+ /**
946
+ * The base of every generated interface: java.lang.Object, provided by the
947
+ * @brew-types language packs — @brew-types/java__java.base: java.lang is a
948
+ * package inside the java.base module. Extending it gives every mirrored
949
+ * class Object's methods (toString, equals, hashCode, ...).
950
+ */
951
+ exports.BASE_TYPE = { name: "Object", module: brewPackageName("java", "java.base") };
952
+ exports.BASE_IMPORT = `import type { ${exports.BASE_TYPE.name} } from "${exports.BASE_TYPE.module}";`;
953
+ /**
954
+ * The base-type import line for one mirrored file. A mirror that declares
955
+ * java.lang.Object itself (a java.base mirror) imports it relatively from
956
+ * its own page — a package must resolve from within itself, and the
957
+ * @brew-types/java__java.base specifier only resolves through node_modules —
958
+ * and the file declaring Object skips the import (it would clash with the
959
+ * file's own export). Every other mirror imports Object from the base
960
+ * package by name, as does a java.base mirror whose Object page was not
961
+ * written (e.g. pruned by --limit). A same-named class outside java/lang
962
+ * does not count as the base type.
963
+ */
964
+ function baseImportFor(index, href, localNames) {
965
+ if (localNames.has(exports.BASE_TYPE.name))
966
+ return null;
967
+ const basePage = (index.classIndex.get(exports.BASE_TYPE.name) ?? []).find((h) => path.posix.dirname(h) === "java/lang" &&
968
+ path.posix.basename(h).replace(/\.[^.]*$/, "") === exports.BASE_TYPE.name);
969
+ if (!basePage)
970
+ return exports.BASE_IMPORT;
971
+ if (basePage === href)
972
+ return null;
973
+ return `import type { ${exports.BASE_TYPE.name} } from "${importPathFor(path.posix.dirname(href), basePage)}";`;
974
+ }
975
+ const CLASS_LIST_FILES = [
976
+ "allclasses-index.html", // modern javadoc
977
+ "allclasses.html", // single-source javadoc
978
+ "allclasses-noframes.html", // pre-javadoc-10 frames layout
979
+ ];
980
+ /**
981
+ * Read a javadoc root's class list (nested classes included); null when not a
982
+ * root. Redirects count as missing: the Oracle docs redirect their absent
983
+ * allclasses pages to the documentation homepage, whose links would mirror
984
+ * garbage (a tree of interfaces named after the homepage title).
985
+ */
986
+ async function readClassList(rootUrl) {
987
+ for (const file of CLASS_LIST_FILES) {
988
+ const res = await fetch(rootUrl + file);
989
+ if (res.ok && !res.redirected) {
990
+ const links = extractClassLinks(await res.text(), { includeNested: true });
991
+ return links.length ? links : null;
992
+ }
993
+ }
994
+ return null;
995
+ }
996
+ /**
997
+ * Read a module javadoc's class list by crawling its package pages; null
998
+ * when the URL is not a module root. Oracle's per-module javadocs (java.base)
999
+ * have no allclasses page: module-summary.html links every package summary,
1000
+ * and those link their class pages.
1001
+ */
1002
+ async function readModuleClassList(rootUrl) {
1003
+ const res = await fetch(rootUrl + "module-summary.html");
1004
+ if (!res.ok || res.redirected)
1005
+ return null;
1006
+ const pkgs = extractPackageLinks(await res.text());
1007
+ if (!pkgs.length)
1008
+ return null;
1009
+ const links = new Set();
1010
+ for (const pkg of pkgs) {
1011
+ const page = await fetch(rootUrl + pkg);
1012
+ if (!page.ok || page.redirected)
1013
+ continue;
1014
+ for (const link of extractClassLinks(await page.text(), {
1015
+ includeNested: true,
1016
+ resolveFrom: path.posix.dirname(pkg),
1017
+ })) {
1018
+ links.add(link);
1019
+ }
1020
+ }
1021
+ return links.size ? [...links] : null;
1022
+ }
1023
+ /**
1024
+ * Extract javadoc roots linked from a landing page (pure, testable).
1025
+ * Some hubs are not a javadoc root themselves: the SpigotMC hub
1026
+ * (hub.spigotmc.org/javadocs) links each API's javadoc as
1027
+ * <root>/index.html?overview-summary.html (bukkit/, spigot/).
1028
+ */
1029
+ function discoverJavadocRoots(html, base) {
1030
+ const roots = new Set();
1031
+ const re = /<a[^>]+href="([^"]+)"/g;
1032
+ let m;
1033
+ while ((m = re.exec(html))) {
1034
+ const href = m[1].split("#")[0];
1035
+ // javadoc entry pages: <root>/index.html[?overview-summary.html]
1036
+ const entry = /^([^?#]*\/)?index\.html(?:\?overview-summary\.html)?$/.exec(href);
1037
+ if (!entry)
1038
+ continue;
1039
+ let url;
1040
+ try {
1041
+ url = new URL((entry[1] ?? "") + "index.html", base);
1042
+ }
1043
+ catch {
1044
+ continue;
1045
+ }
1046
+ const root = url.href.slice(0, -"index.html".length);
1047
+ if (!root.startsWith(base) || root === base)
1048
+ continue;
1049
+ roots.add(root);
1050
+ }
1051
+ return [...roots].sort();
1052
+ }
1053
+ /**
1054
+ * Walk the entire javadoc of a Bukkit/Spigot API: read the class list, fetch
1055
+ * every class page, and mirror the package tree as .ts interfaces on disk,
1056
+ * with an index.ts re-exporting each directory level.
1057
+ *
1058
+ * A base URL that is not a javadoc root itself works too when it links the
1059
+ * javadoc roots (the SpigotMC hub); each discovered root is mirrored under
1060
+ * its own subdirectory of outDir so same-named packages cannot collide.
1061
+ */
1062
+ async function fetchAllApiDocs(baseUrl, outDir, opts = {}) {
1063
+ if (typeof fetch === "undefined") {
1064
+ throw new Error("brewc fetch requires Node.js 18 or newer (global fetch)");
1065
+ }
1066
+ const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
1067
+ const makeRoot = (url, dir, allLinks, limit) => {
1068
+ const { links, classIndex, nestedIndex, nestedOwners, nested } = indexLinks(allLinks, limit);
1069
+ return { url, dir, links, classIndex, nestedIndex, nestedOwners, nested };
1070
+ };
1071
+ const roots = [];
1072
+ const rootLinks = await readClassList(base);
1073
+ const moduleLinks = rootLinks ? null : await readModuleClassList(base);
1074
+ if (rootLinks) {
1075
+ roots.push(makeRoot(base, "", rootLinks, opts.limit));
1076
+ }
1077
+ else if (moduleLinks) {
1078
+ if (opts.log)
1079
+ opts.log(`no class list at ${base}; crawling the module's packages`);
1080
+ roots.push(makeRoot(base, "", moduleLinks, opts.limit));
1081
+ }
1082
+ else {
1083
+ let status = 404;
1084
+ try {
1085
+ const page = await fetch(base);
1086
+ status = page.status;
1087
+ if (page.ok) {
1088
+ for (const rootUrl of discoverJavadocRoots(await page.text(), base)) {
1089
+ const links = await readClassList(rootUrl);
1090
+ if (links) {
1091
+ const name = new URL(rootUrl).pathname.replace(/\/+$/, "").split("/").pop() ?? "api";
1092
+ roots.push(makeRoot(rootUrl, name, links, opts.limit));
1093
+ }
1094
+ }
1095
+ }
1096
+ }
1097
+ catch {
1098
+ // fall through: report the class-list error below
1099
+ }
1100
+ if (!roots.length) {
1101
+ throw new Error(`cannot read the class list (${CLASS_LIST_FILES.join(" / ")}) at ${base}: ${status}`);
1102
+ }
1103
+ if (opts.log) {
1104
+ const names = roots.map((r) => new URL(r.url).pathname.replace(/\/+$/, "").split("/").pop());
1105
+ opts.log(`no javadoc at ${base}; mirroring ${[...new Set(names)].join(", ")}`);
1106
+ }
1107
+ if (roots.length === 1)
1108
+ roots[0].dir = ""; // single root: mirror directly in outDir
1109
+ }
1110
+ const errors = [];
1111
+ let done = 0;
1112
+ const targets = roots.flatMap((root) => root.links.map((href) => ({ root, href })));
1113
+ const queue = [...targets];
1114
+ const total = targets.length;
1115
+ /** Bind a reference to the mirrored page declaring it (shared by the
1116
+ * generation pass and the import reconciliation below). */
1117
+ const resolveFor = (root, href, localNames) => makeResolver(root, href, localNames);
1118
+ const generated = [];
1119
+ const workers = Array.from({ length: opts.concurrency ?? 8 }, async () => {
1120
+ for (let item = queue.shift(); item; item = queue.shift()) {
1121
+ const { root, href } = item;
1122
+ try {
1123
+ const page = await fetch(root.url + href);
1124
+ if (!page.ok || page.redirected) {
1125
+ errors.push(`${href}: ${page.status}`);
1126
+ }
1127
+ else {
1128
+ const pageHtml = await page.text();
1129
+ // package-private nested classes (AbstractQueuedSynchronizer.
1130
+ // ConditionObject) are absent from every class list: discover them
1131
+ // from their outer class's own page and register them for the
1132
+ // import reconciliation below
1133
+ const ownName = decodeURIComponent(path.posix.basename(href, ".html"));
1134
+ const dirPosix = path.posix.dirname(href);
1135
+ const nestedHrefs = [...(root.nested.get(href) ?? [])];
1136
+ for (const found of extractClassLinks(pageHtml, { includeNested: true, resolveFrom: dirPosix })) {
1137
+ if (!isNestedHref(found) || nestedHrefs.includes(found))
1138
+ continue;
1139
+ const base = decodeURIComponent(path.posix.basename(found, ".html"));
1140
+ if (base.slice(0, base.indexOf(".")) !== ownName)
1141
+ continue;
1142
+ nestedHrefs.push(found);
1143
+ root.nestedIndex.set(base, href);
1144
+ const simple = base.slice(base.lastIndexOf(".") + 1);
1145
+ const owners = root.nestedOwners.get(simple) ?? [];
1146
+ if (!owners.includes(href))
1147
+ root.nestedOwners.set(simple, [...owners, href]);
1148
+ }
1149
+ // cross-references: import the mirrored declaration of every
1150
+ // referenced class of this javadoc tree
1151
+ // a class the tree declares keeps its name (JDK externals like
1152
+ // File or Random bind to their mirrored interfaces); unknown ones
1153
+ // still fall back to plain TS types
1154
+ const isTreeClass = (cls) => root.classIndex.has(cls) || root.nestedIndex.has(cls);
1155
+ // nested classes (Outer.Inner) are appended to their outer class's
1156
+ // file — as interfaces they cannot form files of their own — so the
1157
+ // import block covers the union of all their references too
1158
+ const outer = apiDocsToInterface(pageHtml, root.url + href, { isTreeClass });
1159
+ const allRefs = [...outer.refs];
1160
+ const parts = [outer.ts];
1161
+ for (const nestedHref of nestedHrefs) {
1162
+ try {
1163
+ const nestedPage = await fetch(root.url + nestedHref);
1164
+ if (!nestedPage.ok || nestedPage.redirected) {
1165
+ errors.push(`${nestedHref}: ${nestedPage.status}`);
1166
+ continue;
1167
+ }
1168
+ const nested = apiDocsToInterface(await nestedPage.text(), root.url + nestedHref, { isTreeClass });
1169
+ allRefs.push(...nested.refs);
1170
+ parts.push(nested.ts);
1171
+ }
1172
+ catch (e) {
1173
+ errors.push(`${nestedHref}: ${e.message}`);
1174
+ }
1175
+ }
1176
+ // a mirror declaring the base type itself imports it relatively
1177
+ // (baseImportFor); the file declaring Object skips the import
1178
+ const localNames = declaredNames(parts.join("\n"));
1179
+ const block = [
1180
+ baseImportFor(root, href, localNames),
1181
+ importBlockFor(allRefs, resolveFor(root, href, localNames)),
1182
+ ].filter(Boolean).join("\n");
1183
+ const full = withImports(parts.join("\n"), block);
1184
+ const dir = path.join(outDir, root.dir, path.dirname(href));
1185
+ fs.mkdirSync(dir, { recursive: true });
1186
+ // with --package the mirror is a types-only package: .d.ts files
1187
+ // referenced by "types": "index.d.ts"
1188
+ const file = `${outer.name}.${opts.packageName ? "d.ts" : "ts"}`;
1189
+ fs.writeFileSync(path.join(dir, file), full, "utf8");
1190
+ generated.push({ file: path.join(dir, file), root, href, refs: allRefs });
1191
+ }
1192
+ }
1193
+ catch (e) {
1194
+ errors.push(`${href}: ${e.message}`);
1195
+ }
1196
+ done++;
1197
+ if (opts.log && (done % 25 === 0 || done === total)) {
1198
+ opts.log(`fetched ${done}/${total} classes`);
1199
+ }
1200
+ }
1201
+ });
1202
+ await Promise.all(workers);
1203
+ // nested classes discovered mid-run (ConditionObject, found on its outer
1204
+ // class's page) can satisfy imports of files mirrored earlier: recompute
1205
+ // every import block against the now-complete index
1206
+ for (const g of generated) {
1207
+ const src = fs.readFileSync(g.file, "utf8");
1208
+ const local = declaredNames(src);
1209
+ const resolved = importBlockFor(g.refs, resolveFor(g.root, g.href, local));
1210
+ const block = [baseImportFor(g.root, g.href, local), resolved]
1211
+ .filter(Boolean).join("\n");
1212
+ const rewritten = replaceImports(src, block);
1213
+ if (rewritten !== src)
1214
+ fs.writeFileSync(g.file, rewritten, "utf8");
1215
+ }
1216
+ // index for every directory, bottom-up so parents list their children
1217
+ // (.ts files, or .d.ts when the mirror is a types-only package)
1218
+ const ext = opts.packageName ? "d.ts" : "ts";
1219
+ const outRoot = path.resolve(outDir);
1220
+ const treeRoots = new Set([outRoot, ...roots.map((r) => path.join(outRoot, r.dir))]);
1221
+ const dirs = mirrorDirs(outRoot, targets.map(({ root, href }) => ({ rootDir: root.dir, href })));
1222
+ writeMirrorIndexes(dirs, treeRoots, ext);
1223
+ if (opts.packageName)
1224
+ writeMirrorPackage(outDir, opts.packageName, ext);
1225
+ return { classes: total - errors.length, errors: [...new Set(errors)] };
1226
+ }
1227
+ /** Fetch a javadoc page and generate its TypeScript interface. */
1228
+ async function fetchApiDocs(url) {
1229
+ if (typeof fetch === "undefined") {
1230
+ throw new Error("brewc fetch requires Node.js 18 or newer (global fetch)");
1231
+ }
1232
+ const res = await fetch(url);
1233
+ if (!res.ok)
1234
+ throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
1235
+ const { name, ts } = apiDocsToInterface(await res.text(), url);
1236
+ return { name, ts: name === exports.BASE_TYPE.name ? ts : withImports(ts, exports.BASE_IMPORT) };
1237
+ }
1238
+ /** Write a fetch result, used by the CLI. */
1239
+ function writeFetched(result, output) {
1240
+ const file = output ?? `${result.name}.ts`;
1241
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1242
+ fs.writeFileSync(file, result.ts, "utf8");
1243
+ return file;
1244
+ }
1245
+ //# sourceMappingURL=fetch.js.map