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/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { transform } from 'sucrase';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useRef, useState, useEffect } from 'react';
|
|
4
|
+
import { unzipSync } from 'fflate';
|
|
5
|
+
|
|
6
|
+
// src/compiler/analyzer.ts
|
|
7
|
+
function extractBareImports(code) {
|
|
8
|
+
const importRegex = /import\s+(?:(?:\*\s+as\s+\w+|[\w\s{},]+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
9
|
+
const matches = [...code.matchAll(importRegex)];
|
|
10
|
+
const bareImports = matches.map((m) => m[1]).filter((specifier) => !specifier.startsWith(".") && !specifier.startsWith("/"));
|
|
11
|
+
return [...new Set(bareImports)];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/core/errors.ts
|
|
15
|
+
var CompilerError = class extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(`[Compiler Error]: ${message}`);
|
|
18
|
+
this.name = "CompilerError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var SecurityError = class extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(`[Security Violation]: ${message}`);
|
|
24
|
+
this.name = "SecurityError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var NetworkModuleError = class extends Error {
|
|
28
|
+
constructor(moduleName, originalMessage) {
|
|
29
|
+
super(`[Network Error]: \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u043F\u0430\u043A\u0435\u0442 "${moduleName}". ${originalMessage}`);
|
|
30
|
+
this.name = "NetworkModuleError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/compiler/transform.ts
|
|
35
|
+
function compileTsx(code) {
|
|
36
|
+
try {
|
|
37
|
+
const cleanCode = code.replace(/```tsx\n?/g, "").replace(/```\n?/g, "");
|
|
38
|
+
const compiled = transform(cleanCode, {
|
|
39
|
+
transforms: ["typescript", "jsx", "imports"],
|
|
40
|
+
jsxRuntime: "classic"
|
|
41
|
+
});
|
|
42
|
+
return compiled.code;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new CompilerError(error.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/library-manager/cache.ts
|
|
49
|
+
var ModuleCache = class {
|
|
50
|
+
constructor() {
|
|
51
|
+
this.registry = {};
|
|
52
|
+
}
|
|
53
|
+
register(name, module) {
|
|
54
|
+
this.registry[name] = module;
|
|
55
|
+
}
|
|
56
|
+
get(name) {
|
|
57
|
+
return this.registry[name];
|
|
58
|
+
}
|
|
59
|
+
getAll() {
|
|
60
|
+
return { ...this.registry };
|
|
61
|
+
}
|
|
62
|
+
has(name) {
|
|
63
|
+
return !!this.registry[name];
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/library-manager/loader.ts
|
|
68
|
+
var defaultImporter = (url) => import(
|
|
69
|
+
/* @vite-ignore */
|
|
70
|
+
url
|
|
71
|
+
);
|
|
72
|
+
async function loadMissingModules(packages, cache, importer = defaultImporter) {
|
|
73
|
+
const loadPromises = packages.map(async (pkg) => {
|
|
74
|
+
if (cache.has(pkg)) return;
|
|
75
|
+
try {
|
|
76
|
+
const module = await importer(`https://esm.sh/${pkg}`);
|
|
77
|
+
cache.register(pkg, { ...module, default: module.default || module, __esModule: true });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new NetworkModuleError(pkg, error.message);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
await Promise.all(loadPromises);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/sandbox/scope.ts
|
|
86
|
+
function getShadowedGlobals() {
|
|
87
|
+
const forbiddenKeys = [
|
|
88
|
+
"window",
|
|
89
|
+
"document",
|
|
90
|
+
"localStorage",
|
|
91
|
+
"sessionStorage",
|
|
92
|
+
"fetch",
|
|
93
|
+
"XMLHttpRequest",
|
|
94
|
+
"indexedDB",
|
|
95
|
+
"navigator"
|
|
96
|
+
];
|
|
97
|
+
const shadowValues = forbiddenKeys.map(() => void 0);
|
|
98
|
+
return { forbiddenKeys, shadowValues };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/sandbox/evaluator.ts
|
|
102
|
+
function executeComponent(compiledCode, registry, globals) {
|
|
103
|
+
const customRequire = (moduleName) => {
|
|
104
|
+
if (registry[moduleName]) return registry[moduleName];
|
|
105
|
+
throw new SecurityError(`\u041F\u043E\u043F\u044B\u0442\u043A\u0430 \u0438\u043C\u043F\u043E\u0440\u0442\u0430 \u043D\u0435\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043D\u043E\u0433\u043E \u043C\u043E\u0434\u0443\u043B\u044F: "${moduleName}"`);
|
|
106
|
+
};
|
|
107
|
+
const exportsObj = { default: null };
|
|
108
|
+
const React2 = registry["react"];
|
|
109
|
+
if (!React2) throw new Error("\u041C\u043E\u0434\u0443\u043B\u044C 'react' \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u043A\u043E\u043C\u043F\u0438\u043B\u044F\u0446\u0438\u0438 TSX.");
|
|
110
|
+
const { forbiddenKeys, shadowValues } = getShadowedGlobals();
|
|
111
|
+
const globalKeys = Object.keys(globals);
|
|
112
|
+
const globalValues = Object.values(globals);
|
|
113
|
+
try {
|
|
114
|
+
const fn = new Function(
|
|
115
|
+
"require",
|
|
116
|
+
"exports",
|
|
117
|
+
"React",
|
|
118
|
+
...globalKeys,
|
|
119
|
+
...forbiddenKeys,
|
|
120
|
+
compiledCode
|
|
121
|
+
);
|
|
122
|
+
fn(customRequire, exportsObj, React2, ...globalValues, ...shadowValues);
|
|
123
|
+
if (exportsObj.default) return exportsObj.default;
|
|
124
|
+
const namedExport = Object.keys(exportsObj).find((key) => key !== "default");
|
|
125
|
+
return namedExport ? exportsObj[namedExport] : null;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof SecurityError) throw error;
|
|
128
|
+
throw new Error(`[Runtime Error]: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/facade.ts
|
|
133
|
+
var SandboxFacade = class {
|
|
134
|
+
constructor(initialRegistry = {}, importer) {
|
|
135
|
+
this.cache = new ModuleCache();
|
|
136
|
+
this.assetsMap = {};
|
|
137
|
+
this.importer = importer;
|
|
138
|
+
Object.entries(initialRegistry).forEach(([name, module]) => {
|
|
139
|
+
this.cache.register(name, module);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
setAssets(assets) {
|
|
143
|
+
this.assetsMap = assets;
|
|
144
|
+
}
|
|
145
|
+
async compile(rawTsx) {
|
|
146
|
+
const start = performance.now();
|
|
147
|
+
try {
|
|
148
|
+
const requiredPackages = extractBareImports(rawTsx);
|
|
149
|
+
await loadMissingModules(requiredPackages, this.cache, this.importer);
|
|
150
|
+
const jsCode = compileTsx(rawTsx);
|
|
151
|
+
const globals = {
|
|
152
|
+
staticFile: (filename) => this.assetsMap[filename] || ""
|
|
153
|
+
};
|
|
154
|
+
const component = executeComponent(jsCode, this.cache.getAll(), globals);
|
|
155
|
+
return {
|
|
156
|
+
component,
|
|
157
|
+
error: null,
|
|
158
|
+
executionTimeMs: performance.now() - start
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
component: null,
|
|
163
|
+
error,
|
|
164
|
+
executionTimeMs: performance.now() - start
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function useLiveSandbox(code, initialModules, localAssets = {}) {
|
|
170
|
+
const facadeRef = useRef(null);
|
|
171
|
+
const assetsRef = useRef(localAssets);
|
|
172
|
+
assetsRef.current = localAssets;
|
|
173
|
+
const [Component, setComponent] = useState(null);
|
|
174
|
+
const [error, setError] = useState(null);
|
|
175
|
+
const [isCompiling, setIsCompiling] = useState(false);
|
|
176
|
+
if (!facadeRef.current) {
|
|
177
|
+
const modules = { react: React, ...initialModules };
|
|
178
|
+
facadeRef.current = new SandboxFacade(modules);
|
|
179
|
+
}
|
|
180
|
+
const assetsKey = JSON.stringify(localAssets);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!code) return;
|
|
183
|
+
let isMounted = true;
|
|
184
|
+
setIsCompiling(true);
|
|
185
|
+
facadeRef.current.setAssets(assetsRef.current);
|
|
186
|
+
facadeRef.current.compile(code).then((result) => {
|
|
187
|
+
if (!isMounted) return;
|
|
188
|
+
if (result.error) {
|
|
189
|
+
setError(result.error);
|
|
190
|
+
setComponent(null);
|
|
191
|
+
} else {
|
|
192
|
+
setError(null);
|
|
193
|
+
setComponent(() => result.component);
|
|
194
|
+
}
|
|
195
|
+
setIsCompiling(false);
|
|
196
|
+
}).catch((err) => {
|
|
197
|
+
if (!isMounted) return;
|
|
198
|
+
setError(err);
|
|
199
|
+
setComponent(null);
|
|
200
|
+
setIsCompiling(false);
|
|
201
|
+
});
|
|
202
|
+
return () => {
|
|
203
|
+
isMounted = false;
|
|
204
|
+
};
|
|
205
|
+
}, [code, assetsKey]);
|
|
206
|
+
return { Component, error, isCompiling };
|
|
207
|
+
}
|
|
208
|
+
var IGNORED_PREFIXES = ["__MACOSX/"];
|
|
209
|
+
function normalizePath(path) {
|
|
210
|
+
return path.replace(/^\.?\//, "");
|
|
211
|
+
}
|
|
212
|
+
function extractAssetZip(zip) {
|
|
213
|
+
const archive = {};
|
|
214
|
+
for (const [rawPath, bytes] of Object.entries(unzipSync(zip))) {
|
|
215
|
+
const path = normalizePath(rawPath);
|
|
216
|
+
if (path === "" || path.endsWith("/")) continue;
|
|
217
|
+
if (IGNORED_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
|
|
218
|
+
archive[path] = bytes;
|
|
219
|
+
}
|
|
220
|
+
return archive;
|
|
221
|
+
}
|
|
222
|
+
function createAssetUrlMap(archive, createUrl) {
|
|
223
|
+
const urls = {};
|
|
224
|
+
for (const [path, bytes] of Object.entries(archive)) {
|
|
225
|
+
const filename = path.split("/").pop();
|
|
226
|
+
urls[filename] = createUrl(bytes, filename);
|
|
227
|
+
}
|
|
228
|
+
return urls;
|
|
229
|
+
}
|
|
230
|
+
function releaseAssetUrls(urls, revoke) {
|
|
231
|
+
for (const url of Object.values(urls)) revoke(url);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { CompilerError, ModuleCache, NetworkModuleError, SandboxFacade, SecurityError, compileTsx, createAssetUrlMap, executeComponent, extractAssetZip, extractBareImports, getShadowedGlobals, loadMissingModules, releaseAssetUrls, useLiveSandbox };
|
|
235
|
+
//# sourceMappingURL=index.js.map
|
|
236
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compiler/analyzer.ts","../src/core/errors.ts","../src/compiler/transform.ts","../src/library-manager/cache.ts","../src/library-manager/loader.ts","../src/sandbox/scope.ts","../src/sandbox/evaluator.ts","../src/facade.ts","../src/react/useLiveSandbox.ts","../src/assets/zip.ts"],"names":["React"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Извлекает имена NPM-пакетов (bare imports) из сырого TSX кода.\n * Локальные (`./`, `../`) и абсолютные (`/`) импорты игнорируются.\n */\nexport function extractBareImports(code: string): string[] {\n const importRegex =\n /import\\s+(?:(?:\\*\\s+as\\s+\\w+|[\\w\\s{},]+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\n const matches = [...code.matchAll(importRegex)];\n\n const bareImports = matches\n .map((m) => m[1])\n .filter((specifier) => !specifier.startsWith('.') && !specifier.startsWith('/'));\n\n return [...new Set(bareImports)];\n}\n","export class CompilerError extends Error {\n constructor(message: string) {\n super(`[Compiler Error]: ${message}`);\n this.name = 'CompilerError';\n }\n}\n\nexport class SecurityError extends Error {\n constructor(message: string) {\n super(`[Security Violation]: ${message}`);\n this.name = 'SecurityError';\n }\n}\n\nexport class NetworkModuleError extends Error {\n constructor(moduleName: string, originalMessage: string) {\n super(`[Network Error]: Не удалось загрузить пакет \"${moduleName}\". ${originalMessage}`);\n this.name = 'NetworkModuleError';\n }\n}\n","import { transform } from 'sucrase';\nimport { CompilerError } from '../core/errors';\n\n/**\n * Транспилирует TSX в CommonJS (React.createElement).\n */\nexport function compileTsx(code: string): string {\n try {\n const cleanCode = code.replace(/```tsx\\n?/g, '').replace(/```\\n?/g, '');\n\n const compiled = transform(cleanCode, {\n transforms: ['typescript', 'jsx', 'imports'],\n jsxRuntime: 'classic',\n });\n\n return compiled.code;\n } catch (error) {\n throw new CompilerError((error as Error).message);\n }\n}\n","import { ModuleRegistry } from '../core/types';\n\n// Singleton-хранилище в памяти для кэширования библиотек\nexport class ModuleCache {\n private registry: ModuleRegistry = {};\n\n public register(name: string, module: any) {\n this.registry[name] = module;\n }\n\n public get(name: string): any | undefined {\n return this.registry[name];\n }\n\n public getAll(): ModuleRegistry {\n return { ...this.registry };\n }\n\n public has(name: string): boolean {\n return !!this.registry[name];\n }\n}\n","import { ModuleCache } from './cache';\nimport { NetworkModuleError } from '../core/errors';\n\nexport type ModuleImporter = (url: string) => Promise<any>;\n\nconst defaultImporter: ModuleImporter = (url) => import(/* @vite-ignore */ url);\n\n/**\n * Асинхронно подгружает отсутствующие пакеты через CDN.\n *\n * `importer` — точка внедрения (dependency injection) для тестов и\n * замены CDN-провайдера без изменения логики кэша.\n */\nexport async function loadMissingModules(\n packages: string[],\n cache: ModuleCache,\n importer: ModuleImporter = defaultImporter,\n): Promise<void> {\n const loadPromises = packages.map(async (pkg) => {\n if (cache.has(pkg)) return; // Уже в кэше (или это встроенная либа)\n\n try {\n const module = await importer(`https://esm.sh/${pkg}`);\n // Адаптируем ESM в формат, понятный CommonJS-require.\n // `__esModule` нужен, чтобы Sucrase-интероп `_interopRequireDefault`\n // не заворачивал модуль повторно и `default` резолвился напрямую.\n cache.register(pkg, { ...module, default: module.default || module, __esModule: true });\n } catch (error) {\n throw new NetworkModuleError(pkg, (error as Error).message);\n }\n });\n\n await Promise.all(loadPromises);\n}\n","/**\n * Возвращает массивы ключей и значений для \"затенения\" (Shadowing).\n * Переопределяет критические браузерные API в undefined.\n */\nexport function getShadowedGlobals() {\n const forbiddenKeys = [\n 'window',\n 'document',\n 'localStorage',\n 'sessionStorage',\n 'fetch',\n 'XMLHttpRequest',\n 'indexedDB',\n 'navigator',\n ];\n\n const shadowValues = forbiddenKeys.map(() => undefined);\n\n return { forbiddenKeys, shadowValues };\n}\n","import { ModuleRegistry, SandboxGlobals } from '../core/types';\nimport { getShadowedGlobals } from './scope';\nimport { SecurityError } from '../core/errors';\n\nexport function executeComponent(\n compiledCode: string,\n registry: ModuleRegistry,\n globals: SandboxGlobals,\n): any {\n const customRequire = (moduleName: string) => {\n if (registry[moduleName]) return registry[moduleName];\n throw new SecurityError(`Попытка импорта неразрешенного модуля: \"${moduleName}\"`);\n };\n\n const exportsObj = { default: null };\n const React = registry['react'];\n\n if (!React) throw new Error(\"Модуль 'react' обязателен для компиляции TSX.\");\n\n const { forbiddenKeys, shadowValues } = getShadowedGlobals();\n const globalKeys = Object.keys(globals);\n const globalValues = Object.values(globals);\n\n try {\n const fn = new Function(\n 'require',\n 'exports',\n 'React',\n ...globalKeys,\n ...forbiddenKeys,\n compiledCode,\n );\n\n fn(customRequire, exportsObj, React, ...globalValues, ...shadowValues);\n\n // Приоритет `default`, иначе — первый именованный экспорт (например, `export const Scene`).\n if (exportsObj.default) return exportsObj.default;\n\n const namedExport = Object.keys(exportsObj).find((key) => key !== 'default');\n return namedExport ? (exportsObj as any)[namedExport] : null;\n } catch (error) {\n // Не заворачиваем наши собственные ошибки безопасности в generic Runtime Error.\n if (error instanceof SecurityError) throw error;\n throw new Error(`[Runtime Error]: ${(error as Error).message}`);\n }\n}\n","import { ModuleRegistry, SandboxGlobals, EvaluationResult } from './core/types';\nimport { extractBareImports } from './compiler/analyzer';\nimport { compileTsx } from './compiler/transform';\nimport { ModuleCache } from './library-manager/cache';\nimport { loadMissingModules, ModuleImporter } from './library-manager/loader';\nimport { executeComponent } from './sandbox/evaluator';\n\nexport class SandboxFacade {\n private cache: ModuleCache;\n private assetsMap: Record<string, string>;\n private importer?: ModuleImporter;\n\n constructor(initialRegistry: ModuleRegistry = {}, importer?: ModuleImporter) {\n this.cache = new ModuleCache();\n this.assetsMap = {};\n this.importer = importer;\n\n // Предварительно регистрируем базовые библиотеки (React, Remotion)\n Object.entries(initialRegistry).forEach(([name, module]) => {\n this.cache.register(name, module);\n });\n }\n\n public setAssets(assets: Record<string, string>) {\n this.assetsMap = assets;\n }\n\n public async compile(rawTsx: string): Promise<EvaluationResult> {\n const start = performance.now();\n\n try {\n // 1. Анализ\n const requiredPackages = extractBareImports(rawTsx);\n\n // 2. Загрузка зависимостей (NPM/CDN)\n await loadMissingModules(requiredPackages, this.cache, this.importer);\n\n // 3. Транспиляция\n const jsCode = compileTsx(rawTsx);\n\n // 4. Подготовка глобальной области видимости\n const globals: SandboxGlobals = {\n staticFile: (filename: string) => this.assetsMap[filename] || '',\n };\n\n // 5. Выполнение\n const component = executeComponent(jsCode, this.cache.getAll(), globals);\n\n return {\n component,\n error: null,\n executionTimeMs: performance.now() - start,\n };\n } catch (error) {\n return {\n component: null,\n error: error as Error,\n executionTimeMs: performance.now() - start,\n };\n }\n }\n}\n","import { useState, useEffect, useRef } from 'react';\nimport { SandboxFacade } from '../facade';\nimport { ModuleRegistry } from '../core/types';\nimport * as React from 'react';\n\n/**\n * Готовый React-хук для бесшовной интеграции песочницы.\n *\n * Внимание: `localAssets` не должен попадать в зависимости эффекта по ссылке —\n * иначе новый объект на каждый рендер запускает бесконечный цикл рекомпиляции.\n * Поэтому мы сравниваем сериализованный ключ ассетов.\n */\nexport function useLiveSandbox(\n code: string,\n initialModules: ModuleRegistry,\n localAssets: Record<string, string> = {},\n) {\n const facadeRef = useRef<SandboxFacade | null>(null);\n const assetsRef = useRef(localAssets);\n assetsRef.current = localAssets;\n\n const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [isCompiling, setIsCompiling] = useState(false);\n\n // Инициализация фасада (единожды)\n if (!facadeRef.current) {\n // Автоматически добавляем React, если его забыли\n const modules = { react: React, ...initialModules };\n facadeRef.current = new SandboxFacade(modules);\n }\n\n const assetsKey = JSON.stringify(localAssets);\n\n useEffect(() => {\n if (!code) return;\n\n let isMounted = true;\n setIsCompiling(true);\n\n facadeRef.current!.setAssets(assetsRef.current);\n\n facadeRef.current!\n .compile(code)\n .then((result) => {\n if (!isMounted) return;\n\n if (result.error) {\n setError(result.error);\n setComponent(null);\n } else {\n setError(null);\n setComponent(() => result.component);\n }\n setIsCompiling(false);\n })\n .catch((err: Error) => {\n if (!isMounted) return;\n setError(err);\n setComponent(null);\n setIsCompiling(false);\n });\n\n return () => {\n isMounted = false;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [code, assetsKey]);\n\n return { Component, error, isCompiling };\n}\n","import { unzipSync } from 'fflate';\n\n/**\n * Распакованный архив ресурсов: путь внутри архива -> сырые байты.\n */\nexport type AssetArchive = Record<string, Uint8Array>;\n\nconst IGNORED_PREFIXES = ['__MACOSX/'];\n\nfunction normalizePath(path: string): string {\n return path.replace(/^\\.?\\//, '');\n}\n\n/**\n * Распаковывает ZIP с ассетами (медиа, шрифты, JSON) в плоскую карту байтов.\n * Служебные записи каталогов и системные папки macOS отбрасываются.\n */\nexport function extractAssetZip(zip: Uint8Array): AssetArchive {\n const archive: AssetArchive = {};\n\n for (const [rawPath, bytes] of Object.entries(unzipSync(zip))) {\n const path = normalizePath(rawPath);\n if (path === '' || path.endsWith('/')) continue;\n if (IGNORED_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;\n archive[path] = bytes;\n }\n\n return archive;\n}\n\n/**\n * Превращает архив в карту `filename -> url`, понятную `SandboxFacade.setAssets`\n * и глобальной функции `staticFile(filename)`.\n *\n * Фабрика URL внедряется, чтобы функцию можно было тестировать без DOM:\n * в браузере передаётся `URL.createObjectURL`.\n */\nexport function createAssetUrlMap(\n archive: AssetArchive,\n createUrl: (bytes: Uint8Array, filename: string) => string,\n): Record<string, string> {\n const urls: Record<string, string> = {};\n\n for (const [path, bytes] of Object.entries(archive)) {\n const filename = path.split('/').pop()!;\n urls[filename] = createUrl(bytes, filename);\n }\n\n return urls;\n}\n\n/**\n * Освобождает ранее созданные `blob:`-ссылки, чтобы не текла память.\n */\nexport function releaseAssetUrls(\n urls: Record<string, string>,\n revoke: (url: string) => void,\n): void {\n for (const url of Object.values(urls)) revoke(url);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browser-tsx-sandbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pure client-side TSX compiler and sandbox evaluator with dynamic NPM library loading.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"API.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"tsx",
|
|
26
|
+
"sandbox",
|
|
27
|
+
"sucrase",
|
|
28
|
+
"react",
|
|
29
|
+
"remotion",
|
|
30
|
+
"esm.sh",
|
|
31
|
+
"browser",
|
|
32
|
+
"compiler"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"prepack": "npm run build",
|
|
37
|
+
"prepublishOnly": "npm run typecheck && npm test",
|
|
38
|
+
"pack:check": "npm pack --dry-run",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
42
|
+
"render": "node render/render.mjs",
|
|
43
|
+
"render:assets": "node render/render-assets.mjs",
|
|
44
|
+
"render:example": "node render/render-example.mjs",
|
|
45
|
+
"render:widgets": "node render/render-widgets.mjs",
|
|
46
|
+
"render:widgets:logo": "node render/render-widgets.mjs examples/vidora-widgets-logo.json",
|
|
47
|
+
"render:props": "node render/render-props.mjs",
|
|
48
|
+
"verify:video": "node render/verify-video.mjs",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"fflate": "^0.8.3",
|
|
53
|
+
"sucrase": "^3.35.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">=17"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@remotion/bundler": "^4.0.524",
|
|
60
|
+
"@remotion/renderer": "^4.0.524",
|
|
61
|
+
"@testing-library/dom": "^10.4.0",
|
|
62
|
+
"@testing-library/react": "^16.0.1",
|
|
63
|
+
"@types/node": "^26.5.1",
|
|
64
|
+
"@types/react": "^18.3.12",
|
|
65
|
+
"@types/react-dom": "^18.3.1",
|
|
66
|
+
"jsdom": "^25.0.1",
|
|
67
|
+
"lucide-react": "^1.45.0",
|
|
68
|
+
"postcss": "^8.5.28",
|
|
69
|
+
"react": "^18.3.1",
|
|
70
|
+
"react-dom": "^18.3.1",
|
|
71
|
+
"remotion": "^4.0.524",
|
|
72
|
+
"tailwindcss": "^3.4.19",
|
|
73
|
+
"tsup": "^8.5.1",
|
|
74
|
+
"typescript": "^5.6.3",
|
|
75
|
+
"vitest": "^2.1.8"
|
|
76
|
+
}
|
|
77
|
+
}
|