@seed-ship/mcp-ui-solid 5.3.1 → 5.5.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 +104 -0
- package/dist/components/StreamingUIRenderer.cjs +106 -90
- package/dist/components/StreamingUIRenderer.cjs.map +1 -1
- package/dist/components/StreamingUIRenderer.d.ts +7 -0
- package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
- package/dist/components/StreamingUIRenderer.js +107 -91
- package/dist/components/StreamingUIRenderer.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +101 -82
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +23 -0
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +102 -83
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +493 -0
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -0
- package/dist/mcp-ui-spec/dist/schemas.js +493 -0
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.cjs +118 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js +118 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.cjs +10 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js +10 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.cjs +8 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js +9 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.cjs +122 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js +122 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.cjs +137 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js +139 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.cjs +105 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js +106 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.cjs +3229 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.cjs.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js +3230 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js.map +1 -0
- package/dist/services/validation.cjs +70 -152
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +70 -152
- package/dist/services/validation.js.map +1 -1
- package/dist/utils/logger.cjs +26 -4
- package/dist/utils/logger.cjs.map +1 -1
- package/dist/utils/logger.d.ts +30 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +27 -5
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/perf.cjs +34 -0
- package/dist/utils/perf.cjs.map +1 -0
- package/dist/utils/perf.d.ts +19 -0
- package/dist/utils/perf.d.ts.map +1 -0
- package/dist/utils/perf.js +34 -0
- package/dist/utils/perf.js.map +1 -0
- package/package.json +3 -2
- package/src/components/StreamingUIRenderer.tsx +54 -2
- package/src/components/UIResourceRenderer.errorMode.test.tsx +95 -0
- package/src/components/UIResourceRenderer.tsx +72 -4
- package/src/index.ts +7 -0
- package/src/services/validation.spec-migration.test.ts +207 -0
- package/src/services/validation.ts +132 -178
- package/src/utils/logger.test.ts +130 -0
- package/src/utils/logger.ts +60 -7
- package/src/utils/perf.test.ts +59 -0
- package/src/utils/perf.ts +50 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/utils/logger.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
let debugOverride = null;
|
|
2
|
+
function readEnvFlag() {
|
|
3
|
+
if (typeof process !== "undefined" && process.env) {
|
|
4
|
+
if (process.env.MCP_UI_DEBUG === "true") return true;
|
|
5
|
+
if (process.env.NODE_ENV !== "production") return true;
|
|
6
|
+
}
|
|
7
|
+
if (typeof globalThis !== "undefined" && globalThis.__MCP_UI_DEBUG__ === true) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
function isDebugActive() {
|
|
13
|
+
if (debugOverride !== null) return debugOverride;
|
|
14
|
+
return readEnvFlag();
|
|
15
|
+
}
|
|
16
|
+
function setDebugMode(enabled) {
|
|
17
|
+
debugOverride = enabled;
|
|
18
|
+
}
|
|
19
|
+
function isDebugEnabled() {
|
|
20
|
+
return isDebugActive();
|
|
21
|
+
}
|
|
2
22
|
function formatLogMessage(feature, message, context) {
|
|
3
23
|
const contextStr = context ? ` ${JSON.stringify(context)}` : "";
|
|
4
24
|
return `[@seed-ship/mcp-ui-solid:${feature}] ${message}${contextStr}`;
|
|
@@ -6,12 +26,12 @@ function formatLogMessage(feature, message, context) {
|
|
|
6
26
|
function createLogger(feature) {
|
|
7
27
|
return {
|
|
8
28
|
info(message, context) {
|
|
9
|
-
if (
|
|
29
|
+
if (isDebugActive()) {
|
|
10
30
|
console.info(formatLogMessage(feature, message, context));
|
|
11
31
|
}
|
|
12
32
|
},
|
|
13
33
|
warn(message, context) {
|
|
14
|
-
if (
|
|
34
|
+
if (isDebugActive()) {
|
|
15
35
|
console.warn(formatLogMessage(feature, message, context));
|
|
16
36
|
}
|
|
17
37
|
},
|
|
@@ -19,13 +39,15 @@ function createLogger(feature) {
|
|
|
19
39
|
console.error(formatLogMessage(feature, message, context));
|
|
20
40
|
},
|
|
21
41
|
debug(message, context) {
|
|
22
|
-
if (
|
|
42
|
+
if (isDebugActive()) {
|
|
23
43
|
console.debug(formatLogMessage(feature, message, context));
|
|
24
44
|
}
|
|
25
45
|
}
|
|
26
46
|
};
|
|
27
47
|
}
|
|
28
48
|
export {
|
|
29
|
-
createLogger
|
|
49
|
+
createLogger,
|
|
50
|
+
isDebugEnabled,
|
|
51
|
+
setDebugMode
|
|
30
52
|
};
|
|
31
53
|
//# sourceMappingURL=logger.js.map
|
package/dist/utils/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sources":["../../src/utils/logger.ts"],"sourcesContent":["/**\n * Simple internal logger utility\n *\n *
|
|
1
|
+
{"version":3,"file":"logger.js","sources":["../../src/utils/logger.ts"],"sourcesContent":["/**\n * Simple internal logger utility\n *\n * Logging is enabled when EITHER:\n * 1. `process.env.NODE_ENV !== 'production'` (dev build), OR\n * 2. `process.env.MCP_UI_DEBUG === 'true'` (server-side opt-in for prod), OR\n * 3. `globalThis.__MCP_UI_DEBUG__ === true` (browser-side runtime toggle), OR\n * 4. `setDebugMode(true)` has been called from app code.\n *\n * `error` always logs regardless of mode.\n *\n * @see setDebugMode, isDebugEnabled — runtime controls (v5.4.0)\n */\n\ndeclare global {\n // Browser-side runtime flag — settable from devtools console:\n // `globalThis.__MCP_UI_DEBUG__ = true`\n // eslint-disable-next-line no-var\n var __MCP_UI_DEBUG__: boolean | undefined\n}\n\nlet debugOverride: boolean | null = null\n\nfunction readEnvFlag(): boolean {\n if (typeof process !== 'undefined' && process.env) {\n if (process.env.MCP_UI_DEBUG === 'true') return true\n if (process.env.NODE_ENV !== 'production') return true\n }\n if (typeof globalThis !== 'undefined' && globalThis.__MCP_UI_DEBUG__ === true) {\n return true\n }\n return false\n}\n\nfunction isDebugActive(): boolean {\n if (debugOverride !== null) return debugOverride\n return readEnvFlag()\n}\n\n/**\n * Programmatically enable/disable verbose logging at runtime.\n *\n * Pass `null` to clear the override and fall back to env-based detection.\n *\n * @example\n * ```ts\n * import { setDebugMode } from '@seed-ship/mcp-ui-solid'\n * setDebugMode(true) // turn on verbose logs\n * setDebugMode(false) // turn off (overrides NODE_ENV=development)\n * setDebugMode(null) // restore env-based behavior\n * ```\n */\nexport function setDebugMode(enabled: boolean | null): void {\n debugOverride = enabled\n}\n\n/**\n * Whether verbose logging is currently active (env + override combined).\n */\nexport function isDebugEnabled(): boolean {\n return isDebugActive()\n}\n\nexport interface Logger {\n info(message: string, context?: Record<string, unknown>): void\n warn(message: string, context?: Record<string, unknown>): void\n error(message: string, context?: Record<string, unknown>): void\n debug(message: string, context?: Record<string, unknown>): void\n}\n\nfunction formatLogMessage(\n feature: string,\n message: string,\n context?: Record<string, unknown>\n): string {\n const contextStr = context ? ` ${JSON.stringify(context)}` : ''\n return `[@seed-ship/mcp-ui-solid:${feature}] ${message}${contextStr}`\n}\n\n/**\n * Creates a feature-scoped logger\n *\n * @param feature - Feature name for log prefixing\n * @returns Logger instance\n *\n * @example\n * ```typescript\n * const logger = createLogger('my-component')\n * logger.info('Component mounted', { componentId: '123' })\n * ```\n */\nexport function createLogger(feature: string): Logger {\n return {\n info(message: string, context?: Record<string, unknown>) {\n if (isDebugActive()) {\n console.info(formatLogMessage(feature, message, context))\n }\n },\n\n warn(message: string, context?: Record<string, unknown>) {\n if (isDebugActive()) {\n console.warn(formatLogMessage(feature, message, context))\n }\n },\n\n error(message: string, context?: Record<string, unknown>) {\n // Always log errors, even in production\n console.error(formatLogMessage(feature, message, context))\n },\n\n debug(message: string, context?: Record<string, unknown>) {\n if (isDebugActive()) {\n console.debug(formatLogMessage(feature, message, context))\n }\n },\n }\n}\n\n/**\n * No-op logger for testing or when logging is disabled\n */\nexport const noopLogger: Logger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n debug: () => {},\n}\n"],"names":[],"mappings":"AAqBA,IAAI,gBAAgC;AAEpC,SAAS,cAAuB;AAC9B,MAAI,OAAO,YAAY,eAAe,QAAQ,KAAK;AACjD,QAAI,QAAQ,IAAI,iBAAiB,OAAQ,QAAO;AAChD,QAAI,QAAQ,IAAI,aAAa,aAAc,QAAO;AAAA,EACpD;AACA,MAAI,OAAO,eAAe,eAAe,WAAW,qBAAqB,MAAM;AAC7E,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,gBAAyB;AAChC,MAAI,kBAAkB,KAAM,QAAO;AACnC,SAAO,YAAA;AACT;AAeO,SAAS,aAAa,SAA+B;AAC1D,kBAAgB;AAClB;AAKO,SAAS,iBAA0B;AACxC,SAAO,cAAA;AACT;AASA,SAAS,iBACP,SACA,SACA,SACQ;AACR,QAAM,aAAa,UAAU,IAAI,KAAK,UAAU,OAAO,CAAC,KAAK;AAC7D,SAAO,4BAA4B,OAAO,KAAK,OAAO,GAAG,UAAU;AACrE;AAcO,SAAS,aAAa,SAAyB;AACpD,SAAO;AAAA,IACL,KAAK,SAAiB,SAAmC;AACvD,UAAI,iBAAiB;AACnB,gBAAQ,KAAK,iBAAiB,SAAS,SAAS,OAAO,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,IAEA,KAAK,SAAiB,SAAmC;AACvD,UAAI,iBAAiB;AACnB,gBAAQ,KAAK,iBAAiB,SAAS,SAAS,OAAO,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,IAEA,MAAM,SAAiB,SAAmC;AAExD,cAAQ,MAAM,iBAAiB,SAAS,SAAS,OAAO,CAAC;AAAA,IAC3D;AAAA,IAEA,MAAM,SAAiB,SAAmC;AACxD,UAAI,iBAAiB;AACnB,gBAAQ,MAAM,iBAAiB,SAAS,SAAS,OAAO,CAAC;AAAA,MAC3D;AAAA,IACF;AAAA,EAAA;AAEJ;"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const PERF_PREFIX = "mcp-ui:component:";
|
|
4
|
+
function hasPerf() {
|
|
5
|
+
return typeof performance !== "undefined" && typeof performance.mark === "function";
|
|
6
|
+
}
|
|
7
|
+
function markRenderStart(componentId) {
|
|
8
|
+
if (!hasPerf()) return;
|
|
9
|
+
try {
|
|
10
|
+
performance.mark(`${PERF_PREFIX}${componentId}:render-start`);
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function markRenderEnd(componentId) {
|
|
15
|
+
if (!hasPerf()) return;
|
|
16
|
+
try {
|
|
17
|
+
performance.mark(`${PERF_PREFIX}${componentId}:render-end`);
|
|
18
|
+
if (typeof performance.measure === "function") {
|
|
19
|
+
try {
|
|
20
|
+
performance.measure(
|
|
21
|
+
`${PERF_PREFIX}${componentId}:render`,
|
|
22
|
+
`${PERF_PREFIX}${componentId}:render-start`,
|
|
23
|
+
`${PERF_PREFIX}${componentId}:render-end`
|
|
24
|
+
);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.PERF_PREFIX = PERF_PREFIX;
|
|
32
|
+
exports.markRenderEnd = markRenderEnd;
|
|
33
|
+
exports.markRenderStart = markRenderStart;
|
|
34
|
+
//# sourceMappingURL=perf.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perf.cjs","sources":["../../src/utils/perf.ts"],"sourcesContent":["/**\n * Performance markers for component renders (v5.4.0)\n *\n * Emits `performance.mark()` entries that show up automatically in Chrome\n * DevTools \"Performance\" panel under user timings. Consumers can also\n * query them via `performance.getEntriesByName(...)` for custom tracing.\n *\n * Naming convention :\n * `mcp-ui:component:<id>:render-start`\n * `mcp-ui:component:<id>:render-end`\n * `mcp-ui:component:<id>:render` (a `measure` between the two)\n *\n * Always-on: marks are cheap (sub-microsecond) and only matter when a\n * profiler is recording. SSR-safe (`performance` is guarded).\n */\n\nexport const PERF_PREFIX = 'mcp-ui:component:'\n\nfunction hasPerf(): boolean {\n return typeof performance !== 'undefined' && typeof performance.mark === 'function'\n}\n\nexport function markRenderStart(componentId: string): void {\n if (!hasPerf()) return\n try {\n performance.mark(`${PERF_PREFIX}${componentId}:render-start`)\n } catch {\n // Ignore — performance.mark can throw on malformed names; not worth crashing the render.\n }\n}\n\nexport function markRenderEnd(componentId: string): void {\n if (!hasPerf()) return\n try {\n performance.mark(`${PERF_PREFIX}${componentId}:render-end`)\n if (typeof performance.measure === 'function') {\n try {\n performance.measure(\n `${PERF_PREFIX}${componentId}:render`,\n `${PERF_PREFIX}${componentId}:render-start`,\n `${PERF_PREFIX}${componentId}:render-end`\n )\n } catch {\n // Start mark may be missing if the render path was short-circuited — ignore.\n }\n }\n } catch {\n // Ignore.\n }\n}\n"],"names":[],"mappings":";;AAgBO,MAAM,cAAc;AAE3B,SAAS,UAAmB;AAC1B,SAAO,OAAO,gBAAgB,eAAe,OAAO,YAAY,SAAS;AAC3E;AAEO,SAAS,gBAAgB,aAA2B;AACzD,MAAI,CAAC,UAAW;AAChB,MAAI;AACF,gBAAY,KAAK,GAAG,WAAW,GAAG,WAAW,eAAe;AAAA,EAC9D,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,cAAc,aAA2B;AACvD,MAAI,CAAC,UAAW;AAChB,MAAI;AACF,gBAAY,KAAK,GAAG,WAAW,GAAG,WAAW,aAAa;AAC1D,QAAI,OAAO,YAAY,YAAY,YAAY;AAC7C,UAAI;AACF,oBAAY;AAAA,UACV,GAAG,WAAW,GAAG,WAAW;AAAA,UAC5B,GAAG,WAAW,GAAG,WAAW;AAAA,UAC5B,GAAG,WAAW,GAAG,WAAW;AAAA,QAAA;AAAA,MAEhC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;;;;"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance markers for component renders (v5.4.0)
|
|
3
|
+
*
|
|
4
|
+
* Emits `performance.mark()` entries that show up automatically in Chrome
|
|
5
|
+
* DevTools "Performance" panel under user timings. Consumers can also
|
|
6
|
+
* query them via `performance.getEntriesByName(...)` for custom tracing.
|
|
7
|
+
*
|
|
8
|
+
* Naming convention :
|
|
9
|
+
* `mcp-ui:component:<id>:render-start`
|
|
10
|
+
* `mcp-ui:component:<id>:render-end`
|
|
11
|
+
* `mcp-ui:component:<id>:render` (a `measure` between the two)
|
|
12
|
+
*
|
|
13
|
+
* Always-on: marks are cheap (sub-microsecond) and only matter when a
|
|
14
|
+
* profiler is recording. SSR-safe (`performance` is guarded).
|
|
15
|
+
*/
|
|
16
|
+
export declare const PERF_PREFIX = "mcp-ui:component:";
|
|
17
|
+
export declare function markRenderStart(componentId: string): void;
|
|
18
|
+
export declare function markRenderEnd(componentId: string): void;
|
|
19
|
+
//# sourceMappingURL=perf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perf.d.ts","sourceRoot":"","sources":["../../src/utils/perf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,WAAW,sBAAsB,CAAA;AAM9C,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAOzD;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAkBvD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const PERF_PREFIX = "mcp-ui:component:";
|
|
2
|
+
function hasPerf() {
|
|
3
|
+
return typeof performance !== "undefined" && typeof performance.mark === "function";
|
|
4
|
+
}
|
|
5
|
+
function markRenderStart(componentId) {
|
|
6
|
+
if (!hasPerf()) return;
|
|
7
|
+
try {
|
|
8
|
+
performance.mark(`${PERF_PREFIX}${componentId}:render-start`);
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function markRenderEnd(componentId) {
|
|
13
|
+
if (!hasPerf()) return;
|
|
14
|
+
try {
|
|
15
|
+
performance.mark(`${PERF_PREFIX}${componentId}:render-end`);
|
|
16
|
+
if (typeof performance.measure === "function") {
|
|
17
|
+
try {
|
|
18
|
+
performance.measure(
|
|
19
|
+
`${PERF_PREFIX}${componentId}:render`,
|
|
20
|
+
`${PERF_PREFIX}${componentId}:render-start`,
|
|
21
|
+
`${PERF_PREFIX}${componentId}:render-end`
|
|
22
|
+
);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
PERF_PREFIX,
|
|
31
|
+
markRenderEnd,
|
|
32
|
+
markRenderStart
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=perf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perf.js","sources":["../../src/utils/perf.ts"],"sourcesContent":["/**\n * Performance markers for component renders (v5.4.0)\n *\n * Emits `performance.mark()` entries that show up automatically in Chrome\n * DevTools \"Performance\" panel under user timings. Consumers can also\n * query them via `performance.getEntriesByName(...)` for custom tracing.\n *\n * Naming convention :\n * `mcp-ui:component:<id>:render-start`\n * `mcp-ui:component:<id>:render-end`\n * `mcp-ui:component:<id>:render` (a `measure` between the two)\n *\n * Always-on: marks are cheap (sub-microsecond) and only matter when a\n * profiler is recording. SSR-safe (`performance` is guarded).\n */\n\nexport const PERF_PREFIX = 'mcp-ui:component:'\n\nfunction hasPerf(): boolean {\n return typeof performance !== 'undefined' && typeof performance.mark === 'function'\n}\n\nexport function markRenderStart(componentId: string): void {\n if (!hasPerf()) return\n try {\n performance.mark(`${PERF_PREFIX}${componentId}:render-start`)\n } catch {\n // Ignore — performance.mark can throw on malformed names; not worth crashing the render.\n }\n}\n\nexport function markRenderEnd(componentId: string): void {\n if (!hasPerf()) return\n try {\n performance.mark(`${PERF_PREFIX}${componentId}:render-end`)\n if (typeof performance.measure === 'function') {\n try {\n performance.measure(\n `${PERF_PREFIX}${componentId}:render`,\n `${PERF_PREFIX}${componentId}:render-start`,\n `${PERF_PREFIX}${componentId}:render-end`\n )\n } catch {\n // Start mark may be missing if the render path was short-circuited — ignore.\n }\n }\n } catch {\n // Ignore.\n }\n}\n"],"names":[],"mappings":"AAgBO,MAAM,cAAc;AAE3B,SAAS,UAAmB;AAC1B,SAAO,OAAO,gBAAgB,eAAe,OAAO,YAAY,SAAS;AAC3E;AAEO,SAAS,gBAAgB,aAA2B;AACzD,MAAI,CAAC,UAAW;AAChB,MAAI;AACF,gBAAY,KAAK,GAAG,WAAW,GAAG,WAAW,eAAe;AAAA,EAC9D,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,cAAc,aAA2B;AACvD,MAAI,CAAC,UAAW;AAChB,MAAI;AACF,gBAAY,KAAK,GAAG,WAAW,GAAG,WAAW,aAAa;AAC1D,QAAI,OAAO,YAAY,YAAY,YAAY;AAC7C,UAAI;AACF,oBAAY;AAAA,UACV,GAAG,WAAW,GAAG,WAAW;AAAA,UAC5B,GAAG,WAAW,GAAG,WAAW;AAAA,UAC5B,GAAG,WAAW,GAAG,WAAW;AAAA,QAAA;AAAA,MAEhC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-ship/mcp-ui-solid",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "SolidJS components for rendering MCP-generated UI resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -142,7 +142,8 @@
|
|
|
142
142
|
"@types/dompurify": "^3.0.5",
|
|
143
143
|
"dompurify": "^3.4.1",
|
|
144
144
|
"marked": "^16.3.0",
|
|
145
|
-
"zod": "^3.22.4"
|
|
145
|
+
"zod": "^3.22.4",
|
|
146
|
+
"@seed-ship/mcp-ui-spec": "^5.0.1"
|
|
146
147
|
},
|
|
147
148
|
"devDependencies": {
|
|
148
149
|
"@size-limit/esbuild": "^12.0.0",
|
|
@@ -26,12 +26,20 @@ import { useStreamingUI, type UseStreamingUIOptions } from '../hooks/useStreamin
|
|
|
26
26
|
import type { UIComponent, RendererError } from '../types'
|
|
27
27
|
import { validateComponent } from '../services/validation'
|
|
28
28
|
import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
|
|
29
|
+
import { markRenderStart, markRenderEnd } from '../utils/perf'
|
|
30
|
+
import type { ValidationErrorMode } from './UIResourceRenderer'
|
|
29
31
|
|
|
30
32
|
export interface StreamingUIRendererProps extends UseStreamingUIOptions {
|
|
31
33
|
class?: string
|
|
32
34
|
showProgress?: boolean
|
|
33
35
|
showMetadata?: boolean
|
|
34
36
|
onRenderError?: (error: RendererError) => void
|
|
37
|
+
/**
|
|
38
|
+
* How to react when a streamed component fails `validateComponent()`
|
|
39
|
+
* (v5.4.0). Defaults to `'block'` (full red error card — pre-v5.4.0
|
|
40
|
+
* behavior). See `ValidationErrorMode` in `UIResourceRenderer`.
|
|
41
|
+
*/
|
|
42
|
+
errorMode?: ValidationErrorMode
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -41,7 +49,12 @@ export interface StreamingUIRendererProps extends UseStreamingUIOptions {
|
|
|
41
49
|
function StreamingComponentRenderer(props: {
|
|
42
50
|
component: UIComponent
|
|
43
51
|
onError?: (error: RendererError) => void
|
|
52
|
+
errorMode?: ValidationErrorMode
|
|
44
53
|
}) {
|
|
54
|
+
// Performance marks (v5.4.0) — see utils/perf.ts
|
|
55
|
+
markRenderStart(props.component.id)
|
|
56
|
+
onMount(() => markRenderEnd(props.component.id))
|
|
57
|
+
|
|
45
58
|
// Validate component before rendering
|
|
46
59
|
const validation = validateComponent(props.component)
|
|
47
60
|
if (!validation.valid) {
|
|
@@ -52,11 +65,46 @@ function StreamingComponentRenderer(props: {
|
|
|
52
65
|
details: validation.errors,
|
|
53
66
|
})
|
|
54
67
|
|
|
68
|
+
const mode: ValidationErrorMode = props.errorMode ?? 'block'
|
|
69
|
+
const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
|
|
70
|
+
|
|
71
|
+
if (mode === 'silent') {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (mode === 'inline-warn') {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200"
|
|
79
|
+
role="alert"
|
|
80
|
+
aria-label="Component validation warning"
|
|
81
|
+
title={firstError}
|
|
82
|
+
>
|
|
83
|
+
<svg
|
|
84
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
85
|
+
class="w-3.5 h-3.5"
|
|
86
|
+
viewBox="0 0 24 24"
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
stroke-width="2"
|
|
90
|
+
stroke-linecap="round"
|
|
91
|
+
stroke-linejoin="round"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
>
|
|
94
|
+
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
95
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
96
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
97
|
+
</svg>
|
|
98
|
+
<span>Invalid {props.component.type}</span>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
55
103
|
return (
|
|
56
104
|
<div class="w-full bg-error-subtle border border-border-error rounded-lg p-4">
|
|
57
105
|
<p class="text-sm font-medium text-error-primary">Validation Error</p>
|
|
58
106
|
<p class="text-xs text-text-secondary mt-1">
|
|
59
|
-
{
|
|
107
|
+
{firstError}
|
|
60
108
|
</p>
|
|
61
109
|
</div>
|
|
62
110
|
)
|
|
@@ -214,7 +262,11 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
|
|
|
214
262
|
`}
|
|
215
263
|
style={`grid-column-start: ${component.position.colStart}; grid-column-end: ${component.position.colStart + component.position.colSpan}`}
|
|
216
264
|
>
|
|
217
|
-
<StreamingComponentRenderer
|
|
265
|
+
<StreamingComponentRenderer
|
|
266
|
+
component={component}
|
|
267
|
+
onError={props.onRenderError}
|
|
268
|
+
errorMode={props.errorMode}
|
|
269
|
+
/>
|
|
218
270
|
</div>
|
|
219
271
|
)
|
|
220
272
|
}}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `errorMode` prop on `<UIResourceRenderer>` — v5.4.0 (B.3)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
6
|
+
import { render, cleanup } from '@solidjs/testing-library'
|
|
7
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
8
|
+
import type { UIComponent, RendererError } from '../types'
|
|
9
|
+
|
|
10
|
+
// A component that fails `validateComponent()` — colStart=99 violates the 1-12 grid range
|
|
11
|
+
const invalidComponent: UIComponent = {
|
|
12
|
+
id: 'broken-1',
|
|
13
|
+
type: 'metric',
|
|
14
|
+
position: { colStart: 99, colSpan: 1 },
|
|
15
|
+
params: { value: 42 },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const validComponent: UIComponent = {
|
|
19
|
+
id: 'ok-1',
|
|
20
|
+
type: 'metric',
|
|
21
|
+
position: { colStart: 1, colSpan: 6 },
|
|
22
|
+
params: { title: 'OK', value: 42 },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('<UIResourceRenderer errorMode> — v5.4.0', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
cleanup()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("default (no prop) = 'block': renders the red Validation Error card", () => {
|
|
31
|
+
const { getByText } = render(() => <UIResourceRenderer content={invalidComponent} />)
|
|
32
|
+
expect(getByText('Validation Error')).toBeTruthy()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("errorMode='block' explicitly: same as default", () => {
|
|
36
|
+
const { getByText } = render(() => (
|
|
37
|
+
<UIResourceRenderer content={invalidComponent} errorMode="block" />
|
|
38
|
+
))
|
|
39
|
+
expect(getByText('Validation Error')).toBeTruthy()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("errorMode='inline-warn': renders compact yellow chip, no big red card", () => {
|
|
43
|
+
const { container, queryByText } = render(() => (
|
|
44
|
+
<UIResourceRenderer content={invalidComponent} errorMode="inline-warn" />
|
|
45
|
+
))
|
|
46
|
+
expect(queryByText('Validation Error')).toBeNull()
|
|
47
|
+
|
|
48
|
+
const chip = container.querySelector('[role="alert"][aria-label="Component validation warning"]')
|
|
49
|
+
expect(chip).toBeTruthy()
|
|
50
|
+
expect(chip!.textContent).toContain('Invalid metric')
|
|
51
|
+
// tooltip carries the error message
|
|
52
|
+
expect(chip!.getAttribute('title')).toBeTruthy()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("errorMode='silent': renders nothing in the slot, no error UI", () => {
|
|
56
|
+
const { container, queryByText, queryByRole } = render(() => (
|
|
57
|
+
<UIResourceRenderer content={invalidComponent} errorMode="silent" />
|
|
58
|
+
))
|
|
59
|
+
expect(queryByText('Validation Error')).toBeNull()
|
|
60
|
+
expect(queryByRole('alert')).toBeNull()
|
|
61
|
+
// The slot wrapper is still in the DOM (grid layout) but has no error UI inside
|
|
62
|
+
expect(container.querySelector('[role="alert"]')).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("onError still fires for ALL three modes (consumer can always log)", () => {
|
|
66
|
+
const errors: RendererError[] = []
|
|
67
|
+
const onError = (e: RendererError) => errors.push(e)
|
|
68
|
+
|
|
69
|
+
cleanup()
|
|
70
|
+
render(() => (
|
|
71
|
+
<UIResourceRenderer content={invalidComponent} errorMode="block" onError={onError} />
|
|
72
|
+
))
|
|
73
|
+
cleanup()
|
|
74
|
+
render(() => (
|
|
75
|
+
<UIResourceRenderer content={invalidComponent} errorMode="inline-warn" onError={onError} />
|
|
76
|
+
))
|
|
77
|
+
cleanup()
|
|
78
|
+
render(() => (
|
|
79
|
+
<UIResourceRenderer content={invalidComponent} errorMode="silent" onError={onError} />
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
expect(errors.length).toBe(3)
|
|
83
|
+
expect(errors.every((e) => e.type === 'validation')).toBe(true)
|
|
84
|
+
expect(errors.every((e) => e.componentId === 'broken-1')).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("valid components render normally regardless of errorMode", () => {
|
|
88
|
+
const { queryByText } = render(() => (
|
|
89
|
+
<UIResourceRenderer content={validComponent} errorMode="inline-warn" />
|
|
90
|
+
))
|
|
91
|
+
// No error UI for a valid component
|
|
92
|
+
expect(queryByText('Validation Error')).toBeNull()
|
|
93
|
+
expect(queryByText('Invalid metric')).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -4,11 +4,28 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import DOMPurify from 'dompurify'
|
|
7
|
-
import { Component, createSignal, Show, For, createMemo, createEffect } from 'solid-js'
|
|
7
|
+
import { Component, createSignal, Show, For, createMemo, createEffect, onMount } from 'solid-js'
|
|
8
8
|
import { isServer } from 'solid-js/web'
|
|
9
9
|
import type { UIComponent, UILayout, RendererError, TableVirtualizeOptions } from '../types'
|
|
10
10
|
import { validateComponent, DEFAULT_RESOURCE_LIMITS, getIframeSandbox } from '../services/validation'
|
|
11
11
|
import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
|
|
12
|
+
import { markRenderStart, markRenderEnd } from '../utils/perf'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How `<UIResourceRenderer>` reacts when `validateComponent()` rejects a
|
|
16
|
+
* component (v5.4.0).
|
|
17
|
+
*
|
|
18
|
+
* - `'block'` : full-slot red error card (default — backward compatible)
|
|
19
|
+
* - `'inline-warn'` : compact yellow chip in the slot, tooltip carries the
|
|
20
|
+
* error message — keeps the surrounding layout clean
|
|
21
|
+
* (e.g. inside a chat message)
|
|
22
|
+
* - `'silent'` : render nothing in the slot; `onError` still fires so the
|
|
23
|
+
* consumer can log/alert
|
|
24
|
+
*
|
|
25
|
+
* Runtime errors caught by `<GenerativeUIErrorBoundary>` are NOT affected by
|
|
26
|
+
* this prop — they always show the boundary's fallback UI.
|
|
27
|
+
*/
|
|
28
|
+
export type ValidationErrorMode = 'block' | 'inline-warn' | 'silent'
|
|
12
29
|
import { GridRenderer } from './GridRenderer'
|
|
13
30
|
import { FooterRenderer } from './FooterRenderer'
|
|
14
31
|
import { CarouselRenderer } from './CarouselRenderer'
|
|
@@ -93,6 +110,15 @@ export interface UIResourceRendererProps {
|
|
|
93
110
|
* Custom CSS class
|
|
94
111
|
*/
|
|
95
112
|
class?: string
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How to react when a component fails `validateComponent()` (v5.4.0).
|
|
116
|
+
* Defaults to `'block'` (replaces the slot with a red error card —
|
|
117
|
+
* the pre-v5.4.0 behavior).
|
|
118
|
+
*
|
|
119
|
+
* @see ValidationErrorMode
|
|
120
|
+
*/
|
|
121
|
+
errorMode?: ValidationErrorMode
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
/**
|
|
@@ -1102,7 +1128,13 @@ function LinkRenderer(props: { component: UIComponent }) {
|
|
|
1102
1128
|
function ComponentRenderer(props: {
|
|
1103
1129
|
component: UIComponent
|
|
1104
1130
|
onError?: (error: RendererError) => void
|
|
1131
|
+
errorMode?: ValidationErrorMode
|
|
1105
1132
|
}) {
|
|
1133
|
+
// Performance marks — visible in Chrome DevTools "Performance" panel under
|
|
1134
|
+
// user timings. Always-on, SSR-safe (see utils/perf.ts).
|
|
1135
|
+
markRenderStart(props.component.id)
|
|
1136
|
+
onMount(() => markRenderEnd(props.component.id))
|
|
1137
|
+
|
|
1106
1138
|
// Validate component before rendering
|
|
1107
1139
|
const validation = validateComponent(props.component)
|
|
1108
1140
|
if (!validation.valid) {
|
|
@@ -1113,11 +1145,47 @@ function ComponentRenderer(props: {
|
|
|
1113
1145
|
details: validation.errors,
|
|
1114
1146
|
})
|
|
1115
1147
|
|
|
1148
|
+
const mode: ValidationErrorMode = props.errorMode ?? 'block'
|
|
1149
|
+
const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
|
|
1150
|
+
|
|
1151
|
+
if (mode === 'silent') {
|
|
1152
|
+
return null
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (mode === 'inline-warn') {
|
|
1156
|
+
return (
|
|
1157
|
+
<div
|
|
1158
|
+
class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200"
|
|
1159
|
+
role="alert"
|
|
1160
|
+
aria-label="Component validation warning"
|
|
1161
|
+
title={firstError}
|
|
1162
|
+
>
|
|
1163
|
+
<svg
|
|
1164
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1165
|
+
class="w-3.5 h-3.5"
|
|
1166
|
+
viewBox="0 0 24 24"
|
|
1167
|
+
fill="none"
|
|
1168
|
+
stroke="currentColor"
|
|
1169
|
+
stroke-width="2"
|
|
1170
|
+
stroke-linecap="round"
|
|
1171
|
+
stroke-linejoin="round"
|
|
1172
|
+
aria-hidden="true"
|
|
1173
|
+
>
|
|
1174
|
+
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
1175
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
1176
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
1177
|
+
</svg>
|
|
1178
|
+
<span>Invalid {props.component.type}</span>
|
|
1179
|
+
</div>
|
|
1180
|
+
)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// mode === 'block' (default, pre-v5.4.0 behavior)
|
|
1116
1184
|
return (
|
|
1117
1185
|
<div class="w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
1118
1186
|
<p class="text-sm font-medium text-red-900 dark:text-red-100">Validation Error</p>
|
|
1119
1187
|
<p class="text-xs text-red-700 dark:text-red-300 mt-1">
|
|
1120
|
-
{
|
|
1188
|
+
{firstError}
|
|
1121
1189
|
</p>
|
|
1122
1190
|
</div>
|
|
1123
1191
|
)
|
|
@@ -1450,7 +1518,7 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1450
1518
|
|
|
1451
1519
|
// Wrapper function for RenderContext (breaks circular dependency)
|
|
1452
1520
|
const renderComponent = (component: UIComponent, onError?: (error: RendererError) => void) => (
|
|
1453
|
-
<ComponentRenderer component={component} onError={onError} />
|
|
1521
|
+
<ComponentRenderer component={component} onError={onError} errorMode={props.errorMode} />
|
|
1454
1522
|
)
|
|
1455
1523
|
|
|
1456
1524
|
return (
|
|
@@ -1460,7 +1528,7 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1460
1528
|
<For each={layoutData.components}>
|
|
1461
1529
|
{(component) => (
|
|
1462
1530
|
<div style={getGridStyleString(component)}>
|
|
1463
|
-
<ComponentRenderer component={component} onError={props.onError} />
|
|
1531
|
+
<ComponentRenderer component={component} onError={props.onError} errorMode={props.errorMode} />
|
|
1464
1532
|
</div>
|
|
1465
1533
|
)}
|
|
1466
1534
|
</For>
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,13 @@ export type {
|
|
|
89
89
|
GenerativeUIErrorBoundaryProps,
|
|
90
90
|
} from './components'
|
|
91
91
|
|
|
92
|
+
// Validation error mode (v5.4.0)
|
|
93
|
+
export type { ValidationErrorMode } from './components/UIResourceRenderer'
|
|
94
|
+
|
|
95
|
+
// Runtime debug mode + perf marks (v5.4.0)
|
|
96
|
+
export { setDebugMode, isDebugEnabled } from './utils/logger'
|
|
97
|
+
export { markRenderStart, markRenderEnd, PERF_PREFIX } from './utils/perf'
|
|
98
|
+
|
|
92
99
|
export type { DraggableGridItemProps } from './components/DraggableGridItem'
|
|
93
100
|
export type { ResizeHandleProps as ResizeHandleComponentProps } from './components/ResizeHandle'
|
|
94
101
|
export type { EditableUIResourceRendererProps } from './components/EditableUIResourceRenderer'
|