@torba/cleanroom 1.0.7
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/README.md +63 -0
- package/dist/index.cjs +148 -0
- package/dist/index.d.cts +120 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +120 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +147 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @torba/cleanroom
|
|
2
|
+
|
|
3
|
+
[Cleanroom Loader](https://cleanroommc.com/) support for torba. Resolves a Cleanroom release on GitHub, fetches the installer JAR, reads its bundled `version.json` + `install_profile.json`, and emits all artifacts needed to run vanilla 1.12.2 + Cleanroom.
|
|
4
|
+
|
|
5
|
+
No Mojang-controlled bytes are redistributed: vanilla artifacts come from Mojang URLs (via `@torba/minecraft`), Cleanroom's bundled bootstrap jar is materialized on the user's machine by extracting the `maven/` tree from the installer.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @torba/cleanroom @torba/minecraft @torba/core zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { resolveCleanroom } from '@torba/cleanroom';
|
|
17
|
+
|
|
18
|
+
const cr = await resolveCleanroom({ version: '0.5.9-alpha' });
|
|
19
|
+
|
|
20
|
+
cr.artifacts; // Vanilla MC + Cleanroom installer + bundled cleanroom jar + runtime libs
|
|
21
|
+
cr.vars; // Vars including merged classpath
|
|
22
|
+
cr.launch; // Assembled Launch using Foundation as the JVM main class
|
|
23
|
+
cr.jvmArgs; // JVM args alone (Valset)
|
|
24
|
+
cr.mainClass; // Foundation main class wrapped as a Val
|
|
25
|
+
cr.gameArgs; // Game args alone (Valset)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Version input
|
|
29
|
+
|
|
30
|
+
The `version` field accepts:
|
|
31
|
+
|
|
32
|
+
- **Exact release tag** — `'0.5.9-alpha'`.
|
|
33
|
+
- **`'latest'`** — newest non-prerelease GitHub release. (Cleanroom is currently alpha-only, so this throws today; pin a tag or use `'prerelease'`.)
|
|
34
|
+
- **`'prerelease'`** — newest GitHub release including prereleases.
|
|
35
|
+
|
|
36
|
+
### Options
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
resolveCleanroom({
|
|
40
|
+
version: string,
|
|
41
|
+
repo?: string, // default: 'CleanroomMC/Cleanroom'
|
|
42
|
+
token?: string, // optional GitHub token for higher rate limits
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
1. Resolves the requested `version` against `https://api.github.com/repos/<repo>/releases` and picks the matching release's `*-installer.jar` asset.
|
|
49
|
+
2. Downloads the installer JAR once and reads `version.json` + `install_profile.json` directly out of it (no upstream metadata service needed).
|
|
50
|
+
3. Fetches the vanilla MC client for `version.json.inheritsFrom` (typically `1.12.2`) via `@torba/minecraft`.
|
|
51
|
+
4. Emits artifacts for:
|
|
52
|
+
- vanilla MC (client.jar, libraries, asset index, assets, natives)
|
|
53
|
+
- the Cleanroom installer JAR with an `extract: scan('maven/', ...)` rule so the bundled `cleanroom-<v>.jar` lands at the correct library path on first launch
|
|
54
|
+
- the runtime libraries listed in the installer's `version.json` (the `com.cleanroommc:cleanroom:<v>` entry has `url: ""` and is sourced from the `maven/` extract instead of being downloaded)
|
|
55
|
+
- install-time libraries from `install_profile.json` (e.g. `mcp_config`)
|
|
56
|
+
5. Builds the launch classpath as `client.jar + vanilla libs + Cleanroom runtime libs`, sets the launch `mainClass` to `top.outlands.foundation.boot.Foundation`, and uses Cleanroom's legacy `minecraftArguments` as the game args (replacing vanilla's, per the Mojang launcher's `inheritsFrom` semantics).
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- **Java 25 is required** — torba does not manage JVMs, so configure your launch's `command` (e.g. `launch: { ...cr.launch, command: '/path/to/java25/javaw' }`) accordingly.
|
|
61
|
+
- Cleanroom has no install processors, no ForgeWrapper-equivalent, and no jar-merging step. The `Foundation` main class does its own bootstrap at runtime.
|
|
62
|
+
- The installer JAR is downloaded twice per session: once at template-build time to read its bundled JSONs, once at install time as a normal artifact. It's only ~5 MB.
|
|
63
|
+
- The installer's GitHub asset URL is direct — no rate limits beyond GitHub's normal CDN behavior. The release-listing API call respects `options.token` if you need authenticated rate limits.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let fflate = require("fflate");
|
|
3
|
+
let _torba_minecraft = require("@torba/minecraft");
|
|
4
|
+
let _torba_core = require("@torba/core");
|
|
5
|
+
let _torba_mojang = require("@torba/mojang");
|
|
6
|
+
|
|
7
|
+
//#region lib/resolver.ts
|
|
8
|
+
/**
|
|
9
|
+
* Resolver for Cleanroom releases on GitHub.
|
|
10
|
+
*
|
|
11
|
+
* Cleanroom has no fuckforge-equivalent metadata service — releases live
|
|
12
|
+
* directly on GitHub Releases, and each release ships a self-describing
|
|
13
|
+
* installer JAR (with `install_profile.json` + `version.json` inside).
|
|
14
|
+
*
|
|
15
|
+
* Accepted version forms:
|
|
16
|
+
* - Exact tag: '0.5.9-alpha'
|
|
17
|
+
* - 'latest' → newest non-prerelease (none exist as of writing)
|
|
18
|
+
* - 'prerelease' → newest including prereleases
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_REPO = "CleanroomMC/Cleanroom";
|
|
21
|
+
async function fetchReleases(repo, token) {
|
|
22
|
+
const headers = {
|
|
23
|
+
Accept: "application/vnd.github+json",
|
|
24
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
25
|
+
};
|
|
26
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
27
|
+
const res = await (0, _torba_core.fetchWithRetry)(`https://api.github.com/repos/${repo}/releases?per_page=100`, { headers });
|
|
28
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status} ${res.statusText} listing ${repo} releases`);
|
|
29
|
+
return await res.json();
|
|
30
|
+
}
|
|
31
|
+
function findInstaller(release) {
|
|
32
|
+
const asset = release.assets.find((a) => /-installer\.jar$/.test(a.name) && !a.name.includes("-sources"));
|
|
33
|
+
if (!asset) return null;
|
|
34
|
+
const sha256 = asset.digest?.startsWith("sha256:") ? asset.digest.slice(7) : void 0;
|
|
35
|
+
return {
|
|
36
|
+
url: asset.browser_download_url,
|
|
37
|
+
name: asset.name,
|
|
38
|
+
size: asset.size,
|
|
39
|
+
sha256
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function toRelease(release) {
|
|
43
|
+
const installer = findInstaller(release);
|
|
44
|
+
if (!installer) return null;
|
|
45
|
+
return {
|
|
46
|
+
tag: release.tag_name,
|
|
47
|
+
prerelease: release.prerelease,
|
|
48
|
+
installerUrl: installer.url,
|
|
49
|
+
installerName: installer.name,
|
|
50
|
+
installerSize: installer.size,
|
|
51
|
+
installerSha256: installer.sha256,
|
|
52
|
+
publishedAt: release.published_at
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function resolveCleanroomVersion(input, options = {}) {
|
|
56
|
+
const repo = options.repo ?? DEFAULT_REPO;
|
|
57
|
+
const releases = (await fetchReleases(repo, options.token)).filter((r) => !r.draft).map(toRelease).filter((r) => r !== null);
|
|
58
|
+
if (input === "latest") {
|
|
59
|
+
const stable = releases.find((r) => !r.prerelease);
|
|
60
|
+
if (!stable) throw new Error(`No stable Cleanroom release found in ${repo}. Try 'prerelease' or pin a specific tag.`);
|
|
61
|
+
return stable;
|
|
62
|
+
}
|
|
63
|
+
if (input === "prerelease") {
|
|
64
|
+
if (releases.length === 0) throw new Error(`No Cleanroom releases found in ${repo}`);
|
|
65
|
+
return releases[0];
|
|
66
|
+
}
|
|
67
|
+
const match = releases.find((r) => r.tag === input);
|
|
68
|
+
if (!match) throw new Error(`Cleanroom release '${input}' not found in ${repo}. Available: ${releases.slice(0, 5).map((r) => r.tag).join(", ")}${releases.length > 5 ? ", …" : ""}`);
|
|
69
|
+
return match;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region lib/template.ts
|
|
74
|
+
async function fetchInstallerBytes(url) {
|
|
75
|
+
const res = await (0, _torba_core.fetchWithRetry)(url);
|
|
76
|
+
if (!res.ok) throw new Error(`Failed to download Cleanroom installer from ${url}: ${res.status} ${res.statusText}`);
|
|
77
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
78
|
+
}
|
|
79
|
+
function readJsonEntry(zip, name) {
|
|
80
|
+
const buf = zip[name];
|
|
81
|
+
if (!buf) throw new Error(`Cleanroom installer is missing entry '${name}'`);
|
|
82
|
+
return JSON.parse(new TextDecoder().decode(buf));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a torba template that launches Minecraft with the Cleanroom loader.
|
|
86
|
+
*
|
|
87
|
+
* Cleanroom is structurally similar to legacy Forge: vanilla 1.12.2 + a custom
|
|
88
|
+
* mainClass (`top.outlands.foundation.boot.Foundation`) + a legacy
|
|
89
|
+
* `minecraftArguments` string + a flat library list. The installer JAR bundles
|
|
90
|
+
* its own version.json + install_profile.json + a `maven/` tree containing the
|
|
91
|
+
* cleanroom runtime jar (which has `url: ""` in version.json — sourced from
|
|
92
|
+
* the `maven/` extract instead).
|
|
93
|
+
*/
|
|
94
|
+
async function resolveCleanroom(options) {
|
|
95
|
+
const release = await resolveCleanroomVersion(options.version, {
|
|
96
|
+
repo: options.repo,
|
|
97
|
+
token: options.token
|
|
98
|
+
});
|
|
99
|
+
const zip = (0, fflate.unzipSync)(await fetchInstallerBytes(release.installerUrl), { filter: (f) => f.name === "version.json" || f.name === "install_profile.json" });
|
|
100
|
+
const versionJson = readJsonEntry(zip, "version.json");
|
|
101
|
+
const installProfile = readJsonEntry(zip, "install_profile.json");
|
|
102
|
+
const vanillaId = versionJson.inheritsFrom;
|
|
103
|
+
const { client } = await (0, _torba_minecraft.fetchClient)(vanillaId);
|
|
104
|
+
const mc = await (0, _torba_minecraft.clientToTemplate)(client);
|
|
105
|
+
const runtimeLibs = (0, _torba_mojang.parseLibraries)(versionJson.libraries);
|
|
106
|
+
const installLibs = (0, _torba_mojang.parseLibraries)(installProfile.libraries);
|
|
107
|
+
const downloadableRuntime = runtimeLibs.filter((l) => l.artifact.url);
|
|
108
|
+
const downloadableInstall = installLibs.filter((l) => l.artifact.url);
|
|
109
|
+
const cleanroomCoords = new Set(runtimeLibs.map((l) => `${l.name.groupId}:${l.name.artifactId}`));
|
|
110
|
+
const isShadowedByCleanroom = (lib) => {
|
|
111
|
+
if (lib.name.groupId === "org.lwjgl.lwjgl") return true;
|
|
112
|
+
return cleanroomCoords.has(`${lib.name.groupId}:${lib.name.artifactId}`);
|
|
113
|
+
};
|
|
114
|
+
const keptVanillaLibs = client.libraries.filter((l) => !isShadowedByCleanroom(l));
|
|
115
|
+
const shadowedPaths = new Set(client.libraries.filter(isShadowedByCleanroom).map((l) => `\${library_directory}/${l.artifact.path}`));
|
|
116
|
+
const installerArtifact = {
|
|
117
|
+
path: `\${library_directory}/com/cleanroommc/cleanroom/${release.tag}/${release.installerName}`,
|
|
118
|
+
source: (0, _torba_core.sourceUrl)(release.installerUrl),
|
|
119
|
+
size: release.installerSize,
|
|
120
|
+
rules: [],
|
|
121
|
+
...release.installerSha256 ? { integrity: { sha256: release.installerSha256 } } : {},
|
|
122
|
+
extract: [(0, _torba_core.extractScan)("maven/", "${library_directory}", { strip: ["maven/"] })]
|
|
123
|
+
};
|
|
124
|
+
const classpathEntries = (0, _torba_minecraft.buildClasspath)([...runtimeLibs, ...keptVanillaLibs].map((l) => ({
|
|
125
|
+
rules: l.rules,
|
|
126
|
+
artifactPath: `\${library_directory}/${l.artifact.path}`
|
|
127
|
+
})), "${version_dir}/client.jar");
|
|
128
|
+
const vars = {
|
|
129
|
+
...mc.vars,
|
|
130
|
+
classpath: classpathEntries
|
|
131
|
+
};
|
|
132
|
+
const args = (0, _torba_mojang.parseArguments)(versionJson.minecraftArguments ?? versionJson.arguments ?? "");
|
|
133
|
+
const parts = (0, _torba_minecraft.buildLaunch)(versionJson.mainClass, args.game, args.jvm);
|
|
134
|
+
return {
|
|
135
|
+
artifacts: [
|
|
136
|
+
...mc.artifacts.filter((a) => !shadowedPaths.has(a.path)),
|
|
137
|
+
installerArtifact,
|
|
138
|
+
...(0, _torba_minecraft.mapLibraries)(downloadableRuntime),
|
|
139
|
+
...(0, _torba_minecraft.mapLibraries)(downloadableInstall)
|
|
140
|
+
],
|
|
141
|
+
vars,
|
|
142
|
+
...parts
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
exports.resolveCleanroom = resolveCleanroom;
|
|
148
|
+
exports.resolveCleanroomVersion = resolveCleanroomVersion;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Artifact, Launch, ValDefs } from "@torba/core";
|
|
2
|
+
|
|
3
|
+
//#region ../rules/dist/index.d.cts
|
|
4
|
+
//#region lib/os.d.ts
|
|
5
|
+
type OsName = 'linux' | 'windows' | 'osx';
|
|
6
|
+
type OsArch = 'x86' | 'x86_64' | 'arm' | 'aarch64' | 'any';
|
|
7
|
+
/** Platform context passed to rule evaluation. */
|
|
8
|
+
/** Constraint on OS as it appears in Mojang/Manifest rule JSON. */
|
|
9
|
+
type OsConstraint = {
|
|
10
|
+
name: OsName;
|
|
11
|
+
version: string;
|
|
12
|
+
} | {
|
|
13
|
+
name: OsName;
|
|
14
|
+
} | {
|
|
15
|
+
arch: OsArch;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region lib/features.d.ts
|
|
19
|
+
type FeatureConstraint = Record<string, boolean>;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region lib/rule.d.ts
|
|
22
|
+
type RuleAction = 'allow' | 'disallow';
|
|
23
|
+
type Rule = {
|
|
24
|
+
action: RuleAction;
|
|
25
|
+
os: OsConstraint;
|
|
26
|
+
} | {
|
|
27
|
+
action: RuleAction;
|
|
28
|
+
features: FeatureConstraint;
|
|
29
|
+
} | {
|
|
30
|
+
action: RuleAction;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region lib/ruleset.d.ts
|
|
34
|
+
type Ruleset = Rule[];
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/val.d.ts
|
|
37
|
+
interface Val {
|
|
38
|
+
readonly rules: Ruleset;
|
|
39
|
+
readonly value: string[];
|
|
40
|
+
}
|
|
41
|
+
type Valset = Val[];
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region lib/template.d.ts
|
|
44
|
+
interface CleanroomOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Cleanroom version. Accepts:
|
|
47
|
+
* - Exact tag: `'0.5.9-alpha'`
|
|
48
|
+
* - `'prerelease'` — newest GitHub release (Cleanroom is currently alpha-only)
|
|
49
|
+
* - `'latest'` — newest non-prerelease
|
|
50
|
+
*/
|
|
51
|
+
version: string;
|
|
52
|
+
/** GitHub repo override. Default: `CleanroomMC/Cleanroom`. */
|
|
53
|
+
repo?: string;
|
|
54
|
+
/** Optional GitHub token for higher rate limits while resolving releases. */
|
|
55
|
+
token?: string;
|
|
56
|
+
}
|
|
57
|
+
interface CleanroomTemplate {
|
|
58
|
+
/** Vanilla MC + Cleanroom installer + bundled cleanroom jar + runtime libraries. */
|
|
59
|
+
artifacts: Artifact[];
|
|
60
|
+
vars: ValDefs;
|
|
61
|
+
/** Assembled Launch — drop straight into `manifest.launch`. */
|
|
62
|
+
launch: Launch;
|
|
63
|
+
/** JVM args alone, for composition (e.g. interleaving authliberty's `-javaagent`). */
|
|
64
|
+
jvmArgs: Valset;
|
|
65
|
+
/** Main class wrapped as a `Val` so it spreads into a `Valset`. Raw string at `mainClass.value[0]`. */
|
|
66
|
+
mainClass: Val;
|
|
67
|
+
/** Game args alone, for composition. */
|
|
68
|
+
gameArgs: Valset;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a torba template that launches Minecraft with the Cleanroom loader.
|
|
72
|
+
*
|
|
73
|
+
* Cleanroom is structurally similar to legacy Forge: vanilla 1.12.2 + a custom
|
|
74
|
+
* mainClass (`top.outlands.foundation.boot.Foundation`) + a legacy
|
|
75
|
+
* `minecraftArguments` string + a flat library list. The installer JAR bundles
|
|
76
|
+
* its own version.json + install_profile.json + a `maven/` tree containing the
|
|
77
|
+
* cleanroom runtime jar (which has `url: ""` in version.json — sourced from
|
|
78
|
+
* the `maven/` extract instead).
|
|
79
|
+
*/
|
|
80
|
+
declare function resolveCleanroom(options: CleanroomOptions): Promise<CleanroomTemplate>;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region lib/resolver.d.ts
|
|
83
|
+
/**
|
|
84
|
+
* Resolver for Cleanroom releases on GitHub.
|
|
85
|
+
*
|
|
86
|
+
* Cleanroom has no fuckforge-equivalent metadata service — releases live
|
|
87
|
+
* directly on GitHub Releases, and each release ships a self-describing
|
|
88
|
+
* installer JAR (with `install_profile.json` + `version.json` inside).
|
|
89
|
+
*
|
|
90
|
+
* Accepted version forms:
|
|
91
|
+
* - Exact tag: '0.5.9-alpha'
|
|
92
|
+
* - 'latest' → newest non-prerelease (none exist as of writing)
|
|
93
|
+
* - 'prerelease' → newest including prereleases
|
|
94
|
+
*/
|
|
95
|
+
interface CleanroomRelease {
|
|
96
|
+
/** Release tag, e.g. `0.5.9-alpha`. */
|
|
97
|
+
readonly tag: string;
|
|
98
|
+
/** Whether the release is marked as prerelease on GitHub. */
|
|
99
|
+
readonly prerelease: boolean;
|
|
100
|
+
/** Direct download URL for the installer JAR. */
|
|
101
|
+
readonly installerUrl: string;
|
|
102
|
+
/** Installer asset filename, e.g. `cleanroom-0.5.9-alpha-installer.jar`. */
|
|
103
|
+
readonly installerName: string;
|
|
104
|
+
/** Installer asset size in bytes. */
|
|
105
|
+
readonly installerSize: number;
|
|
106
|
+
/** sha256 of the installer asset, when GitHub publishes it. Hex only. */
|
|
107
|
+
readonly installerSha256?: string;
|
|
108
|
+
/** ISO timestamp the release was published. */
|
|
109
|
+
readonly publishedAt: string;
|
|
110
|
+
}
|
|
111
|
+
interface ResolveCleanroomOptions {
|
|
112
|
+
/** GitHub repo `owner/name`. Default: `CleanroomMC/Cleanroom`. */
|
|
113
|
+
repo?: string;
|
|
114
|
+
/** Optional GitHub token for higher rate limits. */
|
|
115
|
+
token?: string;
|
|
116
|
+
}
|
|
117
|
+
declare function resolveCleanroomVersion(input: string, options?: ResolveCleanroomOptions): Promise<CleanroomRelease>;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { type CleanroomOptions, type CleanroomRelease, type CleanroomTemplate, type ResolveCleanroomOptions, resolveCleanroom, resolveCleanroomVersion };
|
|
120
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":["z","OsName","OsArch","OsOptions","name","version","arch","OsConstraint","OsNameSchema","ZodEnum","linux","windows","osx","OsArchSchema","x86","x86_64","arm","aarch64","any","OsConstraintSchema","ZodType","satisfiesOs","constraint","os","SatisfiesOsOptions","FeatureConstraint","Record","FeatureConstraintSchema","ZodString","ZodBoolean","ZodRecord","satisfiesFeatures","feats","RuleAction","Rule","action","features","RuleSchema","satisfiesRule","rule","Ruleset","RulesetSchema","satisfiesRuleset","ruleset","emptyRuleset","allowOsRuleset","RawSingle","RawRuleset","parseShortRule","raw","encodeShortRule","parseShortRuleset","encodeShortRuleset","ShortRule","decode","encode","ShortRuleset","Val","rules","value","ValRawSchema","ZodAny","ZodArray","ZodUnion","core","$strip","ZodObject","parseVal","infer","encodeVal","val","Valset","parseValset","encodeValset","vs","resolveValset"],"sources":["../../rules/dist/index.d.cts","../lib/template.ts","../lib/resolver.ts"],"mappings":";;;;KAGKC,MAAAA;AAAAA,KACAC,MAAAA;;;KAQAK,YAAAA;EACHH,IAAAA,EAAMH,MAAAA;EACNI,OAAAA;AAAAA;EAEAD,IAAAA,EAAMH,MAAAA;AAAAA;EAENK,IAAAA,EAAMJ,MAAAA;AAAAA;AAAAA;AAAAA;AAAAA,KAoBHuB,iBAAAA,GAAoBC,MAAAA;AAAAA;AAAAA;AAAAA,KAKpBO,UAAAA;AAAAA,KACAC,IAAAA;EACHC,MAAAA,EAAQF,UAAAA;EACRV,EAAAA,EAAIhB,YAAAA;AAAAA;EAEJ4B,MAAAA,EAAQF,UAAAA;EACRG,QAAAA,EAAUX,iBAAAA;AAAAA;EAEVU,MAAAA,EAAQF,UAAAA;AAAAA;AAAAA;AAAAA;AAAAA,KAMLO,OAAAA,GAAUN,IAAAA;AAAAA;AAAAA;AAAAA,UAyBLuB,GAAAA;EAAAA,SACCC,KAAAA,EAAOlB,OAAAA;EAAAA,SACPmB,KAAAA;AAAAA;AAAAA,KAQNY,MAAAA,GAASd,GAAAA;;;UC/DG,gBAAA;ED1BZxD;;;;;AAAM;ECiCT,OAAA;;EAEA,IAAA;EDlCS;ECoCT,KAAA;AAAA;AAAA,UAGe,iBAAA;ED9BTA;ECgCN,SAAA,EAAW,QAAA;EACX,IAAA,EAAM,OAAA;ED5BM;EC8BZ,MAAA,EAAQ,MAAA;EDnCRG;ECqCA,OAAA,EAAS,MAAA;EDpCTC;ECsCA,SAAA,EAAW,GAAA;EDpCLJ;ECsCN,QAAA,EAAU,MAAA;AAAA;;;ADnBuB;;;;;AAK8C;;;iBC8D3D,gBAAA,CACpB,OAAA,EAAS,gBAAA,GACR,OAAA,CAAQ,iBAAA;;;;;;ADxGa;;;;;AAGb;;;;UEcM,gBAAA;EFLZM;EAAAA,SEOM,GAAA;;WAEA,UAAA;EFLHN;EAAAA,SEOG,YAAA;EFLG;EAAA,SEOH,aAAA;EFZTG;EAAAA,SEcS,aAAA;EFbTC;EAAAA,SEeS,eAAA;EFbHJ;EAAAA,SEeG,WAAA;AAAA;AAAA,UAGM,uBAAA;EFhBH;EEkBZ,IAAA;EFEoB;EEApB,KAAA;AAAA;AAAA,iBA0EoB,uBAAA,CACpB,KAAA,UACA,OAAA,GAAS,uBAAA,GACR,OAAA,CAAQ,gBAAA"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Artifact, Launch, ValDefs } from "@torba/core";
|
|
2
|
+
|
|
3
|
+
//#region ../rules/dist/index.d.cts
|
|
4
|
+
//#region lib/os.d.ts
|
|
5
|
+
type OsName = 'linux' | 'windows' | 'osx';
|
|
6
|
+
type OsArch = 'x86' | 'x86_64' | 'arm' | 'aarch64' | 'any';
|
|
7
|
+
/** Platform context passed to rule evaluation. */
|
|
8
|
+
/** Constraint on OS as it appears in Mojang/Manifest rule JSON. */
|
|
9
|
+
type OsConstraint = {
|
|
10
|
+
name: OsName;
|
|
11
|
+
version: string;
|
|
12
|
+
} | {
|
|
13
|
+
name: OsName;
|
|
14
|
+
} | {
|
|
15
|
+
arch: OsArch;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region lib/features.d.ts
|
|
19
|
+
type FeatureConstraint = Record<string, boolean>;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region lib/rule.d.ts
|
|
22
|
+
type RuleAction = 'allow' | 'disallow';
|
|
23
|
+
type Rule = {
|
|
24
|
+
action: RuleAction;
|
|
25
|
+
os: OsConstraint;
|
|
26
|
+
} | {
|
|
27
|
+
action: RuleAction;
|
|
28
|
+
features: FeatureConstraint;
|
|
29
|
+
} | {
|
|
30
|
+
action: RuleAction;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region lib/ruleset.d.ts
|
|
34
|
+
type Ruleset = Rule[];
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/val.d.ts
|
|
37
|
+
interface Val {
|
|
38
|
+
readonly rules: Ruleset;
|
|
39
|
+
readonly value: string[];
|
|
40
|
+
}
|
|
41
|
+
type Valset = Val[];
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region lib/template.d.ts
|
|
44
|
+
interface CleanroomOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Cleanroom version. Accepts:
|
|
47
|
+
* - Exact tag: `'0.5.9-alpha'`
|
|
48
|
+
* - `'prerelease'` — newest GitHub release (Cleanroom is currently alpha-only)
|
|
49
|
+
* - `'latest'` — newest non-prerelease
|
|
50
|
+
*/
|
|
51
|
+
version: string;
|
|
52
|
+
/** GitHub repo override. Default: `CleanroomMC/Cleanroom`. */
|
|
53
|
+
repo?: string;
|
|
54
|
+
/** Optional GitHub token for higher rate limits while resolving releases. */
|
|
55
|
+
token?: string;
|
|
56
|
+
}
|
|
57
|
+
interface CleanroomTemplate {
|
|
58
|
+
/** Vanilla MC + Cleanroom installer + bundled cleanroom jar + runtime libraries. */
|
|
59
|
+
artifacts: Artifact[];
|
|
60
|
+
vars: ValDefs;
|
|
61
|
+
/** Assembled Launch — drop straight into `manifest.launch`. */
|
|
62
|
+
launch: Launch;
|
|
63
|
+
/** JVM args alone, for composition (e.g. interleaving authliberty's `-javaagent`). */
|
|
64
|
+
jvmArgs: Valset;
|
|
65
|
+
/** Main class wrapped as a `Val` so it spreads into a `Valset`. Raw string at `mainClass.value[0]`. */
|
|
66
|
+
mainClass: Val;
|
|
67
|
+
/** Game args alone, for composition. */
|
|
68
|
+
gameArgs: Valset;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a torba template that launches Minecraft with the Cleanroom loader.
|
|
72
|
+
*
|
|
73
|
+
* Cleanroom is structurally similar to legacy Forge: vanilla 1.12.2 + a custom
|
|
74
|
+
* mainClass (`top.outlands.foundation.boot.Foundation`) + a legacy
|
|
75
|
+
* `minecraftArguments` string + a flat library list. The installer JAR bundles
|
|
76
|
+
* its own version.json + install_profile.json + a `maven/` tree containing the
|
|
77
|
+
* cleanroom runtime jar (which has `url: ""` in version.json — sourced from
|
|
78
|
+
* the `maven/` extract instead).
|
|
79
|
+
*/
|
|
80
|
+
declare function resolveCleanroom(options: CleanroomOptions): Promise<CleanroomTemplate>;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region lib/resolver.d.ts
|
|
83
|
+
/**
|
|
84
|
+
* Resolver for Cleanroom releases on GitHub.
|
|
85
|
+
*
|
|
86
|
+
* Cleanroom has no fuckforge-equivalent metadata service — releases live
|
|
87
|
+
* directly on GitHub Releases, and each release ships a self-describing
|
|
88
|
+
* installer JAR (with `install_profile.json` + `version.json` inside).
|
|
89
|
+
*
|
|
90
|
+
* Accepted version forms:
|
|
91
|
+
* - Exact tag: '0.5.9-alpha'
|
|
92
|
+
* - 'latest' → newest non-prerelease (none exist as of writing)
|
|
93
|
+
* - 'prerelease' → newest including prereleases
|
|
94
|
+
*/
|
|
95
|
+
interface CleanroomRelease {
|
|
96
|
+
/** Release tag, e.g. `0.5.9-alpha`. */
|
|
97
|
+
readonly tag: string;
|
|
98
|
+
/** Whether the release is marked as prerelease on GitHub. */
|
|
99
|
+
readonly prerelease: boolean;
|
|
100
|
+
/** Direct download URL for the installer JAR. */
|
|
101
|
+
readonly installerUrl: string;
|
|
102
|
+
/** Installer asset filename, e.g. `cleanroom-0.5.9-alpha-installer.jar`. */
|
|
103
|
+
readonly installerName: string;
|
|
104
|
+
/** Installer asset size in bytes. */
|
|
105
|
+
readonly installerSize: number;
|
|
106
|
+
/** sha256 of the installer asset, when GitHub publishes it. Hex only. */
|
|
107
|
+
readonly installerSha256?: string;
|
|
108
|
+
/** ISO timestamp the release was published. */
|
|
109
|
+
readonly publishedAt: string;
|
|
110
|
+
}
|
|
111
|
+
interface ResolveCleanroomOptions {
|
|
112
|
+
/** GitHub repo `owner/name`. Default: `CleanroomMC/Cleanroom`. */
|
|
113
|
+
repo?: string;
|
|
114
|
+
/** Optional GitHub token for higher rate limits. */
|
|
115
|
+
token?: string;
|
|
116
|
+
}
|
|
117
|
+
declare function resolveCleanroomVersion(input: string, options?: ResolveCleanroomOptions): Promise<CleanroomRelease>;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { type CleanroomOptions, type CleanroomRelease, type CleanroomTemplate, type ResolveCleanroomOptions, resolveCleanroom, resolveCleanroomVersion };
|
|
120
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["z","OsName","OsArch","OsOptions","name","version","arch","OsConstraint","OsNameSchema","ZodEnum","linux","windows","osx","OsArchSchema","x86","x86_64","arm","aarch64","any","OsConstraintSchema","ZodType","satisfiesOs","constraint","os","SatisfiesOsOptions","FeatureConstraint","Record","FeatureConstraintSchema","ZodString","ZodBoolean","ZodRecord","satisfiesFeatures","feats","RuleAction","Rule","action","features","RuleSchema","satisfiesRule","rule","Ruleset","RulesetSchema","satisfiesRuleset","ruleset","emptyRuleset","allowOsRuleset","RawSingle","RawRuleset","parseShortRule","raw","encodeShortRule","parseShortRuleset","encodeShortRuleset","ShortRule","decode","encode","ShortRuleset","Val","rules","value","ValRawSchema","ZodAny","ZodArray","ZodUnion","core","$strip","ZodObject","parseVal","infer","encodeVal","val","Valset","parseValset","encodeValset","vs","resolveValset"],"sources":["../../rules/dist/index.d.cts","../lib/template.ts","../lib/resolver.ts"],"mappings":";;;;KAGKC,MAAAA;AAAAA,KACAC,MAAAA;;;KAQAK,YAAAA;EACHH,IAAAA,EAAMH,MAAAA;EACNI,OAAAA;AAAAA;EAEAD,IAAAA,EAAMH,MAAAA;AAAAA;EAENK,IAAAA,EAAMJ,MAAAA;AAAAA;AAAAA;AAAAA;AAAAA,KAoBHuB,iBAAAA,GAAoBC,MAAAA;AAAAA;AAAAA;AAAAA,KAKpBO,UAAAA;AAAAA,KACAC,IAAAA;EACHC,MAAAA,EAAQF,UAAAA;EACRV,EAAAA,EAAIhB,YAAAA;AAAAA;EAEJ4B,MAAAA,EAAQF,UAAAA;EACRG,QAAAA,EAAUX,iBAAAA;AAAAA;EAEVU,MAAAA,EAAQF,UAAAA;AAAAA;AAAAA;AAAAA;AAAAA,KAMLO,OAAAA,GAAUN,IAAAA;AAAAA;AAAAA;AAAAA,UAyBLuB,GAAAA;EAAAA,SACCC,KAAAA,EAAOlB,OAAAA;EAAAA,SACPmB,KAAAA;AAAAA;AAAAA,KAQNY,MAAAA,GAASd,GAAAA;;;UC/DG,gBAAA;ED1BZxD;;;;;AAAM;ECiCT,OAAA;;EAEA,IAAA;EDlCS;ECoCT,KAAA;AAAA;AAAA,UAGe,iBAAA;ED9BTA;ECgCN,SAAA,EAAW,QAAA;EACX,IAAA,EAAM,OAAA;ED5BM;EC8BZ,MAAA,EAAQ,MAAA;EDnCRG;ECqCA,OAAA,EAAS,MAAA;EDpCTC;ECsCA,SAAA,EAAW,GAAA;EDpCLJ;ECsCN,QAAA,EAAU,MAAA;AAAA;;;ADnBuB;;;;;AAK8C;;;iBC8D3D,gBAAA,CACpB,OAAA,EAAS,gBAAA,GACR,OAAA,CAAQ,iBAAA;;;;;;ADxGa;;;;;AAGb;;;;UEcM,gBAAA;EFLZM;EAAAA,SEOM,GAAA;;WAEA,UAAA;EFLHN;EAAAA,SEOG,YAAA;EFLG;EAAA,SEOH,aAAA;EFZTG;EAAAA,SEcS,aAAA;EFbTC;EAAAA,SEeS,eAAA;EFbHJ;EAAAA,SEeG,WAAA;AAAA;AAAA,UAGM,uBAAA;EFhBH;EEkBZ,IAAA;EFEoB;EEApB,KAAA;AAAA;AAAA,iBA0EoB,uBAAA,CACpB,KAAA,UACA,OAAA,GAAS,uBAAA,GACR,OAAA,CAAQ,gBAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { unzipSync } from "fflate";
|
|
2
|
+
import { buildClasspath, buildLaunch, clientToTemplate, fetchClient, mapLibraries } from "@torba/minecraft";
|
|
3
|
+
import { extractScan, fetchWithRetry, sourceUrl } from "@torba/core";
|
|
4
|
+
import { parseArguments, parseLibraries } from "@torba/mojang";
|
|
5
|
+
|
|
6
|
+
//#region lib/resolver.ts
|
|
7
|
+
/**
|
|
8
|
+
* Resolver for Cleanroom releases on GitHub.
|
|
9
|
+
*
|
|
10
|
+
* Cleanroom has no fuckforge-equivalent metadata service — releases live
|
|
11
|
+
* directly on GitHub Releases, and each release ships a self-describing
|
|
12
|
+
* installer JAR (with `install_profile.json` + `version.json` inside).
|
|
13
|
+
*
|
|
14
|
+
* Accepted version forms:
|
|
15
|
+
* - Exact tag: '0.5.9-alpha'
|
|
16
|
+
* - 'latest' → newest non-prerelease (none exist as of writing)
|
|
17
|
+
* - 'prerelease' → newest including prereleases
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_REPO = "CleanroomMC/Cleanroom";
|
|
20
|
+
async function fetchReleases(repo, token) {
|
|
21
|
+
const headers = {
|
|
22
|
+
Accept: "application/vnd.github+json",
|
|
23
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
24
|
+
};
|
|
25
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
26
|
+
const res = await fetchWithRetry(`https://api.github.com/repos/${repo}/releases?per_page=100`, { headers });
|
|
27
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status} ${res.statusText} listing ${repo} releases`);
|
|
28
|
+
return await res.json();
|
|
29
|
+
}
|
|
30
|
+
function findInstaller(release) {
|
|
31
|
+
const asset = release.assets.find((a) => /-installer\.jar$/.test(a.name) && !a.name.includes("-sources"));
|
|
32
|
+
if (!asset) return null;
|
|
33
|
+
const sha256 = asset.digest?.startsWith("sha256:") ? asset.digest.slice(7) : void 0;
|
|
34
|
+
return {
|
|
35
|
+
url: asset.browser_download_url,
|
|
36
|
+
name: asset.name,
|
|
37
|
+
size: asset.size,
|
|
38
|
+
sha256
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function toRelease(release) {
|
|
42
|
+
const installer = findInstaller(release);
|
|
43
|
+
if (!installer) return null;
|
|
44
|
+
return {
|
|
45
|
+
tag: release.tag_name,
|
|
46
|
+
prerelease: release.prerelease,
|
|
47
|
+
installerUrl: installer.url,
|
|
48
|
+
installerName: installer.name,
|
|
49
|
+
installerSize: installer.size,
|
|
50
|
+
installerSha256: installer.sha256,
|
|
51
|
+
publishedAt: release.published_at
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function resolveCleanroomVersion(input, options = {}) {
|
|
55
|
+
const repo = options.repo ?? DEFAULT_REPO;
|
|
56
|
+
const releases = (await fetchReleases(repo, options.token)).filter((r) => !r.draft).map(toRelease).filter((r) => r !== null);
|
|
57
|
+
if (input === "latest") {
|
|
58
|
+
const stable = releases.find((r) => !r.prerelease);
|
|
59
|
+
if (!stable) throw new Error(`No stable Cleanroom release found in ${repo}. Try 'prerelease' or pin a specific tag.`);
|
|
60
|
+
return stable;
|
|
61
|
+
}
|
|
62
|
+
if (input === "prerelease") {
|
|
63
|
+
if (releases.length === 0) throw new Error(`No Cleanroom releases found in ${repo}`);
|
|
64
|
+
return releases[0];
|
|
65
|
+
}
|
|
66
|
+
const match = releases.find((r) => r.tag === input);
|
|
67
|
+
if (!match) throw new Error(`Cleanroom release '${input}' not found in ${repo}. Available: ${releases.slice(0, 5).map((r) => r.tag).join(", ")}${releases.length > 5 ? ", …" : ""}`);
|
|
68
|
+
return match;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region lib/template.ts
|
|
73
|
+
async function fetchInstallerBytes(url) {
|
|
74
|
+
const res = await fetchWithRetry(url);
|
|
75
|
+
if (!res.ok) throw new Error(`Failed to download Cleanroom installer from ${url}: ${res.status} ${res.statusText}`);
|
|
76
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
77
|
+
}
|
|
78
|
+
function readJsonEntry(zip, name) {
|
|
79
|
+
const buf = zip[name];
|
|
80
|
+
if (!buf) throw new Error(`Cleanroom installer is missing entry '${name}'`);
|
|
81
|
+
return JSON.parse(new TextDecoder().decode(buf));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a torba template that launches Minecraft with the Cleanroom loader.
|
|
85
|
+
*
|
|
86
|
+
* Cleanroom is structurally similar to legacy Forge: vanilla 1.12.2 + a custom
|
|
87
|
+
* mainClass (`top.outlands.foundation.boot.Foundation`) + a legacy
|
|
88
|
+
* `minecraftArguments` string + a flat library list. The installer JAR bundles
|
|
89
|
+
* its own version.json + install_profile.json + a `maven/` tree containing the
|
|
90
|
+
* cleanroom runtime jar (which has `url: ""` in version.json — sourced from
|
|
91
|
+
* the `maven/` extract instead).
|
|
92
|
+
*/
|
|
93
|
+
async function resolveCleanroom(options) {
|
|
94
|
+
const release = await resolveCleanroomVersion(options.version, {
|
|
95
|
+
repo: options.repo,
|
|
96
|
+
token: options.token
|
|
97
|
+
});
|
|
98
|
+
const zip = unzipSync(await fetchInstallerBytes(release.installerUrl), { filter: (f) => f.name === "version.json" || f.name === "install_profile.json" });
|
|
99
|
+
const versionJson = readJsonEntry(zip, "version.json");
|
|
100
|
+
const installProfile = readJsonEntry(zip, "install_profile.json");
|
|
101
|
+
const vanillaId = versionJson.inheritsFrom;
|
|
102
|
+
const { client } = await fetchClient(vanillaId);
|
|
103
|
+
const mc = await clientToTemplate(client);
|
|
104
|
+
const runtimeLibs = parseLibraries(versionJson.libraries);
|
|
105
|
+
const installLibs = parseLibraries(installProfile.libraries);
|
|
106
|
+
const downloadableRuntime = runtimeLibs.filter((l) => l.artifact.url);
|
|
107
|
+
const downloadableInstall = installLibs.filter((l) => l.artifact.url);
|
|
108
|
+
const cleanroomCoords = new Set(runtimeLibs.map((l) => `${l.name.groupId}:${l.name.artifactId}`));
|
|
109
|
+
const isShadowedByCleanroom = (lib) => {
|
|
110
|
+
if (lib.name.groupId === "org.lwjgl.lwjgl") return true;
|
|
111
|
+
return cleanroomCoords.has(`${lib.name.groupId}:${lib.name.artifactId}`);
|
|
112
|
+
};
|
|
113
|
+
const keptVanillaLibs = client.libraries.filter((l) => !isShadowedByCleanroom(l));
|
|
114
|
+
const shadowedPaths = new Set(client.libraries.filter(isShadowedByCleanroom).map((l) => `\${library_directory}/${l.artifact.path}`));
|
|
115
|
+
const installerArtifact = {
|
|
116
|
+
path: `\${library_directory}/com/cleanroommc/cleanroom/${release.tag}/${release.installerName}`,
|
|
117
|
+
source: sourceUrl(release.installerUrl),
|
|
118
|
+
size: release.installerSize,
|
|
119
|
+
rules: [],
|
|
120
|
+
...release.installerSha256 ? { integrity: { sha256: release.installerSha256 } } : {},
|
|
121
|
+
extract: [extractScan("maven/", "${library_directory}", { strip: ["maven/"] })]
|
|
122
|
+
};
|
|
123
|
+
const classpathEntries = buildClasspath([...runtimeLibs, ...keptVanillaLibs].map((l) => ({
|
|
124
|
+
rules: l.rules,
|
|
125
|
+
artifactPath: `\${library_directory}/${l.artifact.path}`
|
|
126
|
+
})), "${version_dir}/client.jar");
|
|
127
|
+
const vars = {
|
|
128
|
+
...mc.vars,
|
|
129
|
+
classpath: classpathEntries
|
|
130
|
+
};
|
|
131
|
+
const args = parseArguments(versionJson.minecraftArguments ?? versionJson.arguments ?? "");
|
|
132
|
+
const parts = buildLaunch(versionJson.mainClass, args.game, args.jvm);
|
|
133
|
+
return {
|
|
134
|
+
artifacts: [
|
|
135
|
+
...mc.artifacts.filter((a) => !shadowedPaths.has(a.path)),
|
|
136
|
+
installerArtifact,
|
|
137
|
+
...mapLibraries(downloadableRuntime),
|
|
138
|
+
...mapLibraries(downloadableInstall)
|
|
139
|
+
],
|
|
140
|
+
vars,
|
|
141
|
+
...parts
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//#endregion
|
|
146
|
+
export { resolveCleanroom, resolveCleanroomVersion };
|
|
147
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../lib/resolver.ts","../lib/template.ts"],"sourcesContent":["/**\n * Resolver for Cleanroom releases on GitHub.\n *\n * Cleanroom has no fuckforge-equivalent metadata service — releases live\n * directly on GitHub Releases, and each release ships a self-describing\n * installer JAR (with `install_profile.json` + `version.json` inside).\n *\n * Accepted version forms:\n * - Exact tag: '0.5.9-alpha'\n * - 'latest' → newest non-prerelease (none exist as of writing)\n * - 'prerelease' → newest including prereleases\n */\n\nimport { fetchWithRetry } from '@torba/core';\n\nconst DEFAULT_REPO = 'CleanroomMC/Cleanroom';\n\nexport interface CleanroomRelease {\n /** Release tag, e.g. `0.5.9-alpha`. */\n readonly tag: string;\n /** Whether the release is marked as prerelease on GitHub. */\n readonly prerelease: boolean;\n /** Direct download URL for the installer JAR. */\n readonly installerUrl: string;\n /** Installer asset filename, e.g. `cleanroom-0.5.9-alpha-installer.jar`. */\n readonly installerName: string;\n /** Installer asset size in bytes. */\n readonly installerSize: number;\n /** sha256 of the installer asset, when GitHub publishes it. Hex only. */\n readonly installerSha256?: string;\n /** ISO timestamp the release was published. */\n readonly publishedAt: string;\n}\n\nexport interface ResolveCleanroomOptions {\n /** GitHub repo `owner/name`. Default: `CleanroomMC/Cleanroom`. */\n repo?: string;\n /** Optional GitHub token for higher rate limits. */\n token?: string;\n}\n\ninterface RawRelease {\n tag_name: string;\n prerelease: boolean;\n draft: boolean;\n published_at: string;\n assets: Array<{\n name: string;\n size: number;\n browser_download_url: string;\n digest?: string;\n }>;\n}\n\nasync function fetchReleases(\n repo: string,\n token: string | undefined,\n): Promise<RawRelease[]> {\n const headers: Record<string, string> = {\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n };\n if (token) headers.Authorization = `Bearer ${token}`;\n const res = await fetchWithRetry(\n `https://api.github.com/repos/${repo}/releases?per_page=100`,\n { headers },\n );\n if (!res.ok) {\n throw new Error(\n `GitHub API ${res.status} ${res.statusText} listing ${repo} releases`,\n );\n }\n return (await res.json()) as RawRelease[];\n}\n\nfunction findInstaller(release: RawRelease): {\n url: string;\n name: string;\n size: number;\n sha256?: string;\n} | null {\n const asset = release.assets.find(\n (a) => /-installer\\.jar$/.test(a.name) && !a.name.includes('-sources'),\n );\n if (!asset) return null;\n // GitHub's `digest` field is `sha256:<hex>` when present (introduced\n // 2024). Older releases may not have it.\n const sha256 = asset.digest?.startsWith('sha256:')\n ? asset.digest.slice('sha256:'.length)\n : undefined;\n return {\n url: asset.browser_download_url,\n name: asset.name,\n size: asset.size,\n sha256,\n };\n}\n\nfunction toRelease(release: RawRelease): CleanroomRelease | null {\n const installer = findInstaller(release);\n if (!installer) return null;\n return {\n tag: release.tag_name,\n prerelease: release.prerelease,\n installerUrl: installer.url,\n installerName: installer.name,\n installerSize: installer.size,\n installerSha256: installer.sha256,\n publishedAt: release.published_at,\n };\n}\n\nexport async function resolveCleanroomVersion(\n input: string,\n options: ResolveCleanroomOptions = {},\n): Promise<CleanroomRelease> {\n const repo = options.repo ?? DEFAULT_REPO;\n const releases = (await fetchReleases(repo, options.token))\n .filter((r) => !r.draft)\n .map(toRelease)\n .filter((r): r is CleanroomRelease => r !== null);\n\n if (input === 'latest') {\n const stable = releases.find((r) => !r.prerelease);\n if (!stable) {\n throw new Error(\n `No stable Cleanroom release found in ${repo}. Try 'prerelease' or pin a specific tag.`,\n );\n }\n return stable;\n }\n if (input === 'prerelease') {\n if (releases.length === 0) {\n throw new Error(`No Cleanroom releases found in ${repo}`);\n }\n return releases[0]!;\n }\n\n const match = releases.find((r) => r.tag === input);\n if (!match) {\n throw new Error(\n `Cleanroom release '${input}' not found in ${repo}. Available: ${releases\n .slice(0, 5)\n .map((r) => r.tag)\n .join(', ')}${releases.length > 5 ? ', …' : ''}`,\n );\n }\n return match;\n}\n","import { unzipSync } from 'fflate';\nimport {\n fetchClient,\n clientToTemplate,\n buildClasspath,\n buildLaunch,\n mapLibraries,\n} from '@torba/minecraft';\nimport {\n type Artifact,\n type ValDefs,\n type Launch,\n sourceUrl,\n extractScan,\n fetchWithRetry,\n} from '@torba/core';\nimport type { Val, Valset } from '@torba/rules';\nimport {\n parseArguments,\n parseLibraries,\n type Library,\n type MojangArgValue,\n} from '@torba/mojang';\nimport {\n resolveCleanroomVersion,\n type CleanroomRelease,\n type ResolveCleanroomOptions,\n} from './resolver';\n\nexport interface CleanroomOptions {\n /**\n * Cleanroom version. Accepts:\n * - Exact tag: `'0.5.9-alpha'`\n * - `'prerelease'` — newest GitHub release (Cleanroom is currently alpha-only)\n * - `'latest'` — newest non-prerelease\n */\n version: string;\n /** GitHub repo override. Default: `CleanroomMC/Cleanroom`. */\n repo?: string;\n /** Optional GitHub token for higher rate limits while resolving releases. */\n token?: string;\n}\n\nexport interface CleanroomTemplate {\n /** Vanilla MC + Cleanroom installer + bundled cleanroom jar + runtime libraries. */\n artifacts: Artifact[];\n vars: ValDefs;\n /** Assembled Launch — drop straight into `manifest.launch`. */\n launch: Launch;\n /** JVM args alone, for composition (e.g. interleaving authliberty's `-javaagent`). */\n jvmArgs: Valset;\n /** Main class wrapped as a `Val` so it spreads into a `Valset`. Raw string at `mainClass.value[0]`. */\n mainClass: Val;\n /** Game args alone, for composition. */\n gameArgs: Valset;\n}\n\ninterface InstallerVersionJson {\n id: string;\n inheritsFrom: string;\n mainClass: string;\n minecraftArguments?: string;\n arguments?: { game?: unknown[]; jvm?: unknown[] };\n libraries: unknown[];\n}\n\ninterface InstallerProfileJson {\n spec: number;\n profile: string;\n version: string;\n minecraft: string;\n libraries: unknown[];\n}\n\nasync function fetchInstallerBytes(url: string): Promise<Uint8Array> {\n const res = await fetchWithRetry(url);\n if (!res.ok) {\n throw new Error(\n `Failed to download Cleanroom installer from ${url}: ${res.status} ${res.statusText}`,\n );\n }\n return new Uint8Array(await res.arrayBuffer());\n}\n\nfunction readJsonEntry<T>(zip: Record<string, Uint8Array>, name: string): T {\n const buf = zip[name];\n if (!buf) {\n throw new Error(`Cleanroom installer is missing entry '${name}'`);\n }\n return JSON.parse(new TextDecoder().decode(buf)) as T;\n}\n\n/**\n * Build a torba template that launches Minecraft with the Cleanroom loader.\n *\n * Cleanroom is structurally similar to legacy Forge: vanilla 1.12.2 + a custom\n * mainClass (`top.outlands.foundation.boot.Foundation`) + a legacy\n * `minecraftArguments` string + a flat library list. The installer JAR bundles\n * its own version.json + install_profile.json + a `maven/` tree containing the\n * cleanroom runtime jar (which has `url: \"\"` in version.json — sourced from\n * the `maven/` extract instead).\n */\nexport async function resolveCleanroom(\n options: CleanroomOptions,\n): Promise<CleanroomTemplate> {\n const release = await resolveCleanroomVersion(options.version, {\n repo: options.repo,\n token: options.token,\n } satisfies ResolveCleanroomOptions);\n\n const installerBytes = await fetchInstallerBytes(release.installerUrl);\n const zip = unzipSync(installerBytes, {\n filter: (f) =>\n f.name === 'version.json' || f.name === 'install_profile.json',\n });\n const versionJson = readJsonEntry<InstallerVersionJson>(zip, 'version.json');\n const installProfile = readJsonEntry<InstallerProfileJson>(\n zip,\n 'install_profile.json',\n );\n\n const vanillaId = versionJson.inheritsFrom;\n const { client } = await fetchClient(vanillaId);\n const mc = await clientToTemplate(client);\n\n const runtimeLibs = parseLibraries(versionJson.libraries);\n const installLibs = parseLibraries(installProfile.libraries);\n\n // The bundled cleanroom jar lands under `${library_directory}/<path>` via\n // the installer's `maven/` tree extraction below. Its version.json entry\n // carries `url: \"\"` for that reason — skip it in the download set so the\n // installer doesn't try to fetch an empty URL.\n const downloadableRuntime = runtimeLibs.filter((l) => l.artifact.url);\n const downloadableInstall = installLibs.filter((l) => l.artifact.url);\n\n // Cleanroom replaces most of vanilla's library set: it ships gson 2.13,\n // guava 33, lwjgl 3, log4j 2.25, etc. Vanilla 1.12.2 ships much older\n // versions of those same libraries — leaving them on the classpath wins\n // the resolution race for any class Cleanroom's version doesn't add but\n // the old version had (e.g. `org.lwjgl.Sys` from lwjgl 2.x), causing\n // Cleanroom's transforms to misfire and the JVM to load the wrong\n // implementation. Filter vanilla down to the libs Cleanroom doesn't\n // own — matches what Prism Launcher's MMC instance does via patch\n // overrides. Drop:\n // 1. Anything whose `group:artifact` overlaps with a Cleanroom lib.\n // 2. The lwjgl 2 family (`org.lwjgl.lwjgl:*`) — different group than\n // Cleanroom's `org.lwjgl:*`, so rule 1 doesn't catch it.\n const cleanroomCoords = new Set(\n runtimeLibs.map((l) => `${l.name.groupId}:${l.name.artifactId}`),\n );\n const isShadowedByCleanroom = (lib: Library): boolean => {\n if (lib.name.groupId === 'org.lwjgl.lwjgl') return true;\n return cleanroomCoords.has(`${lib.name.groupId}:${lib.name.artifactId}`);\n };\n const keptVanillaLibs = client.libraries.filter(\n (l) => !isShadowedByCleanroom(l),\n );\n const shadowedPaths = new Set(\n client.libraries\n .filter(isShadowedByCleanroom)\n .map((l) => `\\${library_directory}/${l.artifact.path}`),\n );\n\n const installerPath = `\\${library_directory}/com/cleanroommc/cleanroom/${release.tag}/${release.installerName}`;\n const installerArtifact: Artifact = {\n path: installerPath,\n source: sourceUrl(release.installerUrl),\n size: release.installerSize,\n rules: [],\n ...(release.installerSha256\n ? { integrity: { sha256: release.installerSha256 } }\n : {}),\n extract: [\n extractScan('maven/', '${library_directory}', { strip: ['maven/'] }),\n ],\n };\n\n // Classpath: vanilla client.jar + Cleanroom runtime libs + the trimmed\n // vanilla side (Mojang-only stuff: codecs, authlib, realms, text2speech,\n // java-objc-bridge). Cleanroom entries go first; the trimmed vanilla\n // tail provides classes Cleanroom doesn't ship. No module-path filter,\n // no wrapper shim — Foundation does its own bootstrap.\n const cpLibs: Library[] = [...runtimeLibs, ...keptVanillaLibs];\n const libPaths = cpLibs.map((l) => ({\n rules: l.rules as unknown[],\n artifactPath: `\\${library_directory}/${l.artifact.path}`,\n }));\n const classpathEntries = buildClasspath(\n libPaths,\n '${version_dir}/client.jar',\n );\n\n const vars: ValDefs = { ...mc.vars, classpath: classpathEntries };\n\n // Cleanroom uses the legacy `minecraftArguments` string. parseArguments\n // turns it into game args + LEGACY_JVM_ARGS (-Djava.library.path, -cp).\n // Per legacy semantics, these REPLACE vanilla's args rather than append.\n const args = parseArguments(\n versionJson.minecraftArguments ?? versionJson.arguments ?? '',\n );\n\n const parts = buildLaunch(\n versionJson.mainClass,\n args.game,\n args.jvm as MojangArgValue[],\n );\n\n // Drop the shadowed vanilla library artifacts from the download set too.\n // (mc.artifacts also carries client.jar, asset index, and asset objects —\n // those have non-library paths, so the path-based filter leaves them in.)\n const filteredMcArtifacts = mc.artifacts.filter(\n (a) => !shadowedPaths.has(a.path),\n );\n\n return {\n artifacts: [\n ...filteredMcArtifacts,\n installerArtifact,\n ...mapLibraries(downloadableRuntime),\n ...mapLibraries(downloadableInstall),\n ],\n vars,\n ...parts,\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@torba/cleanroom",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"require": "./dist/index.cjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsdown lib/index.ts --format esm,cjs --dts --clean"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@torba/minecraft": "^1.0.7",
|
|
17
|
+
"@torba/mojang": "^1.0.7",
|
|
18
|
+
"@torba/core": "^1.0.7",
|
|
19
|
+
"fflate": "^0.8.2"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"zod": "^4.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsdown": "*"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
]
|
|
30
|
+
}
|