@nextclaw/app-runtime 0.4.1 → 0.6.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 +29 -4
- package/dist/bundle/app-bundle.service.js +14 -8
- package/dist/bundle/app-bundle.types.d.ts +2 -0
- package/dist/cli/app-runtime-cli.service.d.ts +3 -0
- package/dist/cli/app-runtime-cli.service.js +70 -2
- package/dist/cli/app-runtime-options.service.d.ts +10 -1
- package/dist/cli/app-runtime-options.service.js +39 -2
- package/dist/commands/create.controller.d.ts +2 -1
- package/dist/commands/create.controller.js +3 -2
- package/dist/commands/inspect.controller.js +2 -1
- package/dist/commands/run.controller.js +25 -7
- package/dist/commands/validate-publish.controller.d.ts +15 -0
- package/dist/commands/validate-publish.controller.js +31 -0
- package/dist/host/app-host.service.d.ts +3 -1
- package/dist/host/app-host.service.js +5 -1
- package/dist/host/app-instance.service.js +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +9 -1
- package/dist/install/app-installation.service.js +1 -0
- package/dist/install/app-installation.types.d.ts +1 -0
- package/dist/manifest/app-manifest.service.js +9 -3
- package/dist/manifest/app-manifest.types.d.ts +9 -3
- package/dist/package.js +1 -1
- package/dist/publish/app-publish-validation.service.d.ts +37 -0
- package/dist/publish/app-publish-validation.service.js +76 -0
- package/dist/runtime/app-build.service.d.ts +24 -0
- package/dist/runtime/app-build.service.js +55 -0
- package/dist/runtime/app-runtime-toolchain.service.d.ts +30 -0
- package/dist/runtime/app-runtime-toolchain.service.js +96 -0
- package/dist/runtime/wasm-main-runner.service.js +1 -0
- package/dist/runtime/wasmtime-wasi-http-component.service.d.ts +24 -0
- package/dist/runtime/wasmtime-wasi-http-component.service.js +129 -0
- package/dist/scaffold/app-scaffold.service.d.ts +13 -2
- package/dist/scaffold/app-scaffold.service.js +39 -2
- package/dist/scaffold/app-ts-http-lite-scaffold-template.service.d.ts +18 -0
- package/dist/scaffold/app-ts-http-lite-scaffold-template.service.js +267 -0
- package/dist/scaffold/app-ts-http-scaffold-template.service.d.ts +25 -0
- package/dist/scaffold/app-ts-http-scaffold-template.service.js +496 -0
- package/package.json +1 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
|
|
2
|
+
import { AppTsHttpLiteScaffoldTemplateService } from "./app-ts-http-lite-scaffold-template.service.js";
|
|
1
3
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
//#region src/scaffold/app-scaffold.service.ts
|
|
4
6
|
var AppScaffoldService = class {
|
|
5
|
-
|
|
7
|
+
constructor(tsHttpTemplateService = new AppTsHttpScaffoldTemplateService(), tsHttpLiteTemplateService = new AppTsHttpLiteScaffoldTemplateService()) {
|
|
8
|
+
this.tsHttpTemplateService = tsHttpTemplateService;
|
|
9
|
+
this.tsHttpLiteTemplateService = tsHttpLiteTemplateService;
|
|
10
|
+
}
|
|
11
|
+
scaffold = async (targetDirectory, options) => {
|
|
6
12
|
const appDirectory = path.resolve(targetDirectory);
|
|
13
|
+
const template = options?.template ?? "starter";
|
|
7
14
|
await this.assertTargetDoesNotExist(appDirectory);
|
|
8
15
|
await mkdir(path.join(appDirectory, "main"), { recursive: true });
|
|
9
16
|
await mkdir(path.join(appDirectory, "ui"), { recursive: true });
|
|
@@ -11,6 +18,28 @@ var AppScaffoldService = class {
|
|
|
11
18
|
const appName = this.buildAppName(appDirectory);
|
|
12
19
|
const appId = this.buildAppId(appDirectory);
|
|
13
20
|
const manifestPath = path.join(appDirectory, "manifest.json");
|
|
21
|
+
if (template === "ts-http") {
|
|
22
|
+
await this.writeTemplateFiles(appDirectory, this.tsHttpTemplateService.buildFiles({
|
|
23
|
+
appId,
|
|
24
|
+
appName
|
|
25
|
+
}));
|
|
26
|
+
return {
|
|
27
|
+
appDirectory,
|
|
28
|
+
manifestPath,
|
|
29
|
+
template
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (template === "ts-http-lite") {
|
|
33
|
+
await this.writeTemplateFiles(appDirectory, this.tsHttpLiteTemplateService.buildFiles({
|
|
34
|
+
appId,
|
|
35
|
+
appName
|
|
36
|
+
}));
|
|
37
|
+
return {
|
|
38
|
+
appDirectory,
|
|
39
|
+
manifestPath,
|
|
40
|
+
template
|
|
41
|
+
};
|
|
42
|
+
}
|
|
14
43
|
await Promise.all([
|
|
15
44
|
writeFile(manifestPath, `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\n`, "utf-8"),
|
|
16
45
|
writeFile(path.join(appDirectory, "marketplace.json"), `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\n`, "utf-8"),
|
|
@@ -23,9 +52,17 @@ var AppScaffoldService = class {
|
|
|
23
52
|
]);
|
|
24
53
|
return {
|
|
25
54
|
appDirectory,
|
|
26
|
-
manifestPath
|
|
55
|
+
manifestPath,
|
|
56
|
+
template
|
|
27
57
|
};
|
|
28
58
|
};
|
|
59
|
+
writeTemplateFiles = async (appDirectory, files) => {
|
|
60
|
+
await Promise.all(files.map(async (file) => {
|
|
61
|
+
const filePath = path.join(appDirectory, file.relativePath);
|
|
62
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
63
|
+
await writeFile(filePath, file.content);
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
29
66
|
assertTargetDoesNotExist = async (targetDirectory) => {
|
|
30
67
|
try {
|
|
31
68
|
await access(targetDirectory);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AppScaffoldFile, AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
|
|
2
|
+
|
|
3
|
+
//#region src/scaffold/app-ts-http-lite-scaffold-template.service.d.ts
|
|
4
|
+
declare class AppTsHttpLiteScaffoldTemplateService {
|
|
5
|
+
private readonly standardTemplateService;
|
|
6
|
+
constructor(standardTemplateService?: AppTsHttpScaffoldTemplateService);
|
|
7
|
+
buildFiles: (params: {
|
|
8
|
+
appId: string;
|
|
9
|
+
appName: string;
|
|
10
|
+
}) => AppScaffoldFile[];
|
|
11
|
+
private buildMainPackageJson;
|
|
12
|
+
private buildMarketplaceMetadata;
|
|
13
|
+
private buildReadme;
|
|
14
|
+
private buildComponentSource;
|
|
15
|
+
private normalizeSlug;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { AppTsHttpLiteScaffoldTemplateService };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { AppTsHttpScaffoldTemplateService } from "./app-ts-http-scaffold-template.service.js";
|
|
2
|
+
//#region src/scaffold/app-ts-http-lite-scaffold-template.service.ts
|
|
3
|
+
var AppTsHttpLiteScaffoldTemplateService = class {
|
|
4
|
+
constructor(standardTemplateService = new AppTsHttpScaffoldTemplateService()) {
|
|
5
|
+
this.standardTemplateService = standardTemplateService;
|
|
6
|
+
}
|
|
7
|
+
buildFiles = (params) => {
|
|
8
|
+
return this.standardTemplateService.buildFiles(params).map((file) => {
|
|
9
|
+
switch (file.relativePath) {
|
|
10
|
+
case "marketplace.json": return {
|
|
11
|
+
relativePath: file.relativePath,
|
|
12
|
+
content: `${JSON.stringify(this.buildMarketplaceMetadata(params.appName), null, 2)}\n`
|
|
13
|
+
};
|
|
14
|
+
case "README.md": return {
|
|
15
|
+
relativePath: file.relativePath,
|
|
16
|
+
content: this.buildReadme(params.appName, params.appId)
|
|
17
|
+
};
|
|
18
|
+
case "main/package.json": return {
|
|
19
|
+
relativePath: file.relativePath,
|
|
20
|
+
content: `${JSON.stringify(this.buildMainPackageJson(params.appId), null, 2)}\n`
|
|
21
|
+
};
|
|
22
|
+
case "main/src/component.ts": return {
|
|
23
|
+
relativePath: file.relativePath,
|
|
24
|
+
content: this.buildComponentSource()
|
|
25
|
+
};
|
|
26
|
+
default: return file;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
buildMainPackageJson = (appId) => {
|
|
31
|
+
return {
|
|
32
|
+
name: `${appId.replace(/\./g, "-")}-main`,
|
|
33
|
+
version: "0.1.0",
|
|
34
|
+
private: true,
|
|
35
|
+
type: "module",
|
|
36
|
+
scripts: {
|
|
37
|
+
build: "npm run prepare-wit && npm run guest-types && npm run typecheck && npm run bundle && npm run componentize",
|
|
38
|
+
"prepare-wit": "wkg wit fetch",
|
|
39
|
+
"guest-types": "jco guest-types wit --world-name napp-http --out-dir generated/types",
|
|
40
|
+
typecheck: "tsc --noEmit",
|
|
41
|
+
bundle: "rolldown -c",
|
|
42
|
+
componentize: "jco componentize -w wit -n napp-http -o app.wasm dist/component.js"
|
|
43
|
+
},
|
|
44
|
+
dependencies: { "@bytecodealliance/jco-std": "^0.1.3" },
|
|
45
|
+
devDependencies: {
|
|
46
|
+
"@bytecodealliance/componentize-js": "^0.20.0",
|
|
47
|
+
"@bytecodealliance/jco": "^1.19.0",
|
|
48
|
+
rolldown: "^1.0.0-rc.17",
|
|
49
|
+
typescript: "^5.6.3"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
buildMarketplaceMetadata = (appName) => {
|
|
54
|
+
return {
|
|
55
|
+
slug: this.normalizeSlug(appName),
|
|
56
|
+
summary: `A lightweight TypeScript WASI HTTP ${appName} scaffold created by napp.`,
|
|
57
|
+
summaryI18n: {
|
|
58
|
+
en: `A lightweight TypeScript WASI HTTP ${appName} scaffold created by napp.`,
|
|
59
|
+
zh: `一个由 napp 创建的轻量 TypeScript WASI HTTP ${appName} 应用骨架。`
|
|
60
|
+
},
|
|
61
|
+
description: `A lightweight TypeScript NApp scaffold for ${appName}, designed to keep the WASI HTTP backend smaller than the default Hono-based template.`,
|
|
62
|
+
descriptionI18n: {
|
|
63
|
+
en: `A lightweight TypeScript NApp scaffold for ${appName}, designed to keep the WASI HTTP backend smaller than the default Hono-based template.`,
|
|
64
|
+
zh: `一个面向 ${appName} 的轻量 TypeScript NApp 应用骨架,用更薄的 WASI HTTP handler 替代默认的 Hono 模板,优先追求更小包体。`
|
|
65
|
+
},
|
|
66
|
+
author: "NextClaw",
|
|
67
|
+
tags: [
|
|
68
|
+
"starter",
|
|
69
|
+
"typescript",
|
|
70
|
+
"wasi-http",
|
|
71
|
+
"official",
|
|
72
|
+
"lite"
|
|
73
|
+
],
|
|
74
|
+
sourceRepo: "https://github.com/Peiiii/nextclaw",
|
|
75
|
+
homepage: "https://nextclaw.io",
|
|
76
|
+
featured: false
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
buildReadme = (appName, appId) => {
|
|
80
|
+
return `# ${appName}
|
|
81
|
+
|
|
82
|
+
This app was created by \`napp create --template ts-http-lite\`.
|
|
83
|
+
|
|
84
|
+
It keeps the existing NApp directory contract, while making \`main/app.wasm\` a lighter WASI HTTP component without the default Hono routing layer.
|
|
85
|
+
|
|
86
|
+
## App ID
|
|
87
|
+
|
|
88
|
+
\`${appId}\`
|
|
89
|
+
|
|
90
|
+
## Build the TypeScript backend
|
|
91
|
+
|
|
92
|
+
Recommended:
|
|
93
|
+
|
|
94
|
+
\`\`\`bash
|
|
95
|
+
napp build . --install
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
Manual equivalent:
|
|
99
|
+
|
|
100
|
+
\`\`\`bash
|
|
101
|
+
cd main
|
|
102
|
+
npm install
|
|
103
|
+
npm run build
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
The build writes the backend component to:
|
|
107
|
+
|
|
108
|
+
\`\`\`text
|
|
109
|
+
main/app.wasm
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
## Local workflow
|
|
113
|
+
|
|
114
|
+
\`\`\`bash
|
|
115
|
+
napp inspect .
|
|
116
|
+
napp validate-publish .
|
|
117
|
+
napp run . --data ./data
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
The runtime mounts the host data directory to guest \`/data\`. This example stores todos in:
|
|
121
|
+
|
|
122
|
+
\`\`\`text
|
|
123
|
+
./data/todos.json
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
The frontend still uses ordinary same-origin HTTP:
|
|
127
|
+
|
|
128
|
+
\`\`\`js
|
|
129
|
+
await fetch("/api/todos");
|
|
130
|
+
\`\`\`
|
|
131
|
+
`;
|
|
132
|
+
};
|
|
133
|
+
buildComponentSource = () => {
|
|
134
|
+
return `import { fire, incomingHandler } from "@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server";
|
|
135
|
+
import { getDirectories } from "wasi:filesystem/preopens@0.2.2";
|
|
136
|
+
|
|
137
|
+
type Todo = {
|
|
138
|
+
id: string;
|
|
139
|
+
title: string;
|
|
140
|
+
completed: boolean;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type TodoInput = {
|
|
144
|
+
title?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const TODOS_FILE = "todos.json";
|
|
148
|
+
const encoder = new TextEncoder();
|
|
149
|
+
const decoder = new TextDecoder();
|
|
150
|
+
const app = {
|
|
151
|
+
fetch: (request: Request): Promise<Response> => routeRequest(request),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
fire(app as never);
|
|
155
|
+
|
|
156
|
+
export { incomingHandler };
|
|
157
|
+
|
|
158
|
+
async function routeRequest(request: Request): Promise<Response> {
|
|
159
|
+
const url = new URL(request.url);
|
|
160
|
+
const pathname = url.pathname;
|
|
161
|
+
|
|
162
|
+
if (request.method === "GET" && pathname === "/api/todos") {
|
|
163
|
+
return json(loadTodos());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (request.method === "POST" && pathname === "/api/todos") {
|
|
167
|
+
const input = await request.json() as TodoInput;
|
|
168
|
+
if (!input.title?.trim()) {
|
|
169
|
+
return json({ error: "title is required" }, 400);
|
|
170
|
+
}
|
|
171
|
+
const todos = loadTodos();
|
|
172
|
+
todos.push({
|
|
173
|
+
id: crypto.randomUUID(),
|
|
174
|
+
title: input.title.trim(),
|
|
175
|
+
completed: false,
|
|
176
|
+
});
|
|
177
|
+
saveTodos(todos);
|
|
178
|
+
return json(todos);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (request.method === "PATCH" && pathname.startsWith("/api/todos/")) {
|
|
182
|
+
const todoId = pathname.slice("/api/todos/".length);
|
|
183
|
+
const input = await request.json() as Partial<Todo>;
|
|
184
|
+
const todos = loadTodos();
|
|
185
|
+
const todo = todos.find((entry) => entry.id === todoId);
|
|
186
|
+
if (!todo) {
|
|
187
|
+
return json({ error: "todo not found" }, 404);
|
|
188
|
+
}
|
|
189
|
+
if (typeof input.title === "string") {
|
|
190
|
+
todo.title = input.title;
|
|
191
|
+
}
|
|
192
|
+
if (typeof input.completed === "boolean") {
|
|
193
|
+
todo.completed = input.completed;
|
|
194
|
+
}
|
|
195
|
+
saveTodos(todos);
|
|
196
|
+
return json(todos);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (request.method === "DELETE" && pathname.startsWith("/api/todos/")) {
|
|
200
|
+
const todoId = pathname.slice("/api/todos/".length);
|
|
201
|
+
const nextTodos = loadTodos().filter((entry) => entry.id !== todoId);
|
|
202
|
+
saveTodos(nextTodos);
|
|
203
|
+
return json(nextTodos);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return text("Not found", 404);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function json(value: unknown, status = 200): Response {
|
|
210
|
+
return new Response(JSON.stringify(value), {
|
|
211
|
+
status,
|
|
212
|
+
headers: {
|
|
213
|
+
"content-type": "application/json; charset=utf-8",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function text(value: string, status = 200): Response {
|
|
219
|
+
return new Response(value, { status });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function loadTodos(): Todo[] {
|
|
223
|
+
const file = openTodosFile({ read: true });
|
|
224
|
+
if (!file) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const [bytes] = file.read(BigInt(1024 * 1024), BigInt(0));
|
|
228
|
+
if (bytes.length === 0) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return JSON.parse(decoder.decode(bytes)) as Todo[];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function saveTodos(todos: Todo[]): void {
|
|
235
|
+
const file = getDataDirectory().openAt(
|
|
236
|
+
{ symlinkFollow: true },
|
|
237
|
+
TODOS_FILE,
|
|
238
|
+
{ create: true, truncate: true },
|
|
239
|
+
{ write: true },
|
|
240
|
+
);
|
|
241
|
+
file.write(encoder.encode(JSON.stringify(todos, null, 2)), BigInt(0));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function openTodosFile(flags: { read?: boolean; write?: boolean }) {
|
|
245
|
+
try {
|
|
246
|
+
return getDataDirectory().openAt({ symlinkFollow: true }, TODOS_FILE, {}, flags);
|
|
247
|
+
} catch {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getDataDirectory() {
|
|
253
|
+
for (const [descriptor, guestPath] of getDirectories()) {
|
|
254
|
+
if (guestPath === "/data") {
|
|
255
|
+
return descriptor;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
throw new Error("Missing /data preopen. NApp runtime must mount the app data directory at /data.");
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
};
|
|
262
|
+
normalizeSlug = (value) => {
|
|
263
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
export { AppTsHttpLiteScaffoldTemplateService };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/scaffold/app-ts-http-scaffold-template.service.d.ts
|
|
2
|
+
type AppScaffoldFile = {
|
|
3
|
+
relativePath: string;
|
|
4
|
+
content: string | Buffer;
|
|
5
|
+
};
|
|
6
|
+
declare class AppTsHttpScaffoldTemplateService {
|
|
7
|
+
buildFiles: (params: {
|
|
8
|
+
appId: string;
|
|
9
|
+
appName: string;
|
|
10
|
+
}) => AppScaffoldFile[];
|
|
11
|
+
private buildManifest;
|
|
12
|
+
private buildMainPackageJson;
|
|
13
|
+
private buildTsconfig;
|
|
14
|
+
private buildMarketplaceMetadata;
|
|
15
|
+
private buildReadme;
|
|
16
|
+
private buildRolldownConfig;
|
|
17
|
+
private buildWitWorld;
|
|
18
|
+
private buildComponentSource;
|
|
19
|
+
private buildUiHtml;
|
|
20
|
+
private buildUiScript;
|
|
21
|
+
private buildIconSvg;
|
|
22
|
+
private normalizeSlug;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { AppScaffoldFile, AppTsHttpScaffoldTemplateService };
|