@nextclaw/app-runtime 0.1.0
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 +21 -0
- package/README.md +50 -0
- package/dist/bridge/host-bridge.service.d.ts +13 -0
- package/dist/bridge/host-bridge.service.js +61 -0
- package/dist/commands/dev.controller.js +12 -0
- package/dist/commands/inspect.controller.js +26 -0
- package/dist/commands/run.controller.js +40 -0
- package/dist/host/app-host.service.d.ts +25 -0
- package/dist/host/app-host.service.js +66 -0
- package/dist/host/app-instance.service.d.ts +31 -0
- package/dist/host/app-instance.service.js +80 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +112 -0
- package/dist/manifest/app-manifest.service.d.ts +23 -0
- package/dist/manifest/app-manifest.service.js +139 -0
- package/dist/manifest/app-manifest.types.d.ts +61 -0
- package/dist/package.js +4 -0
- package/dist/permissions/app-permissions.service.d.ts +11 -0
- package/dist/permissions/app-permissions.service.js +40 -0
- package/dist/permissions/app-permissions.types.d.ts +26 -0
- package/dist/runtime/main-runner.service.d.ts +8 -0
- package/dist/runtime/main-runner.service.js +4 -0
- package/dist/runtime/main-runner.types.d.ts +17 -0
- package/dist/runtime/wasm-main-runner.service.d.ts +12 -0
- package/dist/runtime/wasm-main-runner.service.js +22 -0
- package/dist/sidecar/wasm-sidecar-client.service.d.ts +10 -0
- package/dist/sidecar/wasm-sidecar-client.service.js +17 -0
- package/dist/ui/ui-server.service.d.ts +14 -0
- package/dist/ui/ui-server.service.js +51 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NextClaw contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# NextClaw App Runtime
|
|
2
|
+
|
|
3
|
+
`@nextclaw/app-runtime` 是一个独立的微应用 runtime/CLI 包。
|
|
4
|
+
|
|
5
|
+
当前 MVP 提供三件事:
|
|
6
|
+
|
|
7
|
+
- `napp inspect <app-dir>`:校验应用目录与 manifest
|
|
8
|
+
- `napp run <app-dir>`:启动本地宿主,提供 UI 静态服务和桥接 API
|
|
9
|
+
- `napp dev <app-dir>`:当前等价于 `run`
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @nextclaw/app-runtime
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
安装后可用:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
napp --help
|
|
21
|
+
napp --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
第一版先聚焦“独立可运行的微应用宿主”,不接入现有 NextClaw 主产品路由、服务或传播层。
|
|
25
|
+
|
|
26
|
+
## 应用目录
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
manifest.json
|
|
30
|
+
main/
|
|
31
|
+
app.wasm
|
|
32
|
+
ui/
|
|
33
|
+
index.html
|
|
34
|
+
assets/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 当前 MVP 范围
|
|
38
|
+
|
|
39
|
+
- `main` 执行形态:`wasm`
|
|
40
|
+
- UI 装载:本地静态服务
|
|
41
|
+
- 宿主桥接:`/__napp/*`
|
|
42
|
+
- 权限词汇:`documentAccess`、`allowedDomains`、`storage`、`capabilities`
|
|
43
|
+
- 当前 Wasm 执行底座:Node 原生 `WebAssembly`
|
|
44
|
+
|
|
45
|
+
## 示例
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
napp inspect ./apps/examples/hello-notes
|
|
49
|
+
napp run ./apps/examples/hello-notes --document notes=/absolute/path/to/notes
|
|
50
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AppInstanceService } from "../host/app-instance.service.js";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
|
|
4
|
+
//#region src/bridge/host-bridge.service.d.ts
|
|
5
|
+
declare class HostBridgeServer {
|
|
6
|
+
private readonly appInstance;
|
|
7
|
+
constructor(appInstance: AppInstanceService);
|
|
8
|
+
handle: (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
9
|
+
private readJsonBody;
|
|
10
|
+
private writeJson;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { HostBridgeServer };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/bridge/host-bridge.service.ts
|
|
2
|
+
var HostBridgeServer = class {
|
|
3
|
+
constructor(appInstance) {
|
|
4
|
+
this.appInstance = appInstance;
|
|
5
|
+
}
|
|
6
|
+
handle = async (request, response) => {
|
|
7
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
8
|
+
if (!requestUrl.pathname.startsWith("/__napp")) return false;
|
|
9
|
+
if (request.method === "GET" && requestUrl.pathname === "/__napp/health") {
|
|
10
|
+
this.writeJson(response, 200, { ok: true });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (request.method === "GET" && requestUrl.pathname === "/__napp/manifest") {
|
|
14
|
+
this.writeJson(response, 200, {
|
|
15
|
+
ok: true,
|
|
16
|
+
manifest: this.appInstance.summarizeManifest()
|
|
17
|
+
});
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (request.method === "GET" && requestUrl.pathname === "/__napp/permissions") {
|
|
21
|
+
this.writeJson(response, 200, {
|
|
22
|
+
ok: true,
|
|
23
|
+
permissions: this.appInstance.summarizePermissions()
|
|
24
|
+
});
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (request.method === "POST" && requestUrl.pathname === "/__napp/run") {
|
|
28
|
+
const body = await this.readJsonBody(request);
|
|
29
|
+
const result = await this.appInstance.runAction(body.action);
|
|
30
|
+
this.writeJson(response, 200, {
|
|
31
|
+
ok: true,
|
|
32
|
+
result
|
|
33
|
+
});
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
this.writeJson(response, 404, {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: {
|
|
39
|
+
code: "NOT_FOUND",
|
|
40
|
+
message: `Unknown bridge path: ${requestUrl.pathname}`
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
readJsonBody = async (request) => {
|
|
46
|
+
const chunks = [];
|
|
47
|
+
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
48
|
+
const rawBody = Buffer.concat(chunks).toString("utf-8").trim();
|
|
49
|
+
if (!rawBody) return {};
|
|
50
|
+
return JSON.parse(rawBody);
|
|
51
|
+
};
|
|
52
|
+
writeJson = (response, statusCode, payload) => {
|
|
53
|
+
response.writeHead(statusCode, {
|
|
54
|
+
"content-type": "application/json; charset=utf-8",
|
|
55
|
+
"cache-control": "no-store"
|
|
56
|
+
});
|
|
57
|
+
response.end(JSON.stringify(payload, null, 2));
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { HostBridgeServer };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RunCommand } from "./run.controller.js";
|
|
2
|
+
//#region src/commands/dev.controller.ts
|
|
3
|
+
var DevCommand = class {
|
|
4
|
+
constructor(runCommand = new RunCommand()) {
|
|
5
|
+
this.runCommand = runCommand;
|
|
6
|
+
}
|
|
7
|
+
run = async (params) => {
|
|
8
|
+
await this.runCommand.run(params);
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { DevCommand };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
//#region src/commands/inspect.controller.ts
|
|
3
|
+
var InspectCommand = class {
|
|
4
|
+
constructor(manifestService = new AppManifestService()) {
|
|
5
|
+
this.manifestService = manifestService;
|
|
6
|
+
}
|
|
7
|
+
run = async (params) => {
|
|
8
|
+
const { appDirectory, json, write } = params;
|
|
9
|
+
const bundle = await this.manifestService.load(appDirectory);
|
|
10
|
+
const summary = this.manifestService.summarize(bundle);
|
|
11
|
+
if (json) {
|
|
12
|
+
write(`${JSON.stringify({
|
|
13
|
+
ok: true,
|
|
14
|
+
summary
|
|
15
|
+
}, null, 2)}\n`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
write(`App: ${summary.name} (${summary.id})\n`);
|
|
19
|
+
write(`Version: ${summary.version}\n`);
|
|
20
|
+
write(`Action: ${summary.action}\n`);
|
|
21
|
+
write(`Main: ${summary.mainEntryPath}\n`);
|
|
22
|
+
write(`UI: ${summary.uiEntryPath}\n`);
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { InspectCommand };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AppHostService } from "../host/app-host.service.js";
|
|
2
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
3
|
+
import { AppInstanceService } from "../host/app-instance.service.js";
|
|
4
|
+
//#region src/commands/run.controller.ts
|
|
5
|
+
var RunCommand = class {
|
|
6
|
+
constructor(manifestService = new AppManifestService()) {
|
|
7
|
+
this.manifestService = manifestService;
|
|
8
|
+
}
|
|
9
|
+
run = async (params) => {
|
|
10
|
+
const { appDirectory, host, port, json, documentGrantMap, write } = params;
|
|
11
|
+
const bundle = await this.manifestService.load(appDirectory);
|
|
12
|
+
const appInstance = new AppInstanceService(bundle);
|
|
13
|
+
await appInstance.initialize(documentGrantMap);
|
|
14
|
+
const appHost = new AppHostService(appInstance);
|
|
15
|
+
const hostHandle = await appHost.start({
|
|
16
|
+
host,
|
|
17
|
+
port
|
|
18
|
+
});
|
|
19
|
+
const shutdown = async () => {
|
|
20
|
+
await appHost.stop();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
};
|
|
23
|
+
process.once("SIGINT", () => {
|
|
24
|
+
shutdown();
|
|
25
|
+
});
|
|
26
|
+
process.once("SIGTERM", () => {
|
|
27
|
+
shutdown();
|
|
28
|
+
});
|
|
29
|
+
if (json) {
|
|
30
|
+
write(`${JSON.stringify({
|
|
31
|
+
ok: true,
|
|
32
|
+
host: hostHandle
|
|
33
|
+
}, null, 2)}\n`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
write(`[napp] ${bundle.manifest.name} listening on ${hostHandle.url}\n`);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { RunCommand };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AppInstanceService } from "./app-instance.service.js";
|
|
2
|
+
|
|
3
|
+
//#region src/host/app-host.service.d.ts
|
|
4
|
+
type AppHostStartOptions = {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
};
|
|
8
|
+
type AppHostHandle = {
|
|
9
|
+
appId: string;
|
|
10
|
+
url: string;
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
};
|
|
14
|
+
declare class AppHostService {
|
|
15
|
+
private readonly appInstance;
|
|
16
|
+
private server?;
|
|
17
|
+
private readonly bridgeServer;
|
|
18
|
+
private readonly uiServer;
|
|
19
|
+
constructor(appInstance: AppInstanceService);
|
|
20
|
+
start: (options: AppHostStartOptions) => Promise<AppHostHandle>;
|
|
21
|
+
stop: () => Promise<void>;
|
|
22
|
+
private handleRequest;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { AppHostHandle, AppHostService, AppHostStartOptions };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HostBridgeServer } from "../bridge/host-bridge.service.js";
|
|
2
|
+
import { UiServerService } from "../ui/ui-server.service.js";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
//#region src/host/app-host.service.ts
|
|
5
|
+
var AppHostService = class {
|
|
6
|
+
server;
|
|
7
|
+
bridgeServer;
|
|
8
|
+
uiServer;
|
|
9
|
+
constructor(appInstance) {
|
|
10
|
+
this.appInstance = appInstance;
|
|
11
|
+
this.bridgeServer = new HostBridgeServer(appInstance);
|
|
12
|
+
this.uiServer = new UiServerService(appInstance.bundle);
|
|
13
|
+
}
|
|
14
|
+
start = async (options) => {
|
|
15
|
+
if (this.server) throw new Error("应用宿主已经启动。");
|
|
16
|
+
const server = createServer((request, response) => {
|
|
17
|
+
this.handleRequest(request, response);
|
|
18
|
+
});
|
|
19
|
+
this.server = server;
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
server.once("error", reject);
|
|
22
|
+
server.listen(options.port, options.host, () => {
|
|
23
|
+
server.off("error", reject);
|
|
24
|
+
resolve();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
const address = server.address();
|
|
28
|
+
if (!address || typeof address === "string") throw new Error("无法获取应用宿主监听地址。");
|
|
29
|
+
return {
|
|
30
|
+
appId: this.appInstance.bundle.manifest.id,
|
|
31
|
+
url: `http://${address.address}:${address.port}`,
|
|
32
|
+
host: address.address,
|
|
33
|
+
port: address.port
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
stop = async () => {
|
|
37
|
+
if (!this.server) return;
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
this.server?.close((error) => {
|
|
40
|
+
if (error) {
|
|
41
|
+
reject(error);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
resolve();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
this.server = void 0;
|
|
48
|
+
};
|
|
49
|
+
handleRequest = async (request, response) => {
|
|
50
|
+
try {
|
|
51
|
+
if (await this.bridgeServer.handle(request, response)) return;
|
|
52
|
+
await this.uiServer.handle(request, response);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
response.writeHead(500, { "content-type": "application/json; charset=utf-8" });
|
|
55
|
+
response.end(JSON.stringify({
|
|
56
|
+
ok: false,
|
|
57
|
+
error: {
|
|
58
|
+
code: "APP_HOST_ERROR",
|
|
59
|
+
message: error instanceof Error ? error.message : String(error)
|
|
60
|
+
}
|
|
61
|
+
}, null, 2));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
export { AppHostService };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AppManifestBundle, AppManifestSummary } from "../manifest/app-manifest.types.js";
|
|
2
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
3
|
+
import { AppDocumentGrantMap, AppPermissionSummary } from "../permissions/app-permissions.types.js";
|
|
4
|
+
import { AppPermissionsService } from "../permissions/app-permissions.service.js";
|
|
5
|
+
import { MainRunResult, WasmDocumentSummaryInput } from "../runtime/main-runner.types.js";
|
|
6
|
+
import { WasmMainRunnerService } from "../runtime/wasm-main-runner.service.js";
|
|
7
|
+
|
|
8
|
+
//#region src/host/app-instance.service.d.ts
|
|
9
|
+
type AppRunResult = {
|
|
10
|
+
action: string;
|
|
11
|
+
input: WasmDocumentSummaryInput;
|
|
12
|
+
output: MainRunResult;
|
|
13
|
+
observedAt: string;
|
|
14
|
+
};
|
|
15
|
+
declare class AppInstanceService {
|
|
16
|
+
readonly bundle: AppManifestBundle;
|
|
17
|
+
private readonly permissionsService;
|
|
18
|
+
private readonly mainRunner;
|
|
19
|
+
private readonly manifestService;
|
|
20
|
+
private permissions?;
|
|
21
|
+
constructor(bundle: AppManifestBundle, permissionsService?: AppPermissionsService, mainRunner?: WasmMainRunnerService, manifestService?: AppManifestService);
|
|
22
|
+
initialize: (documentGrantMap: AppDocumentGrantMap) => Promise<void>;
|
|
23
|
+
summarizeManifest: () => AppManifestSummary;
|
|
24
|
+
summarizePermissions: () => AppPermissionSummary;
|
|
25
|
+
runAction: (action?: string) => Promise<AppRunResult>;
|
|
26
|
+
private collectDocumentSummaryInput;
|
|
27
|
+
private collectDirectorySummary;
|
|
28
|
+
private getPermissions;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { AppInstanceService, AppRunResult };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { AppManifestService } from "../manifest/app-manifest.service.js";
|
|
2
|
+
import { AppPermissionsService } from "../permissions/app-permissions.service.js";
|
|
3
|
+
import { WasmMainRunnerService } from "../runtime/wasm-main-runner.service.js";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/host/app-instance.service.ts
|
|
7
|
+
var AppInstanceService = class {
|
|
8
|
+
permissions;
|
|
9
|
+
constructor(bundle, permissionsService = new AppPermissionsService(), mainRunner = new WasmMainRunnerService(), manifestService = new AppManifestService()) {
|
|
10
|
+
this.bundle = bundle;
|
|
11
|
+
this.permissionsService = permissionsService;
|
|
12
|
+
this.mainRunner = mainRunner;
|
|
13
|
+
this.manifestService = manifestService;
|
|
14
|
+
}
|
|
15
|
+
initialize = async (documentGrantMap) => {
|
|
16
|
+
this.permissions = await this.permissionsService.resolve(this.bundle, documentGrantMap);
|
|
17
|
+
};
|
|
18
|
+
summarizeManifest = () => {
|
|
19
|
+
return this.manifestService.summarize(this.bundle);
|
|
20
|
+
};
|
|
21
|
+
summarizePermissions = () => {
|
|
22
|
+
return this.permissionsService.summarize(this.bundle, this.getPermissions());
|
|
23
|
+
};
|
|
24
|
+
runAction = async (action) => {
|
|
25
|
+
const expectedAction = this.bundle.manifest.main.action;
|
|
26
|
+
if (action && action !== expectedAction) throw new Error(`当前应用只支持动作 ${expectedAction}。`);
|
|
27
|
+
const input = await this.collectDocumentSummaryInput();
|
|
28
|
+
return {
|
|
29
|
+
action: expectedAction,
|
|
30
|
+
input,
|
|
31
|
+
output: await this.mainRunner.runDocumentSummary({
|
|
32
|
+
bundle: this.bundle,
|
|
33
|
+
input
|
|
34
|
+
}),
|
|
35
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
collectDocumentSummaryInput = async () => {
|
|
39
|
+
const permissions = this.getPermissions();
|
|
40
|
+
let documentCount = 0;
|
|
41
|
+
let textBytes = 0;
|
|
42
|
+
for (const grant of permissions.documentAccess) {
|
|
43
|
+
const scopeSummary = await this.collectDirectorySummary(grant.path);
|
|
44
|
+
documentCount += scopeSummary.documentCount;
|
|
45
|
+
textBytes += scopeSummary.textBytes;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
documentCount,
|
|
49
|
+
textBytes
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
collectDirectorySummary = async (directoryPath) => {
|
|
53
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
54
|
+
let documentCount = 0;
|
|
55
|
+
let textBytes = 0;
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
const childSummary = await this.collectDirectorySummary(entryPath);
|
|
60
|
+
documentCount += childSummary.documentCount;
|
|
61
|
+
textBytes += childSummary.textBytes;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!entry.isFile()) continue;
|
|
65
|
+
const content = await readFile(entryPath, "utf-8");
|
|
66
|
+
documentCount += 1;
|
|
67
|
+
textBytes += Buffer.byteLength(content, "utf-8");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
documentCount,
|
|
71
|
+
textBytes
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
getPermissions = () => {
|
|
75
|
+
if (!this.permissions) throw new Error("应用实例尚未初始化权限。");
|
|
76
|
+
return this.permissions;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export { AppInstanceService };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AppDocumentAccessMode, AppDocumentAccessScope, AppMainManifest, AppManifest, AppManifestBundle, AppManifestSummary, AppPermissions, AppUiManifest } from "./manifest/app-manifest.types.js";
|
|
2
|
+
import { AppManifestService } from "./manifest/app-manifest.service.js";
|
|
3
|
+
import { AppDocumentGrantMap, AppPermissionSummary, ResolvedDocumentGrant, ResolvedPermissions } from "./permissions/app-permissions.types.js";
|
|
4
|
+
import { AppPermissionsService } from "./permissions/app-permissions.service.js";
|
|
5
|
+
import { MainRunRequest, MainRunResult, WasmDocumentSummaryInput } from "./runtime/main-runner.types.js";
|
|
6
|
+
import { MainRunnerService } from "./runtime/main-runner.service.js";
|
|
7
|
+
import { WasmSidecarClientService } from "./sidecar/wasm-sidecar-client.service.js";
|
|
8
|
+
import { WasmMainRunnerService } from "./runtime/wasm-main-runner.service.js";
|
|
9
|
+
import { AppInstanceService, AppRunResult } from "./host/app-instance.service.js";
|
|
10
|
+
import { HostBridgeServer } from "./bridge/host-bridge.service.js";
|
|
11
|
+
import { AppHostHandle, AppHostService, AppHostStartOptions } from "./host/app-host.service.js";
|
|
12
|
+
import { UiServerService } from "./ui/ui-server.service.js";
|
|
13
|
+
export { AppDocumentAccessMode, AppDocumentAccessScope, AppDocumentGrantMap, AppHostHandle, AppHostService, AppHostStartOptions, AppInstanceService, AppMainManifest, AppManifest, AppManifestBundle, AppManifestService, AppManifestSummary, AppPermissionSummary, AppPermissions, AppPermissionsService, AppRunResult, AppUiManifest, HostBridgeServer, MainRunRequest, MainRunResult, MainRunnerService, ResolvedDocumentGrant, ResolvedPermissions, UiServerService, WasmDocumentSummaryInput, WasmMainRunnerService, WasmSidecarClientService };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { HostBridgeServer } from "./bridge/host-bridge.service.js";
|
|
2
|
+
import { UiServerService } from "./ui/ui-server.service.js";
|
|
3
|
+
import { AppHostService } from "./host/app-host.service.js";
|
|
4
|
+
import { AppManifestService } from "./manifest/app-manifest.service.js";
|
|
5
|
+
import { AppPermissionsService } from "./permissions/app-permissions.service.js";
|
|
6
|
+
import { MainRunnerService } from "./runtime/main-runner.service.js";
|
|
7
|
+
import { WasmSidecarClientService } from "./sidecar/wasm-sidecar-client.service.js";
|
|
8
|
+
import { WasmMainRunnerService } from "./runtime/wasm-main-runner.service.js";
|
|
9
|
+
import { AppInstanceService } from "./host/app-instance.service.js";
|
|
10
|
+
export { AppHostService, AppInstanceService, AppManifestService, AppPermissionsService, HostBridgeServer, MainRunnerService, UiServerService, WasmMainRunnerService, WasmSidecarClientService };
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { version } from "./package.js";
|
|
3
|
+
import { RunCommand } from "./commands/run.controller.js";
|
|
4
|
+
import { DevCommand } from "./commands/dev.controller.js";
|
|
5
|
+
import { InspectCommand } from "./commands/inspect.controller.js";
|
|
6
|
+
//#region src/main.ts
|
|
7
|
+
var AppRuntimeCli = class {
|
|
8
|
+
run = async () => {
|
|
9
|
+
const [command, appDirectory, ...restArgs] = process.argv.slice(2);
|
|
10
|
+
if (command === "--help" || command === "help") {
|
|
11
|
+
this.writeUsage();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (command === "--version" || command === "version") {
|
|
15
|
+
this.write(`${version}\n`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!command || !appDirectory) {
|
|
19
|
+
this.writeUsage();
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const options = this.readOptions(restArgs);
|
|
23
|
+
if (command === "inspect") {
|
|
24
|
+
await new InspectCommand().run({
|
|
25
|
+
appDirectory,
|
|
26
|
+
json: options.json,
|
|
27
|
+
write: this.write
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (command === "run") {
|
|
32
|
+
await new RunCommand().run({
|
|
33
|
+
appDirectory,
|
|
34
|
+
host: options.host,
|
|
35
|
+
port: options.port,
|
|
36
|
+
json: options.json,
|
|
37
|
+
documentGrantMap: options.documentGrantMap,
|
|
38
|
+
write: this.write
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (command === "dev") {
|
|
43
|
+
await new DevCommand().run({
|
|
44
|
+
appDirectory,
|
|
45
|
+
host: options.host,
|
|
46
|
+
port: options.port,
|
|
47
|
+
json: options.json,
|
|
48
|
+
documentGrantMap: options.documentGrantMap,
|
|
49
|
+
write: this.write
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.writeUsage();
|
|
54
|
+
process.exit(1);
|
|
55
|
+
};
|
|
56
|
+
readOptions = (rawArgs) => {
|
|
57
|
+
const options = {
|
|
58
|
+
host: "127.0.0.1",
|
|
59
|
+
port: 3100,
|
|
60
|
+
json: false,
|
|
61
|
+
documentGrantMap: {}
|
|
62
|
+
};
|
|
63
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
64
|
+
const current = rawArgs[index];
|
|
65
|
+
if (!current?.startsWith("--")) continue;
|
|
66
|
+
const nextValue = rawArgs[index + 1];
|
|
67
|
+
switch (current) {
|
|
68
|
+
case "--host":
|
|
69
|
+
options.host = this.requireOptionValue(current, nextValue);
|
|
70
|
+
index += 1;
|
|
71
|
+
break;
|
|
72
|
+
case "--port":
|
|
73
|
+
options.port = Number.parseInt(this.requireOptionValue(current, nextValue), 10);
|
|
74
|
+
if (Number.isNaN(options.port) || options.port < 0) throw new Error("--port 必须是非负整数。");
|
|
75
|
+
index += 1;
|
|
76
|
+
break;
|
|
77
|
+
case "--json":
|
|
78
|
+
options.json = true;
|
|
79
|
+
break;
|
|
80
|
+
case "--document":
|
|
81
|
+
this.assignDocumentGrant(options.documentGrantMap, this.requireOptionValue(current, nextValue));
|
|
82
|
+
index += 1;
|
|
83
|
+
break;
|
|
84
|
+
default: throw new Error(`未知参数:${current}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return options;
|
|
88
|
+
};
|
|
89
|
+
assignDocumentGrant = (documentGrantMap, rawGrant) => {
|
|
90
|
+
const delimiterIndex = rawGrant.indexOf("=");
|
|
91
|
+
if (delimiterIndex < 1) throw new Error("--document 必须使用 scopeId=/absolute/or/relative/path 格式。");
|
|
92
|
+
const scopeId = rawGrant.slice(0, delimiterIndex).trim();
|
|
93
|
+
const directoryPath = rawGrant.slice(delimiterIndex + 1).trim();
|
|
94
|
+
if (!scopeId || !directoryPath) throw new Error("--document 缺少 scopeId 或 path。");
|
|
95
|
+
documentGrantMap[scopeId] = directoryPath;
|
|
96
|
+
};
|
|
97
|
+
requireOptionValue = (flag, nextValue) => {
|
|
98
|
+
if (!nextValue || nextValue.startsWith("--")) throw new Error(`${flag} 缺少值。`);
|
|
99
|
+
return nextValue;
|
|
100
|
+
};
|
|
101
|
+
writeUsage = () => {
|
|
102
|
+
this.write("Usage: napp <inspect|run|dev> <app-dir> [--host 127.0.0.1] [--port 3100] [--json] [--document scope=/path]\n");
|
|
103
|
+
this.write(" napp --help\n");
|
|
104
|
+
this.write(" napp --version\n");
|
|
105
|
+
};
|
|
106
|
+
write = (text) => {
|
|
107
|
+
process.stdout.write(text);
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
new AppRuntimeCli().run();
|
|
111
|
+
//#endregion
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppManifestBundle, AppManifestSummary } from "./app-manifest.types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/manifest/app-manifest.service.d.ts
|
|
4
|
+
declare class AppManifestService {
|
|
5
|
+
load: (appDirectory: string) => Promise<AppManifestBundle>;
|
|
6
|
+
summarize: (bundle: AppManifestBundle) => AppManifestSummary;
|
|
7
|
+
private parseManifest;
|
|
8
|
+
private parseMain;
|
|
9
|
+
private parseUi;
|
|
10
|
+
private parsePermissions;
|
|
11
|
+
private parseDocumentAccess;
|
|
12
|
+
private parseStringArray;
|
|
13
|
+
private parseStorage;
|
|
14
|
+
private parseCapabilities;
|
|
15
|
+
private assertResolvedFile;
|
|
16
|
+
private assertObject;
|
|
17
|
+
private readRequiredString;
|
|
18
|
+
private readOptionalString;
|
|
19
|
+
private readNumber;
|
|
20
|
+
private readBoolean;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { AppManifestService };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/manifest/app-manifest.service.ts
|
|
4
|
+
var AppManifestService = class {
|
|
5
|
+
load = async (appDirectory) => {
|
|
6
|
+
const normalizedDirectory = path.resolve(appDirectory);
|
|
7
|
+
const manifestPath = path.join(normalizedDirectory, "manifest.json");
|
|
8
|
+
const rawManifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
9
|
+
const manifest = this.parseManifest(rawManifest);
|
|
10
|
+
const mainEntryPath = await this.assertResolvedFile(normalizedDirectory, manifest.main.entry, "main.entry");
|
|
11
|
+
const uiEntryPath = await this.assertResolvedFile(normalizedDirectory, manifest.ui.entry, "ui.entry");
|
|
12
|
+
return {
|
|
13
|
+
appDirectory: normalizedDirectory,
|
|
14
|
+
manifestPath,
|
|
15
|
+
manifest,
|
|
16
|
+
mainEntryPath,
|
|
17
|
+
uiEntryPath,
|
|
18
|
+
uiDirectoryPath: path.dirname(uiEntryPath),
|
|
19
|
+
assetsDirectoryPath: path.join(normalizedDirectory, "assets"),
|
|
20
|
+
iconPath: manifest.icon ? await this.assertResolvedFile(normalizedDirectory, manifest.icon, "icon") : void 0
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
summarize = (bundle) => {
|
|
24
|
+
const permissions = bundle.manifest.permissions ?? {};
|
|
25
|
+
return {
|
|
26
|
+
id: bundle.manifest.id,
|
|
27
|
+
name: bundle.manifest.name,
|
|
28
|
+
version: bundle.manifest.version,
|
|
29
|
+
description: bundle.manifest.description,
|
|
30
|
+
action: bundle.manifest.main.action,
|
|
31
|
+
manifestPath: bundle.manifestPath,
|
|
32
|
+
mainEntryPath: bundle.mainEntryPath,
|
|
33
|
+
uiEntryPath: bundle.uiEntryPath,
|
|
34
|
+
iconPath: bundle.iconPath,
|
|
35
|
+
permissions
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
parseManifest = (rawManifest) => {
|
|
39
|
+
if (!rawManifest || typeof rawManifest !== "object" || Array.isArray(rawManifest)) throw new Error("manifest.json 必须是一个对象。");
|
|
40
|
+
const candidate = rawManifest;
|
|
41
|
+
if (this.readNumber(candidate.schemaVersion, "schemaVersion") !== 1) throw new Error("当前只支持 schemaVersion = 1。");
|
|
42
|
+
const permissions = this.parsePermissions(candidate.permissions);
|
|
43
|
+
return {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
id: this.readRequiredString(candidate.id, "id"),
|
|
46
|
+
name: this.readRequiredString(candidate.name, "name"),
|
|
47
|
+
version: this.readRequiredString(candidate.version, "version"),
|
|
48
|
+
description: this.readOptionalString(candidate.description, "description"),
|
|
49
|
+
icon: this.readOptionalString(candidate.icon, "icon"),
|
|
50
|
+
main: this.parseMain(candidate.main),
|
|
51
|
+
ui: this.parseUi(candidate.ui),
|
|
52
|
+
permissions
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
parseMain = (rawMain) => {
|
|
56
|
+
const candidate = this.assertObject(rawMain, "main");
|
|
57
|
+
if (this.readRequiredString(candidate.kind, "main.kind") !== "wasm") throw new Error("当前 main.kind 只支持 wasm。");
|
|
58
|
+
return {
|
|
59
|
+
kind: "wasm",
|
|
60
|
+
entry: this.readRequiredString(candidate.entry, "main.entry"),
|
|
61
|
+
export: this.readRequiredString(candidate.export, "main.export"),
|
|
62
|
+
action: this.readRequiredString(candidate.action, "main.action")
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
parseUi = (rawUi) => {
|
|
66
|
+
const candidate = this.assertObject(rawUi, "ui");
|
|
67
|
+
return { entry: this.readRequiredString(candidate.entry, "ui.entry") };
|
|
68
|
+
};
|
|
69
|
+
parsePermissions = (rawPermissions) => {
|
|
70
|
+
if (rawPermissions === void 0) return;
|
|
71
|
+
const candidate = this.assertObject(rawPermissions, "permissions");
|
|
72
|
+
return {
|
|
73
|
+
documentAccess: this.parseDocumentAccess(candidate.documentAccess),
|
|
74
|
+
allowedDomains: this.parseStringArray(candidate.allowedDomains, "permissions.allowedDomains"),
|
|
75
|
+
storage: this.parseStorage(candidate.storage),
|
|
76
|
+
capabilities: this.parseCapabilities(candidate.capabilities)
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
parseDocumentAccess = (rawDocumentAccess) => {
|
|
80
|
+
if (rawDocumentAccess === void 0) return;
|
|
81
|
+
if (!Array.isArray(rawDocumentAccess)) throw new Error("permissions.documentAccess 必须是数组。");
|
|
82
|
+
return rawDocumentAccess.map((scope, index) => {
|
|
83
|
+
const candidate = this.assertObject(scope, `permissions.documentAccess[${index}]`);
|
|
84
|
+
const mode = this.readRequiredString(candidate.mode, `permissions.documentAccess[${index}].mode`);
|
|
85
|
+
if (mode !== "read" && mode !== "read-write") throw new Error(`permissions.documentAccess[${index}].mode 只支持 read 或 read-write。`);
|
|
86
|
+
return {
|
|
87
|
+
id: this.readRequiredString(candidate.id, `permissions.documentAccess[${index}].id`),
|
|
88
|
+
mode,
|
|
89
|
+
description: this.readOptionalString(candidate.description, `permissions.documentAccess[${index}].description`)
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
parseStringArray = (rawValue, fieldName) => {
|
|
94
|
+
if (rawValue === void 0) return;
|
|
95
|
+
if (!Array.isArray(rawValue)) throw new Error(`${fieldName} 必须是字符串数组。`);
|
|
96
|
+
return rawValue.map((item, index) => this.readRequiredString(item, `${fieldName}[${index}]`));
|
|
97
|
+
};
|
|
98
|
+
parseStorage = (rawStorage) => {
|
|
99
|
+
if (rawStorage === void 0) return;
|
|
100
|
+
if (typeof rawStorage === "boolean") return rawStorage;
|
|
101
|
+
const candidate = this.assertObject(rawStorage, "permissions.storage");
|
|
102
|
+
return { namespace: this.readOptionalString(candidate.namespace, "permissions.storage.namespace") };
|
|
103
|
+
};
|
|
104
|
+
parseCapabilities = (rawCapabilities) => {
|
|
105
|
+
if (rawCapabilities === void 0) return;
|
|
106
|
+
const candidate = this.assertObject(rawCapabilities, "permissions.capabilities");
|
|
107
|
+
return { hostBridge: candidate.hostBridge === void 0 ? void 0 : this.readBoolean(candidate.hostBridge, "permissions.capabilities.hostBridge") };
|
|
108
|
+
};
|
|
109
|
+
assertResolvedFile = async (appDirectory, relativePath, fieldName) => {
|
|
110
|
+
if (path.isAbsolute(relativePath)) throw new Error(`${fieldName} 必须使用应用目录内的相对路径。`);
|
|
111
|
+
const resolvedPath = path.resolve(appDirectory, relativePath);
|
|
112
|
+
const relative = path.relative(appDirectory, resolvedPath);
|
|
113
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`${fieldName} 不能指向应用目录之外。`);
|
|
114
|
+
await access(resolvedPath);
|
|
115
|
+
return resolvedPath;
|
|
116
|
+
};
|
|
117
|
+
assertObject = (value, fieldName) => {
|
|
118
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${fieldName} 必须是对象。`);
|
|
119
|
+
return value;
|
|
120
|
+
};
|
|
121
|
+
readRequiredString = (value, fieldName) => {
|
|
122
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${fieldName} 必须是非空字符串。`);
|
|
123
|
+
return value.trim();
|
|
124
|
+
};
|
|
125
|
+
readOptionalString = (value, fieldName) => {
|
|
126
|
+
if (value === void 0) return;
|
|
127
|
+
return this.readRequiredString(value, fieldName);
|
|
128
|
+
};
|
|
129
|
+
readNumber = (value, fieldName) => {
|
|
130
|
+
if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`${fieldName} 必须是数字。`);
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
readBoolean = (value, fieldName) => {
|
|
134
|
+
if (typeof value !== "boolean") throw new Error(`${fieldName} 必须是布尔值。`);
|
|
135
|
+
return value;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
export { AppManifestService };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/manifest/app-manifest.types.d.ts
|
|
2
|
+
type AppDocumentAccessMode = "read" | "read-write";
|
|
3
|
+
type AppDocumentAccessScope = {
|
|
4
|
+
id: string;
|
|
5
|
+
mode: AppDocumentAccessMode;
|
|
6
|
+
description?: string;
|
|
7
|
+
};
|
|
8
|
+
type AppPermissions = {
|
|
9
|
+
documentAccess?: AppDocumentAccessScope[];
|
|
10
|
+
allowedDomains?: string[];
|
|
11
|
+
storage?: boolean | {
|
|
12
|
+
namespace?: string;
|
|
13
|
+
};
|
|
14
|
+
capabilities?: {
|
|
15
|
+
hostBridge?: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
type AppMainManifest = {
|
|
19
|
+
kind: "wasm";
|
|
20
|
+
entry: string;
|
|
21
|
+
export: string;
|
|
22
|
+
action: string;
|
|
23
|
+
};
|
|
24
|
+
type AppUiManifest = {
|
|
25
|
+
entry: string;
|
|
26
|
+
};
|
|
27
|
+
type AppManifest = {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
icon?: string;
|
|
34
|
+
main: AppMainManifest;
|
|
35
|
+
ui: AppUiManifest;
|
|
36
|
+
permissions?: AppPermissions;
|
|
37
|
+
};
|
|
38
|
+
type AppManifestBundle = {
|
|
39
|
+
appDirectory: string;
|
|
40
|
+
manifestPath: string;
|
|
41
|
+
manifest: AppManifest;
|
|
42
|
+
mainEntryPath: string;
|
|
43
|
+
uiEntryPath: string;
|
|
44
|
+
uiDirectoryPath: string;
|
|
45
|
+
assetsDirectoryPath: string;
|
|
46
|
+
iconPath?: string;
|
|
47
|
+
};
|
|
48
|
+
type AppManifestSummary = {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
version: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
action: string;
|
|
54
|
+
manifestPath: string;
|
|
55
|
+
mainEntryPath: string;
|
|
56
|
+
uiEntryPath: string;
|
|
57
|
+
iconPath?: string;
|
|
58
|
+
permissions: AppPermissions;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { AppDocumentAccessMode, AppDocumentAccessScope, AppMainManifest, AppManifest, AppManifestBundle, AppManifestSummary, AppPermissions, AppUiManifest };
|
package/dist/package.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AppManifestBundle } from "../manifest/app-manifest.types.js";
|
|
2
|
+
import { AppDocumentGrantMap, AppPermissionSummary, ResolvedPermissions } from "./app-permissions.types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/permissions/app-permissions.service.d.ts
|
|
5
|
+
declare class AppPermissionsService {
|
|
6
|
+
resolve: (bundle: AppManifestBundle, documentGrantMap: AppDocumentGrantMap) => Promise<ResolvedPermissions>;
|
|
7
|
+
summarize: (bundle: AppManifestBundle, permissions: ResolvedPermissions) => AppPermissionSummary;
|
|
8
|
+
private resolveDocumentGrant;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { AppPermissionsService };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/permissions/app-permissions.service.ts
|
|
4
|
+
var AppPermissionsService = class {
|
|
5
|
+
resolve = async (bundle, documentGrantMap) => {
|
|
6
|
+
const requestedPermissions = bundle.manifest.permissions ?? {};
|
|
7
|
+
return {
|
|
8
|
+
documentAccess: requestedPermissions.documentAccess === void 0 ? [] : await Promise.all(requestedPermissions.documentAccess.map((scope) => this.resolveDocumentGrant(scope, documentGrantMap))),
|
|
9
|
+
allowedDomains: requestedPermissions.allowedDomains ?? [],
|
|
10
|
+
storage: {
|
|
11
|
+
enabled: requestedPermissions.storage !== void 0 && requestedPermissions.storage !== false,
|
|
12
|
+
namespace: typeof requestedPermissions.storage === "object" ? requestedPermissions.storage.namespace : void 0
|
|
13
|
+
},
|
|
14
|
+
capabilities: { hostBridge: requestedPermissions.capabilities?.hostBridge !== false }
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
summarize = (bundle, permissions) => {
|
|
18
|
+
return {
|
|
19
|
+
requested: bundle.manifest.permissions ?? {},
|
|
20
|
+
documentAccess: permissions.documentAccess,
|
|
21
|
+
allowedDomains: permissions.allowedDomains,
|
|
22
|
+
storage: permissions.storage,
|
|
23
|
+
capabilities: permissions.capabilities
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
resolveDocumentGrant = async (scope, documentGrantMap) => {
|
|
27
|
+
const grantedPath = documentGrantMap[scope.id];
|
|
28
|
+
if (!grantedPath) throw new Error(`缺少 documentAccess 授权:${scope.id}`);
|
|
29
|
+
const normalizedPath = path.resolve(grantedPath);
|
|
30
|
+
await access(normalizedPath);
|
|
31
|
+
return {
|
|
32
|
+
id: scope.id,
|
|
33
|
+
mode: scope.mode,
|
|
34
|
+
description: scope.description,
|
|
35
|
+
path: normalizedPath
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { AppPermissionsService };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AppPermissions } from "../manifest/app-manifest.types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/permissions/app-permissions.types.d.ts
|
|
4
|
+
type AppDocumentGrantMap = Record<string, string>;
|
|
5
|
+
type ResolvedDocumentGrant = {
|
|
6
|
+
id: string;
|
|
7
|
+
mode: "read" | "read-write";
|
|
8
|
+
description?: string;
|
|
9
|
+
path: string;
|
|
10
|
+
};
|
|
11
|
+
type ResolvedPermissions = {
|
|
12
|
+
documentAccess: ResolvedDocumentGrant[];
|
|
13
|
+
allowedDomains: string[];
|
|
14
|
+
storage: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
namespace?: string;
|
|
17
|
+
};
|
|
18
|
+
capabilities: {
|
|
19
|
+
hostBridge: boolean;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type AppPermissionSummary = Pick<ResolvedPermissions, "documentAccess" | "allowedDomains" | "storage" | "capabilities"> & {
|
|
23
|
+
requested: AppPermissions;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { AppDocumentGrantMap, AppPermissionSummary, ResolvedDocumentGrant, ResolvedPermissions };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MainRunRequest, MainRunResult } from "./main-runner.types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/main-runner.service.d.ts
|
|
4
|
+
declare abstract class MainRunnerService {
|
|
5
|
+
abstract runDocumentSummary(request: MainRunRequest): Promise<MainRunResult>;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { MainRunnerService };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AppManifestBundle } from "../manifest/app-manifest.types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/main-runner.types.d.ts
|
|
4
|
+
type WasmDocumentSummaryInput = {
|
|
5
|
+
documentCount: number;
|
|
6
|
+
textBytes: number;
|
|
7
|
+
};
|
|
8
|
+
type MainRunRequest = {
|
|
9
|
+
bundle: AppManifestBundle;
|
|
10
|
+
input: WasmDocumentSummaryInput;
|
|
11
|
+
};
|
|
12
|
+
type MainRunResult = {
|
|
13
|
+
exportName: string;
|
|
14
|
+
output: number;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { MainRunRequest, MainRunResult, WasmDocumentSummaryInput };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MainRunRequest, MainRunResult } from "./main-runner.types.js";
|
|
2
|
+
import { MainRunnerService } from "./main-runner.service.js";
|
|
3
|
+
import { WasmSidecarClientService } from "../sidecar/wasm-sidecar-client.service.js";
|
|
4
|
+
|
|
5
|
+
//#region src/runtime/wasm-main-runner.service.d.ts
|
|
6
|
+
declare class WasmMainRunnerService extends MainRunnerService {
|
|
7
|
+
private readonly sidecarClient;
|
|
8
|
+
constructor(sidecarClient?: WasmSidecarClientService);
|
|
9
|
+
runDocumentSummary: (request: MainRunRequest) => Promise<MainRunResult>;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { WasmMainRunnerService };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MainRunnerService } from "./main-runner.service.js";
|
|
2
|
+
import { WasmSidecarClientService } from "../sidecar/wasm-sidecar-client.service.js";
|
|
3
|
+
//#region src/runtime/wasm-main-runner.service.ts
|
|
4
|
+
var WasmMainRunnerService = class extends MainRunnerService {
|
|
5
|
+
constructor(sidecarClient = new WasmSidecarClientService()) {
|
|
6
|
+
super();
|
|
7
|
+
this.sidecarClient = sidecarClient;
|
|
8
|
+
}
|
|
9
|
+
runDocumentSummary = async (request) => {
|
|
10
|
+
const output = await this.sidecarClient.runExport({
|
|
11
|
+
wasmPath: request.bundle.mainEntryPath,
|
|
12
|
+
exportName: request.bundle.manifest.main.export,
|
|
13
|
+
args: [request.input.documentCount, request.input.textBytes]
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
exportName: request.bundle.manifest.main.export,
|
|
17
|
+
output
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { WasmMainRunnerService };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/sidecar/wasm-sidecar-client.service.d.ts
|
|
2
|
+
declare class WasmSidecarClientService {
|
|
3
|
+
runExport: (params: {
|
|
4
|
+
wasmPath: string;
|
|
5
|
+
exportName: string;
|
|
6
|
+
args: number[];
|
|
7
|
+
}) => Promise<number>;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { WasmSidecarClientService };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
//#region src/sidecar/wasm-sidecar-client.service.ts
|
|
3
|
+
var WasmSidecarClientService = class {
|
|
4
|
+
runExport = async (params) => {
|
|
5
|
+
const { wasmPath, exportName, args } = params;
|
|
6
|
+
const wasmBytes = await readFile(wasmPath);
|
|
7
|
+
const wasmRuntime = globalThis.WebAssembly;
|
|
8
|
+
if (!wasmRuntime) throw new Error("当前 Node 运行时不支持 WebAssembly。");
|
|
9
|
+
const exported = (await wasmRuntime.instantiate(wasmBytes, {})).instance.exports[exportName];
|
|
10
|
+
if (typeof exported !== "function") throw new Error(`Wasm 导出不存在或不可调用:${exportName}`);
|
|
11
|
+
const result = exported(...args);
|
|
12
|
+
if (typeof result !== "number") throw new Error(`Wasm 导出 ${exportName} 必须返回 number。`);
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { WasmSidecarClientService };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AppManifestBundle } from "../manifest/app-manifest.types.js";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
|
|
4
|
+
//#region src/ui/ui-server.service.d.ts
|
|
5
|
+
declare class UiServerService {
|
|
6
|
+
private readonly bundle;
|
|
7
|
+
constructor(bundle: AppManifestBundle);
|
|
8
|
+
handle: (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
9
|
+
private resolveContentType;
|
|
10
|
+
private exists;
|
|
11
|
+
private isInsideAllowedDirectory;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { UiServerService };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/ui/ui-server.service.ts
|
|
4
|
+
const CONTENT_TYPE_BY_EXTENSION = {
|
|
5
|
+
".css": "text/css; charset=utf-8",
|
|
6
|
+
".html": "text/html; charset=utf-8",
|
|
7
|
+
".js": "text/javascript; charset=utf-8",
|
|
8
|
+
".json": "application/json; charset=utf-8",
|
|
9
|
+
".png": "image/png",
|
|
10
|
+
".svg": "image/svg+xml; charset=utf-8"
|
|
11
|
+
};
|
|
12
|
+
var UiServerService = class {
|
|
13
|
+
constructor(bundle) {
|
|
14
|
+
this.bundle = bundle;
|
|
15
|
+
}
|
|
16
|
+
handle = async (request, response) => {
|
|
17
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
18
|
+
const requestPath = requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname;
|
|
19
|
+
const candidatePaths = [path.join(this.bundle.uiDirectoryPath, requestPath.slice(1)), path.join(this.bundle.assetsDirectoryPath, requestPath.slice(1))];
|
|
20
|
+
for (const candidatePath of candidatePaths) {
|
|
21
|
+
if (!await this.isInsideAllowedDirectory(candidatePath)) continue;
|
|
22
|
+
if (!await this.exists(candidatePath)) continue;
|
|
23
|
+
const content = await readFile(candidatePath);
|
|
24
|
+
response.writeHead(200, { "content-type": this.resolveContentType(candidatePath) });
|
|
25
|
+
response.end(content);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
29
|
+
response.end(`Not found: ${requestPath}`);
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
resolveContentType = (filePath) => {
|
|
33
|
+
return CONTENT_TYPE_BY_EXTENSION[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
34
|
+
};
|
|
35
|
+
exists = async (filePath) => {
|
|
36
|
+
try {
|
|
37
|
+
await access(filePath);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
isInsideAllowedDirectory = async (candidatePath) => {
|
|
44
|
+
return [this.bundle.uiDirectoryPath, this.bundle.assetsDirectoryPath].some((directoryPath) => {
|
|
45
|
+
const relative = path.relative(directoryPath, candidatePath);
|
|
46
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { UiServerService };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextclaw/app-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Standalone micro app runtime and CLI for NextClaw apps.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Peiiii/nextclaw.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Peiiii/nextclaw/tree/master/packages/nextclaw-app-runtime",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Peiiii/nextclaw/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nextclaw",
|
|
18
|
+
"micro-app",
|
|
19
|
+
"wasm",
|
|
20
|
+
"webview",
|
|
21
|
+
"app-runtime",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"napp": "dist/main.js"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"development": "./src/index.ts",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.17.6",
|
|
44
|
+
"prettier": "^3.3.3",
|
|
45
|
+
"typescript": "^5.6.3",
|
|
46
|
+
"vitest": "^4.1.2"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsdown src/index.ts src/main.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
|
|
50
|
+
"lint": "eslint .",
|
|
51
|
+
"tsc": "tsc -p tsconfig.json",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"smoke": "node scripts/smoke.mjs"
|
|
54
|
+
}
|
|
55
|
+
}
|