prev-cli 0.11.0 → 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 +251 -29
- 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("/@")) {
|
|
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,
|
|
@@ -549,25 +723,74 @@ async function createViteConfig(options) {
|
|
|
549
723
|
previewsPlugin(rootDir),
|
|
550
724
|
{
|
|
551
725
|
name: "prev-preview-server",
|
|
726
|
+
resolveId(id) {
|
|
727
|
+
if (id.startsWith("/_preview/")) {
|
|
728
|
+
const relativePath = id.slice("/_preview/".length);
|
|
729
|
+
const previewsDir = path6.join(rootDir, "previews");
|
|
730
|
+
const resolved = path6.resolve(previewsDir, relativePath);
|
|
731
|
+
if (resolved.startsWith(previewsDir)) {
|
|
732
|
+
return resolved;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
},
|
|
552
736
|
configureServer(server) {
|
|
553
737
|
server.middlewares.use(async (req, res, next) => {
|
|
554
|
-
|
|
555
|
-
|
|
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));
|
|
556
750
|
const previewsDir = path6.join(rootDir, "previews");
|
|
557
|
-
const
|
|
558
|
-
if (!
|
|
559
|
-
|
|
751
|
+
const previewDir = path6.resolve(previewsDir, previewName);
|
|
752
|
+
if (!previewDir.startsWith(previewsDir)) {
|
|
753
|
+
res.statusCode = 403;
|
|
754
|
+
res.end("Forbidden");
|
|
755
|
+
return;
|
|
560
756
|
}
|
|
561
|
-
if (existsSync4(
|
|
757
|
+
if (existsSync4(previewDir)) {
|
|
562
758
|
try {
|
|
563
|
-
const
|
|
564
|
-
res.setHeader("Content-Type", "
|
|
565
|
-
res.end(
|
|
759
|
+
const config = await buildPreviewConfig(previewDir);
|
|
760
|
+
res.setHeader("Content-Type", "application/json");
|
|
761
|
+
res.end(JSON.stringify(config));
|
|
566
762
|
return;
|
|
567
763
|
} catch (err) {
|
|
568
|
-
console.error("Error
|
|
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/")) {
|
|
772
|
+
const isHtmlRequest = !path6.extname(urlPath) || urlPath.endsWith("/");
|
|
773
|
+
if (isHtmlRequest) {
|
|
774
|
+
const previewName = decodeURIComponent(urlPath.slice("/_preview/".length).replace(/\/$/, ""));
|
|
775
|
+
const previewsDir = path6.join(rootDir, "previews");
|
|
776
|
+
const htmlPath = path6.resolve(previewsDir, previewName, "index.html");
|
|
777
|
+
if (!htmlPath.startsWith(previewsDir)) {
|
|
569
778
|
return next();
|
|
570
779
|
}
|
|
780
|
+
if (existsSync4(htmlPath)) {
|
|
781
|
+
try {
|
|
782
|
+
let html = readFileSync3(htmlPath, "utf-8");
|
|
783
|
+
const previewBase = `/_preview/${previewName}/`;
|
|
784
|
+
html = html.replace(/(src|href)=["']\.\/([^"']+)["']/g, `$1="${previewBase}$2"`);
|
|
785
|
+
const transformed = await server.transformIndexHtml(req.url, html);
|
|
786
|
+
res.setHeader("Content-Type", "text/html");
|
|
787
|
+
res.end(transformed);
|
|
788
|
+
return;
|
|
789
|
+
} catch (err) {
|
|
790
|
+
console.error("Error serving preview:", err);
|
|
791
|
+
return next();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
571
794
|
}
|
|
572
795
|
}
|
|
573
796
|
next();
|
|
@@ -632,8 +855,7 @@ async function createViteConfig(options) {
|
|
|
632
855
|
chunkSizeWarningLimit: 1e4,
|
|
633
856
|
rollupOptions: {
|
|
634
857
|
input: {
|
|
635
|
-
main: path6.join(srcRoot2, "theme/index.html")
|
|
636
|
-
...previewInputs
|
|
858
|
+
main: path6.join(srcRoot2, "theme/index.html")
|
|
637
859
|
}
|
|
638
860
|
}
|
|
639
861
|
}
|
|
@@ -713,7 +935,7 @@ async function buildSite(rootDir, options = {}) {
|
|
|
713
935
|
mode: "production",
|
|
714
936
|
include: options.include
|
|
715
937
|
});
|
|
716
|
-
await
|
|
938
|
+
await build2(config);
|
|
717
939
|
console.log();
|
|
718
940
|
console.log(" Done! Your site is ready in ./dist");
|
|
719
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"
|