@dxos/plugin-script 0.6.8-main.046e6cf

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.
Files changed (136) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +4 -0
  3. package/dist/lib/browser/ScriptEditor-6Q7HFLJ6.mjs +220 -0
  4. package/dist/lib/browser/ScriptEditor-6Q7HFLJ6.mjs.map +7 -0
  5. package/dist/lib/browser/TypescriptEditor-HP35MTEQ.mjs +9 -0
  6. package/dist/lib/browser/TypescriptEditor-HP35MTEQ.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-2EM5Y66A.mjs +60 -0
  8. package/dist/lib/browser/chunk-2EM5Y66A.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-EZ7PWOLU.mjs +235 -0
  10. package/dist/lib/browser/chunk-EZ7PWOLU.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-JBACGDOJ.mjs +21 -0
  12. package/dist/lib/browser/chunk-JBACGDOJ.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-Z72YKDJ2.mjs +31 -0
  14. package/dist/lib/browser/chunk-Z72YKDJ2.mjs.map +7 -0
  15. package/dist/lib/browser/edge/index.mjs +19 -0
  16. package/dist/lib/browser/edge/index.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +395 -0
  18. package/dist/lib/browser/index.mjs.map +7 -0
  19. package/dist/lib/browser/meta.json +1 -0
  20. package/dist/lib/browser/meta.mjs +9 -0
  21. package/dist/lib/browser/meta.mjs.map +7 -0
  22. package/dist/lib/browser/types/index.mjs +10 -0
  23. package/dist/lib/browser/types/index.mjs.map +7 -0
  24. package/dist/lib/node/ScriptEditor-AIVKL2NX.cjs +239 -0
  25. package/dist/lib/node/ScriptEditor-AIVKL2NX.cjs.map +7 -0
  26. package/dist/lib/node/TypescriptEditor-PWOAN5RQ.cjs +30 -0
  27. package/dist/lib/node/TypescriptEditor-PWOAN5RQ.cjs.map +7 -0
  28. package/dist/lib/node/chunk-4VMNBXWU.cjs +54 -0
  29. package/dist/lib/node/chunk-4VMNBXWU.cjs.map +7 -0
  30. package/dist/lib/node/chunk-6Y2S6SGK.cjs +49 -0
  31. package/dist/lib/node/chunk-6Y2S6SGK.cjs.map +7 -0
  32. package/dist/lib/node/chunk-LWUBFTPA.cjs +253 -0
  33. package/dist/lib/node/chunk-LWUBFTPA.cjs.map +7 -0
  34. package/dist/lib/node/chunk-SLV3SDZE.cjs +93 -0
  35. package/dist/lib/node/chunk-SLV3SDZE.cjs.map +7 -0
  36. package/dist/lib/node/edge/index.cjs +41 -0
  37. package/dist/lib/node/edge/index.cjs.map +7 -0
  38. package/dist/lib/node/index.cjs +417 -0
  39. package/dist/lib/node/index.cjs.map +7 -0
  40. package/dist/lib/node/meta.cjs +30 -0
  41. package/dist/lib/node/meta.cjs.map +7 -0
  42. package/dist/lib/node/meta.json +1 -0
  43. package/dist/lib/node/types/index.cjs +32 -0
  44. package/dist/lib/node/types/index.cjs.map +7 -0
  45. package/dist/types/src/ScriptPlugin.d.ts +7 -0
  46. package/dist/types/src/ScriptPlugin.d.ts.map +1 -0
  47. package/dist/types/src/compiler/compiler.d.ts +38 -0
  48. package/dist/types/src/compiler/compiler.d.ts.map +1 -0
  49. package/dist/types/src/compiler/compiler.test.d.ts +2 -0
  50. package/dist/types/src/compiler/compiler.test.d.ts.map +1 -0
  51. package/dist/types/src/compiler/index.d.ts +2 -0
  52. package/dist/types/src/compiler/index.d.ts.map +1 -0
  53. package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts +12 -0
  54. package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts.map +1 -0
  55. package/dist/types/src/components/ScriptBlock/FrameContainer/frame.d.ts +2 -0
  56. package/dist/types/src/components/ScriptBlock/FrameContainer/frame.d.ts.map +1 -0
  57. package/dist/types/src/components/ScriptBlock/FrameContainer/index.d.ts +2 -0
  58. package/dist/types/src/components/ScriptBlock/FrameContainer/index.d.ts.map +1 -0
  59. package/dist/types/src/components/ScriptBlock/ScriptBlock.d.ts +18 -0
  60. package/dist/types/src/components/ScriptBlock/ScriptBlock.d.ts.map +1 -0
  61. package/dist/types/src/components/ScriptBlock/ScriptBlock.stories.d.ts +14 -0
  62. package/dist/types/src/components/ScriptBlock/ScriptBlock.stories.d.ts.map +1 -0
  63. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.d.ts +15 -0
  64. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.d.ts.map +1 -0
  65. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.stories.d.ts +13 -0
  66. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.stories.d.ts.map +1 -0
  67. package/dist/types/src/components/ScriptBlock/Splitter/index.d.ts +2 -0
  68. package/dist/types/src/components/ScriptBlock/Splitter/index.d.ts.map +1 -0
  69. package/dist/types/src/components/ScriptBlock/index.d.ts +2 -0
  70. package/dist/types/src/components/ScriptBlock/index.d.ts.map +1 -0
  71. package/dist/types/src/components/ScriptEditor.d.ts +9 -0
  72. package/dist/types/src/components/ScriptEditor.d.ts.map +1 -0
  73. package/dist/types/src/components/Toolbar.d.ts +11 -0
  74. package/dist/types/src/components/Toolbar.d.ts.map +1 -0
  75. package/dist/types/src/components/Toolbar.stories.d.ts +16 -0
  76. package/dist/types/src/components/Toolbar.stories.d.ts.map +1 -0
  77. package/dist/types/src/components/TypescriptEditor.d.ts +10 -0
  78. package/dist/types/src/components/TypescriptEditor.d.ts.map +1 -0
  79. package/dist/types/src/components/TypescriptEditor.stories.d.ts +13 -0
  80. package/dist/types/src/components/TypescriptEditor.stories.d.ts.map +1 -0
  81. package/dist/types/src/components/index.d.ts +4 -0
  82. package/dist/types/src/components/index.d.ts.map +1 -0
  83. package/dist/types/src/edge/codec.d.ts +4 -0
  84. package/dist/types/src/edge/codec.d.ts.map +1 -0
  85. package/dist/types/src/edge/functions.d.ts +23 -0
  86. package/dist/types/src/edge/functions.d.ts.map +1 -0
  87. package/dist/types/src/edge/header-codec.d.ts +24 -0
  88. package/dist/types/src/edge/header-codec.d.ts.map +1 -0
  89. package/dist/types/src/edge/hub-protocol.d.ts +6 -0
  90. package/dist/types/src/edge/hub-protocol.d.ts.map +1 -0
  91. package/dist/types/src/edge/index.d.ts +5 -0
  92. package/dist/types/src/edge/index.d.ts.map +1 -0
  93. package/dist/types/src/index.d.ts +7 -0
  94. package/dist/types/src/index.d.ts.map +1 -0
  95. package/dist/types/src/meta.d.ts +15 -0
  96. package/dist/types/src/meta.d.ts.map +1 -0
  97. package/dist/types/src/translations.d.ts +19 -0
  98. package/dist/types/src/translations.d.ts.map +1 -0
  99. package/dist/types/src/types/index.d.ts +3 -0
  100. package/dist/types/src/types/index.d.ts.map +1 -0
  101. package/dist/types/src/types/script.d.ts +26 -0
  102. package/dist/types/src/types/script.d.ts.map +1 -0
  103. package/dist/types/src/types/types.d.ts +8 -0
  104. package/dist/types/src/types/types.d.ts.map +1 -0
  105. package/package.json +120 -0
  106. package/src/ScriptPlugin.tsx +137 -0
  107. package/src/compiler/compiler.test.ts +17 -0
  108. package/src/compiler/compiler.ts +210 -0
  109. package/src/compiler/index.ts +5 -0
  110. package/src/components/ScriptBlock/FrameContainer/FrameContainer.tsx +82 -0
  111. package/src/components/ScriptBlock/FrameContainer/frame.html +25 -0
  112. package/src/components/ScriptBlock/FrameContainer/frame.tsx +51 -0
  113. package/src/components/ScriptBlock/FrameContainer/index.ts +5 -0
  114. package/src/components/ScriptBlock/ScriptBlock.stories.tsx +64 -0
  115. package/src/components/ScriptBlock/ScriptBlock.tsx +132 -0
  116. package/src/components/ScriptBlock/Splitter/Splitter.stories.tsx +37 -0
  117. package/src/components/ScriptBlock/Splitter/Splitter.tsx +52 -0
  118. package/src/components/ScriptBlock/Splitter/index.ts +5 -0
  119. package/src/components/ScriptBlock/index.ts +5 -0
  120. package/src/components/ScriptEditor.tsx +113 -0
  121. package/src/components/Toolbar.stories.tsx +19 -0
  122. package/src/components/Toolbar.tsx +163 -0
  123. package/src/components/TypescriptEditor.stories.tsx +63 -0
  124. package/src/components/TypescriptEditor.tsx +66 -0
  125. package/src/components/index.ts +8 -0
  126. package/src/edge/codec.ts +13 -0
  127. package/src/edge/functions.ts +128 -0
  128. package/src/edge/header-codec.ts +97 -0
  129. package/src/edge/hub-protocol.ts +27 -0
  130. package/src/edge/index.ts +10 -0
  131. package/src/index.ts +13 -0
  132. package/src/meta.tsx +19 -0
  133. package/src/translations.ts +25 -0
  134. package/src/types/index.ts +6 -0
  135. package/src/types/script.ts +16 -0
  136. package/src/types/types.tsx +27 -0
