@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/LICENSE +373 -0
- package/README.md +348 -2
- package/build/cli.d.ts +2 -0
- package/build/cli.js +256 -0
- package/build/cli.js.map +1 -0
- package/build/fetch.d.ts +294 -0
- package/build/fetch.js +1221 -0
- package/build/fetch.js.map +1 -0
- package/build/fetchgit.d.ts +67 -0
- package/build/fetchgit.js +612 -0
- package/build/fetchgit.js.map +1 -0
- package/build/transpiler.d.ts +13 -0
- package/build/transpiler.js +2025 -0
- package/build/transpiler.js.map +1 -0
- package/package.json +38 -4
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(/ /g, " ")
|
|
190
|
+
.replace(/</g, "<")
|
|
191
|
+
.replace(/>/g, ">")
|
|
192
|
+
.replace(/&/g, "&")
|
|
193
|
+
.replace(/"/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
|