catalyst-core 1.0.1 → 1.0.2-canary.2
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/bin/catalyst.js +105 -1
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +50 -0
- package/dist/cli/buildErrors.js +170 -0
- package/dist/cli/buildWarnings.js +28 -0
- package/dist/cli/devOutput.js +524 -0
- package/dist/cli/diagnostic.js +388 -0
- package/dist/cli/hints.js +69 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/spinner.js +141 -0
- package/dist/cli/theme.js +155 -0
- package/dist/errors/index.js +7 -3
- package/dist/errors/registry.js +8 -0
- package/dist/errors-index.json +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.jsx +1 -1
- package/dist/logger.js +61 -6
- package/dist/native/androidProject/app/build.gradle.kts +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
- package/dist/native/androidSetup.js +46 -57
- package/dist/native/buildAndroid/assets.js +22 -13
- package/dist/native/buildAndroid/build.js +98 -26
- package/dist/native/buildAndroid/config.js +43 -12
- package/dist/native/buildAndroid/index.js +49 -191
- package/dist/native/buildAppAndroid.js +19 -8
- package/dist/native/buildAppIos.js +35 -16
- package/dist/native/buildIos/assets.js +1 -6
- package/dist/native/buildIos/build.js +61 -94
- package/dist/native/buildIos/config.js +26 -8
- package/dist/native/buildIos/index.js +143 -64
- package/dist/native/buildIos/plugins.js +12 -15
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
- package/dist/native/pluginComposerAndroid.js +2 -3
- package/dist/native/renameAndroidProject.js +42 -20
- package/dist/native/setupEmulatorIos.js +28 -49
- package/dist/native/setupServer.js +12 -15
- package/dist/native/terminalProgress.js +186 -147
- package/dist/native/utils.js +335 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +314 -42
- package/dist/scripts/generateOfflineManifest.js +11 -1
- package/dist/scripts/loadEnvironmentVariables.js +24 -2
- package/dist/scripts/preServerInit.js +32 -10
- package/dist/scripts/preflight.js +115 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +65 -9
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/document/Body.d.ts +0 -11
- package/dist/server/renderer/document/Body.jsx +0 -10
- package/dist/server/renderer/document/Head.d.ts +0 -13
- package/dist/server/renderer/document/Head.jsx +0 -12
- package/dist/server/renderer/handler.jsx +87 -11
- package/dist/server/utils/validator.js +14 -11
- package/dist/vite/manifest-categorization-plugin.js +7 -5
- package/dist/vite/resolveDevServerConfig.js +9 -2
- package/dist/vite/vite.config.client.js +19 -1
- package/dist/vite/vite.config.js +96 -19
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +20 -14
- package/dist/web-router/components/MetaTag.test.jsx +108 -32
- package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
- package/dist/web-router/components/RouterDataProvider.jsx +35 -1
- package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
- package/dist/web-router/context.d.ts +15 -1
- package/dist/web-router/context.jsx +11 -0
- package/dist/webmcp/WebMcpProvider.jsx +234 -0
- package/dist/webmcp/declarative.js +207 -0
- package/dist/webmcp/declarative.test.js +122 -0
- package/dist/webmcp/errors.js +59 -0
- package/dist/webmcp/errors.test.js +47 -0
- package/dist/webmcp/index.js +40 -0
- package/dist/webmcp/native.js +37 -0
- package/dist/webmcp/native.test.js +41 -0
- package/dist/webmcp/pageInfo.js +119 -0
- package/dist/webmcp/pageInfo.test.js +85 -0
- package/dist/webmcp/registry.dom.test.js +175 -0
- package/dist/webmcp/registry.js +540 -0
- package/dist/webmcp/registry.test.js +228 -0
- package/dist/webmcp/routeContext.js +11 -0
- package/dist/webmcp/shim-entry.js +8 -0
- package/dist/webmcp/shim.js +149 -0
- package/dist/webmcp/useTool.js +124 -0
- package/package.json +4 -7
- package/test/cli/buildErrors.test.ts +173 -0
- package/test/cli/buildWarnings.test.cjs +62 -0
- package/test/cli/devOutput.test.ts +549 -0
- package/test/cli/diagnostic.test.ts +434 -0
- package/test/cli/output.test.cjs +90 -0
- package/test/cli/spinner.test.ts +177 -0
- package/test/cli/theme.test.ts +163 -0
- package/test/native/redactSecrets.test.ts +72 -0
- package/test/native/utils.test.ts +532 -0
- package/test/scripts/build.test.ts +275 -0
- package/test/scripts/preflight.test.ts +100 -2
- package/test/scripts/serve.test.ts +102 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
- package/test/server/handler.test.ts +17 -0
- package/test/server/logger.test.ts +185 -0
- package/test/server/manifestCache.test.ts +13 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/vitest.config.ts +4 -0
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { flattenHeadElements, pageInfoTool } from "./pageInfo.js"
|
|
3
|
+
|
|
4
|
+
// Plain objects with the stable `.type` / `.props` shape of a React element.
|
|
5
|
+
const el = (type, props = {}) => ({ type, props })
|
|
6
|
+
|
|
7
|
+
describe("flattenHeadElements", () => {
|
|
8
|
+
it("flattens title, description, canonical, keywords, og, twitter and other meta", () => {
|
|
9
|
+
const out = flattenHeadElements([
|
|
10
|
+
el("title", { children: "Home" }),
|
|
11
|
+
[
|
|
12
|
+
el("meta", { name: "description", content: "d" }),
|
|
13
|
+
el("meta", { name: "keywords", content: "k" }),
|
|
14
|
+
el("link", { rel: "canonical", href: "https://x.test/" }),
|
|
15
|
+
el("meta", { property: "og:title", content: "OG" }),
|
|
16
|
+
el("meta", { name: "twitter:card", content: "summary" }),
|
|
17
|
+
el("meta", { httpEquiv: "refresh", content: "5" }),
|
|
18
|
+
],
|
|
19
|
+
])
|
|
20
|
+
expect(out).toMatchObject({
|
|
21
|
+
title: "Home",
|
|
22
|
+
description: "d",
|
|
23
|
+
keywords: "k",
|
|
24
|
+
canonical: "https://x.test/",
|
|
25
|
+
og: { title: "OG" },
|
|
26
|
+
twitter: { card: "summary" },
|
|
27
|
+
other: { refresh: "5" },
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("joins array/number title children and drops empty groups", () => {
|
|
32
|
+
const out = flattenHeadElements([el("title", { children: ["Docs", " - ", 2] })])
|
|
33
|
+
expect(out.title).toBe("Docs - 2")
|
|
34
|
+
expect(out).not.toHaveProperty("og")
|
|
35
|
+
expect(out).not.toHaveProperty("twitter")
|
|
36
|
+
expect(out).not.toHaveProperty("other")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("ignores non-elements, keyless meta, and a canonical link without href", () => {
|
|
40
|
+
const out = flattenHeadElements([
|
|
41
|
+
null,
|
|
42
|
+
"text",
|
|
43
|
+
el(null),
|
|
44
|
+
el("meta", { content: "orphan" }),
|
|
45
|
+
el("link", { rel: "canonical" }),
|
|
46
|
+
])
|
|
47
|
+
expect(out.canonical).toBeNull()
|
|
48
|
+
expect(out.title).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("returns null title for missing or non-text children", () => {
|
|
52
|
+
expect(flattenHeadElements([el("title")]).title).toBeNull()
|
|
53
|
+
expect(flattenHeadElements([el("title", { children: { x: 1 } })]).title).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe("pageInfoTool", () => {
|
|
58
|
+
const matchWith = (fn) => [{ route: { component: { setMetaData: fn } } }]
|
|
59
|
+
|
|
60
|
+
it("collects setMetaData from every matched route, passing route data", async () => {
|
|
61
|
+
let seen
|
|
62
|
+
const tool = pageInfoTool(
|
|
63
|
+
() => matchWith((data) => ((seen = data), [el("title", { children: "T" })])),
|
|
64
|
+
() => ({ a: 1 })
|
|
65
|
+
)
|
|
66
|
+
const res = await tool.execute()
|
|
67
|
+
expect(seen).toEqual({ a: 1 })
|
|
68
|
+
expect(res.title).toBe("T")
|
|
69
|
+
expect(res._note).toBeUndefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("survives a throwing setMetaData and notes that nothing was reported", async () => {
|
|
73
|
+
const tool = pageInfoTool(() => matchWith(() => { throw new Error("nope") }))
|
|
74
|
+
const res = await tool.execute()
|
|
75
|
+
expect(res.title).toBeNull()
|
|
76
|
+
expect(res._note).toMatch(/no setMetaData/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("tolerates no matches and no route-data getter", async () => {
|
|
80
|
+
const tool = pageInfoTool(() => null)
|
|
81
|
+
const res = await tool.execute()
|
|
82
|
+
expect(res._note).toBeDefined()
|
|
83
|
+
expect(tool.annotations.readOnlyHint).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Registry behaviour that needs a DOM: lifecycle events and the native /
|
|
4
|
+
* shim `document.modelContext` backend contract (receipt shapes, spec
|
|
5
|
+
* re-registration, argument validation).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
|
8
|
+
import {
|
|
9
|
+
registerTool,
|
|
10
|
+
unregisterTool,
|
|
11
|
+
updateToolRoute,
|
|
12
|
+
updateToolSpec,
|
|
13
|
+
invokeTool,
|
|
14
|
+
inspect,
|
|
15
|
+
isNative,
|
|
16
|
+
hasBackend,
|
|
17
|
+
newOwnerKey,
|
|
18
|
+
__resetForTests,
|
|
19
|
+
} from "./registry.js"
|
|
20
|
+
|
|
21
|
+
const makeSpec = (over = {}) => ({
|
|
22
|
+
name: "t",
|
|
23
|
+
description: "d",
|
|
24
|
+
inputSchema: { type: "object", properties: {} },
|
|
25
|
+
execute: async () => "ok",
|
|
26
|
+
...over,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function listen(type) {
|
|
30
|
+
const events = []
|
|
31
|
+
const h = (e) => events.push(e.detail)
|
|
32
|
+
document.addEventListener(type, h)
|
|
33
|
+
return { events, off: () => document.removeEventListener(type, h) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("registry with a DOM", () => {
|
|
37
|
+
beforeEach(() => __resetForTests())
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
delete document.modelContext
|
|
40
|
+
__resetForTests()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("emits register / execute / unregister events", async () => {
|
|
44
|
+
const reg = listen("webmcp:register")
|
|
45
|
+
const start = listen("webmcp:execute:start")
|
|
46
|
+
const end = listen("webmcp:execute:end")
|
|
47
|
+
const unreg = listen("webmcp:unregister")
|
|
48
|
+
const k = newOwnerKey()
|
|
49
|
+
registerTool(k, "/r", makeSpec())
|
|
50
|
+
await invokeTool("t", { x: 1 })
|
|
51
|
+
unregisterTool(k)
|
|
52
|
+
expect(reg.events[0]).toMatchObject({ name: "t", routePath: "/r", replaced: false })
|
|
53
|
+
expect(start.events[0]).toMatchObject({ name: "t", args: { x: 1 } })
|
|
54
|
+
expect(end.events[0]).toMatchObject({ name: "t", ok: true, result: "ok" })
|
|
55
|
+
expect(unreg.events[0]).toMatchObject({ name: "t" })
|
|
56
|
+
;[reg, start, end, unreg].forEach((l) => l.off())
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("reports a failed execute and an unknown tool as ok:false end events", async () => {
|
|
60
|
+
const end = listen("webmcp:execute:end")
|
|
61
|
+
registerTool(newOwnerKey(), "", makeSpec({ execute: async () => { throw new Error("bad") } }))
|
|
62
|
+
await expect(invokeTool("t")).rejects.toMatchObject({ code: "WEBMCP_EXECUTE_FAILED" })
|
|
63
|
+
await expect(invokeTool("missing")).rejects.toMatchObject({ code: "WEBMCP_TOOL_NOT_FOUND" })
|
|
64
|
+
expect(end.events.map((e) => e.ok)).toEqual([false, false])
|
|
65
|
+
end.off()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("rejects invalid arguments before executing", async () => {
|
|
69
|
+
const execute = vi.fn(async () => "ran")
|
|
70
|
+
registerTool(
|
|
71
|
+
newOwnerKey(),
|
|
72
|
+
"",
|
|
73
|
+
makeSpec({
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
n: { type: "number", minimum: 2 },
|
|
78
|
+
i: { type: "integer" },
|
|
79
|
+
s: { type: "string" },
|
|
80
|
+
b: { type: "boolean" },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
execute,
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
for (const args of [{ n: "x" }, { n: 1 }, { i: 1.5 }, { s: 3 }, { b: "no" }]) {
|
|
87
|
+
await expect(invokeTool("t", args)).rejects.toMatchObject({ code: "WEBMCP_INVALID_ARGS" })
|
|
88
|
+
}
|
|
89
|
+
await invokeTool("t", { n: 2, i: 3, s: "a", b: true })
|
|
90
|
+
expect(execute).toHaveBeenCalledTimes(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("uses an installed document.modelContext and normalises its receipts", () => {
|
|
94
|
+
const registered = []
|
|
95
|
+
document.modelContext = {
|
|
96
|
+
registerTool: (s) => (registered.push(s), "string-id"),
|
|
97
|
+
unregisterTool: () => {},
|
|
98
|
+
getTools: () => [],
|
|
99
|
+
}
|
|
100
|
+
expect(isNative()).toBe(true)
|
|
101
|
+
const k = newOwnerKey()
|
|
102
|
+
const res = registerTool(k, "/p", makeSpec())
|
|
103
|
+
expect(res.receipt.id).toBe("string-id")
|
|
104
|
+
updateToolRoute(k, "/q")
|
|
105
|
+
expect(inspect().tools[0].routePath).toBe("/q")
|
|
106
|
+
// the string receipt falls back to api.unregisterTool(name)
|
|
107
|
+
expect(res.receipt.unregister()).toBe(true)
|
|
108
|
+
expect(registered).toHaveLength(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("treats a shim-marked backend as non-native and handles object receipts", () => {
|
|
112
|
+
document.modelContext = {
|
|
113
|
+
__isWebMcpShim: true,
|
|
114
|
+
registerTool: () => ({ id: "obj", unregister() { return true } }),
|
|
115
|
+
getTools: () => [],
|
|
116
|
+
}
|
|
117
|
+
expect(isNative()).toBe(false)
|
|
118
|
+
const res = registerTool(newOwnerKey(), "", makeSpec())
|
|
119
|
+
expect(res.receipt.id).toBe("obj")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("surfaces a backend registerTool failure as ok:false", () => {
|
|
123
|
+
document.modelContext = {
|
|
124
|
+
registerTool: () => { throw new Error("backend down") },
|
|
125
|
+
getTools: () => [],
|
|
126
|
+
}
|
|
127
|
+
const res = registerTool(newOwnerKey(), "", makeSpec())
|
|
128
|
+
expect(res.ok).toBe(false)
|
|
129
|
+
expect(res.error.message).toBe("backend down")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("updateToolSpec re-registers only on a real change", () => {
|
|
133
|
+
const calls = []
|
|
134
|
+
document.modelContext = {
|
|
135
|
+
registerTool: (s) => (calls.push(s), { id: `r${calls.length}`, unregister() {} }),
|
|
136
|
+
getTools: () => [],
|
|
137
|
+
}
|
|
138
|
+
const reg = listen("webmcp:register")
|
|
139
|
+
const k = newOwnerKey()
|
|
140
|
+
registerTool(k, "/p", makeSpec())
|
|
141
|
+
updateToolSpec(k, { description: "d", inputSchema: { type: "object", properties: {} }, annotations: {} })
|
|
142
|
+
expect(calls).toHaveLength(1)
|
|
143
|
+
updateToolSpec(k, { description: "new", annotations: { readOnlyHint: true } })
|
|
144
|
+
expect(calls).toHaveLength(2)
|
|
145
|
+
expect(calls[1].description).toBe("new")
|
|
146
|
+
expect(reg.events.at(-1).replaced).toBe(true)
|
|
147
|
+
updateToolSpec("unknown-owner", { description: "x" })
|
|
148
|
+
reg.off()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("aborts in-flight tool signals on unregister", async () => {
|
|
152
|
+
let signal
|
|
153
|
+
const k = newOwnerKey()
|
|
154
|
+
registerTool(k, "", makeSpec({ execute: async (_a, ctx) => { signal = ctx.signal } }))
|
|
155
|
+
await invokeTool("t")
|
|
156
|
+
unregisterTool(k)
|
|
157
|
+
expect(signal.aborted).toBe(true)
|
|
158
|
+
expect(() => unregisterTool(k)).not.toThrow()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("hasBackend", () => {
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
__resetForTests()
|
|
165
|
+
delete document.modelContext
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it("is false with nothing installed, true for a shim (which isNative treats as non-native)", () => {
|
|
169
|
+
expect(hasBackend()).toBe(false)
|
|
170
|
+
document.modelContext = { __isWebMcpShim: true, registerTool: () => ({}) }
|
|
171
|
+
expect(hasBackend()).toBe(true)
|
|
172
|
+
expect(inspect().isNative).toBe(false)
|
|
173
|
+
delete document.modelContext
|
|
174
|
+
})
|
|
175
|
+
})
|