@dxos/app-framework 0.3.11-main.d3a2438 → 0.3.11-main.d46c1ca

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.
@@ -3,20 +3,22 @@
3
3
  //
4
4
 
5
5
  import { shallow } from 'deepsignal/react';
6
- import React, { useEffect, type FC, type PropsWithChildren, type ReactNode } from 'react';
6
+ import React, { useEffect, type FC, type PropsWithChildren, type ReactNode, useState } from 'react';
7
7
 
8
8
  import { LocalStorageStore } from '@dxos/local-storage';
9
9
  import { log } from '@dxos/log';
10
10
 
11
11
  import { type PluginContext, PluginProvider } from './PluginContext';
12
12
  import { type Plugin, type PluginDefinition, type PluginProvides } from './plugin';
13
+ import { ErrorBoundary } from '../SurfacePlugin';
13
14
 
14
15
  export type BootstrapPluginsParams = {
15
16
  order: PluginDefinition['meta'][];
16
17
  plugins: Record<string, () => Promise<PluginDefinition>>;
17
18
  core?: string[];
18
19
  defaults?: string[];
19
- fallback?: ReactNode;
20
+ fallback?: ErrorBoundary['props']['fallback'];
21
+ placeholder?: ReactNode;
20
22
  };
21
23
 
22
24
  export type PluginHostProvides = {
@@ -36,7 +38,8 @@ export const PluginHost = ({
36
38
  plugins: definitions,
37
39
  core = [],
38
40
  defaults = [],
39
- fallback = null,
41
+ fallback = DefaultFallback,
42
+ placeholder = null,
40
43
  }: BootstrapPluginsParams): PluginDefinition<PluginHostProvides> => {
41
44
  const state = new LocalStorageStore<PluginContext>(PLUGIN_HOST, {
42
45
  ready: false,
@@ -63,65 +66,97 @@ export const PluginHost = ({
63
66
  plugins: state.values,
64
67
  context: ({ children }) => <PluginProvider value={state.values}>{children}</PluginProvider>,
65
68
  root: () => {
66
- useEffect(() => {
67
- log('initializing plugins', { enabled: state.values.enabled });
68
- const timeout = setTimeout(async () => {
69
- const enabledIds = [...core, ...state.values.enabled].sort((a, b) => {
70
- const indexA = order.findIndex(({ id }) => id === a);
71
- const indexB = order.findIndex(({ id }) => id === b);
72
- return indexA - indexB;
73
- });
74
-
75
- const enabled = await Promise.all(
76
- enabledIds
77
- .map((id) => definitions[id])
78
- // If local storage indicates a plugin is enabled, but it is not available, ignore it.
79
- .filter((definition): definition is () => Promise<PluginDefinition> => Boolean(definition))
80
- .map((definition) => definition()),
81
- );
82
-
83
- const plugins = await Promise.all(
84
- enabled.map(async (definition) => {
85
- const plugin = await initializePlugin(definition).catch((err) => {
86
- console.error('Failed to initialize plugin:', definition.meta.id, err);
87
- return undefined;
88
- });
89
- return plugin;
90
- }),
91
- ).then((plugins) => plugins.filter((plugin): plugin is Plugin => Boolean(plugin)));
92
- log('plugins initialized', { plugins });
93
-
94
- await Promise.all(
95
- enabled.map(async (pluginDefinition) => {
96
- await pluginDefinition.ready?.(plugins);
97
- }),
98
- );
99
- log('plugins ready', { plugins });
100
-
101
- state.values.plugins = shallow(plugins);
102
- state.values.ready = true;
103
- });
104
-
105
- return () => {
106
- clearTimeout(timeout);
107
- state.values.ready = false;
108
- // TODO(wittjosiah): Does this ever need to be called prior to having dynamic plugins?
109
- // void Promise.all(enabled.map((definition) => definition.unload?.()));
110
- };
111
- }, []);
112
-
113
- if (!state.values.ready) {
114
- return <>{fallback}</>;
115
- }
116
-
117
- const ComposedContext = composeContext(state.values.plugins);
118
-
119
- return <ComposedContext>{rootComponents(state.values.plugins)}</ComposedContext>;
69
+ return (
70
+ <ErrorBoundary fallback={fallback}>
71
+ <Root order={order} core={core} definitions={definitions} state={state.values} placeholder={placeholder} />
72
+ </ErrorBoundary>
73
+ );
120
74
  },
121
75
  },
122
76
  };
123
77
  };
124
78
 
79
+ const DefaultFallback = ({ error }: { error: Error }) => {
80
+ return (
81
+ <div style={{ padding: '1rem' }}>
82
+ {/* TODO(wittjosiah): Link to docs for replacing default. */}
83
+ <h1 style={{ fontSize: '1.2rem', fontWeight: 700, margin: '0.5rem 0' }}>{error.message}</h1>
84
+ <pre>{error.stack}</pre>
85
+ </div>
86
+ );
87
+ };
88
+
89
+ type RootProps = {
90
+ order: PluginDefinition['meta'][];
91
+ state: PluginContext;
92
+ definitions: Record<string, () => Promise<PluginDefinition>>;
93
+ core: string[];
94
+ placeholder: ReactNode;
95
+ };
96
+
97
+ const Root = ({ order, core: corePluginIds, definitions, state, placeholder }: RootProps) => {
98
+ const [error, setError] = useState<unknown>();
99
+
100
+ useEffect(() => {
101
+ log('initializing plugins', { enabled: state.enabled });
102
+ const timeout = setTimeout(async () => {
103
+ try {
104
+ const enabledIds = [...corePluginIds, ...state.enabled].sort((a, b) => {
105
+ const indexA = order.findIndex(({ id }) => id === a);
106
+ const indexB = order.findIndex(({ id }) => id === b);
107
+ return indexA - indexB;
108
+ });
109
+
110
+ const enabled = await Promise.all(
111
+ enabledIds
112
+ .map((id) => definitions[id])
113
+ // If local storage indicates a plugin is enabled, but it is not available, ignore it.
114
+ .filter((definition): definition is () => Promise<PluginDefinition> => Boolean(definition))
115
+ .map((definition) => definition()),
116
+ );
117
+
118
+ const plugins = await Promise.all(
119
+ enabled.map(async (definition) => {
120
+ const plugin = await initializePlugin(definition).catch((err) => {
121
+ console.error('Failed to initialize plugin:', definition.meta.id, err);
122
+ return undefined;
123
+ });
124
+ return plugin;
125
+ }),
126
+ ).then((plugins) => plugins.filter((plugin): plugin is Plugin => Boolean(plugin)));
127
+ log('plugins initialized', { plugins });
128
+
129
+ await Promise.all(enabled.map((pluginDefinition) => pluginDefinition.ready?.(plugins)));
130
+ log('plugins ready', { plugins });
131
+
132
+ state.plugins = shallow(plugins);
133
+ state.ready = true;
134
+ } catch (err) {
135
+ setError(err);
136
+ }
137
+ });
138
+
139
+ return () => {
140
+ clearTimeout(timeout);
141
+ state.ready = false;
142
+ // TODO(wittjosiah): Does this ever need to be called prior to having dynamic plugins?
143
+ // void Promise.all(enabled.map((definition) => definition.unload?.()));
144
+ };
145
+ }, []);
146
+
147
+ if (error) {
148
+ throw error;
149
+ }
150
+
151
+ if (!state.ready) {
152
+ return <>{placeholder}</>;
153
+ }
154
+
155
+ const ComposedContext = composeContext(state.plugins);
156
+
157
+ return <ComposedContext>{rootComponents(state.plugins)}</ComposedContext>;
158
+ };
159
+
125
160
  /**
126
161
  * Resolve a `PluginDefinition` into a fully initialized `Plugin`.
127
162
  */
@@ -99,6 +99,7 @@ export type PluginDefinition<TProvides = {}, TInitializeProvides = {}> = Omit<Pl
99
99
  *
100
100
  * @param plugins All plugins which successfully initialized.
101
101
  */
102
+ // TODO(wittjosiah): Rename `ready` to a verb?
102
103
  ready?: (plugins: Plugin[]) => Promise<void>;
103
104
 
104
105
  /**