diff --git a/CHANGELOG.md b/CHANGELOG.md index b17fdd86..3cc136ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,12 @@ - [Flutter] Poligon Node support with XImage (svg) - [Lint] Primal naming & grouping linting for better code export quality. this is tracked sperately on [lint](https://github.com/bridgedxyz/lint) +## [2022.4.0.1] - 2022-04-08 + +- D2C: CSS Duplication reduction +- UI: Prettier & Dart formatter added +- Preview: React executable live scripting feature added + ## [2022.3.4] - 2022-03-30 - fix incomplete autolayout flex mapping diff --git a/package.json b/package.json index aaf40b5a..1cb0000e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,11 @@ "packages/design-to-code/packages/builder-*", "packages/design-to-code/packages/support-*", "packages/design-to-code/packages/reflect-detection", - "packages/design-to-code/externals/coli/packages/*" + "packages/design-to-code/externals/coli/packages/*", + "packages/design-to-code/editor-packages/editor-services-esbuild", + "packages/design-to-code/editor-packages/editor-services-prettier", + "packages/design-to-code/editor-packages/editor-services-jsx-syntax-highlight", + "packages/design-to-code/editor-packages/editor-services-webworker-core" ] }, "repository": "https://github.com/gridaco/assistant", @@ -39,7 +43,7 @@ "sketch": "yarn workspace sketch run render", "web": "yarn workspace web run dev", "xd": "yarn workspace xd run build", - "test": "cd figma-native && yarn build", + "test": "cd figma && yarn build", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook" }, diff --git a/packages/_utils/debounce.ts b/packages/_utils/debounce.ts new file mode 100644 index 00000000..b03fc7c5 --- /dev/null +++ b/packages/_utils/debounce.ts @@ -0,0 +1,19 @@ +export function debounce( + func: T, + wait: number = 50, + immediate?: boolean +) { + var timeout; + return function () { + // @ts-ignore + var context = this, + args = arguments; + var later = function () { + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} diff --git a/packages/_utils/package.json b/packages/_utils/package.json new file mode 100644 index 00000000..d9d16d1d --- /dev/null +++ b/packages/_utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@app/utils", + "version": "0.0.0", + "private": false +} \ No newline at end of file diff --git a/packages/app-design-to-code/__plugin/design-to-code.ts b/packages/app-design-to-code/__plugin/design-to-code.ts index 12698784..1d4f7006 100644 --- a/packages/app-design-to-code/__plugin/design-to-code.ts +++ b/packages/app-design-to-code/__plugin/design-to-code.ts @@ -11,6 +11,7 @@ import { react_presets, flutter_presets, vanilla_presets, + preview_presets, } from "@grida/builder-config-preset"; type O = output.ComponentOutput; @@ -135,7 +136,7 @@ export async function designToFixedPreviewVanilla( id: reflectDesign.id, entry: reflectDesign, }, - framework: vanilla_presets.vanilla_default, + framework: preview_presets.default, build_config: build_config, asset_config: { // the asset replacement on assistant is handled on ui thread. diff --git a/packages/app-design-to-code/__plugin/index.ts b/packages/app-design-to-code/__plugin/index.ts index 630508de..19ee679c 100644 --- a/packages/app-design-to-code/__plugin/index.ts +++ b/packages/app-design-to-code/__plugin/index.ts @@ -64,9 +64,10 @@ async function _handle_code_gen_request(req: CodeGenRequest) { }; const transportable_config = { type: transportable_config_map[mode] }; // host images - const transportableImageAssetRepository = await repo_assets.MainImageRepository.instance - .get("fill-later-assets") - .makeTransportable(transportable_config); + const transportableImageAssetRepository = + await repo_assets.MainImageRepository.instance + .get("fill-later-assets") + .makeTransportable(transportable_config); figma.ui.postMessage({ type: EK_IMAGE_ASSET_REPOSITORY_MAP, data: transportableImageAssetRepository, @@ -87,6 +88,7 @@ async function _handle_code_gen_request(req: CodeGenRequest) { case "vanilla": { const vanillaBuild = await designToVanilla(rnode); post_cb({ + name: vanillaBuild.name, code: vanillaBuild.code, app: vanillaBuild.scaffold, vanilla_preview_source: vanilla_preview_source, @@ -96,6 +98,7 @@ async function _handle_code_gen_request(req: CodeGenRequest) { case "react": { const reactBuild = await designToReact(rnode); post_cb({ + name: reactBuild.name, code: reactBuild.code, app: reactBuild.scaffold, vanilla_preview_source: vanilla_preview_source, @@ -105,6 +108,7 @@ async function _handle_code_gen_request(req: CodeGenRequest) { case "flutter": { const flutterBuild = await designToFlutter(rnode, asset_export_job); post_cb({ + name: flutterBuild.name, code: flutterBuild.code, app: flutterBuild.scaffold, vanilla_preview_source: vanilla_preview_source, @@ -118,7 +122,12 @@ async function _handle_code_gen_request(req: CodeGenRequest) { //#endregion } -function post_cb(data: { code; app; vanilla_preview_source }) { +function post_cb(data: { + code; + app; + vanilla_preview_source: string; + name: string; +}) { figma.ui.postMessage({ type: EK_GENERATED_CODE_PLAIN, data: data, diff --git a/packages/app-design-to-code/code-screen.tsx b/packages/app-design-to-code/code-screen.tsx index 0de7f52b..302cbfc8 100644 --- a/packages/app-design-to-code/code-screen.tsx +++ b/packages/app-design-to-code/code-screen.tsx @@ -13,6 +13,8 @@ import { Resizable } from "re-resizable"; import { useScrollTriggeredAnimation } from "app/lib/components/motions"; import { useSetRecoilState } from "recoil"; import { hide_navigation } from "app/lib/main/global-state-atoms"; +import bundler from "@code-editor/esbuild-services"; +import { debounce } from "../_utils/debounce"; const resizeBarBase = 5; const resizeBarVerPadding = 5; @@ -22,10 +24,15 @@ const default_responsive_preview_height_for_code_screen = 300; export function CodeScreen() { const selection = useSingleSelection(); - const [vanilla_preview_source, set_vanilla_preview_source] = - useState(); + const [isBuilding, setIsBuilding] = useState(true); + const [initialPreviewData, setInitialPreviewData] = useState(); + const [esbuildPreviewData, setEsbuildPreviewData] = useState(); + const [previewMode, setPreviewMode] = useState<"responsive" | "esbuild">( + "responsive" + ); const [source, setSource] = useState(); const [app, setApp] = useState(); + const [name, setName] = useState(); const [useroption, setUseroption] = useState(); const onCopyClicked = (e) => { @@ -40,15 +47,60 @@ export function CodeScreen() { v: string, r?: repo_assets.TransportableImageRepository ) => { - set_vanilla_preview_source(utils.inject_assets_source_to_vanilla(v, r)); + setIsBuilding(false); + setInitialPreviewData(utils.inject_assets_source_to_vanilla(v, r)); + }; + + // ------------------------ + // ------ for esbuild ----- + + useEffect(() => { + // reset preview mode when switching framework + setPreviewMode("responsive"); + }, [useroption?.framework]); + + const onCodeChangeHandler = debounce((code) => { + handle_esbuild_preview_source(code); + }, 500); + + const handle_esbuild_preview_source = (v: string) => { + if (!initialPreviewData) { + // the vanilla preview must be loaded first + return; + } + const transform = (s, n) => { + return `import React from 'react'; import ReactDOM from 'react-dom'; +${s} +const App = () => <><${n}/> +ReactDOM.render(, document.querySelector('#root'));`; + }; + + if (useroption.framework == "react") { + setIsBuilding(true); + bundler(transform(v, name), "tsx") + .then((d) => { + if (d.err == null) { + if (d.code && d.code !== esbuildPreviewData) { + setEsbuildPreviewData(d.code); + setPreviewMode("esbuild"); + } + } + }) + .finally(() => { + setIsBuilding(false); + }); + } }; + // ------------------------ + // region scrolling handling ------------------- const code_scrolling_area_ref = useRef(null); const set_hide_navigation_state = useSetRecoilState(hide_navigation); const hide = useScrollTriggeredAnimation(code_scrolling_area_ref); useEffect(() => { set_hide_navigation_state(hide); }, [hide]); + // --------------------------------------------- const [previewHeight, setPreviewHeight] = useState( default_responsive_preview_height_for_code_screen @@ -95,10 +147,13 @@ export function CodeScreen() { maxHeight="75vh" > { + onGeneration={({ name, app, src, vanilla_preview_source }) => { + setName(name); setApp(app); setSource(src); handle_vanilla_preview_source(vanilla_preview_source); }} + onCodeChange={onCodeChangeHandler} onAssetsLoad={(r) => { - handle_vanilla_preview_source(vanilla_preview_source, r); + handle_vanilla_preview_source(initialPreviewData, r); }} onUserOptionsChange={setUseroption} /> diff --git a/packages/app-design-to-code/code-view-with-control.tsx b/packages/app-design-to-code/code-view-with-control.tsx index d2edd368..ba8a2a10 100644 --- a/packages/app-design-to-code/code-view-with-control.tsx +++ b/packages/app-design-to-code/code-view-with-control.tsx @@ -23,6 +23,7 @@ export function CodeViewWithControl({ targetid, editor = "monaco", onUserOptionsChange, + onCodeChange, disabled, onGeneration, onAssetsLoad, @@ -32,12 +33,14 @@ export function CodeViewWithControl({ }: { targetid: string; editor?: "monaco" | "prism"; + onCodeChange?: (code: string) => void; onUserOptionsChange?: (options: DesigntoCodeUserOptions) => void; - onGeneration?: ( - app: string, - src: string, - vanilla_preview_source?: string - ) => void; + onGeneration?: (d: { + name: string; + app: string; + src: string; + vanilla_preview_source?: string; + }) => void; onAssetsLoad?: (r: repo_assets.TransportableImageRepository) => void; customMessages?: string[]; automaticRemoteFormatting?: boolean; @@ -103,17 +106,19 @@ export function CodeViewWithControl({ setUseroption(op); }; - const __onGeneration__cb = (app, src, vanilla_preview_source) => { + const __onGeneration__cb = (name, app, src, vanilla_preview_source) => { cacheStore.setCache(src); const _source = typeof src == "string" ? src : src?.raw; - onGeneration?.(app, _source, vanilla_preview_source); + onGeneration?.({ name, app, src: _source, vanilla_preview_source }); }; const handleSourceInput = ({ + name, app, code, vanilla_preview_source, }: { + name: string; app: string; code: SourceInput; vanilla_preview_source?: string; @@ -122,7 +127,7 @@ export function CodeViewWithControl({ app, useroption.language, (s) => { - __onGeneration__cb(s, source, vanilla_preview_source); + __onGeneration__cb(name, s, source, vanilla_preview_source); }, { disable_remote_format: !automaticRemoteFormatting, @@ -136,7 +141,7 @@ export function CodeViewWithControl({ useroption.language, (s) => { setSource(s); - __onGeneration__cb(app, s, vanilla_preview_source); + __onGeneration__cb(name, app, s, vanilla_preview_source); }, { disable_remote_format: !automaticRemoteFormatting, @@ -150,6 +155,7 @@ export function CodeViewWithControl({ switch (msg.type) { case EK_GENERATED_CODE_PLAIN: handleSourceInput({ + name: msg.data.name, app: msg.data.app, code: msg.data.code, vanilla_preview_source: msg.data.vanilla_preview_source, @@ -188,6 +194,7 @@ export function CodeViewWithControl({ diff --git a/packages/design-to-code b/packages/design-to-code index e2022001..e9164fdd 160000 --- a/packages/design-to-code +++ b/packages/design-to-code @@ -1 +1 @@ -Subproject commit e2022001d26891cfc46c951e1435b255d6234929 +Subproject commit e9164fdd5e30b991f8c05fca681bc50237ae3661 diff --git a/packages/ui-code-box/codebox.tsx b/packages/ui-code-box/codebox.tsx index 76683891..805b8de5 100644 --- a/packages/ui-code-box/codebox.tsx +++ b/packages/ui-code-box/codebox.tsx @@ -11,6 +11,7 @@ export function CodeBox({ code, codeActions, disabled, + onChange, }: { language: "dart" | "jsx" | "tsx" | "ts" | "js" | string; editor?: "monaco" | "prism"; @@ -21,12 +22,19 @@ export function CodeBox({ code: SourceInput; codeActions?: Array; disabled?: true; +} & { + onChange?: (code: string) => void; }) { const raw = (code && (typeof code == "string" ? code : code.raw)) || ""; const Editor = editor == "monaco" ? ( - + ) : ( ); diff --git a/packages/ui-code-box/editors/monaco-editor.tsx b/packages/ui-code-box/editors/monaco-editor.tsx index 55a477a8..297c4e02 100644 --- a/packages/ui-code-box/editors/monaco-editor.tsx +++ b/packages/ui-code-box/editors/monaco-editor.tsx @@ -1,12 +1,17 @@ -import React, { useEffect } from "react"; -import Editor, { useMonaco } from "@monaco-editor/react"; +import React, { useRef } from "react"; +import Editor, { OnMount } from "@monaco-editor/react"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import { editor } from "monaco-editor"; +import { register } from "./monaco-utils"; + +type ICodeEditor = monaco.editor.IStandaloneCodeEditor; // TODO: add auto sizing - https://github.com/microsoft/monaco-editor/issues/794#issuecomment-688959283 export function MonacoEditor({ src, language, minHeight = 800, + onChange, }: { /** * minheight is also a initial height. @@ -14,32 +19,47 @@ export function MonacoEditor({ minHeight?: number; src: string; language: string; + onChange?: (code: string) => void; }) { - const monaco = useMonaco(); const [height, setHeight] = React.useState(minHeight); - useEffect(() => { - if (monaco) { - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.Latest, - allowNonTsExtensions: true, - moduleResolution: - monaco.languages.typescript.ModuleResolutionKind.NodeJs, - module: monaco.languages.typescript.ModuleKind.CommonJS, - noEmit: true, - esModuleInterop: true, - jsx: monaco.languages.typescript.JsxEmit.React, - reactNamespace: "React", - allowJs: true, - typeRoots: ["node_modules/@types"], - }); + const instance = useRef<{ editor: ICodeEditor; format: any } | null>(null); + const activeModel = useRef(); + + const onMount: OnMount = (editor, monaco) => { + // REQUIRED + editor.onDidContentSizeChange(() => { + updateHeight(editor); + }); + + const format = editor.getAction("editor.action.formatDocument"); + const rename = editor.getAction("editor.action.rename"); + + instance.current = { editor, format }; + + activeModel.current = editor.getModel(); + + register.initEditor(editor, monaco); - monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: false, - noSyntaxValidation: false, - }); - } - }, [src]); + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () { + format.run(); + }); + + // disabled. todo: find a way to format on new line, but also with adding new line. + // editor.addCommand(monaco.KeyCode.Enter, function () { + // format.run(); + // }); + + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, function () { + // don't reload the entire page, and.. + // Default is F2 + rename.run(); + }); + + editor.onDidChangeModelContent((e) => { + /* add here */ + }); + }; const updateHeight = (editor: editor.IStandaloneCodeEditor) => { const contentHeight = Math.max(minHeight, editor.getContentHeight()); @@ -52,11 +72,9 @@ export function MonacoEditor({ loading={<>} // TODO: add loading state. theme="vs-dark" height={height} - onMount={(editor) => { - editor.onDidContentSizeChange(() => { - updateHeight(editor); - }); - }} + beforeMount={register.initMonaco} + onMount={onMount} + onChange={onChange} defaultLanguage={monacolanguage(language)} defaultValue={src} value={src} @@ -69,7 +87,7 @@ export function MonacoEditor({ // disable minimap a.k.a preview enabled: false, }, - renderIndentGuides: true, // need color customization + // renderIndentGuides: true, // need color customization scrollbar: { // allow parent scoll alwaysConsumeMouseWheel: false, diff --git a/packages/ui-code-box/editors/monaco-utils/README.md b/packages/ui-code-box/editors/monaco-utils/README.md new file mode 100644 index 00000000..69ed0fdb --- /dev/null +++ b/packages/ui-code-box/editors/monaco-utils/README.md @@ -0,0 +1 @@ +This directory is a clone of design-to-code's `monaco-utils` diff --git a/packages/ui-code-box/editors/monaco-utils/index.ts b/packages/ui-code-box/editors/monaco-utils/index.ts new file mode 100644 index 00000000..a123a773 --- /dev/null +++ b/packages/ui-code-box/editors/monaco-utils/index.ts @@ -0,0 +1 @@ +export * as register from "./register"; diff --git a/packages/ui-code-box/editors/monaco-utils/register.ts b/packages/ui-code-box/editors/monaco-utils/register.ts new file mode 100644 index 00000000..665a2d41 --- /dev/null +++ b/packages/ui-code-box/editors/monaco-utils/register.ts @@ -0,0 +1,54 @@ +import * as monaco from "monaco-editor"; +import { Monaco, OnMount } from "@monaco-editor/react"; +import { registerDocumentPrettier } from "@code-editor/prettier-services"; +// import { registerJsxHighlighter } from "@code-editor/jsx-syntax-highlight-services"; + +type CompilerOptions = monaco.languages.typescript.CompilerOptions; + +export const initEditor: OnMount = (editor, monaco) => { + // registerJsxHighlighter(editor, monaco); + registerDocumentPrettier(editor, monaco); +}; + +export const initMonaco = (monaco: Monaco) => { + baseConfigure(monaco); +}; + +const baseConfigure = (monaco: Monaco) => { + monaco.languages.typescript.typescriptDefaults.setMaximumWorkerIdleTime(-1); + monaco.languages.typescript.javascriptDefaults.setMaximumWorkerIdleTime(-1); + + monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); + monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true); + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: true, + noSyntaxValidation: false, + }); + + // compiler options + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.Latest, + allowNonTsExtensions: true, + }); + + /** + * Configure the typescript compiler to detect JSX and load type definitions + */ + + const opts: CompilerOptions = { + allowJs: true, + allowSyntheticDefaultImports: true, + alwaysStrict: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + noEmit: true, + reactNamespace: "React", + typeRoots: ["node_modules/@types"], + jsx: monaco.languages.typescript.JsxEmit.React, + allowNonTsExtensions: true, + target: monaco.languages.typescript.ScriptTarget.ES2016, + jsxFactory: "React.createElement", + }; + + monaco.languages.typescript.typescriptDefaults.setCompilerOptions(opts); + monaco.languages.typescript.javascriptDefaults.setCompilerOptions(opts); +}; diff --git a/packages/ui-code-box/package.json b/packages/ui-code-box/package.json index 9d592495..7d39760d 100644 --- a/packages/ui-code-box/package.json +++ b/packages/ui-code-box/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": false, "dependencies": { - "@monaco-editor/react": "^4.2.2", - "monaco-editor": "^0.27.0" + "@monaco-editor/react": "^4.4.1", + "monaco-editor": "^0.33.0" } -} +} \ No newline at end of file diff --git a/packages/ui-previewer/components/esbuild-result-preview.tsx b/packages/ui-previewer/components/esbuild-result-preview.tsx new file mode 100644 index 00000000..4bdbf6ad --- /dev/null +++ b/packages/ui-previewer/components/esbuild-result-preview.tsx @@ -0,0 +1,128 @@ +import React, { useCallback, useEffect, useRef } from "react"; + +export function VanillaESBuildAppRunner({ + doc, + style, +}: { + doc?: { + html: string; + css?: string; + javascript: string; + }; + style?: React.CSSProperties; +}) { + const ref = useRef(); + + const loadCode = useCallback( + (e: HTMLIFrameElement) => { + e?.contentWindow?.postMessage({ html: doc?.html }, "*"); + e?.contentWindow?.postMessage({ css: doc?.css }, "*"); + e?.contentWindow?.postMessage({ javascript: doc?.javascript }, "*"); + }, + [doc?.html, doc?.css, doc?.javascript] + ); + + useEffect(() => { + if (ref.current) { + loadCode(ref.current); + } + }, [doc?.html, doc?.css, doc?.javascript]); + + return ( +