prev-cli 0.11.1 → 0.12.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/cli.js +227 -22
- package/dist/preview-runtime/build.d.ts +10 -0
- package/dist/preview-runtime/types.d.ts +32 -0
- package/dist/vite/previews.d.ts +17 -0
- package/package.json +1 -1
- package/src/theme/Preview.tsx +93 -9
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "util";
|
|
|
5
5
|
import path7 from "path";
|
|
6
6
|
|
|
7
7
|
// src/vite/start.ts
|
|
8
|
-
import { createServer as createServer2, build, preview } from "vite";
|
|
8
|
+
import { createServer as createServer2, build as build2, preview } from "vite";
|
|
9
9
|
|
|
10
10
|
// src/vite/config.ts
|
|
11
11
|
import { createLogger } from "vite";
|
|
@@ -15,7 +15,7 @@ import remarkGfm from "remark-gfm";
|
|
|
15
15
|
import rehypeHighlight from "rehype-highlight";
|
|
16
16
|
import path6 from "path";
|
|
17
17
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
18
|
-
import { existsSync as existsSync4, readFileSync as
|
|
18
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
19
19
|
|
|
20
20
|
// src/utils/cache.ts
|
|
21
21
|
import { createHash } from "crypto";
|
|
@@ -348,7 +348,7 @@ function entryPlugin(rootDir) {
|
|
|
348
348
|
const html = getHtml(entryPath, false);
|
|
349
349
|
server.middlewares.use(async (req, res, next) => {
|
|
350
350
|
const url = req.url || "/";
|
|
351
|
-
if (url === "/" || !url.includes(".") && !url.startsWith("/@") && !url.startsWith("/_preview
|
|
351
|
+
if (url === "/" || !url.includes(".") && !url.startsWith("/@") && !url.startsWith("/_preview")) {
|
|
352
352
|
try {
|
|
353
353
|
const transformed = await server.transformIndexHtml(url, html);
|
|
354
354
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -370,7 +370,7 @@ function entryPlugin(rootDir) {
|
|
|
370
370
|
// src/vite/previews.ts
|
|
371
371
|
import fg2 from "fast-glob";
|
|
372
372
|
import path4 from "path";
|
|
373
|
-
import { existsSync as existsSync2 } from "fs";
|
|
373
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
374
374
|
async function scanPreviews(rootDir) {
|
|
375
375
|
const previewsDir = path4.join(rootDir, "previews");
|
|
376
376
|
if (!existsSync2(previewsDir)) {
|
|
@@ -390,15 +390,167 @@ async function scanPreviews(rootDir) {
|
|
|
390
390
|
};
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
|
+
async function scanPreviewFiles(previewDir) {
|
|
394
|
+
const files = await fg2.glob("**/*.{tsx,ts,jsx,js,css,json}", {
|
|
395
|
+
cwd: previewDir,
|
|
396
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
397
|
+
});
|
|
398
|
+
return files.map((file) => {
|
|
399
|
+
const content = readFileSync2(path4.join(previewDir, file), "utf-8");
|
|
400
|
+
const ext = path4.extname(file).slice(1);
|
|
401
|
+
return {
|
|
402
|
+
path: file,
|
|
403
|
+
content,
|
|
404
|
+
type: ext
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function detectEntry(files) {
|
|
409
|
+
const priorities = ["App.tsx", "App.jsx", "index.tsx", "index.jsx", "main.tsx", "main.jsx"];
|
|
410
|
+
for (const name of priorities) {
|
|
411
|
+
const file = files.find((f) => f.path === name);
|
|
412
|
+
if (file)
|
|
413
|
+
return file.path;
|
|
414
|
+
}
|
|
415
|
+
const jsxFile = files.find((f) => f.type === "tsx" || f.type === "jsx");
|
|
416
|
+
return jsxFile?.path || files[0]?.path || "App.tsx";
|
|
417
|
+
}
|
|
418
|
+
async function buildPreviewConfig(previewDir) {
|
|
419
|
+
const files = await scanPreviewFiles(previewDir);
|
|
420
|
+
const entry = detectEntry(files);
|
|
421
|
+
return {
|
|
422
|
+
files,
|
|
423
|
+
entry,
|
|
424
|
+
tailwind: true
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/preview-runtime/build.ts
|
|
429
|
+
import { build } from "esbuild";
|
|
430
|
+
async function buildPreviewHtml(config) {
|
|
431
|
+
try {
|
|
432
|
+
const virtualFs = {};
|
|
433
|
+
for (const file of config.files) {
|
|
434
|
+
const ext = file.path.split(".").pop()?.toLowerCase();
|
|
435
|
+
const loader = ext === "css" ? "css" : ext === "json" ? "json" : ext || "tsx";
|
|
436
|
+
virtualFs[file.path] = { contents: file.content, loader };
|
|
437
|
+
}
|
|
438
|
+
const entryFile = config.files.find((f) => f.path === config.entry);
|
|
439
|
+
if (!entryFile) {
|
|
440
|
+
return { html: "", error: `Entry file not found: ${config.entry}` };
|
|
441
|
+
}
|
|
442
|
+
const hasDefaultExport = /export\s+default/.test(entryFile.content);
|
|
443
|
+
const entryCode = hasDefaultExport ? `
|
|
444
|
+
import React from 'react'
|
|
445
|
+
import { createRoot } from 'react-dom/client'
|
|
446
|
+
import App from './${config.entry}'
|
|
447
|
+
|
|
448
|
+
const root = createRoot(document.getElementById('root'))
|
|
449
|
+
root.render(React.createElement(App))
|
|
450
|
+
` : `
|
|
451
|
+
import './${config.entry}'
|
|
452
|
+
`;
|
|
453
|
+
const result = await build({
|
|
454
|
+
stdin: {
|
|
455
|
+
contents: entryCode,
|
|
456
|
+
loader: "tsx",
|
|
457
|
+
resolveDir: "/"
|
|
458
|
+
},
|
|
459
|
+
bundle: true,
|
|
460
|
+
write: false,
|
|
461
|
+
format: "esm",
|
|
462
|
+
jsx: "automatic",
|
|
463
|
+
jsxImportSource: "react",
|
|
464
|
+
target: "es2020",
|
|
465
|
+
minify: true,
|
|
466
|
+
plugins: [{
|
|
467
|
+
name: "virtual-fs",
|
|
468
|
+
setup(build2) {
|
|
469
|
+
build2.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => {
|
|
470
|
+
const parts = args.path.split("/");
|
|
471
|
+
const pkg = parts[0];
|
|
472
|
+
const subpath = parts.slice(1).join("/");
|
|
473
|
+
const url = subpath ? `https://esm.sh/${pkg}@18/${subpath}` : `https://esm.sh/${pkg}@18`;
|
|
474
|
+
return { path: url, external: true };
|
|
475
|
+
});
|
|
476
|
+
build2.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
477
|
+
if (args.path.startsWith("https://"))
|
|
478
|
+
return;
|
|
479
|
+
return { path: `https://esm.sh/${args.path}`, external: true };
|
|
480
|
+
});
|
|
481
|
+
build2.onResolve({ filter: /^\./ }, (args) => {
|
|
482
|
+
let resolved = args.path.replace(/^\.\//, "");
|
|
483
|
+
if (!resolved.includes(".")) {
|
|
484
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js", ".css"]) {
|
|
485
|
+
if (virtualFs[resolved + ext]) {
|
|
486
|
+
resolved = resolved + ext;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return { path: resolved, namespace: "virtual" };
|
|
492
|
+
});
|
|
493
|
+
build2.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
|
|
494
|
+
const file = virtualFs[args.path];
|
|
495
|
+
if (file) {
|
|
496
|
+
if (file.loader === "css") {
|
|
497
|
+
const css = file.contents.replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
498
|
+
return {
|
|
499
|
+
contents: `
|
|
500
|
+
const style = document.createElement('style');
|
|
501
|
+
style.textContent = \`${css}\`;
|
|
502
|
+
document.head.appendChild(style);
|
|
503
|
+
`,
|
|
504
|
+
loader: "js"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return { contents: file.contents, loader: file.loader };
|
|
508
|
+
}
|
|
509
|
+
return { contents: "", loader: "empty" };
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}]
|
|
513
|
+
});
|
|
514
|
+
const jsFile = result.outputFiles.find((f) => f.path.endsWith(".js")) || result.outputFiles[0];
|
|
515
|
+
const jsCode = jsFile?.text || "";
|
|
516
|
+
const html = `<!DOCTYPE html>
|
|
517
|
+
<html lang="en">
|
|
518
|
+
<head>
|
|
519
|
+
<meta charset="UTF-8">
|
|
520
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
521
|
+
<title>Preview</title>
|
|
522
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
523
|
+
<style>
|
|
524
|
+
body { margin: 0; }
|
|
525
|
+
#root { min-height: 100vh; }
|
|
526
|
+
</style>
|
|
527
|
+
</head>
|
|
528
|
+
<body>
|
|
529
|
+
<div id="root"></div>
|
|
530
|
+
<script type="module">${jsCode}</script>
|
|
531
|
+
</body>
|
|
532
|
+
</html>`;
|
|
533
|
+
return { html };
|
|
534
|
+
} catch (err) {
|
|
535
|
+
return {
|
|
536
|
+
html: "",
|
|
537
|
+
error: err instanceof Error ? err.message : String(err)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
393
541
|
|
|
394
542
|
// src/vite/plugins/previews-plugin.ts
|
|
395
|
-
import { existsSync as existsSync3,
|
|
543
|
+
import { existsSync as existsSync3, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
396
544
|
import path5 from "path";
|
|
397
545
|
var VIRTUAL_MODULE_ID2 = "virtual:prev-previews";
|
|
398
546
|
var RESOLVED_VIRTUAL_MODULE_ID2 = "\x00" + VIRTUAL_MODULE_ID2;
|
|
399
547
|
function previewsPlugin(rootDir) {
|
|
548
|
+
let isBuild = false;
|
|
400
549
|
return {
|
|
401
550
|
name: "prev-previews",
|
|
551
|
+
config(_, { command }) {
|
|
552
|
+
isBuild = command === "build";
|
|
553
|
+
},
|
|
402
554
|
resolveId(id) {
|
|
403
555
|
if (id === VIRTUAL_MODULE_ID2) {
|
|
404
556
|
return RESOLVED_VIRTUAL_MODULE_ID2;
|
|
@@ -411,7 +563,7 @@ function previewsPlugin(rootDir) {
|
|
|
411
563
|
}
|
|
412
564
|
},
|
|
413
565
|
handleHotUpdate({ file, server }) {
|
|
414
|
-
if (file.includes("/previews/") &&
|
|
566
|
+
if (file.includes("/previews/") && /\.(html|tsx|ts|jsx|js|css)$/.test(file)) {
|
|
415
567
|
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID2);
|
|
416
568
|
if (mod) {
|
|
417
569
|
server.moduleGraph.invalidateModule(mod);
|
|
@@ -419,16 +571,40 @@ function previewsPlugin(rootDir) {
|
|
|
419
571
|
}
|
|
420
572
|
}
|
|
421
573
|
},
|
|
422
|
-
closeBundle() {
|
|
574
|
+
async closeBundle() {
|
|
575
|
+
if (!isBuild)
|
|
576
|
+
return;
|
|
423
577
|
const distDir = path5.join(rootDir, "dist");
|
|
424
|
-
const previewsDir = path5.join(distDir, "previews");
|
|
425
578
|
const targetDir = path5.join(distDir, "_preview");
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
579
|
+
const previewsDir = path5.join(rootDir, "previews");
|
|
580
|
+
const oldPreviewsDir = path5.join(distDir, "previews");
|
|
581
|
+
if (existsSync3(oldPreviewsDir)) {
|
|
582
|
+
rmSync(oldPreviewsDir, { recursive: true });
|
|
583
|
+
}
|
|
584
|
+
if (existsSync3(targetDir)) {
|
|
585
|
+
rmSync(targetDir, { recursive: true });
|
|
586
|
+
}
|
|
587
|
+
const previews = await scanPreviews(rootDir);
|
|
588
|
+
if (previews.length === 0)
|
|
589
|
+
return;
|
|
590
|
+
console.log(`
|
|
591
|
+
Building ${previews.length} preview(s)...`);
|
|
592
|
+
for (const preview of previews) {
|
|
593
|
+
const previewDir = path5.join(previewsDir, preview.name);
|
|
594
|
+
try {
|
|
595
|
+
const config = await buildPreviewConfig(previewDir);
|
|
596
|
+
const result = await buildPreviewHtml(config);
|
|
597
|
+
if (result.error) {
|
|
598
|
+
console.error(` ✗ ${preview.name}: ${result.error}`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const outputDir = path5.join(targetDir, preview.name);
|
|
602
|
+
mkdirSync(outputDir, { recursive: true });
|
|
603
|
+
writeFileSync2(path5.join(outputDir, "index.html"), result.html);
|
|
604
|
+
console.log(` ✓ ${preview.name}`);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.error(` ✗ ${preview.name}: ${err}`);
|
|
430
607
|
}
|
|
431
|
-
renameSync(previewsDir, targetDir);
|
|
432
608
|
}
|
|
433
609
|
}
|
|
434
610
|
};
|
|
@@ -494,7 +670,7 @@ function findCliRoot2() {
|
|
|
494
670
|
const pkgPath = path6.join(dir, "package.json");
|
|
495
671
|
if (existsSync4(pkgPath)) {
|
|
496
672
|
try {
|
|
497
|
-
const pkg = JSON.parse(
|
|
673
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
498
674
|
if (pkg.name === "prev-cli") {
|
|
499
675
|
return dir;
|
|
500
676
|
}
|
|
@@ -530,8 +706,6 @@ var srcRoot2 = path6.join(cliRoot2, "src");
|
|
|
530
706
|
async function createViteConfig(options) {
|
|
531
707
|
const { rootDir, mode, port, include } = options;
|
|
532
708
|
const cacheDir = await ensureCacheDir(rootDir);
|
|
533
|
-
const previews = await scanPreviews(rootDir);
|
|
534
|
-
const previewInputs = Object.fromEntries(previews.map((p) => [`_preview/${p.name}`, p.htmlPath]));
|
|
535
709
|
return {
|
|
536
710
|
root: rootDir,
|
|
537
711
|
mode,
|
|
@@ -561,8 +735,40 @@ async function createViteConfig(options) {
|
|
|
561
735
|
},
|
|
562
736
|
configureServer(server) {
|
|
563
737
|
server.middlewares.use(async (req, res, next) => {
|
|
564
|
-
|
|
565
|
-
|
|
738
|
+
const urlPath = req.url?.split("?")[0] || "";
|
|
739
|
+
if (urlPath === "/_preview-runtime") {
|
|
740
|
+
const templatePath = path6.join(srcRoot2, "preview-runtime/template.html");
|
|
741
|
+
if (existsSync4(templatePath)) {
|
|
742
|
+
const html = readFileSync3(templatePath, "utf-8");
|
|
743
|
+
res.setHeader("Content-Type", "text/html");
|
|
744
|
+
res.end(html);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (urlPath.startsWith("/_preview-config/")) {
|
|
749
|
+
const previewName = decodeURIComponent(urlPath.slice("/_preview-config/".length));
|
|
750
|
+
const previewsDir = path6.join(rootDir, "previews");
|
|
751
|
+
const previewDir = path6.resolve(previewsDir, previewName);
|
|
752
|
+
if (!previewDir.startsWith(previewsDir)) {
|
|
753
|
+
res.statusCode = 403;
|
|
754
|
+
res.end("Forbidden");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (existsSync4(previewDir)) {
|
|
758
|
+
try {
|
|
759
|
+
const config = await buildPreviewConfig(previewDir);
|
|
760
|
+
res.setHeader("Content-Type", "application/json");
|
|
761
|
+
res.end(JSON.stringify(config));
|
|
762
|
+
return;
|
|
763
|
+
} catch (err) {
|
|
764
|
+
console.error("Error building preview config:", err);
|
|
765
|
+
res.statusCode = 500;
|
|
766
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (urlPath.startsWith("/_preview/")) {
|
|
566
772
|
const isHtmlRequest = !path6.extname(urlPath) || urlPath.endsWith("/");
|
|
567
773
|
if (isHtmlRequest) {
|
|
568
774
|
const previewName = decodeURIComponent(urlPath.slice("/_preview/".length).replace(/\/$/, ""));
|
|
@@ -573,7 +779,7 @@ async function createViteConfig(options) {
|
|
|
573
779
|
}
|
|
574
780
|
if (existsSync4(htmlPath)) {
|
|
575
781
|
try {
|
|
576
|
-
let html =
|
|
782
|
+
let html = readFileSync3(htmlPath, "utf-8");
|
|
577
783
|
const previewBase = `/_preview/${previewName}/`;
|
|
578
784
|
html = html.replace(/(src|href)=["']\.\/([^"']+)["']/g, `$1="${previewBase}$2"`);
|
|
579
785
|
const transformed = await server.transformIndexHtml(req.url, html);
|
|
@@ -649,8 +855,7 @@ async function createViteConfig(options) {
|
|
|
649
855
|
chunkSizeWarningLimit: 1e4,
|
|
650
856
|
rollupOptions: {
|
|
651
857
|
input: {
|
|
652
|
-
main: path6.join(srcRoot2, "theme/index.html")
|
|
653
|
-
...previewInputs
|
|
858
|
+
main: path6.join(srcRoot2, "theme/index.html")
|
|
654
859
|
}
|
|
655
860
|
}
|
|
656
861
|
}
|
|
@@ -730,7 +935,7 @@ async function buildSite(rootDir, options = {}) {
|
|
|
730
935
|
mode: "production",
|
|
731
936
|
include: options.include
|
|
732
937
|
});
|
|
733
|
-
await
|
|
938
|
+
await build2(config);
|
|
734
939
|
console.log();
|
|
735
940
|
console.log(" Done! Your site is ready in ./dist");
|
|
736
941
|
console.log(" You can deploy this folder anywhere.");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PreviewConfig } from './types';
|
|
2
|
+
export interface PreviewBuildResult {
|
|
3
|
+
html: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Build a preview into a standalone HTML file for production
|
|
8
|
+
* Uses esbuild (native) to bundle at build time
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildPreviewHtml(config: PreviewConfig): Promise<PreviewBuildResult>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface PreviewFile {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
type: 'tsx' | 'ts' | 'jsx' | 'js' | 'css' | 'html' | 'json';
|
|
5
|
+
}
|
|
6
|
+
export interface PreviewConfig {
|
|
7
|
+
files: PreviewFile[];
|
|
8
|
+
entry: string;
|
|
9
|
+
tailwind?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface BuildResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
code?: string;
|
|
14
|
+
css?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
buildTime?: number;
|
|
17
|
+
}
|
|
18
|
+
export type PreviewMessage = {
|
|
19
|
+
type: 'init';
|
|
20
|
+
config: PreviewConfig;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'update';
|
|
23
|
+
files: PreviewFile[];
|
|
24
|
+
} | {
|
|
25
|
+
type: 'ready';
|
|
26
|
+
} | {
|
|
27
|
+
type: 'built';
|
|
28
|
+
result: BuildResult;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'error';
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
package/dist/vite/previews.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
import type { PreviewFile, PreviewConfig } from '../preview-runtime/types';
|
|
1
2
|
export interface Preview {
|
|
2
3
|
name: string;
|
|
3
4
|
route: string;
|
|
4
5
|
htmlPath: string;
|
|
5
6
|
}
|
|
7
|
+
export interface PreviewWithFiles extends Preview {
|
|
8
|
+
files: PreviewFile[];
|
|
9
|
+
entry: string;
|
|
10
|
+
}
|
|
6
11
|
export declare function scanPreviews(rootDir: string): Promise<Preview[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Scan all files in a preview directory for WASM bundling
|
|
14
|
+
*/
|
|
15
|
+
export declare function scanPreviewFiles(previewDir: string): Promise<PreviewFile[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Detect the entry file for a preview (looks for App.tsx, index.tsx, etc.)
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectEntry(files: PreviewFile[]): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build a PreviewConfig for WASM runtime
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildPreviewConfig(previewDir: string): Promise<PreviewConfig>;
|
package/package.json
CHANGED
package/src/theme/Preview.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
|
-
import { Smartphone, Tablet, Monitor, SunMedium, Moon, Maximize2, Minimize2, GripVertical, SlidersHorizontal, X } from 'lucide-react'
|
|
2
|
+
import { Smartphone, Tablet, Monitor, SunMedium, Moon, Maximize2, Minimize2, GripVertical, SlidersHorizontal, X, Loader2 } from 'lucide-react'
|
|
3
|
+
import type { PreviewConfig, PreviewMessage, BuildResult } from '../preview-runtime/types'
|
|
3
4
|
|
|
4
5
|
interface PreviewProps {
|
|
5
6
|
src: string
|
|
6
7
|
height?: string | number
|
|
7
8
|
title?: string
|
|
9
|
+
mode?: 'wasm' | 'legacy' // 'wasm' uses browser bundling, 'legacy' uses Vite
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
type DeviceMode = 'mobile' | 'tablet' | 'desktop'
|
|
@@ -20,7 +22,7 @@ interface Position {
|
|
|
20
22
|
y: number
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export function Preview({ src, height = 400, title }: PreviewProps) {
|
|
25
|
+
export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProps) {
|
|
24
26
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
25
27
|
const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop')
|
|
26
28
|
const [customWidth, setCustomWidth] = useState<number | null>(null)
|
|
@@ -30,11 +32,17 @@ export function Preview({ src, height = 400, title }: PreviewProps) {
|
|
|
30
32
|
const [isDragging, setIsDragging] = useState(false)
|
|
31
33
|
const [dragOffset, setDragOffset] = useState<Position>({ x: 0, y: 0 })
|
|
32
34
|
|
|
35
|
+
// WASM preview state
|
|
36
|
+
const [buildStatus, setBuildStatus] = useState<'loading' | 'building' | 'ready' | 'error'>('loading')
|
|
37
|
+
const [buildTime, setBuildTime] = useState<number | null>(null)
|
|
38
|
+
const [buildError, setBuildError] = useState<string | null>(null)
|
|
39
|
+
|
|
33
40
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
34
41
|
const pillRef = useRef<HTMLDivElement>(null)
|
|
35
42
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
// URL depends on mode
|
|
45
|
+
const previewUrl = mode === 'wasm' ? '/_preview-runtime' : `/_preview/${src}`
|
|
38
46
|
const displayTitle = title || src
|
|
39
47
|
|
|
40
48
|
// Calculate current width
|
|
@@ -88,6 +96,60 @@ export function Preview({ src, height = 400, title }: PreviewProps) {
|
|
|
88
96
|
}
|
|
89
97
|
}, [isDarkMode])
|
|
90
98
|
|
|
99
|
+
// WASM preview: Initialize and send config to iframe
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (mode !== 'wasm') return
|
|
102
|
+
|
|
103
|
+
const iframe = iframeRef.current
|
|
104
|
+
if (!iframe) return
|
|
105
|
+
|
|
106
|
+
let configSent = false
|
|
107
|
+
|
|
108
|
+
// Handle messages from iframe
|
|
109
|
+
const handleMessage = (event: MessageEvent) => {
|
|
110
|
+
const msg = event.data as PreviewMessage
|
|
111
|
+
|
|
112
|
+
if (msg.type === 'ready' && !configSent) {
|
|
113
|
+
// Iframe is ready, fetch and send config
|
|
114
|
+
configSent = true
|
|
115
|
+
setBuildStatus('building')
|
|
116
|
+
|
|
117
|
+
fetch(`/_preview-config/${src}`)
|
|
118
|
+
.then(res => res.json())
|
|
119
|
+
.then((config: PreviewConfig) => {
|
|
120
|
+
iframe.contentWindow?.postMessage({ type: 'init', config } as PreviewMessage, '*')
|
|
121
|
+
})
|
|
122
|
+
.catch(err => {
|
|
123
|
+
setBuildStatus('error')
|
|
124
|
+
setBuildError(`Failed to load preview config: ${err.message}`)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (msg.type === 'built') {
|
|
129
|
+
const result = msg.result as BuildResult
|
|
130
|
+
if (result.success) {
|
|
131
|
+
setBuildStatus('ready')
|
|
132
|
+
setBuildTime(result.buildTime || null)
|
|
133
|
+
setBuildError(null)
|
|
134
|
+
} else {
|
|
135
|
+
setBuildStatus('error')
|
|
136
|
+
setBuildError(result.error || 'Build failed')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (msg.type === 'error') {
|
|
141
|
+
setBuildStatus('error')
|
|
142
|
+
setBuildError(msg.error)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
window.addEventListener('message', handleMessage)
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
window.removeEventListener('message', handleMessage)
|
|
150
|
+
}
|
|
151
|
+
}, [mode, src])
|
|
152
|
+
|
|
91
153
|
// Drag handlers
|
|
92
154
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
93
155
|
if (!pillRef.current) return
|
|
@@ -344,14 +406,36 @@ export function Preview({ src, height = 400, title }: PreviewProps) {
|
|
|
344
406
|
>
|
|
345
407
|
{/* Header */}
|
|
346
408
|
<div className="flex items-center justify-between px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
|
|
409
|
+
<div className="flex items-center gap-2">
|
|
410
|
+
<span className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
|
411
|
+
{displayTitle}
|
|
412
|
+
</span>
|
|
413
|
+
{mode === 'wasm' && buildStatus === 'building' && (
|
|
414
|
+
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />
|
|
415
|
+
)}
|
|
416
|
+
{mode === 'wasm' && buildStatus === 'error' && (
|
|
417
|
+
<span className="text-xs text-red-500">Error</span>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
<div className="flex items-center gap-2">
|
|
421
|
+
{mode === 'wasm' && buildTime && (
|
|
422
|
+
<span className="text-xs text-zinc-400">{buildTime}ms</span>
|
|
423
|
+
)}
|
|
424
|
+
<span className="text-xs text-zinc-400">
|
|
425
|
+
{currentWidth ? `${currentWidth}px` : '100%'}
|
|
426
|
+
</span>
|
|
427
|
+
</div>
|
|
353
428
|
</div>
|
|
354
429
|
|
|
430
|
+
{/* Build error display */}
|
|
431
|
+
{mode === 'wasm' && buildError && (
|
|
432
|
+
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800">
|
|
433
|
+
<pre className="text-xs text-red-600 dark:text-red-400 whitespace-pre-wrap font-mono overflow-auto max-h-32">
|
|
434
|
+
{buildError}
|
|
435
|
+
</pre>
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
|
|
355
439
|
{/* Preview area with checkered background */}
|
|
356
440
|
<div
|
|
357
441
|
className="relative bg-zinc-100 dark:bg-zinc-900"
|