prev-cli 0.24.12 → 0.24.14

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 fileURLToPath5 } 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 fileURLToPath4 } 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,98 @@ 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
+ import { fileURLToPath as fileURLToPath3 } from "url";
667
+ var __dirname3 = dirname2(fileURLToPath3(import.meta.url));
668
+ var tailwindBin = join(__dirname3, "../../node_modules/.bin/tailwindcss");
669
+ async function compileTailwind(files) {
670
+ const tempDir = mkdtempSync(join(tmpdir(), "prev-tailwind-"));
671
+ try {
672
+ for (const file of files) {
673
+ const filePath = join(tempDir, file.path);
674
+ const parentDir = dirname2(filePath);
675
+ mkdirSync(parentDir, { recursive: true });
676
+ writeFileSync2(filePath, file.content);
677
+ }
678
+ const configContent = `
679
+ module.exports = {
680
+ content: [${JSON.stringify(tempDir + "/**/*.{tsx,jsx,ts,js,html}")}],
681
+ }
682
+ `;
683
+ const configPath = join(tempDir, "tailwind.config.cjs");
684
+ writeFileSync2(configPath, configContent);
685
+ const inputCss = `
686
+ @tailwind base;
687
+ @tailwind components;
688
+ @tailwind utilities;
689
+ `;
690
+ const inputPath = join(tempDir, "input.css");
691
+ writeFileSync2(inputPath, inputCss);
692
+ const outputPath = join(tempDir, "output.css");
693
+ await $`${tailwindBin} -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet();
694
+ const css = readFileSync4(outputPath, "utf-8");
695
+ return { success: true, css };
696
+ } catch (err) {
697
+ return {
698
+ success: false,
699
+ css: "",
700
+ error: err instanceof Error ? err.message : String(err)
701
+ };
702
+ } finally {
703
+ rmSync(tempDir, { recursive: true, force: true });
704
+ }
705
+ }
706
+
707
+ // src/preview-runtime/build-optimized.ts
708
+ async function buildOptimizedPreview(config, options) {
620
709
  try {
621
710
  const virtualFs = {};
622
711
  for (const file of config.files) {
@@ -626,25 +715,18 @@ async function buildPreviewHtml(config) {
626
715
  }
627
716
  const entryFile = config.files.find((f) => f.path === config.entry);
628
717
  if (!entryFile) {
629
- return { html: "", error: `Entry file not found: ${config.entry}` };
718
+ return { success: false, html: "", css: "", error: `Entry file not found: ${config.entry}` };
630
719
  }
631
720
  const hasDefaultExport = /export\s+default/.test(entryFile.content);
721
+ const userCssCollected = [];
632
722
  const entryCode = hasDefaultExport ? `
633
- import React from 'react'
634
- import { createRoot } from 'react-dom/client'
723
+ import React, { createRoot } from '${options.vendorPath}'
635
724
  import App from './${config.entry}'
636
-
637
725
  const root = createRoot(document.getElementById('root'))
638
726
  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
- },
727
+ ` : `import './${config.entry}'`;
728
+ const result = await build2({
729
+ stdin: { contents: entryCode, loader: "tsx", resolveDir: "/" },
648
730
  bundle: true,
649
731
  write: false,
650
732
  format: "esm",
@@ -652,84 +734,78 @@ async function buildPreviewHtml(config) {
652
734
  jsxImportSource: "react",
653
735
  target: "es2020",
654
736
  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;
737
+ plugins: [
738
+ {
739
+ name: "optimized-preview",
740
+ setup(build3) {
741
+ build3.onResolve({ filter: new RegExp(options.vendorPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) }, (args) => {
742
+ return { path: args.path, external: true };
743
+ });
744
+ build3.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, () => {
745
+ return { path: options.vendorPath, external: true };
746
+ });
747
+ build3.onResolve({ filter: /^\./ }, (args) => {
748
+ let resolved = args.path.replace(/^\.\//, "");
749
+ if (!resolved.includes(".")) {
750
+ for (const ext of [".tsx", ".ts", ".jsx", ".js", ".css"]) {
751
+ if (virtualFs[resolved + ext]) {
752
+ resolved = resolved + ext;
753
+ break;
754
+ }
677
755
  }
678
756
  }
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
- };
757
+ return { path: resolved, namespace: "virtual" };
758
+ });
759
+ build3.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
760
+ const file = virtualFs[args.path];
761
+ if (file) {
762
+ if (file.loader === "css") {
763
+ userCssCollected.push(file.contents);
764
+ return { contents: "", loader: "js" };
765
+ }
766
+ return { contents: file.contents, loader: file.loader };
695
767
  }
696
- return { contents: file.contents, loader: file.loader };
697
- }
698
- return { contents: "", loader: "empty" };
699
- });
768
+ return { contents: "", loader: "empty" };
769
+ });
770
+ }
700
771
  }
701
- }]
772
+ ]
702
773
  });
703
- const jsFile = result.outputFiles.find((f) => f.path.endsWith(".js")) || result.outputFiles[0];
774
+ const jsFile = result.outputFiles?.find((f) => f.path.endsWith(".js")) || result.outputFiles?.[0];
704
775
  const jsCode = jsFile?.text || "";
776
+ let css = "";
777
+ if (config.tailwind) {
778
+ const tailwindResult = await compileTailwind(config.files.map((f) => ({ path: f.path, content: f.content })));
779
+ if (tailwindResult.success)
780
+ css = tailwindResult.css;
781
+ }
782
+ const userCss = userCssCollected.join(`
783
+ `);
784
+ const allCss = css + `
785
+ ` + userCss;
705
786
  const html = `<!DOCTYPE html>
706
787
  <html lang="en">
707
788
  <head>
708
789
  <meta charset="UTF-8">
709
790
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
710
791
  <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>
792
+ <style>${allCss}</style>
793
+ <style>body { margin: 0; } #root { min-height: 100vh; }</style>
716
794
  </head>
717
795
  <body>
718
796
  <div id="root"></div>
797
+ <script type="module" src="${options.vendorPath}"></script>
719
798
  <script type="module">${jsCode}</script>
720
799
  </body>
721
800
  </html>`;
722
- return { html };
801
+ return { success: true, html, css: allCss };
723
802
  } catch (err) {
724
- return {
725
- html: "",
726
- error: err instanceof Error ? err.message : String(err)
727
- };
803
+ return { success: false, html: "", css: "", error: err instanceof Error ? err.message : String(err) };
728
804
  }
729
805
  }
730
806
 
731
807
  // src/vite/plugins/previews-plugin.ts
732
- import { existsSync as existsSync4, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
808
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
733
809
  import path5 from "path";
734
810
  var VIRTUAL_MODULE_ID2 = "virtual:prev-previews";
735
811
  var RESOLVED_VIRTUAL_MODULE_ID2 = "\x00" + VIRTUAL_MODULE_ID2;
@@ -792,31 +868,43 @@ export function getByStatus(status) {
792
868
  return;
793
869
  const distDir = path5.join(rootDir, "dist");
794
870
  const targetDir = path5.join(distDir, "_preview");
871
+ const vendorsDir = path5.join(targetDir, "_vendors");
795
872
  const previewsDir = path5.join(rootDir, "previews");
796
873
  const oldPreviewsDir = path5.join(distDir, "previews");
797
874
  if (existsSync4(oldPreviewsDir)) {
798
- rmSync(oldPreviewsDir, { recursive: true });
875
+ rmSync2(oldPreviewsDir, { recursive: true });
799
876
  }
800
877
  if (existsSync4(targetDir)) {
801
- rmSync(targetDir, { recursive: true });
878
+ rmSync2(targetDir, { recursive: true });
802
879
  }
803
880
  const previews = await scanPreviews(rootDir);
804
881
  if (previews.length === 0)
805
882
  return;
806
883
  console.log(`
807
884
  Building ${previews.length} preview(s)...`);
885
+ console.log(" Building shared vendor bundle...");
886
+ mkdirSync2(vendorsDir, { recursive: true });
887
+ const vendorResult = await buildVendorBundle();
888
+ if (!vendorResult.success) {
889
+ console.error(` ✗ Vendor bundle: ${vendorResult.error}`);
890
+ return;
891
+ }
892
+ writeFileSync3(path5.join(vendorsDir, "runtime.js"), vendorResult.code);
893
+ console.log(" ✓ _vendors/runtime.js");
808
894
  for (const preview of previews) {
809
895
  const previewDir = path5.join(previewsDir, preview.name);
810
896
  try {
811
897
  const config = await buildPreviewConfig(previewDir);
812
- const result = await buildPreviewHtml(config);
813
- if (result.error) {
898
+ const depth = preview.name.split("/").length;
899
+ const vendorPath = "../".repeat(depth) + "_vendors/runtime.js";
900
+ const result = await buildOptimizedPreview(config, { vendorPath });
901
+ if (!result.success) {
814
902
  console.error(` ✗ ${preview.name}: ${result.error}`);
815
903
  continue;
816
904
  }
817
905
  const outputDir = path5.join(targetDir, preview.name);
818
- mkdirSync(outputDir, { recursive: true });
819
- writeFileSync2(path5.join(outputDir, "index.html"), result.html);
906
+ mkdirSync2(outputDir, { recursive: true });
907
+ writeFileSync3(path5.join(outputDir, "index.html"), result.html);
820
908
  console.log(` ✓ ${preview.name}`);
821
909
  } catch (err) {
822
910
  console.error(` ✗ ${preview.name}: ${err}`);
@@ -936,7 +1024,7 @@ function validateConfig(raw) {
936
1024
  return config;
937
1025
  }
938
1026
  // src/config/loader.ts
939
- import { readFileSync as readFileSync4, existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
1027
+ import { readFileSync as readFileSync5, existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
940
1028
  import path6 from "path";
941
1029
  import yaml2 from "js-yaml";
942
1030
  function findConfigFile(rootDir) {
@@ -954,7 +1042,7 @@ function loadConfig(rootDir) {
954
1042
  return defaultConfig;
955
1043
  }
956
1044
  try {
957
- const content = readFileSync4(configPath, "utf-8");
1045
+ const content = readFileSync5(configPath, "utf-8");
958
1046
  const raw = yaml2.load(content);
959
1047
  return validateConfig(raw);
960
1048
  } catch (error) {
@@ -970,7 +1058,7 @@ function saveConfig(rootDir, config) {
970
1058
  quotingType: '"',
971
1059
  forceQuotes: false
972
1060
  });
973
- writeFileSync3(configPath, content, "utf-8");
1061
+ writeFileSync4(configPath, content, "utf-8");
974
1062
  }
975
1063
  function updateOrder(rootDir, pathKey, order) {
976
1064
  const config = loadConfig(rootDir);
@@ -978,7 +1066,7 @@ function updateOrder(rootDir, pathKey, order) {
978
1066
  saveConfig(rootDir, config);
979
1067
  }
980
1068
  // src/utils/debug.ts
981
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1069
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
982
1070
  import path7 from "path";
983
1071
 
984
1072
  class DebugCollector {
@@ -1058,11 +1146,11 @@ class DebugCollector {
1058
1146
  summary: this.generateSummary()
1059
1147
  };
1060
1148
  const debugDir = path7.join(this.rootDir, ".prev-debug");
1061
- mkdirSync2(debugDir, { recursive: true });
1149
+ mkdirSync3(debugDir, { recursive: true });
1062
1150
  const date = new Date;
1063
1151
  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
1152
  const filepath = path7.join(debugDir, filename);
1065
- writeFileSync4(filepath, JSON.stringify(report, null, 2));
1153
+ writeFileSync5(filepath, JSON.stringify(report, null, 2));
1066
1154
  return filepath;
1067
1155
  }
1068
1156
  }
@@ -1136,12 +1224,12 @@ function createFriendlyLogger() {
1136
1224
  };
1137
1225
  }
1138
1226
  function findCliRoot2() {
1139
- let dir = path8.dirname(fileURLToPath2(import.meta.url));
1227
+ let dir = path8.dirname(fileURLToPath4(import.meta.url));
1140
1228
  for (let i = 0;i < 10; i++) {
1141
1229
  const pkgPath = path8.join(dir, "package.json");
1142
1230
  if (existsSync6(pkgPath)) {
1143
1231
  try {
1144
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1232
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1145
1233
  if (pkg.name === "prev-cli") {
1146
1234
  return dir;
1147
1235
  }
@@ -1152,7 +1240,7 @@ function findCliRoot2() {
1152
1240
  break;
1153
1241
  dir = parent;
1154
1242
  }
1155
- return path8.dirname(path8.dirname(fileURLToPath2(import.meta.url)));
1243
+ return path8.dirname(path8.dirname(fileURLToPath4(import.meta.url)));
1156
1244
  }
1157
1245
  function findNodeModules(cliRoot2) {
1158
1246
  const localNodeModules = path8.join(cliRoot2, "node_modules");
@@ -1249,7 +1337,7 @@ async function createViteConfig(options) {
1249
1337
  }
1250
1338
  const indexPath = path8.join(srcRoot2, "theme/index.html");
1251
1339
  if (existsSync6(indexPath)) {
1252
- server.transformIndexHtml(req.url, readFileSync5(indexPath, "utf-8")).then((html) => {
1340
+ server.transformIndexHtml(req.url, readFileSync6(indexPath, "utf-8")).then((html) => {
1253
1341
  res.setHeader("Content-Type", "text/html");
1254
1342
  res.end(html);
1255
1343
  }).catch(next);
@@ -1278,7 +1366,7 @@ async function createViteConfig(options) {
1278
1366
  if (urlPath === "/_preview-runtime") {
1279
1367
  const templatePath = path8.join(srcRoot2, "preview-runtime/template.html");
1280
1368
  if (existsSync6(templatePath)) {
1281
- const html = readFileSync5(templatePath, "utf-8");
1369
+ const html = readFileSync6(templatePath, "utf-8");
1282
1370
  res.setHeader("Content-Type", "text/html");
1283
1371
  res.end(html);
1284
1372
  return;
@@ -1394,7 +1482,7 @@ async function createViteConfig(options) {
1394
1482
  }
1395
1483
  if (existsSync6(htmlPath)) {
1396
1484
  try {
1397
- let html = readFileSync5(htmlPath, "utf-8");
1485
+ let html = readFileSync6(htmlPath, "utf-8");
1398
1486
  const previewBase = `/_preview/${previewName}/`;
1399
1487
  html = html.replace(/(src|href)=["']\.\/([^"']+)["']/g, `$1="${previewBase}$2"`);
1400
1488
  const transformed = await server.transformIndexHtml(req.url, html);
@@ -1521,7 +1609,7 @@ async function findAvailablePort(minPort, maxPort) {
1521
1609
 
1522
1610
  // src/vite/start.ts
1523
1611
  import { exec } from "child_process";
1524
- import { existsSync as existsSync7, rmSync as rmSync2, copyFileSync } from "fs";
1612
+ import { existsSync as existsSync7, rmSync as rmSync3, copyFileSync } from "fs";
1525
1613
  import path9 from "path";
1526
1614
  function printWelcome(type) {
1527
1615
  console.log();
@@ -1559,11 +1647,11 @@ function clearCache(rootDir) {
1559
1647
  const nodeModulesVite = path9.join(rootDir, "node_modules", ".vite");
1560
1648
  let cleared = 0;
1561
1649
  if (existsSync7(viteCacheDir)) {
1562
- rmSync2(viteCacheDir, { recursive: true });
1650
+ rmSync3(viteCacheDir, { recursive: true });
1563
1651
  cleared++;
1564
1652
  }
1565
1653
  if (existsSync7(nodeModulesVite)) {
1566
- rmSync2(nodeModulesVite, { recursive: true });
1654
+ rmSync3(nodeModulesVite, { recursive: true });
1567
1655
  cleared++;
1568
1656
  }
1569
1657
  if (cleared === 0) {
@@ -1674,7 +1762,7 @@ async function buildSite(rootDir, options = {}) {
1674
1762
  base: options.base,
1675
1763
  debug: options.debug
1676
1764
  });
1677
- await build2(config);
1765
+ await build3(config);
1678
1766
  const debugCollector = getDebugCollector();
1679
1767
  if (debugCollector) {
1680
1768
  debugCollector.startPhase("buildComplete");
@@ -1766,11 +1854,11 @@ async function cleanCache(options) {
1766
1854
  import yaml3 from "js-yaml";
1767
1855
  function getVersion() {
1768
1856
  try {
1769
- let dir = path11.dirname(fileURLToPath3(import.meta.url));
1857
+ let dir = path11.dirname(fileURLToPath5(import.meta.url));
1770
1858
  for (let i = 0;i < 5; i++) {
1771
1859
  const pkgPath = path11.join(dir, "package.json");
1772
1860
  if (existsSync8(pkgPath)) {
1773
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1861
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
1774
1862
  if (pkg.name === "prev-cli")
1775
1863
  return pkg.version;
1776
1864
  }
@@ -1898,7 +1986,7 @@ async function clearViteCache(rootDir2) {
1898
1986
  try {
1899
1987
  const prevCacheDir = await getCacheDir(rootDir2);
1900
1988
  if (existsSync8(prevCacheDir)) {
1901
- rmSync3(prevCacheDir, { recursive: true });
1989
+ rmSync4(prevCacheDir, { recursive: true });
1902
1990
  cleared++;
1903
1991
  console.log(` ✓ Removed ${prevCacheDir}`);
1904
1992
  }
@@ -1906,12 +1994,12 @@ async function clearViteCache(rootDir2) {
1906
1994
  const viteCacheDir = path11.join(rootDir2, ".vite");
1907
1995
  const nodeModulesVite = path11.join(rootDir2, "node_modules", ".vite");
1908
1996
  if (existsSync8(viteCacheDir)) {
1909
- rmSync3(viteCacheDir, { recursive: true });
1997
+ rmSync4(viteCacheDir, { recursive: true });
1910
1998
  cleared++;
1911
1999
  console.log(` ✓ Removed .vite/`);
1912
2000
  }
1913
2001
  if (existsSync8(nodeModulesVite)) {
1914
- rmSync3(nodeModulesVite, { recursive: true });
2002
+ rmSync4(nodeModulesVite, { recursive: true });
1915
2003
  cleared++;
1916
2004
  console.log(` ✓ Removed node_modules/.vite/`);
1917
2005
  }
@@ -1992,7 +2080,7 @@ order: {}
1992
2080
  # - "getting-started.md"
1993
2081
  # - "guides/"
1994
2082
  `;
1995
- writeFileSync5(targetPath, configContent, "utf-8");
2083
+ writeFileSync6(targetPath, configContent, "utf-8");
1996
2084
  console.log(`
1997
2085
  ✨ Created ${targetPath}
1998
2086
  `);
@@ -2020,7 +2108,7 @@ function createPreview(rootDir2, name) {
2020
2108
  console.error(`Preview "${name}" already exists at: ${previewDir}`);
2021
2109
  process.exit(1);
2022
2110
  }
2023
- mkdirSync3(previewDir, { recursive: true });
2111
+ mkdirSync4(previewDir, { recursive: true });
2024
2112
  const appTsx = `import { useState } from 'react'
2025
2113
  import './styles.css'
2026
2114
 
@@ -2141,8 +2229,8 @@ export default function App() {
2141
2229
  .dark\\:text-white { color: #fff; }
2142
2230
  }
2143
2231
  `;
2144
- writeFileSync5(path11.join(previewDir, "App.tsx"), appTsx);
2145
- writeFileSync5(path11.join(previewDir, "styles.css"), stylesCss);
2232
+ writeFileSync6(path11.join(previewDir, "App.tsx"), appTsx);
2233
+ writeFileSync6(path11.join(previewDir, "styles.css"), stylesCss);
2146
2234
  console.log(`
2147
2235
  ✨ Created preview: previews/${name}/
2148
2236
 
@@ -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.14",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,7 +64,7 @@
64
64
  "rehype-highlight": "^7.0.0",
65
65
  "remark-gfm": "^4.0.0",
66
66
  "tailwind-merge": "^2.5.0",
67
- "tailwindcss": "^4.0.0",
67
+ "tailwindcss": "3",
68
68
  "vite": "npm:rolldown-vite@^7.3.1",
69
69
  "zod": "^4.3.5"
70
70
  },
@@ -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,69 @@
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
+ import { fileURLToPath } from 'url'
6
+
7
+ // Resolve tailwindcss CLI from this package's node_modules
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+ const tailwindBin = join(__dirname, '../../node_modules/.bin/tailwindcss')
10
+
11
+ export interface TailwindResult {
12
+ success: boolean
13
+ css: string
14
+ error?: string
15
+ }
16
+
17
+ interface ContentFile {
18
+ path: string
19
+ content: string
20
+ }
21
+
22
+ export async function compileTailwind(files: ContentFile[]): Promise<TailwindResult> {
23
+ const tempDir = mkdtempSync(join(tmpdir(), 'prev-tailwind-'))
24
+
25
+ try {
26
+ // Write content files (create parent dirs for nested paths)
27
+ for (const file of files) {
28
+ const filePath = join(tempDir, file.path)
29
+ const parentDir = dirname(filePath)
30
+ mkdirSync(parentDir, { recursive: true })
31
+ writeFileSync(filePath, file.content)
32
+ }
33
+
34
+ // Create Tailwind config - use .cjs for compatibility
35
+ const configContent = `
36
+ module.exports = {
37
+ content: [${JSON.stringify(tempDir + '/**/*.{tsx,jsx,ts,js,html}')}],
38
+ }
39
+ `
40
+ const configPath = join(tempDir, 'tailwind.config.cjs')
41
+ writeFileSync(configPath, configContent)
42
+
43
+ // Create input CSS
44
+ const inputCss = `
45
+ @tailwind base;
46
+ @tailwind components;
47
+ @tailwind utilities;
48
+ `
49
+ const inputPath = join(tempDir, 'input.css')
50
+ writeFileSync(inputPath, inputCss)
51
+
52
+ const outputPath = join(tempDir, 'output.css')
53
+
54
+ // Run Tailwind CLI from package's node_modules
55
+ await $`${tailwindBin} -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet()
56
+
57
+ const css = readFileSync(outputPath, 'utf-8')
58
+
59
+ return { success: true, css }
60
+ } catch (err) {
61
+ return {
62
+ success: false,
63
+ css: '',
64
+ error: err instanceof Error ? err.message : String(err),
65
+ }
66
+ } finally {
67
+ rmSync(tempDir, { recursive: true, force: true })
68
+ }
69
+ }
@@ -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
+ }