@nextclaw/app-runtime 0.1.0 → 0.2.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/README.md +9 -0
- package/dist/commands/create.controller.js +23 -0
- package/dist/main.js +28 -4
- package/dist/package.js +1 -1
- package/dist/scaffold/app-scaffold.service.js +260 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
当前 MVP 提供三件事:
|
|
6
6
|
|
|
7
|
+
- `napp create <app-dir>`:生成一个最小可跑的微应用骨架
|
|
7
8
|
- `napp inspect <app-dir>`:校验应用目录与 manifest
|
|
8
9
|
- `napp run <app-dir>`:启动本地宿主,提供 UI 静态服务和桥接 API
|
|
9
10
|
- `napp dev <app-dir>`:当前等价于 `run`
|
|
@@ -44,6 +45,14 @@ assets/
|
|
|
44
45
|
|
|
45
46
|
## 示例
|
|
46
47
|
|
|
48
|
+
```bash
|
|
49
|
+
napp create ./my-first-napp
|
|
50
|
+
napp inspect ./my-first-napp
|
|
51
|
+
napp run ./my-first-napp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
已有示例应用:
|
|
55
|
+
|
|
47
56
|
```bash
|
|
48
57
|
napp inspect ./apps/examples/hello-notes
|
|
49
58
|
napp run ./apps/examples/hello-notes --document notes=/absolute/path/to/notes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppScaffoldService } from "../scaffold/app-scaffold.service.js";
|
|
2
|
+
//#region src/commands/create.controller.ts
|
|
3
|
+
var CreateCommand = class {
|
|
4
|
+
constructor(scaffoldService = new AppScaffoldService()) {
|
|
5
|
+
this.scaffoldService = scaffoldService;
|
|
6
|
+
}
|
|
7
|
+
run = async (params) => {
|
|
8
|
+
const { appDirectory, json, write } = params;
|
|
9
|
+
const result = await this.scaffoldService.scaffold(appDirectory);
|
|
10
|
+
if (json) {
|
|
11
|
+
write(`${JSON.stringify({
|
|
12
|
+
ok: true,
|
|
13
|
+
created: result
|
|
14
|
+
}, null, 2)}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
write(`Created app scaffold at ${result.appDirectory}\n`);
|
|
18
|
+
write(`Manifest: ${result.manifestPath}\n`);
|
|
19
|
+
write("Next: napp inspect <app-dir> && napp run <app-dir>\n");
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CreateCommand };
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { version } from "./package.js";
|
|
3
|
+
import { CreateCommand } from "./commands/create.controller.js";
|
|
3
4
|
import { RunCommand } from "./commands/run.controller.js";
|
|
4
5
|
import { DevCommand } from "./commands/dev.controller.js";
|
|
5
6
|
import { InspectCommand } from "./commands/inspect.controller.js";
|
|
@@ -19,7 +20,16 @@ var AppRuntimeCli = class {
|
|
|
19
20
|
this.writeUsage();
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
if (command === "create") {
|
|
24
|
+
const options = this.readCreateOptions(restArgs);
|
|
25
|
+
await new CreateCommand().run({
|
|
26
|
+
appDirectory,
|
|
27
|
+
json: options.json,
|
|
28
|
+
write: this.write
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const options = this.readRuntimeOptions(restArgs);
|
|
23
33
|
if (command === "inspect") {
|
|
24
34
|
await new InspectCommand().run({
|
|
25
35
|
appDirectory,
|
|
@@ -53,7 +63,20 @@ var AppRuntimeCli = class {
|
|
|
53
63
|
this.writeUsage();
|
|
54
64
|
process.exit(1);
|
|
55
65
|
};
|
|
56
|
-
|
|
66
|
+
readCreateOptions = (rawArgs) => {
|
|
67
|
+
const options = { json: false };
|
|
68
|
+
for (const current of rawArgs) {
|
|
69
|
+
if (!current?.startsWith("--")) throw new Error(`未知参数:${current}`);
|
|
70
|
+
switch (current) {
|
|
71
|
+
case "--json":
|
|
72
|
+
options.json = true;
|
|
73
|
+
break;
|
|
74
|
+
default: throw new Error(`未知参数:${current}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return options;
|
|
78
|
+
};
|
|
79
|
+
readRuntimeOptions = (rawArgs) => {
|
|
57
80
|
const options = {
|
|
58
81
|
host: "127.0.0.1",
|
|
59
82
|
port: 3100,
|
|
@@ -62,7 +85,7 @@ var AppRuntimeCli = class {
|
|
|
62
85
|
};
|
|
63
86
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
64
87
|
const current = rawArgs[index];
|
|
65
|
-
if (!current?.startsWith("--"))
|
|
88
|
+
if (!current?.startsWith("--")) throw new Error(`未知参数:${current}`);
|
|
66
89
|
const nextValue = rawArgs[index + 1];
|
|
67
90
|
switch (current) {
|
|
68
91
|
case "--host":
|
|
@@ -99,7 +122,8 @@ var AppRuntimeCli = class {
|
|
|
99
122
|
return nextValue;
|
|
100
123
|
};
|
|
101
124
|
writeUsage = () => {
|
|
102
|
-
this.write("Usage: napp
|
|
125
|
+
this.write("Usage: napp create <app-dir> [--json]\n");
|
|
126
|
+
this.write(" napp <inspect|run|dev> <app-dir> [--host 127.0.0.1] [--port 3100] [--json] [--document scope=/path]\n");
|
|
103
127
|
this.write(" napp --help\n");
|
|
104
128
|
this.write(" napp --version\n");
|
|
105
129
|
};
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/scaffold/app-scaffold.service.ts
|
|
4
|
+
var AppScaffoldService = class {
|
|
5
|
+
scaffold = async (targetDirectory) => {
|
|
6
|
+
const appDirectory = path.resolve(targetDirectory);
|
|
7
|
+
await this.assertTargetDoesNotExist(appDirectory);
|
|
8
|
+
await mkdir(path.join(appDirectory, "main"), { recursive: true });
|
|
9
|
+
await mkdir(path.join(appDirectory, "ui"), { recursive: true });
|
|
10
|
+
await mkdir(path.join(appDirectory, "assets"), { recursive: true });
|
|
11
|
+
const appName = this.buildAppName(appDirectory);
|
|
12
|
+
const appId = this.buildAppId(appDirectory);
|
|
13
|
+
const manifestPath = path.join(appDirectory, "manifest.json");
|
|
14
|
+
await Promise.all([
|
|
15
|
+
writeFile(manifestPath, `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\n`, "utf-8"),
|
|
16
|
+
writeFile(path.join(appDirectory, "main", "app.wasm"), Buffer.from(APP_WASM_BASE64, "base64")),
|
|
17
|
+
writeFile(path.join(appDirectory, "main", "app.wat"), this.buildWatSource(), "utf-8"),
|
|
18
|
+
writeFile(path.join(appDirectory, "ui", "index.html"), this.buildUiHtml(appName), "utf-8"),
|
|
19
|
+
writeFile(path.join(appDirectory, "ui", "app.controller.js"), this.buildUiController(), "utf-8"),
|
|
20
|
+
writeFile(path.join(appDirectory, "assets", "icon.svg"), this.buildIconSvg(), "utf-8")
|
|
21
|
+
]);
|
|
22
|
+
return {
|
|
23
|
+
appDirectory,
|
|
24
|
+
manifestPath
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
assertTargetDoesNotExist = async (targetDirectory) => {
|
|
28
|
+
try {
|
|
29
|
+
await access(targetDirectory);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`目标目录已存在:${targetDirectory}`);
|
|
34
|
+
};
|
|
35
|
+
buildAppId = (targetDirectory) => {
|
|
36
|
+
return `nextclaw.${this.normalizeSlug(path.basename(targetDirectory))}`;
|
|
37
|
+
};
|
|
38
|
+
buildAppName = (targetDirectory) => {
|
|
39
|
+
return this.normalizeSlug(path.basename(targetDirectory)).split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
40
|
+
};
|
|
41
|
+
normalizeSlug = (value) => {
|
|
42
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
43
|
+
if (!normalized) throw new Error("应用目录名无法生成合法的应用标识。");
|
|
44
|
+
return normalized;
|
|
45
|
+
};
|
|
46
|
+
buildManifest = (appId, appName) => {
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
id: appId,
|
|
50
|
+
name: appName,
|
|
51
|
+
version: "0.1.0",
|
|
52
|
+
description: "A minimal NextClaw micro app scaffold created by napp.",
|
|
53
|
+
icon: "assets/icon.svg",
|
|
54
|
+
main: {
|
|
55
|
+
kind: "wasm",
|
|
56
|
+
entry: "main/app.wasm",
|
|
57
|
+
export: "summarize_notes",
|
|
58
|
+
action: "runStarterDemo"
|
|
59
|
+
},
|
|
60
|
+
ui: { entry: "ui/index.html" },
|
|
61
|
+
permissions: {
|
|
62
|
+
storage: { namespace: appId.replace(/\./g, "-") },
|
|
63
|
+
capabilities: { hostBridge: true }
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
buildWatSource = () => {
|
|
68
|
+
return `(module
|
|
69
|
+
(func (export "summarize_notes") (param i32 i32) (result i32)
|
|
70
|
+
local.get 0
|
|
71
|
+
local.get 1
|
|
72
|
+
i32.add
|
|
73
|
+
i32.const 200
|
|
74
|
+
i32.add))\n`;
|
|
75
|
+
};
|
|
76
|
+
buildUiHtml = (appName) => {
|
|
77
|
+
return `<!doctype html>
|
|
78
|
+
<html lang="zh-CN">
|
|
79
|
+
<head>
|
|
80
|
+
<meta charset="utf-8" />
|
|
81
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
82
|
+
<title>${appName}</title>
|
|
83
|
+
<style>
|
|
84
|
+
:root {
|
|
85
|
+
color-scheme: light;
|
|
86
|
+
font-family: "IBM Plex Sans", "Helvetica Neue", sans-serif;
|
|
87
|
+
background:
|
|
88
|
+
radial-gradient(circle at top left, rgba(255, 206, 134, 0.24), transparent 30%),
|
|
89
|
+
linear-gradient(165deg, #f7f1e7 0%, #fcfcfe 48%, #e1ebf5 100%);
|
|
90
|
+
color: #11233a;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
body {
|
|
94
|
+
margin: 0;
|
|
95
|
+
min-height: 100vh;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main {
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
max-width: 760px;
|
|
101
|
+
margin: 0 auto;
|
|
102
|
+
padding: 48px 20px 72px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.panel {
|
|
106
|
+
background: rgba(255, 255, 255, 0.78);
|
|
107
|
+
border: 1px solid rgba(17, 35, 58, 0.08);
|
|
108
|
+
border-radius: 24px;
|
|
109
|
+
box-shadow: 0 18px 42px rgba(17, 35, 58, 0.12);
|
|
110
|
+
padding: 28px;
|
|
111
|
+
backdrop-filter: blur(18px);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
h1 {
|
|
115
|
+
margin: 0 0 12px;
|
|
116
|
+
font-size: clamp(30px, 6vw, 54px);
|
|
117
|
+
line-height: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
p {
|
|
121
|
+
line-height: 1.6;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
button {
|
|
125
|
+
border: 0;
|
|
126
|
+
border-radius: 999px;
|
|
127
|
+
background: #11233a;
|
|
128
|
+
color: #fff;
|
|
129
|
+
padding: 12px 18px;
|
|
130
|
+
font-size: 15px;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
dl {
|
|
135
|
+
display: grid;
|
|
136
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
137
|
+
gap: 12px;
|
|
138
|
+
margin: 24px 0 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dt {
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
color: #556881;
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
letter-spacing: 0.08em;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dd {
|
|
149
|
+
margin: 6px 0 0;
|
|
150
|
+
font-size: 24px;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pre {
|
|
155
|
+
margin-top: 20px;
|
|
156
|
+
padding: 14px;
|
|
157
|
+
border-radius: 16px;
|
|
158
|
+
background: rgba(17, 35, 58, 0.07);
|
|
159
|
+
overflow: auto;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<main>
|
|
165
|
+
<section class="panel">
|
|
166
|
+
<p>NextClaw Micro App</p>
|
|
167
|
+
<h1 id="title">Loading...</h1>
|
|
168
|
+
<p id="description">A starter scaffold created by napp create.</p>
|
|
169
|
+
<button id="run-button" type="button">Run Starter Demo</button>
|
|
170
|
+
<dl>
|
|
171
|
+
<div>
|
|
172
|
+
<dt>Action</dt>
|
|
173
|
+
<dd id="action">-</dd>
|
|
174
|
+
</div>
|
|
175
|
+
<div>
|
|
176
|
+
<dt>Documents</dt>
|
|
177
|
+
<dd id="document-count">-</dd>
|
|
178
|
+
</div>
|
|
179
|
+
<div>
|
|
180
|
+
<dt>Text Bytes</dt>
|
|
181
|
+
<dd id="text-bytes">-</dd>
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<dt>Wasm Score</dt>
|
|
185
|
+
<dd id="wasm-score">-</dd>
|
|
186
|
+
</div>
|
|
187
|
+
</dl>
|
|
188
|
+
<pre id="details"></pre>
|
|
189
|
+
</section>
|
|
190
|
+
</main>
|
|
191
|
+
<script type="module" src="./app.controller.js"><\/script>
|
|
192
|
+
</body>
|
|
193
|
+
</html>
|
|
194
|
+
`;
|
|
195
|
+
};
|
|
196
|
+
buildUiController = () => {
|
|
197
|
+
return `const titleNode = document.getElementById("title");
|
|
198
|
+
const descriptionNode = document.getElementById("description");
|
|
199
|
+
const actionNode = document.getElementById("action");
|
|
200
|
+
const documentCountNode = document.getElementById("document-count");
|
|
201
|
+
const textBytesNode = document.getElementById("text-bytes");
|
|
202
|
+
const wasmScoreNode = document.getElementById("wasm-score");
|
|
203
|
+
const detailsNode = document.getElementById("details");
|
|
204
|
+
const runButton = document.getElementById("run-button");
|
|
205
|
+
|
|
206
|
+
const setDetails = (value) => {
|
|
207
|
+
detailsNode.textContent = JSON.stringify(value, null, 2);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const loadManifest = async () => {
|
|
211
|
+
const response = await fetch("/__napp/manifest");
|
|
212
|
+
const payload = await response.json();
|
|
213
|
+
titleNode.textContent = payload.manifest.name;
|
|
214
|
+
descriptionNode.textContent = payload.manifest.description;
|
|
215
|
+
actionNode.textContent = payload.manifest.main.action;
|
|
216
|
+
setDetails(payload);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const runDemo = async () => {
|
|
220
|
+
runButton.disabled = true;
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch("/__napp/run", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: {
|
|
225
|
+
"content-type": "application/json",
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
action: actionNode.textContent,
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
const payload = await response.json();
|
|
232
|
+
documentCountNode.textContent = String(payload.result.input.documentCount);
|
|
233
|
+
textBytesNode.textContent = String(payload.result.input.textBytes);
|
|
234
|
+
wasmScoreNode.textContent = String(payload.result.output.output);
|
|
235
|
+
setDetails(payload);
|
|
236
|
+
} finally {
|
|
237
|
+
runButton.disabled = false;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
runButton.addEventListener("click", () => {
|
|
242
|
+
void runDemo();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await loadManifest();
|
|
246
|
+
`;
|
|
247
|
+
};
|
|
248
|
+
buildIconSvg = () => {
|
|
249
|
+
return `<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
250
|
+
<rect width="160" height="160" rx="40" fill="#11233A" />
|
|
251
|
+
<path d="M42 45H118V58H74V115H58V58H42V45Z" fill="#F8C97E" />
|
|
252
|
+
<path d="M92 74H118V87H92V74Z" fill="#FFFFFF" fill-opacity="0.92" />
|
|
253
|
+
<path d="M92 95H118V108H92V95Z" fill="#FFFFFF" fill-opacity="0.72" />
|
|
254
|
+
</svg>
|
|
255
|
+
`;
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
const APP_WASM_BASE64 = "AGFzbQEAAAABBwFgAn9/AX8DAgEABxMBD3N1bW1hcml6ZV9ub3RlcwAACg0BCwAgACABagsQQQQK";
|
|
259
|
+
//#endregion
|
|
260
|
+
export { AppScaffoldService };
|