Skip to content

Commit 3936205

Browse files
committed
feat!: merge nightly into extensions
2 parents 7a42005 + f624d6a commit 3936205

File tree

11 files changed

+326
-51
lines changed

11 files changed

+326
-51
lines changed

docs/PLAN.md

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,71 @@ A public figma file
44

55
# Plan
66

7+
## Routing
8+
9+
<details>
10+
I don't see any advantages in using a package for page routes
11+
12+
Just define a global store using `pinia` with fields like this:
13+
14+
```ts
15+
// just a mock-up, i dont remember how pinia stores look like lol
16+
{
17+
"page" : "home" // "home" | "library" | "settings" | `custom-${string}`,
18+
"setPage": (to: /* a type above */) => {
19+
for (const hook of /* window.[...].hooks */) {
20+
// handle responses
21+
const response = hook(to);
22+
// let it be { "navigate": boolean; "navigated": boolean }
23+
// 'navigate' handles whether page should change
24+
// 'navigated' handles whether link should show that page was opened
25+
}
26+
27+
this.page = to;
28+
},
29+
"states": {
30+
"home" : {},
31+
"library" : {},
32+
"settings": { "tab": "plugins" },
33+
// plugins can add their own fields
34+
};
35+
"setState": (key: string; value: object) {
36+
// handle hooks
37+
38+
this.states[key] = value;
39+
},
40+
};
41+
42+
window.__KAEDE__.router = /* pass the non-reactive object, or properties */
43+
```
44+
45+
and in the `layout.vue`:
46+
47+
```vue
48+
<script setup lang="ts">
49+
const router = useRouterStore();
50+
</script>
51+
52+
<template>
53+
<...>
54+
<home-page v-if="router.page === 'home'" />
55+
<... />
56+
<custom-page v-else />
57+
</...>
58+
</template>
59+
```
60+
61+
simple ass.
62+
63+
and!! to make custom layouts even easier to do, we can use a `<Teleport />` for each page
64+
65+
plugins will change some state for `<Teleport />` to mount a page component on the page change to somewhere
66+
67+
___
68+
69+
wait, but why am I using a global store? This can be implemented with a simple top-level state in the App.vue buh
70+
</details>
71+
772
## Extensions
873

974
<details>
@@ -18,10 +83,12 @@ Make a page that fetches extensions from some remote repository
1883

1984
That repository must include only moderated extensions. Moderation process should require extension's source code and, if differs from usual, a build manual. Every extension update must go through the moderation process. This is the only way to make custom extensions secure, I guess
2085

21-
Those extensions should be loaded once at application launch. Programming language must be a JavaScript. Any framework is ok, as long as it is capable running in a browser. Extensions need to be able to communicate with launcher
86+
Those extensions should be loaded once at application launch. Programming language must be a JavaScript. Any framework is ok, as long as it is capable running in a browser. Extensions will be able to communicate with the launcher
2287

2388
### More
2489

