@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
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
//#region src/scaffold/app-ts-http-scaffold-template.service.ts
|
|
2
|
+
var AppTsHttpScaffoldTemplateService = class {
|
|
3
|
+
buildFiles = (params) => {
|
|
4
|
+
const { appId, appName } = params;
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
relativePath: "manifest.json",
|
|
8
|
+
content: `${JSON.stringify(this.buildManifest(appId, appName), null, 2)}\n`
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
relativePath: "marketplace.json",
|
|
12
|
+
content: `${JSON.stringify(this.buildMarketplaceMetadata(appName), null, 2)}\n`
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
relativePath: "README.md",
|
|
16
|
+
content: this.buildReadme(appName, appId)
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
relativePath: "main/package.json",
|
|
20
|
+
content: `${JSON.stringify(this.buildMainPackageJson(appId), null, 2)}\n`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
relativePath: "main/tsconfig.json",
|
|
24
|
+
content: `${JSON.stringify(this.buildTsconfig(), null, 2)}\n`
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
relativePath: "main/rolldown.config.mjs",
|
|
28
|
+
content: this.buildRolldownConfig()
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
relativePath: "main/wit/world.wit",
|
|
32
|
+
content: this.buildWitWorld()
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
relativePath: "main/src/component.ts",
|
|
36
|
+
content: this.buildComponentSource()
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
relativePath: "main/app.wasm",
|
|
40
|
+
content: Buffer.from(APP_WASM_PLACEHOLDER_BASE64, "base64")
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
relativePath: "ui/index.html",
|
|
44
|
+
content: this.buildUiHtml(appName)
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
relativePath: "ui/app.js",
|
|
48
|
+
content: this.buildUiScript()
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
relativePath: "assets/icon.svg",
|
|
52
|
+
content: this.buildIconSvg()
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
};
|
|
56
|
+
buildManifest = (appId, appName) => {
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
id: appId,
|
|
60
|
+
name: appName,
|
|
61
|
+
version: "0.1.0",
|
|
62
|
+
description: "A TypeScript WASI HTTP NApp scaffold created by napp.",
|
|
63
|
+
icon: "assets/icon.svg",
|
|
64
|
+
main: {
|
|
65
|
+
kind: "wasi-http-component",
|
|
66
|
+
entry: "main/app.wasm"
|
|
67
|
+
},
|
|
68
|
+
ui: { entry: "ui/index.html" }
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
buildMainPackageJson = (appId) => {
|
|
72
|
+
return {
|
|
73
|
+
name: `${appId.replace(/\./g, "-")}-main`,
|
|
74
|
+
version: "0.1.0",
|
|
75
|
+
private: true,
|
|
76
|
+
type: "module",
|
|
77
|
+
scripts: {
|
|
78
|
+
build: "npm run prepare-wit && npm run guest-types && npm run typecheck && npm run bundle && npm run componentize",
|
|
79
|
+
"prepare-wit": "wkg wit fetch",
|
|
80
|
+
"guest-types": "jco guest-types wit --world-name napp-http --out-dir generated/types",
|
|
81
|
+
typecheck: "tsc --noEmit",
|
|
82
|
+
bundle: "rolldown -c",
|
|
83
|
+
componentize: "jco componentize -w wit -n napp-http -o app.wasm dist/component.js"
|
|
84
|
+
},
|
|
85
|
+
dependencies: {
|
|
86
|
+
"@bytecodealliance/jco-std": "^0.1.3",
|
|
87
|
+
hono: "^4.10.0"
|
|
88
|
+
},
|
|
89
|
+
devDependencies: {
|
|
90
|
+
"@bytecodealliance/componentize-js": "^0.20.0",
|
|
91
|
+
"@bytecodealliance/jco": "^1.19.0",
|
|
92
|
+
rolldown: "^1.0.0-rc.17",
|
|
93
|
+
typescript: "^5.6.3"
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
buildTsconfig = () => {
|
|
98
|
+
return {
|
|
99
|
+
compilerOptions: {
|
|
100
|
+
target: "ES2022",
|
|
101
|
+
module: "ESNext",
|
|
102
|
+
moduleResolution: "Bundler",
|
|
103
|
+
strict: true,
|
|
104
|
+
skipLibCheck: true,
|
|
105
|
+
lib: ["ES2022", "DOM"],
|
|
106
|
+
types: ["./generated/types/interfaces/wasi-filesystem-preopens", "./generated/types/interfaces/wasi-filesystem-types"]
|
|
107
|
+
},
|
|
108
|
+
include: ["src/**/*.ts"]
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
buildMarketplaceMetadata = (appName) => {
|
|
112
|
+
return {
|
|
113
|
+
slug: this.normalizeSlug(appName),
|
|
114
|
+
summary: `A TypeScript WASI HTTP ${appName} scaffold created by napp.`,
|
|
115
|
+
summaryI18n: {
|
|
116
|
+
en: `A TypeScript WASI HTTP ${appName} scaffold created by napp.`,
|
|
117
|
+
zh: `一个由 napp 创建的 TypeScript WASI HTTP ${appName} 应用骨架。`
|
|
118
|
+
},
|
|
119
|
+
description: `A TypeScript NApp scaffold for ${appName}, designed for ordinary frontend-to-backend HTTP development on WASI components.`,
|
|
120
|
+
descriptionI18n: {
|
|
121
|
+
en: `A TypeScript NApp scaffold for ${appName}, designed for ordinary frontend-to-backend HTTP development on WASI components.`,
|
|
122
|
+
zh: `一个面向 ${appName} 的 TypeScript NApp 应用骨架,用 WASI component 承载普通前后端 HTTP 开发心智。`
|
|
123
|
+
},
|
|
124
|
+
author: "NextClaw",
|
|
125
|
+
tags: [
|
|
126
|
+
"starter",
|
|
127
|
+
"typescript",
|
|
128
|
+
"wasi-http",
|
|
129
|
+
"official"
|
|
130
|
+
],
|
|
131
|
+
sourceRepo: "https://github.com/Peiiii/nextclaw",
|
|
132
|
+
homepage: "https://nextclaw.io",
|
|
133
|
+
featured: false
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
buildReadme = (appName, appId) => {
|
|
137
|
+
return `# ${appName}
|
|
138
|
+
|
|
139
|
+
This app was created by \`napp create --template ts-http\`.
|
|
140
|
+
|
|
141
|
+
It keeps the existing NApp directory contract, while making \`main/app.wasm\` a WASI HTTP component.
|
|
142
|
+
|
|
143
|
+
## App ID
|
|
144
|
+
|
|
145
|
+
\`${appId}\`
|
|
146
|
+
|
|
147
|
+
## Build the TypeScript backend
|
|
148
|
+
|
|
149
|
+
Recommended:
|
|
150
|
+
|
|
151
|
+
\`\`\`bash
|
|
152
|
+
napp build . --install
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
Manual equivalent:
|
|
156
|
+
|
|
157
|
+
\`\`\`bash
|
|
158
|
+
cd main
|
|
159
|
+
npm install
|
|
160
|
+
npm run build
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
The build writes the backend component to:
|
|
164
|
+
|
|
165
|
+
\`\`\`text
|
|
166
|
+
main/app.wasm
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
The build uses the official Bytecode Alliance toolchain and expects \`wkg\` to be available for fetching WIT dependencies:
|
|
170
|
+
|
|
171
|
+
\`\`\`bash
|
|
172
|
+
cargo install wkg
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
## Local workflow
|
|
176
|
+
|
|
177
|
+
\`\`\`bash
|
|
178
|
+
napp inspect .
|
|
179
|
+
napp run . --data ./data
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
The runtime mounts the host data directory to guest \`/data\`. This example stores todos in:
|
|
183
|
+
|
|
184
|
+
\`\`\`text
|
|
185
|
+
./data/todos.json
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
The frontend can call the backend as ordinary same-origin HTTP:
|
|
189
|
+
|
|
190
|
+
\`\`\`js
|
|
191
|
+
await fetch("/api/todos");
|
|
192
|
+
\`\`\`
|
|
193
|
+
`;
|
|
194
|
+
};
|
|
195
|
+
buildRolldownConfig = () => {
|
|
196
|
+
return `export default {
|
|
197
|
+
input: "src/component.ts",
|
|
198
|
+
output: {
|
|
199
|
+
file: "dist/component.js",
|
|
200
|
+
format: "esm"
|
|
201
|
+
},
|
|
202
|
+
external: [/^wasi:/]
|
|
203
|
+
};
|
|
204
|
+
`;
|
|
205
|
+
};
|
|
206
|
+
buildWitWorld = () => {
|
|
207
|
+
return `package nextclaw:napp;
|
|
208
|
+
|
|
209
|
+
world napp-http {
|
|
210
|
+
import wasi:filesystem/types@0.2.2;
|
|
211
|
+
import wasi:filesystem/preopens@0.2.2;
|
|
212
|
+
|
|
213
|
+
export wasi:http/incoming-handler@0.2.6;
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
};
|
|
217
|
+
buildComponentSource = () => {
|
|
218
|
+
return `import { Hono } from "hono";
|
|
219
|
+
import { fire } from "@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server";
|
|
220
|
+
import { getDirectories } from "wasi:filesystem/preopens@0.2.2";
|
|
221
|
+
|
|
222
|
+
type Todo = {
|
|
223
|
+
id: string;
|
|
224
|
+
title: string;
|
|
225
|
+
completed: boolean;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
type TodoInput = {
|
|
229
|
+
title?: string;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const TODOS_FILE = "todos.json";
|
|
233
|
+
const encoder = new TextEncoder();
|
|
234
|
+
const decoder = new TextDecoder();
|
|
235
|
+
const app = new Hono();
|
|
236
|
+
|
|
237
|
+
app.get("/api/todos", (context) => {
|
|
238
|
+
return context.json(loadTodos());
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
app.post("/api/todos", async (context) => {
|
|
242
|
+
const input = await context.req.json<TodoInput>();
|
|
243
|
+
if (!input.title?.trim()) {
|
|
244
|
+
return context.json({ error: "title is required" }, 400);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const todos = loadTodos();
|
|
248
|
+
todos.push({
|
|
249
|
+
id: crypto.randomUUID(),
|
|
250
|
+
title: input.title.trim(),
|
|
251
|
+
completed: false,
|
|
252
|
+
});
|
|
253
|
+
saveTodos(todos);
|
|
254
|
+
return context.json(todos);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
app.patch("/api/todos/:id", async (context) => {
|
|
258
|
+
const input = await context.req.json<Partial<Todo>>();
|
|
259
|
+
const todos = loadTodos();
|
|
260
|
+
const todo = todos.find((entry) => entry.id === context.req.param("id"));
|
|
261
|
+
if (!todo) {
|
|
262
|
+
return context.json({ error: "todo not found" }, 404);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof input.title === "string") {
|
|
266
|
+
todo.title = input.title;
|
|
267
|
+
}
|
|
268
|
+
if (typeof input.completed === "boolean") {
|
|
269
|
+
todo.completed = input.completed;
|
|
270
|
+
}
|
|
271
|
+
saveTodos(todos);
|
|
272
|
+
return context.json(todos);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
app.delete("/api/todos/:id", (context) => {
|
|
276
|
+
const nextTodos = loadTodos().filter((entry) => entry.id !== context.req.param("id"));
|
|
277
|
+
saveTodos(nextTodos);
|
|
278
|
+
return context.json(nextTodos);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
app.notFound((context) => context.text("Not found", 404));
|
|
282
|
+
|
|
283
|
+
fire(app);
|
|
284
|
+
|
|
285
|
+
export { incomingHandler } from "@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server";
|
|
286
|
+
|
|
287
|
+
function loadTodos(): Todo[] {
|
|
288
|
+
const file = openTodosFile({ read: true });
|
|
289
|
+
if (!file) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
const [bytes] = file.read(BigInt(1024 * 1024), BigInt(0));
|
|
293
|
+
if (bytes.length === 0) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
return JSON.parse(decoder.decode(bytes)) as Todo[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function saveTodos(todos: Todo[]): void {
|
|
300
|
+
const file = getDataDirectory().openAt(
|
|
301
|
+
{ symlinkFollow: true },
|
|
302
|
+
TODOS_FILE,
|
|
303
|
+
{ create: true, truncate: true },
|
|
304
|
+
{ write: true },
|
|
305
|
+
);
|
|
306
|
+
file.write(encoder.encode(JSON.stringify(todos, null, 2)), BigInt(0));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function openTodosFile(flags: { read?: boolean; write?: boolean }) {
|
|
310
|
+
try {
|
|
311
|
+
return getDataDirectory().openAt({ symlinkFollow: true }, TODOS_FILE, {}, flags);
|
|
312
|
+
} catch {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getDataDirectory() {
|
|
318
|
+
for (const [descriptor, guestPath] of getDirectories()) {
|
|
319
|
+
if (guestPath === "/data") {
|
|
320
|
+
return descriptor;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
throw new Error("Missing /data preopen. NApp runtime must mount the app data directory at /data.");
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
};
|
|
327
|
+
buildUiHtml = (appName) => {
|
|
328
|
+
return `<!doctype html>
|
|
329
|
+
<html lang="zh-CN">
|
|
330
|
+
<head>
|
|
331
|
+
<meta charset="utf-8" />
|
|
332
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
333
|
+
<title>${appName}</title>
|
|
334
|
+
<style>
|
|
335
|
+
:root {
|
|
336
|
+
color-scheme: light;
|
|
337
|
+
font-family: "IBM Plex Sans", "Helvetica Neue", sans-serif;
|
|
338
|
+
background: linear-gradient(135deg, #f4efe6 0%, #eff6f9 50%, #f8fafc 100%);
|
|
339
|
+
color: #13212f;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
body {
|
|
343
|
+
margin: 0;
|
|
344
|
+
min-height: 100vh;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
main {
|
|
348
|
+
box-sizing: border-box;
|
|
349
|
+
max-width: 720px;
|
|
350
|
+
margin: 0 auto;
|
|
351
|
+
padding: 48px 20px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.panel {
|
|
355
|
+
border: 1px solid rgba(19, 33, 47, 0.1);
|
|
356
|
+
border-radius: 24px;
|
|
357
|
+
background: rgba(255, 255, 255, 0.82);
|
|
358
|
+
box-shadow: 0 24px 70px rgba(19, 33, 47, 0.14);
|
|
359
|
+
padding: 28px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
h1 {
|
|
363
|
+
margin: 0 0 10px;
|
|
364
|
+
font-size: clamp(32px, 7vw, 58px);
|
|
365
|
+
letter-spacing: -0.06em;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
form {
|
|
369
|
+
display: flex;
|
|
370
|
+
gap: 10px;
|
|
371
|
+
margin: 24px 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
input {
|
|
375
|
+
flex: 1;
|
|
376
|
+
border: 1px solid rgba(19, 33, 47, 0.18);
|
|
377
|
+
border-radius: 999px;
|
|
378
|
+
padding: 12px 14px;
|
|
379
|
+
font-size: 15px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
button {
|
|
383
|
+
border: 0;
|
|
384
|
+
border-radius: 999px;
|
|
385
|
+
background: #13212f;
|
|
386
|
+
color: white;
|
|
387
|
+
padding: 12px 18px;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
li {
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: space-between;
|
|
395
|
+
gap: 12px;
|
|
396
|
+
padding: 12px 0;
|
|
397
|
+
border-top: 1px solid rgba(19, 33, 47, 0.1);
|
|
398
|
+
}
|
|
399
|
+
</style>
|
|
400
|
+
</head>
|
|
401
|
+
<body>
|
|
402
|
+
<main>
|
|
403
|
+
<section class="panel">
|
|
404
|
+
<p>TypeScript WASI HTTP NApp</p>
|
|
405
|
+
<h1>${appName}</h1>
|
|
406
|
+
<form id="todo-form">
|
|
407
|
+
<input id="todo-title" type="text" placeholder="Add a todo" />
|
|
408
|
+
<button type="submit">Add</button>
|
|
409
|
+
</form>
|
|
410
|
+
<ul id="todos"></ul>
|
|
411
|
+
</section>
|
|
412
|
+
</main>
|
|
413
|
+
<script type="module" src="./app.js"><\/script>
|
|
414
|
+
</body>
|
|
415
|
+
</html>
|
|
416
|
+
`;
|
|
417
|
+
};
|
|
418
|
+
buildUiScript = () => {
|
|
419
|
+
return `const form = document.getElementById("todo-form");
|
|
420
|
+
const input = document.getElementById("todo-title");
|
|
421
|
+
const list = document.getElementById("todos");
|
|
422
|
+
|
|
423
|
+
async function loadTodos() {
|
|
424
|
+
const response = await fetch("/api/todos");
|
|
425
|
+
render(await response.json());
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function addTodo(title) {
|
|
429
|
+
const response = await fetch("/api/todos", {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: {
|
|
432
|
+
"content-type": "application/json"
|
|
433
|
+
},
|
|
434
|
+
body: JSON.stringify({ title })
|
|
435
|
+
});
|
|
436
|
+
render(await response.json());
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function toggleTodo(todo) {
|
|
440
|
+
const response = await fetch(\`/api/todos/\${todo.id}\`, {
|
|
441
|
+
method: "PATCH",
|
|
442
|
+
headers: {
|
|
443
|
+
"content-type": "application/json"
|
|
444
|
+
},
|
|
445
|
+
body: JSON.stringify({ completed: !todo.completed })
|
|
446
|
+
});
|
|
447
|
+
render(await response.json());
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function render(todos) {
|
|
451
|
+
list.replaceChildren(
|
|
452
|
+
...todos.map((todo) => {
|
|
453
|
+
const item = document.createElement("li");
|
|
454
|
+
const label = document.createElement("span");
|
|
455
|
+
label.textContent = todo.completed ? \`✓ \${todo.title}\` : todo.title;
|
|
456
|
+
const button = document.createElement("button");
|
|
457
|
+
button.type = "button";
|
|
458
|
+
button.textContent = todo.completed ? "Reopen" : "Done";
|
|
459
|
+
button.addEventListener("click", () => {
|
|
460
|
+
void toggleTodo(todo);
|
|
461
|
+
});
|
|
462
|
+
item.append(label, button);
|
|
463
|
+
return item;
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
form.addEventListener("submit", (event) => {
|
|
469
|
+
event.preventDefault();
|
|
470
|
+
const title = input.value.trim();
|
|
471
|
+
if (!title) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
input.value = "";
|
|
475
|
+
void addTodo(title);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
void loadTodos();
|
|
479
|
+
`;
|
|
480
|
+
};
|
|
481
|
+
buildIconSvg = () => {
|
|
482
|
+
return `<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
483
|
+
<rect width="160" height="160" rx="38" fill="#13212F" />
|
|
484
|
+
<path d="M44 49H116V63H44V49Z" fill="#F2C879" />
|
|
485
|
+
<path d="M44 76H116V90H44V76Z" fill="#F7F4EC" />
|
|
486
|
+
<path d="M44 103H92V117H44V103Z" fill="#8FD3C7" />
|
|
487
|
+
</svg>
|
|
488
|
+
`;
|
|
489
|
+
};
|
|
490
|
+
normalizeSlug = (value) => {
|
|
491
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
const APP_WASM_PLACEHOLDER_BASE64 = "AGFzbQEAAAABBwFgAn9/AX8DAgEABxMBD3N1bW1hcml6ZV9ub3RlcwAACg0BCwAgACABakHIAWoL";
|
|
495
|
+
//#endregion
|
|
496
|
+
export { AppTsHttpScaffoldTemplateService };
|