sandlot 0.1.0 → 0.1.2
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 +53 -156
- package/dist/build-emitter.d.ts +29 -0
- package/dist/build-emitter.d.ts.map +1 -0
- package/dist/bundler.d.ts +2 -2
- package/dist/bundler.d.ts.map +1 -1
- package/dist/fs.d.ts +18 -4
- package/dist/fs.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -69
- package/dist/internal.d.ts +5 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +79 -24
- package/dist/sandbox-manager.d.ts +0 -12
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox.d.ts +0 -8
- package/dist/sandbox.d.ts.map +1 -1
- package/package.json +2 -7
- package/src/build-emitter.ts +61 -0
- package/src/bundler.ts +68 -27
- package/src/fs.ts +19 -6
- package/src/index.ts +18 -1
- package/src/internal.ts +5 -2
- package/src/sandbox-manager.ts +1 -82
- package/src/sandbox.ts +1 -75
- package/src/ts-libs.ts +1 -1
- package/src/react.tsx +0 -331
package/src/sandbox.ts
CHANGED
|
@@ -3,81 +3,7 @@ import { IndexedDbFs, type IndexedDbFsOptions } from "./fs";
|
|
|
3
3
|
import { initBundler, type BundleResult } from "./bundler";
|
|
4
4
|
import { createDefaultCommands, type CommandDeps } from "./commands";
|
|
5
5
|
import { getDefaultResources, type SharedResources } from "./shared-resources";
|
|
6
|
-
|
|
7
|
-
// Re-export for convenience
|
|
8
|
-
export type { BundleResult } from "./bundler";
|
|
9
|
-
export type { TypecheckResult } from "./typechecker";
|
|
10
|
-
export type { SharedResources, TypesCache } from "./shared-resources";
|
|
11
|
-
export type { PackageManifest, InstallResult } from "./packages";
|
|
12
|
-
export type { RunContext, RunOptions, RunResult } from "./commands";
|
|
13
|
-
export { installPackage, uninstallPackage, listPackages, getPackageManifest } from "./packages";
|
|
14
|
-
export { InMemoryTypesCache } from "./shared-resources";
|
|
15
|
-
|
|
16
|
-
// Loader utilities
|
|
17
|
-
export {
|
|
18
|
-
loadModule,
|
|
19
|
-
loadExport,
|
|
20
|
-
loadDefault,
|
|
21
|
-
getExportNames,
|
|
22
|
-
hasExport,
|
|
23
|
-
createModuleUrl,
|
|
24
|
-
revokeModuleUrl,
|
|
25
|
-
ModuleLoadError,
|
|
26
|
-
ExportNotFoundError,
|
|
27
|
-
} from "./loader";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Simple typed event emitter for build results.
|
|
31
|
-
* Caches the last result so waitFor() can be called after build completes.
|
|
32
|
-
*/
|
|
33
|
-
class BuildEmitter {
|
|
34
|
-
private listeners = new Set<(result: BundleResult) => void | Promise<void>>();
|
|
35
|
-
private lastResult: BundleResult | null = null;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Emit a build result to all listeners and cache it
|
|
39
|
-
*/
|
|
40
|
-
emit = async (result: BundleResult): Promise<void> => {
|
|
41
|
-
this.lastResult = result;
|
|
42
|
-
const promises: Promise<void>[] = [];
|
|
43
|
-
for (const listener of this.listeners) {
|
|
44
|
-
const ret = listener(result);
|
|
45
|
-
if (ret instanceof Promise) {
|
|
46
|
-
promises.push(ret);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
await Promise.all(promises);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Subscribe to build events. Returns an unsubscribe function.
|
|
54
|
-
*/
|
|
55
|
-
on(callback: (result: BundleResult) => void | Promise<void>): () => void {
|
|
56
|
-
this.listeners.add(callback);
|
|
57
|
-
return () => {
|
|
58
|
-
this.listeners.delete(callback);
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get the last build result, or wait for the next one if none exists.
|
|
64
|
-
* Clears the cached result after returning, so subsequent calls wait for new builds.
|
|
65
|
-
*/
|
|
66
|
-
waitFor(): Promise<BundleResult> {
|
|
67
|
-
if (this.lastResult) {
|
|
68
|
-
const result = this.lastResult;
|
|
69
|
-
this.lastResult = null;
|
|
70
|
-
return Promise.resolve(result);
|
|
71
|
-
}
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
const unsub = this.on((result) => {
|
|
74
|
-
unsub();
|
|
75
|
-
this.lastResult = null;
|
|
76
|
-
resolve(result);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
6
|
+
import { BuildEmitter } from "./build-emitter";
|
|
81
7
|
|
|
82
8
|
/**
|
|
83
9
|
* Options for creating a sandbox environment
|
package/src/ts-libs.ts
CHANGED
package/src/react.tsx
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React-specific helpers for Sandlot
|
|
3
|
-
*
|
|
4
|
-
* These helpers simplify working with dynamically loaded React components,
|
|
5
|
-
* particularly when using the render function pattern for isolation.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```tsx
|
|
9
|
-
* import { DynamicMount } from 'sandlot/react';
|
|
10
|
-
*
|
|
11
|
-
* function App() {
|
|
12
|
-
* const [module, setModule] = useState(null);
|
|
13
|
-
*
|
|
14
|
-
* return (
|
|
15
|
-
* <DynamicMount
|
|
16
|
-
* module={module}
|
|
17
|
-
* props={{ name: "World" }}
|
|
18
|
-
* fallback={<div>Loading...</div>}
|
|
19
|
-
* />
|
|
20
|
-
* );
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
useRef,
|
|
27
|
-
useEffect,
|
|
28
|
-
useCallback,
|
|
29
|
-
useState,
|
|
30
|
-
type ReactElement,
|
|
31
|
-
type RefObject,
|
|
32
|
-
type CSSProperties,
|
|
33
|
-
} from "react";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Interface for dynamic modules that use the render function pattern.
|
|
37
|
-
* Dynamic components should export a `render` function that mounts
|
|
38
|
-
* the component into a container and returns a cleanup function.
|
|
39
|
-
*/
|
|
40
|
-
export interface DynamicRenderModule<P = Record<string, unknown>> {
|
|
41
|
-
/**
|
|
42
|
-
* Mount the component into a container element
|
|
43
|
-
*
|
|
44
|
-
* @param container - The DOM element to render into
|
|
45
|
-
* @param props - Props to pass to the component
|
|
46
|
-
* @returns A cleanup function to unmount the component
|
|
47
|
-
*/
|
|
48
|
-
render: (container: HTMLElement, props?: P) => (() => void) | void;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Props for the DynamicMount component
|
|
53
|
-
*/
|
|
54
|
-
export interface DynamicMountProps<P = Record<string, unknown>> {
|
|
55
|
-
/** The loaded dynamic module with a render function */
|
|
56
|
-
module: DynamicRenderModule<P> | null | undefined;
|
|
57
|
-
/** Props to pass to the dynamic component */
|
|
58
|
-
props?: P;
|
|
59
|
-
/** Optional className for the container div */
|
|
60
|
-
className?: string;
|
|
61
|
-
/** Optional style for the container div */
|
|
62
|
-
style?: CSSProperties;
|
|
63
|
-
/** Optional id for the container div */
|
|
64
|
-
id?: string;
|
|
65
|
-
/** Content to render while module is loading (null/undefined) */
|
|
66
|
-
fallback?: ReactElement | null;
|
|
67
|
-
/** Called when the dynamic component mounts */
|
|
68
|
-
onMount?: () => void;
|
|
69
|
-
/** Called when the dynamic component unmounts */
|
|
70
|
-
onUnmount?: () => void;
|
|
71
|
-
/** Called if rendering fails */
|
|
72
|
-
onError?: (error: Error) => void;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Component that mounts a dynamic module's render function into a container.
|
|
77
|
-
* Handles cleanup automatically when the module changes or unmounts.
|
|
78
|
-
*
|
|
79
|
-
* This is the recommended way to render dynamically loaded components
|
|
80
|
-
* that use the render function pattern for React instance isolation.
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* ```tsx
|
|
84
|
-
* import { DynamicMount } from 'sandlot/react';
|
|
85
|
-
* import { loadModule } from 'sandlot';
|
|
86
|
-
*
|
|
87
|
-
* function App() {
|
|
88
|
-
* const [module, setModule] = useState(null);
|
|
89
|
-
*
|
|
90
|
-
* const loadComponent = async (buildResult: BundleResult) => {
|
|
91
|
-
* const mod = await loadModule(buildResult);
|
|
92
|
-
* setModule(mod);
|
|
93
|
-
* };
|
|
94
|
-
*
|
|
95
|
-
* return (
|
|
96
|
-
* <div>
|
|
97
|
-
* <button onClick={loadComponent}>Load</button>
|
|
98
|
-
* <DynamicMount
|
|
99
|
-
* module={module}
|
|
100
|
-
* props={{ count: 5 }}
|
|
101
|
-
* fallback={<div>Click to load...</div>}
|
|
102
|
-
* className="dynamic-container"
|
|
103
|
-
* />
|
|
104
|
-
* </div>
|
|
105
|
-
* );
|
|
106
|
-
* }
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
export function DynamicMount<P = Record<string, unknown>>({
|
|
110
|
-
module,
|
|
111
|
-
props,
|
|
112
|
-
className,
|
|
113
|
-
style,
|
|
114
|
-
id,
|
|
115
|
-
fallback = null,
|
|
116
|
-
onMount,
|
|
117
|
-
onUnmount,
|
|
118
|
-
onError,
|
|
119
|
-
}: DynamicMountProps<P>): ReactElement | null {
|
|
120
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
121
|
-
const cleanupRef = useRef<(() => void) | null>(null);
|
|
122
|
-
const [error, setError] = useState<Error | null>(null);
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (!module || !containerRef.current) return;
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
// Clean up previous render
|
|
129
|
-
if (cleanupRef.current) {
|
|
130
|
-
cleanupRef.current();
|
|
131
|
-
cleanupRef.current = null;
|
|
132
|
-
onUnmount?.();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Clear any previous error
|
|
136
|
-
setError(null);
|
|
137
|
-
|
|
138
|
-
// Mount the new component
|
|
139
|
-
const cleanup = module.render(containerRef.current, props);
|
|
140
|
-
cleanupRef.current = cleanup ?? null;
|
|
141
|
-
onMount?.();
|
|
142
|
-
} catch (err) {
|
|
143
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
144
|
-
setError(error);
|
|
145
|
-
onError?.(error);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return () => {
|
|
149
|
-
if (cleanupRef.current) {
|
|
150
|
-
cleanupRef.current();
|
|
151
|
-
cleanupRef.current = null;
|
|
152
|
-
onUnmount?.();
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
}, [module, props, onMount, onUnmount, onError]);
|
|
156
|
-
|
|
157
|
-
// Show fallback if no module
|
|
158
|
-
if (!module) {
|
|
159
|
-
return fallback;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Show error if render failed
|
|
163
|
-
if (error) {
|
|
164
|
-
return (
|
|
165
|
-
<div
|
|
166
|
-
style={{
|
|
167
|
-
color: "red",
|
|
168
|
-
padding: "8px",
|
|
169
|
-
border: "1px solid red",
|
|
170
|
-
borderRadius: "4px",
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
Failed to render dynamic component: {error.message}
|
|
174
|
-
</div>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return <div ref={containerRef} className={className} style={style} id={id} />;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Result returned by useDynamicComponent hook
|
|
183
|
-
*/
|
|
184
|
-
export interface UseDynamicComponentResult {
|
|
185
|
-
/** Ref to attach to the container element */
|
|
186
|
-
containerRef: RefObject<HTMLDivElement | null>;
|
|
187
|
-
/** Whether the component is currently mounted */
|
|
188
|
-
isMounted: boolean;
|
|
189
|
-
/** Any error that occurred during mounting */
|
|
190
|
-
error: Error | null;
|
|
191
|
-
/** Manually trigger a re-render with new props */
|
|
192
|
-
update: (props?: Record<string, unknown>) => void;
|
|
193
|
-
/** Manually unmount the component */
|
|
194
|
-
unmount: () => void;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Hook for mounting dynamic modules with more control than DynamicMount.
|
|
199
|
-
* Provides access to mount state and manual control over mounting/unmounting.
|
|
200
|
-
*
|
|
201
|
-
* @param module - The dynamic module to mount
|
|
202
|
-
* @param props - Props to pass to the component
|
|
203
|
-
* @returns Object with containerRef, state, and control functions
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* ```tsx
|
|
207
|
-
* import { useDynamicComponent } from 'sandlot/react';
|
|
208
|
-
*
|
|
209
|
-
* function App() {
|
|
210
|
-
* const { containerRef, isMounted, error, unmount } = useDynamicComponent(
|
|
211
|
-
* module,
|
|
212
|
-
* { initialCount: 0 }
|
|
213
|
-
* );
|
|
214
|
-
*
|
|
215
|
-
* return (
|
|
216
|
-
* <div>
|
|
217
|
-
* <div ref={containerRef} />
|
|
218
|
-
* {isMounted && <button onClick={unmount}>Remove</button>}
|
|
219
|
-
* {error && <div>Error: {error.message}</div>}
|
|
220
|
-
* </div>
|
|
221
|
-
* );
|
|
222
|
-
* }
|
|
223
|
-
* ```
|
|
224
|
-
*/
|
|
225
|
-
export function useDynamicComponent<P = Record<string, unknown>>(
|
|
226
|
-
module: DynamicRenderModule<P> | null | undefined,
|
|
227
|
-
props?: P,
|
|
228
|
-
): UseDynamicComponentResult {
|
|
229
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
230
|
-
const cleanupRef = useRef<(() => void) | null>(null);
|
|
231
|
-
const propsRef = useRef(props);
|
|
232
|
-
const [isMounted, setIsMounted] = useState(false);
|
|
233
|
-
const [error, setError] = useState<Error | null>(null);
|
|
234
|
-
|
|
235
|
-
// Keep props ref updated
|
|
236
|
-
propsRef.current = props;
|
|
237
|
-
|
|
238
|
-
const unmount = useCallback(() => {
|
|
239
|
-
if (cleanupRef.current) {
|
|
240
|
-
cleanupRef.current();
|
|
241
|
-
cleanupRef.current = null;
|
|
242
|
-
}
|
|
243
|
-
setIsMounted(false);
|
|
244
|
-
}, []);
|
|
245
|
-
|
|
246
|
-
const update = useCallback(
|
|
247
|
-
(newProps?: Record<string, unknown>) => {
|
|
248
|
-
if (!module || !containerRef.current) return;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
unmount();
|
|
252
|
-
setError(null);
|
|
253
|
-
const cleanup = module.render(
|
|
254
|
-
containerRef.current,
|
|
255
|
-
(newProps ?? propsRef.current) as P,
|
|
256
|
-
);
|
|
257
|
-
cleanupRef.current = cleanup ?? null;
|
|
258
|
-
setIsMounted(true);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
[module, unmount],
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
if (module && containerRef.current) {
|
|
268
|
-
update(props as Record<string, unknown>);
|
|
269
|
-
}
|
|
270
|
-
return unmount;
|
|
271
|
-
}, [module, props, update, unmount]);
|
|
272
|
-
|
|
273
|
-
return { containerRef, isMounted, error, update, unmount };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Code template for a React component's render function.
|
|
278
|
-
* This is the pattern dynamic components should follow when using
|
|
279
|
-
* the render function pattern for React instance isolation.
|
|
280
|
-
*
|
|
281
|
-
* Dynamic components import React from esm.sh and use their own
|
|
282
|
-
* ReactDOM.createRoot to mount, avoiding conflicts with the host's React.
|
|
283
|
-
*/
|
|
284
|
-
export const REACT_RENDER_TEMPLATE = `
|
|
285
|
-
import React from "react";
|
|
286
|
-
import { createRoot } from "react-dom/client";
|
|
287
|
-
|
|
288
|
-
// Your component here
|
|
289
|
-
function MyComponent(props) {
|
|
290
|
-
return <div>Hello {props.name}</div>;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Export a render function that mounts using esm.sh's ReactDOM
|
|
294
|
-
export function render(container, props) {
|
|
295
|
-
const root = createRoot(container);
|
|
296
|
-
root.render(<MyComponent {...props} />);
|
|
297
|
-
return () => root.unmount();
|
|
298
|
-
}
|
|
299
|
-
`.trim();
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Generate render function code for a given component name.
|
|
303
|
-
* Useful for code generation tools or agents.
|
|
304
|
-
*
|
|
305
|
-
* @param componentName - The name of the component to wrap
|
|
306
|
-
* @param hasProps - Whether the component accepts props
|
|
307
|
-
*/
|
|
308
|
-
export function generateRenderFunction(
|
|
309
|
-
componentName: string,
|
|
310
|
-
hasProps = true,
|
|
311
|
-
): string {
|
|
312
|
-
if (hasProps) {
|
|
313
|
-
return `
|
|
314
|
-
// Export a render function that mounts using esm.sh's ReactDOM
|
|
315
|
-
export function render(container: HTMLElement, props?: Parameters<typeof ${componentName}>[0]) {
|
|
316
|
-
const root = createRoot(container);
|
|
317
|
-
root.render(<${componentName} {...props} />);
|
|
318
|
-
return () => root.unmount();
|
|
319
|
-
}
|
|
320
|
-
`.trim();
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return `
|
|
324
|
-
// Export a render function that mounts using esm.sh's ReactDOM
|
|
325
|
-
export function render(container: HTMLElement) {
|
|
326
|
-
const root = createRoot(container);
|
|
327
|
-
root.render(<${componentName} />);
|
|
328
|
-
return () => root.unmount();
|
|
329
|
-
}
|
|
330
|
-
`.trim();
|
|
331
|
-
}
|