prev-cli 0.24.12 → 0.24.13

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 CHANGED
@@ -3,11 +3,11 @@
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
5
  import path11 from "path";
6
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, rmSync as rmSync3, readFileSync as readFileSync6 } from "fs";
7
- import { fileURLToPath as fileURLToPath3 } from "url";
6
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6, rmSync as rmSync4, readFileSync as readFileSync7 } from "fs";
7
+ import { fileURLToPath as fileURLToPath4 } from "url";
8
8
 
9
9
  // src/vite/start.ts
10
- import { createServer as createServer2, build as build2, preview } from "vite";
10
+ import { createServer as createServer2, build as build3, preview } from "vite";
11
11
 
12
12
  // src/vite/config.ts
13
13
  import { createLogger } from "vite";
@@ -17,8 +17,8 @@ import remarkGfm from "remark-gfm";
17
17
  import rehypeHighlight from "rehype-highlight";
18
18
  import path8 from "path";
19
19
  import os from "os";
20
- import { fileURLToPath as fileURLToPath2 } from "url";
21
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
20
+ import { fileURLToPath as fileURLToPath3 } from "url";
21
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
22
22
 
23
23
  // src/vite/plugins/pages-plugin.ts
24
24
  import path2 from "path";
@@ -614,9 +614,95 @@ async function detectUnitFiles(unitDir, type) {
614
614
  return result;
615
615
  }
616
616
 
617
- // src/preview-runtime/build.ts
617
+ // src/preview-runtime/vendors.ts
618
618
  import { build } from "esbuild";
