diorama-js 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/dist/vue.js ADDED
@@ -0,0 +1,2324 @@
1
+ import { defineComponent, ref, onMounted, onUnmounted, watch, h } from 'vue';
2
+
3
+ // src/adapters/vue.ts
4
+
5
+ // src/errors.ts
6
+ var DioramaError = class extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "DioramaError";
11
+ this.code = code;
12
+ Object.setPrototypeOf(this, new.target.prototype);
13
+ }
14
+ };
15
+ var RepoNotFoundError = class extends DioramaError {
16
+ constructor(owner, repo) {
17
+ super(
18
+ "REPO_NOT_FOUND",
19
+ `Repository ${owner}/${repo} not found. Make sure it's a public repository.`
20
+ );
21
+ this.name = "RepoNotFoundError";
22
+ }
23
+ };
24
+ var BranchNotFoundError = class extends DioramaError {
25
+ constructor(owner, repo, branch) {
26
+ super(
27
+ "BRANCH_NOT_FOUND",
28
+ `Branch '${branch}' not found in ${owner}/${repo}.`
29
+ );
30
+ this.name = "BranchNotFoundError";
31
+ }
32
+ };
33
+ var RateLimitError = class extends DioramaError {
34
+ resetAt;
35
+ constructor(resetTimestamp) {
36
+ const resetAt = resetTimestamp ? new Date(resetTimestamp * 1e3) : null;
37
+ const resetMsg = resetAt ? ` Wait until ${resetAt.toLocaleTimeString()} or pass a GitHub token to increase the limit.` : " Pass a GitHub token to increase the limit, or wait and try again.";
38
+ super("RATE_LIMIT", `GitHub API rate limit exceeded.${resetMsg}`);
39
+ this.name = "RateLimitError";
40
+ this.resetAt = resetAt;
41
+ }
42
+ };
43
+ var NetworkError = class extends DioramaError {
44
+ constructor(message = "A network error occurred. Check your connection and try again.") {
45
+ super("NETWORK_ERROR", message);
46
+ this.name = "NetworkError";
47
+ }
48
+ };
49
+ var NodeBuiltinError = class extends DioramaError {
50
+ moduleName;
51
+ constructor(moduleName) {
52
+ super(
53
+ "NODE_BUILTIN",
54
+ `This project imports '${moduleName}' which is a Node.js built-in. Diorama only supports browser-compatible projects.`
55
+ );
56
+ this.name = "NodeBuiltinError";
57
+ this.moduleName = moduleName;
58
+ }
59
+ };
60
+ var TranspileError = class extends DioramaError {
61
+ file;
62
+ line;
63
+ column;
64
+ constructor(message, file, line, column) {
65
+ super("TRANSPILE_FAILED", message);
66
+ this.name = "TranspileError";
67
+ this.file = file;
68
+ this.line = line;
69
+ this.column = column;
70
+ }
71
+ };
72
+ var TranspilerLoadError = class extends DioramaError {
73
+ constructor(message = "Failed to load the transpiler. Check your network connection.") {
74
+ super("TRANSPILER_LOAD_FAILED", message);
75
+ this.name = "TranspilerLoadError";
76
+ }
77
+ };
78
+ var AssemblyError = class extends DioramaError {
79
+ constructor(message) {
80
+ super("ASSEMBLY_FAILED", message);
81
+ this.name = "AssemblyError";
82
+ }
83
+ };
84
+ var EntryPointError = class extends DioramaError {
85
+ constructor(message = "No entry point found. Diorama looks for index.html, src/index.jsx, src/index.tsx, or src/index.js.") {
86
+ super("NO_ENTRY_POINT", message);
87
+ this.name = "EntryPointError";
88
+ }
89
+ };
90
+
91
+ // src/core/resolver.ts
92
+ var GITHUB_API = "https://api.github.com";
93
+ var RAW_BASE = "https://raw.githubusercontent.com";
94
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
95
+ ".png",
96
+ ".jpg",
97
+ ".jpeg",
98
+ ".gif",
99
+ ".ico",
100
+ ".webp",
101
+ ".woff",
102
+ ".woff2",
103
+ ".ttf",
104
+ ".eot",
105
+ ".otf",
106
+ ".mp3",
107
+ ".mp4",
108
+ ".ogg",
109
+ ".wav",
110
+ ".webm",
111
+ ".pdf",
112
+ ".zip"
113
+ ]);
114
+ var DEFAULT_SKIP_DIRS = [
115
+ "node_modules/",
116
+ ".git/",
117
+ ".github/",
118
+ "__tests__/"
119
+ ];
120
+ var DEFAULT_SKIP_FILES = [
121
+ "README.md",
122
+ "LICENSE",
123
+ "LICENSE.md",
124
+ ".gitignore",
125
+ ".env",
126
+ ".env.local",
127
+ ".env.production",
128
+ "package-lock.json",
129
+ "yarn.lock",
130
+ "pnpm-lock.yaml"
131
+ ];
132
+ var DEFAULT_SKIP_PATTERNS = [
133
+ /\.test\.\w+$/,
134
+ /\.spec\.\w+$/
135
+ ];
136
+ function parseGitHubURL(input) {
137
+ let cleaned = input.trim();
138
+ cleaned = cleaned.replace(/^https?:\/\//, "").replace(/^www\./, "");
139
+ cleaned = cleaned.replace(/^github\.com\//, "");
140
+ cleaned = cleaned.replace(/\/+$/, "");
141
+ const parts = cleaned.split("/");
142
+ if (parts.length < 2 || !parts[0] || !parts[1]) {
143
+ throw new RepoNotFoundError(parts[0] ?? "", parts[1] ?? "");
144
+ }
145
+ const owner = parts[0];
146
+ const repo = parts[1];
147
+ if (parts.length === 2) {
148
+ return { owner, repo };
149
+ }
150
+ if (parts[2] === "tree" && parts.length >= 4) {
151
+ const branch = parts[3];
152
+ const subdirectory = parts.length > 4 ? parts.slice(4).join("/") : void 0;
153
+ return { owner, repo, branch, subdirectory };
154
+ }
155
+ return { owner, repo };
156
+ }
157
+ var Semaphore = class {
158
+ constructor(concurrency) {
159
+ this.concurrency = concurrency;
160
+ }
161
+ queue = [];
162
+ active = 0;
163
+ async acquire() {
164
+ if (this.active < this.concurrency) {
165
+ this.active++;
166
+ return;
167
+ }
168
+ return new Promise((resolve) => {
169
+ this.queue.push(() => {
170
+ this.active++;
171
+ resolve();
172
+ });
173
+ });
174
+ }
175
+ release() {
176
+ this.active--;
177
+ const next = this.queue.shift();
178
+ if (next) next();
179
+ }
180
+ };
181
+ function extensionOf(path) {
182
+ const dot = path.lastIndexOf(".");
183
+ return dot === -1 ? "" : path.slice(dot).toLowerCase();
184
+ }
185
+ function isBinary(path) {
186
+ return BINARY_EXTENSIONS.has(extensionOf(path));
187
+ }
188
+ function shouldSkip(path, extraExclude) {
189
+ for (const dir of DEFAULT_SKIP_DIRS) {
190
+ if (path.startsWith(dir) || path.includes(`/${dir}`)) return true;
191
+ }
192
+ for (const file of DEFAULT_SKIP_FILES) {
193
+ if (path === file) return true;
194
+ }
195
+ for (const pattern of DEFAULT_SKIP_PATTERNS) {
196
+ if (pattern.test(path)) return true;
197
+ }
198
+ if (extraExclude) {
199
+ for (const ex of extraExclude) {
200
+ if (path.includes(ex)) return true;
201
+ }
202
+ }
203
+ return false;
204
+ }
205
+ function authHeaders(token) {
206
+ const headers = {
207
+ Accept: "application/vnd.github.v3+json"
208
+ };
209
+ if (token) {
210
+ headers["Authorization"] = `token ${token}`;
211
+ }
212
+ return headers;
213
+ }
214
+ async function resolveProject(urlOrShorthand, options = {}) {
215
+ const parsed = parseGitHubURL(urlOrShorthand);
216
+ const { owner, repo } = parsed;
217
+ const branch = options.branch ?? parsed.branch;
218
+ const subdirectory = options.subdirectory ?? parsed.subdirectory;
219
+ const token = options.githubToken;
220
+ const concurrency = options.maxConcurrentFetches ?? 6;
221
+ const resolvedBranch = branch ?? await resolveDefaultBranch(owner, repo, token);
222
+ const tree = await fetchTree(owner, repo, resolvedBranch, token);
223
+ let paths = tree.paths.filter((p) => !shouldSkip(p, options.exclude));
224
+ if (subdirectory) {
225
+ const prefix = subdirectory.endsWith("/") ? subdirectory : `${subdirectory}/`;
226
+ paths = paths.filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length));
227
+ }
228
+ const sem = new Semaphore(concurrency);
229
+ const files = /* @__PURE__ */ new Map();
230
+ const binaryFiles = /* @__PURE__ */ new Map();
231
+ const fetchJobs = paths.map(async (relativePath) => {
232
+ await sem.acquire();
233
+ try {
234
+ const rawPath = subdirectory ? `${subdirectory}/${relativePath}` : relativePath;
235
+ const url = `${RAW_BASE}/${owner}/${repo}/${resolvedBranch}/${rawPath}`;
236
+ if (isBinary(relativePath)) {
237
+ const res = await fetch(url);
238
+ if (!res.ok) return;
239
+ const blob = await res.blob();
240
+ binaryFiles.set(relativePath, blob);
241
+ } else {
242
+ const res = await fetch(url);
243
+ if (!res.ok) return;
244
+ const text = await res.text();
245
+ files.set(relativePath, text);
246
+ }
247
+ } catch {
248
+ } finally {
249
+ sem.release();
250
+ }
251
+ });
252
+ await Promise.all(fetchJobs);
253
+ return {
254
+ owner,
255
+ repo,
256
+ branch: resolvedBranch,
257
+ sha: tree.sha,
258
+ files,
259
+ binaryFiles
260
+ };
261
+ }
262
+ async function resolveDefaultBranch(owner, repo, token) {
263
+ const url = `${GITHUB_API}/repos/${owner}/${repo}`;
264
+ const res = await safeFetch(url, authHeaders(token));
265
+ if (res.status === 404) {
266
+ throw new RepoNotFoundError(owner, repo);
267
+ }
268
+ handleRateLimit(res);
269
+ if (!res.ok) {
270
+ throw new NetworkError(`GitHub API returned ${res.status} for ${url}`);
271
+ }
272
+ const data = await res.json();
273
+ return data.default_branch;
274
+ }
275
+ async function fetchTree(owner, repo, branch, token) {
276
+ const url = `${GITHUB_API}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
277
+ const res = await safeFetch(url, authHeaders(token));
278
+ if (res.status === 404) {
279
+ throw new BranchNotFoundError(owner, repo, branch);
280
+ }
281
+ handleRateLimit(res);
282
+ if (!res.ok) {
283
+ throw new NetworkError(`GitHub API returned ${res.status} fetching tree for ${owner}/${repo}`);
284
+ }
285
+ const data = await res.json();
286
+ const paths = data.tree.filter((entry) => entry.type === "blob").map((entry) => entry.path);
287
+ return { sha: data.sha, paths };
288
+ }
289
+ function handleRateLimit(res) {
290
+ if (res.status === 403 || res.status === 429) {
291
+ const resetHeader = res.headers.get("x-ratelimit-reset");
292
+ const remaining = res.headers.get("x-ratelimit-remaining");
293
+ if (remaining === "0" || res.status === 429) {
294
+ throw new RateLimitError(
295
+ resetHeader ? parseInt(resetHeader, 10) : void 0
296
+ );
297
+ }
298
+ }
299
+ }
300
+ async function safeFetch(url, headers) {
301
+ try {
302
+ return await fetch(url, { headers });
303
+ } catch (err) {
304
+ throw new NetworkError(
305
+ `Network request failed for ${url}: ${err instanceof Error ? err.message : String(err)}`
306
+ );
307
+ }
308
+ }
309
+
310
+ // src/core/cache.ts
311
+ var CACHE_PREFIX = "diorama:";
312
+ var CACHE_META_KEY = "diorama:__meta__";
313
+ var CacheManager = class {
314
+ enabled;
315
+ ttl;
316
+ _strategy;
317
+ constructor(options = {}) {
318
+ this.enabled = options.enabled ?? true;
319
+ this.ttl = (options.ttl ?? 3600) * 1e3;
320
+ this._strategy = options.strategy ?? "normal";
321
+ }
322
+ // ─── Public API ────────────────────────────────────────────
323
+ /**
324
+ * Try to read a cached project.
325
+ * Returns `null` on miss, expiry, or if caching is disabled.
326
+ */
327
+ get(owner, repo, sha) {
328
+ if (!this.enabled) return null;
329
+ try {
330
+ const key = this.cacheKey(owner, repo);
331
+ const raw = localStorage.getItem(key);
332
+ if (!raw) return null;
333
+ const entry = JSON.parse(raw);
334
+ if (entry.sha !== sha) return null;
335
+ if (Date.now() - entry.timestamp > this.ttl) return null;
336
+ return this.deserialize(entry);
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+ /**
342
+ * For aggressive strategy: check if we have a recently-cached SHA
343
+ * so we can skip even the tree API call.
344
+ */
345
+ getCachedSHA(owner, repo) {
346
+ if (!this.enabled || this._strategy !== "aggressive") return null;
347
+ try {
348
+ const key = this.cacheKey(owner, repo);
349
+ const raw = localStorage.getItem(key);
350
+ if (!raw) return null;
351
+ const entry = JSON.parse(raw);
352
+ const aggressiveTTL = 5 * 60 * 1e3;
353
+ if (Date.now() - entry.timestamp > aggressiveTTL) return null;
354
+ return entry.sha;
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ /**
360
+ * Store a resolved project in cache.
361
+ * Silently handles quota errors by evicting old entries.
362
+ */
363
+ async set(project) {
364
+ if (!this.enabled) return;
365
+ const entry = await this.serialize(project);
366
+ const key = this.cacheKey(project.owner, project.repo);
367
+ try {
368
+ this.writeEntry(key, entry, project.owner, project.repo);
369
+ } catch (err) {
370
+ if (isQuotaError(err)) {
371
+ this.evictOldest();
372
+ try {
373
+ this.writeEntry(key, entry, project.owner, project.repo);
374
+ } catch {
375
+ console.warn("[Diorama] Cache quota exceeded; skipping cache write.");
376
+ }
377
+ }
378
+ }
379
+ }
380
+ /** Clear cache for a specific repo or all cached projects. */
381
+ clear(repoSlug) {
382
+ if (repoSlug) {
383
+ const key = `${CACHE_PREFIX}${repoSlug}`;
384
+ localStorage.removeItem(key);
385
+ this.removeFromMeta(repoSlug);
386
+ } else {
387
+ const meta = this.getMeta();
388
+ for (const slug of Object.keys(meta.entries)) {
389
+ localStorage.removeItem(`${CACHE_PREFIX}${slug}`);
390
+ }
391
+ localStorage.removeItem(CACHE_META_KEY);
392
+ }
393
+ }
394
+ // ─── Internal helpers ──────────────────────────────────────
395
+ cacheKey(owner, repo) {
396
+ return `${CACHE_PREFIX}${owner}/${repo}`;
397
+ }
398
+ async serialize(project) {
399
+ const files = {};
400
+ for (const [path, content] of project.files) {
401
+ files[path] = content;
402
+ }
403
+ const binaryFiles = {};
404
+ for (const [path, blob] of project.binaryFiles) {
405
+ binaryFiles[path] = await blobToBase64(blob);
406
+ }
407
+ return {
408
+ sha: project.sha,
409
+ owner: project.owner,
410
+ repo: project.repo,
411
+ branch: project.branch,
412
+ files,
413
+ binaryFiles,
414
+ timestamp: Date.now()
415
+ };
416
+ }
417
+ deserialize(entry) {
418
+ const files = /* @__PURE__ */ new Map();
419
+ for (const [path, content] of Object.entries(entry.files)) {
420
+ files.set(path, content);
421
+ }
422
+ const binaryFiles = /* @__PURE__ */ new Map();
423
+ for (const [path, dataURL] of Object.entries(entry.binaryFiles)) {
424
+ binaryFiles.set(path, base64ToBlob(dataURL));
425
+ }
426
+ return {
427
+ owner: entry.owner,
428
+ repo: entry.repo,
429
+ branch: entry.branch,
430
+ sha: entry.sha,
431
+ files,
432
+ binaryFiles
433
+ };
434
+ }
435
+ writeEntry(key, entry, owner, repo) {
436
+ localStorage.setItem(key, JSON.stringify(entry));
437
+ this.updateMeta(`${owner}/${repo}`);
438
+ }
439
+ getMeta() {
440
+ try {
441
+ const raw = localStorage.getItem(CACHE_META_KEY);
442
+ return raw ? JSON.parse(raw) : { entries: {} };
443
+ } catch {
444
+ return { entries: {} };
445
+ }
446
+ }
447
+ updateMeta(slug) {
448
+ const meta = this.getMeta();
449
+ meta.entries[slug] = Date.now();
450
+ localStorage.setItem(CACHE_META_KEY, JSON.stringify(meta));
451
+ }
452
+ removeFromMeta(slug) {
453
+ const meta = this.getMeta();
454
+ delete meta.entries[slug];
455
+ localStorage.setItem(CACHE_META_KEY, JSON.stringify(meta));
456
+ }
457
+ evictOldest() {
458
+ const meta = this.getMeta();
459
+ const entries = Object.entries(meta.entries);
460
+ if (entries.length === 0) return;
461
+ entries.sort((a, b) => a[1] - b[1]);
462
+ const [oldestSlug] = entries[0];
463
+ localStorage.removeItem(`${CACHE_PREFIX}${oldestSlug}`);
464
+ delete meta.entries[oldestSlug];
465
+ localStorage.setItem(CACHE_META_KEY, JSON.stringify(meta));
466
+ }
467
+ };
468
+ function blobToBase64(blob) {
469
+ return new Promise((resolve, reject) => {
470
+ const reader = new FileReader();
471
+ reader.onloadend = () => resolve(reader.result);
472
+ reader.onerror = reject;
473
+ reader.readAsDataURL(blob);
474
+ });
475
+ }
476
+ function base64ToBlob(dataURL) {
477
+ const [header, data] = dataURL.split(",");
478
+ const mime = header.match(/:(.*?);/)?.[1] ?? "application/octet-stream";
479
+ const binary = atob(data);
480
+ const bytes = new Uint8Array(binary.length);
481
+ for (let i = 0; i < binary.length; i++) {
482
+ bytes[i] = binary.charCodeAt(i);
483
+ }
484
+ return new Blob([bytes], { type: mime });
485
+ }
486
+ function isQuotaError(err) {
487
+ return err instanceof DOMException && (err.name === "QuotaExceededError" || err.code === 22 || err.code === 1014);
488
+ }
489
+
490
+ // src/core/analyzer.ts
491
+ var HTML_ENTRY_CANDIDATES = [
492
+ "index.html",
493
+ "public/index.html",
494
+ "src/index.html"
495
+ ];
496
+ var JS_ENTRY_CANDIDATES = [
497
+ "src/main.jsx",
498
+ "src/main.tsx",
499
+ "src/main.js",
500
+ "src/main.ts",
501
+ "src/index.jsx",
502
+ "src/index.tsx",
503
+ "src/index.js",
504
+ "src/index.ts",
505
+ "src/App.jsx",
506
+ "src/App.tsx",
507
+ "src/App.js",
508
+ "src/App.ts",
509
+ "index.js",
510
+ "index.ts",
511
+ "index.jsx",
512
+ "index.tsx"
513
+ ];
514
+ var BARE_IMPORT_RE = /(?:^|\n)\s*import\s+[\s\S]*?from\s*['"]([^./'"][^'"]*)['"]/;
515
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
516
+ "assert",
517
+ "buffer",
518
+ "child_process",
519
+ "cluster",
520
+ "crypto",
521
+ "dgram",
522
+ "dns",
523
+ "events",
524
+ "fs",
525
+ "http",
526
+ "http2",
527
+ "https",
528
+ "net",
529
+ "os",
530
+ "path",
531
+ "perf_hooks",
532
+ "process",
533
+ "querystring",
534
+ "readline",
535
+ "stream",
536
+ "string_decoder",
537
+ "tls",
538
+ "tty",
539
+ "url",
540
+ "util",
541
+ "v8",
542
+ "vm",
543
+ "worker_threads",
544
+ "zlib"
545
+ ]);
546
+ function analyzeProject(project, overrideType, overrideEntry) {
547
+ const { files } = project;
548
+ let dependencies = {};
549
+ let devDependencies = {};
550
+ const pkgRaw = files.get("package.json");
551
+ if (pkgRaw) {
552
+ try {
553
+ const pkg = JSON.parse(pkgRaw);
554
+ dependencies = pkg.dependencies ?? {};
555
+ devDependencies = pkg.devDependencies ?? {};
556
+ } catch {
557
+ }
558
+ }
559
+ const allDeps = { ...devDependencies, ...dependencies };
560
+ let framework = "none";
561
+ if ("react" in dependencies || "react-dom" in dependencies) {
562
+ framework = "react";
563
+ } else if ("preact" in dependencies) {
564
+ framework = "preact";
565
+ } else if ("solid-js" in dependencies) {
566
+ framework = "solid";
567
+ }
568
+ const paths = Array.from(files.keys());
569
+ const hasJSX = paths.some((p) => p.endsWith(".jsx") || p.endsWith(".tsx"));
570
+ const hasTypeScript = paths.some((p) => p.endsWith(".ts") || p.endsWith(".tsx")) || "typescript" in allDeps;
571
+ const hasViteConfig = paths.some(
572
+ (p) => /^vite\.config\.(js|ts|mjs|mts)$/.test(p)
573
+ );
574
+ const hasViteDep = "vite" in allDeps;
575
+ const isVite = hasViteConfig || hasViteDep;
576
+ let htmlEntry = overrideEntry ?? null;
577
+ if (!htmlEntry) {
578
+ for (const candidate of HTML_ENTRY_CANDIDATES) {
579
+ if (files.has(candidate)) {
580
+ htmlEntry = candidate;
581
+ break;
582
+ }
583
+ }
584
+ }
585
+ if (!htmlEntry) {
586
+ const rootHtmlFiles = paths.filter(
587
+ (p) => !p.includes("/") && p.endsWith(".html")
588
+ );
589
+ if (rootHtmlFiles.length === 1) {
590
+ htmlEntry = rootHtmlFiles[0];
591
+ }
592
+ }
593
+ let jsEntry;
594
+ for (const candidate of JS_ENTRY_CANDIDATES) {
595
+ if (files.has(candidate)) {
596
+ jsEntry = candidate;
597
+ break;
598
+ }
599
+ }
600
+ if (htmlEntry && !jsEntry) {
601
+ const htmlContent = files.get(htmlEntry);
602
+ if (htmlContent) {
603
+ const scriptMatch = htmlContent.match(
604
+ /<script[^>]+src=["']([^"']+)["']/
605
+ );
606
+ if (scriptMatch) {
607
+ let src = scriptMatch[1];
608
+ if (src.startsWith("/")) {
609
+ src = src.slice(1);
610
+ }
611
+ jsEntry = src;
612
+ }
613
+ }
614
+ }
615
+ if (!htmlEntry && !jsEntry) {
616
+ throw new EntryPointError();
617
+ }
618
+ if (!htmlEntry) {
619
+ htmlEntry = "__generated__/index.html";
620
+ }
621
+ let type = overrideType ?? detectProjectType({
622
+ hasPkg: !!pkgRaw,
623
+ hasJSX,
624
+ hasTypeScript,
625
+ hasBareImports: hasBareImports(files),
626
+ isVite
627
+ });
628
+ return {
629
+ type,
630
+ entryPoint: htmlEntry,
631
+ dependencies,
632
+ framework,
633
+ hasJSX,
634
+ hasTypeScript,
635
+ jsEntryPoint: jsEntry,
636
+ isVite
637
+ };
638
+ }
639
+ function detectProjectType(input) {
640
+ const { hasPkg, hasJSX, hasTypeScript, hasBareImports: hasBareImports2, isVite } = input;
641
+ if (isVite) return "vite";
642
+ if (hasJSX && hasTypeScript) return "jsx-typescript";
643
+ if (hasJSX) return "jsx";
644
+ if (hasTypeScript) return "typescript";
645
+ if (hasPkg || hasBareImports2) return "static-esm";
646
+ return "static";
647
+ }
648
+ function hasBareImports(files) {
649
+ for (const [path, content] of files) {
650
+ if (/\.(js|ts|jsx|tsx|mjs)$/.test(path)) {
651
+ if (BARE_IMPORT_RE.test(content)) {
652
+ return true;
653
+ }
654
+ }
655
+ }
656
+ return false;
657
+ }
658
+
659
+ // src/core/sandbox.ts
660
+ function buildErrorHTML(message) {
661
+ return `<!DOCTYPE html>
662
+ <html>
663
+ <head><meta charset="UTF-8"></head>
664
+ <body style="margin:0;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#fef2f2;">
665
+ <div style="max-width:540px;padding:32px;text-align:center;">
666
+ <h2 style="margin:0 0 12px;color:#dc2626;font-size:20px;">Diorama: Render error</h2>
667
+ <p style="margin:0 0 16px;color:#333;font-size:15px;line-height:1.5;">${escapeHTML(message)}</p>
668
+ <p style="margin:0;color:#888;font-size:13px;">
669
+ This project may use features not supported by Diorama.
670
+ <a href="https://github.com/becca/diorama#supported-projects" target="_blank" rel="noopener" style="color:#2563eb;">Learn more</a>
671
+ </p>
672
+ </div>
673
+ </body>
674
+ </html>`;
675
+ }
676
+ function createSandbox(options) {
677
+ const {
678
+ container,
679
+ html,
680
+ height = "500px",
681
+ sandboxFlags = ["allow-scripts"],
682
+ loading = "eager",
683
+ frame = "none",
684
+ expand = false,
685
+ placeholder,
686
+ repoName,
687
+ useBlobURL = false,
688
+ onLoad,
689
+ onError
690
+ } = options;
691
+ const maybeEl = typeof container === "string" ? document.querySelector(container) : container;
692
+ if (!maybeEl) {
693
+ throw new Error(
694
+ `Diorama: container "${typeof container === "string" ? container : "element"}" not found in the DOM.`
695
+ );
696
+ }
697
+ const el = maybeEl;
698
+ let blobURL = null;
699
+ let iframe = null;
700
+ let observer = null;
701
+ let destroyed = false;
702
+ let isExpanded = false;
703
+ let backdrop = null;
704
+ let expandBtn = null;
705
+ let expandEscHandler = null;
706
+ function buildIframe(content) {
707
+ const frame2 = document.createElement("iframe");
708
+ frame2.setAttribute("sandbox", sandboxFlags.join(" "));
709
+ frame2.setAttribute("loading", "lazy");
710
+ frame2.setAttribute("aria-label", repoName ? `Preview of ${repoName}` : "Diorama project preview");
711
+ frame2.style.width = "100%";
712
+ frame2.style.height = height;
713
+ frame2.style.border = "none";
714
+ frame2.style.display = "block";
715
+ if (useBlobURL) {
716
+ const blob = new Blob([content], { type: "text/html" });
717
+ blobURL = URL.createObjectURL(blob);
718
+ frame2.src = blobURL;
719
+ } else {
720
+ frame2.srcdoc = content;
721
+ }
722
+ frame2.addEventListener("load", () => {
723
+ onLoad?.();
724
+ });
725
+ return frame2;
726
+ }
727
+ function buildPlaceholder(onClick) {
728
+ const wrapper = document.createElement("div");
729
+ wrapper.style.position = "relative";
730
+ wrapper.style.width = "100%";
731
+ wrapper.style.height = height;
732
+ wrapper.style.cursor = "pointer";
733
+ wrapper.style.overflow = "hidden";
734
+ wrapper.style.borderRadius = "8px";
735
+ wrapper.style.background = "#f3f4f6";
736
+ wrapper.style.display = "flex";
737
+ wrapper.style.alignItems = "center";
738
+ wrapper.style.justifyContent = "center";
739
+ wrapper.setAttribute("role", "button");
740
+ wrapper.setAttribute("tabindex", "0");
741
+ wrapper.setAttribute("aria-label", "Launch project preview");
742
+ if (placeholder) {
743
+ const img = document.createElement("img");
744
+ img.src = placeholder;
745
+ img.alt = repoName ? `Preview of ${repoName}` : "Project preview";
746
+ img.style.width = "100%";
747
+ img.style.height = "100%";
748
+ img.style.objectFit = "cover";
749
+ wrapper.appendChild(img);
750
+ }
751
+ const overlay = document.createElement("div");
752
+ overlay.style.position = "absolute";
753
+ overlay.style.inset = "0";
754
+ overlay.style.display = "flex";
755
+ overlay.style.flexDirection = "column";
756
+ overlay.style.alignItems = "center";
757
+ overlay.style.justifyContent = "center";
758
+ overlay.style.background = placeholder ? "rgba(0,0,0,0.35)" : "transparent";
759
+ const playBtn = document.createElement("div");
760
+ playBtn.innerHTML = `<svg width="48" height="48" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="24" fill="rgba(0,0,0,0.5)"/><polygon points="19,14 19,34 36,24" fill="white"/></svg>`;
761
+ overlay.appendChild(playBtn);
762
+ if (repoName && !placeholder) {
763
+ const label = document.createElement("div");
764
+ label.textContent = repoName;
765
+ label.style.marginTop = "12px";
766
+ label.style.fontSize = "15px";
767
+ label.style.fontFamily = "system-ui, sans-serif";
768
+ label.style.color = "#374151";
769
+ label.style.fontWeight = "500";
770
+ overlay.appendChild(label);
771
+ }
772
+ const hint = document.createElement("div");
773
+ hint.textContent = "Click to launch preview";
774
+ hint.style.marginTop = "8px";
775
+ hint.style.fontSize = "13px";
776
+ hint.style.fontFamily = "system-ui, sans-serif";
777
+ hint.style.color = placeholder ? "rgba(255,255,255,0.85)" : "#6b7280";
778
+ overlay.appendChild(hint);
779
+ wrapper.appendChild(overlay);
780
+ wrapper.addEventListener("click", onClick);
781
+ wrapper.addEventListener("keydown", (e) => {
782
+ if (e.key === "Enter" || e.key === " ") {
783
+ e.preventDefault();
784
+ onClick();
785
+ }
786
+ });
787
+ return wrapper;
788
+ }
789
+ function renderIframe(content) {
790
+ if (destroyed) return;
791
+ el.innerHTML = "";
792
+ iframe = buildIframe(content);
793
+ if (frame !== "none") {
794
+ const wrapper = buildFrame(iframe, frame, repoName);
795
+ el.appendChild(wrapper);
796
+ } else {
797
+ el.appendChild(iframe);
798
+ }
799
+ if (expand) attachExpandButton();
800
+ }
801
+ function setupEager() {
802
+ renderIframe(html);
803
+ }
804
+ function setupClick() {
805
+ const placeholderEl = buildPlaceholder(() => {
806
+ renderIframe(html);
807
+ });
808
+ el.innerHTML = "";
809
+ el.appendChild(placeholderEl);
810
+ }
811
+ function setupViewport() {
812
+ const placeholderEl = buildPlaceholder(() => {
813
+ renderIframe(html);
814
+ observer?.disconnect();
815
+ observer = null;
816
+ });
817
+ el.innerHTML = "";
818
+ el.appendChild(placeholderEl);
819
+ observer = new IntersectionObserver(
820
+ (entries) => {
821
+ for (const entry of entries) {
822
+ if (entry.isIntersecting) {
823
+ renderIframe(html);
824
+ observer?.disconnect();
825
+ observer = null;
826
+ break;
827
+ }
828
+ }
829
+ },
830
+ { threshold: 0.1 }
831
+ );
832
+ observer.observe(el);
833
+ }
834
+ function expandDiorama() {
835
+ if (isExpanded || destroyed) return;
836
+ isExpanded = true;
837
+ injectExpandStyles();
838
+ backdrop = document.createElement("div");
839
+ backdrop.className = "diorama-expand-backdrop";
840
+ backdrop.setAttribute("aria-label", "Close expanded preview");
841
+ backdrop.addEventListener("click", collapseDiorama);
842
+ document.body.appendChild(backdrop);
843
+ el.classList.add("diorama-expanded");
844
+ expandEscHandler = (e) => {
845
+ if (e.key === "Escape") collapseDiorama();
846
+ };
847
+ document.addEventListener("keydown", expandEscHandler);
848
+ }
849
+ function collapseDiorama() {
850
+ if (!isExpanded) return;
851
+ isExpanded = false;
852
+ backdrop?.remove();
853
+ backdrop = null;
854
+ el.classList.remove("diorama-expanded");
855
+ if (expandEscHandler) {
856
+ document.removeEventListener("keydown", expandEscHandler);
857
+ expandEscHandler = null;
858
+ }
859
+ }
860
+ function attachExpandButton() {
861
+ injectExpandStyles();
862
+ el.classList.add("diorama-expandable");
863
+ expandBtn?.remove();
864
+ expandBtn = document.createElement("button");
865
+ expandBtn.className = "diorama-expand-btn";
866
+ expandBtn.setAttribute("aria-label", "Expand preview");
867
+ expandBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><polyline points="10,2 14,2 14,6"/><polyline points="6,14 2,14 2,10"/><line x1="14" y1="2" x2="9" y2="7"/><line x1="2" y1="14" x2="7" y2="9"/></svg>';
868
+ expandBtn.addEventListener("click", (e) => {
869
+ e.stopPropagation();
870
+ e.preventDefault();
871
+ if (!isExpanded) expandDiorama();
872
+ });
873
+ el.appendChild(expandBtn);
874
+ }
875
+ try {
876
+ switch (loading) {
877
+ case "click":
878
+ setupClick();
879
+ break;
880
+ case "viewport":
881
+ setupViewport();
882
+ break;
883
+ case "eager":
884
+ default:
885
+ setupEager();
886
+ break;
887
+ }
888
+ } catch (err) {
889
+ onError?.(err);
890
+ }
891
+ return {
892
+ destroy() {
893
+ destroyed = true;
894
+ collapseDiorama();
895
+ expandBtn?.remove();
896
+ expandBtn = null;
897
+ if (blobURL) {
898
+ URL.revokeObjectURL(blobURL);
899
+ blobURL = null;
900
+ }
901
+ observer?.disconnect();
902
+ observer = null;
903
+ el.innerHTML = "";
904
+ iframe = null;
905
+ },
906
+ update(newHTML) {
907
+ if (destroyed) return;
908
+ if (blobURL) {
909
+ URL.revokeObjectURL(blobURL);
910
+ blobURL = null;
911
+ }
912
+ if (iframe) {
913
+ if (useBlobURL) {
914
+ const blob = new Blob([newHTML], { type: "text/html" });
915
+ blobURL = URL.createObjectURL(blob);
916
+ iframe.src = blobURL;
917
+ } else {
918
+ iframe.srcdoc = newHTML;
919
+ }
920
+ } else {
921
+ renderIframe(newHTML);
922
+ }
923
+ }
924
+ };
925
+ }
926
+ function buildFrame(iframe, style, repoName) {
927
+ const wrapper = document.createElement("div");
928
+ wrapper.className = `diorama-frame diorama-frame--${style}`;
929
+ injectFrameStyles();
930
+ switch (style) {
931
+ case "polaroid":
932
+ return buildPolaroidFrame(wrapper, iframe, repoName);
933
+ case "museum":
934
+ return buildMuseumFrame(wrapper, iframe);
935
+ case "standard":
936
+ return buildStandardFrame(wrapper, iframe);
937
+ case "terminal":
938
+ return buildTerminalFrame(wrapper, iframe, repoName);
939
+ case "postcard":
940
+ return buildPostcardFrame(wrapper, iframe, repoName);
941
+ case "blueprint":
942
+ return buildBlueprintFrame(wrapper, iframe, repoName);
943
+ case "browser":
944
+ return buildBrowserFrame(wrapper, iframe, repoName);
945
+ default:
946
+ wrapper.appendChild(iframe);
947
+ return wrapper;
948
+ }
949
+ }
950
+ function buildPolaroidFrame(wrapper, iframe, repoName) {
951
+ wrapper.appendChild(iframe);
952
+ const caption = document.createElement("div");
953
+ caption.className = "diorama-polaroid-caption";
954
+ caption.textContent = repoName || "";
955
+ wrapper.appendChild(caption);
956
+ return wrapper;
957
+ }
958
+ function buildMuseumFrame(wrapper, iframe) {
959
+ const mat = document.createElement("div");
960
+ mat.className = "diorama-museum-mat";
961
+ mat.appendChild(iframe);
962
+ wrapper.appendChild(mat);
963
+ return wrapper;
964
+ }
965
+ function buildStandardFrame(wrapper, iframe) {
966
+ wrapper.appendChild(iframe);
967
+ return wrapper;
968
+ }
969
+ function buildTerminalFrame(wrapper, iframe, repoName) {
970
+ const titlebar = document.createElement("div");
971
+ titlebar.className = "diorama-terminal-bar";
972
+ const dots = document.createElement("div");
973
+ dots.className = "diorama-terminal-dots";
974
+ dots.innerHTML = "<i></i><i></i><i></i>";
975
+ titlebar.appendChild(dots);
976
+ const title = document.createElement("span");
977
+ title.className = "diorama-terminal-title";
978
+ title.textContent = repoName || "diorama";
979
+ titlebar.appendChild(title);
980
+ wrapper.appendChild(titlebar);
981
+ wrapper.appendChild(iframe);
982
+ return wrapper;
983
+ }
984
+ function buildPostcardFrame(wrapper, iframe, repoName) {
985
+ wrapper.appendChild(iframe);
986
+ const stamp = document.createElement("div");
987
+ stamp.className = "diorama-postcard-stamp";
988
+ stamp.innerHTML = `<span class="diorama-postcard-stamp-text">\u{1F4EE}</span>`;
989
+ wrapper.appendChild(stamp);
990
+ const postmark = document.createElement("div");
991
+ postmark.className = "diorama-postcard-mark";
992
+ postmark.textContent = repoName || "DIORAMA";
993
+ wrapper.appendChild(postmark);
994
+ return wrapper;
995
+ }
996
+ function buildBlueprintFrame(wrapper, iframe, repoName) {
997
+ wrapper.appendChild(iframe);
998
+ const label = document.createElement("div");
999
+ label.className = "diorama-blueprint-label";
1000
+ label.textContent = repoName ? `DRAWING: ${repoName.toUpperCase()}` : "DRAWING: UNTITLED";
1001
+ wrapper.appendChild(label);
1002
+ return wrapper;
1003
+ }
1004
+ function buildBrowserFrame(wrapper, iframe, repoName) {
1005
+ const bar = document.createElement("div");
1006
+ bar.className = "diorama-browser-bar";
1007
+ const dots = document.createElement("div");
1008
+ dots.className = "diorama-browser-dots";
1009
+ dots.innerHTML = "<i></i><i></i><i></i>";
1010
+ bar.appendChild(dots);
1011
+ const url = document.createElement("div");
1012
+ url.className = "diorama-browser-url";
1013
+ url.textContent = repoName ? `https://${repoName.replace(/\//g, ".").toLowerCase()}.github.io` : "https://example.github.io";
1014
+ bar.appendChild(url);
1015
+ wrapper.appendChild(bar);
1016
+ wrapper.appendChild(iframe);
1017
+ return wrapper;
1018
+ }
1019
+ var stylesInjected = false;
1020
+ function injectFrameStyles() {
1021
+ if (stylesInjected) return;
1022
+ stylesInjected = true;
1023
+ const css = `
1024
+ /* \u2500\u2500 Diorama Frame Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1025
+
1026
+ .diorama-frame {
1027
+ position: relative;
1028
+ display: inline-block;
1029
+ width: 100%;
1030
+ box-sizing: border-box;
1031
+ }
1032
+ .diorama-frame iframe {
1033
+ display: block;
1034
+ width: 100%;
1035
+ }
1036
+
1037
+ /* \u2500\u2500 Standard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1038
+ .diorama-frame--standard {
1039
+ border: 1px solid #d1d5db;
1040
+ border-radius: 8px;
1041
+ overflow: hidden;
1042
+ box-shadow:
1043
+ 0 1px 3px rgba(0,0,0,.06),
1044
+ 0 4px 12px rgba(0,0,0,.04);
1045
+ }
1046
+
1047
+ /* \u2500\u2500 Polaroid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1048
+ .diorama-frame--polaroid {
1049
+ background: #fff;
1050
+ padding: 12px 12px 0 12px;
1051
+ border-radius: 2px;
1052
+ box-shadow:
1053
+ 0 2px 8px rgba(0,0,0,.10),
1054
+ 0 8px 30px rgba(0,0,0,.08);
1055
+ transform: rotate(-1.2deg);
1056
+ }
1057
+ .diorama-frame--polaroid iframe {
1058
+ border-radius: 0;
1059
+ }
1060
+ .diorama-polaroid-caption {
1061
+ padding: 16px 4px 20px;
1062
+ text-align: center;
1063
+ font-family: 'Caveat', 'Segoe Print', 'Comic Sans MS', cursive;
1064
+ font-size: 1.05rem;
1065
+ color: #4b5563;
1066
+ letter-spacing: .3px;
1067
+ min-height: 24px;
1068
+ }
1069
+
1070
+ /* \u2500\u2500 Museum \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1071
+ .diorama-frame--museum {
1072
+ --gold-light: #e8c96e;
1073
+ --gold-mid: #c9a23a;
1074
+ --gold-dark: #9e7a22;
1075
+ --gold-shadow: #7a5c18;
1076
+
1077
+ padding: 18px;
1078
+ background:
1079
+ linear-gradient(145deg, var(--gold-light) 0%, var(--gold-mid) 30%, var(--gold-dark) 70%, var(--gold-shadow) 100%);
1080
+ border-radius: 3px;
1081
+ box-shadow:
1082
+ inset 0 2px 4px rgba(255,255,255,.35),
1083
+ inset 0 -2px 4px rgba(0,0,0,.25),
1084
+ 0 4px 20px rgba(0,0,0,.25),
1085
+ 0 8px 40px rgba(0,0,0,.15);
1086
+ border: 2px solid var(--gold-dark);
1087
+ }
1088
+ .diorama-frame--museum::before {
1089
+ content: '';
1090
+ position: absolute;
1091
+ inset: 6px;
1092
+ border: 1px solid rgba(255,255,255,.2);
1093
+ border-radius: 2px;
1094
+ pointer-events: none;
1095
+ }
1096
+ .diorama-frame--museum::after {
1097
+ content: '';
1098
+ position: absolute;
1099
+ inset: 10px;
1100
+ border: 1px solid rgba(0,0,0,.15);
1101
+ border-radius: 1px;
1102
+ pointer-events: none;
1103
+ }
1104
+ .diorama-museum-mat {
1105
+ background: #f5f0e8;
1106
+ padding: 10px;
1107
+ box-shadow: inset 0 1px 4px rgba(0,0,0,.1);
1108
+ }
1109
+ .diorama-museum-mat iframe {
1110
+ border-radius: 0;
1111
+ }
1112
+
1113
+ /* \u2500\u2500 Terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1114
+ .diorama-frame--terminal {
1115
+ background: #1e1e2e;
1116
+ border-radius: 10px;
1117
+ overflow: hidden;
1118
+ box-shadow:
1119
+ 0 4px 20px rgba(0,0,0,.3),
1120
+ 0 0 0 1px rgba(255,255,255,.06);
1121
+ }
1122
+ .diorama-terminal-bar {
1123
+ display: flex;
1124
+ align-items: center;
1125
+ gap: 8px;
1126
+ padding: 10px 14px;
1127
+ background: linear-gradient(180deg, #2a2a3d, #232336);
1128
+ border-bottom: 1px solid rgba(255,255,255,.06);
1129
+ user-select: none;
1130
+ }
1131
+ .diorama-terminal-dots {
1132
+ display: flex;
1133
+ gap: 6px;
1134
+ }
1135
+ .diorama-terminal-dots i {
1136
+ display: block;
1137
+ width: 12px;
1138
+ height: 12px;
1139
+ border-radius: 50%;
1140
+ }
1141
+ .diorama-terminal-dots i:nth-child(1) { background: #ff5f56; }
1142
+ .diorama-terminal-dots i:nth-child(2) { background: #ffbd2e; }
1143
+ .diorama-terminal-dots i:nth-child(3) { background: #27c93f; }
1144
+ .diorama-terminal-title {
1145
+ flex: 1;
1146
+ text-align: center;
1147
+ font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
1148
+ font-size: .75rem;
1149
+ color: rgba(255,255,255,.45);
1150
+ padding-right: 52px; /* offset for dots so title is visually centered */
1151
+ }
1152
+
1153
+ /* \u2500\u2500 Postcard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1154
+ .diorama-frame--postcard {
1155
+ background: #fdf8f0;
1156
+ padding: 14px;
1157
+ border-radius: 3px;
1158
+ box-shadow:
1159
+ 0 2px 6px rgba(0,0,0,.08),
1160
+ 0 6px 24px rgba(0,0,0,.06);
1161
+ border: 1px dashed #d4c5a9;
1162
+ transform: rotate(0.6deg);
1163
+ }
1164
+ .diorama-frame--postcard iframe {
1165
+ border-radius: 2px;
1166
+ }
1167
+ .diorama-postcard-stamp {
1168
+ position: absolute;
1169
+ top: 4px;
1170
+ right: 4px;
1171
+ width: 40px;
1172
+ height: 48px;
1173
+ background: #fff;
1174
+ border: 2px dashed #c9b894;
1175
+ border-radius: 2px;
1176
+ display: flex;
1177
+ align-items: center;
1178
+ justify-content: center;
1179
+ transform: rotate(3deg);
1180
+ pointer-events: none;
1181
+ }
1182
+ .diorama-postcard-stamp-text {
1183
+ font-size: 22px;
1184
+ line-height: 1;
1185
+ }
1186
+ .diorama-postcard-mark {
1187
+ position: absolute;
1188
+ top: 18px;
1189
+ right: 52px;
1190
+ font-family: ui-monospace, 'Courier New', monospace;
1191
+ font-size: .55rem;
1192
+ font-weight: 700;
1193
+ letter-spacing: .1em;
1194
+ text-transform: uppercase;
1195
+ color: rgba(180,80,60,.35);
1196
+ border: 2px solid rgba(180,80,60,.25);
1197
+ border-radius: 50%;
1198
+ padding: 6px 8px;
1199
+ transform: rotate(-12deg);
1200
+ pointer-events: none;
1201
+ white-space: nowrap;
1202
+ max-width: 120px;
1203
+ overflow: hidden;
1204
+ text-overflow: ellipsis;
1205
+ }
1206
+
1207
+ /* \u2500\u2500 Blueprint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1208
+ .diorama-frame--blueprint {
1209
+ background: #1a3a5c;
1210
+ padding: 16px;
1211
+ border-radius: 2px;
1212
+ box-shadow: 0 2px 12px rgba(0,0,0,.2);
1213
+ background-image:
1214
+ linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px),
1215
+ linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px);
1216
+ background-size: 20px 20px;
1217
+ }
1218
+ .diorama-frame--blueprint::before {
1219
+ content: '';
1220
+ position: absolute;
1221
+ inset: 8px;
1222
+ border: 1px solid rgba(255,255,255,.12);
1223
+ pointer-events: none;
1224
+ }
1225
+ .diorama-frame--blueprint iframe {
1226
+ border: 1px solid rgba(255,255,255,.15);
1227
+ }
1228
+ .diorama-blueprint-label {
1229
+ margin-top: 8px;
1230
+ font-family: ui-monospace, 'Courier New', monospace;
1231
+ font-size: .65rem;
1232
+ font-weight: 600;
1233
+ letter-spacing: .15em;
1234
+ text-transform: uppercase;
1235
+ color: rgba(255,255,255,.35);
1236
+ text-align: right;
1237
+ padding-right: 4px;
1238
+ }
1239
+
1240
+ /* \u2500\u2500 Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1241
+ .diorama-frame--browser {
1242
+ background: #f0ebe6;
1243
+ border-radius: 10px;
1244
+ overflow: hidden;
1245
+ box-shadow:
1246
+ 0 2px 8px rgba(0,0,0,.08),
1247
+ 0 8px 30px rgba(0,0,0,.06);
1248
+ border: 1px solid #d5ccc3;
1249
+ }
1250
+ .diorama-browser-bar {
1251
+ display: flex;
1252
+ align-items: center;
1253
+ gap: 10px;
1254
+ padding: 8px 12px;
1255
+ background: linear-gradient(180deg, #f5f0eb, #ece7e1);
1256
+ border-bottom: 1px solid #d5ccc3;
1257
+ user-select: none;
1258
+ }
1259
+ .diorama-browser-dots {
1260
+ display: flex;
1261
+ gap: 6px;
1262
+ }
1263
+ .diorama-browser-dots i {
1264
+ display: block;
1265
+ width: 11px;
1266
+ height: 11px;
1267
+ border-radius: 50%;
1268
+ }
1269
+ .diorama-browser-dots i:nth-child(1) { background: #ed6a5e; }
1270
+ .diorama-browser-dots i:nth-child(2) { background: #f4bf4f; }
1271
+ .diorama-browser-dots i:nth-child(3) { background: #61c554; }
1272
+ .diorama-browser-url {
1273
+ flex: 1;
1274
+ font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
1275
+ font-size: .72rem;
1276
+ color: #7a7067;
1277
+ background: #fff;
1278
+ border: 1px solid #d5ccc3;
1279
+ border-radius: 5px;
1280
+ padding: 4px 12px;
1281
+ overflow: hidden;
1282
+ text-overflow: ellipsis;
1283
+ white-space: nowrap;
1284
+ }
1285
+ `;
1286
+ const styleEl = document.createElement("style");
1287
+ styleEl.setAttribute("data-diorama-frames", "");
1288
+ styleEl.textContent = css;
1289
+ document.head.appendChild(styleEl);
1290
+ }
1291
+ function escapeHTML(str) {
1292
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1293
+ }
1294
+ var expandStylesInjected = false;
1295
+ function injectExpandStyles() {
1296
+ if (expandStylesInjected) return;
1297
+ expandStylesInjected = true;
1298
+ const css = `
1299
+ /* \u2500\u2500 Diorama Expand Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1300
+
1301
+ .diorama-expandable {
1302
+ position: relative;
1303
+ }
1304
+
1305
+ .diorama-expand-btn {
1306
+ position: absolute;
1307
+ top: 8px;
1308
+ right: 8px;
1309
+ width: 32px;
1310
+ height: 32px;
1311
+ border-radius: 6px;
1312
+ background: rgba(0, 0, 0, 0.55);
1313
+ border: 1px solid rgba(255, 255, 255, 0.15);
1314
+ cursor: zoom-in;
1315
+ display: flex;
1316
+ align-items: center;
1317
+ justify-content: center;
1318
+ opacity: 0;
1319
+ transition: opacity 0.15s ease, background 0.15s ease;
1320
+ z-index: 10;
1321
+ color: #fff;
1322
+ padding: 0;
1323
+ line-height: 1;
1324
+ }
1325
+ .diorama-expandable:hover .diorama-expand-btn,
1326
+ .diorama-expand-btn:focus-visible {
1327
+ opacity: 1;
1328
+ }
1329
+ .diorama-expand-btn:hover {
1330
+ background: rgba(0, 0, 0, 0.75);
1331
+ }
1332
+
1333
+ .diorama-expand-backdrop {
1334
+ position: fixed;
1335
+ inset: 0;
1336
+ z-index: 9998;
1337
+ background: rgba(0, 0, 0, 0.45);
1338
+ backdrop-filter: blur(8px);
1339
+ -webkit-backdrop-filter: blur(8px);
1340
+ cursor: zoom-out;
1341
+ animation: diorama-fade-in 0.2s ease;
1342
+ }
1343
+
1344
+ @keyframes diorama-fade-in {
1345
+ from { opacity: 0; }
1346
+ to { opacity: 1; }
1347
+ }
1348
+
1349
+ .diorama-expanded {
1350
+ position: fixed !important;
1351
+ top: 3vh !important;
1352
+ left: 3vw !important;
1353
+ width: 94vw !important;
1354
+ height: 94vh !important;
1355
+ z-index: 9999 !important;
1356
+ display: flex !important;
1357
+ flex-direction: column !important;
1358
+ animation: diorama-expand-in 0.25s ease;
1359
+ }
1360
+
1361
+ @keyframes diorama-expand-in {
1362
+ from { opacity: 0.85; transform: scale(0.97); }
1363
+ to { opacity: 1; transform: scale(1); }
1364
+ }
1365
+
1366
+ .diorama-expanded .diorama-expand-btn {
1367
+ display: none;
1368
+ }
1369
+
1370
+ .diorama-expanded > .diorama-frame {
1371
+ flex: 1 1 0 !important;
1372
+ width: 100% !important;
1373
+ display: flex !important;
1374
+ flex-direction: column !important;
1375
+ max-height: 100% !important;
1376
+ }
1377
+
1378
+ .diorama-expanded > iframe,
1379
+ .diorama-expanded .diorama-frame iframe {
1380
+ flex: 1 1 0 !important;
1381
+ height: auto !important;
1382
+ min-height: 0 !important;
1383
+ }
1384
+
1385
+ /* \u2500\u2500 Slim down frames when expanded (keep identity, reduce bulk) */
1386
+
1387
+ .diorama-expanded > .diorama-frame {
1388
+ overflow: hidden !important;
1389
+ }
1390
+
1391
+ /* Museum: thin gold border instead of chunky gilded frame */
1392
+ .diorama-expanded > .diorama-frame--museum {
1393
+ padding: 5px !important;
1394
+ border-radius: 6px !important;
1395
+ box-shadow:
1396
+ inset 0 1px 2px rgba(255,255,255,.25),
1397
+ 0 4px 24px rgba(0,0,0,.18) !important;
1398
+ }
1399
+ .diorama-expanded > .diorama-frame--museum::before {
1400
+ inset: 2px !important;
1401
+ }
1402
+ .diorama-expanded > .diorama-frame--museum::after {
1403
+ display: none !important;
1404
+ }
1405
+ .diorama-expanded .diorama-museum-mat {
1406
+ padding: 3px !important;
1407
+ flex: 1 1 0;
1408
+ display: flex;
1409
+ flex-direction: column;
1410
+ }
1411
+ .diorama-expanded .diorama-museum-mat iframe {
1412
+ flex: 1 1 0 !important;
1413
+ }
1414
+
1415
+ /* Polaroid: slim white border, compact caption */
1416
+ .diorama-expanded > .diorama-frame--polaroid {
1417
+ padding: 6px 6px 0 6px !important;
1418
+ border-radius: 6px !important;
1419
+ box-shadow: 0 4px 24px rgba(0,0,0,.14) !important;
1420
+ }
1421
+ .diorama-expanded .diorama-polaroid-caption {
1422
+ padding: 6px 10px !important;
1423
+ font-size: .8rem !important;
1424
+ min-height: auto !important;
1425
+ }
1426
+
1427
+ /* Standard: just tighten padding */
1428
+ .diorama-expanded > .diorama-frame--standard {
1429
+ padding: 5px !important;
1430
+ border-radius: 6px !important;
1431
+ }
1432
+
1433
+ /* Postcard: slim border, shrink stamp + postmark */
1434
+ .diorama-expanded > .diorama-frame--postcard {
1435
+ padding: 5px !important;
1436
+ border-radius: 6px !important;
1437
+ box-shadow: 0 4px 24px rgba(0,0,0,.14) !important;
1438
+ }
1439
+ .diorama-expanded .diorama-postcard-stamp {
1440
+ width: 28px !important;
1441
+ height: 34px !important;
1442
+ top: -2px !important;
1443
+ right: 2px !important;
1444
+ }
1445
+ .diorama-expanded .diorama-postcard-mark {
1446
+ font-size: .45rem !important;
1447
+ padding: 4px 5px !important;
1448
+ top: 8px !important;
1449
+ right: 36px !important;
1450
+ }
1451
+
1452
+ /* Blueprint: slim border, shrink label */
1453
+ .diorama-expanded > .diorama-frame--blueprint {
1454
+ padding: 5px !important;
1455
+ border-radius: 6px !important;
1456
+ box-shadow: 0 4px 24px rgba(0,0,0,.18) !important;
1457
+ }
1458
+ .diorama-expanded > .diorama-frame--blueprint::before {
1459
+ inset: 2px !important;
1460
+ }
1461
+ .diorama-expanded .diorama-blueprint-label {
1462
+ margin-top: 4px !important;
1463
+ font-size: .55rem !important;
1464
+ }
1465
+
1466
+ /* Terminal / Browser: already slim \u2014 just round corners */
1467
+ .diorama-expanded > .diorama-frame--terminal,
1468
+ .diorama-expanded > .diorama-frame--browser {
1469
+ border-radius: 10px !important;
1470
+ box-shadow: 0 4px 24px rgba(0,0,0,.2) !important;
1471
+ }
1472
+ `;
1473
+ const styleEl = document.createElement("style");
1474
+ styleEl.setAttribute("data-diorama-expand", "");
1475
+ styleEl.textContent = css;
1476
+ document.head.appendChild(styleEl);
1477
+ }
1478
+
1479
+ // src/transform/rewriter.ts
1480
+ var esmShProvider = {
1481
+ buildURL(name, version, subpath) {
1482
+ const ver = version ? `@${version}` : "";
1483
+ const sub = subpath ? `/${subpath}` : "";
1484
+ return `https://esm.sh/${name}${ver}${sub}`;
1485
+ }
1486
+ };
1487
+ var skypackProvider = {
1488
+ buildURL(name, version, subpath) {
1489
+ const ver = version ? `@${version}` : "";
1490
+ const sub = subpath ? `/${subpath}` : "";
1491
+ return `https://cdn.skypack.dev/${name}${ver}${sub}`;
1492
+ }
1493
+ };
1494
+ var unpkgProvider = {
1495
+ buildURL(name, version, subpath) {
1496
+ const ver = version ? `@${version}` : "";
1497
+ const sub = subpath ? `/${subpath}` : "";
1498
+ return `https://unpkg.com/${name}${ver}${sub}?module`;
1499
+ }
1500
+ };
1501
+ function getCDNProvider(name) {
1502
+ switch (name) {
1503
+ case "skypack":
1504
+ return skypackProvider;
1505
+ case "unpkg":
1506
+ return unpkgProvider;
1507
+ case "esm.sh":
1508
+ default:
1509
+ return esmShProvider;
1510
+ }
1511
+ }
1512
+ function rewriteImports(source, options = {}) {
1513
+ const {
1514
+ dependencies = {},
1515
+ cdnProvider = "esm.sh",
1516
+ framework = "none",
1517
+ reactVersion,
1518
+ isVite = false
1519
+ } = options;
1520
+ const provider = getCDNProvider(cdnProvider);
1521
+ function rewriteSpecifier(raw) {
1522
+ if (raw.startsWith(".") || raw.startsWith("/") || /^https?:\/\//.test(raw)) {
1523
+ return raw;
1524
+ }
1525
+ const { packageName, subpath } = parseSpecifier(raw);
1526
+ if (NODE_BUILTINS.has(packageName) || NODE_BUILTINS.has(packageName.replace("node:", ""))) {
1527
+ throw new NodeBuiltinError(packageName);
1528
+ }
1529
+ const version = dependencies[packageName];
1530
+ let url = provider.buildURL(packageName, version, subpath);
1531
+ if (cdnProvider === "esm.sh" && reactVersion && framework === "react") {
1532
+ if (packageName !== "react" && packageName !== "react-dom") {
1533
+ const sep = url.includes("?") ? "&" : "?";
1534
+ url += `${sep}deps=react@${reactVersion},react-dom@${reactVersion}`;
1535
+ }
1536
+ }
1537
+ return url;
1538
+ }
1539
+ let result = source;
1540
+ result = result.replace(
1541
+ /(import\s+(?:[\s\S]*?)\s+from\s*['"])([^'"]+)(['"])/g,
1542
+ (_match, pre, specifier, post) => {
1543
+ return `${pre}${rewriteSpecifier(specifier)}${post}`;
1544
+ }
1545
+ );
1546
+ result = result.replace(
1547
+ /(?<!\bfrom\s*)(import\s*['"])([^'"]+)(['"])/g,
1548
+ (_m, pre, specifier, post) => {
1549
+ return `${pre}${rewriteSpecifier(specifier)}${post}`;
1550
+ }
1551
+ );
1552
+ result = result.replace(
1553
+ /(import\s*\(\s*['"])([^'"]+)(['"]\s*\))/g,
1554
+ (_match, pre, specifier, post) => {
1555
+ return `${pre}${rewriteSpecifier(specifier)}${post}`;
1556
+ }
1557
+ );
1558
+ result = result.replace(
1559
+ /(export\s+(?:\{[^}]*\}|\*)\s+from\s*['"])([^'"]+)(['"])/g,
1560
+ (_match, pre, specifier, post) => {
1561
+ return `${pre}${rewriteSpecifier(specifier)}${post}`;
1562
+ }
1563
+ );
1564
+ if (isVite) {
1565
+ result = result.replace(/\bimport\.meta\.env\.MODE\b/g, '"production"');
1566
+ result = result.replace(/\bimport\.meta\.env\.BASE_URL\b/g, '"/"');
1567
+ result = result.replace(/\bimport\.meta\.env\.PROD\b/g, "true");
1568
+ result = result.replace(/\bimport\.meta\.env\.DEV\b/g, "false");
1569
+ result = result.replace(/\bimport\.meta\.env\.SSR\b/g, "false");
1570
+ result = result.replace(/\bimport\.meta\.env\.VITE_\w+\b/g, '""');
1571
+ }
1572
+ return result;
1573
+ }
1574
+ function parseSpecifier(raw) {
1575
+ if (raw.startsWith("@")) {
1576
+ const parts = raw.split("/");
1577
+ const packageName = `${parts[0]}/${parts[1]}`;
1578
+ const subpath = parts.length > 2 ? parts.slice(2).join("/") : void 0;
1579
+ return { packageName, subpath };
1580
+ }
1581
+ const slashIndex = raw.indexOf("/");
1582
+ if (slashIndex === -1) {
1583
+ return { packageName: raw };
1584
+ }
1585
+ return {
1586
+ packageName: raw.slice(0, slashIndex),
1587
+ subpath: raw.slice(slashIndex + 1)
1588
+ };
1589
+ }
1590
+
1591
+ // src/transform/transpiler.ts
1592
+ var esbuildModule = null;
1593
+ var esbuildReady = null;
1594
+ var ESBUILD_CDN = "https://esm.sh/esbuild-wasm@0.24.0";
1595
+ async function ensureEsbuild(wasmURL) {
1596
+ if (!esbuildReady) {
1597
+ esbuildReady = (async () => {
1598
+ try {
1599
+ const mod = await import(
1600
+ /* @vite-ignore */
1601
+ ESBUILD_CDN
1602
+ );
1603
+ esbuildModule = mod.default ?? mod;
1604
+ const resolvedURL = wasmURL && wasmURL !== "auto" ? wasmURL : `${ESBUILD_CDN}/esbuild.wasm`;
1605
+ await esbuildModule.initialize({ wasmURL: resolvedURL });
1606
+ } catch (err) {
1607
+ esbuildReady = null;
1608
+ esbuildModule = null;
1609
+ throw new TranspilerLoadError(
1610
+ err instanceof Error ? err.message : "Failed to load esbuild-wasm."
1611
+ );
1612
+ }
1613
+ })();
1614
+ }
1615
+ return esbuildReady;
1616
+ }
1617
+ async function transpileFile(options) {
1618
+ const { filePath, source, framework = "none", esbuildWasmURL } = options;
1619
+ await ensureEsbuild(esbuildWasmURL);
1620
+ const loader = loaderFor(filePath);
1621
+ const jsxConfig = jsxConfigFor(framework, loader);
1622
+ try {
1623
+ const result = await esbuildModule.transform(source, {
1624
+ loader,
1625
+ ...jsxConfig,
1626
+ target: "es2022",
1627
+ format: "esm",
1628
+ charset: "utf8"
1629
+ });
1630
+ return { code: result.code };
1631
+ } catch (err) {
1632
+ const msg = err instanceof Error ? err.message : String(err);
1633
+ const lineMatch = msg.match(/:(\d+):(\d+):/);
1634
+ throw new TranspileError(
1635
+ msg,
1636
+ filePath,
1637
+ lineMatch ? parseInt(lineMatch[1], 10) : void 0,
1638
+ lineMatch ? parseInt(lineMatch[2], 10) : void 0
1639
+ );
1640
+ }
1641
+ }
1642
+ async function transpileAll(files, framework = "none", esbuildWasmURL) {
1643
+ const result = new Map(files);
1644
+ const toTranspile = [];
1645
+ for (const [path, content] of files) {
1646
+ if (needsTranspilation(path)) {
1647
+ toTranspile.push([path, content]);
1648
+ }
1649
+ }
1650
+ if (toTranspile.length === 0) return result;
1651
+ await ensureEsbuild(esbuildWasmURL);
1652
+ const jobs = toTranspile.map(async ([path, content]) => {
1653
+ const transpiled = await transpileFile({
1654
+ filePath: path,
1655
+ source: content,
1656
+ framework,
1657
+ esbuildWasmURL
1658
+ });
1659
+ return [path, transpiled.code];
1660
+ });
1661
+ const results = await Promise.all(jobs);
1662
+ for (const [path, code] of results) {
1663
+ result.set(path, code);
1664
+ }
1665
+ return result;
1666
+ }
1667
+ function loaderFor(filePath) {
1668
+ if (filePath.endsWith(".tsx")) return "tsx";
1669
+ if (filePath.endsWith(".ts")) return "ts";
1670
+ if (filePath.endsWith(".jsx")) return "jsx";
1671
+ return "js";
1672
+ }
1673
+ function jsxConfigFor(framework, loader) {
1674
+ if (loader !== "tsx" && loader !== "jsx") {
1675
+ return {};
1676
+ }
1677
+ switch (framework) {
1678
+ case "react":
1679
+ return { jsx: "automatic", jsxImportSource: "react" };
1680
+ case "preact":
1681
+ return { jsx: "automatic", jsxImportSource: "preact" };
1682
+ case "solid":
1683
+ return { jsx: "automatic", jsxImportSource: "react" };
1684
+ default:
1685
+ return { jsx: "automatic", jsxImportSource: "react" };
1686
+ }
1687
+ }
1688
+ function needsTranspilation(path) {
1689
+ return /\.(tsx?|jsx)$/.test(path);
1690
+ }
1691
+ function processEnvShim() {
1692
+ return `if(typeof globalThis.process==='undefined'){globalThis.process={env:{NODE_ENV:'production'}};}`;
1693
+ }
1694
+
1695
+ // src/transform/assembler.ts
1696
+ var SMALL_ASSET_LIMIT = 1e5;
1697
+ var INLINEABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1698
+ ".png",
1699
+ ".jpg",
1700
+ ".jpeg",
1701
+ ".gif",
1702
+ ".svg",
1703
+ ".ico",
1704
+ ".webp",
1705
+ ".woff",
1706
+ ".woff2",
1707
+ ".ttf",
1708
+ ".eot",
1709
+ ".otf"
1710
+ ]);
1711
+ var IMPORT_MAP_PREFIX = "__diorama__/";
1712
+ var ASSET_IMPORT_EXTENSIONS = {
1713
+ ".svg": "image/svg+xml",
1714
+ ".png": "image/png",
1715
+ ".jpg": "image/jpeg",
1716
+ ".jpeg": "image/jpeg",
1717
+ ".gif": "image/gif",
1718
+ ".webp": "image/webp",
1719
+ ".ico": "image/x-icon",
1720
+ ".woff": "font/woff",
1721
+ ".woff2": "font/woff2",
1722
+ ".ttf": "font/ttf",
1723
+ ".eot": "application/vnd.ms-fontobject",
1724
+ ".otf": "font/otf"
1725
+ };
1726
+ function assembleHTML(options) {
1727
+ const { project, config, transformedFiles } = options;
1728
+ const files = transformedFiles ?? project.files;
1729
+ switch (config.type) {
1730
+ case "static":
1731
+ return assembleStatic(files, project, config);
1732
+ case "static-esm":
1733
+ case "jsx":
1734
+ case "typescript":
1735
+ case "jsx-typescript":
1736
+ case "vite":
1737
+ return assembleESM(files, project, config);
1738
+ default:
1739
+ throw new AssemblyError(`Unsupported project type: ${config.type}`);
1740
+ }
1741
+ }
1742
+ function assembleStatic(files, project, config) {
1743
+ let html = files.get(config.entryPoint);
1744
+ if (!html) {
1745
+ throw new AssemblyError(
1746
+ `Entry point "${config.entryPoint}" not found in the project files.`
1747
+ );
1748
+ }
1749
+ html = inlineLocalCSS(html, files, config.entryPoint);
1750
+ html = inlineLocalJS(html, files, config.entryPoint);
1751
+ html = inlineAssets(html, project, config.entryPoint);
1752
+ return { html, usesESM: false };
1753
+ }
1754
+ function assembleESM(files, project, config) {
1755
+ const isGenerated = config.entryPoint === "__generated__/index.html";
1756
+ rewriteAssetImports(files, project);
1757
+ const cssFromJS = extractCSSImports(files);
1758
+ if (config.isVite) {
1759
+ injectImportMetaEnv(files);
1760
+ }
1761
+ const importMap = buildImportMap(files);
1762
+ let html;
1763
+ if (isGenerated) {
1764
+ html = generateHTMLShell(files, config);
1765
+ } else {
1766
+ html = files.get(config.entryPoint) ?? "";
1767
+ if (!html) {
1768
+ throw new AssemblyError(
1769
+ `Entry point "${config.entryPoint}" not found in the project files.`
1770
+ );
1771
+ }
1772
+ html = inlineLocalCSS(html, files, config.entryPoint);
1773
+ html = rewriteScriptSrcsToImportMap(html, config.entryPoint);
1774
+ html = inlineAssets(html, project, config.entryPoint);
1775
+ }
1776
+ if (cssFromJS) {
1777
+ html = injectIntoHead(html, `<style>
1778
+ ${cssFromJS}
1779
+ </style>`);
1780
+ }
1781
+ if (Object.keys(importMap).length > 0) {
1782
+ const importMapJSON = JSON.stringify({ imports: importMap }, null, 2);
1783
+ html = injectIntoHead(
1784
+ html,
1785
+ `<script type="importmap">
1786
+ ${importMapJSON}
1787
+ </script>`
1788
+ );
1789
+ }
1790
+ if (config.framework !== "none" || config.hasTypeScript || config.hasJSX) {
1791
+ html = injectIntoHead(
1792
+ html,
1793
+ `<script>${processEnvShim()}</script>`
1794
+ );
1795
+ }
1796
+ if (config.isVite) {
1797
+ if (config.framework === "none" && !config.hasTypeScript && !config.hasJSX) {
1798
+ html = injectIntoHead(
1799
+ html,
1800
+ `<script>${processEnvShim()}</script>`
1801
+ );
1802
+ }
1803
+ }
1804
+ return { html, usesESM: true };
1805
+ }
1806
+ function generateHTMLShell(files, config) {
1807
+ const jsEntry = config.jsEntryPoint;
1808
+ let mountScript = "";
1809
+ if (config.framework === "react" && jsEntry) {
1810
+ mountScript = `
1811
+ import { createRoot } from 'react-dom/client';
1812
+ import App from '${IMPORT_MAP_PREFIX}${jsEntry}';
1813
+ const root = createRoot(document.getElementById('root'));
1814
+ root.render(typeof App === 'function' ? App() : App);`;
1815
+ } else if (config.framework === "preact" && jsEntry) {
1816
+ mountScript = `
1817
+ import { render } from 'preact';
1818
+ import App from '${IMPORT_MAP_PREFIX}${jsEntry}';
1819
+ render(typeof App === 'function' ? App() : App, document.getElementById('root'));`;
1820
+ } else if (jsEntry) {
1821
+ mountScript = `import '${IMPORT_MAP_PREFIX}${jsEntry}';`;
1822
+ }
1823
+ const cssFiles = [];
1824
+ for (const [path, content] of files) {
1825
+ if (path.endsWith(".css") && !path.includes("module.css")) {
1826
+ cssFiles.push(content);
1827
+ }
1828
+ }
1829
+ return `<!DOCTYPE html>
1830
+ <html>
1831
+ <head>
1832
+ <meta charset="UTF-8">
1833
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1834
+ <style>* { margin: 0; box-sizing: border-box; }</style>
1835
+ ${cssFiles.map((css) => ` <style>
1836
+ ${css}
1837
+ </style>`).join("\n")}
1838
+ </head>
1839
+ <body>
1840
+ <div id="root"></div>
1841
+ <script type="module">${mountScript}
1842
+ </script>
1843
+ </body>
1844
+ </html>`;
1845
+ }
1846
+ function buildImportMap(files, _config) {
1847
+ const jsFiles = /* @__PURE__ */ new Map();
1848
+ for (const [path, content] of files) {
1849
+ if (/\.(js|ts|jsx|tsx|mjs)$/.test(path)) {
1850
+ jsFiles.set(path, content);
1851
+ }
1852
+ }
1853
+ const map = {};
1854
+ for (const [path, content] of jsFiles) {
1855
+ const dir = directoryOf(path);
1856
+ const rewritten = rewriteRelativeImportsToBareSpecifiers(content, dir, jsFiles);
1857
+ const blobURL = createBlobURL(rewritten, "application/javascript");
1858
+ map[`${IMPORT_MAP_PREFIX}${path}`] = blobURL;
1859
+ }
1860
+ return map;
1861
+ }
1862
+ function rewriteRelativeImportsToBareSpecifiers(source, dir, allFiles) {
1863
+ return source.replace(
1864
+ /((?:import|export)\s+(?:[\s\S]*?\s+from\s*|)['"])(\.[^'"]+)(['"])/g,
1865
+ (_match, pre, specifier, post) => {
1866
+ const resolved = dir ? resolvePath(dir, specifier) : specifier.replace(/^\.\//, "");
1867
+ if (allFiles.has(resolved)) {
1868
+ return `${pre}${IMPORT_MAP_PREFIX}${resolved}${post}`;
1869
+ }
1870
+ const withoutExt = resolved.replace(/\.[^.]+$/, "");
1871
+ for (const ext of [".js", ".ts", ".jsx", ".tsx", ".mjs"]) {
1872
+ if (allFiles.has(withoutExt + ext)) {
1873
+ return `${pre}${IMPORT_MAP_PREFIX}${withoutExt + ext}${post}`;
1874
+ }
1875
+ }
1876
+ return _match;
1877
+ }
1878
+ );
1879
+ }
1880
+ function createBlobURL(content, mime) {
1881
+ return `data:${mime};base64,${btoa(unescape(encodeURIComponent(content)))}`;
1882
+ }
1883
+ function inlineLocalCSS(html, files, htmlPath) {
1884
+ const dir = directoryOf(htmlPath);
1885
+ return html.replace(
1886
+ /<link\s+[^>]*(?:rel=["']stylesheet["'][^>]*href=["']([^"']+)["']|href=["']([^"']+)["'][^>]*rel=["']stylesheet["'])[^>]*\/?>/gi,
1887
+ (match, href1, href2) => {
1888
+ const href = href1 ?? href2;
1889
+ if (!href) return match;
1890
+ if (/^https?:\/\//.test(href)) return match;
1891
+ let resolved;
1892
+ if (href.startsWith("/")) {
1893
+ resolved = href.slice(1);
1894
+ } else {
1895
+ resolved = resolvePath(dir, href);
1896
+ }
1897
+ const css = files.get(resolved);
1898
+ if (css) {
1899
+ return `<style>
1900
+ ${css}
1901
+ </style>`;
1902
+ }
1903
+ return match;
1904
+ }
1905
+ );
1906
+ }
1907
+ function inlineLocalJS(html, files, htmlPath) {
1908
+ const dir = directoryOf(htmlPath);
1909
+ return html.replace(
1910
+ /<script\s+[^>]*src=["']([^"']+)["'][^>]*><\/script>/gi,
1911
+ (match, src) => {
1912
+ if (/^https?:\/\//.test(src)) return match;
1913
+ let resolved;
1914
+ if (src.startsWith("/")) {
1915
+ resolved = src.slice(1);
1916
+ } else {
1917
+ resolved = resolvePath(dir, src);
1918
+ }
1919
+ const js = files.get(resolved);
1920
+ if (js) {
1921
+ const typeMatch = match.match(/type=["']([^"']+)["']/);
1922
+ const typeAttr = typeMatch ? ` type="${typeMatch[1]}"` : "";
1923
+ return `<script${typeAttr}>
1924
+ ${js}
1925
+ </script>`;
1926
+ }
1927
+ return match;
1928
+ }
1929
+ );
1930
+ }
1931
+ function rewriteScriptSrcsToImportMap(html, htmlPath) {
1932
+ const dir = directoryOf(htmlPath);
1933
+ return html.replace(
1934
+ /<script\s+[^>]*src=["']([^"']+)["'][^>]*><\/script>/gi,
1935
+ (match, src) => {
1936
+ if (/^https?:\/\//.test(src)) return match;
1937
+ let resolved;
1938
+ if (src.startsWith("/")) {
1939
+ resolved = src.slice(1);
1940
+ } else {
1941
+ resolved = resolvePath(dir, src);
1942
+ }
1943
+ return `<script type="module">import '${IMPORT_MAP_PREFIX}${resolved}';</script>`;
1944
+ }
1945
+ );
1946
+ }
1947
+ function inlineAssets(html, project, htmlPath) {
1948
+ const dir = directoryOf(htmlPath);
1949
+ return html.replace(
1950
+ /(src|href)=["']([^"']+)["']/gi,
1951
+ (match, attr, ref2) => {
1952
+ if (/^(https?:\/\/|data:|blob:|#)/.test(ref2)) return match;
1953
+ let resolved;
1954
+ if (ref2.startsWith("/")) {
1955
+ resolved = ref2.slice(1);
1956
+ } else {
1957
+ resolved = resolvePath(dir, ref2);
1958
+ }
1959
+ const ext = extensionOf2(resolved);
1960
+ if (!INLINEABLE_EXTENSIONS.has(ext)) return match;
1961
+ const blob = project.binaryFiles.get(resolved);
1962
+ if (blob && blob.size <= SMALL_ASSET_LIMIT) {
1963
+ if (ext === ".svg") {
1964
+ const svgText = project.files.get(resolved);
1965
+ if (svgText) {
1966
+ const encoded = `data:image/svg+xml;base64,${btoa(svgText)}`;
1967
+ return `${attr}="${encoded}"`;
1968
+ }
1969
+ }
1970
+ return match;
1971
+ }
1972
+ if (ext === ".svg") {
1973
+ const svgText = project.files.get(resolved);
1974
+ if (svgText) {
1975
+ const encoded = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgText)))}`;
1976
+ return `${attr}="${encoded}"`;
1977
+ }
1978
+ }
1979
+ return match;
1980
+ }
1981
+ );
1982
+ }
1983
+ function rewriteAssetImports(files, project) {
1984
+ const assetImportRe = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?/g;
1985
+ for (const [filePath, content] of files) {
1986
+ if (!/\.(js|ts|jsx|tsx|mjs)$/.test(filePath)) continue;
1987
+ let modified = content;
1988
+ let didModify = false;
1989
+ let match;
1990
+ assetImportRe.lastIndex = 0;
1991
+ while ((match = assetImportRe.exec(content)) !== null) {
1992
+ const binding = match[1];
1993
+ const specifier = match[2];
1994
+ const ext = extensionOf2(specifier);
1995
+ if (!(ext in ASSET_IMPORT_EXTENSIONS)) continue;
1996
+ let resolved;
1997
+ if (specifier.startsWith("/")) {
1998
+ resolved = specifier.slice(1);
1999
+ } else {
2000
+ resolved = resolvePath(directoryOf(filePath), specifier);
2001
+ }
2002
+ const mime = ASSET_IMPORT_EXTENSIONS[ext];
2003
+ let dataURL = null;
2004
+ const textContent = project.files.get(resolved);
2005
+ if (textContent) {
2006
+ dataURL = `data:${mime};base64,${btoa(unescape(encodeURIComponent(textContent)))}`;
2007
+ }
2008
+ if (!dataURL) {
2009
+ const blob = project.binaryFiles.get(resolved);
2010
+ if (blob) {
2011
+ dataURL = `data:${mime};base64,`;
2012
+ }
2013
+ }
2014
+ if (!dataURL) {
2015
+ dataURL = `data:${mime};base64,`;
2016
+ }
2017
+ modified = modified.replace(match[0], `const ${binding} = "${dataURL}";`);
2018
+ didModify = true;
2019
+ }
2020
+ if (didModify) {
2021
+ files.set(filePath, modified);
2022
+ }
2023
+ }
2024
+ }
2025
+ function extractCSSImports(files) {
2026
+ const cssChunks = [];
2027
+ const cssImportRe = /import\s+['"]([^'"]+\.css)['"]\s*;?/g;
2028
+ for (const [path, content] of files) {
2029
+ if (!/\.(js|ts|jsx|tsx|mjs)$/.test(path)) continue;
2030
+ let modified = content;
2031
+ let match;
2032
+ while ((match = cssImportRe.exec(content)) !== null) {
2033
+ const cssPath = match[1];
2034
+ const resolved = resolvePath(directoryOf(path), cssPath);
2035
+ const css = files.get(resolved);
2036
+ if (css) {
2037
+ cssChunks.push(`/* ${resolved} */
2038
+ ${css}`);
2039
+ }
2040
+ modified = modified.replace(match[0], "");
2041
+ }
2042
+ if (modified !== content) {
2043
+ files.set(path, modified);
2044
+ }
2045
+ }
2046
+ return cssChunks.join("\n\n");
2047
+ }
2048
+ function injectImportMetaEnv(files) {
2049
+ const shim = `if(!import.meta.env){Object.defineProperty(import.meta,'env',{value:{MODE:'production',BASE_URL:'/',PROD:true,DEV:false,SSR:false}});}`;
2050
+ for (const [path, content] of files) {
2051
+ if (!/\.(js|ts|jsx|tsx|mjs)$/.test(path)) continue;
2052
+ files.set(path, shim + "\n" + content);
2053
+ }
2054
+ }
2055
+ function directoryOf(filePath) {
2056
+ const lastSlash = filePath.lastIndexOf("/");
2057
+ return lastSlash === -1 ? "" : filePath.slice(0, lastSlash);
2058
+ }
2059
+ function resolvePath(dir, relative) {
2060
+ let cleaned = relative.replace(/^\.\//, "");
2061
+ if (!dir) return cleaned;
2062
+ const parts = dir.split("/");
2063
+ const segments = cleaned.split("/");
2064
+ for (const seg of segments) {
2065
+ if (seg === "..") {
2066
+ parts.pop();
2067
+ } else if (seg !== ".") {
2068
+ parts.push(seg);
2069
+ }
2070
+ }
2071
+ return parts.join("/");
2072
+ }
2073
+ function extensionOf2(path) {
2074
+ const dot = path.lastIndexOf(".");
2075
+ return dot === -1 ? "" : path.slice(dot).toLowerCase();
2076
+ }
2077
+ function injectIntoHead(html, tag) {
2078
+ if (html.includes("</head>")) {
2079
+ return html.replace("</head>", ` ${tag}
2080
+ </head>`);
2081
+ }
2082
+ if (html.includes("<body")) {
2083
+ return html.replace(/<body/, `${tag}
2084
+ <body`);
2085
+ }
2086
+ return `${tag}
2087
+ ${html}`;
2088
+ }
2089
+
2090
+ // src/index.ts
2091
+ var Diorama = class {
2092
+ options;
2093
+ cache;
2094
+ constructor(options = {}) {
2095
+ this.options = {
2096
+ cache: options.cache ?? true,
2097
+ cacheTTL: options.cacheTTL ?? 3600,
2098
+ cacheStrategy: options.cacheStrategy ?? "normal",
2099
+ cdnProvider: options.cdnProvider ?? "esm.sh",
2100
+ githubToken: options.githubToken ?? "",
2101
+ esbuildWasmURL: options.esbuildWasmURL ?? "auto",
2102
+ maxConcurrentFetches: options.maxConcurrentFetches ?? 6,
2103
+ timeout: options.timeout ?? 3e4
2104
+ };
2105
+ this.cache = new CacheManager({
2106
+ enabled: this.options.cache,
2107
+ ttl: this.options.cacheTTL,
2108
+ strategy: this.options.cacheStrategy
2109
+ });
2110
+ }
2111
+ // ─── Render ────────────────────────────────────────────────
2112
+ /**
2113
+ * Fetch, transform, and render a GitHub project inside a
2114
+ * sandboxed iframe.
2115
+ *
2116
+ * @param container CSS selector or DOM element.
2117
+ * @param repoURL GitHub URL or `owner/repo` shorthand.
2118
+ * @param options Per-render options.
2119
+ * @returns A handle for reloading or destroying the instance.
2120
+ */
2121
+ async render(container, repoURL, options = {}) {
2122
+ const renderImpl = async () => {
2123
+ const project = await this.fetchProject(repoURL, options);
2124
+ const repoName2 = `${project.owner}/${project.repo}`;
2125
+ const config = analyzeProject(
2126
+ project,
2127
+ options.projectType,
2128
+ options.entryPoint
2129
+ );
2130
+ let files = project.files;
2131
+ const needsTranspile = config.type === "jsx" || config.type === "typescript" || config.type === "jsx-typescript" || config.type === "vite" && (config.hasJSX || config.hasTypeScript);
2132
+ if (needsTranspile) {
2133
+ files = await transpileAll(
2134
+ files,
2135
+ config.framework,
2136
+ this.options.esbuildWasmURL
2137
+ );
2138
+ }
2139
+ if (config.type !== "static") {
2140
+ const reactVersion = config.framework === "react" ? config.dependencies["react"] : void 0;
2141
+ for (const [path, content] of files) {
2142
+ if (/\.(js|ts|jsx|tsx|mjs)$/.test(path)) {
2143
+ const rewritten = rewriteImports(content, {
2144
+ dependencies: config.dependencies,
2145
+ cdnProvider: this.options.cdnProvider,
2146
+ framework: config.framework,
2147
+ reactVersion,
2148
+ isVite: config.isVite
2149
+ });
2150
+ files.set(path, rewritten);
2151
+ }
2152
+ }
2153
+ }
2154
+ const { html: html2, usesESM: usesESM2 } = assembleHTML({
2155
+ project,
2156
+ config,
2157
+ transformedFiles: files
2158
+ });
2159
+ return { html: html2, usesESM: usesESM2, repoName: repoName2 };
2160
+ };
2161
+ let html;
2162
+ let usesESM;
2163
+ let repoName;
2164
+ try {
2165
+ const result = await withTimeout(
2166
+ renderImpl(),
2167
+ this.options.timeout
2168
+ );
2169
+ html = result.html;
2170
+ usesESM = result.usesESM;
2171
+ repoName = result.repoName;
2172
+ } catch (err) {
2173
+ const dioErr = err instanceof DioramaError ? err : new DioramaError("UNKNOWN", String(err));
2174
+ options.onError?.(dioErr);
2175
+ html = buildErrorHTML(dioErr.message);
2176
+ usesESM = false;
2177
+ repoName = repoURL;
2178
+ }
2179
+ const sandbox = createSandbox({
2180
+ container,
2181
+ html,
2182
+ height: options.height,
2183
+ sandboxFlags: options.sandbox,
2184
+ loading: options.loading,
2185
+ frame: options.frame,
2186
+ expand: options.expand,
2187
+ placeholder: options.placeholder,
2188
+ repoName,
2189
+ useBlobURL: usesESM,
2190
+ onLoad: options.onLoad,
2191
+ onError: options.onError
2192
+ });
2193
+ const instance = {
2194
+ reload: async () => {
2195
+ try {
2196
+ const result = await renderImpl();
2197
+ sandbox.update(result.html);
2198
+ options.onLoad?.();
2199
+ } catch (err) {
2200
+ const dioErr = err instanceof DioramaError ? err : new DioramaError("UNKNOWN", String(err));
2201
+ options.onError?.(dioErr);
2202
+ sandbox.update(buildErrorHTML(dioErr.message));
2203
+ }
2204
+ },
2205
+ destroy: () => {
2206
+ sandbox.destroy();
2207
+ }
2208
+ };
2209
+ return instance;
2210
+ }
2211
+ // ─── Utilities ─────────────────────────────────────────────
2212
+ /**
2213
+ * Pre-fetch a project (resolve + cache) without rendering.
2214
+ */
2215
+ async prefetch(repoURL, options = {}) {
2216
+ await this.fetchProject(repoURL, options);
2217
+ }
2218
+ /**
2219
+ * Clear cached data.
2220
+ * @param repoSlug Optional `owner/repo` to clear a specific entry.
2221
+ */
2222
+ clearCache(repoSlug) {
2223
+ this.cache.clear(repoSlug);
2224
+ }
2225
+ // ─── Internal ──────────────────────────────────────────────
2226
+ async fetchProject(repoURL, options) {
2227
+ const project = await resolveProject(repoURL, {
2228
+ githubToken: this.options.githubToken || void 0,
2229
+ maxConcurrentFetches: this.options.maxConcurrentFetches,
2230
+ branch: options.branch,
2231
+ subdirectory: options.subdirectory,
2232
+ exclude: options.exclude
2233
+ });
2234
+ await this.cache.set(project);
2235
+ return project;
2236
+ }
2237
+ };
2238
+ function withTimeout(promise, ms) {
2239
+ return new Promise((resolve, reject) => {
2240
+ const timer = setTimeout(
2241
+ () => reject(new DioramaError("TIMEOUT", `Render timed out after ${ms}ms`)),
2242
+ ms
2243
+ );
2244
+ promise.then((val) => {
2245
+ clearTimeout(timer);
2246
+ resolve(val);
2247
+ }).catch((err) => {
2248
+ clearTimeout(timer);
2249
+ reject(err);
2250
+ });
2251
+ });
2252
+ }
2253
+
2254
+ // src/adapters/vue.ts
2255
+ var sharedInstance = null;
2256
+ function getOrCreateInstance(options) {
2257
+ if (!sharedInstance) {
2258
+ sharedInstance = new Diorama(options);
2259
+ }
2260
+ return sharedInstance;
2261
+ }
2262
+ var dioramaProps = {
2263
+ repo: { type: String, required: true },
2264
+ branch: { type: String, default: void 0 },
2265
+ subdirectory: { type: String, default: void 0 },
2266
+ loading: { type: String, default: "eager" },
2267
+ placeholder: { type: String, default: void 0 },
2268
+ height: { type: String, default: "500px" },
2269
+ frame: { type: String, default: "none" },
2270
+ expand: { type: Boolean, default: false },
2271
+ options: { type: Object, default: void 0 }
2272
+ };
2273
+ var DioramaPreview = defineComponent({
2274
+ name: "DioramaPreview",
2275
+ props: dioramaProps,
2276
+ emits: ["load", "error"],
2277
+ setup(props, { emit }) {
2278
+ const containerRef = ref(null);
2279
+ let instance = null;
2280
+ async function renderProject() {
2281
+ const container = containerRef.value;
2282
+ if (!container) return;
2283
+ instance?.destroy();
2284
+ instance = null;
2285
+ const diorama = getOrCreateInstance(props.options);
2286
+ try {
2287
+ instance = await diorama.render(container, props.repo, {
2288
+ branch: props.branch,
2289
+ subdirectory: props.subdirectory,
2290
+ loading: props.loading,
2291
+ placeholder: props.placeholder,
2292
+ height: props.height,
2293
+ frame: props.frame,
2294
+ expand: props.expand,
2295
+ onLoad: () => emit("load"),
2296
+ onError: (err) => emit("error", err)
2297
+ });
2298
+ } catch (err) {
2299
+ emit("error", err);
2300
+ }
2301
+ }
2302
+ onMounted(() => {
2303
+ renderProject();
2304
+ });
2305
+ onUnmounted(() => {
2306
+ instance?.destroy();
2307
+ instance = null;
2308
+ });
2309
+ watch(
2310
+ () => [props.repo, props.branch, props.subdirectory, props.loading, props.height, props.frame, props.expand],
2311
+ () => {
2312
+ renderProject();
2313
+ }
2314
+ );
2315
+ return () => h("div", {
2316
+ ref: containerRef,
2317
+ style: { width: "100%", minHeight: props.height }
2318
+ });
2319
+ }
2320
+ });
2321
+
2322
+ export { Diorama, DioramaPreview };
2323
+ //# sourceMappingURL=vue.js.map
2324
+ //# sourceMappingURL=vue.js.map