linters and prod transform

This commit is contained in:
pqcqaq 2024-07-22 19:06:15 +08:00
parent a40ce69bfc
commit 9bd6454ea9
22 changed files with 1087 additions and 282 deletions

View File

@ -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",

View File

@ -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);

View File

@ -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)",
},
};

View File

@ -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",
},
];

View File

@ -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);
}, []);

View File

@ -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>
)
}

View File

@ -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>
);

View File

@ -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];

View File

@ -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];

View File

@ -95,4 +95,4 @@ export function colorCompletions(data: Completion[] = []): Extension {
});
}
export const plugin = colorCompletions(colorMap);
export default colorCompletions(colorMap);

View File

@ -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;

View File

@ -0,0 +1,2 @@
export { default as exportLinter } from "./exportLinter";

View File

@ -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;

View File

@ -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;

View File

@ -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>
);

View File

@ -107,7 +107,6 @@ export const Editor = () => {
return (
<LiveProvider
code={func.code}
noInline
key={index}
>
<ClickContextProvider

View File

@ -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`);
}

View File

@ -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);
}
});
};

View File

@ -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);
}
};

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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[];
};