@datalayer/agent-runtimes 1.3.97 → 1.3.98
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/dist-embed/datalayer-app-main.js +7 -7
- package/lib/apps/apps/AppRenderer.js +5 -2
- package/lib/apps/apps/AppRuntimePlugins.d.ts +6 -2
- package/lib/apps/apps/AppRuntimePlugins.js +31 -4
- package/lib/apps/apps/customComponents.d.ts +20 -2
- package/lib/apps/apps/customComponents.js +24 -3
- package/lib/apps/core/appFiles.d.ts +7 -0
- package/lib/apps/core/appFiles.js +24 -0
- package/lib/apps/core/index.d.ts +1 -0
- package/lib/apps/core/index.js +3 -0
- package/lib/apps/embed/AppEmbed.js +5 -2
- package/lib/apps/index.d.ts +1 -1
- package/lib/apps/index.js +1 -1
- package/lib/apps/plugins/app-components/index.js +1 -1
- package/lib/components/a2ui/custom/index.d.ts +15 -3
- package/lib/components/a2ui/custom/index.js +65 -19
- package/lib/specs/appspecSchema.js +1 -1
- package/package.json +1 -1
|
@@ -71,6 +71,7 @@ import { keepsFeedback } from './feedback';
|
|
|
71
71
|
import { keptBeforeFirstMessage } from './kept';
|
|
72
72
|
import { ChatLanguage } from '../../chat/ChatLanguage';
|
|
73
73
|
import { defineAppRuntimePlugin } from './AppRuntimePlugins';
|
|
74
|
+
import { hasFolderModules } from './customComponents';
|
|
74
75
|
import { appPluginPair } from './pluginPair';
|
|
75
76
|
/** The id of an agent or a Cog, without its version. */
|
|
76
77
|
export const agentIdOf = (app) => {
|
|
@@ -232,8 +233,10 @@ export function appPreset(app, options = {}) {
|
|
|
232
233
|
return {
|
|
233
234
|
plugins: [
|
|
234
235
|
defineAppPlugin(app, shown),
|
|
235
|
-
// What its runtime holds, told to the page (F-15)
|
|
236
|
-
|
|
236
|
+
// What its runtime holds, told to the page (F-15), and its packaged
|
|
237
|
+
// page side read from it when its components are files of its folder
|
|
238
|
+
// (P-29).
|
|
239
|
+
defineAppRuntimePlugin(app.id, { pageSide: hasFolderModules(app) }),
|
|
237
240
|
...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
|
|
238
241
|
// The components its developer wrote (LOOP P-17): blocks of its page
|
|
239
242
|
// and renderers of its page, its elements and its answers — its own.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
2
2
|
export declare const APP_RUNTIME_PLUGIN_NAME = "@datalayer/loop-plugin-app-runtime";
|
|
3
3
|
/** Follows the application's runtime, and tells the page what it holds. */
|
|
4
|
-
export declare function AppRuntimePlugins({ appId }: {
|
|
4
|
+
export declare function AppRuntimePlugins({ appId, pageSide, }: {
|
|
5
5
|
appId: string;
|
|
6
|
+
/** Its page side is packaged, to be read from its runtime (P-29). */
|
|
7
|
+
pageSide?: boolean;
|
|
6
8
|
}): null;
|
|
7
9
|
/** The plugin that follows an application's runtime for its page. */
|
|
8
|
-
export declare function defineAppRuntimePlugin(appId: string
|
|
10
|
+
export declare function defineAppRuntimePlugin(appId: string, { pageSide }?: {
|
|
11
|
+
pageSide?: boolean;
|
|
12
|
+
}): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
@@ -23,13 +23,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
23
23
|
* its own runtime, and the pair is whole: nothing stands down, nothing is
|
|
24
24
|
* blank.
|
|
25
25
|
*
|
|
26
|
+
* An application whose components are files of its folder (LOOP P-29) has
|
|
27
|
+
* its page side in its package, installed beside the server that runs it:
|
|
28
|
+
* once on a runtime, the page reads it there with Reactor's
|
|
29
|
+
* `bootstrapExtensions` (`<base>/plugins/frontend-extensions`) and installs
|
|
30
|
+
* it into the running platform — the extension `loop-app-<id>`, the
|
|
31
|
+
* application's, its page-side plugin saying where the folder's files are
|
|
32
|
+
* (`LoopAppFiles`). A runtime with no package of it answers nothing, and its
|
|
33
|
+
* components say so.
|
|
34
|
+
*
|
|
26
35
|
* A plugin of its own, with no backend requirement, so that it keeps
|
|
27
36
|
* listening while the application's plugin is stood down. It renders nothing.
|
|
28
37
|
*
|
|
29
38
|
* @module apps/apps/AppRuntimePlugins
|
|
30
39
|
*/
|
|
31
40
|
import { useCallback, useEffect } from 'react';
|
|
32
|
-
import { definePlugin } from '@datalayer/reactor';
|
|
41
|
+
import { bootstrapExtensions, definePlugin, } from '@datalayer/reactor';
|
|
33
42
|
import { registerReactor, useBackendPluginStream, useReactorPlatform, useSignalValue, } from '@datalayer/reactor/react';
|
|
34
43
|
import { IDLE_SANDBOX_SNAPSHOT_SIGNAL, IDLE_SANDBOX_TARGET_SIGNAL, LoopSlots, } from '../core';
|
|
35
44
|
import { useOptionalSandboxService } from '../plugins/agents/useSandboxService';
|
|
@@ -38,7 +47,7 @@ export const APP_RUNTIME_PLUGIN_NAME = '@datalayer/loop-plugin-app-runtime';
|
|
|
38
47
|
/** A host's answer to "is this runtime plugin running?", from a list. */
|
|
39
48
|
const runningOf = (names) => (name) => names.includes(name);
|
|
40
49
|
/** Follows the application's runtime, and tells the page what it holds. */
|
|
41
|
-
export function AppRuntimePlugins({ appId }) {
|
|
50
|
+
export function AppRuntimePlugins({ appId, pageSide = false, }) {
|
|
42
51
|
const reactor = useReactorPlatform();
|
|
43
52
|
const service = useOptionalSandboxService();
|
|
44
53
|
const target = useSignalValue(service?.target ?? IDLE_SANDBOX_TARGET_SIGNAL);
|
|
@@ -64,11 +73,29 @@ export function AppRuntimePlugins({ appId }) {
|
|
|
64
73
|
useBackendPluginStream(runtime ? appPluginsBase(runtime, appId) : undefined, {
|
|
65
74
|
onState,
|
|
66
75
|
});
|
|
76
|
+
// Its packaged page side, installed from its runtime (P-29): the
|
|
77
|
+
// application's extension only, whatever else the server holds.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!runtime || !pageSide) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let current = true;
|
|
83
|
+
void bootstrapExtensions(appPluginsBase(runtime, appId)).then(found => {
|
|
84
|
+
for (const extension of found.filter(item => item.name === name)) {
|
|
85
|
+
if (current) {
|
|
86
|
+
void reactor.install(extension);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
current = false;
|
|
92
|
+
};
|
|
93
|
+
}, [reactor, runtime, appId, name, pageSide]);
|
|
67
94
|
return null;
|
|
68
95
|
}
|
|
69
96
|
/** The plugin that follows an application's runtime for its page. */
|
|
70
|
-
export function defineAppRuntimePlugin(appId) {
|
|
71
|
-
const Follow = () => _jsx(AppRuntimePlugins, { appId: appId });
|
|
97
|
+
export function defineAppRuntimePlugin(appId, { pageSide = false } = {}) {
|
|
98
|
+
const Follow = () => _jsx(AppRuntimePlugins, { appId: appId, pageSide: pageSide });
|
|
72
99
|
return definePlugin({
|
|
73
100
|
name: APP_RUNTIME_PLUGIN_NAME,
|
|
74
101
|
displayName: 'Its runtime',
|
|
@@ -7,10 +7,23 @@ export declare const CUSTOM_PROPERTY_TYPES: readonly ["string", "integer", "numb
|
|
|
7
7
|
export declare const CUSTOM_RESERVED_NAMES: readonly ["id", "component", "action", "weight", "visible_when", "children", "child"];
|
|
8
8
|
/**
|
|
9
9
|
* Where its module is loaded from: an address served over HTTPS, or this
|
|
10
|
-
* machine while it is written.
|
|
11
|
-
* its packaging (P-29).
|
|
10
|
+
* machine while it is written.
|
|
12
11
|
*/
|
|
13
12
|
export declare const CUSTOM_SOURCE: RegExp;
|
|
13
|
+
/**
|
|
14
|
+
* Or a file of the application's folder, by its path in it (LOOP P-29): a
|
|
15
|
+
* `.js` or `.mjs` file inside the folder — no absolute path, no `..`.
|
|
16
|
+
* Packaged with the application (`loop apps package`), it is served by the
|
|
17
|
+
* server its package is installed beside (`LoopAppFiles`).
|
|
18
|
+
*/
|
|
19
|
+
export declare const CUSTOM_FILE: RegExp;
|
|
20
|
+
/** Whether a component's module is a file of its application's folder. */
|
|
21
|
+
export declare const isFolderSource: (source: string) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The address a file of the folder is fetched from, under the base its
|
|
24
|
+
* package's page side said (`LoopAppFiles`).
|
|
25
|
+
*/
|
|
26
|
+
export declare const folderSourceUrl: (source: string, base: string) => string;
|
|
14
27
|
/** A Subresource Integrity hash: the module as it was reviewed. */
|
|
15
28
|
export declare const INTEGRITY: RegExp;
|
|
16
29
|
/**
|
|
@@ -21,6 +34,11 @@ export declare const INTEGRITY: RegExp;
|
|
|
21
34
|
export declare function customPropsRefused(component: AppCustomComponentSpec, props: Record<string, unknown>, bound?: readonly string[]): string[];
|
|
22
35
|
/** What stops one component its developer wrote from being reviewed, in sentences. */
|
|
23
36
|
export declare function customComponentProblems(component: AppCustomComponentSpec): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Whether an application's page side is packaged (LOOP P-29): a component
|
|
39
|
+
* its developer wrote is a file of its folder.
|
|
40
|
+
*/
|
|
41
|
+
export declare const hasFolderModules: (app: Pick<AppSpec, "interface">) => boolean;
|
|
24
42
|
/** What stops the components an application's developer wrote, in sentences. */
|
|
25
43
|
export declare function customComponentsProblems(app: AppSpec): string[];
|
|
26
44
|
/** A component its developer wrote, by name, or undefined. */
|
|
@@ -40,10 +40,24 @@ export const CUSTOM_RESERVED_NAMES = [
|
|
|
40
40
|
];
|
|
41
41
|
/**
|
|
42
42
|
* Where its module is loaded from: an address served over HTTPS, or this
|
|
43
|
-
* machine while it is written.
|
|
44
|
-
* its packaging (P-29).
|
|
43
|
+
* machine while it is written.
|
|
45
44
|
*/
|
|
46
45
|
export const CUSTOM_SOURCE = /^(?:https:\/\/[A-Za-z0-9.-]+(?::\d+)?|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?)\/\S*$/;
|
|
46
|
+
/**
|
|
47
|
+
* Or a file of the application's folder, by its path in it (LOOP P-29): a
|
|
48
|
+
* `.js` or `.mjs` file inside the folder — no absolute path, no `..`.
|
|
49
|
+
* Packaged with the application (`loop apps package`), it is served by the
|
|
50
|
+
* server its package is installed beside (`LoopAppFiles`).
|
|
51
|
+
*/
|
|
52
|
+
export const CUSTOM_FILE = /^(?:\.\/)?(?:[A-Za-z0-9_][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_][A-Za-z0-9_.-]*\.m?js$/;
|
|
53
|
+
/** Whether a component's module is a file of its application's folder. */
|
|
54
|
+
export const isFolderSource = (source) => !/^[a-z][a-z0-9+.-]*:/.test(source) && CUSTOM_FILE.test(source);
|
|
55
|
+
/**
|
|
56
|
+
* The address a file of the folder is fetched from, under the base its
|
|
57
|
+
* package's page side said (`LoopAppFiles`).
|
|
58
|
+
*/
|
|
59
|
+
export const folderSourceUrl = (source, base) => new URL(source.replace(/^\.\//, ''), base.endsWith('/') ? base : `${base}/`)
|
|
60
|
+
.href;
|
|
47
61
|
/** A Subresource Integrity hash: the module as it was reviewed. */
|
|
48
62
|
export const INTEGRITY = /^sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}$/;
|
|
49
63
|
const PROPERTY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -117,7 +131,9 @@ export function customComponentProblems(component) {
|
|
|
117
131
|
problems.push(`${said} says what it is for, under \`description\`.`);
|
|
118
132
|
}
|
|
119
133
|
if (!/^[a-z][a-z0-9+.-]*:/.test(component.source)) {
|
|
120
|
-
|
|
134
|
+
if (!CUSTOM_FILE.test(component.source)) {
|
|
135
|
+
problems.push(`${said}'s module “${component.source}” is a file of the application's folder, named by its path in it: a \`.js\` or \`.mjs\` file inside the folder (\`gauge.js\`, \`components/gauge.js\`).`);
|
|
136
|
+
}
|
|
121
137
|
}
|
|
122
138
|
else if (!CUSTOM_SOURCE.test(component.source)) {
|
|
123
139
|
problems.push(`${said}'s module “${component.source}” is loaded over \`https://\`, or from \`http://localhost\` while it is written.`);
|
|
@@ -197,6 +213,11 @@ export function customComponentProblems(component) {
|
|
|
197
213
|
}
|
|
198
214
|
return problems;
|
|
199
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Whether an application's page side is packaged (LOOP P-29): a component
|
|
218
|
+
* its developer wrote is a file of its folder.
|
|
219
|
+
*/
|
|
220
|
+
export const hasFolderModules = (app) => (app.interface.customComponents ?? []).some(component => isFolderSource(component.source));
|
|
200
221
|
/** What stops the components an application's developer wrote, in sentences. */
|
|
201
222
|
export function customComponentsProblems(app) {
|
|
202
223
|
const own = app.interface.customComponents ?? [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type AppFilesContribution = {
|
|
2
|
+
/** The application whose folder it is, by id. */
|
|
3
|
+
app: string;
|
|
4
|
+
/** The absolute address its folder's files are served under, ending in `/`. */
|
|
5
|
+
base: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const LoopAppFiles: import("@datalayer/reactor").ContributionPoint<AppFilesContribution>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Where the files of an application's folder are served (LOOP P-29), as a
|
|
7
|
+
* contribution point, re-exported by `apps/core`.
|
|
8
|
+
*
|
|
9
|
+
* A component its developer wrote may be a file of its folder (`source:
|
|
10
|
+
* gauge.js`). `loop apps package` puts the folder's files in the
|
|
11
|
+
* application's wheel, beside a generated page-side plugin
|
|
12
|
+
* (`loop-app-<id>/page`, `agent_runtimes/loop/apps/packaging.py`) whose one
|
|
13
|
+
* contribution is to this point: the address its own directory is served at
|
|
14
|
+
* by the server the wheel is installed beside. The page installs that plugin
|
|
15
|
+
* through Reactor's `bootstrapExtensions` (`AppRuntimePlugins`), and a file
|
|
16
|
+
* of the folder is fetched from there and drawn in its sandboxed frame.
|
|
17
|
+
*
|
|
18
|
+
* The plugin is a plain object of no import, so the point is named by its
|
|
19
|
+
* id: `loop.app.files` is what the generated module says too.
|
|
20
|
+
*
|
|
21
|
+
* @module apps/core/appFiles
|
|
22
|
+
*/
|
|
23
|
+
import { defineContributionPoint } from '@datalayer/reactor';
|
|
24
|
+
export const LoopAppFiles = defineContributionPoint('loop.app.files');
|
package/lib/apps/core/index.d.ts
CHANGED
|
@@ -600,6 +600,7 @@ export declare const LoopAssistantMenu: import("@datalayer/reactor").Contributio
|
|
|
600
600
|
export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
|
|
601
601
|
export { LoopCanvasPart, type CanvasPartContribution, type CanvasPartKind, } from './canvasParts';
|
|
602
602
|
export { LoopA2uiComponent, type A2uiComponentContribution, } from './a2uiComponents';
|
|
603
|
+
export { LoopAppFiles, type AppFilesContribution } from './appFiles';
|
|
603
604
|
/**
|
|
604
605
|
* The per-example chat extras a host feeds the loop's conversation live.
|
|
605
606
|
*
|
package/lib/apps/core/index.js
CHANGED
|
@@ -178,6 +178,9 @@ export { LoopCanvasPart, } from './canvasParts';
|
|
|
178
178
|
// A component an application adds to the catalog it is drawn with (LOOP
|
|
179
179
|
// P-17): the components its developer wrote, for its workspace alone.
|
|
180
180
|
export { LoopA2uiComponent, } from './a2uiComponents';
|
|
181
|
+
// Where the files of an application's folder are served (LOOP P-29): said
|
|
182
|
+
// by the page side its package carries.
|
|
183
|
+
export { LoopAppFiles } from './appFiles';
|
|
181
184
|
export const LoopChatExtras = defineContributionPoint('loop.chat.extras');
|
|
182
185
|
export const LoopOpeningNotebook = defineContributionPoint('loop.notebook.opening');
|
|
183
186
|
export const LoopOpeningDocument = defineContributionPoint('loop.document.opening');
|
|
@@ -359,7 +359,11 @@ export function EmbedThemed({ accent, colorMode, font, variant, children, style,
|
|
|
359
359
|
_jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: getThemeConfig(variant).primerTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
|
|
360
360
|
}
|
|
361
361
|
export function AppEmbed({ app, mode, accent, colorMode, font, language, serverUrl, instance, embedToken, visitors, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
|
|
362
|
-
const
|
|
362
|
+
const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
|
|
363
|
+
// Inline, the page is bridged here; in a bubble, a panel or the assistant,
|
|
364
|
+
// `AppFloating` bridges it itself. Bridged twice, every window message of a
|
|
365
|
+
// turn reached the page twice (seen 2026-10-10 with P-27's copilot).
|
|
366
|
+
const bridge = useHostBridge(app, shownAs === 'inline' ? host : undefined);
|
|
363
367
|
// Where a visitor without an account runs it (D-07): one object, so that
|
|
364
368
|
// the renderer never reads a change.
|
|
365
369
|
const renderer = useMemo(() => (visitors ? { datalayerVisitors: visitors } : undefined), [visitors]);
|
|
@@ -377,7 +381,6 @@ export function AppEmbed({ app, mode, accent, colorMode, font, language, serverU
|
|
|
377
381
|
const variant = app.interface.theme?.variant ?? 'loop';
|
|
378
382
|
const asked = colorMode ?? app.interface.theme?.mode ?? 'auto';
|
|
379
383
|
const resolvedMode = asked === 'auto' ? system : asked;
|
|
380
|
-
const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
|
|
381
384
|
const view = floatingViewOf(shownAs);
|
|
382
385
|
const worn = accent ?? app.interface.accent;
|
|
383
386
|
const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font, variant }), [worn, font, variant]);
|
package/lib/apps/index.d.ts
CHANGED
|
@@ -76,4 +76,4 @@ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSn
|
|
|
76
76
|
export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
|
|
77
77
|
export { loopPlugins, type LoopPresetOptions } from './presets';
|
|
78
78
|
export { DocumentExtension, NotebookExtension } from './extensions';
|
|
79
|
-
export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, pageOutputShows, } from './apps';
|
|
79
|
+
export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, folderSourceUrl, hasFolderModules, isFolderSource, pageOutputShows, } from './apps';
|
package/lib/apps/index.js
CHANGED
|
@@ -103,4 +103,4 @@ export { loopPlugins } from './presets';
|
|
|
103
103
|
export { DocumentExtension, NotebookExtension } from './extensions';
|
|
104
104
|
// Applications: what one does when its agent calls a tool — the rules a
|
|
105
105
|
// person wrote, decided on what the tool does.
|
|
106
|
-
export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, defineAppPlugin, appLayoutOptions, readAppspecYaml, writeAppspecYaml, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, pageOutputShows, } from './apps';
|
|
106
|
+
export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, isOrganizationFrame, ORGANIZATION_FRAME_PREFIX, defineAppPlugin, appLayoutOptions, readAppspecYaml, writeAppspecYaml, appComponents, customComponentEntry, customComponentOf, customComponentProblems, customComponentsProblems, folderSourceUrl, hasFolderModules, isFolderSource, pageOutputShows, } from './apps';
|
|
@@ -49,7 +49,7 @@ export function defineAppComponentsPlugin(app, fetcher) {
|
|
|
49
49
|
contribution(LoopA2uiComponent, {
|
|
50
50
|
id: component.name,
|
|
51
51
|
app: app.id,
|
|
52
|
-
implementation: customImplementation(component, app.version, fetcher),
|
|
52
|
+
implementation: customImplementation(component, app.version, fetcher, app.id),
|
|
53
53
|
}, { id: component.name }),
|
|
54
54
|
]),
|
|
55
55
|
});
|
|
@@ -21,19 +21,31 @@ type CustomProps = Record<string, unknown> & {
|
|
|
21
21
|
};
|
|
22
22
|
/** What the frame is given: its declared properties and what it shows, nothing else. */
|
|
23
23
|
export declare function givenToFrame(component: AppCustomComponentSpec, props: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Where the files of an application's folder are served, as its package's
|
|
26
|
+
* page side said (`LoopAppFiles`, LOOP P-29); undefined until it says it —
|
|
27
|
+
* while its runtime starts, or for ever on a server it is not installed
|
|
28
|
+
* beside.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useAppFilesBase(appId: string): string | undefined;
|
|
24
31
|
/** A component its developer wrote, in its frame. */
|
|
25
|
-
export declare function CustomComponentView({ component, props, fetcher, }: {
|
|
32
|
+
export declare function CustomComponentView({ component, props, fetcher, appId, }: {
|
|
26
33
|
component: AppCustomComponentSpec;
|
|
27
34
|
props: CustomProps;
|
|
28
35
|
/** How its module is fetched; the page's `fetch` unless given (tests). */
|
|
29
36
|
fetcher?: typeof fetch;
|
|
37
|
+
/**
|
|
38
|
+
* Its application's id: where a module that is a file of its folder is
|
|
39
|
+
* served is said for that application (LOOP P-29).
|
|
40
|
+
*/
|
|
41
|
+
appId?: string;
|
|
30
42
|
}): JSX.Element;
|
|
31
43
|
/**
|
|
32
44
|
* A component its developer wrote as A2UI's renderer registers one: its
|
|
33
45
|
* schema from its declaration, its view by A2UI's own factory, drawn in its
|
|
34
46
|
* frame.
|
|
35
47
|
*/
|
|
36
|
-
export declare function customImplementation(component: AppCustomComponentSpec, version?: string, fetcher?: typeof fetch): ReactComponentImplementation;
|
|
48
|
+
export declare function customImplementation(component: AppCustomComponentSpec, version?: string, fetcher?: typeof fetch, appId?: string): ReactComponentImplementation;
|
|
37
49
|
/** The renderers of the components an application's developer wrote, for its page alone. */
|
|
38
|
-
export declare function customImplementations(app: Pick<AppSpec, 'interface' | 'version'>, fetcher?: typeof fetch): ReactComponentImplementation[];
|
|
50
|
+
export declare function customImplementations(app: Pick<AppSpec, 'id' | 'interface' | 'version'>, fetcher?: typeof fetch): ReactComponentImplementation[];
|
|
39
51
|
export { CUSTOM_FRAME_SANDBOX, customFrameDocument, customFramePolicy, customModule, fromFrame, integrityHolds, } from './sandbox';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
3
|
+
import { useOptionalReactorPlatform } from '@datalayer/reactor/react';
|
|
3
4
|
import { createComponentImplementation, } from '@a2ui/react/v0_9';
|
|
4
|
-
import { customComponentEntry } from '../../../apps/apps/customComponents';
|
|
5
|
+
import { customComponentEntry, folderSourceUrl, isFolderSource, } from '../../../apps/apps/customComponents';
|
|
6
|
+
import { LoopAppFiles } from '../../../apps/core/appFiles';
|
|
5
7
|
import { componentSchemaOf } from '../datalayer/schema';
|
|
6
8
|
import { BlockFrame, Problem } from '../datalayer/parts';
|
|
7
9
|
import { CUSTOM_FRAME_SANDBOX, FRAME_TAG, customFrameDocument, customModule, frameNonce, fromFrame, } from './sandbox';
|
|
@@ -13,11 +15,37 @@ export function givenToFrame(component, props) {
|
|
|
13
15
|
}
|
|
14
16
|
/** The setter A2UI gives a binding: `setValue` for `value`. */
|
|
15
17
|
const setterOf = (name) => `set${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
18
|
+
/**
|
|
19
|
+
* Where the files of an application's folder are served, as its package's
|
|
20
|
+
* page side said (`LoopAppFiles`, LOOP P-29); undefined until it says it —
|
|
21
|
+
* while its runtime starts, or for ever on a server it is not installed
|
|
22
|
+
* beside.
|
|
23
|
+
*/
|
|
24
|
+
export function useAppFilesBase(appId) {
|
|
25
|
+
const reactor = useOptionalReactorPlatform();
|
|
26
|
+
const subscribe = useCallback((listener) => reactor ? reactor.subscribe(listener) : () => undefined, [reactor]);
|
|
27
|
+
const revision = useSyncExternalStore(subscribe, () => reactor?.getRevision() ?? 0);
|
|
28
|
+
return useMemo(() => reactor
|
|
29
|
+
?.getContributions(LoopAppFiles)
|
|
30
|
+
.find(entry => entry.value.app === appId)?.value.base,
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
[reactor, revision, appId]);
|
|
33
|
+
}
|
|
16
34
|
/** A component its developer wrote, in its frame. */
|
|
17
|
-
export function CustomComponentView({ component, props, fetcher, }) {
|
|
35
|
+
export function CustomComponentView({ component, props, fetcher, appId = '', }) {
|
|
18
36
|
const frame = useRef(null);
|
|
19
37
|
const [problem, setProblem] = useState(null);
|
|
20
38
|
const [ready, setReady] = useState(false);
|
|
39
|
+
const [frameReady, setFrameReady] = useState(false);
|
|
40
|
+
// Its module's address: its own, or a file of its folder under the base
|
|
41
|
+
// its package's page side said — none until said.
|
|
42
|
+
const folder = isFolderSource(component.source);
|
|
43
|
+
const base = useAppFilesBase(appId);
|
|
44
|
+
const source = folder
|
|
45
|
+
? base
|
|
46
|
+
? folderSourceUrl(component.source, base)
|
|
47
|
+
: undefined
|
|
48
|
+
: component.source;
|
|
21
49
|
const srcDoc = useMemo(() => customFrameDocument(frameNonce(), component.name), [component.name]);
|
|
22
50
|
// What it is given, told again only when a value of it changes.
|
|
23
51
|
const givenKey = JSON.stringify(givenToFrame(component, props));
|
|
@@ -32,17 +60,7 @@ export function CustomComponentView({ component, props, fetcher, }) {
|
|
|
32
60
|
return;
|
|
33
61
|
}
|
|
34
62
|
if (said.kind === 'ready') {
|
|
35
|
-
|
|
36
|
-
.then(code => {
|
|
37
|
-
tell({
|
|
38
|
-
tag: FRAME_TAG,
|
|
39
|
-
kind: 'props',
|
|
40
|
-
props: givenToFrame(component, propsRef.current),
|
|
41
|
-
});
|
|
42
|
-
tell({ tag: FRAME_TAG, kind: 'module', code });
|
|
43
|
-
setReady(true);
|
|
44
|
-
})
|
|
45
|
-
.catch((error) => setProblem(error instanceof Error ? error.message : String(error)));
|
|
63
|
+
setFrameReady(true);
|
|
46
64
|
}
|
|
47
65
|
else if (said.kind === 'failed') {
|
|
48
66
|
setProblem(`It could not be drawn: ${said.message}.`);
|
|
@@ -62,13 +80,41 @@ export function CustomComponentView({ component, props, fetcher, }) {
|
|
|
62
80
|
};
|
|
63
81
|
window.addEventListener('message', listen);
|
|
64
82
|
return () => window.removeEventListener('message', listen);
|
|
65
|
-
}, [component
|
|
83
|
+
}, [component]);
|
|
84
|
+
// Its module, handed over once the frame is ready and its address known.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!frameReady || !source) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let current = true;
|
|
90
|
+
customModule(source, component.integrity, fetcher)
|
|
91
|
+
.then(code => {
|
|
92
|
+
if (!current) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
tell({
|
|
96
|
+
tag: FRAME_TAG,
|
|
97
|
+
kind: 'props',
|
|
98
|
+
props: givenToFrame(component, propsRef.current),
|
|
99
|
+
});
|
|
100
|
+
tell({ tag: FRAME_TAG, kind: 'module', code });
|
|
101
|
+
setReady(true);
|
|
102
|
+
})
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
if (current) {
|
|
105
|
+
setProblem(error instanceof Error ? error.message : String(error));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return () => {
|
|
109
|
+
current = false;
|
|
110
|
+
};
|
|
111
|
+
}, [frameReady, source, component, fetcher]);
|
|
66
112
|
useEffect(() => {
|
|
67
113
|
if (ready) {
|
|
68
114
|
tell({ tag: FRAME_TAG, kind: 'props', props: given });
|
|
69
115
|
}
|
|
70
116
|
}, [ready, given]);
|
|
71
|
-
return (_jsxs(BlockFrame, { label: component.description || component.name, weight: props.weight, testId: `custom-${component.name}`, children: [problem ? _jsx(Problem, { children: problem }) : null, _jsx("iframe", { ref: frame, title: component.description || component.name, sandbox: CUSTOM_FRAME_SANDBOX, srcDoc: srcDoc, referrerPolicy: "no-referrer", style: {
|
|
117
|
+
return (_jsxs(BlockFrame, { label: component.description || component.name, weight: props.weight, testId: `custom-${component.name}`, children: [problem ? _jsx(Problem, { children: problem }) : null, !problem && folder && !source ? (_jsx(Problem, { children: `Its module “${component.source}” is a file of its application's folder: it is drawn once the server running the application serves its package (loop apps package, installed beside that server).` })) : null, _jsx("iframe", { ref: frame, title: component.description || component.name, sandbox: CUSTOM_FRAME_SANDBOX, srcDoc: srcDoc, referrerPolicy: "no-referrer", style: {
|
|
72
118
|
width: '100%',
|
|
73
119
|
height: `${component.height}px`,
|
|
74
120
|
border: 0,
|
|
@@ -80,14 +126,14 @@ export function CustomComponentView({ component, props, fetcher, }) {
|
|
|
80
126
|
* schema from its declaration, its view by A2UI's own factory, drawn in its
|
|
81
127
|
* frame.
|
|
82
128
|
*/
|
|
83
|
-
export function customImplementation(component, version = '0.0.0', fetcher) {
|
|
129
|
+
export function customImplementation(component, version = '0.0.0', fetcher, appId = '') {
|
|
84
130
|
const entry = customComponentEntry(component, version);
|
|
85
131
|
const api = { name: component.name, schema: componentSchemaOf(entry) };
|
|
86
|
-
const Render = ({ props }) => (_jsx(CustomComponentView, { component: component, props: props, fetcher: fetcher }));
|
|
132
|
+
const Render = ({ props }) => (_jsx(CustomComponentView, { component: component, props: props, fetcher: fetcher, appId: appId }));
|
|
87
133
|
return createComponentImplementation(api, Render);
|
|
88
134
|
}
|
|
89
135
|
/** The renderers of the components an application's developer wrote, for its page alone. */
|
|
90
136
|
export function customImplementations(app, fetcher) {
|
|
91
|
-
return (app.interface.customComponents ?? []).map(component => customImplementation(component, app.version, fetcher));
|
|
137
|
+
return (app.interface.customComponents ?? []).map(component => customImplementation(component, app.version, fetcher, app.id));
|
|
92
138
|
}
|
|
93
139
|
export { CUSTOM_FRAME_SANDBOX, customFrameDocument, customFramePolicy, customModule, fromFrame, integrityHolds, } from './sandbox';
|
|
@@ -269,7 +269,7 @@ export const APPSPEC_SCHEMA = {
|
|
|
269
269
|
type: 'array',
|
|
270
270
|
},
|
|
271
271
|
source: {
|
|
272
|
-
description: "
|
|
272
|
+
description: "Its module, a built ES module whose default export draws it (`export default function (root, {props, send})`): an address over `https://`, or `http://localhost` while it is written; or a file of the application's folder, by its path in it (`gauge.js`), packaged with the application and served by the server its package is installed beside (LOOP P-29)",
|
|
273
273
|
title: 'Source',
|
|
274
274
|
type: 'string',
|
|
275
275
|
},
|