@akanjs/devkit 3.1.0-rc.8 → 3.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/CHANGELOG.md +382 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/akanConfig/akanConfig.test.ts +8 -4
- package/akanConfig/akanConfig.ts +2 -1
- package/applicationBuildRunner.test.ts +77 -2
- package/applicationBuildRunner.ts +28 -5
- package/artifact/implicitRootLayout.test.ts +11 -1
- package/artifact/implicitRootLayout.ts +32 -7
- package/biome.base.json +8 -3
- package/biomeBase.test.ts +78 -0
- package/biomeBase.ts +1 -1
- package/codeAgent/agent/AkanCodeCompaction.ts +49 -0
- package/codeAgent/agent/AkanCodeServices.ts +2 -3
- package/codeAgent/agent/CodeAgent.ts +46 -2
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
- package/codeAgent/agent/akanCodeCompaction.test.ts +36 -0
- package/codeAgent/agent/codeAgent.test.ts +61 -3
- package/codeAgent/agent/codeAgentContract.test.ts +1 -0
- package/codeAgent/index.ts +1 -0
- package/codeAgent/skills/akan-code/SKILL.md +4 -0
- package/codeAgent/skills/akan-store/SKILL.md +4 -2
- package/codeAgent/tools/AkanCodePlugins.ts +3 -1
- package/codeAgent/tools/DeepSeekWeb.ts +127 -0
- package/codeAgent/tools/WebToolPack.ts +149 -25
- package/codeAgent/tools/akanCodePlugins.test.ts +191 -0
- package/codeAgent/tools/deepSeekWeb.test.ts +113 -0
- package/commandDecorators/argMeta.ts +1 -0
- package/commandDecorators/command.ts +49 -10
- package/commandDecorators/commandBuilder.ts +6 -3
- package/commandDecorators/commandDecorators.test.ts +169 -6
- package/commandDecorators/helpFormatter.ts +2 -1
- package/commandDecorators/targetMeta.ts +10 -0
- package/executors.test.ts +53 -1
- package/executors.ts +10 -6
- package/frontendBuild/csrArtifactBuilder.test.ts +40 -0
- package/frontendBuild/csrArtifactBuilder.ts +24 -1
- package/frontendBuild/cssCandidateCache.test.ts +16 -0
- package/frontendBuild/cssCandidateCache.ts +8 -6
- package/frontendBuild/fontPruner.ts +1 -0
- package/frontendBuild/mcpAppViewBuilder.test.ts +170 -0
- package/frontendBuild/mcpAppViewBuilder.ts +245 -0
- package/incrementalBuilder/buildBatch.proc.ts +3 -3
- package/incrementalBuilder/buildBatchProtocol.ts +2 -2
- package/lint/__fixtures__/no-conditional-st-hook/bad.tsx +45 -0
- package/lint/__fixtures__/no-conditional-st-hook/good.tsx +33 -0
- package/lint/no-conditional-st-hook.grit +146 -0
- package/mobile/desktopServerStage.ts +1 -0
- package/package.json +21 -21
- package/piDependencyPins.test.ts +18 -19
- package/routeSourceValidator.test.ts +19 -4
- package/routeSourceValidator.ts +62 -14
- package/scanInfo.ts +36 -3
- package/subspace.test.ts +3 -2
- package/subspaceClone.ts +1 -12
- package/transforms/mcpAppPageTrimmer.test.ts +89 -0
- package/transforms/mcpAppPageTrimmer.ts +70 -0
- package/transforms/pureLazyMarker.test.ts +58 -0
- package/transforms/pureLazyMarker.ts +42 -0
- package/transforms/sideEffectImportOrder.test.ts +183 -0
- package/transforms/sideEffectImportOrder.ts +109 -0
|
@@ -61,4 +61,44 @@ console.info(await result.outputs[0].text());
|
|
|
61
61
|
expect(firstOutput).toBe(secondOutput);
|
|
62
62
|
expect(firstOutput).not.toContain(path.basename(first));
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
//? The shape of `@lexical/code-prism` under a `lazy()` editor, which threw in every view and CSR shell carrying it.
|
|
66
|
+
test("runs a lazily imported module's CommonJS side-effect import before the imports after it", async () => {
|
|
67
|
+
const root = await makeTempRoot();
|
|
68
|
+
const app = { getPublicEnv: () => ({ AKAN_PUBLIC_ENV: "main" }) };
|
|
69
|
+
await writeText(path.join(root, "node_modules/core/package.json"), '{ "name": "core", "main": "index.js" }\n');
|
|
70
|
+
await writeText(
|
|
71
|
+
path.join(root, "node_modules/core/index.js"),
|
|
72
|
+
"var Core = { langs: [] };\nglobalThis.Core = Core;\nmodule.exports = Core;\n",
|
|
73
|
+
);
|
|
74
|
+
await writeText(path.join(root, "node_modules/core/components/a.js"), 'Core.langs.push("a");\n');
|
|
75
|
+
await writeText(
|
|
76
|
+
path.join(root, "node_modules/highlight/package.json"),
|
|
77
|
+
'{ "name": "highlight", "type": "module", "main": "index.js" }\n',
|
|
78
|
+
);
|
|
79
|
+
await writeText(
|
|
80
|
+
path.join(root, "node_modules/highlight/index.js"),
|
|
81
|
+
'import "core";\nimport "core/components/a.js";\nexport const langs = () => globalThis.Core.langs;\n',
|
|
82
|
+
);
|
|
83
|
+
await writeText(
|
|
84
|
+
path.join(root, "entry.ts"),
|
|
85
|
+
'import("highlight").then(({ langs }) => console.info(langs().join()));\n',
|
|
86
|
+
);
|
|
87
|
+
const result = await Bun.build({
|
|
88
|
+
...CsrArtifactBuilder.bundleOptions(app as never, "build"),
|
|
89
|
+
entrypoints: [path.join(root, "entry.ts")],
|
|
90
|
+
});
|
|
91
|
+
expect(result.success).toBe(true);
|
|
92
|
+
await Bun.write(path.join(root, "out.js"), (await result.outputs[0]?.text()) ?? "");
|
|
93
|
+
|
|
94
|
+
const proc = Bun.spawn([process.execPath, "out.js"], { cwd: root, stdout: "pipe", stderr: "pipe" });
|
|
95
|
+
const [stdout, stderr] = await Promise.all([
|
|
96
|
+
new Response(proc.stdout).text(),
|
|
97
|
+
new Response(proc.stderr).text(),
|
|
98
|
+
proc.exited,
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
expect(stderr).toBe("");
|
|
102
|
+
expect(stdout.trim()).toBe("a");
|
|
103
|
+
});
|
|
64
104
|
});
|
|
@@ -4,8 +4,10 @@ import type { BaseBuildArtifact } from "akanjs/server";
|
|
|
4
4
|
import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
6
|
import { createBrowserDirnamePlugin } from "../transforms/browserDirnamePlugin";
|
|
7
|
+
import { SideEffectImportOrder } from "../transforms/sideEffectImportOrder";
|
|
7
8
|
import { bundleDefine } from "./bundleDefine";
|
|
8
9
|
import { CsrEntryFiles } from "./csrEntryFiles";
|
|
10
|
+
import { type McpAppView, McpAppViewBuilder } from "./mcpAppViewBuilder";
|
|
9
11
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
10
12
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
11
13
|
|
|
@@ -77,11 +79,32 @@ export class CsrArtifactBuilder {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
await this.#inlineCsrArtifacts(cssAssets);
|
|
82
|
+
await this.#buildMcpAppViews(pageEntries, { basePaths, cssAssets, options });
|
|
80
83
|
if (this.#command === "build") await CsrArtifactBuilder.writePublicEnv(this.#app, options.define);
|
|
81
84
|
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
82
85
|
return { outputDir: this.#outputDir };
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/** The MCP App views alone: a dev server under the CSR registry reads them, with no `csr/` to build beside them. */
|
|
89
|
+
async buildMcpAppViews(): Promise<McpAppView[]> {
|
|
90
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
91
|
+
if (pageKeys.length === 0) return [];
|
|
92
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
93
|
+
const akanConfig = await this.#app.getConfig();
|
|
94
|
+
return await this.#buildMcpAppViews(pageEntries, {
|
|
95
|
+
basePaths: [...akanConfig.basePaths],
|
|
96
|
+
cssAssets: await this.#loadCssAssets(),
|
|
97
|
+
options: CsrArtifactBuilder.bundleOptions(this.#app, this.#command, akanConfig.optimizeImports),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async #buildMcpAppViews(pageEntries: PageEntry[], option: Parameters<McpAppViewBuilder["build"]>[1]) {
|
|
102
|
+
const views = await new McpAppViewBuilder(this.#app, this.#command).build(pageEntries, option);
|
|
103
|
+
if (views.length)
|
|
104
|
+
this.#app.verbose(`[csr-build] ${views.length} MCP App view(s): ${views.map((view) => view.name).join(", ")}`);
|
|
105
|
+
return views;
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
//? No `env` option: Bun inlines the `.env` it read at process start over `define`, so a native build's `--env` lost.
|
|
86
109
|
static bundleOptions(app: App, command: "build" | "start", optimizeImports: string[] = []) {
|
|
87
110
|
return {
|
|
@@ -91,7 +114,7 @@ export class CsrArtifactBuilder {
|
|
|
91
114
|
define: bundleDefine(app, command, "csr"),
|
|
92
115
|
optimizeImports,
|
|
93
116
|
// A raw `.css` in the route graph is Tailwind source; the base artifact's compiled sheet is the only stylesheet.
|
|
94
|
-
plugins: [PagesBundleBuilder.createCssStubPlugin(), createBrowserDirnamePlugin()],
|
|
117
|
+
plugins: [PagesBundleBuilder.createCssStubPlugin(), createBrowserDirnamePlugin(), SideEffectImportOrder.plugin()],
|
|
95
118
|
};
|
|
96
119
|
}
|
|
97
120
|
|
|
@@ -23,6 +23,22 @@ describe("CssCandidateCache token extraction", () => {
|
|
|
23
23
|
expect(candidates).not.toContain("_td");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test("keeps a bracket nested one level inside another whole, rather than cutting it at the inner `]`", async () => {
|
|
27
|
+
const candidates = await scan(
|
|
28
|
+
`const c = "hidden [[data-theme=light]_&]:block has-[[aria-expanded=true]]:opacity-100 [&_[data-x]]:p-2 [&:has([data-state=open])]:p-1";`,
|
|
29
|
+
);
|
|
30
|
+
expect(candidates).toEqual(
|
|
31
|
+
expect.arrayContaining([
|
|
32
|
+
"[[data-theme=light]_&]:block",
|
|
33
|
+
"has-[[aria-expanded=true]]:opacity-100",
|
|
34
|
+
"[&_[data-x]]:p-2",
|
|
35
|
+
"[&:has([data-state=open])]:p-1",
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
expect(candidates).not.toContain("[[data-theme=light]_");
|
|
39
|
+
expect(candidates).not.toContain("[&_[data-x]");
|
|
40
|
+
});
|
|
41
|
+
|
|
26
42
|
test("keeps Tailwind v4's variable shorthand whole, and leaves a call's argument its own candidate", async () => {
|
|
27
43
|
const candidates = await scan(
|
|
28
44
|
`const c = "max-md:top-(--keyboard-top) min-h-(--x) bg-(color:--brand)/50"; cn("flex"); fn(value);`,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
// The leading `[` alternation keeps arbitrary variants (`[&_td]:px-3`) whole
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
3
|
+
// The leading `[` alternation keeps arbitrary variants (`[&_td]:px-3`) whole, and a bracket may hold one nested
|
|
4
|
+
// bracket (`[&_[data-slot=x]]:p-2`, `has-[[aria-expanded=true]]:x`), or the candidate is cut at the inner `]` and the
|
|
5
|
+
// rule silently never compiles. Brackets hold no whitespace: a labelled tuple type scanned as a class compiles to a
|
|
6
|
+
// rule lightningcss cannot parse, and it then drops every variant rule. Parentheses pass only as Tailwind v4's
|
|
7
|
+
// variable shorthand, `(--x)` or `(length:--x)`, so `cn("flex")` still yields `flex` on its own.
|
|
8
|
+
const CANDIDATE_RE =
|
|
9
|
+
/-?(?:[\w@]|\[(?:[^[\]\s]|\[[^[\]\s]+\])+\])[\w:/.-]*(?:(?:\[(?:[^[\]\s]|\[[^[\]\s]+\])+\]|\((?:[a-z-]+:)?--[^)\s]+\))[\w:/.-]*)*/g;
|
|
8
10
|
|
|
9
11
|
interface CachedFile {
|
|
10
12
|
mtimeMs: number;
|
|
@@ -20,7 +22,7 @@ interface CacheFile {
|
|
|
20
22
|
/** Tailwind candidates per source file, on disk: the batch worker that compiles CSS exits before the next save. */
|
|
21
23
|
export class CssCandidateCache {
|
|
22
24
|
// Bump when the token regex or the entry shape changes, so stale extractions are not reused.
|
|
23
|
-
static readonly #version =
|
|
25
|
+
static readonly #version = 5;
|
|
24
26
|
readonly #path: string;
|
|
25
27
|
readonly #entries = new Map<string, CachedFile>();
|
|
26
28
|
#dirty = false;
|
|
@@ -103,6 +103,7 @@ export class FontPruner {
|
|
|
103
103
|
{ dir: path.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
|
|
104
104
|
{ dir: path.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
|
|
105
105
|
{ dir: path.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled },
|
|
106
|
+
{ dir: path.join(this.#app.dist.cwdPath, "mcp-apps"), label: "mcp-apps", needles: bundled },
|
|
106
107
|
];
|
|
107
108
|
for (const root of roots) {
|
|
108
109
|
const pending = root.needles.filter((candidate) => !referrers.has(candidate.rel));
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
4
|
+
import { McpAppViewBuilder } from "./mcpAppViewBuilder";
|
|
5
|
+
|
|
6
|
+
const makeTempRoot = tempDirs("akan-mcp-app-views-");
|
|
7
|
+
|
|
8
|
+
const memoPage = [
|
|
9
|
+
'import { page } from "akanjs/client";',
|
|
10
|
+
"export default page()",
|
|
11
|
+
' .param("memoId", ID)',
|
|
12
|
+
' .app("showMemo", "Show one memo.", ({ memoId }) => null)',
|
|
13
|
+
' .app("showMemoImages", "Show its images.", ({ memoId }) => null)',
|
|
14
|
+
" .render(() => null);",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
describe("McpAppViewBuilder.discover", () => {
|
|
18
|
+
test("finds each app of each page, under the root layout that renders the system provider", async () => {
|
|
19
|
+
const appRoot = await makeTempRoot();
|
|
20
|
+
const file = (rel: string) => path.join(appRoot, rel);
|
|
21
|
+
await write(file("generated/__root_layout.tsx"), "export function McpAppRoot() {}\n");
|
|
22
|
+
await write(file("generated/(home)/__root_layout.tsx"), "export const guard = userLayout.guard;\n");
|
|
23
|
+
await write(file("page/(home)/memo/[memoId].tsx"), memoPage);
|
|
24
|
+
await write(
|
|
25
|
+
file("page/admin/report.tsx"),
|
|
26
|
+
'export default page().app("showReport", "Show the report.", () => null).render(() => null);',
|
|
27
|
+
);
|
|
28
|
+
await write(file("page/plain.tsx"), "export default page().render(() => null);");
|
|
29
|
+
const entries = [
|
|
30
|
+
{ key: "./__root_layout.tsx", moduleAbsPath: file("generated/__root_layout.tsx") },
|
|
31
|
+
{ key: "./(home)/__root_layout.tsx", moduleAbsPath: file("generated/(home)/__root_layout.tsx") },
|
|
32
|
+
{ key: "./(home)/memo/[memoId].tsx", moduleAbsPath: file("page/(home)/memo/[memoId].tsx") },
|
|
33
|
+
{ key: "./admin/report.tsx", moduleAbsPath: file("page/admin/report.tsx") },
|
|
34
|
+
{ key: "./plain.tsx", moduleAbsPath: file("page/plain.tsx") },
|
|
35
|
+
];
|
|
36
|
+
const views = await McpAppViewBuilder.discover(entries, ["admin"]);
|
|
37
|
+
expect(views.map(({ name, page, root, basePath }) => [name, page.key, root?.key, basePath])).toEqual([
|
|
38
|
+
["showMemo", "./(home)/memo/[memoId].tsx", "./__root_layout.tsx", ""],
|
|
39
|
+
["showMemoImages", "./(home)/memo/[memoId].tsx", "./__root_layout.tsx", ""],
|
|
40
|
+
["showReport", "./admin/report.tsx", "./__root_layout.tsx", "admin"],
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("takes the overrides over each page by route, outermost first, and none from a sibling group", async () => {
|
|
45
|
+
const appRoot = await makeTempRoot();
|
|
46
|
+
const file = (rel: string) => path.join(appRoot, rel);
|
|
47
|
+
await write(file("page/(home)/memo.tsx"), memoPage);
|
|
48
|
+
const entries = [
|
|
49
|
+
{ key: "./(home)/memo.tsx", moduleAbsPath: file("page/(home)/memo.tsx") },
|
|
50
|
+
{ key: "./(home)/memo/_overrides.tsx", moduleAbsPath: file("generated/memo_overrides.tsx") },
|
|
51
|
+
{ key: "./_overrides.tsx", moduleAbsPath: file("generated/_overrides.tsx") },
|
|
52
|
+
{ key: "./(home)/_overrides.tsx", moduleAbsPath: file("generated/home_overrides.tsx") },
|
|
53
|
+
{ key: "./(tab)/_overrides.tsx", moduleAbsPath: file("generated/tab_overrides.tsx") },
|
|
54
|
+
];
|
|
55
|
+
const [view] = await McpAppViewBuilder.discover(entries, []);
|
|
56
|
+
expect(view?.rootOverrides).toEqual([]);
|
|
57
|
+
expect(view?.overrides.map((entry) => entry.key)).toEqual([
|
|
58
|
+
"./_overrides.tsx",
|
|
59
|
+
"./(home)/_overrides.tsx",
|
|
60
|
+
"./(home)/memo/_overrides.tsx",
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("puts the overrides on the root layout's node and above it outside that layout, as the route tree does", async () => {
|
|
65
|
+
const appRoot = await makeTempRoot();
|
|
66
|
+
const file = (rel: string) => path.join(appRoot, rel);
|
|
67
|
+
await write(file("generated/(home)/__root_layout.tsx"), "export function McpAppRoot() {}\n");
|
|
68
|
+
await write(file("page/(home)/memo.tsx"), memoPage);
|
|
69
|
+
const entries = [
|
|
70
|
+
{ key: "./(home)/__root_layout.tsx", moduleAbsPath: file("generated/(home)/__root_layout.tsx") },
|
|
71
|
+
{ key: "./(home)/memo.tsx", moduleAbsPath: file("page/(home)/memo.tsx") },
|
|
72
|
+
{ key: "./(home)/memo/_overrides.tsx", moduleAbsPath: file("generated/memo_overrides.tsx") },
|
|
73
|
+
{ key: "./(home)/_overrides.tsx", moduleAbsPath: file("generated/home_overrides.tsx") },
|
|
74
|
+
{ key: "./_overrides.tsx", moduleAbsPath: file("generated/_overrides.tsx") },
|
|
75
|
+
];
|
|
76
|
+
const [view] = await McpAppViewBuilder.discover(entries, []);
|
|
77
|
+
expect(view?.root?.key).toBe("./(home)/__root_layout.tsx");
|
|
78
|
+
expect(view?.rootOverrides.map((entry) => entry.key)).toEqual(["./_overrides.tsx", "./(home)/_overrides.tsx"]);
|
|
79
|
+
expect(view?.overrides.map((entry) => entry.key)).toEqual(["./(home)/memo/_overrides.tsx"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("keeps an override inside the root layout when its folder holds a layout above the root's", async () => {
|
|
83
|
+
const appRoot = await makeTempRoot();
|
|
84
|
+
const file = (rel: string) => path.join(appRoot, rel);
|
|
85
|
+
await write(file("generated/oauth/__root_layout.tsx"), "export function McpAppRoot() {}\n");
|
|
86
|
+
await write(file("page/(libs)/(shared)/oauth/consent.tsx"), memoPage);
|
|
87
|
+
const entries = [
|
|
88
|
+
{ key: "./(libs)/(shared)/oauth/__root_layout.tsx", moduleAbsPath: file("generated/oauth/__root_layout.tsx") },
|
|
89
|
+
{ key: "./(libs)/(shared)/_layout.tsx", moduleAbsPath: file("page/(libs)/(shared)/_layout.tsx") },
|
|
90
|
+
{ key: "./(libs)/(shared)/oauth/consent.tsx", moduleAbsPath: file("page/(libs)/(shared)/oauth/consent.tsx") },
|
|
91
|
+
{ key: "./(libs)/(shared)/oauth/_overrides.tsx", moduleAbsPath: file("generated/oauth_overrides.tsx") },
|
|
92
|
+
{ key: "./(libs)/(shared)/_overrides.tsx", moduleAbsPath: file("generated/shared_overrides.tsx") },
|
|
93
|
+
{ key: "./_overrides.tsx", moduleAbsPath: file("generated/_overrides.tsx") },
|
|
94
|
+
];
|
|
95
|
+
const [view] = await McpAppViewBuilder.discover(entries, []);
|
|
96
|
+
expect(view?.rootOverrides.map((entry) => entry.key)).toEqual([
|
|
97
|
+
"./_overrides.tsx",
|
|
98
|
+
"./(libs)/(shared)/oauth/_overrides.tsx",
|
|
99
|
+
]);
|
|
100
|
+
expect(view?.overrides.map((entry) => entry.key)).toEqual(["./(libs)/(shared)/_overrides.tsx"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("names two pages that claim one app, and a page with no root layout to provide for it", async () => {
|
|
104
|
+
const appRoot = await makeTempRoot();
|
|
105
|
+
const file = (rel: string) => path.join(appRoot, rel);
|
|
106
|
+
await write(file("page/a.tsx"), memoPage);
|
|
107
|
+
await write(file("page/b.tsx"), memoPage);
|
|
108
|
+
const entries = [
|
|
109
|
+
{ key: "./a.tsx", moduleAbsPath: file("page/a.tsx") },
|
|
110
|
+
{ key: "./b.tsx", moduleAbsPath: file("page/b.tsx") },
|
|
111
|
+
];
|
|
112
|
+
await expect(McpAppViewBuilder.discover(entries, [])).rejects.toThrow(
|
|
113
|
+
'app "showMemo" is declared by both ./a.tsx and ./b.tsx',
|
|
114
|
+
);
|
|
115
|
+
const [view] = await McpAppViewBuilder.discover(entries.slice(0, 1), []);
|
|
116
|
+
expect(view?.root).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("refuses a name MCP would not list, and two that differ only in case", async () => {
|
|
120
|
+
const appRoot = await makeTempRoot();
|
|
121
|
+
const file = (rel: string) => path.join(appRoot, rel);
|
|
122
|
+
await write(file("page/a.tsx"), memoPage);
|
|
123
|
+
await write(file("page/b.tsx"), memoPage.replace('"showMemo"', '"ShowMemo"'));
|
|
124
|
+
await write(file("page/c.tsx"), memoPage.replace('"showMemo"', '"../showMemo"'));
|
|
125
|
+
const entryOf = (name: string) => ({ key: `./${name}.tsx`, moduleAbsPath: file(`page/${name}.tsx`) });
|
|
126
|
+
await expect(McpAppViewBuilder.discover([entryOf("a"), entryOf("b")], [])).rejects.toThrow(
|
|
127
|
+
'apps "showMemo" (./a.tsx) and "ShowMemo" (./b.tsx) differ only in case',
|
|
128
|
+
);
|
|
129
|
+
await expect(McpAppViewBuilder.discover([entryOf("c")], [])).rejects.toThrow(
|
|
130
|
+
'app name "../showMemo" in ./c.tsx must match',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("McpAppViewBuilder.entryOf", () => {
|
|
136
|
+
test("boots the one app with its page, the root layout and overrides it sits under, by relative path", () => {
|
|
137
|
+
const view = {
|
|
138
|
+
name: "showMemo",
|
|
139
|
+
page: { key: "./memo/[memoId].tsx", moduleAbsPath: "/app/page/memo/[memoId].tsx" },
|
|
140
|
+
root: { key: "./__root_layout.tsx", moduleAbsPath: "/app/.akan/generated/root-layouts/__root_layout.tsx" },
|
|
141
|
+
rootOverrides: [{ key: "./_overrides.tsx", moduleAbsPath: "/app/.akan/generated/overrides/_overrides.tsx" }],
|
|
142
|
+
overrides: [{ key: "./memo/_overrides.tsx", moduleAbsPath: "/app/.akan/generated/overrides/memo.tsx" }],
|
|
143
|
+
basePath: "admin",
|
|
144
|
+
};
|
|
145
|
+
expect(McpAppViewBuilder.entryOf(view, "/app/.akan/generated/mcp-apps")).toBe(
|
|
146
|
+
[
|
|
147
|
+
'import { McpAppBoot } from "akanjs/webkit";',
|
|
148
|
+
'import * as page from "../../../page/memo/[memoId].tsx";',
|
|
149
|
+
'import * as root from "../root-layouts/__root_layout.tsx";',
|
|
150
|
+
'import rootOverride0 from "../overrides/_overrides.tsx";',
|
|
151
|
+
'import override0 from "../overrides/memo.tsx";',
|
|
152
|
+
"void McpAppBoot.run({",
|
|
153
|
+
' app: "showMemo",',
|
|
154
|
+
" page,",
|
|
155
|
+
" root,",
|
|
156
|
+
" rootOverrides: [rootOverride0],",
|
|
157
|
+
" overrides: [override0],",
|
|
158
|
+
' basePath: "admin",',
|
|
159
|
+
"});",
|
|
160
|
+
"",
|
|
161
|
+
].join("\n"),
|
|
162
|
+
);
|
|
163
|
+
expect(
|
|
164
|
+
McpAppViewBuilder.entryOf(
|
|
165
|
+
{ ...view, root: null, rootOverrides: [], overrides: [] },
|
|
166
|
+
"/app/.akan/generated/mcp-apps",
|
|
167
|
+
),
|
|
168
|
+
).toContain("const root = {};\nvoid McpAppBoot.run({");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { mkdir, readdir, rename, rm, unlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseRouteModuleKey } from "akanjs/common";
|
|
4
|
+
import type { BaseBuildArtifact } from "akanjs/server";
|
|
5
|
+
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
6
|
+
import type { App } from "../commandDecorators";
|
|
7
|
+
import { McpAppPageTrimmer } from "../transforms/mcpAppPageTrimmer";
|
|
8
|
+
import { PureLazyMarker } from "../transforms/pureLazyMarker";
|
|
9
|
+
import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
10
|
+
import { CsrEntryFiles } from "./csrEntryFiles";
|
|
11
|
+
|
|
12
|
+
type CssAsset = NonNullable<BaseBuildArtifact["cssAssets"]>[string];
|
|
13
|
+
|
|
14
|
+
interface McpAppViewBuildOption {
|
|
15
|
+
basePaths: string[];
|
|
16
|
+
cssAssets: Record<string, CssAsset>;
|
|
17
|
+
/** The CSR shell's own bundle options, so a view inlines the same env and stubs the same stylesheets. */
|
|
18
|
+
options: ReturnType<typeof CsrArtifactBuilder.bundleOptions>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface McpAppView {
|
|
22
|
+
name: string;
|
|
23
|
+
page: PageEntry;
|
|
24
|
+
/** The generated root layout that renders `System.Provider` for the page, whose `McpAppRoot` the view uses. */
|
|
25
|
+
root: PageEntry | null;
|
|
26
|
+
/** The generated wrappers of the `_overrides.tsx` the web draws outside the root layout, outermost first: they also
|
|
27
|
+
* reach what it draws beside the page, its toasts and approvals. */
|
|
28
|
+
rootOverrides: PageEntry[];
|
|
29
|
+
/** The generated wrappers of the rest of the `_overrides.tsx` over the page, outermost first, around it alone. */
|
|
30
|
+
overrides: PageEntry[];
|
|
31
|
+
basePath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//* One single-file HTML per `.app()`, which an MCP host renders as that app's view: its entry imports the app's page,
|
|
35
|
+
//* trimmed to its loads, and the page's root layout for its providers. Built beside `csr/`, never inside it: a native
|
|
36
|
+
//* app packs `csr/`, which needs none of them.
|
|
37
|
+
export class McpAppViewBuilder {
|
|
38
|
+
/** The folder the server reads views from: `WebRouter.mcpAppViewDir`. */
|
|
39
|
+
static readonly dirName = "mcp-apps";
|
|
40
|
+
/** `PageDefinition.promptName`, which also keeps a view's file name inside its folder. */
|
|
41
|
+
static readonly #appName = /^[A-Za-z0-9_-]{1,64}$/;
|
|
42
|
+
|
|
43
|
+
readonly #app: App;
|
|
44
|
+
readonly #command: "build" | "start";
|
|
45
|
+
|
|
46
|
+
constructor(app: App, command: "build" | "start") {
|
|
47
|
+
this.#app = app;
|
|
48
|
+
this.#command = command;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get outputDir(): string {
|
|
52
|
+
return this.#command === "build"
|
|
53
|
+
? path.join(this.#app.dist.cwdPath, McpAppViewBuilder.dirName)
|
|
54
|
+
: path.join(this.#app.cwdPath, ".akan", "artifact", McpAppViewBuilder.dirName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async build(
|
|
58
|
+
pageEntries: PageEntry[],
|
|
59
|
+
{ basePaths, cssAssets, options }: McpAppViewBuildOption,
|
|
60
|
+
): Promise<McpAppView[]> {
|
|
61
|
+
const views = await McpAppViewBuilder.discover(pageEntries, basePaths);
|
|
62
|
+
if (!views.length) {
|
|
63
|
+
await rm(this.outputDir, { recursive: true, force: true });
|
|
64
|
+
return views;
|
|
65
|
+
}
|
|
66
|
+
//? Built aside, then moved in a file at a time: a host may read a view while `akan start` rebuilds them.
|
|
67
|
+
const stagingDir = `${this.outputDir}.next`;
|
|
68
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
69
|
+
const generatedDir = path.join(this.#app.cwdPath, ".akan", "generated", McpAppViewBuilder.dirName);
|
|
70
|
+
await rm(generatedDir, { recursive: true, force: true });
|
|
71
|
+
await mkdir(generatedDir, { recursive: true });
|
|
72
|
+
const files = Object.fromEntries(
|
|
73
|
+
views.flatMap((view) => [
|
|
74
|
+
[path.join(generatedDir, `${view.name}.html`), McpAppViewBuilder.htmlOf(view)],
|
|
75
|
+
[path.join(generatedDir, `${view.name}.view.tsx`), McpAppViewBuilder.entryOf(view, generatedDir)],
|
|
76
|
+
]),
|
|
77
|
+
);
|
|
78
|
+
const pagePaths = [...new Set(views.map((view) => view.page.moduleAbsPath))];
|
|
79
|
+
const result = await Bun.build({
|
|
80
|
+
...options,
|
|
81
|
+
plugins: [...options.plugins, McpAppPageTrimmer.plugin(pagePaths), PureLazyMarker.plugin()],
|
|
82
|
+
entrypoints: views.map((view) => path.join(generatedDir, `${view.name}.html`)),
|
|
83
|
+
files,
|
|
84
|
+
root: generatedDir,
|
|
85
|
+
outdir: stagingDir,
|
|
86
|
+
});
|
|
87
|
+
if (!result.success) {
|
|
88
|
+
const logs = result.logs.map((log) => log.message).join("\n");
|
|
89
|
+
throw new Error(`[mcp-app-build] failed${logs ? `\n${logs}` : ""}`);
|
|
90
|
+
}
|
|
91
|
+
for (const view of views) await this.#inline(view, stagingDir, cssAssets[view.basePath]);
|
|
92
|
+
await this.#publish(views, stagingDir);
|
|
93
|
+
return views;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async #publish(views: McpAppView[], stagingDir: string) {
|
|
97
|
+
const names = new Set(views.map((view) => `${view.name}.html`));
|
|
98
|
+
await mkdir(this.outputDir, { recursive: true });
|
|
99
|
+
for (const name of names) await rename(path.join(stagingDir, name), path.join(this.outputDir, name));
|
|
100
|
+
for (const name of await readdir(this.outputDir))
|
|
101
|
+
if (!names.has(name)) await rm(path.join(this.outputDir, name), { recursive: true, force: true });
|
|
102
|
+
const strays = await readdir(stagingDir);
|
|
103
|
+
if (strays.length)
|
|
104
|
+
this.#app.logger.warn(
|
|
105
|
+
`[mcp-app-build] ${strays.join(", ")} reached no view; a view loads nothing beside its own file`,
|
|
106
|
+
);
|
|
107
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Every `.app()` the pages declare, read off their sources without evaluating them. */
|
|
111
|
+
static async discover(pageEntries: PageEntry[], basePaths: string[]): Promise<McpAppView[]> {
|
|
112
|
+
const { RouteSourceValidator } = await import("../routeSourceValidator");
|
|
113
|
+
const roots = pageEntries.filter((entry) => parseRouteModuleKey(entry.key).isInternalRootLayout);
|
|
114
|
+
const overrides = pageEntries.filter((entry) => parseRouteModuleKey(entry.key).kind === "overrides");
|
|
115
|
+
const layoutNodes = new Set(
|
|
116
|
+
pageEntries.flatMap((entry) => {
|
|
117
|
+
const parsed = parseRouteModuleKey(entry.key);
|
|
118
|
+
return parsed.kind === "layout" && !parsed.isInternalRootLayout ? [parsed.routeSegments.join("/")] : [];
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const owners = new Map<string, { name: string; key: string }>();
|
|
122
|
+
const views: McpAppView[] = [];
|
|
123
|
+
for (const page of pageEntries) {
|
|
124
|
+
if (parseRouteModuleKey(page.key).kind !== "page") continue;
|
|
125
|
+
const source = await Bun.file(page.moduleAbsPath).text();
|
|
126
|
+
const apps =
|
|
127
|
+
RouteSourceValidator.validateRouteSourceExports(source, page.moduleAbsPath, "page").chain?.apps ?? [];
|
|
128
|
+
for (const name of apps) {
|
|
129
|
+
if (!McpAppViewBuilder.#appName.test(name))
|
|
130
|
+
throw new Error(
|
|
131
|
+
`[route-convention] app name "${name}" in ${page.key} must match ${McpAppViewBuilder.#appName}`,
|
|
132
|
+
);
|
|
133
|
+
//? Keyed in lower case: a case-insensitive disk would write two such views to one file.
|
|
134
|
+
const owner = owners.get(name.toLowerCase());
|
|
135
|
+
if (owner?.name === name)
|
|
136
|
+
throw new Error(`[route-convention] app "${name}" is declared by both ${owner.key} and ${page.key}`);
|
|
137
|
+
if (owner)
|
|
138
|
+
throw new Error(
|
|
139
|
+
`[route-convention] apps "${owner.name}" (${owner.key}) and "${name}" (${page.key}) differ only in case; their views would share one file`,
|
|
140
|
+
);
|
|
141
|
+
owners.set(name.toLowerCase(), { name, key: page.key });
|
|
142
|
+
const root = await McpAppViewBuilder.#rootOf(page.key, roots);
|
|
143
|
+
views.push({
|
|
144
|
+
name,
|
|
145
|
+
page,
|
|
146
|
+
root,
|
|
147
|
+
...McpAppViewBuilder.#overridesOf(page.key, root, overrides, layoutNodes),
|
|
148
|
+
basePath: CsrEntryFiles.basePathOfPageKey(page.key, basePaths) ?? "",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return views;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static htmlOf(view: McpAppView): string {
|
|
156
|
+
return `<!doctype html>
|
|
157
|
+
<html lang="en">
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="utf-8" />
|
|
160
|
+
<title>${view.name}</title>
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<div id="root"></div>
|
|
165
|
+
<script type="module" src="./${view.name}.view.tsx"></script>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static entryOf(view: McpAppView, fromDir: string): string {
|
|
172
|
+
const specifier = (absPath: string) => {
|
|
173
|
+
const relative = path.relative(fromDir, absPath).split(path.sep).join("/");
|
|
174
|
+
return JSON.stringify(relative.startsWith(".") ? relative : `./${relative}`);
|
|
175
|
+
};
|
|
176
|
+
const importsOf = (entries: PageEntry[], name: string) =>
|
|
177
|
+
entries.map((entry, idx) => `import ${name}${idx} from ${specifier(entry.moduleAbsPath)};\n`).join("");
|
|
178
|
+
const namesOf = (entries: PageEntry[], name: string) => entries.map((_, idx) => `${name}${idx}`).join(", ");
|
|
179
|
+
return `import { McpAppBoot } from "akanjs/webkit";
|
|
180
|
+
import * as page from ${specifier(view.page.moduleAbsPath)};
|
|
181
|
+
${view.root ? `import * as root from ${specifier(view.root.moduleAbsPath)};` : "const root = {};"}
|
|
182
|
+
${importsOf(view.rootOverrides, "rootOverride")}${importsOf(view.overrides, "override")}void McpAppBoot.run({
|
|
183
|
+
app: ${JSON.stringify(view.name)},
|
|
184
|
+
page,
|
|
185
|
+
root,
|
|
186
|
+
rootOverrides: [${namesOf(view.rootOverrides, "rootOverride")}],
|
|
187
|
+
overrides: [${namesOf(view.overrides, "override")}],
|
|
188
|
+
basePath: ${JSON.stringify(view.basePath)},
|
|
189
|
+
});
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//? By route segments, as the route tree nests them (`RouteLayering`): a folder's `_overrides.tsx` also covers the
|
|
194
|
+
//? page file named like the folder (`memo/_overrides.tsx` over `memo.tsx`), and one on a root node sits outside the
|
|
195
|
+
//? root layout — the root layout's own node, or one above it that holds no layout of its own.
|
|
196
|
+
static #overridesOf(pageKey: string, root: PageEntry | null, overrides: PageEntry[], layoutNodes: Set<string>) {
|
|
197
|
+
const pageSegments = parseRouteModuleKey(pageKey).routeSegments;
|
|
198
|
+
const rootDepth = root ? parseRouteModuleKey(root.key).routeSegments.length : 0;
|
|
199
|
+
const over = overrides
|
|
200
|
+
.map((entry) => ({ entry, segments: parseRouteModuleKey(entry.key).routeSegments }))
|
|
201
|
+
.filter(({ segments }) => segments.every((segment, idx) => pageSegments[idx] === segment))
|
|
202
|
+
.sort((a, b) => a.segments.length - b.segments.length);
|
|
203
|
+
const onRootNode = (segments: string[]) =>
|
|
204
|
+
segments.length === rootDepth || (segments.length < rootDepth && !layoutNodes.has(segments.join("/")));
|
|
205
|
+
return {
|
|
206
|
+
rootOverrides: over.filter(({ segments }) => onRootNode(segments)).map(({ entry }) => entry),
|
|
207
|
+
overrides: over.filter(({ segments }) => !onRootNode(segments)).map(({ entry }) => entry),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//? The outermost root boundary over the page: the one a page is drawn under without an ancestor root, which alone
|
|
212
|
+
//? renders `System.Provider` and so exports `McpAppRoot`.
|
|
213
|
+
static async #rootOf(pageKey: string, roots: PageEntry[]): Promise<PageEntry | null> {
|
|
214
|
+
const dirOf = (key: string) => key.slice(0, key.lastIndexOf("/") + 1);
|
|
215
|
+
const pageDir = dirOf(pageKey);
|
|
216
|
+
const over = roots
|
|
217
|
+
.filter((root) => pageDir.startsWith(dirOf(root.key)))
|
|
218
|
+
.sort((a, b) => dirOf(a.key).length - dirOf(b.key).length);
|
|
219
|
+
for (const root of over)
|
|
220
|
+
if ((await Bun.file(root.moduleAbsPath).text()).includes("export function McpAppRoot")) return root;
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async #inline(view: McpAppView, dir: string, cssAsset: CssAsset | undefined) {
|
|
225
|
+
const htmlPath = path.join(dir, `${view.name}.html`);
|
|
226
|
+
let html = await Bun.file(htmlPath).text();
|
|
227
|
+
if (cssAsset) {
|
|
228
|
+
const artifactDir = path.join(
|
|
229
|
+
this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
|
|
230
|
+
".akan",
|
|
231
|
+
"artifact",
|
|
232
|
+
);
|
|
233
|
+
const css = await Bun.file(path.join(artifactDir, cssAsset.cssRelPath)).text();
|
|
234
|
+
html = CsrArtifactBuilder.injectBeforeHeadEnd(html, CsrArtifactBuilder.createInlineStyle(css));
|
|
235
|
+
} else this.#app.logger.warn(`[mcp-app-build] no compiled stylesheet for ${view.name}; its view ships without CSS`);
|
|
236
|
+
const scripts: string[] = [];
|
|
237
|
+
html = await CsrArtifactBuilder.replaceModuleScriptSrc(html, async (src) => {
|
|
238
|
+
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
239
|
+
scripts.push(jsPath);
|
|
240
|
+
return await Bun.file(jsPath).text();
|
|
241
|
+
});
|
|
242
|
+
await Bun.write(htmlPath, html);
|
|
243
|
+
for (const script of scripts) await unlink(script).catch(() => undefined);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -108,10 +108,10 @@ class BuildBatch {
|
|
|
108
108
|
async #buildShell(): Promise<void> {
|
|
109
109
|
const started = Date.now();
|
|
110
110
|
try {
|
|
111
|
-
await new CsrArtifactBuilder(this.#app, "start", "en", { cssAssets: this.#request.cssAssets }).
|
|
112
|
-
this.#logger.verbose(`
|
|
111
|
+
await new CsrArtifactBuilder(this.#app, "start", "en", { cssAssets: this.#request.cssAssets }).buildMcpAppViews();
|
|
112
|
+
this.#logger.verbose(`mcp-app-views ok (${Date.now() - started}ms)`);
|
|
113
113
|
} catch (err) {
|
|
114
|
-
this.#fail("shell", "
|
|
114
|
+
this.#fail("shell", "mcp-app-views", err);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -3,8 +3,8 @@ import type { BaseBuildArtifact, CssPayload, HmrTrace, PagesBundlePayload } from
|
|
|
3
3
|
|
|
4
4
|
export type OptimizedFonts = Awaited<ReturnType<FontOptimizer["optimize"]>>;
|
|
5
5
|
|
|
6
|
-
// `base` is the boot build: it always runs alone, before the builder can serve anything. `shell` is the
|
|
7
|
-
//
|
|
6
|
+
// `base` is the boot build: it always runs alone, before the builder can serve anything. `shell` is the MCP App views'
|
|
7
|
+
// single-file build under the dev CSR registry, made on request and reported to no tab.
|
|
8
8
|
export type BuildBatchNeed = "base" | "pages" | "css" | "csr" | "ssr" | "shell";
|
|
9
9
|
|
|
10
10
|
// JSON only: the worker is spawned per batch and receives this in its argv.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const A = () => {
|
|
2
|
+
const busy = st.use.xUploading() || !!st.use.xFormUploading().voice; // @flag
|
|
3
|
+
const shown = open && st.sel((s) => s.a); // @flag
|
|
4
|
+
const label = name ?? st.use.defaultName(); // @flag
|
|
5
|
+
const picked = open ? st.use.b() : null; // @flag
|
|
6
|
+
const latest = open ? null : st.ref((s) => s.c); // @flag
|
|
7
|
+
if (open) { const d = st.use.d(); } // @flag
|
|
8
|
+
if (open) { run(); } else { const e = st.slice.inOrg.use.e(); } // @flag
|
|
9
|
+
switch (mode) { case "a": st.use.f(); } // @flag
|
|
10
|
+
for (const item of items) st.use.g(); // @flag
|
|
11
|
+
while (more) st.use.h(); // @flag
|
|
12
|
+
try { run(); } catch { st.use.i(); } // @flag
|
|
13
|
+
const rows = items.map((item) => st.use.j()); // @flag
|
|
14
|
+
useEffect(() => { st.use.k(); }, []); // @flag
|
|
15
|
+
const tool = open && st.tool("x").desc("d").exec(() => run()); // @flag
|
|
16
|
+
const card = open ? st.tool("y").desc("d").card(() => null) : null; // @flag
|
|
17
|
+
const [l] = open ? st.useState("l", String).desc("d").init("") : []; // @flag
|
|
18
|
+
if (open) st.expose("m", String).desc("d").value("x"); // @flag
|
|
19
|
+
const save = st.tool("z").desc("d").exec(async () => { st.use.n(); }); // @flag
|
|
20
|
+
const { title = st.use.title() } = props; // @flag
|
|
21
|
+
let cached = initial; cached ||= st.use.s(); // @flag
|
|
22
|
+
cached ??= st.use.t(); // @flag
|
|
23
|
+
cached &&= st.use.u(); // @flag
|
|
24
|
+
for (let i = 0; i < st.use.count(); i++) run(i); // @flag
|
|
25
|
+
const formatted = props.format?.(st.use.v()); // @flag
|
|
26
|
+
const chained = props.api?.format(st.use.w()); // @flag
|
|
27
|
+
const computed = open ? st["use"].x() : null; // @flag
|
|
28
|
+
const computedKey = open ? st.use["y"]() : null; // @flag
|
|
29
|
+
const computedSlice = open ? st.slice["inOrg"].use.z() : null; // @flag
|
|
30
|
+
useEffect(() => { void (async () => { await load(); st.use.aa(); })(); }, []); // @flag
|
|
31
|
+
return <div onClick={() => st.use.o()} />; // @flag
|
|
32
|
+
};
|
|
33
|
+
export const B = () => <Load.Units renderList={(list) => <X a={st.use.p()} />} />; // @flag
|
|
34
|
+
export const C = () => <Load.Stream of={q}>{(v) => <X a={st.sel((s) => s.q)} />}</Load.Stream>; // @flag
|
|
35
|
+
export const useD = () => useMemo(() => st.use.r(), []); // @flag
|
|
36
|
+
export const E = ({ name = st.use.defaultName() }) => <p>{name}</p>; // @flag
|
|
37
|
+
export function F({ name = st.sel((s) => s.name) }) { return <p>{name}</p>; } // @flag
|
|
38
|
+
export const G = () => {
|
|
39
|
+
const renderRow = (item) => <X a={st.use.bb()} />; // @flag
|
|
40
|
+
return <Load.Units renderItem={renderRow} />;
|
|
41
|
+
};
|
|
42
|
+
export const H = () => {
|
|
43
|
+
function row(item) { return <X a={st.ref((s) => s.cc)} />; } // @flag
|
|
44
|
+
return <ul>{items.map(row)}</ul>;
|
|
45
|
+
};
|