browser-tsx-sandbox 0.1.0
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/API.md +659 -0
- package/README.md +428 -0
- package/dist/index.cjs +270 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/API.md
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
# 📚 API Reference — browser-tsx-sandbox
|
|
2
|
+
|
|
3
|
+
Максимально подробное описание публичных контрактов, сигнатур, типов и поведения пакета.
|
|
4
|
+
|
|
5
|
+
- [1. Назначение и область применения](#1-назначение-и-область-применения)
|
|
6
|
+
- [2. Установка и форматы модулей](#2-установка-и-форматы-модулей)
|
|
7
|
+
- [3. Карта модулей](#3-карта-модулей)
|
|
8
|
+
- [4. Публичный API (`src/index.ts`)](#4-публичный-api-srcindexts)
|
|
9
|
+
- [5. Базовые типы](#5-базовые-типы)
|
|
10
|
+
- [6. Ошибки](#6-ошибки)
|
|
11
|
+
- [7. `SandboxFacade`](#7-sandboxfacade)
|
|
12
|
+
- [8. `useLiveSandbox`](#8-uselivesandbox)
|
|
13
|
+
- [9. Compiler](#9-compiler)
|
|
14
|
+
- [10. Library Manager](#10-library-manager)
|
|
15
|
+
- [11. Sandbox (изоляция и выполнение)](#11-sandbox-изоляция-и-выполнение)
|
|
16
|
+
- [12. Assets (ZIP)](#12-assets-zip)
|
|
17
|
+
- [13. Интеграция с JSON-каталогом виджетов](#13-интеграция-с-json-каталогом-виджетов)
|
|
18
|
+
- [14. Рендер-харнесс (эталонная интеграция)](#14-рендер-харнесс-эталонная-интеграция)
|
|
19
|
+
- [15. Модель безопасности](#15-модель-безопасности)
|
|
20
|
+
- [16. Тестирование](#16-тестирование)
|
|
21
|
+
- [17. Совместимость и версии](#17-совместимость-и-версии)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Назначение и область применения
|
|
26
|
+
|
|
27
|
+
`browser-tsx-sandbox` — клиентская (Zero-Backend) среда, которая:
|
|
28
|
+
|
|
29
|
+
1. извлекает список NPM-зависимостей из сырого TSX;
|
|
30
|
+
2. подгружает отсутствующие библиотеки с CDN (`esm.sh`);
|
|
31
|
+
3. транспилирует TSX → CommonJS (`React.createElement`) через Sucrase;
|
|
32
|
+
4. выполняет код в изолированной области (`new Function`) с затенением опасных глобальных API;
|
|
33
|
+
5. возвращает готовый React-компонент для рендера (например, Remotion Player/Composition).
|
|
34
|
+
|
|
35
|
+
Пакет не зависит от Node.js и не требует серверной сборки бандлов. Тяжёлые e2e-сценарии рендера требуют Node.js и Chrome (см. §14).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Установка и форматы модулей
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install browser-tsx-sandbox react sucrase
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **Формат:** ESM (`"type": "module"`), точка входа — `src/index.ts` (TypeScript-исходники; для публикации рекомендуется собрать в `dist`).
|
|
46
|
+
- **Peer dependencies:** `react >= 17`, `sucrase >= 3`.
|
|
47
|
+
- **Runtime dependencies:** `react`, `sucrase`, `fflate`.
|
|
48
|
+
- **Dev-only (для e2e-рендера):** `remotion`, `@remotion/bundler`, `@remotion/renderer`, `lucide-react`, `tailwindcss`, `postcss`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. Карта модулей
|
|
53
|
+
|
|
54
|
+
| Модуль | Файл | Ответственность |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Core | `src/core/types.ts` | Общие типы и интерфейсы |
|
|
57
|
+
| Core | `src/core/errors.ts` | Классы ошибок |
|
|
58
|
+
| Compiler | `src/compiler/analyzer.ts` | Поиск bare-импортов |
|
|
59
|
+
| Compiler | `src/compiler/transform.ts` | Транспиляция TSX → CJS |
|
|
60
|
+
| Library Manager | `src/library-manager/cache.ts` | Реестр модулей |
|
|
61
|
+
| Library Manager | `src/library-manager/loader.ts` | Загрузка с CDN + нормализация ESM |
|
|
62
|
+
| Sandbox | `src/sandbox/scope.ts` | Список затеняемых глобалов |
|
|
63
|
+
| Sandbox | `src/sandbox/evaluator.ts` | `new Function` и `require` |
|
|
64
|
+
| Assets | `src/assets/zip.ts` | Распаковка ZIP и blob-URL |
|
|
65
|
+
| React | `src/react/useLiveSandbox.ts` | React-хук |
|
|
66
|
+
| Facade | `src/facade.ts` | Оркестратор |
|
|
67
|
+
| Entry | `src/index.ts` | Публичные экспорты |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 4. Публичный API (`src/index.ts`)
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
export { SandboxFacade } from './facade';
|
|
75
|
+
export { useLiveSandbox } from './react/useLiveSandbox';
|
|
76
|
+
|
|
77
|
+
export { ModuleCache } from './library-manager/cache';
|
|
78
|
+
export { loadMissingModules } from './library-manager/loader';
|
|
79
|
+
export type { ModuleImporter } from './library-manager/loader';
|
|
80
|
+
|
|
81
|
+
export { extractBareImports } from './compiler/analyzer';
|
|
82
|
+
export { compileTsx } from './compiler/transform';
|
|
83
|
+
|
|
84
|
+
export { executeComponent } from './sandbox/evaluator';
|
|
85
|
+
export { getShadowedGlobals } from './sandbox/scope';
|
|
86
|
+
|
|
87
|
+
export { extractAssetZip, createAssetUrlMap, releaseAssetUrls } from './assets/zip';
|
|
88
|
+
export type { AssetArchive } from './assets/zip';
|
|
89
|
+
|
|
90
|
+
export * from './core/types';
|
|
91
|
+
export * from './core/errors';
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 5. Базовые типы
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
type ModuleRegistry = Record<string, any>;
|
|
100
|
+
|
|
101
|
+
interface SandboxGlobals {
|
|
102
|
+
staticFile: (filename: string) => string;
|
|
103
|
+
[key: string]: any;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface EvaluationResult<T = any> {
|
|
107
|
+
component: T | null;
|
|
108
|
+
error: Error | null;
|
|
109
|
+
executionTimeMs: number;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `ModuleRegistry` — карта `имя пакета → модуль`, доступная внутри песочницы через `require(name)`.
|
|
114
|
+
- `SandboxGlobals.staticFile` — резолвер локальных ассетов: `assetsMap[filename] || ''`. Можно добавлять любые дополнительные глобалы (они становятся параметрами `new Function`).
|
|
115
|
+
- `EvaluationResult` — результат `SandboxFacade.compile`: при ошибке `component === null`, при успехе `error === null`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 6. Ошибки
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
class CompilerError extends Error {
|
|
123
|
+
name = 'CompilerError';
|
|
124
|
+
message.includes('[Compiler Error]: ');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class SecurityError extends Error {
|
|
128
|
+
name = 'SecurityError';
|
|
129
|
+
message.includes('[Security Violation]: ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class NetworkModuleError extends Error {
|
|
133
|
+
name = 'NetworkModuleError';
|
|
134
|
+
message.includes(`[Network Error]: Не удалось загрузить пакет "${moduleName}". ${originalMessage}`);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Класс | Когда возникает |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `CompilerError` | Sucrase не смог разобрать/транспилировать TSX |
|
|
141
|
+
| `SecurityError` | код попытался импортировать модуль, которого нет в реестре |
|
|
142
|
+
| `NetworkModuleError` | не удалось подгрузить пакет с CDN |
|
|
143
|
+
|
|
144
|
+
Важно: `executeComponent` **не заворачивает** `SecurityError` в generic `[Runtime Error]` — ошибки безопасности сохраняют свой тип и доходят до вызывающего кода.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 7. `SandboxFacade`
|
|
149
|
+
|
|
150
|
+
### 7.1 Конструктор
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
new SandboxFacade(initialRegistry?: ModuleRegistry, importer?: ModuleImporter)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Параметр | Тип | Описание |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `initialRegistry` | `ModuleRegistry` | Предрегистрированные модули (React, Remotion, lucide-react и т.п.). По умолчанию `{}`. |
|
|
159
|
+
| `importer` | `ModuleImporter` | Точка внедрения загрузчика для тестов / замены CDN. По умолчанию — динамический `import()` с `https://esm.sh/<pkg>`. |
|
|
160
|
+
|
|
161
|
+
Конструктор копирует `initialRegistry` в собственный `ModuleCache`. Внешний объект можно мутировать после — на песочницу это не влияет.
|
|
162
|
+
|
|
163
|
+
### 7.2 `setAssets`
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
setAssets(assets: Record<string, string>): void
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Регистрирует карту `имя файла → URL` (обычно `blob:` из `URL.createObjectURL`). `staticFile(name)` внутри сцены вернёт `assets[name] ?? ''`.
|
|
170
|
+
|
|
171
|
+
### 7.3 `compile`
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
compile(rawTsx: string): Promise<EvaluationResult>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Последовательность шагов:
|
|
178
|
+
|
|
179
|
+
1. `extractBareImports(rawTsx)` — список внешних пакетов.
|
|
180
|
+
2. `loadMissingModules(packages, cache, importer)` — догрузка отсутствующих.
|
|
181
|
+
3. `compileTsx(rawTsx)` — TSX → CommonJS.
|
|
182
|
+
4. Формирование `globals = { staticFile }`.
|
|
183
|
+
5. `executeComponent(jsCode, cache.getAll(), globals)`.
|
|
184
|
+
|
|
185
|
+
Гарантии:
|
|
186
|
+
|
|
187
|
+
- Никогда не бросает — все ошибки возвращаются в `EvaluationResult.error`.
|
|
188
|
+
- `executionTimeMs` — измеренное через `performance.now()` (в Node доступно как глобал).
|
|
189
|
+
- Загруженные библиотеки кэшируются между вызовами (сеть не дёргается повторно).
|
|
190
|
+
- Если `rawTsx` импортирует уже предрегистрированный пакет (например, `react`), сеть не используется.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const facade = new SandboxFacade({ react: React });
|
|
194
|
+
facade.setAssets({ 'logo.png': 'blob:http://localhost/...' });
|
|
195
|
+
|
|
196
|
+
const { component, error, executionTimeMs } = await facade.compile(`
|
|
197
|
+
import React from 'react';
|
|
198
|
+
export default function Scene() { return <div>{staticFile('logo.png')}</div>; }
|
|
199
|
+
`);
|
|
200
|
+
|
|
201
|
+
if (error) console.error(error);
|
|
202
|
+
else renderToDom(component);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 7.4 Возврат экспортов
|
|
206
|
+
|
|
207
|
+
`executeComponent` возвращает:
|
|
208
|
+
|
|
209
|
+
1. `exports.default`, если он truthy;
|
|
210
|
+
2. иначе — первый именованный экспорт (например, `export const Scene`);
|
|
211
|
+
3. иначе — `null`.
|
|
212
|
+
|
|
213
|
+
Это покрывает оба стиля: `export default Scene` и `export const Scene`.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 8. `useLiveSandbox`
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
function useLiveSandbox(
|
|
221
|
+
code: string,
|
|
222
|
+
initialModules: ModuleRegistry,
|
|
223
|
+
localAssets?: Record<string, string>,
|
|
224
|
+
): {
|
|
225
|
+
Component: React.ComponentType<any> | null;
|
|
226
|
+
error: Error | null;
|
|
227
|
+
isCompiling: boolean;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Параметр | Описание |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `code` | TSX-строка для компиляции |
|
|
234
|
+
| `initialModules` | Предрегистрированные модули; `react` подставляется автоматически |
|
|
235
|
+
| `localAssets` | Карта `имя → blob:url` для `staticFile` |
|
|
236
|
+
|
|
237
|
+
Поведение:
|
|
238
|
+
|
|
239
|
+
- `SandboxFacade` создаётся один раз (`useRef`); `react` добавляется автоматически.
|
|
240
|
+
- Компиляция перезапускается при изменении `code` или содержимого ассетов.
|
|
241
|
+
- **`localAssets` не должен передаваться новым объектом каждый рендер без изменения содержимого** — хук сравнивает `JSON.stringify(localAssets)`, поэтому идентичность объекта безопасна.
|
|
242
|
+
- При размонтировании результат отбрасывается (`isMounted`-guard).
|
|
243
|
+
- Ошибки компиляции попадают в `error`, а не выбрасываются.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
function Preview({ code, assets }: { code: string; assets: Record<string, string> }) {
|
|
247
|
+
const { Component, error, isCompiling } = useLiveSandbox(code, {});
|
|
248
|
+
if (isCompiling) return <Spinner />;
|
|
249
|
+
if (error) return <ErrorOverlay error={error} />;
|
|
250
|
+
return Component ? <Component /> : null;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 9. Compiler
|
|
257
|
+
|
|
258
|
+
### 9.1 `compileTsx`
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
function compileTsx(code: string): string
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- Удаляет маркеры markdown-ограждений ```` ```tsx ```` / ```` ``` ````.
|
|
265
|
+
- Транспилирует `typescript`, `jsx`, `imports` с `jsxRuntime: 'classic'` (JSX → `React.createElement`).
|
|
266
|
+
- Импорты превращаются в `require(...)`, экспорты — в `exports.*`.
|
|
267
|
+
- Результат — строка CommonJS, исполняемая через `new Function`.
|
|
268
|
+
- При синтаксической ошибке бросает `CompilerError`.
|
|
269
|
+
|
|
270
|
+
Особенности classic-runtime: JSX требует наличия `React` в области видимости. `executeComponent` инжектит `React` параметром, поэтому `import React` в коде не обязателен.
|
|
271
|
+
|
|
272
|
+
### 9.2 `extractBareImports`
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
function extractBareImports(code: string): string[]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Возвращает уникальные имена пакетов (bare-импортов), игнорируя относительные (`./`, `../`) и абсолютные (`/`) пути.
|
|
279
|
+
|
|
280
|
+
Поддерживаемые формы:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import React from 'react'; // ['react']
|
|
284
|
+
import { motion } from 'framer-motion'; // ['framer-motion']
|
|
285
|
+
import * as THREE from 'three'; // ['three']
|
|
286
|
+
import React, { useState } from "react"; // ['react']
|
|
287
|
+
import 'swiper/css'; // ['swiper/css']
|
|
288
|
+
import type { FC } from 'react'; // ['react']
|
|
289
|
+
import { x } from '@scope/thing'; // ['@scope/thing']
|
|
290
|
+
import get from 'lodash/fp/get'; // ['lodash/fp/get']
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Ограничения:
|
|
294
|
+
|
|
295
|
+
- не является полноценным AST-парсером: `import` внутри строк/комментариев может дать ложное срабатывание;
|
|
296
|
+
- динамический `import('pkg')` не извлекается;
|
|
297
|
+
- сложные формы (`import a, * as b from ...`) не гарантируются.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 10. Library Manager
|
|
302
|
+
|
|
303
|
+
### 10.1 `ModuleCache`
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
class ModuleCache {
|
|
307
|
+
register(name: string, module: any): void;
|
|
308
|
+
get(name: string): any | undefined;
|
|
309
|
+
getAll(): ModuleRegistry; // поверхностная копия
|
|
310
|
+
has(name: string): boolean; // false для falsy-значений
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `getAll()` возвращает копию: мутация снимка не меняет кэш.
|
|
315
|
+
- Повторный `register(name, ...)` перезаписывает значение.
|
|
316
|
+
|
|
317
|
+
### 10.2 `loadMissingModules`
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
type ModuleImporter = (url: string) => Promise<any>;
|
|
321
|
+
|
|
322
|
+
function loadMissingModules(
|
|
323
|
+
packages: string[],
|
|
324
|
+
cache: ModuleCache,
|
|
325
|
+
importer?: ModuleImporter,
|
|
326
|
+
): Promise<void>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- Для каждого пакета: если `cache.has(pkg)` — пропустить; иначе `importer('https://esm.sh/' + pkg)`.
|
|
330
|
+
- Загрузка всех пакетов параллельна (`Promise.all`).
|
|
331
|
+
- Нормализация результата: `{ ...module, default: module.default || module, __esModule: true }`.
|
|
332
|
+
- `default` гарантированно есть (для CJS-библиотек это сам модуль).
|
|
333
|
+
- `__esModule: true` нужен, чтобы интероп Sucrase (`_interopRequireDefault`) не заворачивал модуль повторно.
|
|
334
|
+
- При ошибке бросает `NetworkModuleError(pkg, originalMessage)`.
|
|
335
|
+
|
|
336
|
+
Стандартный импортёр — `import(/* @vite-ignore */ url)`; в тестах/других средах можно передать свой.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 11. Sandbox (изоляция и выполнение)
|
|
341
|
+
|
|
342
|
+
### 11.1 `getShadowedGlobals`
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
function getShadowedGlobals(): {
|
|
346
|
+
forbiddenKeys: string[];
|
|
347
|
+
shadowValues: undefined[];
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`forbiddenKeys`:
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
window, document, localStorage, sessionStorage, fetch,
|
|
355
|
+
XMLHttpRequest, indexedDB, navigator
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`shadowValues` — массив `undefined` той же длины. Функция каждый раз создаёт новые массивы (нет общего мутабельного состояния).
|
|
359
|
+
|
|
360
|
+
### 11.2 `executeComponent`
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
function executeComponent(
|
|
364
|
+
compiledCode: string,
|
|
365
|
+
registry: ModuleRegistry,
|
|
366
|
+
globals: SandboxGlobals,
|
|
367
|
+
): any
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Как работает:
|
|
371
|
+
|
|
372
|
+
1. `customRequire(name)` возвращает `registry[name]` или бросает `SecurityError`.
|
|
373
|
+
2. Требуется `registry.react`, иначе бросается `Error("Модуль 'react' обязателен для компиляции TSX.")`.
|
|
374
|
+
3. Формируется функция:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
new Function(
|
|
378
|
+
'require', 'exports', 'React',
|
|
379
|
+
...Object.keys(globals), // кастомные глобалы (staticFile и др.)
|
|
380
|
+
...forbiddenKeys, // затеняются в undefined
|
|
381
|
+
compiledCode,
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
4. Возврат: `exports.default` → первый именованный экспорт → `null`.
|
|
386
|
+
5. Ошибки времени выполнения оборачиваются в `Error('[Runtime Error]: ...')`, **кроме** `SecurityError`, который пробрасывается как есть.
|
|
387
|
+
|
|
388
|
+
Контракт глобалов: ключи `globals` должны быть валидными идентификаторами JS (они становятся именами параметров).
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## 12. Assets (ZIP)
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
type AssetArchive = Record<string, Uint8Array>;
|
|
396
|
+
|
|
397
|
+
function extractAssetZip(zip: Uint8Array): AssetArchive;
|
|
398
|
+
|
|
399
|
+
function createAssetUrlMap(
|
|
400
|
+
archive: AssetArchive,
|
|
401
|
+
createUrl: (bytes: Uint8Array, filename: string) => string,
|
|
402
|
+
): Record<string, string>;
|
|
403
|
+
|
|
404
|
+
function releaseAssetUrls(urls: Record<string, string>, revoke: (url: string) => void): void;
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### `extractAssetZip`
|
|
408
|
+
|
|
409
|
+
- Распаковывает ZIP (через `fflate.unzipSync`).
|
|
410
|
+
- Игнорирует записи каталогов (пути, оканчивающиеся на `/`).
|
|
411
|
+
- Игнорирует системную папку macOS `__MACOSX/`.
|
|
412
|
+
- Нормализует ведущий `./`.
|
|
413
|
+
- Ключи — пути внутри архива (`assets/clip.mp4`).
|
|
414
|
+
|
|
415
|
+
### `createAssetUrlMap`
|
|
416
|
+
|
|
417
|
+
- Превращает архив в карту `имя файла → url` (берётся basename, подкаталоги схлопываются).
|
|
418
|
+
- Фабрика URL внедряется, поэтому функция тестируема без DOM.
|
|
419
|
+
|
|
420
|
+
### `releaseAssetUrls`
|
|
421
|
+
|
|
422
|
+
- Вызывает `revoke` для каждого URL (обычно `URL.revokeObjectURL`), чтобы не текла память.
|
|
423
|
+
|
|
424
|
+
Типовое использование в браузере:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
const archive = extractAssetZip(zipBytes);
|
|
428
|
+
const urls = createAssetUrlMap(archive, (bytes) => URL.createObjectURL(new Blob([bytes])));
|
|
429
|
+
facade.setAssets(urls);
|
|
430
|
+
// ... later
|
|
431
|
+
releaseAssetUrls(urls, URL.revokeObjectURL);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## 13. Интеграция с JSON-каталогом виджетов
|
|
437
|
+
|
|
438
|
+
Примеры: `examples/vidora-widgets.json` (Word By Word), `examples/vidora-widgets-logo.json` (Logo Shine Badge).
|
|
439
|
+
|
|
440
|
+
### 13.1 Схема каталога
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
interface VidoraCatalog {
|
|
444
|
+
vidora_schema_version: string; // '1.0'
|
|
445
|
+
exported_at: string; // ISO 8601
|
|
446
|
+
generator: string; // 'Vidora Motion Studio'
|
|
447
|
+
widgets: VidoraWidget[];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
interface VidoraWidget {
|
|
451
|
+
id: string; // уникальный id и id Remotion-композиции
|
|
452
|
+
name: string;
|
|
453
|
+
category: string;
|
|
454
|
+
description: string;
|
|
455
|
+
import_path: string; // '../widgets'
|
|
456
|
+
is_custom: boolean;
|
|
457
|
+
props: VidoraProp[];
|
|
458
|
+
default_props: Record<string, unknown>;
|
|
459
|
+
example_snippet: string;
|
|
460
|
+
tags: string[];
|
|
461
|
+
tsx_code: string; // исходник React-компонента
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
interface VidoraProp {
|
|
465
|
+
name: string;
|
|
466
|
+
type: 'string' | 'number' | 'boolean' | 'enum' | 'object';
|
|
467
|
+
required: boolean;
|
|
468
|
+
default: unknown;
|
|
469
|
+
enum_values?: string[];
|
|
470
|
+
description: string;
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 13.2 Контракт рендера виджета
|
|
475
|
+
|
|
476
|
+
- `tsx_code` компилируется `compileTsx` и выполняется `executeComponent`. Виджеты экспортируют **именованный** экспорт (`export const WordByWordText16x9`), поэтому evaluator возвращает первый именованный экспорт.
|
|
477
|
+
- `default_props` прокидываются в компонент как React-пропсы. Любой проп из `props[]` можно переопределить.
|
|
478
|
+
- Рекомендуемая обёртка: `AbsoluteFill` для центрирования + `<style>` с Tailwind CSS, скомпилированным из `tsx_code`.
|
|
479
|
+
|
|
480
|
+
### 13.3 Приоритет источников контента (Logo Shine Badge)
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
imageUrl > iconName > logoText
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- `imageUrl` непустой → рендерится `<img src={imageUrl}>`, текст/иконка игнорируются.
|
|
487
|
+
- иначе `iconName` непустой → `LucideIcons[iconName]`.
|
|
488
|
+
- иначе → текст `logoText`.
|
|
489
|
+
|
|
490
|
+
### 13.4 Разбор изображений
|
|
491
|
+
|
|
492
|
+
`imageUrl` должен быть валидным источником для `<img>`:
|
|
493
|
+
|
|
494
|
+
| Источник | Как получается | Где работает |
|
|
495
|
+
|---|---|---|
|
|
496
|
+
| `blob:` | `createAssetUrlMap` из ZIP | Браузер (клиентский рендер) |
|
|
497
|
+
| `data:` | inline base64 | Везде |
|
|
498
|
+
| `https://` | внешний URL | Везде (нужна сеть) |
|
|
499
|
+
| `asset:<path>` | токен харнесса → `staticFile(path)` | Серверный рендер Remotion |
|
|
500
|
+
|
|
501
|
+
Токен `asset:` — соглашение рендер-харнесса (§14): значение `asset:assets/logo.svg` резолвится в `staticFile('assets/logo.svg')`. Для гарантии, что произвольный `<img>` успеет загрузиться до снимка кадра, харнесс дополнительно монтирует скрытый Remotion `<Img>` с тем же `src`.
|
|
502
|
+
|
|
503
|
+
### 13.5 Проверка, что пропсы действительно работают
|
|
504
|
+
|
|
505
|
+
`render/props.e2e.test.ts`:
|
|
506
|
+
|
|
507
|
+
- структурно (без браузера) вызывает компонент с разными пропсами и обходит дерево React-элементов: проверяет `logoText`, `style.width` (`size`), `style.color`, `<img src>`, подстановку иконки и дефолты;
|
|
508
|
+
- рендерит PNG для вариантов `PropsDefault`, `PropsRedSmall`, `PropsGreenBig`, `PropsImage`, `PropsIcon` и проверяет, что файлы различаются.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 14. Рендер-харнесс (эталонная интеграция)
|
|
513
|
+
|
|
514
|
+
Каталог `render/` — не часть библиотеки, а эталонный потребитель песочницы.
|
|
515
|
+
|
|
516
|
+
| Скрипт | Вход | Выход |
|
|
517
|
+
|---|---|---|
|
|
518
|
+
| `npm run render` | встроенная сцена | `render/out/frame-30.png`, `sandbox.mp4` |
|
|
519
|
+
| `npm run render:assets` | ZIP (`render/public/assets`) | `asset-still.png`, `asset-video.mp4` |
|
|
520
|
+
| `npm run render:example` | `examples/remotion-scene.tsx` | `example-frame-*.png`, `example-map.mp4` |
|
|
521
|
+
| `npm run render:widgets` | `examples/vidora-widgets.json` | `widget-*.png`, `widget-*.mp4` |
|
|
522
|
+
| `npm run render:widgets:logo` | `examples/vidora-widgets-logo.json` | `widget-LogoShineBadge*.png` |
|
|
523
|
+
| `npm run render:props` | лого-каталог + вариации | `props-Props*.png` |
|
|
524
|
+
|
|
525
|
+
Механика:
|
|
526
|
+
|
|
527
|
+
1. Node-скрипт читает каталог/сцену, компилирует Tailwind из исходников и пишет сгенерированные модули в `render/.generated/`.
|
|
528
|
+
2. `render/entry-*.tsx` — точка входа Remotion: импортирует описание, компилирует/выполняет TSX песочницей, регистрирует `Composition` через `registerRoot`.
|
|
529
|
+
3. `@remotion/bundler` собирает бандл, `@remotion/renderer` рендерит `renderStill`/`renderMedia` в headless Chrome.
|
|
530
|
+
|
|
531
|
+
Переменная окружения `REMOTION_BROWSER` — путь к Chrome/Edge, если автопоиск не сработал (по умолчанию ищется `C:\Program Files\Google\Chrome\Application\chrome.exe`).
|
|
532
|
+
|
|
533
|
+
`renderWidgets` принимает путь к каталогу вторым аргументом:
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
node render/render-widgets.mjs examples/vidora-widgets-logo.json
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 15. Модель безопасности
|
|
542
|
+
|
|
543
|
+
### Что даёт песочница
|
|
544
|
+
|
|
545
|
+
- Белый список модулей: `require` резолвит только то, что зарегистрировано в `ModuleRegistry`; всё прочее → `SecurityError`.
|
|
546
|
+
- Затенение опасных глобалов (`window`, `document`, `fetch`, `localStorage`, ...) — внутри функции они `undefined`.
|
|
547
|
+
- Ограниченная поверхность: код не получает доступ к замыканию модуля хоста, только к параметрам `new Function`.
|
|
548
|
+
|
|
549
|
+
### Чего песочница НЕ гарантирует
|
|
550
|
+
|
|
551
|
+
- `new Function` исполняет код в основной области JavaScript. Это **defense-in-depth**, а не жёсткий sandbox. Целенаправленная атака (например, через `Function`-конструктор/прототипы) теоретически возможна.
|
|
552
|
+
- Требуется CSP-разрешение `unsafe-eval` для `new Function`; при этом CSP не сможет запретить `eval` в самом сгенерированном коде.
|
|
553
|
+
- Для недоверенного кода рекомендуется дополнительная изоляция: `iframe` с другим origin, `Worker` или серверная валидация.
|
|
554
|
+
|
|
555
|
+
### Рекомендации
|
|
556
|
+
|
|
557
|
+
- Всегда предрегистрируйте `react` (и все доверенные библиотеки) в `initialRegistry`.
|
|
558
|
+
- Не храните секреты в `SandboxGlobals`, которые становятся параметрами `new Function`.
|
|
559
|
+
- Ограничивайте внешние URL CDN, если нужен контроль (через собственный `importer`).
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## 16. Тестирование
|
|
564
|
+
|
|
565
|
+
| Команда | Что проверяет |
|
|
566
|
+
|---|---|
|
|
567
|
+
| `npm test` | Юнит-тесты (77 тестов): analyzer, transform, cache, loader, scope, evaluator, errors, zip, facade, `useLiveSandbox` |
|
|
568
|
+
| `npm run test:e2e` | E2E: реальный рендер через Remotion + Chrome (без ассетов, с ZIP, пример, виджеты, пропсы) |
|
|
569
|
+
| `npm run verify:video` | Проверка выданного MP4: контейнер (`ftyp`/`moov`), кодек `avc1`, размеры, длительность, сверка с `ffprobe` |
|
|
570
|
+
| `npm run typecheck` | `tsc --noEmit` |
|
|
571
|
+
|
|
572
|
+
Юнит-тесты не требуют сети и браузера: `loader` тестируется через инъекцию `importer`, `zip` — через `fflate` и фейковую фабрику URL, React-хук — в jsdom.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## 17. Совместимость и версии
|
|
577
|
+
|
|
578
|
+
- **Node.js:** 18+ (e2e проверен на Node 24).
|
|
579
|
+
- **React:** 17/18/19 (peer `>=17`; эталонные тесты — 18).
|
|
580
|
+
- **Браузеры:** любые с поддержкой `new Function`, `URL.createObjectURL`, динамического `import()`.
|
|
581
|
+
- **Схема каталога:** `vidora_schema_version: "1.0"`.
|
|
582
|
+
- **Remotion (харнесс):** 4.x.
|
|
583
|
+
|
|
584
|
+
### Изменения поведения относительно чернового плана
|
|
585
|
+
|
|
586
|
+
| Место | Было | Стало | Причина |
|
|
587
|
+
|---|---|---|---|
|
|
588
|
+
| `analyzer` | regex захватывал 1 символ | `([^'"]+)` + фильтр путей | баг: пакеты не находились |
|
|
589
|
+
| `loader` | без `__esModule` | `__esModule: true` | интероп Sucrase заворачивал модуль |
|
|
590
|
+
| `evaluator` | `SecurityError` заворачивался | пробрасывается как есть | сохранить тип ошибки |
|
|
591
|
+
| `evaluator` | fallback на `default` (мёртвый код) | первый именованный экспорт | поддержать `export const Scene` |
|
|
592
|
+
| `useLiveSandbox` | `localAssets` по ссылке в deps | сравнение по значению | бесконечный цикл рекомпиляции |
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 18. Сборка и публикация
|
|
597
|
+
|
|
598
|
+
### 18.1 Артефакты
|
|
599
|
+
|
|
600
|
+
`npm run build` (`tsup`) создаёт `dist/`:
|
|
601
|
+
|
|
602
|
+
| Файл | Формат |
|
|
603
|
+
|---|---|
|
|
604
|
+
| `dist/index.js` | ESM |
|
|
605
|
+
| `dist/index.cjs` | CommonJS |
|
|
606
|
+
| `dist/index.d.ts` / `dist/index.d.cts` | Типы |
|
|
607
|
+
| `*.map` | Source maps |
|
|
608
|
+
|
|
609
|
+
`package.json`:
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{
|
|
613
|
+
"type": "module",
|
|
614
|
+
"main": "./dist/index.cjs",
|
|
615
|
+
"module": "./dist/index.js",
|
|
616
|
+
"types": "./dist/index.d.ts",
|
|
617
|
+
"exports": {
|
|
618
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }
|
|
619
|
+
},
|
|
620
|
+
"files": ["dist", "README.md", "API.md"],
|
|
621
|
+
"sideEffects": false
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Внешние зависимости (не бандлятся): `react` (`peerDependencies`), `sucrase`, `fflate` (`dependencies`).
|
|
626
|
+
|
|
627
|
+
### 18.2 Скрипты
|
|
628
|
+
|
|
629
|
+
| Скрипт | Действие |
|
|
630
|
+
|---|---|
|
|
631
|
+
| `build` | `tsup` — сборка `dist/` |
|
|
632
|
+
| `prepack` | автосборка перед `npm pack`/`npm publish` |
|
|
633
|
+
| `prepublishOnly` | `npm run typecheck && npm test` |
|
|
634
|
+
| `pack:check` | `npm pack --dry-run` |
|
|
635
|
+
| `verify:video` | `node render/verify-video.mjs <file.mp4 \| dir>` |
|
|
636
|
+
|
|
637
|
+
### 18.3 Инструмент `verify-video`
|
|
638
|
+
|
|
639
|
+
`render/mp4-metadata.mjs` разбирает ISO-BMFF без внешних зависимостей и возвращает:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
interface Mp4Metadata {
|
|
643
|
+
majorBrand: string | null;
|
|
644
|
+
brands: string[];
|
|
645
|
+
hasMoov: boolean;
|
|
646
|
+
hasAvc1: boolean;
|
|
647
|
+
timescale: number | null;
|
|
648
|
+
duration: number | null;
|
|
649
|
+
durationSeconds: number | null;
|
|
650
|
+
width: number | null; // px (16.16 fixed-point)
|
|
651
|
+
height: number | null;
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
`render/verify-video.mjs <path>` печатает JSON-отчёт по каждому `.mp4` и завершается кодом `1`, если файл невалиден. Если `ffprobe` доступен, добавляется поле `ffprobe` с `codec`, `width`, `height`, `nbFrames`, `fps`, `durationSeconds`.
|
|
656
|
+
|
|
657
|
+
### 18.4 Игнорируемые артефакты
|
|
658
|
+
|
|
659
|
+
`.gitignore`: `node_modules/`, `dist/`, `coverage/`, `render/out/`, `render/bundle*/`, `render/.generated/`, `render/public/`, `*.log`, `*.tgz`.
|