minecraft-dev-starter 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Gradle 构建脚本的静态解析。
3
+ *
4
+ * 只做"读得出事实"的解析,不求完整:
5
+ * - 注释/字符串安全的块提取(`runs { ... }` 的花括号匹配不能数错)
6
+ * - 插件 id、settings 的 include、run 配置、Java 等级、MC 版本字面量
7
+ *
8
+ * 识别引擎不执行 Gradle:本机实测 `gradlew tasks` 要 6 分半且可能失败,
9
+ * 见 doc/调研-DSH插件机制.md §8.1。
10
+ * @module minecraft-dev-starter/lib/gradle
11
+ */
12
+
13
+ /**
14
+ * 去掉 Groovy/Kotlin 注释,字符串内容原样保留(替换成等长空白,索引不变)。
15
+ * @param {string} text
16
+ * @returns {string}
17
+ */
18
+ export function stripComments(text) {
19
+ let out = ''
20
+ let quote
21
+ for (let i = 0; i < text.length; i += 1) {
22
+ const ch = text[i]
23
+ const next = text[i + 1]
24
+
25
+ if (quote !== undefined) {
26
+ out += ch
27
+ if (ch === '\\' && quote !== "'") {
28
+ out += next ?? ''
29
+ i += 1
30
+ continue
31
+ }
32
+ if (ch === quote) quote = undefined
33
+ continue
34
+ }
35
+
36
+ if (ch === '"' || ch === "'") {
37
+ quote = ch
38
+ out += ch
39
+ continue
40
+ }
41
+
42
+ if (ch === '/' && next === '/') {
43
+ while (i < text.length && text[i] !== '\n') {
44
+ out += ' '
45
+ i += 1
46
+ }
47
+ out += '\n'
48
+ continue
49
+ }
50
+
51
+ if (ch === '/' && next === '*') {
52
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) {
53
+ out += text[i] === '\n' ? '\n' : ' '
54
+ i += 1
55
+ }
56
+ out += ' '
57
+ i += 1
58
+ continue
59
+ }
60
+
61
+ out += ch
62
+ }
63
+ return out
64
+ }
65
+
66
+ /**
67
+ * 提取 `name { ... }` 的块内容(花括号配平,已去注释)。
68
+ * @param {string} text 已去注释的脚本
69
+ * @param {string} name 块名,例如 `runs` / `loom`
70
+ * @returns {string[]}
71
+ */
72
+ export function findBlocks(text, name) {
73
+ /** @type {string[]} */
74
+ const bodies = []
75
+ const pattern = new RegExp(`(^|[^\\w.])${name}\\s*\\{`, 'g')
76
+ let match
77
+ while ((match = pattern.exec(text)) !== null) {
78
+ const open = text.indexOf('{', match.index + match[0].length - 1)
79
+ if (open < 0) break
80
+ const close = matchBrace(text, open)
81
+ if (close < 0) break
82
+ bodies.push(text.slice(open + 1, close))
83
+ pattern.lastIndex = close + 1
84
+ }
85
+ return bodies
86
+ }
87
+
88
+ /**
89
+ * 从 `{` 的位置找配对的 `}`。
90
+ * @param {string} text
91
+ * @param {number} open
92
+ * @returns {number} 未配对时 -1
93
+ */
94
+ function matchBrace(text, open) {
95
+ let depth = 0
96
+ for (let i = open; i < text.length; i += 1) {
97
+ if (text[i] === '{') depth += 1
98
+ else if (text[i] === '}') {
99
+ depth -= 1
100
+ if (depth === 0) return i
101
+ }
102
+ }
103
+ return -1
104
+ }
105
+
106
+ /**
107
+ * 收集插件 id:`id 'x'` / `id("x")` / `apply plugin: 'x'`。
108
+ * @param {string} raw 原始脚本
109
+ * @returns {string[]}
110
+ */
111
+ export function parsePluginIds(raw) {
112
+ const text = stripComments(raw)
113
+ /** @type {Set<string>} */
114
+ const ids = new Set()
115
+ for (const match of text.matchAll(/\bid\s*\(?\s*["']([\w.\-]+)["']/g)) ids.add(match[1])
116
+ for (const match of text.matchAll(/apply\s+plugin\s*:\s*["']([\w.\-]+)["']/g)) ids.add(match[1])
117
+ for (const match of text.matchAll(/kotlin\(\s*["']([\w.\-]+)["']\s*\)/g)) ids.add(match[1])
118
+ return [...ids]
119
+ }
120
+
121
+ /**
122
+ * settings.gradle(.kts) 里的 include 列表。
123
+ * @param {string} raw
124
+ * @returns {string[]} 形如 `fabric`、`common`(去掉前导冒号)
125
+ */
126
+ export function parseSettingsIncludes(raw) {
127
+ const text = stripComments(raw)
128
+ /** @type {string[]} */
129
+ const modules = []
130
+ for (const match of text.matchAll(/include\s*\(([^)]*)\)/g)) {
131
+ pushIncludes(modules, match[1])
132
+ }
133
+ for (const match of text.matchAll(/include\s+((?:["'][^"']+["']\s*,?\s*)+)/g)) {
134
+ pushIncludes(modules, match[1])
135
+ }
136
+ return [...new Set(modules)]
137
+ }
138
+
139
+ /**
140
+ * @param {string[]} modules
141
+ * @param {string} raw
142
+ */
143
+ function pushIncludes(modules, raw) {
144
+ for (const match of raw.matchAll(/["']([^"']+)["']/g)) {
145
+ const name = match[1].trim().replace(/^:/, '')
146
+ if (name !== '') modules.push(name)
147
+ }
148
+ }
149
+
150
+ /**
151
+ * @typedef {object} RunTarget
152
+ * @property {string} block runs 块名,如 `client`
153
+ * @property {string} kind 块里调用的类型,如 `client`
154
+ * @property {string} task Gradle 任务名,如 `runClient`
155
+ * @property {string} label 界面上显示的名字,如 `Client`
156
+ */
157
+
158
+ const LABELS = {
159
+ client: 'Client',
160
+ server: 'Server',
161
+ data: 'Data',
162
+ gameTestServer: 'GameTestServer',
163
+ gameTest: 'GameTestServer',
164
+ datagen: 'Data',
165
+ }
166
+
167
+ /**
168
+ * 解析 `runs { client { client() } ... }`,得到可运行目标。
169
+ * 任务名规则:`runs` 块名首字母大写后拼到 `run` 后面(Gradle 的 RunConfig 约定)。
170
+ * @param {string} raw
171
+ * @returns {RunTarget[]}
172
+ */
173
+ export function parseRuns(raw) {
174
+ const text = stripComments(raw)
175
+ /** @type {RunTarget[]} */
176
+ const targets = []
177
+ for (const body of findBlocks(text, 'runs')) {
178
+ for (const match of body.matchAll(/(^|[^\w.])("?)([A-Za-z_][\w]*)\2\s*\{/g)) {
179
+ const name = match[3]
180
+ const open = body.indexOf('{', match.index + match[0].length - 1)
181
+ if (open < 0) continue
182
+ const close = matchBrace(body, open)
183
+ if (close < 0) continue
184
+ const inner = body.slice(open + 1, close)
185
+ const call = /(^|[^\w.])([A-Za-z_][\w]*)\s*\(/.exec(inner)
186
+ const kind = call?.[2] ?? name
187
+ targets.push({
188
+ block: name,
189
+ kind,
190
+ task: `run${name[0].toUpperCase()}${name.slice(1)}`,
191
+ label: LABELS[kind] ?? LABELS[name] ?? `${kind}`,
192
+ })
193
+ }
194
+ }
195
+ // 同名任务去重(多模块时由调用方按模块分组)
196
+ const seen = new Set()
197
+ return targets.filter((target) => {
198
+ if (seen.has(target.task)) return false
199
+ seen.add(target.task)
200
+ return true
201
+ })
202
+ }
203
+
204
+ /**
205
+ * Java toolchain / 兼容等级。
206
+ * @param {string} raw
207
+ * @returns {number | undefined}
208
+ */
209
+ export function parseJavaLevel(raw) {
210
+ const text = stripComments(raw)
211
+ const patterns = [
212
+ /JavaLanguageVersion\.of\(\s*(\d+)\s*\)/,
213
+ /jvmToolchain\(\s*(\d+)\s*\)/,
214
+ /sourceCompatibility\s*=\s*(?:JavaVersion\.VERSION_)?(\d+)/,
215
+ /targetCompatibility\s*=\s*(?:JavaVersion\.VERSION_)?(\d+)/,
216
+ /languageVersion\s*=\s*JavaLanguageVersion\.of\(\s*(\d+)\s*\)/,
217
+ ]
218
+ for (const pattern of patterns) {
219
+ const match = pattern.exec(text)
220
+ if (match) return Number(match[1])
221
+ }
222
+ return undefined
223
+ }
224
+
225
+ /**
226
+ * 从构建脚本里找 MC 版本字面量。
227
+ * @param {string} raw
228
+ * @param {Record<string, string>} properties
229
+ * @returns {string | undefined}
230
+ */
231
+ export function parseMinecraftVersion(raw, properties) {
232
+ const text = stripComments(raw)
233
+
234
+ // loom: minecraft "com.mojang:minecraft:1.20.1"
235
+ const dependency = /minecraft\s*\(?\s*["'][^"']*minecraft:(\d+\.\d+(?:\.\d+)?)["']/.exec(text)
236
+ if (dependency) return dependency[1]
237
+
238
+ // forge: minecraft { version = "1.20.1-47.2.0" }
239
+ const forgeBlock = /minecraft\s*\{[^}]*?version\s*=\s*["'](\d+\.\d+(?:\.\d+)?)/.exec(text)
240
+ if (forgeBlock) return forgeBlock[1]
241
+
242
+ // 直接写死:minecraft_version = "1.21.1" / minecraftVersion = '1.21.1'
243
+ const literal = /(?:minecraft_version|minecraftVersion|mc_version|mcVersion)\s*[=:]\s*["']?(\d+\.\d+(?:\.\d+)?)["']?/.exec(
244
+ `${text}\n${renderProperties(properties)}`,
245
+ )
246
+ if (literal) return literal[1]
247
+
248
+ return undefined
249
+ }
250
+
251
+ /**
252
+ * @param {Record<string, string>} properties
253
+ * @returns {string}
254
+ */
255
+ function renderProperties(properties) {
256
+ return Object.entries(properties)
257
+ .map(([key, value]) => `${key}=${value}`)
258
+ .join('\n')
259
+ }
260
+
261
+ /**
262
+ * wrapper 里的 Gradle 版本。
263
+ * @param {Record<string, string>} wrapperProperties
264
+ * @returns {string | undefined}
265
+ */
266
+ export function parseWrapperVersion(wrapperProperties) {
267
+ const url = wrapperProperties.distributionUrl
268
+ if (!url) return undefined
269
+ const match = /gradle-(\d+\.\d+(?:\.\d+)?)-(?:bin|all)\.zip/.exec(url)
270
+ return match?.[1]
271
+ }
272
+
273
+ /**
274
+ * NeoForge 版本 → MC 版本。`21.1.251 → 1.21.1`、`20.4.237 → 1.20.4`、`20.2.x → 1.20.2`。
275
+ * 只映射到小版本(1.20.x 线),映射不到返回 undefined,不猜。
276
+ * @param {string | undefined} neoVersion
277
+ * @returns {string | undefined}
278
+ */
279
+ export function minecraftFromNeoVersion(neoVersion) {
280
+ if (!neoVersion) return undefined
281
+ const match = /^(\d+)\.(\d+)\./.exec(neoVersion.trim())
282
+ if (!match) return undefined
283
+ const major = Number(match[1])
284
+ const minor = Number(match[2])
285
+ if (major === 20) {
286
+ // 20.2 → 1.20.2、20.4 → 1.20.4、20.6 → 1.20.6
287
+ if (minor >= 2) return `1.20.${minor}`
288
+ return '1.20.1'
289
+ }
290
+ if (major === 21 || major === 22 || major === 23 || major >= 24) {
291
+ // 21.1 → 1.21.1;未来大版本按同样规律,但只在小版本 >0 时给三位
292
+ return minor > 0 ? `1.${major}.${minor}` : `1.${major}`
293
+ }
294
+ return undefined
295
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * JDK 需求推导与探测。
3
+ *
4
+ * 只读文件系统,不启动 `java -version`:识别面板要在打开瞬间出结果,
5
+ * 起进程既慢又会被 PATH 上的无关 JDK 带偏。版本从 JDK 目录里的 `release` 文件读。
6
+ * @module minecraft-dev-starter/lib/java
7
+ */
8
+
9
+ import { readdir } from 'node:fs/promises'
10
+ import { join } from 'node:path'
11
+ import { readText } from './text.js'
12
+
13
+ /**
14
+ * 目标 MC 版本需要的 JDK 大版本。
15
+ * 1.7.10/1.12.2/1.16.5 → 8;1.17 → 16;1.18–1.20.4 → 17;1.20.5+/1.21+ → 21;26.x → 25。
16
+ * @param {string | undefined} mcVersion
17
+ * @returns {number | undefined}
18
+ */
19
+ export function requiredJava(mcVersion) {
20
+ if (mcVersion === undefined) return undefined
21
+ const parts = mcVersion.split('.').map((part) => Number(part))
22
+ if (parts.some((part) => Number.isNaN(part))) return undefined
23
+ const [major, minor = 0, patch = 0] = parts
24
+ if (major >= 26) return 25
25
+ if (major !== 1) return undefined
26
+ if (minor >= 21) return 21
27
+ if (minor === 20) return patch >= 5 ? 21 : 17
28
+ if (minor >= 18) return 17
29
+ if (minor === 17) return 16
30
+ return 8
31
+ }
32
+
33
+ /**
34
+ * Gradle 从哪个版本开始支持某个 JDK 大版本。
35
+ * 只列已知边界;不在表里的按 0 处理。
36
+ * @type {Record<number, string>}
37
+ */
38
+ const GRADLE_JDK_FLOOR = {
39
+ 25: '9.0',
40
+ 24: '8.14',
41
+ 23: '8.10',
42
+ 22: '8.8',
43
+ 21: '8.5',
44
+ 20: '8.3',
45
+ 19: '7.6',
46
+ 17: '7.3',
47
+ 16: '7.0',
48
+ }
49
+
50
+ /**
51
+ * 判断 wrapper 的 Gradle 能不能跑这个 JDK。
52
+ * @param {string | undefined} gradleVersion
53
+ * @param {number | undefined} javaMajor
54
+ * @returns {{ ok: boolean, reason?: string }}
55
+ */
56
+ export function gradleSupportsJava(gradleVersion, javaMajor) {
57
+ if (gradleVersion === undefined || javaMajor === undefined) return { ok: true }
58
+ const floor = GRADLE_JDK_FLOOR[javaMajor]
59
+ if (floor === undefined) return { ok: true }
60
+ if (compareVersions(gradleVersion, floor) >= 0) return { ok: true }
61
+ return {
62
+ ok: false,
63
+ reason: `wrapper 是 Gradle ${gradleVersion},JDK ${javaMajor} 需要 Gradle ${floor} 以上`,
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {string} a
69
+ * @param {string} b
70
+ * @returns {number}
71
+ */
72
+ export function compareVersions(a, b) {
73
+ const left = a.split('.').map((part) => Number(part) || 0)
74
+ const right = b.split('.').map((part) => Number(part) || 0)
75
+ const length = Math.max(left.length, right.length)
76
+ for (let i = 0; i < length; i += 1) {
77
+ const diff = (left[i] ?? 0) - (right[i] ?? 0)
78
+ if (diff !== 0) return diff
79
+ }
80
+ return 0
81
+ }
82
+
83
+ /**
84
+ * @typedef {object} JdkInfo
85
+ * @property {string} home JDK 根目录
86
+ * @property {string} version 完整版本号,如 21.0.7
87
+ * @property {number} major 大版本,如 21
88
+ * @property {string} source 从哪找到的:env / dir:xxx
89
+ */
90
+
91
+ /** 常见的 JDK 安装根目录(Windows 为主,其他平台给几个通用位置)。 */
92
+ function jdkRoots(env = process.env, platform = process.platform) {
93
+ /** @type {string[]} */
94
+ const roots = []
95
+ if (env.JAVA_HOME) roots.push(env.JAVA_HOME)
96
+ if (env.MC_DEV_STARTER_JDKS) {
97
+ for (const entry of env.MC_DEV_STARTER_JDKS.split(';')) {
98
+ if (entry.trim() !== '') roots.push(entry.trim())
99
+ }
100
+ }
101
+ if (platform === 'win32') {
102
+ roots.push(
103
+ 'C:\\Program Files\\Java',
104
+ 'C:\\Program Files\\Microsoft',
105
+ 'C:\\Program Files\\Eclipse Adoptium',
106
+ 'C:\\Program Files\\BellSoft',
107
+ 'C:\\Program Files\\Zulu',
108
+ 'C:\\Program Files\\Amazon Corretto',
109
+ 'C:\\Program Files (x86)\\Java',
110
+ )
111
+ } else {
112
+ roots.push('/usr/lib/jvm', '/Library/Java/JavaVirtualMachines', join(env.HOME ?? '', '.sdkman/candidates/java'))
113
+ }
114
+ if (env.USERPROFILE) roots.push(join(env.USERPROFILE, '.jdks'))
115
+ return roots
116
+ }
117
+
118
+ /**
119
+ * 找本机所有 JDK。
120
+ * @param {{ env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, extraDirs?: string[] }} [options]
121
+ * @returns {Promise<JdkInfo[]>}
122
+ */
123
+ export async function discoverJdks(options = {}) {
124
+ const env = options.env ?? process.env
125
+ const platform = options.platform ?? process.platform
126
+ /** @type {Set<string>} */
127
+ const candidates = new Set()
128
+ for (const dir of options.extraDirs ?? []) candidates.add(dir)
129
+ for (const entry of jdkRoots(env, platform)) {
130
+ if (!entry) continue
131
+ candidates.add(entry)
132
+ // 根目录本身可能就是 JDK(JAVA_HOME / .jdks/<name>),也可能是装了一堆 JDK 的父目录
133
+ for (const child of await safeReaddir(entry)) {
134
+ candidates.add(join(entry, child))
135
+ }
136
+ }
137
+
138
+ /** @type {Map<string, JdkInfo>} */
139
+ const found = new Map()
140
+ for (const home of candidates) {
141
+ const info = await readJdk(home)
142
+ if (info === undefined) continue
143
+ if (!found.has(info.home)) found.set(info.home, info)
144
+ }
145
+ return [...found.values()].sort((left, right) => right.major - left.major)
146
+ }
147
+
148
+ /**
149
+ * @param {string} dir
150
+ * @returns {Promise<string[]>}
151
+ */
152
+ async function safeReaddir(dir) {
153
+ try {
154
+ return await readdir(dir)
155
+ } catch {
156
+ return []
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 读一个目录像不像 JDK,并拿到版本。
162
+ * @param {string} home
163
+ * @returns {Promise<JdkInfo | undefined>}
164
+ */
165
+ async function readJdk(home) {
166
+ const release = await readText(join(home, 'release'))
167
+ if (release !== undefined) {
168
+ const match = /JAVA_VERSION="([^"]+)"/.exec(release)
169
+ if (match) {
170
+ const version = match[1]
171
+ const major = Number.parseInt(version, 10)
172
+ if (!Number.isNaN(major)) return { home, version, major, source: 'release' }
173
+ }
174
+ }
175
+ // 没有 release(比如某些精简发行版)时退一步看目录名
176
+ const name = home.split(/[\\/]/).filter(Boolean).at(-1) ?? ''
177
+ const byName = /jdk-?(\d+)(?:\.\d+)*/.exec(name) ?? /java-?1\.(\d+)/.exec(name)
178
+ if (byName) {
179
+ const major = Number(byName[1])
180
+ if (!Number.isNaN(major)) return { home, version: String(major), major, source: 'dirname' }
181
+ }
182
+ return undefined
183
+ }
184
+
185
+ /**
186
+ * 挑一个满足要求的 JDK:优先大版本完全匹配,其次取不低于要求的最高版本。
187
+ * @param {JdkInfo[]} jdks
188
+ * @param {number | undefined} required
189
+ * @param {string | undefined} configured 显式配置的 JAVA_HOME / org.gradle.java.home
190
+ * @returns {{ home: string, exact: boolean } | undefined}
191
+ */
192
+ export function pickJdk(jdks, required, configured) {
193
+ if (configured) {
194
+ const hit = jdks.find((jdk) => jdk.home.toLowerCase() === configured.toLowerCase())
195
+ if (hit) return { home: hit.home, exact: hit.major === required }
196
+ return { home: configured, exact: false }
197
+ }
198
+ if (required === undefined) return jdks[0] ? { home: jdks[0].home, exact: false } : undefined
199
+ const exact = jdks.find((jdk) => jdk.major === required)
200
+ if (exact) return { home: exact.home, exact: true }
201
+ const above = jdks.find((jdk) => jdk.major > required)
202
+ if (above) return { home: above.home, exact: false }
203
+ return undefined
204
+ }