@dxos/plugin-script 0.6.10-main.bbdfaa4 → 0.6.10-staging.00555f6
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 +45 -0
- package/dist/lib/browser/ScriptEditor-WNCGR5OU.mjs +452 -0
- package/dist/lib/browser/ScriptEditor-WNCGR5OU.mjs.map +7 -0
- package/dist/lib/browser/{TypescriptEditor-PIRIDDUS.mjs → TypescriptEditor-XTGRVCMY.mjs} +2 -2
- package/dist/lib/browser/{chunk-ISMNAD3P.mjs → chunk-DL5XQRQM.mjs} +13 -7
- package/dist/lib/browser/{chunk-ISMNAD3P.mjs.map → chunk-DL5XQRQM.mjs.map} +3 -3
- package/dist/lib/browser/{chunk-D4O723CR.mjs → chunk-DX5CSOBP.mjs} +22 -5
- package/dist/lib/browser/chunk-DX5CSOBP.mjs.map +7 -0
- package/dist/lib/browser/chunk-RHEZ5LUU.mjs +229 -0
- package/dist/lib/browser/chunk-RHEZ5LUU.mjs.map +7 -0
- package/dist/lib/browser/edge/index.mjs +1 -1
- package/dist/lib/browser/index.mjs +88 -195
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/ScriptEditor-RR7AVOQL.cjs +465 -0
- package/dist/lib/node/ScriptEditor-RR7AVOQL.cjs.map +7 -0
- package/dist/lib/node/{TypescriptEditor-OTWJUIWB.cjs → TypescriptEditor-CRONCQSI.cjs} +7 -7
- package/dist/lib/node/{TypescriptEditor-OTWJUIWB.cjs.map → TypescriptEditor-CRONCQSI.cjs.map} +2 -2
- package/dist/lib/node/chunk-4GTXIELQ.cjs +252 -0
- package/dist/lib/node/chunk-4GTXIELQ.cjs.map +7 -0
- package/dist/lib/node/{chunk-HN3AVLUL.cjs → chunk-PFH3ROEH.cjs} +16 -10
- package/dist/lib/node/{chunk-HN3AVLUL.cjs.map → chunk-PFH3ROEH.cjs.map} +3 -3
- package/dist/lib/node/{chunk-ITLHJMWP.cjs → chunk-RXI576K3.cjs} +24 -7
- package/dist/lib/node/chunk-RXI576K3.cjs.map +7 -0
- package/dist/lib/node/edge/index.cjs +8 -8
- package/dist/lib/node/edge/index.cjs.map +1 -1
- package/dist/lib/node/index.cjs +85 -194
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/ScriptPlugin.d.ts +1 -4
- package/dist/types/src/ScriptPlugin.d.ts.map +1 -1
- package/dist/types/src/bundler/bundler.d.ts +39 -0
- package/dist/types/src/bundler/bundler.d.ts.map +1 -0
- package/dist/types/src/bundler/bundler.test.d.ts +2 -0
- package/dist/types/src/bundler/bundler.test.d.ts.map +1 -0
- package/dist/types/src/bundler/index.d.ts +2 -0
- package/dist/types/src/bundler/index.d.ts.map +1 -0
- package/dist/types/src/compiler/compiler.browser.test.d.ts +2 -0
- package/dist/types/src/compiler/compiler.browser.test.d.ts.map +1 -0
- package/dist/types/src/compiler/compiler.d.ts +9 -34
- package/dist/types/src/compiler/compiler.d.ts.map +1 -1
- package/dist/types/src/components/DetailsPanel.d.ts +9 -0
- package/dist/types/src/components/DetailsPanel.d.ts.map +1 -0
- package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts +2 -2
- package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts.map +1 -1
- package/dist/types/src/components/ScriptBlock/ScriptBlock.d.ts.map +1 -1
- package/dist/types/src/components/ScriptEditor.d.ts +3 -2
- package/dist/types/src/components/ScriptEditor.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar.d.ts +9 -7
- package/dist/types/src/components/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar.stories.d.ts +1 -1
- package/dist/types/src/components/TypescriptEditor.d.ts +3 -1
- package/dist/types/src/components/TypescriptEditor.d.ts.map +1 -1
- package/dist/types/src/components/TypescriptEditor.stories.d.ts +1 -1
- package/dist/types/src/components/index.d.ts +2 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/data/templates.md.d.ts +3 -0
- package/dist/types/src/data/templates.md.d.ts.map +1 -0
- package/dist/types/src/edge/functions.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +3 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +38 -33
- package/src/ScriptPlugin.tsx +29 -10
- package/src/bundler/bundler.test.ts +44 -0
- package/src/bundler/bundler.ts +246 -0
- package/src/bundler/index.ts +5 -0
- package/src/compiler/compiler.browser.test.ts +18 -0
- package/src/compiler/compiler.ts +31 -190
- package/src/compiler/index.ts +1 -1
- package/src/components/DetailsPanel.tsx +218 -0
- package/src/components/ScriptBlock/FrameContainer/FrameContainer.tsx +2 -2
- package/src/components/ScriptBlock/ScriptBlock.tsx +10 -7
- package/src/components/ScriptEditor.tsx +71 -13
- package/src/components/Toolbar.tsx +69 -91
- package/src/components/TypescriptEditor.tsx +18 -4
- package/src/data/templates.md.ts +44 -0
- package/src/edge/functions.ts +9 -2
- package/src/index.ts +1 -0
- package/src/translations.ts +4 -3
- package/dist/lib/browser/ScriptEditor-MSQDR6D7.mjs +0 -241
- package/dist/lib/browser/ScriptEditor-MSQDR6D7.mjs.map +0 -7
- package/dist/lib/browser/chunk-D4O723CR.mjs.map +0 -7
- package/dist/lib/node/ScriptEditor-KBSACFYK.cjs +0 -258
- package/dist/lib/node/ScriptEditor-KBSACFYK.cjs.map +0 -7
- package/dist/lib/node/chunk-ITLHJMWP.cjs.map +0 -7
- package/dist/types/src/compiler/compiler.test.d.ts +0 -2
- package/dist/types/src/compiler/compiler.test.d.ts.map +0 -1
- package/src/compiler/compiler.test.ts +0 -17
- /package/dist/lib/browser/{TypescriptEditor-PIRIDDUS.mjs.map → TypescriptEditor-XTGRVCMY.mjs.map} +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { log } from '@dxos/log';
|
|
8
|
+
import {
|
|
9
|
+
Avatar,
|
|
10
|
+
DensityProvider,
|
|
11
|
+
Icon,
|
|
12
|
+
Input,
|
|
13
|
+
type ThemedClassName,
|
|
14
|
+
Toolbar,
|
|
15
|
+
useControlledValue,
|
|
16
|
+
useDynamicRef,
|
|
17
|
+
useTranslation,
|
|
18
|
+
} from '@dxos/react-ui';
|
|
19
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
20
|
+
|
|
21
|
+
import { SCRIPT_PLUGIN } from '../meta';
|
|
22
|
+
|
|
23
|
+
type State = 'pending' | 'responding';
|
|
24
|
+
|
|
25
|
+
type Item = { type: 'request' | 'response'; text: string };
|
|
26
|
+
|
|
27
|
+
export type DetailsPanelProps = ThemedClassName<{
|
|
28
|
+
functionUrl?: string;
|
|
29
|
+
binding?: string;
|
|
30
|
+
onBindingChange?: (binding: string) => void;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export const DetailsPanel = ({ classNames, functionUrl, binding: _binding, onBindingChange }: DetailsPanelProps) => {
|
|
34
|
+
const { t } = useTranslation(SCRIPT_PLUGIN);
|
|
35
|
+
|
|
36
|
+
const bindingInputRef = useRef<HTMLInputElement>(null);
|
|
37
|
+
const [binding, setBinding] = useControlledValue(_binding ?? '');
|
|
38
|
+
const handleBlur = () => {
|
|
39
|
+
onBindingChange?.(binding);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
+
const [input, setInput] = useState('');
|
|
44
|
+
const [result, setResult] = useState('');
|
|
45
|
+
const resultRef = useDynamicRef(result);
|
|
46
|
+
const [state, setState] = useState<State | null>(null);
|
|
47
|
+
|
|
48
|
+
// TODO(burdon): Persistent history (non-space ECHO data?)
|
|
49
|
+
const [history, setHistory] = useState<Item[]>([]);
|
|
50
|
+
|
|
51
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
const handleScroll = () => {
|
|
53
|
+
if (scrollerRef.current) {
|
|
54
|
+
scrollerRef.current.scrollTop = scrollerRef.current.scrollHeight;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const controller = useRef<AbortController>();
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
return () => {
|
|
61
|
+
handleStop();
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleStop = () => {
|
|
66
|
+
controller.current?.abort('stop');
|
|
67
|
+
controller.current = undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// TODO(burdon): Halt streaming request?
|
|
71
|
+
const handleClear = () => {
|
|
72
|
+
handleStop();
|
|
73
|
+
setInput('');
|
|
74
|
+
setResult('');
|
|
75
|
+
setHistory([]);
|
|
76
|
+
inputRef.current?.focus();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleDone = () => {
|
|
80
|
+
controller.current = undefined;
|
|
81
|
+
setHistory((history) => [...history, { type: 'response', text: resultRef.current }]);
|
|
82
|
+
setResult('');
|
|
83
|
+
setState(null);
|
|
84
|
+
handleScroll();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleRun = async (input: string) => {
|
|
88
|
+
if (input.trim().length === 0) {
|
|
89
|
+
inputRef.current?.focus();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setHistory((history) => [...history, { type: 'request', text: input }]);
|
|
94
|
+
setInput('');
|
|
95
|
+
setResult('');
|
|
96
|
+
|
|
97
|
+
// Returns a promise that resolves when a value has been received.
|
|
98
|
+
try {
|
|
99
|
+
setState('pending');
|
|
100
|
+
|
|
101
|
+
// Throws DOMException when aborted.
|
|
102
|
+
controller.current = new AbortController();
|
|
103
|
+
const response = await fetch(functionUrl!, {
|
|
104
|
+
signal: controller.current.signal,
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: input,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// TODO(burdon): Error handling.
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
setState(null);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setState('responding');
|
|
116
|
+
const reader = response.body!.getReader();
|
|
117
|
+
const pump = async ({ done, value }: ReadableStreamReadResult<Uint8Array>): Promise<void> => {
|
|
118
|
+
setResult((result) => result + new TextDecoder().decode(value));
|
|
119
|
+
handleScroll();
|
|
120
|
+
if (!controller.current || done) {
|
|
121
|
+
handleDone();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return reader.read().then(pump);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
void reader
|
|
129
|
+
.read()
|
|
130
|
+
.then(pump)
|
|
131
|
+
.catch((err) => {
|
|
132
|
+
if (err.name !== 'AbortError') {
|
|
133
|
+
log.catch(err);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
setState(null);
|
|
138
|
+
if (err !== 'stop') {
|
|
139
|
+
log.catch(err);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<DensityProvider density='fine'>
|
|
146
|
+
<div className={mx('flex flex-col h-full overflow-hidden', classNames)}>
|
|
147
|
+
{/* TODO(burdon): Replace with Thread? */}
|
|
148
|
+
<div ref={scrollerRef} className='flex flex-col gap-6 h-full p-4 overflow-x-hidden overflow-y-auto'>
|
|
149
|
+
<MessageThread state={state} result={result} history={history} />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<Toolbar.Root classNames='p-2'>
|
|
153
|
+
<Input.Root>
|
|
154
|
+
<Input.TextInput
|
|
155
|
+
ref={inputRef}
|
|
156
|
+
autoFocus
|
|
157
|
+
placeholder={t('function request placeholder')}
|
|
158
|
+
value={input}
|
|
159
|
+
onChange={(ev) => setInput(ev.target.value)}
|
|
160
|
+
onKeyDown={(ev) => ev.key === 'Enter' && handleRun(input)}
|
|
161
|
+
/>
|
|
162
|
+
</Input.Root>
|
|
163
|
+
<Input.Root>
|
|
164
|
+
<Input.TextInput
|
|
165
|
+
ref={bindingInputRef}
|
|
166
|
+
classNames='!w-[10rem]'
|
|
167
|
+
placeholder={t('binding placeholder')}
|
|
168
|
+
value={binding}
|
|
169
|
+
onChange={(ev) => setBinding(ev.target.value.toUpperCase())}
|
|
170
|
+
onBlur={handleBlur}
|
|
171
|
+
/>
|
|
172
|
+
</Input.Root>
|
|
173
|
+
<Toolbar.Button onClick={() => handleRun(input)}>
|
|
174
|
+
<Icon icon='ph--play--regular' size={4} />
|
|
175
|
+
</Toolbar.Button>
|
|
176
|
+
<Toolbar.Button onClick={() => (state ? handleStop() : handleClear())}>
|
|
177
|
+
<Icon icon={state ? 'ph--stop--regular' : 'ph--trash--regular'} size={4} />
|
|
178
|
+
</Toolbar.Button>
|
|
179
|
+
</Toolbar.Root>
|
|
180
|
+
</div>
|
|
181
|
+
</DensityProvider>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const RobotAvatar = () => (
|
|
186
|
+
<Avatar.Root size={6} variant='circle'>
|
|
187
|
+
<Avatar.Frame>
|
|
188
|
+
<Avatar.Icon icon='ph--robot--regular' />
|
|
189
|
+
</Avatar.Frame>
|
|
190
|
+
</Avatar.Root>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// TODO(burdon): Replace with thread.
|
|
194
|
+
// TODO(burdon): Delete individual messages.
|
|
195
|
+
// TODO(burdon): Re-run question.
|
|
196
|
+
const MessageThread = ({ state, result, history }: { state: State | null; result: string; history: Item[] }) => {
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
{history.map(({ type, text }, i) => (
|
|
200
|
+
<div key={i} className='grid grid-cols-[3rem_1fr]'>
|
|
201
|
+
<div>{type === 'response' && <RobotAvatar />}</div>
|
|
202
|
+
<div className={mx('flex', type === 'request' && 'justify-end')}>
|
|
203
|
+
<span className={mx(type === 'request' && 'p-1 px-2 rounded-lg bg-hoverSurface')}>{text}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
<div className='grid grid-cols-[3rem_1fr]'>
|
|
208
|
+
<div>
|
|
209
|
+
{(state === 'pending' && <Icon icon='ph--spinner--regular' size={6} classNames='animate-spin-slow' />) ||
|
|
210
|
+
(result.length > 0 && <RobotAvatar />)}
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<span>{result}</span>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
@@ -11,11 +11,11 @@ import { baseSurface, mx } from '@dxos/react-ui-theme';
|
|
|
11
11
|
import { createProtoRpcPeer } from '@dxos/rpc';
|
|
12
12
|
import { createIFramePort } from '@dxos/rpc-tunnel';
|
|
13
13
|
|
|
14
|
-
import { type
|
|
14
|
+
import { type BundlerResult } from '../../../bundler';
|
|
15
15
|
|
|
16
16
|
export type FrameContainerProps = {
|
|
17
17
|
containerUrl: string;
|
|
18
|
-
result:
|
|
18
|
+
result: BundlerResult;
|
|
19
19
|
debug?: boolean;
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Check, Play, Warning } from '@phosphor-icons/react';
|
|
6
6
|
// @ts-ignore
|
|
7
|
-
import
|
|
7
|
+
import wasmUrl from 'esbuild-wasm/esbuild.wasm?url';
|
|
8
8
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
10
|
import { createDocAccessor, DocAccessor } from '@dxos/react-client/echo';
|
|
@@ -13,7 +13,7 @@ import { mx, getSize } from '@dxos/react-ui-theme';
|
|
|
13
13
|
|
|
14
14
|
import { FrameContainer } from './FrameContainer';
|
|
15
15
|
import { Splitter, SplitterSelector, type View } from './Splitter';
|
|
16
|
-
import {
|
|
16
|
+
import { Bundler, type BundlerResult, initializeBundler } from '../../bundler';
|
|
17
17
|
import { type ScriptType } from '../../types';
|
|
18
18
|
import { ScriptEditor } from '../ScriptEditor';
|
|
19
19
|
|
|
@@ -55,11 +55,14 @@ export const ScriptBlock = ({
|
|
|
55
55
|
const [view, setView] = useState<View>(controlledView ?? 'editor');
|
|
56
56
|
useEffect(() => handleSetView(controlledView ?? 'editor'), [controlledView]);
|
|
57
57
|
|
|
58
|
-
const [result, setResult] = useState<
|
|
59
|
-
const
|
|
58
|
+
const [result, setResult] = useState<BundlerResult>();
|
|
59
|
+
const bundler = useMemo(
|
|
60
|
+
() => new Bundler({ platform: 'browser', sandboxedModules: PROVIDED_MODULES, remoteModules: {} }),
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
60
63
|
useEffect(() => {
|
|
61
64
|
// TODO(burdon): Create useCompiler hook (with initialization).
|
|
62
|
-
void
|
|
65
|
+
void initializeBundler({ wasmUrl });
|
|
63
66
|
}, []);
|
|
64
67
|
useEffect(() => {
|
|
65
68
|
// TODO(burdon): Throttle and listen for update.
|
|
@@ -68,7 +71,7 @@ export const ScriptBlock = ({
|
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
const result = await
|
|
74
|
+
const result = await bundler.bundle(DocAccessor.getValue(source));
|
|
72
75
|
setResult(result);
|
|
73
76
|
});
|
|
74
77
|
|
|
@@ -90,7 +93,7 @@ export const ScriptBlock = ({
|
|
|
90
93
|
if (!source) {
|
|
91
94
|
return;
|
|
92
95
|
}
|
|
93
|
-
const result = await
|
|
96
|
+
const result = await bundler.bundle(DocAccessor.getValue(source));
|
|
94
97
|
setResult(result);
|
|
95
98
|
if (auto && view === 'editor') {
|
|
96
99
|
setView('preview');
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type DID } from 'iso-did/types';
|
|
6
|
+
import { format } from 'prettier';
|
|
7
|
+
import prettierPluginEstree from 'prettier/plugins/estree';
|
|
8
|
+
import prettierPluginTypescript from 'prettier/plugins/typescript';
|
|
6
9
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
10
|
|
|
8
11
|
import { log } from '@dxos/log';
|
|
@@ -13,8 +16,10 @@ import { useTranslation } from '@dxos/react-ui';
|
|
|
13
16
|
import { createDataExtensions, listener } from '@dxos/react-ui-editor';
|
|
14
17
|
import { mx } from '@dxos/react-ui-theme';
|
|
15
18
|
|
|
19
|
+
import { DetailsPanel } from './DetailsPanel';
|
|
16
20
|
import { Toolbar } from './Toolbar';
|
|
17
|
-
import { TypescriptEditor } from './TypescriptEditor';
|
|
21
|
+
import { TypescriptEditor, type TypescriptEditorProps } from './TypescriptEditor';
|
|
22
|
+
import { Bundler } from '../bundler';
|
|
18
23
|
import {
|
|
19
24
|
getUserFunctionUrlInMetadata,
|
|
20
25
|
publicKeyToDid,
|
|
@@ -27,9 +32,9 @@ import { FunctionType, type ScriptType } from '../types';
|
|
|
27
32
|
export type ScriptEditorProps = {
|
|
28
33
|
script: ScriptType;
|
|
29
34
|
role?: string;
|
|
30
|
-
}
|
|
35
|
+
} & Pick<TypescriptEditorProps, 'env'>;
|
|
31
36
|
|
|
32
|
-
export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
|
|
37
|
+
export const ScriptEditor = ({ env, script, role }: ScriptEditorProps) => {
|
|
33
38
|
const { t } = useTranslation(SCRIPT_PLUGIN);
|
|
34
39
|
const client = useClient();
|
|
35
40
|
const identity = useIdentity();
|
|
@@ -38,6 +43,7 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
|
|
|
38
43
|
space,
|
|
39
44
|
Filter.schema(FunctionType, (fn) => fn.source === script),
|
|
40
45
|
);
|
|
46
|
+
|
|
41
47
|
const extensions = useMemo(
|
|
42
48
|
() => [
|
|
43
49
|
listener({
|
|
@@ -56,6 +62,8 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
|
|
|
56
62
|
],
|
|
57
63
|
[script, script.source, space, identity],
|
|
58
64
|
);
|
|
65
|
+
|
|
66
|
+
const [showPanel, setShowPanel] = useState(false);
|
|
59
67
|
const initialValue = useMemo(() => script.source?.content, [script.source]);
|
|
60
68
|
const existingFunctionUrl = fn && getUserFunctionUrlInMetadata(getMeta(fn));
|
|
61
69
|
const [error, setError] = useState<string>();
|
|
@@ -67,6 +75,24 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
|
|
|
67
75
|
[fn],
|
|
68
76
|
);
|
|
69
77
|
|
|
78
|
+
const handleFormat = useCallback(async () => {
|
|
79
|
+
if (!script.source) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
script.source.content = await format(script.source.content, {
|
|
85
|
+
parser: 'typescript',
|
|
86
|
+
plugins: [prettierPluginEstree, prettierPluginTypescript],
|
|
87
|
+
semi: true,
|
|
88
|
+
singleQuote: true,
|
|
89
|
+
});
|
|
90
|
+
} catch (err: any) {
|
|
91
|
+
// TODO(wittjosiah): Show error in UI.
|
|
92
|
+
log.catch(err);
|
|
93
|
+
}
|
|
94
|
+
}, [script.source]);
|
|
95
|
+
|
|
70
96
|
const handleDeploy = useCallback(async () => {
|
|
71
97
|
if (!script.source || !identity || !space) {
|
|
72
98
|
return;
|
|
@@ -78,12 +104,22 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
|
|
|
78
104
|
const existingFunctionId = existingFunctionUrl?.split('/').at(-1);
|
|
79
105
|
const ownerDid = (existingFunctionUrl?.split('/').at(-2) as DID) ?? publicKeyToDid(identity.identityKey);
|
|
80
106
|
|
|
107
|
+
const bundler = new Bundler({
|
|
108
|
+
platform: 'browser',
|
|
109
|
+
sandboxedModules: [],
|
|
110
|
+
remoteModules: {},
|
|
111
|
+
});
|
|
112
|
+
const buildResult = await bundler.bundle(script.source.content);
|
|
113
|
+
if (buildResult.error || !buildResult.bundle) {
|
|
114
|
+
throw buildResult.error;
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
const { result, functionId, functionVersionNumber, errorMessage } = await uploadWorkerFunction({
|
|
82
118
|
clientConfig: client.config,
|
|
83
119
|
halo: client.halo,
|
|
84
120
|
ownerDid,
|
|
85
121
|
functionId: existingFunctionId,
|
|
86
|
-
source:
|
|
122
|
+
source: buildResult.bundle,
|
|
87
123
|
});
|
|
88
124
|
if (result !== 'success' || functionId === undefined || functionVersionNumber === undefined) {
|
|
89
125
|
throw new Error(errorMessage);
|
|
@@ -103,25 +139,47 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
|
|
|
103
139
|
}
|
|
104
140
|
}, [fn, existingFunctionUrl, script, script.name, script.source]);
|
|
105
141
|
|
|
142
|
+
const functionUrl = useMemo(() => {
|
|
143
|
+
if (!existingFunctionUrl) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const baseUrl = new URL('functions/', client.config.values.runtime?.services?.edge?.url);
|
|
148
|
+
|
|
149
|
+
// Leading slashes cause the URL to be treated as an absolute path.
|
|
150
|
+
const relativeUrl = existingFunctionUrl.replace(/^\//, '');
|
|
151
|
+
const url = new URL(`./${relativeUrl}`, baseUrl.toString());
|
|
152
|
+
space && url.searchParams.set('spaceId', space.id);
|
|
153
|
+
url.protocol = 'https';
|
|
154
|
+
return url.toString();
|
|
155
|
+
}, [existingFunctionUrl, space]);
|
|
156
|
+
|
|
106
157
|
return (
|
|
107
158
|
<div
|
|
108
159
|
role='none'
|
|
109
160
|
className={mx(role === 'article' && 'row-span-2', 'flex flex-col is-full bs-full overflow-hidden')}
|
|
110
161
|
>
|
|
111
|
-
<Toolbar
|
|
112
|
-
binding={fn?.binding ?? ''}
|
|
113
|
-
onBindingChange={handleBindingChange}
|
|
114
|
-
deployed={Boolean(existingFunctionUrl) && !script.changed}
|
|
115
|
-
onDeploy={handleDeploy}
|
|
116
|
-
functionUrl={existingFunctionUrl}
|
|
117
|
-
error={error}
|
|
118
|
-
/>
|
|
119
162
|
<TypescriptEditor
|
|
120
163
|
id={script.id}
|
|
164
|
+
env={env}
|
|
121
165
|
initialValue={initialValue}
|
|
122
166
|
extensions={extensions}
|
|
123
|
-
className='flex is-full bs-full overflow-hidden'
|
|
167
|
+
className='flex is-full bs-full overflow-hidden border-b border-separator'
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
<Toolbar
|
|
171
|
+
deployed={Boolean(existingFunctionUrl) && !script.changed}
|
|
172
|
+
functionUrl={functionUrl}
|
|
173
|
+
error={error}
|
|
174
|
+
showPanel={showPanel}
|
|
175
|
+
onDeploy={handleDeploy}
|
|
176
|
+
onFormat={handleFormat}
|
|
177
|
+
onTogglePanel={async () => setShowPanel((showDetails) => !showDetails)}
|
|
124
178
|
/>
|
|
179
|
+
|
|
180
|
+
{showPanel && (
|
|
181
|
+
<DetailsPanel functionUrl={functionUrl} binding={fn?.binding} onBindingChange={handleBindingChange} />
|
|
182
|
+
)}
|
|
125
183
|
</div>
|
|
126
184
|
);
|
|
127
185
|
};
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import React, {
|
|
5
|
+
import { Checks, CircleNotch, Link, MagicWand, UploadSimple, WarningCircle } from '@phosphor-icons/react';
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
Button,
|
|
10
9
|
DensityProvider,
|
|
11
10
|
ElevationProvider,
|
|
12
|
-
|
|
11
|
+
Icon,
|
|
12
|
+
type ThemedClassName,
|
|
13
13
|
Toolbar as NaturalToolbar,
|
|
14
14
|
Tooltip,
|
|
15
15
|
useTranslation,
|
|
@@ -18,48 +18,28 @@ import { getSize, mx } from '@dxos/react-ui-theme';
|
|
|
18
18
|
|
|
19
19
|
import { SCRIPT_PLUGIN } from '../meta';
|
|
20
20
|
|
|
21
|
-
export type ToolbarProps = {
|
|
22
|
-
binding: string;
|
|
23
|
-
onBindingChange: (binding: string) => void;
|
|
21
|
+
export type ToolbarProps = ThemedClassName<{
|
|
24
22
|
deployed?: boolean;
|
|
25
|
-
onDeploy?: () => Promise<void>;
|
|
26
|
-
functionUrl?: string;
|
|
27
23
|
error?: string;
|
|
28
|
-
|
|
24
|
+
functionUrl?: string;
|
|
25
|
+
showPanel?: boolean;
|
|
26
|
+
onDeploy?: () => Promise<void>;
|
|
27
|
+
onFormat?: () => Promise<void>;
|
|
28
|
+
onTogglePanel?: () => Promise<void>;
|
|
29
|
+
}>;
|
|
29
30
|
|
|
30
31
|
export const Toolbar = ({
|
|
31
|
-
|
|
32
|
-
onBindingChange,
|
|
32
|
+
classNames,
|
|
33
33
|
deployed,
|
|
34
|
-
onDeploy,
|
|
35
|
-
functionUrl,
|
|
36
34
|
error,
|
|
35
|
+
functionUrl,
|
|
36
|
+
showPanel,
|
|
37
|
+
onDeploy,
|
|
38
|
+
onFormat,
|
|
39
|
+
onTogglePanel,
|
|
37
40
|
}: ToolbarProps) => {
|
|
38
41
|
const { t } = useTranslation(SCRIPT_PLUGIN);
|
|
39
42
|
const [pending, setPending] = useState(false);
|
|
40
|
-
const [editing, setEditing] = useState(false);
|
|
41
|
-
const [binding, setBinding] = useState(_binding);
|
|
42
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (!editing && _binding !== binding) {
|
|
46
|
-
setBinding(_binding);
|
|
47
|
-
}
|
|
48
|
-
}, [_binding]);
|
|
49
|
-
|
|
50
|
-
const handleEdit = () => {
|
|
51
|
-
setEditing(true);
|
|
52
|
-
inputRef.current?.focus();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const handleSave = () => {
|
|
56
|
-
setEditing(false);
|
|
57
|
-
onBindingChange(binding);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const handleCancel = () => {
|
|
61
|
-
setEditing(false);
|
|
62
|
-
};
|
|
63
43
|
|
|
64
44
|
const handleCopyLink = async () => {
|
|
65
45
|
if (!functionUrl) {
|
|
@@ -75,49 +55,45 @@ export const Toolbar = ({
|
|
|
75
55
|
setPending(false);
|
|
76
56
|
};
|
|
77
57
|
|
|
58
|
+
// TODO(burdon): Factor out common toolbar components with sheet/editor?
|
|
78
59
|
return (
|
|
79
60
|
<DensityProvider density='fine'>
|
|
80
61
|
<ElevationProvider elevation='chrome'>
|
|
81
|
-
<NaturalToolbar.Root
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<X className={getSize(4)} />
|
|
99
|
-
</Button>
|
|
100
|
-
<Button variant='ghost' onClick={handleSave}>
|
|
101
|
-
<Check className={getSize(4)} />
|
|
102
|
-
</Button>
|
|
103
|
-
</>
|
|
104
|
-
) : (
|
|
105
|
-
<Tooltip.Root>
|
|
106
|
-
<Tooltip.Trigger asChild>
|
|
107
|
-
<Button variant='ghost' onClick={handleEdit}>
|
|
108
|
-
<PencilSimple className={getSize(4)} />
|
|
109
|
-
</Button>
|
|
110
|
-
</Tooltip.Trigger>
|
|
111
|
-
<Tooltip.Portal>
|
|
112
|
-
<Tooltip.Content>
|
|
113
|
-
<Tooltip.Arrow />
|
|
114
|
-
{t('edit binding label')}
|
|
115
|
-
</Tooltip.Content>
|
|
116
|
-
</Tooltip.Portal>
|
|
117
|
-
</Tooltip.Root>
|
|
118
|
-
)}
|
|
119
|
-
</Input.Root>
|
|
62
|
+
<NaturalToolbar.Root classNames={['p-1', classNames]} style={{ contain: 'layout' }}>
|
|
63
|
+
{onFormat && (
|
|
64
|
+
<Tooltip.Root>
|
|
65
|
+
<Tooltip.Trigger asChild>
|
|
66
|
+
<NaturalToolbar.Button variant='ghost' onClick={onFormat}>
|
|
67
|
+
<MagicWand className={getSize(4)} />
|
|
68
|
+
</NaturalToolbar.Button>
|
|
69
|
+
</Tooltip.Trigger>
|
|
70
|
+
<Tooltip.Portal>
|
|
71
|
+
<Tooltip.Content>
|
|
72
|
+
<Tooltip.Arrow />
|
|
73
|
+
{t('format label')}
|
|
74
|
+
</Tooltip.Content>
|
|
75
|
+
</Tooltip.Portal>
|
|
76
|
+
</Tooltip.Root>
|
|
77
|
+
)}
|
|
78
|
+
|
|
120
79
|
<div role='separator' className='grow' />
|
|
80
|
+
|
|
81
|
+
{functionUrl && (
|
|
82
|
+
<Tooltip.Root>
|
|
83
|
+
<Tooltip.Trigger asChild>
|
|
84
|
+
<NaturalToolbar.Button variant='ghost' onClick={handleCopyLink}>
|
|
85
|
+
<Link className={getSize(4)} />
|
|
86
|
+
</NaturalToolbar.Button>
|
|
87
|
+
</Tooltip.Trigger>
|
|
88
|
+
<Tooltip.Portal>
|
|
89
|
+
<Tooltip.Content>
|
|
90
|
+
<Tooltip.Arrow />
|
|
91
|
+
{t('copy link label')}
|
|
92
|
+
</Tooltip.Content>
|
|
93
|
+
</Tooltip.Portal>
|
|
94
|
+
</Tooltip.Root>
|
|
95
|
+
)}
|
|
96
|
+
|
|
121
97
|
{/* TODO(wittjosiah): Better treatment for status indicators in toolbar? */}
|
|
122
98
|
{error ? (
|
|
123
99
|
<Tooltip.Root>
|
|
@@ -144,21 +120,7 @@ export const Toolbar = ({
|
|
|
144
120
|
</Tooltip.Portal>
|
|
145
121
|
</Tooltip.Root>
|
|
146
122
|
) : null}
|
|
147
|
-
|
|
148
|
-
<Tooltip.Root>
|
|
149
|
-
<Tooltip.Trigger asChild>
|
|
150
|
-
<NaturalToolbar.Button variant='ghost' onClick={handleCopyLink}>
|
|
151
|
-
<Link className={getSize(4)} />
|
|
152
|
-
</NaturalToolbar.Button>
|
|
153
|
-
</Tooltip.Trigger>
|
|
154
|
-
<Tooltip.Portal>
|
|
155
|
-
<Tooltip.Content>
|
|
156
|
-
<Tooltip.Arrow />
|
|
157
|
-
{t('copy link label')}
|
|
158
|
-
</Tooltip.Content>
|
|
159
|
-
</Tooltip.Portal>
|
|
160
|
-
</Tooltip.Root>
|
|
161
|
-
)}
|
|
123
|
+
|
|
162
124
|
<Tooltip.Root>
|
|
163
125
|
<Tooltip.Trigger asChild>
|
|
164
126
|
<NaturalToolbar.Button variant='ghost' onClick={handleDeploy} disabled={pending}>
|
|
@@ -176,6 +138,22 @@ export const Toolbar = ({
|
|
|
176
138
|
</Tooltip.Content>
|
|
177
139
|
</Tooltip.Portal>
|
|
178
140
|
</Tooltip.Root>
|
|
141
|
+
|
|
142
|
+
{onTogglePanel && (
|
|
143
|
+
<Tooltip.Root>
|
|
144
|
+
<Tooltip.Trigger asChild>
|
|
145
|
+
<NaturalToolbar.Button variant='ghost' onClick={onTogglePanel}>
|
|
146
|
+
<Icon icon={showPanel ? 'ph--caret-up--regular' : 'ph--caret-down--regular'} size={4} />
|
|
147
|
+
</NaturalToolbar.Button>
|
|
148
|
+
</Tooltip.Trigger>
|
|
149
|
+
<Tooltip.Portal>
|
|
150
|
+
<Tooltip.Content>
|
|
151
|
+
<Tooltip.Arrow />
|
|
152
|
+
{t('toggle details label')}
|
|
153
|
+
</Tooltip.Content>
|
|
154
|
+
</Tooltip.Portal>
|
|
155
|
+
</Tooltip.Root>
|
|
156
|
+
)}
|
|
179
157
|
</NaturalToolbar.Root>
|
|
180
158
|
</ElevationProvider>
|
|
181
159
|
</DensityProvider>
|