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 +194 -106
- package/dist/preview-runtime/build-optimized.d.ts +11 -0
- package/dist/preview-runtime/tailwind.d.ts +11 -0
- package/dist/preview-runtime/vendors.d.ts +6 -0
- package/package.json +2 -2
- package/src/preview-runtime/build-optimized.test.ts +47 -0
- package/src/preview-runtime/build-optimized.ts +136 -0
- package/src/preview-runtime/tailwind.test.ts +30 -0
- package/src/preview-runtime/tailwind.ts +69 -0
- package/src/preview-runtime/vendors.test.ts +15 -0
- package/src/preview-runtime/vendors.ts +52 -0
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
|
|
7
|
-
import { fileURLToPath as
|
|
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
|
|
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
|
|
21
|
-
import { existsSync as existsSync6, readFileSync as
|
|
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/
|
|
617
|
+
// src/preview-runtime/vendors.ts
|
|
618
618
|
import { build } from "esbuild";
|
|
619
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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:
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
});
|
|
768
|
+
return { contents: "", loader: "empty" };
|
|
769
|
+
});
|
|
770
|
+
}
|
|
700
771
|
}
|
|
701
|
-
|
|
772
|
+
]
|
|
702
773
|
});
|
|
703
|
-
const jsFile = result.outputFiles
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
875
|
+
rmSync2(oldPreviewsDir, { recursive: true });
|
|
799
876
|
}
|
|
800
877
|
if (existsSync4(targetDir)) {
|
|
801
|
-
|
|
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
|
|
813
|
-
|
|
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
|
-
|
|
819
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1650
|
+
rmSync3(viteCacheDir, { recursive: true });
|
|
1563
1651
|
cleared++;
|
|
1564
1652
|
}
|
|
1565
1653
|
if (existsSync7(nodeModulesVite)) {
|
|
1566
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1997
|
+
rmSync4(viteCacheDir, { recursive: true });
|
|
1910
1998
|
cleared++;
|
|
1911
1999
|
console.log(` ✓ Removed .vite/`);
|
|
1912
2000
|
}
|
|
1913
2001
|
if (existsSync8(nodeModulesVite)) {
|
|
1914
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2145
|
-
|
|
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 {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prev-cli",
|
|
3
|
-
"version": "0.24.
|
|
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": "
|
|
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
|
+
}
|