@@ -0,0 +1,137 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Code, type IconProps } from '@phosphor-icons/react';
6
+ import React from 'react';
7
+
8
+ import { parseIntentPlugin, type PluginDefinition, resolvePlugin, NavigationAction } from '@dxos/app-framework';
9
+ import { create } from '@dxos/echo-schema';
10
+ import { parseClientPlugin } from '@dxos/plugin-client';
11
+ import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
12
+ import { TextType } from '@dxos/plugin-markdown/types';
13
+ import { SpaceAction } from '@dxos/plugin-space';
14
+ import { loadObjectReferences } from '@dxos/react-client/echo';
15
+
16
+ import { ScriptEditor } from './components';
17
+ import meta, { SCRIPT_PLUGIN } from './meta';
18
+ import translations from './translations';
19
+ import { ScriptType } from './types';
20
+ import { ScriptAction, type ScriptPluginProvides } from './types';
21
+
22
+ export type ScriptPluginProps = {
23
+ containerUrl: string;
24
+ };
25
+
26
+ export const ScriptPlugin = ({ containerUrl }: ScriptPluginProps): PluginDefinition<ScriptPluginProvides> => {
27
+ return {
28
+ meta,
29
+ provides: {
30
+ metadata: {
31
+ records: {
32
+ [ScriptType.typename]: {
33
+ placeholder: ['object title placeholder', { ns: SCRIPT_PLUGIN }],
34
+ icon: (props: IconProps) => <Code {...props} />,
35
+ iconSymbol: 'ph--code--regular',
36
+ // TODO(wittjosiah): Move out of metadata.
37
+ loadReferences: (script: ScriptType) => loadObjectReferences(script, (script) => [script.source]),
38
+ },
39
+ },
40
+ },
41
+ translations,
42
+ echo: {
43
+ schema: [ScriptType],
44
+ },
45
+ graph: {
46
+ builder: (plugins) => {
47
+ const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
48
+ const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatch;
49
+ if (!client || !dispatch) {
50
+ return [];
51
+ }
52
+
53
+ return createExtension({
54
+ id: ScriptAction.CREATE,
55
+ filter: (node): node is ActionGroup => isActionGroup(node) && node.id.startsWith(SpaceAction.ADD_OBJECT),
56
+ actions: ({ node }) => {
57
+ const id = node.id.split('/').at(-1);
58
+ const [spaceId, objectId] = id?.split(':') ?? [];
59
+ const space = client.spaces.get().find((space) => space.id === spaceId);
60
+ const object = objectId && space?.db.getObjectById(objectId);
61
+ const target = objectId ? object : space;
62
+ if (!target) {
63
+ return;
64
+ }
65
+
66
+ return [
67
+ {
68
+ id: `${SCRIPT_PLUGIN}/create/${node.id}`,
69
+ data: async () => {
70
+ await dispatch([
71
+ { plugin: SCRIPT_PLUGIN, action: ScriptAction.CREATE },
72
+ { action: SpaceAction.ADD_OBJECT, data: { target } },
73
+ { action: NavigationAction.OPEN },
74
+ ]);
75
+ },
76
+ properties: {
77
+ label: ['create object label', { ns: SCRIPT_PLUGIN }],
78
+ icon: (props: IconProps) => <Code {...props} />,
79
+ iconSymbol: 'ph--code--regular',
80
+ testId: 'scriptPlugin.createObject',
81
+ },
82
+ },
83
+ ];
84
+ },
85
+ });
86
+ },
87
+ },
88
+ stack: {
89
+ creators: [
90
+ {
91
+ id: 'create-stack-section-script',
92
+ testId: 'scriptPlugin.createSectionSpaceScript',
93
+ type: ['plugin name', { ns: SCRIPT_PLUGIN }],
94
+ label: ['create stack section label', { ns: SCRIPT_PLUGIN }],
95
+ icon: (props: any) => <Code {...props} />,
96
+ intent: {
97
+ plugin: SCRIPT_PLUGIN,
98
+ action: ScriptAction.CREATE,
99
+ },
100
+ },
101
+ ],
102
+ },
103
+ surface: {
104
+ component: ({ data, role }) => {
105
+ if (role && !['article', 'section'].includes(role)) {
106
+ return null;
107
+ }
108
+
109
+ if (data.object instanceof ScriptType) {
110
+ return <ScriptEditor script={data.object} role={role} />;
111
+ }
112
+
113
+ return null;
114
+ },
115
+ },
116
+ intent: {
117
+ resolver: (intent) => {
118
+ switch (intent.action) {
119
+ case ScriptAction.CREATE: {
120
+ return { data: create(ScriptType, { source: create(TextType, { content: example }) }) };
121
+ }
122
+ }
123
+ },
124
+ },
125
+ },
126
+ };
127
+ };
128
+
129
+ // TODO(burdon): Import.
130
+ const example = [
131
+ 'export default {',
132
+ ' async fetch(request, env, ctx) {',
133
+ " return new Response('Hello World, from ScriptPlugin!');",
134
+ ' }',
135
+ '}',
136
+ '',
137
+ ].join('\n');
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { describe, test } from '@dxos/test';
8
+
9
+ import { Compiler } from './compiler';
10
+
11
+ describe('Compiler', () => {
12
+ test('Basic', async () => {
13
+ const compiler = new Compiler({ platform: 'node', providedModules: [] });
14
+ const result = await compiler.compile('const x = 100'); // TODO(burdon): Test import.
15
+ expect(result).to.exist;
16
+ });
17
+ });
@@ -0,0 +1,210 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type BuildOptions } from 'esbuild';
6
+ import { build, initialize, type BuildResult } from 'esbuild-wasm';
7
+
8
+ import { subtleCrypto } from '@dxos/crypto';
9
+ import { invariant } from '@dxos/invariant';
10
+
11
+ export type Import = {
12
+ moduleUrl: string;
13
+ defaultImport: boolean;
14
+ namedImports: string[];
15
+ };
16
+
17
+ export type CompilerResult = {
18
+ timestamp: number;
19
+ sourceHash: Buffer;
20
+ imports?: Import[];
21
+ bundle?: string;
22
+ error?: any;
23
+ };
24
+
25
+ export type CompilerOptions = {
26
+ platform: BuildOptions['platform'];
27
+ providedModules: string[];
28
+ };
29
+
30
+ let initialized: Promise<void>;
31
+ export const initializeCompiler = async (options: { wasmURL: string }) => {
32
+ await (initialized ??= initialize({
33
+ wasmURL: options.wasmURL,
34
+ }));
35
+ };
36
+
37
+ /**
38
+ * ESBuild compiler.
39
+ */
40
+ export class Compiler {
41
+ constructor(private readonly _options: CompilerOptions) {}
42
+
43
+ async compile(source: string): Promise<CompilerResult> {
44
+ const { providedModules, ...options } = this._options;
45
+
46
+ const createResult = async (result?: Partial<CompilerResult>) => {
47
+ return {
48
+ timestamp: Date.now(),
49
+ sourceHash: Buffer.from(await subtleCrypto.digest('SHA-256', Buffer.from(source))),
50
+ ...result,
51
+ };
52
+ };
53
+
54
+ if (this._options.platform === 'browser') {
55
+ invariant(initialized, 'Compiler not initialized.');
56
+ await initialized;
57
+ }
58
+
59
+ const imports = analyzeSourceFileImports(source);
60
+
61
+ // https://esbuild.github.io/api/#build
62
+ try {
63
+ const result = await build({
64
+ ...options,
65
+ metafile: true,
66
+ write: false,
67
+ entryPoints: ['memory:main.tsx'],
68
+ bundle: true,
69
+ format: 'esm',
70
+ plugins: [
71
+ {
72
+ name: 'memory',
73
+ setup: (build) => {
74
+ build.onResolve({ filter: /^memory:/ }, ({ path }) => {
75
+ return { path: path.split(':')[1], namespace: 'memory' };
76
+ });
77
+
78
+ build.onLoad({ filter: /.*/, namespace: 'memory' }, ({ path }) => {
79
+ const imports = ["import React from 'react';"];
80
+ if (path === 'main.tsx') {
81
+ return {
82
+ contents: [...imports, source].join('\n'),
83
+ loader: 'tsx',
84
+ };
85
+ }
86
+ });
87
+
88
+ for (const module of providedModules) {
89
+ build.onResolve({ filter: new RegExp(`^${module}$`) }, ({ path }) => {
90
+ return { path, namespace: 'injected-module' };
91
+ });
92
+ }
93
+
94
+ build.onLoad({ filter: /.*/, namespace: 'injected-module' }, ({ path }) => {
95
+ const namedImports = imports.find((entry) => entry.moduleIdentifier === path)?.namedImports ?? [];
96
+ return {
97
+ contents: `
98
+ const { ${namedImports.join(',')} } = window.__DXOS_SANDBOX_MODULES__[${JSON.stringify(path)}];
99
+ export { ${namedImports.join(',')} };
100
+ export default window.__DXOS_SANDBOX_MODULES__[${JSON.stringify(path)}].default;
101
+ `,
102
+ loader: 'tsx',
103
+ };
104
+ });
105
+ },
106
+ },
107
+ ],
108
+ });
109
+
110
+ return await createResult({
111
+ imports: this.analyzeImports(result),
112
+ bundle: result.outputFiles![0].text,
113
+ });
114
+ } catch (err) {
115
+ return await createResult({ error: err });
116
+ }
117
+ }
118
+
119
+ // TODO(dmaretskyi): In the future we can replace the compiler with SWC with plugins running in WASM.
120
+ analyzeImports(result: BuildResult): Import[] {
121
+ invariant(result.outputFiles);
122
+
123
+ // TODO(dmaretskyi): Support import aliases and wildcard imports.
124
+ const parsedImports = allMatches(IMPORT_REGEX, result.outputFiles[0].text);
125
+ return Object.values(result.metafile!.outputs)[0].imports.map((entry): Import => {
126
+ const namedImports: string[] = [];
127
+
128
+ const parsedImport = parsedImports.find((capture) => capture?.[4] === entry.path);
129
+ if (parsedImport?.[2]) {
130
+ NAMED_IMPORTS_REGEX.lastIndex = 0;
131
+ const namedImportsMatch = NAMED_IMPORTS_REGEX.exec(parsedImport[2]);
132
+ if (namedImportsMatch) {
133
+ namedImportsMatch[1].split(',').forEach((importName) => {
134
+ namedImports.push(importName.trim());
135
+ });
136
+ }
137
+ }
138
+
139
+ return {
140
+ moduleUrl: entry.path,
141
+ defaultImport: !!parsedImport?.[1],
142
+ namedImports,
143
+ };
144
+ });
145
+ }
146
+
147
+ analyzeSourceFileImports(code: string) {
148
+ // TODO(dmaretskyi): Support import aliases and wildcard imports.
149
+ const parsedImports = allMatches(IMPORT_REGEX, code);
150
+ return parsedImports.map((capture) => {
151
+ return {
152
+ defaultImportName: capture[1],
153
+ namedImports: capture[2]?.split(',').map((importName) => importName.trim()),
154
+ wildcardImportName: capture[3],
155
+ moduleIdentifier: capture[4],
156
+ quotes: capture[5],
157
+ };
158
+ });
159
+ }
160
+ }
161
+
162
+ // https://regex101.com/r/FEN5ks/1
163
+ // https://stackoverflow.com/a/73265022
164
+ // $1 = default import name (can be non-existent)
165
+ // $2 = destructured exports (can be non-existent)
166
+ // $3 = wildcard import name (can be non-existent)
167
+ // $4 = module identifier
168
+ // $5 = quotes used (either ' or ")
169
+ const IMPORT_REGEX =
170
+ /import(?:(?:(?:[ \n\t]+([^ *\n\t{},]+)[ \n\t]*(?:,|[ \n\t]+))?([ \n\t]*{(?:[ \n\t]*[^ \n\t"'{}]+[ \n\t]*,?)+})?[ \n\t]*)|[ \n\t]*\*[ \n\t]*as[ \n\t]+([^ \n\t{}]+)[ \n\t]+)from[ \n\t]*(?:['"])([^'"\n]+)(['"])/gm;
171
+
172
+ const NAMED_IMPORTS_REGEX = /[ \n\t]*{((?:[ \n\t]*[^ \n\t"'{}]+[ \n\t]*,?)+)}[ \n\t]*/gm;
173
+
174
+ const allMatches = (regex: RegExp, str: string) => {
175
+ regex.lastIndex = 0;
176
+
177
+ let match;
178
+ const matches = [];
179
+ while ((match = regex.exec(str))) {
180
+ matches.push(match);
181
+ }
182
+
183
+ return matches;
184
+ };
185
+
186
+ type ParsedImport = {
187
+ defaultImportName?: string;
188
+ namedImports: string[];
189
+ wildcardImportName?: string;
190
+ moduleIdentifier: string;
191
+ quotes: string;
192
+ };
193
+
194
+ const analyzeSourceFileImports = (code: string): ParsedImport[] => {
195
+ // TODO(dmaretskyi): Support import aliases and wildcard imports.
196
+ const parsedImports = allMatches(IMPORT_REGEX, code);
197
+ return parsedImports.map((capture) => {
198
+ return {
199
+ defaultImportName: capture[1],
200
+ namedImports: capture[2]
201
+ ?.trim()
202
+ .slice(1, -1)
203
+ .split(',')
204
+ .map((importName) => importName.trim()),
205
+ wildcardImportName: capture[3],
206
+ moduleIdentifier: capture[4],
207
+ quotes: capture[5],
208
+ };
209
+ });
210
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './compiler';
@@ -0,0 +1,82 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useRef } from 'react';
6
+
7
+ import { clientServiceBundle } from '@dxos/client-protocol';
8
+ import { log } from '@dxos/log';
9
+ import { useClient } from '@dxos/react-client';
10
+ import { baseSurface, mx } from '@dxos/react-ui-theme';
11
+ import { createProtoRpcPeer } from '@dxos/rpc';
12
+ import { createIFramePort } from '@dxos/rpc-tunnel';
13
+
14
+ import { type CompilerResult } from '../../../compiler';
15
+
16
+ export type FrameContainerProps = {
17
+ containerUrl: string;
18
+ result: CompilerResult;
19
+ debug?: boolean;
20
+ };
21
+
22
+ /**
23
+ * IFrame container for the compiled script.
24
+ */
25
+ export const FrameContainer = ({ containerUrl, result, debug = true }: FrameContainerProps) => {
26
+ const client = useClient();
27
+ const iframeRef = useRef<HTMLIFrameElement>(null);
28
+ useEffect(() => {
29
+ if (iframeRef.current) {
30
+ // Connect iframe to client.
31
+ const rpc = createProtoRpcPeer({
32
+ exposed: clientServiceBundle,
33
+ handlers: (client as any)._services.services, // TODO(burdon): Remove cast?
34
+ port: createIFramePort({
35
+ channel: 'frame',
36
+ iframe: iframeRef.current,
37
+ origin: '*',
38
+ }),
39
+ });
40
+
41
+ rpc.open().catch((e) => log.catch(e));
42
+ return () => {
43
+ rpc.close().catch((e) => log.catch(e));
44
+ };
45
+ }
46
+ }, [iframeRef]);
47
+
48
+ // Encodes compiled code via URL.
49
+ const sourceHash = Buffer.from(result.sourceHash).toString('hex');
50
+ const src = result.bundle && `${containerUrl}?ts=${sourceHash}#code=${encodeURIComponent(result.bundle)}`;
51
+
52
+ return (
53
+ <>
54
+ <iframe ref={iframeRef} sandbox='allow-scripts' src={src} style={{ width: '100%', height: '100%' }} />
55
+
56
+ {debug && (
57
+ <div className='relative'>
58
+ <div
59
+ className={mx(
60
+ baseSurface,
61
+ 'flex absolute right-2 bottom-2 w-[400px] h-[200px] ring rounded',
62
+ 'z-[100] overflow-x-hidden overflow-y-auto',
63
+ )}
64
+ >
65
+ <pre className='text-xs whitespace-break-spaces break-all p-2'>
66
+ {JSON.stringify(
67
+ {
68
+ timestamp: result.timestamp,
69
+ sourceHash,
70
+ error: result.error,
71
+ src,
72
+ },
73
+ undefined,
74
+ 2,
75
+ )}
76
+ </pre>
77
+ </div>
78
+ </div>
79
+ )}
80
+ </>
81
+ );
82
+ };
@@ -0,0 +1,25 @@
1
+ <html>
2
+ <head>
3
+ <script src="https://cdn.tailwindcss.com"></script>
4
+ <script type="importmap">
5
+ __IMPORT_MAP__
6
+ </script>
7
+
8
+ <script type="module">
9
+ import RefreshRuntime from '/@react-refresh';
10
+ RefreshRuntime.injectIntoGlobalHook(window);
11
+ window.$RefreshReg$ = () => {};
12
+ window.$RefreshSig$ = () => (type) => type;
13
+ window.__vite_plugin_react_preamble_installed__ = true;
14
+ </script>
15
+ <script type="module" src="/@vite/client"></script>
16
+ </head>
17
+ <body>
18
+ <div id="root"></div>
19
+
20
+ <!-- Prevent vite from compiling this. -->
21
+ <script type="module">
22
+ eval('import("@frame/main")');
23
+ </script>
24
+ </body>
25
+ </html>
@@ -0,0 +1,51 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+
8
+ import { ClientProvider } from '@dxos/react-client';
9
+
10
+ const init = async (f: () => Promise<Record<string, any>>) =>
11
+ Object.entries(await f()).reduce<Record<string, any>>((map, [key, module]) => {
12
+ map[key] = module;
13
+ return map;
14
+ }, {});
15
+
16
+ const main = async () => {
17
+ const { ClientServicesProxy } = await import('@dxos/react-client');
18
+ const { createIFramePort } = await import('@dxos/rpc-tunnel');
19
+
20
+ // @ts-ignore
21
+ window.__DXOS_SANDBOX_MODULES__ = await init(async () => ({
22
+ // prettier-ignore
23
+ 'react': await import('react'),
24
+ 'react-dom/client': await import('react-dom/client'),
25
+ '@dxos/client': await import('@dxos/client'),
26
+ '@dxos/react-client': await import('@dxos/react-client'),
27
+ '@dxos/react-client/echo': await import('@dxos/react-client/echo'),
28
+ '@dxos/plugin-explorer': await import('@dxos/plugin-explorer'),
29
+ }));
30
+
31
+ // eslint-disable-next-line no-new-func
32
+ const Component = Function('React', "return React.lazy(() => import('@frame/bundle'))")(React);
33
+
34
+ const services = new ClientServicesProxy(
35
+ createIFramePort({
36
+ channel: 'frame',
37
+ origin: '*',
38
+ }),
39
+ );
40
+
41
+ // TODO(burdon): Error handling.
42
+ createRoot(document.getElementById('root')!).render(
43
+ <div className='flex fixed inset-0'>
44
+ <ClientProvider services={() => services}>
45
+ <Component />
46
+ </ClientProvider>
47
+ </div>,
48
+ );
49
+ };
50
+
51
+ void main();
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './FrameContainer';
@@ -0,0 +1,64 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React, { useEffect, useMemo } from 'react';
8
+
9
+ import { createSpaceObjectGenerator, TestSchemaType } from '@dxos/echo-generator';
10
+ import { TextType } from '@dxos/plugin-markdown/types';
11
+ import { useClient } from '@dxos/react-client';
12
+ import { create, createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
13
+ import { ClientRepeater } from '@dxos/react-client/testing';
14
+ import { withTheme } from '@dxos/storybook-utils';
15
+
16
+ // @ts-ignore
17
+ import mainUrl from './FrameContainer/frame?url';
18
+ import { ScriptBlock } from './ScriptBlock';
19
+ import { ScriptType } from '../../types';
20
+
21
+ const code = [
22
+ "import { Filter, useQuery, useSpaces} from '@dxos/react-client/echo';",
23
+ "import { Chart, Explorer, Globe } from '@dxos/plugin-explorer';",
24
+ '',
25
+ 'export default () => {',
26
+ ' const spaces = useSpaces();',
27
+ ' const space = spaces[1]',
28
+ " const objects = useQuery(space, Filter.typename('example.com/type/contact'));",
29
+ ' return <Chart items={objects} accessor={object => ({ x: object.lat, y: object.lng })} />',
30
+ '}',
31
+ ].join('\n');
32
+
33
+ const Story = () => {
34
+ const client = useClient();
35
+ // TODO(dmaretskyi): Review what's the right way to create automerge-backed objects.
36
+ const object = useMemo(
37
+ () => createEchoObject(create(ScriptType, { source: create(TextType, { content: code }) })),
38
+ [code],
39
+ );
40
+ const accessor = useMemo(() => createDocAccessor(object, ['content']), [object]);
41
+ useEffect(() => {
42
+ const generator = createSpaceObjectGenerator(client.spaces.default);
43
+ generator.addSchemas();
44
+ void generator.createObjects({ [TestSchemaType.organization]: 20, [TestSchemaType.contact]: 50 }).catch();
45
+ }, []);
46
+
47
+ // TODO(dmaretskyi): Not sure how to provide `containerUrl` here since the html now lives in composer-app.
48
+ // TODO(burdon): Normalize html/frame.tsx with composer-app to test locally.
49
+ return (
50
+ <div className={'flex fixed inset-0'}>{accessor && <ScriptBlock script={object} containerUrl={mainUrl} />}</div>
51
+ );
52
+ };
53
+
54
+ export default {
55
+ title: 'plugin-script/ScriptBlock',
56
+ component: ScriptBlock,
57
+ render: () => <ClientRepeater component={Story} createSpace />,
58
+ decorators: [withTheme],
59
+ parameters: {
60
+ layout: 'fullscreen',
61
+ },
62
+ };
63
+
64
+ export const Default = {};