@brew-lang/sdk 0.0.0-stage → 0.3.0-alpha

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