linters and prod transform
This commit is contained in:
parent
a40ce69bfc
commit
9bd6454ea9
11
package.json
11
package.json
|
|
@ -10,12 +10,19 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-extensions-color": "^4.23.0",
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/lint": "^6.8.1",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.28.4",
|
||||
"@uiw/codemirror-extensions-langs": "^4.23.0",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@uiw/react-json-view": "1.12.0",
|
||||
"color-selector-react": "0.3.0-beta.4",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"localforage": "^1.10.0",
|
||||
"match-sorter": "^6.3.4",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -30,8 +37,6 @@
|
|||
"valtio-history": "^0.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState, ComponentType, PropsWithChildren } from "react";
|
||||
import LiveContext from "./LiveContext";
|
||||
import { generateElement, renderElementAsync } from "../../utils/transpile";
|
||||
import { renderElementAsync } from "../../utils/transpile";
|
||||
import { themes } from "prism-react-renderer";
|
||||
|
||||
type ProviderState = {
|
||||
|
|
@ -13,7 +13,6 @@ type Props = {
|
|||
disabled?: boolean;
|
||||
enableTypeScript?: boolean;
|
||||
language?: string;
|
||||
noInline?: boolean;
|
||||
scope?: Record<string, unknown>;
|
||||
theme?: typeof themes.nightOwl;
|
||||
transformCode?(code: string): void;
|
||||
|
|
@ -28,7 +27,6 @@ function LiveProvider({
|
|||
disabled = false,
|
||||
scope,
|
||||
transformCode,
|
||||
noInline = true,
|
||||
}: PropsWithChildren<Props>) {
|
||||
const [state, setState] = useState<ProviderState>({
|
||||
error: undefined,
|
||||
|
|
@ -58,12 +56,8 @@ function LiveProvider({
|
|||
enableTypeScript,
|
||||
};
|
||||
|
||||
if (noInline) {
|
||||
setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation
|
||||
renderElementAsync(input, renderElement, errorCallback);
|
||||
} else {
|
||||
renderElement(generateElement(input, errorCallback));
|
||||
}
|
||||
setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation
|
||||
renderElementAsync(input, renderElement, errorCallback);
|
||||
} catch (error) {
|
||||
return errorCallback(error as Error);
|
||||
}
|
||||
|
|
@ -77,7 +71,7 @@ function LiveProvider({
|
|||
|
||||
useEffect(() => {
|
||||
transpileAsync(code).catch(onError);
|
||||
}, [code, scope, noInline, transformCode]);
|
||||
}, [code, scope, transformCode]);
|
||||
|
||||
const onChange = (newCode: string) => {
|
||||
transpileAsync(newCode).catch(onError);
|
||||
|
|
|
|||
|
|
@ -3,40 +3,40 @@ import { ColorSchema } from "../../typings";
|
|||
export const themeColorSchema: ColorSchema = {
|
||||
onLight: {
|
||||
"primary-7": "rgb(14, 66, 210)",
|
||||
"primary-6": "rgb(22, 93, 255)",
|
||||
"primary-5": "rgb(64, 128, 255)",
|
||||
"primary-4": "rgb(106, 161, 255)",
|
||||
"primary-3": "rgb(148, 191, 255)",
|
||||
"primary-2": "rgb(190, 218, 255)",
|
||||
"primary-1": "rgb(232, 243, 255)",
|
||||
// "primary-6": "rgb(22, 93, 255)",
|
||||
// "primary-5": "rgb(64, 128, 255)",
|
||||
// "primary-4": "rgb(106, 161, 255)",
|
||||
// "primary-3": "rgb(148, 191, 255)",
|
||||
// "primary-2": "rgb(190, 218, 255)",
|
||||
// "primary-1": "rgb(232, 243, 255)",
|
||||
"success-7": "rgb(0, 154, 41)",
|
||||
"success-6": "rgb(0, 180, 42)",
|
||||
"success-5": "rgb(35, 195, 67)",
|
||||
"success-4": "rgb(76, 210, 99)",
|
||||
"success-3": "rgb(123, 225, 136)",
|
||||
"success-2": "rgb(175, 240, 181)",
|
||||
"success-1": "rgb(232, 255, 234)",
|
||||
// "success-6": "rgb(0, 180, 42)",
|
||||
// "success-5": "rgb(35, 195, 67)",
|
||||
// "success-4": "rgb(76, 210, 99)",
|
||||
// "success-3": "rgb(123, 225, 136)",
|
||||
// "success-2": "rgb(175, 240, 181)",
|
||||
// "success-1": "rgb(232, 255, 234)",
|
||||
"warning-7": "rgb(210, 95, 0)",
|
||||
"warning-6": "rgb(255, 125, 0)",
|
||||
"warning-5": "rgb(255, 154, 46)",
|
||||
"warning-4": "rgb(255, 182, 93)",
|
||||
"warning-3": "rgb(255, 207, 139)",
|
||||
"warning-2": "rgb(255, 228, 186)",
|
||||
"warning-1": "rgb(255, 247, 232)",
|
||||
// "warning-6": "rgb(255, 125, 0)",
|
||||
// "warning-5": "rgb(255, 154, 46)",
|
||||
// "warning-4": "rgb(255, 182, 93)",
|
||||
// "warning-3": "rgb(255, 207, 139)",
|
||||
// "warning-2": "rgb(255, 228, 186)",
|
||||
// "warning-1": "rgb(255, 247, 232)",
|
||||
"danger-7": "rgb(203, 39, 45)",
|
||||
"danger-6": "rgb(245, 63, 63)",
|
||||
"danger-5": "rgb(247, 101, 96)",
|
||||
"danger-4": "rgb(249, 137, 129)",
|
||||
"danger-3": "rgb(251, 172, 163)",
|
||||
"danger-2": "rgb(253, 205, 197)",
|
||||
"danger-1": "rgb(255, 236, 232)",
|
||||
// "danger-6": "rgb(245, 63, 63)",
|
||||
// "danger-5": "rgb(247, 101, 96)",
|
||||
// "danger-4": "rgb(249, 137, 129)",
|
||||
// "danger-3": "rgb(251, 172, 163)",
|
||||
// "danger-2": "rgb(253, 205, 197)",
|
||||
// "danger-1": "rgb(255, 236, 232)",
|
||||
"link-7": "rgb(14, 66, 210)",
|
||||
"link-6": "rgb(22, 93, 255)",
|
||||
"link-5": "rgb(64, 128, 255)",
|
||||
"link-4": "rgb(106, 161, 255)",
|
||||
"link-3": "rgb(148, 191, 255)",
|
||||
"link-2": "rgb(190, 218, 255)",
|
||||
"link-1": "rgb(232, 243, 255)",
|
||||
// "link-6": "rgb(22, 93, 255)",
|
||||
// "link-5": "rgb(64, 128, 255)",
|
||||
// "link-4": "rgb(106, 161, 255)",
|
||||
// "link-3": "rgb(148, 191, 255)",
|
||||
// "link-2": "rgb(190, 218, 255)",
|
||||
// "link-1": "rgb(232, 243, 255)",
|
||||
"data-1": "rgb(64, 128, 255)",
|
||||
"data-2": "rgb(190, 218, 255)",
|
||||
"data-3": "rgb(85, 197, 253)",
|
||||
|
|
@ -57,36 +57,36 @@ export const themeColorSchema: ColorSchema = {
|
|||
"data-18": "rgb(137, 233, 224)",
|
||||
"data-19": "rgb(232, 101, 223)",
|
||||
"data-20": "rgb(247, 186, 239)",
|
||||
"border-1": "rgb(242, 243, 245)",
|
||||
"border-2": "rgb(229, 230, 235)",
|
||||
"border-3": "rgb(201, 205, 212)",
|
||||
"border-4": "rgb(134, 144, 156)",
|
||||
"border-5": "rgb(78, 89, 105)",
|
||||
"border-6": "rgb(29, 33, 41)",
|
||||
// "border-1": "rgb(242, 243, 245)",
|
||||
// "border-2": "rgb(229, 230, 235)",
|
||||
// "border-3": "rgb(201, 205, 212)",
|
||||
// "border-4": "rgb(134, 144, 156)",
|
||||
// "border-5": "rgb(78, 89, 105)",
|
||||
// "border-6": "rgb(29, 33, 41)",
|
||||
"border-7": "rgb(14, 16, 20)",
|
||||
"fill-1": "rgb(247, 248, 250)",
|
||||
"fill-2": "rgb(242, 243, 245)",
|
||||
"fill-3": "rgb(229, 230, 235)",
|
||||
"fill-4": "rgb(201, 205, 212)",
|
||||
"fill-5": "rgb(134, 144, 156)",
|
||||
"fill-6": "rgb(78, 89, 105)",
|
||||
"fill-7": "rgb(29, 33, 41)",
|
||||
"text-1": "rgb(29, 33, 41)",
|
||||
"text-2": "rgb(78, 89, 105)",
|
||||
"text-3": "rgb(134, 144, 156)",
|
||||
"text-4": "rgb(201, 205, 212)",
|
||||
"text-5": "rgb(229, 230, 235)",
|
||||
"text-6": "rgb(242, 243, 245)",
|
||||
"text-7": "rgb(247, 248, 250)",
|
||||
"background-1": "rgb(255, 255, 255)",
|
||||
"background-2": "rgb(255, 255, 255)",
|
||||
"background-3": "rgb(255, 255, 255)",
|
||||
"background-4": "rgb(255, 255, 255)",
|
||||
"background-5": "rgb(255, 255, 255)",
|
||||
"background-6": "rgb(255, 255, 255)",
|
||||
"background-7": "rgb(255, 255, 255)",
|
||||
"background-white": "rgb(255, 255, 255)",
|
||||
"background-black": "rgb(0, 0, 0)",
|
||||
"shadow": "rgb(255, 255, 255)",
|
||||
// "fill-1": "rgb(247, 248, 250)",
|
||||
// "fill-2": "rgb(242, 243, 245)",
|
||||
// "fill-3": "rgb(229, 230, 235)",
|
||||
// "fill-4": "rgb(201, 205, 212)",
|
||||
// "fill-5": "rgb(134, 144, 156)",
|
||||
// "fill-6": "rgb(78, 89, 105)",
|
||||
// "fill-7": "rgb(29, 33, 41)",
|
||||
// "text-1": "rgb(29, 33, 41)",
|
||||
// "text-2": "rgb(78, 89, 105)",
|
||||
// "text-3": "rgb(134, 144, 156)",
|
||||
// "text-4": "rgb(201, 205, 212)",
|
||||
// "text-5": "rgb(229, 230, 235)",
|
||||
// "text-6": "rgb(242, 243, 245)",
|
||||
// "text-7": "rgb(247, 248, 250)",
|
||||
// "background-1": "rgb(255, 255, 255)",
|
||||
// "background-2": "rgb(255, 255, 255)",
|
||||
// "background-3": "rgb(255, 255, 255)",
|
||||
// "background-4": "rgb(255, 255, 255)",
|
||||
// "background-5": "rgb(255, 255, 255)",
|
||||
// "background-6": "rgb(255, 255, 255)",
|
||||
// "background-7": "rgb(255, 255, 255)",
|
||||
// "background-white": "rgb(255, 255, 255)",
|
||||
// "background-black": "rgb(0, 0, 0)",
|
||||
// "shadow": "rgb(255, 255, 255)",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { CodeBlock } from '../../typings/index';
|
||||
import { CodeBlock } from "../../typings/index";
|
||||
|
||||
export const functionBlockList: CodeBlock[] = [
|
||||
{
|
||||
code: `
|
||||
|
||||
const Banner = (props: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
|
|
@ -27,7 +28,7 @@ const Banner = (props: {
|
|||
);
|
||||
};
|
||||
|
||||
export default Banner
|
||||
export default Banner;
|
||||
`,
|
||||
props: {
|
||||
title: "AI智能建站助手",
|
||||
|
|
@ -303,4 +304,15 @@ export default Banner
|
|||
},
|
||||
cpnName: "Contant",
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const Test = () => {
|
||||
return <div className="text-center" >Test</div>;
|
||||
};
|
||||
|
||||
export default Test;
|
||||
`,
|
||||
props: {},
|
||||
cpnName: "Test",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import React, { useState, useEffect, ComponentType } from "react";
|
||||
import { functionBlockList } from "../../data/functionBlocks";
|
||||
import { Transform } from "sucrase";
|
||||
import evalCode from "../../utils/transpile/evalCode";
|
||||
import transform from "../../utils/transpile/transform";
|
||||
import { ThemeProvider } from "../../components/Theme/ThemePrivider";
|
||||
import errorBoundary from "../../utils/transpile/errorBoundary";
|
||||
import "./style.css";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
import minifyCode from "../../utils/minify";
|
||||
// import { execToModule } from "../../utils/transpile/moduleImport";
|
||||
// import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
transformCodeProd,
|
||||
} from "../../utils/transpile/code-transformer";
|
||||
import evalCode from "../../utils/transpile/evalCode";
|
||||
|
||||
export const App = () => {
|
||||
// 定义成一个数组便于下面操作,实际上只有一个元素
|
||||
|
|
@ -19,8 +17,6 @@ export const App = () => {
|
|||
console.error(error);
|
||||
};
|
||||
|
||||
const transforms: Transform[] = ["jsx", "typescript"];
|
||||
|
||||
const render = (element: ComponentType) => {
|
||||
if (typeof element === "undefined") {
|
||||
errorCallBack(new SyntaxError("`render` must be called with valid JSX."));
|
||||
|
|
@ -32,13 +28,14 @@ export const App = () => {
|
|||
|
||||
// 开发环境下,useEffect会执行两次,模拟装载和卸载组件,生产环境没事。
|
||||
useEffect(() => {
|
||||
const allCpns = functionBlockList.map((item) => item.code).join("\n").replace(/export\s+default\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, "");
|
||||
const allCpns = functionBlockList
|
||||
.map((item) => "import " + item.cpnName + " from '" + item.cpnName + "';")
|
||||
.join("\n");
|
||||
const allPage = functionBlockList
|
||||
.map((item, index) => `<${item.cpnName} { ...props[${index}] }/>`)
|
||||
.join("\n");
|
||||
const allCode = `
|
||||
${allCpns}
|
||||
|
||||
const ApplicationContext = () => {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -47,30 +44,30 @@ export const App = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ApplicationContext
|
||||
export default ApplicationContext;
|
||||
`;
|
||||
|
||||
const propsList = functionBlockList.map((item) => item.props);
|
||||
|
||||
const allTransformed = transform({ transforms })(allCode);
|
||||
|
||||
setTimeout(async () => {
|
||||
const minifiedCode = await minifyCode(allTransformed);
|
||||
const codeToRun = minifiedCode.replace(/export\s+default/g, "return")
|
||||
const ApplicationContext = evalCode(
|
||||
codeToRun,
|
||||
{
|
||||
React,
|
||||
props: propsList,
|
||||
}
|
||||
);
|
||||
render(ApplicationContext);
|
||||
// // console.log(ApplicationContext);
|
||||
// const module = await execToModule(minifiedCode, {
|
||||
// React: 'https://unpkg.com/react@17/umd/react.production.min.js',
|
||||
// props: propsList,
|
||||
// })
|
||||
// render(module.default);
|
||||
const prod = await transformCodeProd({
|
||||
code: allCode,
|
||||
dependencies: (item) => {
|
||||
return {
|
||||
code: functionBlockList.find((i) => i.cpnName === item.identifier)
|
||||
?.code,
|
||||
identifier: item.identifier,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`prod: ${prod}`);
|
||||
|
||||
const app = await evalCode(prod, {
|
||||
React,
|
||||
props: propsList,
|
||||
});
|
||||
|
||||
render(app);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import JsonViewEditor from "@uiw/react-json-view/editor";
|
||||
|
||||
export const PropsEditor = (
|
||||
{
|
||||
newPropKey,
|
||||
setNewPropKey,
|
||||
handleAddProp,
|
||||
componentsProps,
|
||||
setComponentsProps
|
||||
}: {
|
||||
newPropKey: string,
|
||||
setNewPropKey: (value: string) => void,
|
||||
handleAddProp: () => void,
|
||||
componentsProps: Record<string, any>,
|
||||
setComponentsProps: (value: Record<string, any>) => void
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
<div className="p-4 border rounded shadow ">
|
||||
<h2 className="font-bold text-lg mb-2">Props 编辑器</h2>
|
||||
<div className="props-edit">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 font-bold">新属性名</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newPropKey}
|
||||
onChange={(e) => setNewPropKey(e.target.value)}
|
||||
className="border border-black rounded p-2 flex-grow"
|
||||
/>
|
||||
<button onClick={handleAddProp} className="p-2 rounded ml-2 border-gray-600">
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<JsonViewEditor
|
||||
value={componentsProps}
|
||||
keyName="props"
|
||||
style={
|
||||
{
|
||||
"--w-rjv-background-color": "#ffffff",
|
||||
"--w-rjv-border-left": "1px dashed #ebebeb",
|
||||
"--w-rjv-update-color": "#ff6ffd",
|
||||
} as any
|
||||
}
|
||||
onEdit={(opts) => {
|
||||
const updateNestedProp = (
|
||||
obj: Record<string, any>,
|
||||
path: string[],
|
||||
value: unknown
|
||||
) => {
|
||||
if (path.length === 1) {
|
||||
obj[path[0]] = value;
|
||||
} else {
|
||||
if (!obj[path[0]]) {
|
||||
obj[path[0]] = {};
|
||||
}
|
||||
updateNestedProp(obj[path[0]], path.slice(1), value);
|
||||
}
|
||||
};
|
||||
const updatedProps = { ...componentsProps };
|
||||
updateNestedProp(
|
||||
updatedProps,
|
||||
(opts as any).namespace,
|
||||
opts.value
|
||||
);
|
||||
setComponentsProps(updatedProps);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
export const Sidebar = ({
|
||||
items,
|
||||
selectedItem,
|
||||
onSelectItem,
|
||||
}: {
|
||||
items: { cpnName: string; props: Record<string, any>; code: string }[];
|
||||
selectedItem: number;
|
||||
onSelectItem: (index: number) => void;
|
||||
}) => (
|
||||
<div className="w-40 bg-gray-200 p-2">
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`p-2 cursor-pointer ${
|
||||
selectedItem === index ? "bg-blue-500 text-white" : "bg-white"
|
||||
}`}
|
||||
onClick={() => onSelectItem(index)}
|
||||
>
|
||||
{item.cpnName}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
ViewPlugin,
|
||||
EditorView,
|
||||
WidgetType,
|
||||
Decoration,
|
||||
ViewUpdate,
|
||||
DecorationSet,
|
||||
} from "@codemirror/view";
|
||||
import { Range } from "@codemirror/state";
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
|
||||
// Function to create a decoration for the className attribute
|
||||
function classNameDecorations(view: EditorView): DecorationSet {
|
||||
const widgets: Range<Decoration>[] = [];
|
||||
syntaxTree(view.state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name === "JSXAttribute") {
|
||||
const attributeName = view.state.doc.sliceString(from, to);
|
||||
|
||||
if (attributeName.startsWith("className")) {
|
||||
const widget = Decoration.widget({
|
||||
widget: new ClassNameWidget(),
|
||||
side: -1,
|
||||
});
|
||||
widgets.push(widget.range(from));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets);
|
||||
}
|
||||
|
||||
class ClassNameWidget extends WidgetType {
|
||||
toDOM() {
|
||||
const wrapper = document.createElement("span");
|
||||
const square = document.createElement("div");
|
||||
square.style.width = "12px";
|
||||
square.style.height = "12px";
|
||||
square.style.backgroundColor = "white";
|
||||
square.style.display = "inline-block";
|
||||
square.style.marginRight = "0.5ch";
|
||||
square.style.border = "1px solid #00000040";
|
||||
square.style.verticalAlign = "middle";
|
||||
wrapper.appendChild(square);
|
||||
return wrapper;
|
||||
}
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const classNameView = () =>
|
||||
ViewPlugin.fromClass(
|
||||
class ClassNameView {
|
||||
decorations;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = classNameDecorations(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = classNameDecorations(update.view);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
}
|
||||
);
|
||||
|
||||
export const classNamePlugin = [classNameView()];
|
||||
|
||||
export const classNameTheme = EditorView.baseTheme({
|
||||
"span[data-classname]": {
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
display: "inline-block",
|
||||
borderRadius: "2px",
|
||||
marginRight: "0.5ch",
|
||||
outline: "1px solid #00000040",
|
||||
overflow: "hidden",
|
||||
verticalAlign: "middle",
|
||||
marginTop: "-2px",
|
||||
},
|
||||
});
|
||||
|
||||
export const classNameExtension = [classNamePlugin, classNameTheme];
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
import { ViewPlugin, EditorView, ViewUpdate, WidgetType, Decoration, DecorationSet } from '@codemirror/view';
|
||||
import { Extension, Range } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import colors from 'colors-named';
|
||||
import hexs from 'colors-named-hex';
|
||||
import hslMatcher, { hlsStringToRGB, RGBAColor } from 'hsl-matcher';
|
||||
|
||||
export function toFullHex(color: string): string[] {
|
||||
if (color.length === 4) {
|
||||
// 3-char hex
|
||||
return [`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`, ''];
|
||||
}
|
||||
|
||||
if (color.length === 5) {
|
||||
// 4-char hex (alpha)
|
||||
return [`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`, color[4].repeat(2)];
|
||||
}
|
||||
|
||||
if (color.length === 9) {
|
||||
// 8-char hex (alpha)
|
||||
return [`#${color.slice(1, -2)}`, color.slice(-2)];
|
||||
}
|
||||
|
||||
return [color, ''];
|
||||
}
|
||||
/** https://stackoverflow.com/a/5624139/1334703 */
|
||||
export function rgbToHex(r: number, g: number, b: number) {
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
/** https://stackoverflow.com/a/5624139/1334703 */
|
||||
export function hexToRgb(hex: string) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/** https://css-tricks.com/converting-color-spaces-in-javascript/#aa-rgb-to-hsl */
|
||||
export function RGBToHSL(r: number, g: number, b: number) {
|
||||
(r /= 255), (g /= 255), (b /= 255);
|
||||
const max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b);
|
||||
let h = 0,
|
||||
s,
|
||||
l = (max + min) / 2;
|
||||
|
||||
if (max == min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
return { h: Math.floor(h * 360), s: Math.floor(s * 100), l: Math.floor(l * 100) };
|
||||
}
|
||||
|
||||
export enum ColorType {
|
||||
rgb = 'RGB',
|
||||
hex = 'HEX',
|
||||
named = 'NAMED',
|
||||
hsl = 'HSL',
|
||||
}
|
||||
|
||||
export interface ColorState {
|
||||
from: number;
|
||||
to: number;
|
||||
alpha: string;
|
||||
colorType: ColorType;
|
||||
}
|
||||
|
||||
const colorState = new WeakMap<HTMLInputElement, ColorState>();
|
||||
|
||||
type GetArrayElementType<T extends readonly any[]> = T extends readonly (infer U)[] ? U : never;
|
||||
|
||||
function colorDecorations(view: EditorView) {
|
||||
const widgets: Array<Range<Decoration>> = [];
|
||||
for (const range of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
enter: ({ type, from, to }) => {
|
||||
const callExp: string = view.state.doc.sliceString(from, to);
|
||||
|
||||
if (type.name === 'CallExpression' && callExp.startsWith('rgb')) {
|
||||
const match =
|
||||
/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,?\s*(\d{1,3})\s*(,\s*\d*\.\d*\s*)?\)/i.exec(callExp) ||
|
||||
/rgba?\(\s*(\d{1,3})\s*(\d{1,3})\s*(\d{1,3})\s*(\/?\s*\d+%)?(\/\s*\d+\.\d\s*)?\)/i.exec(callExp);
|
||||
if (!match) return;
|
||||
const [_, r, g, b, a] = match;
|
||||
const hex = rgbToHex(Number(r), Number(g), Number(b));
|
||||
const widget = Decoration.widget({
|
||||
widget: new ColorWidget({
|
||||
colorType: ColorType.rgb,
|
||||
color: hex,
|
||||
colorRaw: callExp,
|
||||
from,
|
||||
to,
|
||||
alpha: a ? a.replace(/(\/|,)/g, '') : '',
|
||||
}),
|
||||
side: 0,
|
||||
});
|
||||
widgets.push(widget.range(from));
|
||||
} else if (type.name === 'CallExpression' && hslMatcher(callExp)) {
|
||||
|
||||
const match = hlsStringToRGB(callExp) as RGBAColor;
|
||||
if (!match) return;
|
||||
const { r, g, b } = match;
|
||||
const hex = rgbToHex(Number(r), Number(g), Number(b));
|
||||
const widget = Decoration.widget({
|
||||
widget: new ColorWidget({
|
||||
colorType: ColorType.hsl,
|
||||
color: hex,
|
||||
colorRaw: callExp,
|
||||
from,
|
||||
to,
|
||||
alpha: match.a ? match.a.toString() : '',
|
||||
}),
|
||||
side: 0,
|
||||
});
|
||||
widgets.push(widget.range(from));
|
||||
} else if (type.name === 'ColorLiteral') {
|
||||
const [color, alpha] = toFullHex(callExp);
|
||||
const widget = Decoration.widget({
|
||||
widget: new ColorWidget({
|
||||
colorType: ColorType.hex,
|
||||
color,
|
||||
colorRaw: callExp,
|
||||
from,
|
||||
to,
|
||||
alpha,
|
||||
}),
|
||||
side: 0,
|
||||
});
|
||||
widgets.push(widget.range(from));
|
||||
} else if (type.name === 'ValueName') {
|
||||
const name = callExp as unknown as GetArrayElementType<typeof colors>;
|
||||
if (colors.includes(name)) {
|
||||
const widget = Decoration.widget({
|
||||
widget: new ColorWidget({
|
||||
colorType: ColorType.named,
|
||||
color: hexs[colors.indexOf(name)],
|
||||
colorRaw: callExp,
|
||||
from,
|
||||
to,
|
||||
alpha: '',
|
||||
}),
|
||||
side: 0,
|
||||
});
|
||||
widgets.push(widget.range(from));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return Decoration.set(widgets);
|
||||
}
|
||||
|
||||
class ColorWidget extends WidgetType {
|
||||
private readonly state: ColorState;
|
||||
private readonly color: string;
|
||||
private readonly colorRaw: string;
|
||||
|
||||
constructor({
|
||||
color,
|
||||
colorRaw,
|
||||
...state
|
||||
}: ColorState & {
|
||||
color: string;
|
||||
colorRaw: string;
|
||||
}) {
|
||||
super();
|
||||
this.state = state;
|
||||
this.color = color;
|
||||
this.colorRaw = colorRaw;
|
||||
}
|
||||
eq(other: ColorWidget) {
|
||||
return (
|
||||
other.state.colorType === this.state.colorType &&
|
||||
other.color === this.color &&
|
||||
other.state.from === this.state.from &&
|
||||
other.state.to === this.state.to &&
|
||||
other.state.alpha === this.state.alpha
|
||||
);
|
||||
}
|
||||
toDOM() {
|
||||
const picker = document.createElement('input');
|
||||
colorState.set(picker, this.state);
|
||||
picker.type = 'color';
|
||||
picker.value = this.color;
|
||||
picker.dataset['color'] = this.color;
|
||||
picker.dataset['colorraw'] = this.colorRaw;
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.appendChild(picker);
|
||||
wrapper.dataset['color'] = this.color;
|
||||
wrapper.style.backgroundColor = this.colorRaw;
|
||||
return wrapper;
|
||||
}
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const colorView = (showPicker: boolean = true) =>
|
||||
ViewPlugin.fromClass(
|
||||
class ColorView {
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = colorDecorations(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = colorDecorations(update.view);
|
||||
}
|
||||
const readOnly = update.view.contentDOM.ariaReadOnly === 'true';
|
||||
const editable = update.view.contentDOM.contentEditable === 'true';
|
||||
|
||||
const canBeEdited = readOnly === false && editable;
|
||||
this.changePicker(update.view, canBeEdited);
|
||||
}
|
||||
changePicker(view: EditorView, canBeEdited: boolean) {
|
||||
const doms = view.contentDOM.querySelectorAll('input[type=color]');
|
||||
doms.forEach((inp) => {
|
||||
if (!showPicker) {
|
||||
inp.setAttribute('disabled', '');
|
||||
} else {
|
||||
canBeEdited ? inp.removeAttribute('disabled') : inp.setAttribute('disabled', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
eventHandlers: {
|
||||
change: (e, view) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (
|
||||
target.nodeName !== 'INPUT' ||
|
||||
!target.parentElement ||
|
||||
(!target.dataset.color && !target.dataset.colorraw)
|
||||
)
|
||||
return false;
|
||||
const data = colorState.get(target)!;
|
||||
const value = target.value;
|
||||
const rgb = hexToRgb(value);
|
||||
const colorraw = target.dataset.colorraw;
|
||||
const slash = (target.dataset.colorraw || '').indexOf('/') > 4;
|
||||
const comma = (target.dataset.colorraw || '').indexOf(',') > 4;
|
||||
let converted = target.value;
|
||||
if (data.colorType === ColorType.rgb) {
|
||||
let funName = colorraw?.match(/^(rgba?)/) ? colorraw?.match(/^(rgba?)/)![0] : undefined;
|
||||
if (comma) {
|
||||
converted = rgb
|
||||
? `${funName}(${rgb.r}, ${rgb.g}, ${rgb.b}${data.alpha ? ', ' + data.alpha.trim() : ''})`
|
||||
: value;
|
||||
} else if (slash) {
|
||||
converted = rgb
|
||||
? `${funName}(${rgb.r} ${rgb.g} ${rgb.b}${data.alpha ? ' / ' + data.alpha.trim() : ''})`
|
||||
: value;
|
||||
} else {
|
||||
converted = rgb ? `${funName}(${rgb.r} ${rgb.g} ${rgb.b})` : value;
|
||||
}
|
||||
} else if (data.colorType === ColorType.hsl) {
|
||||
const rgb = hexToRgb(value);
|
||||
if (rgb) {
|
||||
const { h, s, l } = RGBToHSL(rgb?.r, rgb?.g, rgb?.b);
|
||||
converted = `hsl(${h}deg ${s}% ${l}%${data.alpha ? ' / ' + data.alpha : ''})`;
|
||||
}
|
||||
}
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
insert: converted,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const colorTheme = EditorView.baseTheme({
|
||||
'span[data-color]': {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
display: 'inline-block',
|
||||
borderRadius: '2px',
|
||||
marginRight: '0.5ch',
|
||||
outline: '1px solid #00000040',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'middle',
|
||||
marginTop: '-2px',
|
||||
},
|
||||
'span[data-color] input[type="color"]': {
|
||||
background: 'transparent',
|
||||
display: 'block',
|
||||
border: 'none',
|
||||
outline: '0',
|
||||
paddingLeft: '24px',
|
||||
height: '12px',
|
||||
},
|
||||
'span[data-color] input[type="color"]::-webkit-color-swatch': {
|
||||
border: 'none',
|
||||
paddingLeft: '24px',
|
||||
},
|
||||
});
|
||||
|
||||
export const color: Extension = [colorView(), colorTheme];
|
||||
|
|
@ -95,4 +95,4 @@ export function colorCompletions(data: Completion[] = []): Extension {
|
|||
});
|
||||
}
|
||||
|
||||
export const plugin = colorCompletions(colorMap);
|
||||
export default colorCompletions(colorMap);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { syntaxTree } from "@codemirror/language";
|
||||
import { linter, Diagnostic } from "@codemirror/lint";
|
||||
|
||||
const exportLinter = linter((view) => {
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
syntaxTree(view.state)
|
||||
.cursor()
|
||||
.iterate((node) => {
|
||||
if (node.name == "ExportDeclaration") {
|
||||
let code = view.state.doc.sliceString(node.from, node.to);
|
||||
if (!code.trim().endsWith(";")) {
|
||||
diagnostics.push({
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
severity: "error",
|
||||
message: "Export declaration must end with a semicolon",
|
||||
actions: [
|
||||
{
|
||||
name: "Add semicolon",
|
||||
apply(view, _from, to) {
|
||||
view.dispatch({
|
||||
changes: { from: to, to, insert: ";" },
|
||||
});
|
||||
// 把分号添加到结尾
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return diagnostics;
|
||||
});
|
||||
|
||||
export default exportLinter;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default as exportLinter } from "./exportLinter";
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { syntaxTree } from "@codemirror/language";
|
||||
import { linter, Diagnostic } from "@codemirror/lint";
|
||||
|
||||
// List of common JSX native identifiers to ignore
|
||||
const jsxNativeIdentifiers = new Set([
|
||||
"div", "span", "p", "a", "ul", "li", "ol", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"input", "button", "form", "label", "textarea", "select", "option", "img",
|
||||
"className", "id", "style", "key", "ref", "children"
|
||||
]);
|
||||
|
||||
const jsxIdentifierLinter = linter(view => {
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
let imports = new Set();
|
||||
let variableNames = new Set();
|
||||
|
||||
// Traverse the syntax tree to collect all import declarations and variable names
|
||||
syntaxTree(view.state).cursor().iterate(node => {
|
||||
if (node.name === "ImportDeclaration") {
|
||||
let code = view.state.doc.sliceString(node.from, node.to);
|
||||
let importedNames = code.match(/import\s+([^\s]+)\s+from\s+/);
|
||||
if (importedNames && importedNames[1]) {
|
||||
importedNames[1].split(',').forEach(name => imports.add(name.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.name === "VariableDeclaration") {
|
||||
let variableNode = node.node;
|
||||
let variableNameNode = variableNode.firstChild?.nextSibling;
|
||||
if (variableNameNode && variableNameNode.name === "VariableName") {
|
||||
let variableName = view.state.doc.sliceString(variableNameNode.from, variableNameNode.to);
|
||||
variableNames.add(variableName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Traverse the syntax tree to check for JSXIdentifiers and missing imports
|
||||
syntaxTree(view.state).cursor().iterate(node => {
|
||||
if (node.name === "JSXIdentifier") {
|
||||
let identifierName = view.state.doc.sliceString(node.from, node.to);
|
||||
if (!jsxNativeIdentifiers.has(identifierName) && !imports.has(identifierName) && !variableNames.has(identifierName)) {
|
||||
diagnostics.push({
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
severity: "error",
|
||||
message: `Missing import for ${identifierName}`,
|
||||
actions: [{
|
||||
name: "Add import",
|
||||
apply(view, _from, _to) {
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: 0, insert: `import ${identifierName} from "${identifierName}";\n` }
|
||||
});
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return diagnostics;
|
||||
});
|
||||
|
||||
export default jsxIdentifierLinter;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { syntaxTree } from "@codemirror/language";
|
||||
import { linter, Diagnostic } from "@codemirror/lint";
|
||||
|
||||
const variableImportLinter = linter(view => {
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
// Traverse the syntax tree to collect all import declarations and global variable names
|
||||
syntaxTree(view.state).cursor().iterate(node => {
|
||||
if (node.name == "ImportDeclaration") {
|
||||
let code = view.state.doc.sliceString(node.from, node.to);
|
||||
if (!code.trim().endsWith(";")) {
|
||||
diagnostics.push({
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
severity: "error",
|
||||
message: "Import declaration must end with a semicolon",
|
||||
actions: [{
|
||||
name: "Add semicolon",
|
||||
apply(view, _from, to) {
|
||||
view.dispatch({
|
||||
changes: { from: to, to, insert: ";" }
|
||||
});
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return diagnostics;
|
||||
});
|
||||
|
||||
export default variableImportLinter;
|
||||
|
|
@ -4,19 +4,29 @@ import { loadLanguage, langs } from "@uiw/codemirror-extensions-langs";
|
|||
import LiveProvider from "../../components/Live/LiveProvider";
|
||||
import LivePreview from "../../components/Live/LivePreview";
|
||||
import LiveError from "../../components/Live/LiveError";
|
||||
import { color, colorView, colorTheme } from "@uiw/codemirror-extensions-color";
|
||||
import JsonViewEditor from "@uiw/react-json-view/editor";
|
||||
import { ThemeProvider } from "../../components/Theme/ThemePrivider";
|
||||
import { plugin as colorCom } from "./completions";
|
||||
import { functionBlockList } from "../../data/functionBlocks";
|
||||
import { vscodeDark } from "@uiw/codemirror-theme-vscode";
|
||||
import ColorPalette from "./components/colorPalette";
|
||||
import { PropsEditor } from "./components/props-editor";
|
||||
import { Sidebar } from "./components/sidebar";
|
||||
import { color } from "./extensions/colorExtension";
|
||||
import exportLinter from "./extensions/exportLinter";
|
||||
import variableImportLinter from "./extensions/variableImportLinter";
|
||||
import jsxIdentifierLinter from "./extensions/jsxIdentifierLinter";
|
||||
import { classNameExtension } from "./extensions/classNameExtension ";
|
||||
|
||||
loadLanguage("tsx");
|
||||
const extensions = [color, langs.tsx(), colorView(false), colorTheme, colorCom];
|
||||
const extensions = [
|
||||
langs.tsx(),
|
||||
color,
|
||||
exportLinter,
|
||||
variableImportLinter,
|
||||
jsxIdentifierLinter,
|
||||
classNameExtension,
|
||||
];
|
||||
|
||||
export const Creation = () => {
|
||||
// useEffect 拦截ctrls等操作,避免浏览器默认行为
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && event.key === "s") {
|
||||
|
|
@ -30,15 +40,21 @@ export const Creation = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const [selectedBlockIndex, setSelectedBlockIndex] = React.useState(0);
|
||||
const [componentName, setComponentName] = React.useState(
|
||||
functionBlockList[0].cpnName
|
||||
);
|
||||
|
||||
const [componentsProps, setComponentsProps] = React.useState(
|
||||
functionBlockList[0].props
|
||||
);
|
||||
|
||||
const [newPropKey, setNewPropKey] = React.useState("");
|
||||
const [code, setCode] = React.useState(functionBlockList[0].code);
|
||||
|
||||
React.useEffect(() => {
|
||||
setComponentName(functionBlockList[selectedBlockIndex].cpnName);
|
||||
setComponentsProps(functionBlockList[selectedBlockIndex].props);
|
||||
setCode(functionBlockList[selectedBlockIndex].code);
|
||||
}, [selectedBlockIndex]);
|
||||
|
||||
const handleAddProp = () => {
|
||||
if (newPropKey.trim() !== "") {
|
||||
|
|
@ -50,8 +66,6 @@ export const Creation = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const [code, setCode] = React.useState(functionBlockList[0].code);
|
||||
|
||||
const onChange = React.useCallback((value: string) => {
|
||||
setCode(value);
|
||||
}, []);
|
||||
|
|
@ -63,100 +77,68 @@ export const Creation = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 font-bold text-lg border-primary-1">
|
||||
组件名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={componentName}
|
||||
onChange={handleComponentNameChange}
|
||||
className="border border-black rounded p-2 w-full"
|
||||
/>
|
||||
</div>
|
||||
<LiveProvider code={code} noInline>
|
||||
<div className="h-full grid lg:grid-cols-2 gap-4 border p-4 rounded shadow">
|
||||
<div className="border rounded p-2">
|
||||
<CodeMirror
|
||||
value={code}
|
||||
height="100%"
|
||||
theme={vscodeDark}
|
||||
extensions={[
|
||||
...extensions,
|
||||
basicSetup({
|
||||
autocompletion: true,
|
||||
lintKeymap: true,
|
||||
}),
|
||||
]}
|
||||
onChange={onChange}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="border rounded p-2">
|
||||
<ThemeProvider>
|
||||
<LivePreview {...componentsProps} />
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Sidebar
|
||||
items={functionBlockList}
|
||||
selectedItem={selectedBlockIndex}
|
||||
onSelectItem={setSelectedBlockIndex}
|
||||
/>
|
||||
<div className="p-4 flex-1 overflow-y-scroll h-screen">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 font-bold text-lg border-primary-1">
|
||||
组件名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={componentName}
|
||||
onChange={handleComponentNameChange}
|
||||
className="border border-black rounded p-2 w-full"
|
||||
/>
|
||||
</div>
|
||||
<LiveError className="text-red-700 bg-gray-200 mt-2 p-2 rounded mb-2" />
|
||||
</LiveProvider>
|
||||
<div className="h-full grid lg:grid-cols-2 gap-4 border p-4 rounded shadow">
|
||||
<div className="p-4 border rounded shadow ">
|
||||
<h2 className="font-bold text-lg mb-2">Props 编辑器</h2>
|
||||
<div className="props-edit">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 font-bold">新属性名</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newPropKey}
|
||||
onChange={(e) => setNewPropKey(e.target.value)}
|
||||
className="border border-black rounded p-2 flex-grow"
|
||||
<div className="editor">
|
||||
<LiveProvider code={code}>
|
||||
<div className="grid lg:grid-cols-2 gap-4 border p-4 rounded shadow">
|
||||
<div
|
||||
className="border rounded p-2
|
||||
max-h-screen overflow-y-scroll"
|
||||
>
|
||||
<CodeMirror
|
||||
value={code}
|
||||
theme={vscodeDark}
|
||||
extensions={[
|
||||
...extensions,
|
||||
basicSetup({
|
||||
autocompletion: true,
|
||||
lintKeymap: true,
|
||||
syntaxHighlighting: true,
|
||||
searchKeymap: true,
|
||||
}),
|
||||
]}
|
||||
onChange={onChange}
|
||||
className="font-mono"
|
||||
/>
|
||||
<button onClick={handleAddProp} className="p-2 rounded ml-2 border-gray-600">
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="border rounded p-2 max-h-screen overflow-y-scroll">
|
||||
<ThemeProvider>
|
||||
<LivePreview {...componentsProps} />
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
</div>
|
||||
<JsonViewEditor
|
||||
value={componentsProps}
|
||||
keyName="props"
|
||||
style={
|
||||
{
|
||||
"--w-rjv-background-color": "#ffffff",
|
||||
"--w-rjv-border-left": "1px dashed #ebebeb",
|
||||
"--w-rjv-update-color": "#ff6ffd",
|
||||
} as any
|
||||
}
|
||||
onEdit={(opts) => {
|
||||
const updateNestedProp = (
|
||||
obj: Record<string, any>,
|
||||
path: string[],
|
||||
value: unknown
|
||||
) => {
|
||||
if (path.length === 1) {
|
||||
obj[path[0]] = value;
|
||||
} else {
|
||||
if (!obj[path[0]]) {
|
||||
obj[path[0]] = {};
|
||||
}
|
||||
updateNestedProp(obj[path[0]], path.slice(1), value);
|
||||
}
|
||||
};
|
||||
const updatedProps = { ...componentsProps };
|
||||
updateNestedProp(
|
||||
updatedProps,
|
||||
(opts as any).namespace,
|
||||
opts.value
|
||||
);
|
||||
setComponentsProps(updatedProps);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<LiveError className="text-red-700 bg-gray-200 mt-2 p-2 rounded mb-2" />
|
||||
</LiveProvider>
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 gap-4 border p-4 rounded shadow">
|
||||
<PropsEditor
|
||||
{...{
|
||||
newPropKey,
|
||||
setNewPropKey,
|
||||
handleAddProp,
|
||||
componentsProps,
|
||||
setComponentsProps,
|
||||
}}
|
||||
/>
|
||||
<ColorPalette />
|
||||
</div>
|
||||
<ColorPalette />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ export const Editor = () => {
|
|||
return (
|
||||
<LiveProvider
|
||||
code={func.code}
|
||||
noInline
|
||||
key={index}
|
||||
>
|
||||
<ClickContextProvider
|
||||
|
|
|
|||
|
|
@ -1,26 +1,12 @@
|
|||
import { minify } from "terser";
|
||||
import { jsxConst } from "../handler";
|
||||
// import { jsxConst } from "../handler";
|
||||
|
||||
export default async function minifyCode(code: string) {
|
||||
export default async function minifyCode(
|
||||
code: string,
|
||||
reserved: string[] = []
|
||||
) {
|
||||
|
||||
console.log("minifyCode", code);
|
||||
|
||||
const replaced = code
|
||||
// .replace(RegExp(`React.createElement`, "g"), `rc`)
|
||||
// .replace(RegExp(`React.Fragment`, "g"), `rf`)
|
||||
// .replace(RegExp(`React.useState`, "g"), `rs`)
|
||||
// .replace(RegExp(`React.useEffect`, "g"), `re`)
|
||||
// .replace(RegExp(`React.Component`, "g"), `rcp`)
|
||||
// .replace(RegExp(`React.createContext`, "g"), `rcc`)
|
||||
// .replace(RegExp(`React.useContext`, "g"), `rcu`)
|
||||
// .replace(RegExp(`React.useReducer`, "g"), `rd`)
|
||||
// .replace(RegExp(`React.memo`, "g"), `rm`)
|
||||
// .replace(
|
||||
// jsxConst,
|
||||
// `${jsxConst} const rc=React.createElement,rf=React.Fragment,rs=React.useState,re=React.useEffect,rcp=React.Component,rcc=React.createContext,rcu=React.useContext,rd=React.useReducer,rm=React.memo;`
|
||||
// );
|
||||
|
||||
const { code: minifiedCode } = await minify(replaced, {
|
||||
const { code: minifiedCode } = await minify(code, {
|
||||
compress: {
|
||||
passes: 2,
|
||||
drop_console: true,
|
||||
|
|
@ -29,7 +15,13 @@ export default async function minifyCode(code: string) {
|
|||
},
|
||||
mangle: {
|
||||
toplevel: true,
|
||||
reserved,
|
||||
},
|
||||
});
|
||||
return minifiedCode || "";
|
||||
|
||||
if (!minifiedCode) {
|
||||
throw new Error("Failed to minify code");
|
||||
}
|
||||
|
||||
return "const rc=React.createElement;" + minifiedCode.replace(RegExp(`React.createElement`, "g"), `rc`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
import { Transform } from "sucrase";
|
||||
import transform from "./transform";
|
||||
import {
|
||||
ImportInfo,
|
||||
ReactComponentInfo,
|
||||
TransformCodeProdProps,
|
||||
TransformerProps,
|
||||
} from "../../../typings";
|
||||
import evalCode from "./evalCode";
|
||||
import minifyCode from "../minify";
|
||||
|
||||
const defaultTransforms: Transform[] = ["jsx", "typescript"];
|
||||
|
||||
/**
|
||||
* 将代码编译并生成函数体
|
||||
* @param code 需要编译的代码
|
||||
* @param transforms 编译选项
|
||||
* @returns 返回编译后的代码和导入信息
|
||||
*/
|
||||
export const transformCode = async (
|
||||
code: string,
|
||||
transforms: Transform[] = defaultTransforms
|
||||
): Promise<[string, ImportInfo[]]> => {
|
||||
const allTransformed = transform({ transforms })(code);
|
||||
|
||||
// 这里还有import和export default,需要处理一下
|
||||
const imports: ImportInfo[] = [];
|
||||
const regex = /import\s+([^;]+)\s+from\s+['"]([^'"]+)['"];/g;
|
||||
const matches = allTransformed.matchAll(regex);
|
||||
|
||||
for (const match of matches) {
|
||||
imports.push({
|
||||
identifier: match[1].trim(),
|
||||
path: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有的标识符, 防止压缩的时候被替换
|
||||
const identifiers = imports.map((item) => item.identifier);
|
||||
|
||||
// 压缩
|
||||
const minifiedCode = await minifyCode(allTransformed, identifiers);
|
||||
|
||||
// 替换导入和导出
|
||||
const codeToRun = minifiedCode
|
||||
.replace(/export\s+default/g, "return")
|
||||
.replace(/import\s+[^;]+;/g, "");
|
||||
return [codeToRun, imports];
|
||||
};
|
||||
|
||||
/**
|
||||
* 将代码转换为React组件
|
||||
* @param param0 参数
|
||||
* @returns 返回React组件和导入信息
|
||||
*/
|
||||
export const transformCodeToReactComponent = async ({
|
||||
code,
|
||||
functonProps,
|
||||
transforms = defaultTransforms,
|
||||
}: TransformerProps): Promise<ReactComponentInfo> => {
|
||||
const [codeToRun, imports] = await transformCode(code, transforms);
|
||||
|
||||
return {
|
||||
element: evalCode(codeToRun, functonProps),
|
||||
importMap: imports,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归处理代码和依赖,构建生产环境代码
|
||||
* @param param0 参数
|
||||
* @returns 返回生产环境代码
|
||||
*/
|
||||
export const transformCodeProd = async ({
|
||||
code,
|
||||
dependencies,
|
||||
excludeImports = [],
|
||||
}: TransformCodeProdProps): Promise<string> => {
|
||||
// 递归处理代码和依赖
|
||||
const importedIdentifiers = new Map<string, string>();
|
||||
|
||||
const resolveImports = async (code: string): Promise<string> => {
|
||||
const imports: ImportInfo[] = [];
|
||||
const regex = /import\s+([^;]+)\s+from\s+['"]([^'"]+)['"];/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
imports.push({
|
||||
identifier: match[1].trim(),
|
||||
path: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有依赖的代码
|
||||
const promises = imports.map(async (imp) => {
|
||||
// 如果是排除的导入或者已经导入过了,则直接跳过
|
||||
if (excludeImports.includes(imp.identifier)) {
|
||||
return {
|
||||
identifier: imp.identifier,
|
||||
code: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (importedIdentifiers.has(imp.identifier)) {
|
||||
return {
|
||||
identifier: imp.identifier,
|
||||
code: importedIdentifiers.get(imp.identifier) || "",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await dependencies(imp);
|
||||
if (!result || !result.code) {
|
||||
throw new Error(
|
||||
`Failed to resolve dependency: ${imp.identifier} in code: \n${code}`
|
||||
);
|
||||
}
|
||||
|
||||
// 递归处理获取的依赖代码
|
||||
const resolvedCode = await resolveImports(result.code);
|
||||
|
||||
return {
|
||||
identifier: imp.identifier,
|
||||
code: resolvedCode,
|
||||
};
|
||||
});
|
||||
|
||||
const resolvedDependencies = await Promise.all(promises);
|
||||
|
||||
// 替换导入标识符为实际代码
|
||||
resolvedDependencies.forEach(({ identifier, code: depCode }) => {
|
||||
if (!depCode) return;
|
||||
|
||||
// 如果这个标识符已经处理过,直接替换掉import
|
||||
if (importedIdentifiers.has(identifier)) {
|
||||
const importRegex = new RegExp(
|
||||
`import\\s+${identifier}\\s+from\\s+['"][^'"]+['"];`,
|
||||
"g"
|
||||
);
|
||||
code = code.replace(importRegex, "");
|
||||
return;
|
||||
}
|
||||
|
||||
const noExport = depCode.replace(/export\s+default\s+[\s\S]*?;/g, "");
|
||||
|
||||
const importRegex = new RegExp(
|
||||
`import\\s+${identifier}\\s+from\\s+['"][^'"]+['"];`,
|
||||
"g"
|
||||
);
|
||||
|
||||
const all = code.match(importRegex);
|
||||
|
||||
// 如果大于1个,则在这个代码块中存在问题(重复导入)
|
||||
if (all!.length > 1) {
|
||||
throw new Error(
|
||||
`Duplicate import found for ${identifier} in code: \n${code}`
|
||||
);
|
||||
}
|
||||
|
||||
code = code.replace(importRegex, noExport);
|
||||
|
||||
// 标记为已经处理过
|
||||
importedIdentifiers.set(identifier, depCode);
|
||||
});
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理代码
|
||||
*/
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// 初始解析代码
|
||||
try {
|
||||
const resolvedCode = await resolveImports(code);
|
||||
|
||||
console.log(`resolvedCode: ${resolvedCode}`);
|
||||
|
||||
const prod = await transformCode(resolvedCode);
|
||||
resolve(prod[0]);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
import React, { ComponentType } from "react";
|
||||
import transform from "./transform";
|
||||
import errorBoundary from "./errorBoundary";
|
||||
import evalCode from "./evalCode";
|
||||
import compose from "./compose";
|
||||
import { Transform } from "sucrase";
|
||||
import { addJsxConst, spliceJsxConst, trimCode, wrapReturn } from "../handler";
|
||||
import { transformCodeToReactComponent } from "./code-transformer";
|
||||
|
||||
type GenerateOptions = {
|
||||
code: string;
|
||||
|
|
@ -12,40 +9,7 @@ type GenerateOptions = {
|
|||
enableTypeScript: boolean;
|
||||
};
|
||||
|
||||
export const generateElement = (
|
||||
{ code = "", scope = {}, enableTypeScript = true }: GenerateOptions,
|
||||
errorCallback: (error: Error) => void
|
||||
) => {
|
||||
/**
|
||||
* To enable TypeScript we need to transform the TS to JS code first,
|
||||
* splice off the JSX const, wrap the eval in a return statement, then
|
||||
* transform any imports. The two-phase approach is required to do
|
||||
* the implicit evaluation and not wrap leading Interface or Type
|
||||
* statements in the return.
|
||||
*/
|
||||
|
||||
const firstPassTransforms: Transform[] = ["jsx"];
|
||||
enableTypeScript && firstPassTransforms.push("typescript");
|
||||
|
||||
// 通过compose函数,将多个函数组合成一个函数,用于Inline编辑器的代码转换
|
||||
const transformed = compose<string>(
|
||||
addJsxConst,
|
||||
transform({ transforms: ["imports"] }),
|
||||
spliceJsxConst,
|
||||
trimCode,
|
||||
transform({ transforms: firstPassTransforms }),
|
||||
wrapReturn,
|
||||
trimCode
|
||||
)(code);
|
||||
|
||||
return errorBoundary(
|
||||
// 作用域在这里定义,用到React,所以需要传入React
|
||||
evalCode(transformed, { React, ...scope }),
|
||||
errorCallback
|
||||
);
|
||||
};
|
||||
|
||||
export const renderElementAsync = (
|
||||
export const renderElementAsync = async (
|
||||
{ code = "", scope = {}, enableTypeScript = true }: GenerateOptions,
|
||||
resultCallback: (sender: ComponentType) => void,
|
||||
errorCallback: (error: Error) => void
|
||||
|
|
@ -53,7 +17,9 @@ export const renderElementAsync = (
|
|||
) => {
|
||||
const render = (element: ComponentType) => {
|
||||
if (typeof element === "undefined") {
|
||||
errorCallback(new SyntaxError("The Code didn't return a valid JSX element!"));
|
||||
errorCallback(
|
||||
new SyntaxError("The Code didn't return a valid JSX element!")
|
||||
);
|
||||
} else {
|
||||
resultCallback(errorBoundary(element, errorCallback));
|
||||
}
|
||||
|
|
@ -68,18 +34,14 @@ export const renderElementAsync = (
|
|||
const transforms: Transform[] = ["jsx"];
|
||||
enableTypeScript && transforms.splice(1, 0, "typescript");
|
||||
|
||||
const codeTorun = transform({ transforms })(code).replace(
|
||||
/export default/g,
|
||||
"return"
|
||||
);
|
||||
|
||||
const Cpn = evalCode(codeTorun, { React, ...scope, render });
|
||||
|
||||
// if (!React.isValidElement(Cpn)) {
|
||||
// return errorCallback(
|
||||
// new SyntaxError("The Code didn't return a valid JSX element!")
|
||||
// );
|
||||
// }
|
||||
|
||||
render(Cpn);
|
||||
try {
|
||||
const elementInfo = await transformCodeToReactComponent({
|
||||
code,
|
||||
functonProps: { React, ...scope } as any,
|
||||
});
|
||||
console.log(`importMap: ${JSON.stringify(elementInfo.importMap)}`);
|
||||
render(elementInfo.element);
|
||||
} catch (error: any) {
|
||||
errorCallback(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export const execToModule = async (
|
|||
// 添加到code之前
|
||||
const scopedCode = `${importStatements}\n${code}`;
|
||||
|
||||
// 使用Babel或其他工具进行转换
|
||||
const allTransformed = transform({ transforms })(scopedCode);
|
||||
const minifiedCode = await minifyCode(allTransformed);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { transform as _transform, Transform } from "sucrase";
|
||||
|
||||
const defaultTransforms: Transform[] = ["jsx", "imports"];
|
||||
const defaultTransforms: Transform[] = ["jsx", "typescript"];
|
||||
|
||||
type Options = {
|
||||
transforms?: Transform[];
|
||||
|
|
@ -13,4 +13,4 @@ export default function transform(opts: Options = {}) {
|
|||
|
||||
return (code: string) =>
|
||||
_transform(code, { transforms, production: true }).code;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import React, { ComponentType } from "react";
|
||||
import { Transform } from "sucrase";
|
||||
|
||||
export type CodeBlock = {
|
||||
code: string;
|
||||
cpnName: string;
|
||||
|
|
@ -58,3 +61,40 @@ export type Change = {
|
|||
key: string;
|
||||
type: ChangeType;
|
||||
};
|
||||
|
||||
export type ImportInfo = {
|
||||
identifier: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type ReactComponentInfo = {
|
||||
element: ComponentType;
|
||||
importMap: ImportInfo[];
|
||||
};
|
||||
|
||||
export type TransformerProps = {
|
||||
code: string;
|
||||
functonProps: {
|
||||
React: typeof React;
|
||||
props: Record<string, any>;
|
||||
[key: string]: any;
|
||||
};
|
||||
transforms?: Transform[];
|
||||
};
|
||||
|
||||
export type ReslovedDependency = {
|
||||
identifier: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
export type DependencyGetter = (info: ImportInfo) => ReslovedDependency | Promise<ReslovedDependency>;
|
||||
|
||||
export type TransformCodeProdProps = {
|
||||
code: string;
|
||||
/**
|
||||
* 组件名(标识符) to 组件code
|
||||
*/
|
||||
dependencies: DependencyGetter;
|
||||
transforms?: Transform[];
|
||||
excludeImports?: string[];
|
||||
};
|
||||
Loading…
Reference in New Issue