619
- async function buildPreviewHtml(config) {
619
+ import { dirname } from "path";
620
+ import { fileURLToPath as fileURLToPath2 } from "url";
621
+ var __dirname2 = dirname(fileURLToPath2(import.meta.url));
622
+ async function buildVendorBundle() {
623
+ try {
624
+ const entryCode = `
625
+ import * as React from 'react'
626
+ import * as ReactDOM from 'react-dom'
627
+ import { createRoot } from 'react-dom/client'
628
+ export { jsx, jsxs, Fragment } from 'react/jsx-runtime'
629
+ export { React, ReactDOM, createRoot }
630
+ export default React
631
+ `;
632
+ const result = await build({
633
+ stdin: {
634
+ contents: entryCode,
635
+ loader: "ts",
636
+ resolveDir: __dirname2
637
+ },
638
+ bundle: true,
639
+ write: false,
640
+ format: "esm",
641
+ target: "es2020",
642
+ minify: true
643
+ });
644
+ const jsFile = result.outputFiles?.find((f) => f.path.endsWith(".js")) || result.outputFiles?.[0];
645
+ if (!jsFile) {
646
+ return { success: false, code: "", error: "No output generated" };
647
+ }
648
+ return { success: true, code: jsFile.text };
649
+ } catch (err) {
650
+ return {
651
+ success: false,
652
+ code: "",
653
+ error: err instanceof Error ? err.message : String(err)
654
+ };
655
+ }
656
+ }
657
+
658
+ // src/preview-runtime/build-optimized.ts
659
+ import { build as build2 } from "esbuild";
660
+
661
+ // src/preview-runtime/tailwind.ts
662
+ import { $ } from "bun";
663
+ import { mkdtempSync, mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
664
+ import { join, dirname as dirname2 } from "path";
665
+ import { tmpdir } from "os";
666
+ async function compileTailwind(files) {
667
+ const tempDir = mkdtempSync(join(tmpdir(), "prev-tailwind-"));
668
+ try {
669
+ for (const file of files) {
670
+ const filePath = join(tempDir, file.path);
671
+ const parentDir = dirname2(filePath);
672
+ mkdirSync(parentDir, { recursive: true });
673
+ writeFileSync2(filePath, file.content);
674
+ }
675
+ const configContent = `
676
+ module.exports = {
677
+ content: [${JSON.stringify(tempDir + "/**/*.{tsx,jsx,ts,js,html}")}],
678
+ }
679
+ `;
680
+ const configPath = join(tempDir, "tailwind.config.cjs");
681
+ writeFileSync2(configPath, configContent);
682
+ const inputCss = `
683
+ @tailwind base;
684
+ @tailwind components;
685
+ @tailwind utilities;
686
+ `;
687
+ const inputPath = join(tempDir, "input.css");
688
+ writeFileSync2(inputPath, inputCss);
689
+ const outputPath = join(tempDir, "output.css");
690
+ await $`bunx tailwindcss -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet();
691
+ const css = readFileSync4(outputPath, "utf-8");
692
+ return { success: true, css };
693
+ } catch (err) {
694
+ return {
695
+ success: false,
696
+ css: "",
697
+ error: err instanceof Error ? err.message : String(err)
698
+ };
699
+ } finally {
700
+ rmSync(tempDir, { recursive: true, force: true });
701
+ }
702
+ }
703
+
704
+ // src/preview-runtime/build-optimized.ts
705
+ async function buildOptimizedPreview(config, options) {
620
706
  try {
621
707
  const virtualFs = {};
622
708
  for (const file of config.files) {
@@ -626,25 +712,18 @@ async function buildPreviewHtml(config) {
626
712
  }
627
713
  const entryFile = config.files.find((f) => f.path === config.entry);
628
714
  if (!entryFile) {
629
- return { html: "", error: `Entry file not found: ${config.entry}` };
715
+ return { success: false, html: "", css: "", error: `Entry file not found: ${config.entry}` };
630
716
  }
631
717
  const hasDefaultExport = /export\s+default/.test(entryFile.content);
718
+ const userCssCollected = [];
632
719
  const entryCode = hasDefaultExport ? `
633
- import React from 'react'
634
- import { createRoot } from 'react-dom/client'
720
+ import React, { createRoot } from '${options.vendorPath}'
635
721
  import App from './${config.entry}'
636
-
637
722
  const root = createRoot(document.getElementById('root'))
638
723
  root.render(React.createElement(App))
639
- ` : `
640
- import './${config.entry}'
641
- `;
642
- const result = await build({
643
- stdin: {
644
- contents: entryCode,
645
- loader: "tsx",
646
- resolveDir: "/"
647
- },
724
+ ` : `import './${config.entry}'`;
725
+ const result = await build2({
726
+ stdin: { contents: entryCode, loader: "tsx", resolveDir: "/" },
648
727
  bundle: true,
649
728
  write: false,
650
729
  format: "esm",
@@ -652,84 +731,78 @@ async function buildPreviewHtml(config) {
652
731
  jsxImportSource: "react",
653
732
  target: "es2020",
654
733
  minify: true,
655
- plugins: [{
656
- name: "virtual-fs",
657
- setup(build2) {
658
- build2.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => {
659
- const parts = args.path.split("/");
660
- const pkg = parts[0];
661
- const subpath = parts.slice(1).join("/");
662
- const url = subpath ? `https://esm.sh/${pkg}@18/${subpath}` : `https://esm.sh/${pkg}@18`;
663
- return { path: url, external: true };
664
- });
665
- build2.onResolve({ filter: /^[^./]/ }, (args) => {
666
- if (args.path.startsWith("https://"))
667
- return;
668
- return { path: `https://esm.sh/${args.path}`, external: true };
669
- });
670
- build2.onResolve({ filter: /^\./ }, (args) => {
671
- let resolved = args.path.replace(/^\.\//, "");
672
- if (!resolved.includes(".")) {
673
- for (const ext of [".tsx", ".ts", ".jsx", ".js", ".css"]) {
674
- if (virtualFs[resolved + ext]) {
675
- resolved = resolved + ext;
676
- break;
734
+ plugins: [
735
+ {
736
+ name: "optimized-preview",
737
+ setup(build3) {
738
+ build3.onResolve({ filter: new RegExp(options.vendorPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) }, (args) => {
739
+ return { path: args.path, external: true };
740
+ });
741
+ build3.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, () => {
742
+ return { path: options.vendorPath, external: true };
743
+ });
744
+ build3.onResolve({ filter: /^\./ }, (args) => {
745
+ let resolved = args.path.replace(/^\.\//, "");
746
+ if (!resolved.includes(".")) {
747
+ for (const ext of [".tsx", ".ts", ".jsx", ".js", ".css"]) {
748
+ if (virtualFs[resolved + ext]) {
749
+ resolved = resolved + ext;
750
+ break;
751
+ }
677
752
  }
678
753
  }
679
- }
680
- return { path: resolved, namespace: "virtual" };
681
- });
682
- build2.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
683
- const file = virtualFs[args.path];
684
- if (file) {
685
- if (file.loader === "css") {
686
- const css = file.contents.replace(/`/g, "\\`").replace(/\$/g, "\\$");
687
- return {
688
- contents: `
689
- const style = document.createElement('style');
690
- style.textContent = \`${css}\`;
691
- document.head.appendChild(style);
692
- `,
693
- loader: "js"
694
- };
754
+ return { path: resolved, namespace: "virtual" };
755
+ });
756
+ build3.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
757
+ const file = virtualFs[args.path];
758
+ if (file) {
759
+ if (file.loader === "css") {
760
+ userCssCollected.push(file.contents);
761
+ return { contents: "", loader: "js" };
762
+ }
763
+ return { contents: file.contents, loader: file.loader };
695
764
  }
696
- return { contents: file.contents, loader: file.loader };
697
- }
698
- return { contents: "", loader: "empty" };
699
- });
765
+ return { contents: "", loader: "empty" };
766
+ });
767
+ }
700
768
  }
701
- }]
769
+ ]
702
770
  });
703
- const jsFile = result.outputFiles.find((f) => f.path.endsWith(".js")) || result.outputFiles[0];
771
+ const jsFile = result.outputFiles?.find((f) => f.path.endsWith(".js")) || result.outputFiles?.[0];
704
772
  const jsCode = jsFile?.text || "";
773
+ let css = "";
774
+ if (config.tailwind) {
775
+ const tailwindResult = await compileTailwind(config.files.map((f) => ({ path: f.path, content: f.content })));
776
+ if (tailwindResult.success)
777
+ css = tailwindResult.css;
778
+ }
779
+ const userCss = userCssCollected.join(`
780
+ `);
781
+ const allCss = css + `
782
+ ` + userCss;
705
783
  const html = `<!DOCTYPE html>
706
784
  <html lang="en">
707
785
  <head>
708
786
  <meta charset="UTF-8">
709
787
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
710
788
  <title>Preview</title>
711
- <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
712
- <style>
713
- body { margin: 0; }
714
- #root { min-height: 100vh; }
715
- </style>
789
+ <style>${allCss}</style>
790
+ <style>body { margin: 0; } #root { min-height: 100vh; }</style>
716
791
  </head>
717
792
  <body>
718
793
  <div id="root"></div>
794
+ <script type="module" src="${options.vendorPath}"></script>
719
795
  <script type="module">${jsCode}</script>
720
796
  </body>
721
797
  </html>`;
722
- return { html };
798
+ return { success: true, html, css: allCss };
723
799
  } catch (err) {
724
- return {
725
- html: "",
726
- error: err instanceof Error ? err.message : String(err)
727
- };
800
+ return { success: false, html: "", css: "", error: err instanceof Error ? err.message : String(err) };
728
801
  }
729
802
  }
730
803
 
731
804
  // src/vite/plugins/previews-plugin.ts
732
- import { existsSync as existsSync4, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
805
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
733
806
  import path5 from "path";
734
807
  var VIRTUAL_MODULE_ID2 = "virtual:prev-previews";
735
808
  var RESOLVED_VIRTUAL_MODULE_ID2 = "\x00" + VIRTUAL_MODULE_ID2;
@@ -792,31 +865,43 @@ export function getByStatus(status) {
792
865
  return;
793
866
  const distDir = path5.join(rootDir, "dist");
794
867
  const targetDir = path5.join(distDir, "_preview");
868
+ const vendorsDir = path5.join(targetDir, "_vendors");
795
869
  const previewsDir = path5.join(rootDir, "previews");
796
870
  const oldPreviewsDir = path5.join(distDir, "previews");
797
871
  if (existsSync4(oldPreviewsDir)) {
798
- rmSync(oldPreviewsDir, { recursive: true });
872
+ rmSync2(oldPreviewsDir, { recursive: true });
799
873
  }
800
874
  if (existsSync4(targetDir)) {
801
- rmSync(targetDir, { recursive: true });
875
+ rmSync2(targetDir, { recursive: true });
802
876
  }
803
877
  const previews = await scanPreviews(rootDir);
804
878
  if (previews.length === 0)
805
879
  return;
806
880
  console.log(`
807
881
  Building ${previews.length} preview(s)...`);
882
+ console.log(" Building shared vendor bundle...");
883
+ mkdirSync2(vendorsDir, { recursive: true });
884
+ const vendorResult = await buildVendorBundle();
885
+ if (!vendorResult.success) {
886
+ console.error(` ✗ Vendor bundle: ${vendorResult.error}`);
887
+ return;
888
+ }
889
+ writeFileSync3(path5.join(vendorsDir, "runtime.js"), vendorResult.code);
890
+ console.log(" ✓ _vendors/runtime.js");
808
891
  for (const preview of previews) {
809
892
  const previewDir = path5.join(previewsDir, preview.name);
810
893
  try {
811
894
  const config = await buildPreviewConfig(previewDir);
812
- const result = await buildPreviewHtml(config);
813
- if (result.error) {
895
+ const depth = preview.name.split("/").length;
896
+ const vendorPath = "../".repeat(depth) + "_vendors/runtime.js";
897
+ const result = await buildOptimizedPreview(config, { vendorPath });
898
+ if (!result.success) {
814
899
  console.error(` ✗ ${preview.name}: ${result.error}`);
815
900
  continue;
816
901
  }
817
902
  const outputDir = path5.join(targetDir, preview.name);
818
- mkdirSync(outputDir, { recursive: true });
819
- writeFileSync2(path5.join(outputDir, "index.html"), result.html);
903
+ mkdirSync2(outputDir, { recursive: true });
904
+ writeFileSync3(path5.join(outputDir, "index.html"), result.html);
820
905
  console.log(` ✓ ${preview.name}`);
821
906
  } catch (err) {
822
907
  console.error(` ✗ ${preview.name}: ${err}`);
@@ -936,7 +1021,7 @@ function validateConfig(raw) {
936
1021
  return config;
937
1022
  }
938
1023
  // src/config/loader.ts
939
- import { readFileSync as readFileSync4, existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
1024
+ import { readFileSync as readFileSync5, existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
940
1025
  import path6 from "path";
941
1026
  import yaml2 from "js-yaml";
942
1027
  function findConfigFile(rootDir) {
@@ -954,7 +1039,7 @@ function loadConfig(rootDir) {
954
1039
  return defaultConfig;
955
1040
  }
956
1041
  try {
957
- const content = readFileSync4(configPath, "utf-8");
1042
+ const content = readFileSync5(configPath, "utf-8");
958
1043
  const raw = yaml2.load(content);
959
1044
  return validateConfig(raw);
960
1045
  } catch (error) {
@@ -970,7 +1055,7 @@ function saveConfig(rootDir, config) {
970
1055
  quotingType: '"',
971
1056
  forceQuotes: false
972
1057
  });
973
- writeFileSync3(configPath, content, "utf-8");
1058
+ writeFileSync4(configPath, content, "utf-8");
974
1059
  }
975
1060
  function updateOrder(rootDir, pathKey, order) {
976
1061
  const config = loadConfig(rootDir);
@@ -978,7 +1063,7 @@ function updateOrder(rootDir, pathKey, order) {
978
1063
  saveConfig(rootDir, config);
979
1064
  }
980
1065
  // src/utils/debug.ts
981
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1066
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
982
1067
  import path7 from "path";
983
1068
 
984
1069
  class DebugCollector {
@@ -1058,11 +1143,11 @@ class DebugCollector {
1058
1143
  summary: this.generateSummary()
1059
1144
  };
1060
1145
  const debugDir = path7.join(this.rootDir, ".prev-debug");
1061
- mkdirSync2(debugDir, { recursive: true });
1146
+ mkdirSync3(debugDir, { recursive: true });
1062
1147
  const date = new Date;
1063
1148
  const filename = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${String(date.getHours()).padStart(2, "0")}-${String(date.getMinutes()).padStart(2, "0")}-${String(date.getSeconds()).padStart(2, "0")}.json`;
1064
1149
  const filepath = path7.join(debugDir, filename);
1065
- writeFileSync4(filepath, JSON.stringify(report, null, 2));
1150
+ writeFileSync5(filepath, JSON.stringify(report, null, 2));
1066
1151
  return filepath;
1067
1152
  }
1068
1153
  }
@@ -1136,12 +1221,12 @@ function createFriendlyLogger() {
1136
1221
  };
1137
1222
  }
1138
1223
  function findCliRoot2() {
1139
- let dir = path8.dirname(fileURLToPath2(import.meta.url));
1224
+ let dir = path8.dirname(fileURLToPath3(import.meta.url));
1140
1225
  for (let i = 0;i < 10; i++) {
1141
1226
  const pkgPath = path8.join(dir, "package.json");
1142
1227
  if (existsSync6(pkgPath)) {
1143
1228
  try {
1144
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1229
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1145
1230
  if (pkg.name === "prev-cli") {
1146
1231
  return dir;
1147
1232
  }
@@ -1152,7 +1237,7 @@ function findCliRoot2() {
1152
1237
  break;
1153
1238
  dir = parent;
1154
1239
  }
1155
- return path8.dirname(path8.dirname(fileURLToPath2(import.meta.url)));
1240
+ return path8.dirname(path8.dirname(fileURLToPath3(import.meta.url)));
1156
1241
  }
1157
1242
  function findNodeModules(cliRoot2) {
1158
1243
  const localNodeModules = path8.join(cliRoot2, "node_modules");
@@ -1249,7 +1334,7 @@ async function createViteConfig(options) {
1249
1334
  }
1250
1335
  const indexPath = path8.join(srcRoot2, "theme/index.html");
1251
1336
  if (existsSync6(indexPath)) {
1252
- server.transformIndexHtml(req.url, readFileSync5(indexPath, "utf-8")).then((html) => {
1337
+ server.transformIndexHtml(req.url, readFileSync6(indexPath, "utf-8")).then((html) => {
1253
1338
  res.setHeader("Content-Type", "text/html");
1254
1339
  res.end(html);
1255
1340
  }).catch(next);
@@ -1278,7 +1363,7 @@ async function createViteConfig(options) {
1278
1363
  if (urlPath === "/_preview-runtime") {
1279
1364
  const templatePath = path8.join(srcRoot2, "preview-runtime/template.html");
1280
1365
  if (existsSync6(templatePath)) {
1281
- const html = readFileSync5(templatePath, "utf-8");
1366
+ const html = readFileSync6(templatePath, "utf-8");
1282
1367
  res.setHeader("Content-Type", "text/html");
1283
1368
  res.end(html);
1284
1369
  return;
@@ -1394,7 +1479,7 @@ async function createViteConfig(options) {
1394
1479
  }
1395
1480
  if (existsSync6(htmlPath)) {
1396
1481
  try {
1397
- let html = readFileSync5(htmlPath, "utf-8");
1482
+ let html = readFileSync6(htmlPath, "utf-8");
1398
1483
  const previewBase = `/_preview/${previewName}/`;
1399
1484
  html = html.replace(/(src|href)=["']\.\/([^"']+)["']/g, `$1="${previewBase}$2"`);
1400
1485
  const transformed = await server.transformIndexHtml(req.url, html);
@@ -1521,7 +1606,7 @@ async function findAvailablePort(minPort, maxPort) {
1521
1606
 
1522
1607
  // src/vite/start.ts
1523
1608
  import { exec } from "child_process";
1524
- import { existsSync as existsSync7, rmSync as rmSync2, copyFileSync } from "fs";
1609
+ import { existsSync as existsSync7, rmSync as rmSync3, copyFileSync } from "fs";
1525
1610
  import path9 from "path";
1526
1611
  function printWelcome(type) {
1527
1612
  console.log();
@@ -1559,11 +1644,11 @@ function clearCache(rootDir) {
1559
1644
  const nodeModulesVite = path9.join(rootDir, "node_modules", ".vite");
1560
1645
  let cleared = 0;
1561
1646
  if (existsSync7(viteCacheDir)) {
1562
- rmSync2(viteCacheDir, { recursive: true });
1647
+ rmSync3(viteCacheDir, { recursive: true });
1563
1648
  cleared++;
1564
1649
  }
1565
1650
  if (existsSync7(nodeModulesVite)) {
1566
- rmSync2(nodeModulesVite, { recursive: true });
1651
+ rmSync3(nodeModulesVite, { recursive: true });
1567
1652
  cleared++;
1568
1653
  }
1569
1654
  if (cleared === 0) {
@@ -1674,7 +1759,7 @@ async function buildSite(rootDir, options = {}) {
1674
1759
  base: options.base,
1675
1760
  debug: options.debug
1676
1761
  });
1677
- await build2(config);
1762
+ await build3(config);
1678
1763
  const debugCollector = getDebugCollector();
1679
1764
  if (debugCollector) {
1680
1765
  debugCollector.startPhase("buildComplete");
@@ -1766,11 +1851,11 @@ async function cleanCache(options) {
1766
1851
  import yaml3 from "js-yaml";
1767
1852
  function getVersion() {
1768
1853
  try {
1769
- let dir = path11.dirname(fileURLToPath3(import.meta.url));
1854
+ let dir = path11.dirname(fileURLToPath4(import.meta.url));
1770
1855
  for (let i = 0;i < 5; i++) {
1771
1856
  const pkgPath = path11.join(dir, "package.json");
1772
1857
  if (existsSync8(pkgPath)) {
1773
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1858
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
1774
1859
  if (pkg.name === "prev-cli")
1775
1860
  return pkg.version;
1776
1861
  }
@@ -1898,7 +1983,7 @@ async function clearViteCache(rootDir2) {
1898
1983
  try {
1899
1984
  const prevCacheDir = await getCacheDir(rootDir2);
1900
1985
  if (existsSync8(prevCacheDir)) {
1901
- rmSync3(prevCacheDir, { recursive: true });
1986
+ rmSync4(prevCacheDir, { recursive: true });
1902
1987
  cleared++;
1903
1988
  console.log(` ✓ Removed ${prevCacheDir}`);
1904
1989
  }
@@ -1906,12 +1991,12 @@ async function clearViteCache(rootDir2) {
1906
1991
  const viteCacheDir = path11.join(rootDir2, ".vite");
1907
1992
  const nodeModulesVite = path11.join(rootDir2, "node_modules", ".vite");
1908
1993
  if (existsSync8(viteCacheDir)) {
1909
- rmSync3(viteCacheDir, { recursive: true });
1994
+ rmSync4(viteCacheDir, { recursive: true });
1910
1995
  cleared++;
1911
1996
  console.log(` ✓ Removed .vite/`);
1912
1997
  }
1913
1998
  if (existsSync8(nodeModulesVite)) {
1914
- rmSync3(nodeModulesVite, { recursive: true });
1999
+ rmSync4(nodeModulesVite, { recursive: true });
1915
2000
  cleared++;
1916
2001
  console.log(` ✓ Removed node_modules/.vite/`);
1917
2002
  }
@@ -1992,7 +2077,7 @@ order: {}
1992
2077
  # - "getting-started.md"
1993
2078
  # - "guides/"
1994
2079
  `;
1995
- writeFileSync5(targetPath, configContent, "utf-8");
2080
+ writeFileSync6(targetPath, configContent, "utf-8");
1996
2081
  console.log(`
1997
2082
  ✨ Created ${targetPath}
1998
2083
  `);
@@ -2020,7 +2105,7 @@ function createPreview(rootDir2, name) {
2020
2105
  console.error(`Preview "${name}" already exists at: ${previewDir}`);
2021
2106
  process.exit(1);
2022
2107
  }
2023
- mkdirSync3(previewDir, { recursive: true });
2108
+ mkdirSync4(previewDir, { recursive: true });
2024
2109
  const appTsx = `import { useState } from 'react'
2025
2110
  import './styles.css'
2026
2111
 
@@ -2141,8 +2226,8 @@ export default function App() {
2141
2226
  .dark\\:text-white { color: #fff; }
2142
2227
  }
2143
2228
  `;
2144
- writeFileSync5(path11.join(previewDir, "App.tsx"), appTsx);
2145
- writeFileSync5(path11.join(previewDir, "styles.css"), stylesCss);
2229
+ writeFileSync6(path11.join(previewDir, "App.tsx"), appTsx);
2230
+ writeFileSync6(path11.join(previewDir, "styles.css"), stylesCss);
2146
2231
  console.log(`
2147
2232
  ✨ Created preview: previews/${name}/
2148
2233
 
@@ -0,0 +1,11 @@
1
+ import type { PreviewConfig } from './types';
2
+ export interface OptimizedBuildOptions {
3
+ vendorPath: string;
4
+ }
5
+ export interface OptimizedBuildResult {
6
+ success: boolean;
7
+ html: string;
8
+ css: string;
9
+ error?: string;
10
+ }
11
+ export declare function buildOptimizedPreview(config: PreviewConfig, options: OptimizedBuildOptions): Promise<OptimizedBuildResult>;
@@ -0,0 +1,11 @@
1
+ export interface TailwindResult {
2
+ success: boolean;
3
+ css: string;
4
+ error?: string;
5
+ }
6
+ interface ContentFile {
7
+ path: string;
8
+ content: string;
9
+ }
10
+ export declare function compileTailwind(files: ContentFile[]): Promise<TailwindResult>;
11
+ export {};
@@ -0,0 +1,6 @@
1
+ export interface VendorBundleResult {
2
+ success: boolean;
3
+ code: string;
4
+ error?: string;
5
+ }
6
+ export declare function buildVendorBundle(): Promise<VendorBundleResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.24.12",
3
+ "version": "0.24.13",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,47 @@
1
+ import { test, expect } from 'bun:test'
2
+ import { buildOptimizedPreview } from './build-optimized'
3
+ import type { PreviewConfig } from './types'
4
+
5
+ // Tailwind CLI can be slow on first run
6
+ const TAILWIND_TIMEOUT = 30000
7
+
8
+ test('buildOptimizedPreview generates HTML with local vendor imports', async () => {
9
+ const config: PreviewConfig = {
10
+ files: [
11
+ {
12
+ path: 'index.tsx',
13
+ content: `export default function App() { return <div className="p-4">Hello</div> }`,
14
+ type: 'tsx',
15
+ },
16
+ ],
17
+ entry: 'index.tsx',
18
+ tailwind: true,
19
+ }
20
+
21
+ const result = await buildOptimizedPreview(config, { vendorPath: '../_vendors/runtime.js' })
22
+
23
+ expect(result.success).toBe(true)
24
+ expect(result.html).toContain('../_vendors/runtime.js')
25
+ expect(result.html).not.toContain('esm.sh')
26
+ expect(result.html).not.toContain('tailwindcss/browser')
27
+ }, TAILWIND_TIMEOUT)
28
+
29
+ test('buildOptimizedPreview includes compiled CSS', async () => {
30
+ const config: PreviewConfig = {
31
+ files: [
32
+ {
33
+ path: 'index.tsx',
34
+ content: `export default function App() { return <div className="flex items-center bg-red-500">Hello</div> }`,
35
+ type: 'tsx',
36
+ },
37
+ ],
38
+ entry: 'index.tsx',
39
+ tailwind: true,
40
+ }
41
+
42
+ const result = await buildOptimizedPreview(config, { vendorPath: '../_vendors/runtime.js' })
43
+
44
+ expect(result.success).toBe(true)
45
+ expect(result.css).toContain('flex')
46
+ expect(result.css).toContain('bg-red-500')
47
+ }, TAILWIND_TIMEOUT)
@@ -0,0 +1,136 @@
1
+ import { build } from 'esbuild'
2
+ import type { PreviewConfig } from './types'
3
+ import { compileTailwind } from './tailwind'
4
+
5
+ export interface OptimizedBuildOptions {
6
+ vendorPath: string
7
+ }
8
+
9
+ export interface OptimizedBuildResult {
10
+ success: boolean
11
+ html: string
12
+ css: string
13
+ error?: string
14
+ }
15
+
16
+ export async function buildOptimizedPreview(
17
+ config: PreviewConfig,
18
+ options: OptimizedBuildOptions
19
+ ): Promise<OptimizedBuildResult> {
20
+ try {
21
+ const virtualFs: Record<string, { contents: string; loader: string }> = {}
22
+ for (const file of config.files) {
23
+ const ext = file.path.split('.').pop()?.toLowerCase()
24
+ const loader = ext === 'css' ? 'css' : ext === 'json' ? 'json' : ext || 'tsx'
25
+ virtualFs[file.path] = { contents: file.content, loader }
26
+ }
27
+
28
+ const entryFile = config.files.find(f => f.path === config.entry)
29
+ if (!entryFile) {
30
+ return { success: false, html: '', css: '', error: `Entry file not found: ${config.entry}` }
31
+ }
32
+
33
+ const hasDefaultExport = /export\s+default/.test(entryFile.content)
34
+ const userCssCollected: string[] = []
35
+
36
+ const entryCode = hasDefaultExport
37
+ ? `
38
+ import React, { createRoot } from '${options.vendorPath}'
39
+ import App from './${config.entry}'
40
+ const root = createRoot(document.getElementById('root'))
41
+ root.render(React.createElement(App))
42
+ `
43
+ : `import './${config.entry}'`
44
+
45
+ const result = await build({
46
+ stdin: { contents: entryCode, loader: 'tsx', resolveDir: '/' },
47
+ bundle: true,
48
+ write: false,
49
+ format: 'esm',
50
+ jsx: 'automatic',
51
+ jsxImportSource: 'react',
52
+ target: 'es2020',
53
+ minify: true,
54
+ plugins: [
55
+ {
56
+ name: 'optimized-preview',
57
+ setup(build) {
58
+ // External: vendor runtime
59
+ build.onResolve(
60
+ { filter: new RegExp(options.vendorPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) },
61
+ args => {
62
+ return { path: args.path, external: true }
63
+ }
64
+ )
65
+
66
+ // External: React (map to vendor bundle)
67
+ build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, () => {
68
+ return { path: options.vendorPath, external: true }
69
+ })
70
+
71
+ // Resolve relative imports
72
+ build.onResolve({ filter: /^\./ }, args => {
73
+ let resolved = args.path.replace(/^\.\//, '')
74
+ if (!resolved.includes('.')) {
75
+ for (const ext of ['.tsx', '.ts', '.jsx', '.js', '.css']) {
76
+ if (virtualFs[resolved + ext]) {
77
+ resolved = resolved + ext
78
+ break
79
+ }
80
+ }
81
+ }
82
+ return { path: resolved, namespace: 'virtual' }
83
+ })
84
+
85
+ // Load from virtual FS
86
+ build.onLoad({ filter: /.*/, namespace: 'virtual' }, args => {
87
+ const file = virtualFs[args.path]
88
+ if (file) {
89
+ if (file.loader === 'css') {
90
+ userCssCollected.push(file.contents)
91
+ return { contents: '', loader: 'js' }
92
+ }
93
+ return { contents: file.contents, loader: file.loader as any }
94
+ }
95
+ return { contents: '', loader: 'empty' }
96
+ })
97
+ },
98
+ },
99
+ ],
100
+ })
101
+
102
+ const jsFile = result.outputFiles?.find(f => f.path.endsWith('.js')) || result.outputFiles?.[0]
103
+ const jsCode = jsFile?.text || ''
104
+
105
+ let css = ''
106
+ if (config.tailwind) {
107
+ const tailwindResult = await compileTailwind(
108
+ config.files.map(f => ({ path: f.path, content: f.content }))
109
+ )
110
+ if (tailwindResult.success) css = tailwindResult.css
111
+ }
112
+
113
+ const userCss = userCssCollected.join('\n')
114
+ const allCss = css + '\n' + userCss
115
+
116
+ const html = `<!DOCTYPE html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="UTF-8">
120
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
121
+ <title>Preview</title>
122
+ <style>${allCss}</style>
123
+ <style>body { margin: 0; } #root { min-height: 100vh; }</style>
124
+ </head>
125
+ <body>
126
+ <div id="root"></div>
127
+ <script type="module" src="${options.vendorPath}"></script>
128
+ <script type="module">${jsCode}</script>
129
+ </body>
130
+ </html>`
131
+
132
+ return { success: true, html, css: allCss }
133
+ } catch (err) {
134
+ return { success: false, html: '', css: '', error: err instanceof Error ? err.message : String(err) }
135
+ }
136
+ }
@@ -0,0 +1,30 @@
1
+ import { test, expect } from 'bun:test'
2
+ import { compileTailwind } from './tailwind'
3
+
4
+ test('compileTailwind extracts used classes from content', async () => {
5
+ const content = `
6
+ export default function App() {
7
+ return <div className="flex items-center p-4 bg-blue-500">Hello</div>
8
+ }
9
+ `
10
+
11
+ const result = await compileTailwind([{ path: 'App.tsx', content }])
12
+
13
+ expect(result.success).toBe(true)
14
+ expect(result.css).toBeDefined()
15
+ expect(result.css).toContain('flex')
16
+ expect(result.css).toContain('items-center')
17
+ expect(result.css).toContain('bg-blue-500')
18
+ })
19
+
20
+ test('compileTailwind returns empty CSS for no Tailwind classes', async () => {
21
+ const content = `
22
+ export default function App() {
23
+ return <div style={{ color: 'red' }}>Hello</div>
24
+ }
25
+ `
26
+
27
+ const result = await compileTailwind([{ path: 'App.tsx', content }])
28
+
29
+ expect(result.success).toBe(true)
30
+ })
@@ -0,0 +1,64 @@
1
+ import { $ } from 'bun'
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
3
+ import { join, dirname } from 'path'
4
+ import { tmpdir } from 'os'
5
+
6
+ export interface TailwindResult {
7
+ success: boolean
8
+ css: string
9
+ error?: string
10
+ }
11
+
12
+ interface ContentFile {
13
+ path: string
14
+ content: string
15
+ }
16
+
17
+ export async function compileTailwind(files: ContentFile[]): Promise<TailwindResult> {
18
+ const tempDir = mkdtempSync(join(tmpdir(), 'prev-tailwind-'))
19
+
20
+ try {
21
+ // Write content files (create parent dirs for nested paths)
22
+ for (const file of files) {
23
+ const filePath = join(tempDir, file.path)
24
+ const parentDir = dirname(filePath)
25
+ mkdirSync(parentDir, { recursive: true })
26
+ writeFileSync(filePath, file.content)
27
+ }
28
+
29
+ // Create Tailwind config - use .cjs for compatibility
30
+ const configContent = `
31
+ module.exports = {
32
+ content: [${JSON.stringify(tempDir + '/**/*.{tsx,jsx,ts,js,html}')}],
33
+ }
34
+ `
35
+ const configPath = join(tempDir, 'tailwind.config.cjs')
36
+ writeFileSync(configPath, configContent)
37
+
38
+ // Create input CSS
39
+ const inputCss = `
40
+ @tailwind base;
41
+ @tailwind components;
42
+ @tailwind utilities;
43
+ `
44
+ const inputPath = join(tempDir, 'input.css')
45
+ writeFileSync(inputPath, inputCss)
46
+
47
+ const outputPath = join(tempDir, 'output.css')
48
+
49
+ // Run Tailwind CLI
50
+ await $`bunx tailwindcss -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet()
51
+
52
+ const css = readFileSync(outputPath, 'utf-8')
53
+
54
+ return { success: true, css }
55
+ } catch (err) {
56
+ return {
57
+ success: false,
58
+ css: '',
59
+ error: err instanceof Error ? err.message : String(err),
60
+ }
61
+ } finally {
62
+ rmSync(tempDir, { recursive: true, force: true })
63
+ }
64
+ }
@@ -0,0 +1,15 @@
1
+ import { test, expect } from 'bun:test'
2
+ import { buildVendorBundle } from './vendors'
3
+
4
+ test('buildVendorBundle creates runtime.js with React', async () => {
5
+ const result = await buildVendorBundle()
6
+ expect(result.success).toBe(true)
7
+ expect(result.code).toBeDefined()
8
+ expect(result.code).toContain('createElement')
9
+ expect(result.code).toContain('createRoot')
10
+ })
11
+
12
+ test('buildVendorBundle output is valid ESM', async () => {
13
+ const result = await buildVendorBundle()
14
+ expect(result.code).toContain('export')
15
+ })
@@ -0,0 +1,52 @@
1
+ import { build } from 'esbuild'
2
+ import { dirname } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ // Resolve from CLI's location, not user's project (React is our dependency)
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+
8
+ export interface VendorBundleResult {
9
+ success: boolean
10
+ code: string
11
+ error?: string
12
+ }
13
+
14
+ export async function buildVendorBundle(): Promise<VendorBundleResult> {
15
+ try {
16
+ const entryCode = `
17
+ import * as React from 'react'
18
+ import * as ReactDOM from 'react-dom'
19
+ import { createRoot } from 'react-dom/client'
20
+ export { jsx, jsxs, Fragment } from 'react/jsx-runtime'
21
+ export { React, ReactDOM, createRoot }
22
+ export default React
23
+ `
24
+
25
+ const result = await build({
26
+ stdin: {
27
+ contents: entryCode,
28
+ loader: 'ts',
29
+ resolveDir: __dirname, // Resolve React from CLI's node_modules
30
+ },
31
+ bundle: true,
32
+ write: false,
33
+ format: 'esm',
34
+ target: 'es2020',
35
+ minify: true,
36
+ })
37
+
38
+ // Select JS output file explicitly (in case sourcemaps are added later)
39
+ const jsFile = result.outputFiles?.find(f => f.path.endsWith('.js')) || result.outputFiles?.[0]
40
+ if (!jsFile) {
41
+ return { success: false, code: '', error: 'No output generated' }
42
+ }
43
+
44
+ return { success: true, code: jsFile.text }
45
+ } catch (err) {
46
+ return {
47
+ success: false,
48
+ code: '',
49
+ error: err instanceof Error ? err.message : String(err),
50
+ }
51
+ }
52
+ }