90+
**Previous implementation details**
91+
2592
Any JS code can be dynamically fetched in runtime from somewhere and executed with the `new Function` constructor. Example (I implemented it in [one of my projects](https://github.com/notwindstone/tsuki)):
2693

2794
```ts
@@ -44,11 +111,71 @@ Unfortunately, if VSCode, Obsidian, Vencord and other apps can't implement a sec
44111

45112
Btw, Figma chose security over functionality with their `iframe` approach
46113

47-
// 19.09.2025 update
114+
**Latest implementation details (19.09.2025)**
115+
116+
Microfrontends supremacy >.<
117+
118+
Using a Module Federation Runtime we can load any Vue component just like an ordinary component in the node tree. To make it possible for plugins to render components anywhere, a `<Teleport />` could be used. Module Federation Runtime also allows us to share dependencies, so the final extension budle size should be low.
119+
120+
For other JS frameworks, we can run them using the previous implementation: `new Function`.
121+
122+
Launcher should expose everything that it can through the `window.__KAEDE__`. Previously, communication was done using the `window.postMessage` function, but now I came up with the idea of exposing an array of functions for almost every action in app. That array can be changed by extensions, and then the launcher will execute every function listed in that array.
123+
124+
For example, see the next code:
125+
126+
```ts
127+
/** Launcher */
128+
window.postMessage("kaede-route-changed", /* some data */)
129+
130+
/** Extension */
131+
const handleRouteChange = (pathname: string) => {
132+
if (pathname === "/home") {
133+
// do something
134+
}
135+
};
136+
const handleKaedeMessages = (event: { data: string ) => {
137+
if (event.data === "tsuki_updated_window") {
138+
handleRouteChange(/* some data from the 'event', or maybe pass down the 'window.__KAEDE__.dynamic.currentRoute' variable */);
139+
140+
return;
141+
}
142+
143+
// other code
144+
};
145+
146+
window.addEventListener("message", handleKaedeMessages);
147+
```
148+
149+
While it works, it doesn't look good to me. Adding a lot of window listeners can affect launcher's performance. If user has 20 extensions, that `handleKaedeMessages` function will fire on every new event 20 times, even if no one wanted to listen to that event.
150+
151+
Now I'm suggesting the next structure:
152+
153+
```ts
154+
/** Launcher */
155+
window.__KAEDE__.hooks.onRouteChange.before = [];
156+
157+
/** Extension */
158+
const currentRoute = ref<RouteType>("home");
159+
160+
window.__KAEDE__.hooks.onRouteChange.before.push(({ pathname }: { pathname: RouteType }) => {
161+
currentRoute.value = pathname;
162+
});
163+
164+
/** Launcher */
165+
// example with the '@kitbag/router'
166+
onBeforeRouteChange(() => {
167+
for (const hook of window.__KAEDE__.hooks.onRouteChange.before) {
168+
hook({ pathname });
169+
}
170+
});
171+
```
172+
173+
Extensions are not needed to listen for window events anymore. Only specified actions will be triggered. Must be a perfect solution? Maybe. I'm not sure that this will work, because, well, launcher doesn't have the same scope as that extension code block where `window.__KAEDE__` was reassigned (?)
48174
49-
consider using runtime microfrontends. mounting components can be achieved using portals (react)/teleports (vue)
175+
Update: it works, somehow. I tested it both ways: firstly, I made a Svelte plugin state change from the Launcher (in Vue), and then I made a Vue state change from the Svelte plugin.
50176
51-
plugin manifest should also contain a `customLoader: string | undefined` field
177+
___
178+
about security: what if i introduce a permission-based plugin system? plugins will be able to use only those things (`window.__TAURI__` object scopes, localStorage, WASM, .dll/.so, etc.) that user has allowed
52179
53180
</details>
54181

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ If you found a bug or want to suggest a feature, please open an issue in [GitHub
144144

145145
### Discord
146146

147-
[![discord-banner]](https://freesmlauncher.org/discord)
147+
[![discord-banner]](https://discord.gg/zE2XcswKK7)
148148

149149
## Building from Source
150150

@@ -197,5 +197,5 @@ bun run build
197197
[vue-badge]: https://img.shields.io/badge/vuejs-%2335495e.svg?style=for-the-badge&logo=vuedotjs&logoColor=%234FC08D
198198
[tauri-badge]: https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF
199199
[unocss-badge]: https://img.shields.io/badge/unocss-333333.svg?style=for-the-badge&logo=unocss&logoColor=white
200-
[discord-banner]: https://discordapp.com/api/guilds/1332079164341354506/widget.png?style=banner3
200+
[discord-banner]: https://discordapp.com/api/guilds/1422266074908594199/widget.png?style=banner3
201201
[license-badge]: https://img.shields.io/github/license/freesmteam/kaede?style=for-the-badge

src/constants/application.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export const ConfigFilename = "config.json";
3232

3333
export const TreeResources = `${BaseDirectory.AppData}/resources`;
3434
export const TreeAssets = `${TreeResources}/assets`;
35+
export const TreeAssetObjects = `${TreeAssets}/objects`;
3536
export const TreeAssetIndexes = `${TreeAssets}/indexes`;
3637
export const TreeLibraries = `${TreeResources}/libraries`;
38+
export const TreeNatives = `${TreeResources}/natives`;
3739
export const TreeLogging = `${TreeResources}/logging`;
3840
export const TreeInstances = `${BaseDirectory.AppData}/instances`;
Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { readFile, stat } from "@tauri-apps/plugin-fs";
2-
import { ChecksumError, SizeError } from "./errors";
1+
import { readFile } from "@tauri-apps/plugin-fs";
2+
import { ChecksumError } from "../launcher/core/errors";
33

44
export async function checksum(path: string, hash: string) {
55
const file = await readFile(path);
@@ -15,12 +15,4 @@ export async function checksum(path: string, hash: string) {
1515
if (fileHash != hash) {
1616
throw new ChecksumError(path, hash, fileHash);
1717
}
18-
}
19-
20-
export async function validateFileSize(path: string, size: number) {
21-
const file = await stat(path);
22-
23-
if (file.size != size) {
24-
throw new SizeError(path, size, file.size);
25-
}
2618
}

src/lib/helpers/parse-rules.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { arch, version } from "@tauri-apps/plugin-os";
2+
import type { Rule } from "../schemas/minecrafts-schemas";
3+
import { transformPlatform } from "./transform-platform";
4+
5+
export async function evaluateRules(rules: Rule[]): Promise<boolean> {
6+
const platform = transformPlatform();
7+
const architecture = arch();
8+
const version_ = version();
9+
10+
let allow = false;
11+
12+
for (const rule of rules) {
13+
const os = rule.os;
14+
15+
const nameMatch = !os?.name || os.name === platform;
16+
const archMatch = !os?.arch || os.arch === architecture;
17+
const versionMatch = !os?.version || new RegExp(os.version).test(version_);
18+
19+
if (nameMatch && archMatch && versionMatch) {
20+
allow = rule.action === "allow";
21+
}
22+
}
23+
24+
return allow;
25+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { platform } from "@tauri-apps/plugin-os";
2+
3+
export function transformPlatform(): string {
4+
switch (platform()) {
5+
case "windows": { return "windows"; }
6+
case "linux": { return "linux"; }
7+
case "macos": { return "osx"; }
8+
default: { throw new Error(`Unknown platform passed (${platform()})`); }
9+
}
10+
}

src/lib/helpers/unzip-file.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createReadStream, createWriteStream } from "node:fs";
2+
import unzipper from "unzipper";
3+
4+
export async function unzipFile(input: string, output: string) {
5+
createReadStream(input)
6+
.pipe(unzipper.Parse())
7+
.on("entry", entry => {
8+
const fileName = entry.path;
9+
10+
if (fileName.startsWith("META-INF/")) {
11+
entry.autodrain();
12+
} else {
13+
entry.pipe(createWriteStream(`${output}/${fileName}`));
14+
}
15+
});
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { stat } from "@tauri-apps/plugin-fs";
2+
import { SizeError } from "../launcher/core/errors";
3+
4+
export async function validateFileSize(path: string, size: number) {
5+
const file = await stat(path);
6+
7+
if (file.size != size) {
8+
throw new SizeError(path, size, file.size);
9+
}
10+
}

0 commit comments

Comments
 (0)