@morit/cli 1.6.0 → 1.9.1
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 +3 -2
- package/assets/plugin_contract.json +827 -116
- package/package.json +3 -2
- package/src/cli.js +19 -1
- package/src/morit-script.js +174 -0
- package/src/preview-v3.js +83 -0
- package/src/script-contract.js +62 -0
- package/src/script-data.js +41 -0
- package/src/script-project.js +60 -0
- package/src/script-runtime.js +252 -0
- package/src/script-schema.js +32 -0
- package/src/script-spec.js +113 -0
- package/src/ui-v3.js +159 -0
- package/src/workspace.js +205 -16
package/src/workspace.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { renderV3PreviewHtml } from './preview-v3.js';
|
|
2
|
+
import { compileUiV3, requiresMorit19, uiV3AllowedProps } from './ui-v3.js';
|
|
3
|
+
import { compileScriptProject } from './script-project.js';
|
|
4
|
+
import { ScriptRuntime } from './script-runtime.js';
|
|
5
|
+
import { runtimeSnapshot } from './script-data.js';
|
|
1
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
2
8
|
import {
|
|
3
9
|
chmod,
|
|
4
10
|
copyFile,
|
|
@@ -11,6 +17,7 @@ import {
|
|
|
11
17
|
} from "node:fs/promises";
|
|
12
18
|
import { readFileSync } from "node:fs";
|
|
13
19
|
import { homedir } from "node:os";
|
|
20
|
+
import { promisify } from "node:util";
|
|
14
21
|
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
15
22
|
import { pathToFileURL } from "node:url";
|
|
16
23
|
import {
|
|
@@ -27,6 +34,7 @@ import { renderPreviewHtml } from "./preview.js";
|
|
|
27
34
|
const contract = JSON.parse(
|
|
28
35
|
readFileSync(new URL("../assets/plugin_contract.json", import.meta.url), "utf8"),
|
|
29
36
|
);
|
|
37
|
+
const execFileAsync = promisify(execFile);
|
|
30
38
|
|
|
31
39
|
const MAX_FILES = 64;
|
|
32
40
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
@@ -130,7 +138,7 @@ export class LocalWorkspace {
|
|
|
130
138
|
await rename(temporary, this.statePath);
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
async createProject({ plugin_id, name, publisher, description = "", advanced = true, directory }) {
|
|
141
|
+
async createProject({ plugin_id, name, publisher, description = "", advanced = true, directory, ui_schema = 3 }) {
|
|
134
142
|
validateMetadata(plugin_id, name, publisher, description);
|
|
135
143
|
const relativePath = safeProjectPath(directory || join("morit-plugins", plugin_id.split(".").at(-1)));
|
|
136
144
|
const projectRoot = this.inside(relativePath);
|
|
@@ -152,7 +160,7 @@ export class LocalWorkspace {
|
|
|
152
160
|
version: "1.0.0",
|
|
153
161
|
cloud_project_id: null,
|
|
154
162
|
required_secrets: [],
|
|
155
|
-
min_morit_version: "1.7.14",
|
|
163
|
+
min_morit_version: ui_schema === 3 ? "1.9.0" : "1.7.14",
|
|
156
164
|
max_morit_version: "1.999.999",
|
|
157
165
|
permissions: [],
|
|
158
166
|
capabilities: [],
|
|
@@ -163,6 +171,13 @@ export class LocalWorkspace {
|
|
|
163
171
|
slash_commands: [],
|
|
164
172
|
data_policy: "purge",
|
|
165
173
|
};
|
|
174
|
+
if (![2,3].includes(ui_schema)) throw new Error('ui_schema must be 2 or 3');
|
|
175
|
+
if(ui_schema===3){
|
|
176
|
+
manifest.min_morit_version='1.9.0';
|
|
177
|
+
manifest.ui_extensions=[{id:plugin_id+'.screen',point:'screen',title:name,permissions:[],config:{ui_schema:3,initial_state:{},view:{type:'column',children:[{type:'text',props:{text:'{{= runtime.today }}'}}]}}}];
|
|
178
|
+
} else {
|
|
179
|
+
manifest.ui_extensions=[{id:plugin_id+'.screen',point:'screen',title:name,permissions:[],config:{ui_schema:2,initial_state:{},view:{type:'column',children:[{type:'text',props:{text:name}}]}}}];
|
|
180
|
+
}
|
|
166
181
|
await writeFile(join(projectRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
167
182
|
await writeFile(
|
|
168
183
|
join(projectRoot, "README.md"),
|
|
@@ -279,16 +294,39 @@ export class LocalWorkspace {
|
|
|
279
294
|
};
|
|
280
295
|
}
|
|
281
296
|
|
|
282
|
-
async
|
|
297
|
+
async runtimeTest(projectId, options) {
|
|
298
|
+
const project = await this.getProject(projectId, true);
|
|
299
|
+
const result = await runtimeTestProjectFiles(project.files, options);
|
|
300
|
+
const manifest = compileManifest(project.files);
|
|
301
|
+
const fixture = options.fixture ?? 'fixtures/live.json';
|
|
302
|
+
const files = {...project.files, [fixture]: JSON.stringify(result.inspector.raw_response)};
|
|
303
|
+
const html = renderV3PreviewHtml(manifest, files);
|
|
304
|
+
const fileName = `${manifest.id}-runtime-test.html`;
|
|
305
|
+
const target = join(project.workspace_path, "dist", fileName);
|
|
306
|
+
await mkdir(dirname(target), { recursive: true });
|
|
307
|
+
await writeFile(target, html);
|
|
308
|
+
const preview = await this.recordArtifact(projectId, "preview", fileName, "text/html", target, html, null);
|
|
309
|
+
return { project_id: projectId, revision: project.revision, ...result, preview };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async preview(projectId, {target: previewTarget = 'semantic'} = {}) {
|
|
283
313
|
const project = await this.getProject(projectId, true);
|
|
284
314
|
const manifest = compileManifest(project.files);
|
|
285
315
|
validateManifest(manifest, project.files);
|
|
286
316
|
const fileName = `${manifest.id}-preview.html`;
|
|
287
317
|
const target = join(project.workspace_path, "dist", fileName);
|
|
288
318
|
await mkdir(dirname(target), { recursive: true });
|
|
289
|
-
const html = previewHtml(manifest);
|
|
319
|
+
const html = manifest.ui_extensions.some(e=>e.config?.ui_schema===3) ? renderV3PreviewHtml(manifest,project.files) : previewHtml(manifest);
|
|
290
320
|
await writeFile(target, html);
|
|
291
|
-
|
|
321
|
+
const artifact=await this.recordArtifact(projectId, "preview", fileName, "text/html", target, html, null);
|
|
322
|
+
if(previewTarget!=='android')return {...artifact,preview_mode:'semantic'};
|
|
323
|
+
try{
|
|
324
|
+
const {stdout}=await execFileAsync(process.env.ADB??'adb',['devices'],{timeout:3000});
|
|
325
|
+
const serial=stdout.split('\n').map(line=>line.trim().split(/\s+/)).find(parts=>parts[1]==='device')?.[0];
|
|
326
|
+
if(!serial)return {...artifact,preview_mode:'semantic',fallback_reason:'No Android emulator or device is available'};
|
|
327
|
+
await execFileAsync(process.env.ADB??'adb',['-s',serial,'shell','am','start','-W','-a','android.intent.action.VIEW','-d',`morit://open/plugin/${encodeURIComponent(manifest.id)}`],{timeout:10000});
|
|
328
|
+
return {...artifact,preview_mode:'android',device_serial:serial};
|
|
329
|
+
}catch(error){return {...artifact,preview_mode:'semantic',fallback_reason:`Android Host preview unavailable: ${error.message}`};}
|
|
292
330
|
}
|
|
293
331
|
|
|
294
332
|
async sourceDownload(projectId, requestedFileName) {
|
|
@@ -415,6 +453,136 @@ export class LocalWorkspace {
|
|
|
415
453
|
}
|
|
416
454
|
}
|
|
417
455
|
|
|
456
|
+
export async function runtimeTestProjectDirectory(source, options) {
|
|
457
|
+
const number=value=>value===undefined?undefined:Number(value);
|
|
458
|
+
const screen=options?.screen??{
|
|
459
|
+
width:number(options?.['screen-width']),height:number(options?.['screen-height']),text_scale:number(options?.['screen-text-scale']),
|
|
460
|
+
orientation:options?.['screen-orientation'],size_class:options?.['screen-size-class'],platform:options?.['screen-platform'],
|
|
461
|
+
};
|
|
462
|
+
return runtimeTestProjectFiles(await readStandaloneProjectFiles(resolve(source)), {...options,screen});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function runtimeTestProjectFiles(files, options = {}) {
|
|
466
|
+
if ((options.mode ?? 'fixture') === 'fixture') return runtimeTestFiles(files, options);
|
|
467
|
+
if (options.mode !== 'live') throw new Error('mode must be fixture or live');
|
|
468
|
+
const origin = new URL(process.env.MORIT_RUNTIME_TEST_BACKEND_URL ?? 'https://invalid.local');
|
|
469
|
+
const token = process.env.MORIT_RUNTIME_TEST_HOST_TOKEN;
|
|
470
|
+
if (origin.protocol !== 'https:' || origin.username || origin.password || !token || !process.env.MORIT_RUNTIME_TEST_BACKEND_URL) throw new Error('Live mode requires configured HTTPS Host URL and Host access token');
|
|
471
|
+
const manifest = JSON.parse(files['manifest.json']);
|
|
472
|
+
if (!manifest.capabilities?.some(c => c.id === options.capability)) throw new Error('Unknown project capability');
|
|
473
|
+
const response = await fetch(new URL(`/v1/plugins/${encodeURIComponent(manifest.id)}/capabilities/${encodeURIComponent(options.capability)}/runtime-test`, origin), {
|
|
474
|
+
method:'POST', redirect:'error', signal:AbortSignal.timeout(20000), headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json','X-Morit-Plugin-Sdk':'1.9.0'},body:JSON.stringify({arguments:options.arguments??{}}),
|
|
475
|
+
});
|
|
476
|
+
if (!response.ok) throw new Error('Live capability request failed; check installation and read-only connector permissions');
|
|
477
|
+
const reader=response.body.getReader();let size=0;const chunks=[];
|
|
478
|
+
try {for (;;) {const {value,done}=await reader.read();if(done)break;size+=value.length;if(size>262144)throw new Error('Live response exceeds 256 KiB');chunks.push(value);}} finally {await reader.cancel();}
|
|
479
|
+
const live=JSON.parse(Buffer.concat(chunks).toString());if(live.state!=='completed')throw new Error('Live capability did not complete');
|
|
480
|
+
const fixture=options.fixture??'fixtures/live.json';
|
|
481
|
+
return {...runtimeTestFiles({...files,[fixture]:JSON.stringify(live.data)},{...options,fixture}),mode:'live'};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function runtimeTestFiles(files, { module, export: exportName, fixture, extension, now, timezone = 'UTC', locale = 'en-US', screen = {} } = {}) {
|
|
485
|
+
const fixturePath=fixture??['fixtures/live.json','fixtures/day.json'].find(path=>Object.hasOwn(files,path));
|
|
486
|
+
if (typeof fixturePath !== 'string' || !/^fixtures\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*\.json$/.test(fixturePath)) throw new Error('fixture must be fixtures/*.json');
|
|
487
|
+
if (!Object.hasOwn(files, fixturePath)) throw new Error('Fixture not found');
|
|
488
|
+
const manifest = files['manifest.json'] ? compileManifest(files) : null;
|
|
489
|
+
const config = extension ? manifest?.ui_extensions?.find(value => value.id === extension)?.config : files['runtime.json'] ? JSON.parse(files['runtime.json']) : manifest?.ui_extensions?.find(value => value.config?.ui_schema === 3)?.config;
|
|
490
|
+
if (!config || config.ui_schema !== 3) throw new Error('Select a Runtime v3 extension or provide runtime.json');
|
|
491
|
+
if(!config.compiled)compileUiV3(config,files,{capabilities:new Set((manifest?.capabilities??[]).map(value=>value.id)),routes:new Set((manifest?.ui_extensions??[]).map(value=>value.id)),nodeTypes:contract.ui_nodes,runtimeContract:contract.ui_runtime_v3,baseRuntimeContract:contract.ui_runtime});
|
|
492
|
+
const compiled = compileScriptProject(config, files);
|
|
493
|
+
const runtime = runtimeSnapshot({now, timezone, locale, screen: {
|
|
494
|
+
width: screen.width ?? 390, height: screen.height ?? 844,
|
|
495
|
+
orientation: screen.orientation ?? ((screen.width ?? 390) >= (screen.height ?? 844) ? 'landscape' : 'portrait'),
|
|
496
|
+
size_class: screen.size_class ?? ((screen.width ?? 390) < 600 ? 'compact' : (screen.width ?? 390) < 840 ? 'medium' : 'expanded'),
|
|
497
|
+
text_scale: screen.text_scale ?? 1, platform: screen.platform ?? 'preview',
|
|
498
|
+
}});
|
|
499
|
+
const input = JSON.parse(files[fixturePath]);
|
|
500
|
+
const redact = value => Array.isArray(value) ? value.map(redact) : value && typeof value === 'object'
|
|
501
|
+
? Object.fromEntries(Object.entries(value).map(([key,item]) => [key, /authorization|password|secret|token|api[_-]?key/i.test(key) ? '[REDACTED]' : redact(item)])) : value;
|
|
502
|
+
const state={},computed={},data={},raw={},errors={},locals={},transformSteps=[],timeline=[];
|
|
503
|
+
const scope=extra=>({state,computed,data,runtime,context:{platform:'preview'},...extra});
|
|
504
|
+
function evaluate(value, extra={}) {
|
|
505
|
+
if(typeof value==='string'){
|
|
506
|
+
const matches=[...value.matchAll(/\{\{=([\s\S]*?)\}\}/g)];
|
|
507
|
+
const run=source=>{
|
|
508
|
+
const key=createHash('sha256').update(source.trim()).digest('hex');
|
|
509
|
+
const ast=config.compiled.expressions[key];
|
|
510
|
+
if(!ast)throw new Error(`Missing compiled expression ${source.trim()}`);
|
|
511
|
+
return new ScriptRuntime({modules:compiled.modules,profile:'inline'}).evaluate(ast,scope(extra)).value;
|
|
512
|
+
};
|
|
513
|
+
if(matches.length===1&&matches[0][0]===value)return run(matches[0][1]);
|
|
514
|
+
return value.replace(/\{\{=([\s\S]*?)\}\}/g,(_,source)=>run(source)??'');
|
|
515
|
+
}
|
|
516
|
+
if(Array.isArray(value))return value.map(item=>evaluate(item,extra));
|
|
517
|
+
if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,evaluate(item,extra)]));
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
for(const [key,value] of Object.entries(config.initial_state??{}))state[key]=evaluate(value);
|
|
521
|
+
const invoke=(transform,value,extra={})=>new ScriptRuntime({modules:compiled.modules}).invoke(transform.module,transform.export,value,scope(extra));
|
|
522
|
+
if(module&&exportName){
|
|
523
|
+
const result=new ScriptRuntime({modules:compiled.modules}).invoke(module,exportName,input,{runtime});
|
|
524
|
+
data.result=result.value;transformSteps.push({module,export:exportName,input:redact(input),output:redact(result.value),diagnostics:result.diagnostics});
|
|
525
|
+
}else for(const id of config.compiled.source_order??[]){
|
|
526
|
+
const source=config.data_sources.find(item=>item.id===id);
|
|
527
|
+
if(!source||source.when!==undefined&&!evaluate(source.when))continue;
|
|
528
|
+
const sourceInput=input&&typeof input==='object'&&Object.hasOwn(input,id)?input[id]:input;
|
|
529
|
+
raw[id]=redact(sourceInput);let value=sourceInput;
|
|
530
|
+
try{
|
|
531
|
+
for(const transform of Array.isArray(source.transform)?source.transform:source.transform?[source.transform]:[]){
|
|
532
|
+
const before=redact(value),result=invoke(transform,value);value=result.value;
|
|
533
|
+
transformSteps.push({source:id,module:transform.module,export:transform.export,input:before,output:redact(value),diagnostics:result.diagnostics});
|
|
534
|
+
}
|
|
535
|
+
data[id]=value;timeline.push({type:'source_completed',source:id});
|
|
536
|
+
}catch(error){errors[id]=String(error.message??error);timeline.push({type:'source_failed',source:id,error:errors[id]});}
|
|
537
|
+
}
|
|
538
|
+
for(const key of config.compiled.computed_order??[]){
|
|
539
|
+
const definition=config.computed_state?.[key];
|
|
540
|
+
computed[key]=definition?.module?invoke(definition,evaluate(definition.input)).value:evaluate(definition?.expression??definition);
|
|
541
|
+
}
|
|
542
|
+
let rendered=0;
|
|
543
|
+
const slot=(value,extra)=>[...(Array.isArray(value)?value:value?[value]:[])].map(item=>resolve(item,extra));
|
|
544
|
+
function resolve(node,extra={}){
|
|
545
|
+
if(!node||typeof node!=='object'||++rendered>2000)throw new Error('Resolved UI tree exceeds 2000 nodes');
|
|
546
|
+
if(node.visible_when!==undefined&&!evaluate(node.visible_when,extra))return {type:'hidden'};
|
|
547
|
+
const rawProps=node.props??{},deferred={...rawProps},slots=node.slots??{};
|
|
548
|
+
if(node.type==='repeat')delete deferred.key;if(node.type==='component')delete deferred.events;
|
|
549
|
+
const props=evaluate(deferred,extra);
|
|
550
|
+
for(const [key,value] of Object.entries(props))if(value&&typeof value==='object'&&!Array.isArray(value)&&['compact','medium','expanded'].some(name=>Object.hasOwn(value,name)))props[key]=value[runtime.screen.size_class]??value.compact??Object.values(value)[0];
|
|
551
|
+
if(node.type==='if')return {type:'fragment',children:slot(slots[props.condition?'then':'else'],extra)};
|
|
552
|
+
if(node.type==='repeat')return {type:'repeat',children:(Array.isArray(props.items)?props.items:[]).slice(0,200).map((item,index)=>({key:evaluate(rawProps.key,{...extra,[rawProps.as]:item,[rawProps.index_as??'index']:index}),children:slot(slots.item??node.children,{...extra,[rawProps.as]:item,[rawProps.index_as??'index']:index})}))};
|
|
553
|
+
if(node.type==='component'){
|
|
554
|
+
const definition=config.components[props.component],key=`${props.component}:${props.key??extra._key??node.id??''}`;
|
|
555
|
+
const local=locals[key]??=evaluate(definition.initial_state??{},extra),componentProps={...evaluate(definition.default_props??{},extra),...(props.props??{})},localComputed={};
|
|
556
|
+
const next={...extra,props:componentProps,state:local,root:{state},computed:localComputed,_slots:slots,_parent:extra};
|
|
557
|
+
for(const [name,value] of Object.entries(definition.computed_state??{}))localComputed[name]=evaluate(value,next);
|
|
558
|
+
return {type:'component',component:props.component,key,props:componentProps,child:resolve(definition.view,next)};
|
|
559
|
+
}
|
|
560
|
+
if(node.type==='slot')return {type:'fragment',children:slot(extra._slots?.[props.name],extra._parent??extra)};
|
|
561
|
+
if(node.type==='data_state'){
|
|
562
|
+
const status=errors[props.source]?'error':data[props.source]===undefined?'loading':props.empty_when===true||Array.isArray(data[props.source])&&!data[props.source].length?'empty':'content';
|
|
563
|
+
return {type:'data_state',source:props.source,status,children:slot(slots[status],{...extra,error:errors[props.source]??null})};
|
|
564
|
+
}
|
|
565
|
+
return {type:node.type,...(node.id?{id:node.id}:{}),props,...(node.action?{action:evaluate(node.action,extra)}:{}),children:(node.children??[]).map(child=>resolve(child,extra))};
|
|
566
|
+
}
|
|
567
|
+
const semanticTree=resolve(config.view);
|
|
568
|
+
const output=redact(Object.keys(data).length===1?Object.values(data)[0]:data);
|
|
569
|
+
return {
|
|
570
|
+
mode: 'fixture', valid: true, runtime, output,
|
|
571
|
+
diagnostics: transformSteps.map(step=>step.diagnostics), warnings: compiled.warnings,
|
|
572
|
+
inspector: {
|
|
573
|
+
root_state:redact(state),local_state:redact(locals),computed_state:redact(computed),raw_response:redact(input),raw_data:raw,data:redact(data),errors,
|
|
574
|
+
transform_steps:transformSteps,dependencies:Object.fromEntries((config.data_sources??[]).map(source=>[source.id,source.depends_on??[]])),
|
|
575
|
+
request_timeline:timeline.slice(-200),cache:[],execution:transformSteps.map(step=>step.diagnostics),component_instances:Object.keys(locals),
|
|
576
|
+
repeat_scopes:[],layout:{screen:runtime.screen,overflow:[]},source_map:[],runtime,resolved_ui_tree:redact(semanticTree),
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function compileProjectDirectory(source) {
|
|
582
|
+
const loaded = await loadProjectDirectory(source);
|
|
583
|
+
return {manifest: loaded.manifest};
|
|
584
|
+
}
|
|
585
|
+
|
|
418
586
|
export async function validateProjectDirectory(source) {
|
|
419
587
|
const loaded = await loadProjectDirectory(source);
|
|
420
588
|
return projectDirectoryResult(loaded);
|
|
@@ -444,7 +612,7 @@ export async function previewProjectDirectory(source, output) {
|
|
|
444
612
|
join(loaded.root, "dist", `${loaded.manifest.id}-preview.html`),
|
|
445
613
|
".html",
|
|
446
614
|
);
|
|
447
|
-
const content = previewHtml(loaded.manifest);
|
|
615
|
+
const content = loaded.manifest.ui_extensions.some(e=>e.config?.ui_schema===3) ? renderV3PreviewHtml(loaded.manifest,loaded.files) : previewHtml(loaded.manifest);
|
|
448
616
|
await mkdir(dirname(target), { recursive: true });
|
|
449
617
|
await writeFile(target, content);
|
|
450
618
|
return {
|
|
@@ -694,6 +862,7 @@ function installablePackagePath(name) {
|
|
|
694
862
|
if (["manifest.json", "signature.json", "README.md"].includes(name)) return true;
|
|
695
863
|
const parts = name.split("/");
|
|
696
864
|
if (parts[0] === "assets" && parts.length >= 2) return SAFE_ASSET.has(extname(name).toLowerCase());
|
|
865
|
+
if (parts[0] === "logic" && parts.length >= 2) return /\.(morit|ast\.json)$/.test(name);
|
|
697
866
|
if (parts[0] === "src" && parts.length >= 2) return name.toLowerCase().endsWith(".py");
|
|
698
867
|
return parts[0] === "children" && parts.length === 2 && name.toLowerCase().endsWith(".mplg");
|
|
699
868
|
}
|
|
@@ -799,6 +968,8 @@ function validateManifest(manifest, files) {
|
|
|
799
968
|
const minimum = manifest.min_morit_version.split(".").map(Number);
|
|
800
969
|
if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 14)))) && uiExtensions.some(extension => extension.config?.bottom_bar != null || usesUi1714(extension.config?.view))) throw new Error("pagination and bottom_bar require min_morit_version 1.7.14 or newer");
|
|
801
970
|
if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 13)))) && uiExtensions.some(extension => usesUi1713(extension.config?.view))) throw new Error("new UI properties require min_morit_version 1.7.13 or newer");
|
|
971
|
+
if (uiExtensions.some(e => e.config?.ui_schema === 3) && (minimum[0] < 1 || (minimum[0] === 1 && minimum[1] < 8))) throw new Error("Runtime v3 requires min_morit_version 1.8.0");
|
|
972
|
+
if (uiExtensions.some(e => requiresMorit19(e.config, contract.ui_runtime_v3)) && (minimum[0] < 1 || (minimum[0] === 1 && minimum[1] < 9))) throw new Error("Runtime v3.1 features require min_morit_version 1.9.0");
|
|
802
973
|
const credentials = objectCollection(manifest.credentials === undefined ? [] : manifest.credentials, "manifest.credentials", 16);
|
|
803
974
|
const slashCommands = objectCollection(manifest.slash_commands === undefined ? [] : manifest.slash_commands, "manifest.slash_commands", 32);
|
|
804
975
|
const connectors = objectCollection(manifest.connectors === undefined ? [] : manifest.connectors, "manifest.connectors", 32);
|
|
@@ -858,7 +1029,7 @@ function validateManifest(manifest, files) {
|
|
|
858
1029
|
assertObject(extensionConfig, `UI ${extension.id} config`);
|
|
859
1030
|
// Twelve legal component levels add an object and a children array per
|
|
860
1031
|
// level before semantic UI validation runs.
|
|
861
|
-
assertJsonSize(extensionConfig, 32 * 1024, `UI ${extension.id} config`, 40);
|
|
1032
|
+
assertJsonSize(extensionConfig, extensionConfig.ui_schema === 3 ? 512 * 1024 : 32 * 1024, `UI ${extension.id} config`, 40);
|
|
862
1033
|
}
|
|
863
1034
|
const storage = validateStorage(manifest, requestedPermissions);
|
|
864
1035
|
for (const extension of uiExtensions) validateUiConfig(extension, capabilities, uiIds, files, storage !== null);
|
|
@@ -885,13 +1056,22 @@ function validateManifest(manifest, files) {
|
|
|
885
1056
|
].filter(Boolean).join("; ");
|
|
886
1057
|
throw new Error(`every src/*.py file must be declared by exactly one sandbox_python entrypoint (${details})`);
|
|
887
1058
|
}
|
|
888
|
-
const
|
|
1059
|
+
const compiledManifest = canonicalJson(manifest);
|
|
1060
|
+
if (compiledManifest.length > 256 * 1024) throw new Error("compiled manifest.json exceeds Morit's 256 KiB install limit");
|
|
1061
|
+
const entries = new Map([["manifest.json", compiledManifest]]);
|
|
889
1062
|
for (const [name, content] of Object.entries(files)) {
|
|
890
1063
|
if (name === "README.md") entries.set(name, projectFileBytes(name, content));
|
|
891
1064
|
else if (name.startsWith("assets/") && SAFE_ASSET.has(extname(name).toLowerCase())) entries.set(name, projectFileBytes(name, content));
|
|
1065
|
+
else if (name.startsWith("logic/") && name.endsWith(".morit")) entries.set(name, projectFileBytes(name, content));
|
|
892
1066
|
else if (name.startsWith("src/") && name.endsWith(".py")) entries.set(name, projectFileBytes(name, content));
|
|
893
1067
|
else if (name.startsWith("children/") && name.endsWith(".mplg")) entries.set(name, projectFileBytes(name, content));
|
|
894
1068
|
}
|
|
1069
|
+
for (const extension of uiExtensions) {
|
|
1070
|
+
for (const declaration of extension.config?.logic_modules ?? []) {
|
|
1071
|
+
const compiled = extension.config.compiled.modules[declaration.id];
|
|
1072
|
+
entries.set(declaration.path.replace(/\.morit$/, '.ast.json'), canonicalJson({version:compiled.version,source_sha256:compiled.source_sha256,ast:compiled.ast}));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
895
1075
|
const declaredChildren = new Set(dependencies.filter((value) => value.package_path).map((value) => value.package_path));
|
|
896
1076
|
const packagedChildren = new Set([...entries.keys()].filter((name) => name.startsWith("children/")));
|
|
897
1077
|
if (!equalSets(declaredChildren, packagedChildren)) {
|
|
@@ -1231,6 +1411,7 @@ function validateHttpRuntime(runtime, endpoint) {
|
|
|
1231
1411
|
if (new URLSearchParams(entries).toString().length > 16 * 1024) throw new Error("http_json request_params is too large");
|
|
1232
1412
|
}
|
|
1233
1413
|
const response = runtime.response || {};
|
|
1414
|
+
if(response.result_mode!==undefined&&response.result_mode!=="raw")throw new Error("invalid response.result_mode");
|
|
1234
1415
|
rejectUnknownFields(response, new Set(contract.object_fields.http_json_response), "http_json response");
|
|
1235
1416
|
const projection = "summary_path" in response || "data_path" in response;
|
|
1236
1417
|
if (projection && (Object.keys(response).some(name => !["summary_path", "data_path", "summary_prefix"].includes(name)) ||
|
|
@@ -1285,11 +1466,6 @@ function validateRuntime(capability, runtime, permissions, credentials, connecto
|
|
|
1285
1466
|
if (!permissions.has("storage")) throw new Error("calendar_store requires storage permission");
|
|
1286
1467
|
if (runtime.operation === "reminder" && !permissions.has("notifications")) throw new Error("calendar reminder requires notifications permission");
|
|
1287
1468
|
}
|
|
1288
|
-
if (adapter === "neis_school") {
|
|
1289
|
-
if (!permissions.has("network") || !permissions.has("storage")) throw new Error("neis_school requires network and storage permissions");
|
|
1290
|
-
if (!["school_search", "setup", "lookup", "overview", "search", "reminder", "briefing"].includes(runtime.operation)) throw new Error("invalid neis_school operation");
|
|
1291
|
-
if (["reminder", "briefing"].includes(runtime.operation) && !permissions.has("notifications")) throw new Error("NEIS reminder requires notifications permission");
|
|
1292
|
-
}
|
|
1293
1469
|
if (capability.kind === "provider" && (runtime.role !== "search" || !adapter)) throw new Error("provider capabilities must declare a search runtime");
|
|
1294
1470
|
if (capability.kind === "background" && Object.keys(runtime).length) {
|
|
1295
1471
|
if (!Number.isInteger(runtime.interval_minutes) || runtime.interval_minutes < 15 || runtime.interval_minutes > 10080) {
|
|
@@ -1322,6 +1498,11 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
|
|
|
1322
1498
|
output_schema: value.output_schema,
|
|
1323
1499
|
}]));
|
|
1324
1500
|
if (hasStorage) for (const capability of STORAGE_TOOLS) executable.add(capability);
|
|
1501
|
+
if (config.ui_schema === 3) {
|
|
1502
|
+
compileUiV3(config, files, {capabilities: executable, routes: routeIds, nodeTypes: contract.ui_nodes, runtimeContract: contract.ui_runtime_v3, baseRuntimeContract: contract.ui_runtime});
|
|
1503
|
+
validateUiCapabilityBindings(config, config.data_sources ?? [], capabilitySchemas);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1325
1506
|
if (extension.point === "response" && config.ui_schema !== 2) throw new Error("response UI requires UI Runtime v2");
|
|
1326
1507
|
if (config.ui_schema === 2) {
|
|
1327
1508
|
if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
|
|
@@ -1588,13 +1769,21 @@ function validateThemeContrast(theme) {
|
|
|
1588
1769
|
function collectThemeWarnings(manifest) {
|
|
1589
1770
|
const warnings = [];
|
|
1590
1771
|
for (const extension of manifest.ui_extensions || []) {
|
|
1772
|
+
const v3 = extension.config?.ui_schema === 3;
|
|
1591
1773
|
function visit(node, path) {
|
|
1592
1774
|
if (!node || typeof node !== "object") return;
|
|
1593
|
-
const allowed =
|
|
1594
|
-
|
|
1775
|
+
const allowed = v3
|
|
1776
|
+
? uiV3AllowedProps(node.type, contract.ui_runtime_v3, contract.ui_runtime)
|
|
1777
|
+
: new Set([
|
|
1778
|
+
...(contract.ui_runtime.component_props[node.type] || []),
|
|
1779
|
+
...(contract.ui_runtime.common_props_excluded_components.includes(node.type) ? [] : contract.ui_runtime.common_props),
|
|
1780
|
+
]);
|
|
1781
|
+
for (const key of Object.keys(node.props || {})) if (!allowed.has(key)) warnings.push(`${path}.props.${key}: ignored by ${node.type}; remove or move to a supported component`);
|
|
1595
1782
|
(node.children || []).forEach((child, index) => visit(child, `${path}.children[${index}]`));
|
|
1783
|
+
for (const [name, value] of Object.entries(node.slots || {})) (Array.isArray(value) ? value : [value]).forEach((child, index) => visit(child, `${path}.slots.${name}[${index}]`));
|
|
1596
1784
|
}
|
|
1597
1785
|
for (const field of ["view", "bottom_bar"]) visit(extension.config?.[field], `${extension.id}.${field}`);
|
|
1786
|
+
if (v3) for (const [name, component] of Object.entries(extension.config?.components || {})) visit(component.view, `${extension.id}.components.${name}.view`);
|
|
1598
1787
|
const theme = extension?.config?.theme;
|
|
1599
1788
|
if (!theme || typeof theme !== "object" || Array.isArray(theme)) continue;
|
|
1600
1789
|
for (const [mode, variant] of [["base", theme], ["light", theme.light], ["dark", theme.dark]]) {
|
|
@@ -1952,7 +2141,7 @@ function validateUiAction(action, executable, routes, sources, stateKeys, requir
|
|
|
1952
2141
|
invoke: new Set(["type", "capability", "query", "arguments", "store", "validate"]),
|
|
1953
2142
|
navigate: new Set(["type", "target", "transition", "replace", "parameters", "result_state", "validate"]),
|
|
1954
2143
|
set_state: new Set(["type", "values", "persist"]),
|
|
1955
|
-
refresh: new Set(["type", "source"]),
|
|
2144
|
+
refresh: new Set(["type", "source", "validate"]),
|
|
1956
2145
|
back: new Set(["type", "result"]),
|
|
1957
2146
|
}[action.type];
|
|
1958
2147
|
rejectUnknownFields(action, allowed, "UI action");
|