@morit/cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/assets/docs/README.md +18 -16
- package/assets/docs/ai-response-and-timeline.md +25 -0
- package/assets/docs/components.md +48 -1
- package/assets/docs/design-tokens-responsive.md +33 -10
- package/assets/docs/docs-index.json +1 -0
- package/assets/docs/examples-school-life.md +12 -7
- package/assets/docs/getting-started.md +1 -1
- package/assets/docs/lifecycle-and-api.md +11 -0
- package/assets/docs/local-cli.md +4 -3
- package/assets/docs/manifest.md +23 -1
- package/assets/docs/packaging-and-testing.md +8 -2
- package/assets/docs/permissions-and-data.md +23 -5
- package/assets/docs/plugin-storage.md +175 -0
- package/assets/docs/project-structure.md +12 -0
- package/assets/docs/remote-mcp.md +9 -3
- package/assets/docs/school-life-privacy.md +7 -1
- package/assets/docs/sdk-and-mcp.md +13 -2
- package/assets/docs/tool-and-skill.md +10 -1
- package/assets/docs/ui-runtime-v2.md +67 -0
- package/assets/docs/verification.md +4 -4
- package/assets/plugin_contract.json +71 -4
- package/package.json +1 -1
- package/src/cli.js +36 -3
- package/src/preview.js +132 -0
- package/src/secure-store.js +113 -43
- package/src/workspace.js +170 -16
package/src/workspace.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
signEntries,
|
|
23
23
|
verifySignedEntries,
|
|
24
24
|
} from "./archive.js";
|
|
25
|
+
import { renderPreviewHtml } from "./preview.js";
|
|
25
26
|
|
|
26
27
|
const contract = JSON.parse(
|
|
27
28
|
readFileSync(new URL("../assets/plugin_contract.json", import.meta.url), "utf8"),
|
|
@@ -63,6 +64,10 @@ const CONNECTOR_FIELDS = new Set(contract.object_fields.connector);
|
|
|
63
64
|
const DEPENDENCY_FIELDS = new Set(contract.object_fields.dependency);
|
|
64
65
|
const DEVELOPER_FIELDS = new Set(contract.object_fields.developer);
|
|
65
66
|
const REQUIRED_SECRET_FIELDS = new Set(contract.object_fields.required_secret);
|
|
67
|
+
const STORAGE_FIELDS = new Set(contract.object_fields.storage);
|
|
68
|
+
const STORAGE_NAMESPACE_FIELDS = new Set(contract.object_fields.storage_namespace);
|
|
69
|
+
const STORAGE_MIGRATION_FIELDS = new Set(contract.object_fields.storage_migration);
|
|
70
|
+
const STORAGE_TOOLS = new Set(contract.storage_tools);
|
|
66
71
|
const CAPABILITY_RUNTIME_SHORTHAND = contract.capability_runtime_shorthand;
|
|
67
72
|
const UI_POINTS = new Set(contract.ui_points);
|
|
68
73
|
const RUNTIME_ADAPTERS = new Set(contract.runtime_adapters);
|
|
@@ -147,7 +152,7 @@ export class LocalWorkspace {
|
|
|
147
152
|
version: "1.0.0",
|
|
148
153
|
cloud_project_id: null,
|
|
149
154
|
required_secrets: [],
|
|
150
|
-
min_morit_version: "1.7.
|
|
155
|
+
min_morit_version: "1.7.8",
|
|
151
156
|
max_morit_version: "1.999.999",
|
|
152
157
|
permissions: [],
|
|
153
158
|
capabilities: [],
|
|
@@ -269,6 +274,7 @@ export class LocalWorkspace {
|
|
|
269
274
|
revision: project.revision,
|
|
270
275
|
valid: true,
|
|
271
276
|
message: `validated ${manifest.id} ${manifest.version} (${packageEntries.size} package entries)`,
|
|
277
|
+
warnings: collectThemeWarnings(manifest),
|
|
272
278
|
workspace_path: project.workspace_path,
|
|
273
279
|
};
|
|
274
280
|
}
|
|
@@ -515,6 +521,7 @@ export async function verifyPackageFile(packagePath) {
|
|
|
515
521
|
size_bytes: content.length,
|
|
516
522
|
sha256: sha256(content),
|
|
517
523
|
signature: verified.signature,
|
|
524
|
+
warnings: collectThemeWarnings(verified.manifest),
|
|
518
525
|
};
|
|
519
526
|
}
|
|
520
527
|
|
|
@@ -535,6 +542,7 @@ function projectDirectoryResult({ root, manifest, entries }) {
|
|
|
535
542
|
publisher: manifest.publisher,
|
|
536
543
|
version: manifest.version,
|
|
537
544
|
package_entries: entries.size + 1,
|
|
545
|
+
warnings: collectThemeWarnings(manifest),
|
|
538
546
|
};
|
|
539
547
|
}
|
|
540
548
|
|
|
@@ -719,14 +727,10 @@ async function signingKeyForPublisher(keyDirectory, publisher) {
|
|
|
719
727
|
}
|
|
720
728
|
}
|
|
721
729
|
|
|
722
|
-
function previewHtml(manifest) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
"utf8",
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
+
function previewHtml(manifest) {
|
|
731
|
+
return renderPreviewHtml(manifest);
|
|
732
|
+
}
|
|
733
|
+
|
|
730
734
|
function validateMetadata(pluginId, name, publisher, description) {
|
|
731
735
|
if (!PLUGIN_ID.test(pluginId) || pluginId.length > 120) throw new Error("plugin_id must be a reverse-domain lowercase identifier");
|
|
732
736
|
if (typeof name !== "string" || name.trim().length < 1 || name.trim().length > 80) throw new Error("name must contain 1 to 80 characters");
|
|
@@ -790,6 +794,7 @@ function validateManifest(manifest, files) {
|
|
|
790
794
|
for (const capability of capabilities) {
|
|
791
795
|
rejectUnknownFields(capability, CAPABILITY_FIELDS, "capability");
|
|
792
796
|
requireUniqueIdentifier(capability.id, capabilityIds, "capability");
|
|
797
|
+
if (STORAGE_TOOLS.has(capability.id)) throw new Error(`capability ${capability.id} is reserved by Morit`);
|
|
793
798
|
capabilityIds.add(capability.id);
|
|
794
799
|
if (!CAPABILITY_KINDS.has(capability.kind)) throw new Error(`unknown capability kind: ${capability.kind}`);
|
|
795
800
|
requireText(capability.title, 80, `capability ${capability.id} title`);
|
|
@@ -832,7 +837,8 @@ function validateManifest(manifest, files) {
|
|
|
832
837
|
// level before semantic UI validation runs.
|
|
833
838
|
assertJsonSize(extensionConfig, 32 * 1024, `UI ${extension.id} config`, 40);
|
|
834
839
|
}
|
|
835
|
-
|
|
840
|
+
const storage = validateStorage(manifest, requestedPermissions);
|
|
841
|
+
for (const extension of uiExtensions) validateUiConfig(extension, capabilities, uiIds, files, storage !== null);
|
|
836
842
|
|
|
837
843
|
const credentialIds = validateCredentials(credentials, requestedPermissions);
|
|
838
844
|
const secretIds = validateRequiredSecrets(requiredSecrets);
|
|
@@ -879,6 +885,79 @@ function validatePermissions(values, label) {
|
|
|
879
885
|
return new Set(values);
|
|
880
886
|
}
|
|
881
887
|
|
|
888
|
+
function validateStorage(manifest, permissions) {
|
|
889
|
+
if (manifest.storage === undefined) {
|
|
890
|
+
if (permissions.has("ai_storage")) throw new Error("ai_storage requires manifest.storage");
|
|
891
|
+
return permissions.has("storage") ? { version: 1, namespaces: [{ id: "default", max_bytes: 128 * 1024, ai_access: "none" }], migrations: [] } : null;
|
|
892
|
+
}
|
|
893
|
+
if (manifest.schema_version !== 2 || !permissions.has("storage")) {
|
|
894
|
+
throw new Error("manifest.storage requires schema 2 and storage permission");
|
|
895
|
+
}
|
|
896
|
+
const storage = manifest.storage;
|
|
897
|
+
assertObject(storage, "manifest.storage");
|
|
898
|
+
rejectUnknownFields(storage, STORAGE_FIELDS, "manifest.storage");
|
|
899
|
+
if (!Number.isInteger(storage.version) || storage.version < 1 || storage.version > 1000) {
|
|
900
|
+
throw new Error("manifest.storage.version is invalid");
|
|
901
|
+
}
|
|
902
|
+
const namespaces = objectCollection(storage.namespaces, "manifest.storage.namespaces", 16);
|
|
903
|
+
if (!namespaces.length) throw new Error("manifest.storage.namespaces must not be empty");
|
|
904
|
+
const namespaceIds = new Set();
|
|
905
|
+
let hasAiAccess = false;
|
|
906
|
+
for (const namespace of namespaces) {
|
|
907
|
+
rejectUnknownFields(namespace, STORAGE_NAMESPACE_FIELDS, "storage namespace");
|
|
908
|
+
requireUniqueIdentifier(namespace.id, namespaceIds, "storage namespace");
|
|
909
|
+
namespaceIds.add(namespace.id);
|
|
910
|
+
const maxBytes = namespace.max_bytes === undefined ? 128 * 1024 : namespace.max_bytes;
|
|
911
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 1024 || maxBytes > 256 * 1024) {
|
|
912
|
+
throw new Error(`storage namespace ${namespace.id} max_bytes is invalid`);
|
|
913
|
+
}
|
|
914
|
+
const aiAccess = namespace.ai_access === undefined ? "none" : namespace.ai_access;
|
|
915
|
+
if (!contract.storage_ai_access.includes(aiAccess)) throw new Error(`storage namespace ${namespace.id} ai_access is invalid`);
|
|
916
|
+
hasAiAccess ||= aiAccess !== "none";
|
|
917
|
+
}
|
|
918
|
+
const migrations = objectCollection(storage.migrations === undefined ? [] : storage.migrations, "manifest.storage.migrations", 64);
|
|
919
|
+
const migrationSteps = new Set();
|
|
920
|
+
for (const migration of migrations) {
|
|
921
|
+
rejectUnknownFields(migration, STORAGE_MIGRATION_FIELDS, "storage migration");
|
|
922
|
+
if (!Number.isInteger(migration.from) || migration.from < 1 || migration.to !== migration.from + 1 || migration.to > storage.version) {
|
|
923
|
+
throw new Error("storage migration version is invalid");
|
|
924
|
+
}
|
|
925
|
+
if (!namespaceIds.has(migration.namespace)) throw new Error("storage migration namespace is unknown");
|
|
926
|
+
const step = `${migration.from}\0${migration.namespace}`;
|
|
927
|
+
if (migrationSteps.has(step)) throw new Error("storage migration step is duplicated");
|
|
928
|
+
migrationSteps.add(step);
|
|
929
|
+
const rename = migration.rename === undefined ? {} : migration.rename;
|
|
930
|
+
assertObject(rename, "storage migration rename");
|
|
931
|
+
const deleted = migration.delete === undefined ? [] : migration.delete;
|
|
932
|
+
if (Object.keys(rename).length > 32 || !Array.isArray(deleted) || deleted.length > 32 || new Set(deleted).size !== deleted.length) {
|
|
933
|
+
throw new Error("storage migration operations are invalid");
|
|
934
|
+
}
|
|
935
|
+
const targets = new Set();
|
|
936
|
+
for (const [source, target] of Object.entries(rename)) {
|
|
937
|
+
validateStorageKey(source);
|
|
938
|
+
validateStorageKey(target);
|
|
939
|
+
if (source === target || targets.has(target) || deleted.includes(source)) throw new Error("storage migration rename is invalid");
|
|
940
|
+
targets.add(target);
|
|
941
|
+
}
|
|
942
|
+
for (const key of deleted) validateStorageKey(key);
|
|
943
|
+
}
|
|
944
|
+
if (permissions.has("ai_storage") !== hasAiAccess) {
|
|
945
|
+
throw new Error("AI-accessible storage and ai_storage permission must be declared together");
|
|
946
|
+
}
|
|
947
|
+
return storage;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function validateStorageKey(value) {
|
|
951
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/.test(value) || value.startsWith("_")) {
|
|
952
|
+
throw new Error("storage key is invalid");
|
|
953
|
+
}
|
|
954
|
+
const compact = value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
955
|
+
if (/(^|[_.-])(authorization|credential|password|secret|token|api[_-]?key|private[_-]?key)($|[_.-])/i.test(value)
|
|
956
|
+
|| ["authorization", "credential", "credentials", "password", "secret", "token", "accesstoken", "refreshtoken", "apikey", "privatekey"].includes(compact)) {
|
|
957
|
+
throw new Error("credential-shaped storage keys are not allowed");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
882
961
|
function assertObject(value, label) {
|
|
883
962
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
884
963
|
throw new Error(`${label} must be an object`);
|
|
@@ -1160,9 +1239,10 @@ function validateRuntimeReferences(capabilities, connectors, connectorIds, depen
|
|
|
1160
1239
|
}
|
|
1161
1240
|
}
|
|
1162
1241
|
|
|
1163
|
-
function validateUiConfig(extension, capabilities, routeIds, files) {
|
|
1242
|
+
function validateUiConfig(extension, capabilities, routeIds, files, hasStorage = false) {
|
|
1164
1243
|
const config = extension.config === undefined ? {} : extension.config;
|
|
1165
1244
|
const executable = new Set(capabilities.filter((value) => ["tool", "skill", "provider", "notification"].includes(value.kind)).map((value) => value.id));
|
|
1245
|
+
if (hasStorage) for (const capability of STORAGE_TOOLS) executable.add(capability);
|
|
1166
1246
|
if (config.ui_schema === 2) {
|
|
1167
1247
|
if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
|
|
1168
1248
|
validateUiRuntimeV2(config, executable, routeIds);
|
|
@@ -1283,9 +1363,9 @@ function validateUiRuntimeV2(config, executable, routes) {
|
|
|
1283
1363
|
validateUiNode(config.view, 0, { value: 0 }, executable, routes, sourceIds, stateKeys, null);
|
|
1284
1364
|
}
|
|
1285
1365
|
|
|
1286
|
-
function validateUiTheme(theme) {
|
|
1366
|
+
function validateUiTheme(theme, variant = false) {
|
|
1287
1367
|
if (theme == null) return;
|
|
1288
|
-
rejectUnknownFields(theme, new Set(contract.ui_runtime.theme_fields), "UI theme");
|
|
1368
|
+
rejectUnknownFields(theme, new Set(variant ? contract.ui_runtime.theme_variant_fields : contract.ui_runtime.theme_fields), "UI theme");
|
|
1289
1369
|
const colors = theme.color_scheme === undefined ? {} : theme.color_scheme;
|
|
1290
1370
|
assertObject(colors, "UI color scheme");
|
|
1291
1371
|
const roles = new Set([...UI_COLOR_TOKENS].filter((value) => value !== "transparent"));
|
|
@@ -1297,6 +1377,79 @@ function validateUiTheme(theme) {
|
|
|
1297
1377
|
if (theme.spacing !== undefined) validateUiNumber(theme.spacing, "UI theme spacing", 0, 32);
|
|
1298
1378
|
const density = theme.density === undefined ? "standard" : theme.density;
|
|
1299
1379
|
if (!["compact", "standard", "comfortable"].includes(density)) throw new Error("UI theme density is invalid");
|
|
1380
|
+
const typography = theme.typography === undefined ? {} : theme.typography;
|
|
1381
|
+
rejectUnknownFields(typography, new Set(contract.ui_runtime.theme_typography_fields), "UI theme typography");
|
|
1382
|
+
if (typography.scale !== undefined) validateUiNumber(typography.scale, "UI theme typography scale", 0.8, 1.4);
|
|
1383
|
+
for (const key of ["body_weight", "title_weight"]) {
|
|
1384
|
+
if (typography[key] !== undefined && ![400, 500, 600, 700].includes(typography[key])) throw new Error("UI theme typography weight is invalid");
|
|
1385
|
+
}
|
|
1386
|
+
validateThemeComponent(theme.surface, contract.ui_runtime.theme_surface_fields, ["color", "container_color"], { elevation: [0, 24] }, "surface");
|
|
1387
|
+
validateThemeComponent(theme.border, contract.ui_runtime.theme_border_fields, ["color"], { width: [0, 8], radius: [0, 64] }, "border");
|
|
1388
|
+
validateThemeComponent(theme.icon, contract.ui_runtime.theme_icon_fields, ["color"], { size: [12, 64] }, "icon");
|
|
1389
|
+
validateThemeComponent(theme.states, contract.ui_runtime.theme_state_fields, ["selected_color", "focus_color"], { disabled_opacity: [0.2, 0.8] }, "states");
|
|
1390
|
+
if (!variant) for (const mode of ["light", "dark"]) if (theme[mode] !== undefined) validateUiTheme(theme[mode], true);
|
|
1391
|
+
validateThemeContrast(theme);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function validateThemeComponent(value, fields, colors, numbers, label) {
|
|
1395
|
+
if (value === undefined) return;
|
|
1396
|
+
rejectUnknownFields(value, new Set(fields), `UI theme ${label}`);
|
|
1397
|
+
for (const key of colors) if (value[key] !== undefined && (typeof value[key] !== "string" || !UI_HEX_COLOR.test(value[key]))) throw new Error(`UI theme ${label} color is invalid`);
|
|
1398
|
+
for (const [key, [minimum, maximum]] of Object.entries(numbers)) if (value[key] !== undefined) validateUiNumber(value[key], `UI theme ${label} ${key}`, minimum, maximum);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function hexRgb(value) {
|
|
1402
|
+
if (typeof value !== "string" || !UI_HEX_COLOR.test(value)) return null;
|
|
1403
|
+
let raw = value.slice(1);
|
|
1404
|
+
if (raw.length === 8) {
|
|
1405
|
+
if (Number.parseInt(raw.slice(0, 2), 16) < 242) return null;
|
|
1406
|
+
raw = raw.slice(2);
|
|
1407
|
+
}
|
|
1408
|
+
return [0, 2, 4].map((index) => Number.parseInt(raw.slice(index, index + 2), 16) / 255);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function contrastRatio(first, second) {
|
|
1412
|
+
const values = [hexRgb(first), hexRgb(second)];
|
|
1413
|
+
if (values.some((value) => value === null)) return null;
|
|
1414
|
+
const luminance = (rgb) => {
|
|
1415
|
+
const [r, g, b] = rgb.map((value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
|
|
1416
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1417
|
+
};
|
|
1418
|
+
const [one, two] = values.map(luminance);
|
|
1419
|
+
return (Math.max(one, two) + 0.05) / (Math.min(one, two) + 0.05);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const THEME_CONTRAST_PAIRS = [
|
|
1423
|
+
["primary", "on_primary"], ["primary_container", "on_primary_container"],
|
|
1424
|
+
["secondary", "on_secondary"], ["secondary_container", "on_secondary_container"],
|
|
1425
|
+
["tertiary", "on_tertiary"], ["error", "on_error"],
|
|
1426
|
+
["error_container", "on_error_container"], ["surface", "on_surface"],
|
|
1427
|
+
["surface_variant", "on_surface_variant"],
|
|
1428
|
+
];
|
|
1429
|
+
|
|
1430
|
+
function validateThemeContrast(theme) {
|
|
1431
|
+
const colors = theme.color_scheme || {};
|
|
1432
|
+
for (const [background, foreground] of THEME_CONTRAST_PAIRS) {
|
|
1433
|
+
const ratio = contrastRatio(colors[background], colors[foreground]);
|
|
1434
|
+
if (ratio !== null && ratio < 1.5) throw new Error(`UI theme ${foreground} is not visible on ${background}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function collectThemeWarnings(manifest) {
|
|
1439
|
+
const warnings = [];
|
|
1440
|
+
for (const extension of manifest.ui_extensions || []) {
|
|
1441
|
+
const theme = extension?.config?.theme;
|
|
1442
|
+
if (!theme || typeof theme !== "object" || Array.isArray(theme)) continue;
|
|
1443
|
+
for (const [mode, variant] of [["base", theme], ["light", theme.light], ["dark", theme.dark]]) {
|
|
1444
|
+
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
1445
|
+
const colors = variant.color_scheme || {};
|
|
1446
|
+
for (const [background, foreground] of [["primary", "on_primary"], ["surface", "on_surface"]]) {
|
|
1447
|
+
const ratio = contrastRatio(colors[background], colors[foreground]);
|
|
1448
|
+
if (ratio !== null && ratio < 4.5) warnings.push(`${extension.id} ${mode} theme ${foreground}/${background} contrast is ${ratio.toFixed(2)}:1; verify text accessibility`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return warnings;
|
|
1300
1453
|
}
|
|
1301
1454
|
|
|
1302
1455
|
function validateUiAppBar(appBar, executable, routes, sources, stateKeys) {
|
|
@@ -1385,7 +1538,7 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
|
|
|
1385
1538
|
validateUiAction(node.action, executable, routes, sources, stateKeys);
|
|
1386
1539
|
const children = node.children === undefined ? [] : node.children;
|
|
1387
1540
|
if (!Array.isArray(children) || children.length > 32) throw new Error("UI component children are invalid");
|
|
1388
|
-
const leaves = new Set(["text", "icon", "image", "avatar", "divider", "spacer", "button", "chip", "metric", "progress", "calendar", "chart", "field", "select", "switch", "empty"]);
|
|
1541
|
+
const leaves = new Set(["text", "icon", "image", "avatar", "divider", "spacer", "button", "chip", "metric", "progress", "calendar", "chart", "table", "field", "select", "switch", "empty"]);
|
|
1389
1542
|
if (leaves.has(node.type) && children.length) throw new Error("leaf UI component cannot have children");
|
|
1390
1543
|
if (["list", "timeline"].includes(node.type) && children.length !== 1) throw new Error("list UI component requires one item template");
|
|
1391
1544
|
if (["positioned", "scroll", "padding", "center", "expanded", "badge"].includes(node.type) && children.length !== 1) throw new Error("UI component requires exactly one child");
|
|
@@ -1460,11 +1613,11 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
|
|
|
1460
1613
|
if (nodeType !== "positioned" && ["left", "top", "right", "bottom"].some((key) => props[key] !== undefined)) throw new Error("position offsets require a positioned UI component");
|
|
1461
1614
|
if (nodeType !== "expanded" && props.flex !== undefined) throw new Error("flex requires an expanded UI component");
|
|
1462
1615
|
if (nodeType !== "scroll" && ["scroll_direction", "shrink_wrap"].some((key) => props[key] !== undefined)) throw new Error("scroll properties require a scroll UI component");
|
|
1463
|
-
if (["list", "timeline", "calendar", "chart"].includes(nodeType)) {
|
|
1616
|
+
if (["list", "timeline", "calendar", "chart", "table"].includes(nodeType)) {
|
|
1464
1617
|
if (typeof props.source !== "string" || !UI_BINDING.test(props.source) || !props.source.startsWith("data.") || !sources.has(props.source.split(".")[1])) throw new Error("UI list source is invalid");
|
|
1465
1618
|
}
|
|
1466
1619
|
const chartType = props.chart_type === undefined ? "bar" : props.chart_type;
|
|
1467
|
-
if (nodeType === "chart" && !["bar", "line", "donut"].includes(chartType)) throw new Error("UI chart type is invalid");
|
|
1620
|
+
if (nodeType === "chart" && !["bar", "line", "donut", "scatter"].includes(chartType)) throw new Error("UI chart type is invalid");
|
|
1468
1621
|
if (nodeType === "calendar" && props.state_key != null && !stateKeys.has(props.state_key)) throw new Error("UI calendar state is unknown");
|
|
1469
1622
|
if (["field", "select", "switch"].includes(nodeType) && !stateKeys.has(props.state_key)) throw new Error("UI state field is unknown");
|
|
1470
1623
|
if (props.options != null) {
|
|
@@ -1832,6 +1985,7 @@ function escapeHtml(value) {
|
|
|
1832
1985
|
|
|
1833
1986
|
export const internal = {
|
|
1834
1987
|
artifactFileName,
|
|
1988
|
+
collectThemeWarnings,
|
|
1835
1989
|
compileManifest,
|
|
1836
1990
|
normalizeEntrypointPath,
|
|
1837
1991
|
projectFileBytes,
|