UNPKG

@builder.io/mitosis

Version:

Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io

400 lines (394 loc) 18.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.componentToReact = exports.componentToPreact = exports.contextPropDrillingKey = void 0; const default_props_1 = require("../../generators/react/helpers/default-props"); const hooks_1 = require("../../generators/react/helpers/hooks"); const bindings_1 = require("../../helpers/bindings"); const create_mitosis_node_1 = require("../../helpers/create-mitosis-node"); const dedent_1 = require("../../helpers/dedent"); const fast_clone_1 = require("../../helpers/fast-clone"); const get_props_ref_1 = require("../../helpers/get-props-ref"); const get_refs_1 = require("../../helpers/get-refs"); const get_state_object_string_1 = require("../../helpers/get-state-object-string"); const getters_to_functions_1 = require("../../helpers/getters-to-functions"); const handle_missing_state_1 = require("../../helpers/handle-missing-state"); const is_root_text_node_1 = require("../../helpers/is-root-text-node"); const map_refs_1 = require("../../helpers/map-refs"); const merge_options_1 = require("../../helpers/merge-options"); const nullable_1 = require("../../helpers/nullable"); const on_event_1 = require("../../helpers/on-event"); const process_code_1 = require("../../helpers/plugins/process-code"); const process_http_requests_1 = require("../../helpers/process-http-requests"); const render_imports_1 = require("../../helpers/render-imports"); const replace_identifiers_1 = require("../../helpers/replace-identifiers"); const replace_new_lines_in_strings_1 = require("../../helpers/replace-new-lines-in-strings"); const state_1 = require("../../helpers/state"); const strip_meta_properties_1 = require("../../helpers/strip-meta-properties"); const collect_css_1 = require("../../helpers/styles/collect-css"); const collect_styled_components_1 = require("../../helpers/styles/collect-styled-components"); const helpers_1 = require("../../helpers/styles/helpers"); const plugins_1 = require("../../modules/plugins"); const core_1 = require("@babel/core"); const hash_sum_1 = __importDefault(require("hash-sum")); const json5_1 = __importDefault(require("json5")); const standalone_1 = require("prettier/standalone"); const context_1 = require("../helpers/context"); const rsc_1 = require("../helpers/rsc"); const react_native_1 = require("../react-native"); const blocks_1 = require("./blocks"); const helpers_2 = require("./helpers"); const state_2 = require("./helpers/state"); exports.contextPropDrillingKey = '_context'; /** * If the root Mitosis component only has 1 child, and it is a `Show`/`For` node, then we need to wrap it in a fragment. * Otherwise, we end up with invalid React render code. */ const isRootSpecialNode = (json) => json.children.length === 1 && ['Show', 'For'].includes(json.children[0].name); const getRefsString = (json, refs, options) => { var _a, _b; let hasStateArgument = false; let code = ''; const domRefs = (0, get_refs_1.getRefs)(json); for (const ref of refs) { const typeParameter = ((_a = json['refs'][ref]) === null || _a === void 0 ? void 0 : _a.typeParameter) || ''; // domRefs must have null argument const argument = ((_b = json['refs'][ref]) === null || _b === void 0 ? void 0 : _b.argument) || (domRefs.has(ref) ? 'null' : ''); hasStateArgument = /state\./.test(argument); code += `\nconst ${ref} = useRef${typeParameter && options.typescript ? `<${typeParameter}>` : ''}(${(0, state_2.processHookCode)({ str: argument, options, })});`; } return [hasStateArgument, code]; }; function provideContext(json, options) { if (options.contextType === 'prop-drill') { let str = ''; for (const key in json.context.set) { const { name, ref, value } = json.context.set[key]; if (value) { str += ` ${exports.contextPropDrillingKey}.${name} = ${(0, get_state_object_string_1.stringifyContextValue)(value)}; `; } // TODO: support refs. I'm not sure what those are so unclear how to support them } return str; } else { for (const key in json.context.set) { const { name, ref, value } = json.context.set[key]; if (value) { json.children = [ (0, create_mitosis_node_1.createMitosisNode)({ name: `${name}.Provider`, children: json.children, ...(value && { bindings: { value: (0, bindings_1.createSingleBinding)({ code: (0, get_state_object_string_1.stringifyContextValue)(value), }), }, }), }), ]; } else if (ref) { json.children = [ (0, create_mitosis_node_1.createMitosisNode)({ name: `${name}.Provider`, children: json.children, ...(ref && { bindings: { value: (0, bindings_1.createSingleBinding)({ code: ref }), }, }), }), ]; } } } } function getContextString(component, options) { let str = ''; for (const key in component.context.get) { if (options.contextType === 'prop-drill') { str += ` const ${key} = ${exports.contextPropDrillingKey}['${component.context.get[key].name}']; `; } else { str += ` const ${key} = useContext(${component.context.get[key].name}); `; } } return str; } const componentToPreact = (reactOptions = {}) => (0, exports.componentToReact)({ ...reactOptions, preact: true, }); exports.componentToPreact = componentToPreact; const componentToReact = (reactOptions = {}) => ({ component, path }) => { let json = (0, fast_clone_1.fastClone)(component); const target = reactOptions.preact ? 'preact' : reactOptions.type === 'native' ? 'reactNative' : reactOptions.type === 'taro' ? 'taro' : reactOptions.rsc ? 'rsc' : 'react'; const stateType = reactOptions.stateType || 'useState'; const DEFAULT_OPTIONS = { addUseClientDirectiveIfNeeded: true, stateType, stylesType: 'styled-jsx', styleTagsPlacement: 'bottom', type: 'dom', plugins: [ (0, on_event_1.processOnEventHooksPlugin)({ setBindings: false }), ...(stateType === 'variables' ? [ (0, process_code_1.CODE_PROCESSOR_PLUGIN)((codeType, json) => (code, hookType) => { if (codeType === 'types') return code; code = (0, replace_identifiers_1.replaceNodes)({ code, nodeMaps: Object.entries(json.state) .filter(([key, value]) => (value === null || value === void 0 ? void 0 : value.type) === 'getter') .map(([key, value]) => { const expr = core_1.types.memberExpression(core_1.types.identifier('state'), core_1.types.identifier(key)); return { from: expr, // condition: (path) => !types.isObjectMethod(path.parent), to: core_1.types.callExpression(expr, []), }; }), }); code = (0, replace_identifiers_1.replaceStateIdentifier)(null)(code); return code; }), ] : []), ], }; const options = (0, merge_options_1.initializeOptions)({ target, component, defaults: DEFAULT_OPTIONS, userOptions: reactOptions, }); if (options.plugins) { json = (0, plugins_1.runPreJsonPlugins)({ json, plugins: options.plugins }); } let str = _componentToReact(json, options); str += '\n\n\n' + json.subComponents.map((item) => _componentToReact(item, options, true)).join('\n\n\n'); if (options.plugins) { str = (0, plugins_1.runPreCodePlugins)({ json, code: str, plugins: options.plugins }); } if (options.prettier !== false) { try { str = (0, standalone_1.format)(str, { parser: 'typescript', plugins: [ require('prettier/parser-typescript'), // To support running in browsers require('prettier/parser-postcss'), ], }) // Remove spaces between imports .replace(/;\n\nimport\s/g, ';\nimport '); } catch (err) { if (process.env.NODE_ENV !== 'test') { console.error('Format error for file:', str); } throw err; } } if (options.plugins) { str = (0, plugins_1.runPostCodePlugins)({ json, code: str, plugins: options.plugins }); } return str; }; exports.componentToReact = componentToReact; const isRSC = (json, options) => { var _a, _b; // When using RSC generator, we check `componentType` field in metadata to determine if it's a server component const componentType = (_b = (_a = json.meta.useMetadata) === null || _a === void 0 ? void 0 : _a.rsc) === null || _b === void 0 ? void 0 : _b.componentType; if (options.rsc && (0, nullable_1.checkIsDefined)(componentType)) { return componentType === 'server'; } return !(0, rsc_1.checkIfIsClientComponent)(json); }; const checkShouldAddUseClientDirective = (json, options) => { if (!options.addUseClientDirectiveIfNeeded) return false; if (options.type === 'native') return false; if (options.preact) return false; return !isRSC(json, options); }; const generateStyleTags = (placement, json, options, componentHasStyles, css, shouldInjectCustomStyles) => { if (placement !== options.styleTagsPlacement) return ''; return (0, dedent_1.dedent) ` ${componentHasStyles && options.stylesType === 'styled-jsx' ? `<style jsx>{\`${css}\`}</style>` : ''} ${componentHasStyles && options.stylesType === 'style-tag' ? `<style>{\`${css}\`}</style>` : ''} ${shouldInjectCustomStyles ? `<style>{\`${json.style}\`}</style>` : ''} `; }; const _componentToReact = (json, options, isSubComponent = false) => { var _a, _b, _c, _d, _e, _f; (0, process_http_requests_1.processHttpRequests)(json); (0, handle_missing_state_1.handleMissingState)(json); (0, helpers_2.processTagReferences)(json, options); const contextStr = provideContext(json, options); const componentHasStyles = (0, helpers_1.hasCss)(json); if (options.stateType === 'useState') { (0, getters_to_functions_1.gettersToFunctions)(json); (0, state_2.updateStateSetters)(json, options); } if (!json.name) { json.name = 'MyComponent'; } // const domRefs = getRefs(json); const allRefs = Object.keys(json.refs); (0, map_refs_1.mapRefs)(json, (refName) => `${refName}.current`); // Always use state if we are generate Builder react code const hasState = options.stateType === 'builder' || (0, state_1.checkHasState)(json); const [forwardRef, hasPropRef] = (0, get_props_ref_1.getPropsRef)(json); const isForwardRef = !options.preact && Boolean((0, helpers_2.isReactForwardRef)(json) || hasPropRef); if (isForwardRef) { const meta = (0, helpers_2.isReactForwardRef)(json); options.forwardRef = meta || forwardRef; } const forwardRefType = options.typescript && json.propsTypeRef && forwardRef && json.propsTypeRef !== 'any' ? `<${json.propsTypeRef}["${forwardRef}"]>` : ''; const useStateCode = options.stateType === 'useState' ? (0, state_2.getUseStateCode)(json, options) : ''; if (options.plugins) { json = (0, plugins_1.runPostJsonPlugins)({ json, plugins: options.plugins }); } const css = options.stylesType === 'styled-jsx' ? (0, collect_css_1.collectCss)(json) : options.stylesType === 'style-tag' ? (0, collect_css_1.collectCss)(json, { prefix: (0, hash_sum_1.default)(json), }) : null; const styledComponentsCode = (options.stylesType === 'styled-components' && componentHasStyles && (0, collect_styled_components_1.collectStyledComponents)(json)) || ''; if (options.format !== 'lite') { (0, strip_meta_properties_1.stripMetaProperties)(json); } const reactLibImports = new Set(); if (useStateCode.includes('useState')) { reactLibImports.add('useState'); } if ((0, context_1.hasContext)(json) && options.contextType !== 'prop-drill') { reactLibImports.add('useContext'); } const shouldAddUseClientDirective = checkShouldAddUseClientDirective(json, options); const shouldInlineOnInitHook = !shouldAddUseClientDirective && options.rsc && isRSC(json, options); if (allRefs.length || (((_a = json.hooks.onInit) === null || _a === void 0 ? void 0 : _a.code) && !shouldInlineOnInitHook)) { reactLibImports.add('useRef'); } if (!options.preact && hasPropRef) { reactLibImports.add('forwardRef'); } if (json.hooks.onMount.length || ((_b = json.hooks.onUnMount) === null || _b === void 0 ? void 0 : _b.code) || ((_c = json.hooks.onUpdate) === null || _c === void 0 ? void 0 : _c.length)) { reactLibImports.add('useEffect'); } const hasCustomStyles = !!((_d = json.style) === null || _d === void 0 ? void 0 : _d.length); const shouldInjectCustomStyles = hasCustomStyles && (options.stylesType === 'styled-components' || options.stylesType === 'emotion'); const wrap = (0, helpers_2.wrapInFragment)(json) || (0, is_root_text_node_1.isRootTextNode)(json) || (componentHasStyles && (options.stylesType === 'styled-jsx' || options.stylesType === 'style-tag')) || shouldInjectCustomStyles || isRootSpecialNode(json); const [hasStateArgument, refsString] = getRefsString(json, allRefs, options); // NOTE: `collectReactNativeStyles` must run before style generation in the component generation body, as it has // side effects that delete styles bindings from the JSON. const reactNativeStyles = options.stylesType === 'react-native' && componentHasStyles ? (0, react_native_1.collectReactNativeStyles)(json, options) : undefined; const propType = json.propsTypeRef || 'any'; const componentArgs = [`props${options.typescript ? `:${propType}` : ''}`, options.forwardRef] .filter(Boolean) .join(','); const componentBody = (0, dedent_1.dedent) ` ${options.contextType === 'prop-drill' ? `const ${exports.contextPropDrillingKey} = { ...props['${exports.contextPropDrillingKey}'] };` : ''} ${hasStateArgument ? '' : refsString} ${(0, state_2.getReactVariantStateString)({ hasState, useStateCode, json, options })} ${hasStateArgument ? refsString : ''} ${getContextString(json, options)} ${((_e = json.hooks.init) === null || _e === void 0 ? void 0 : _e.code) ? (0, state_2.processHookCode)({ str: (_f = json.hooks.init) === null || _f === void 0 ? void 0 : _f.code, options }) : ''} ${contextStr || ''} ${(0, hooks_1.getOnInitHookComponentBody)({ shouldInlineOnInitHook, json, options })} ${(0, hooks_1.getOnEventHookComponentBody)(json)} ${(0, hooks_1.getOnMountComponentBody)({ json, options })} ${(0, hooks_1.getOnUpdateComponentBody)({ json, options })} ${(0, hooks_1.getOnUnMountComponentBody)({ json, options })} return ( ${wrap ? (0, helpers_2.openFrag)(options) : ''} ${generateStyleTags('top', json, options, componentHasStyles, css, shouldInjectCustomStyles)} ${json.children.map((item) => (0, blocks_1.blockToReact)(item, options, json, wrap, [])).join('\n')} ${generateStyleTags('bottom', json, options, componentHasStyles, css, shouldInjectCustomStyles)} ${wrap ? (0, helpers_2.closeFrag)(options) : ''} ); `; const str = (0, dedent_1.dedent) ` ${shouldAddUseClientDirective ? `'use client';` : ''} ${(0, state_2.getDefaultImport)(options)} ${styledComponentsCode ? `import styled from 'styled-components';\n` : ''} ${reactLibImports.size ? `import { ${Array.from(reactLibImports).join(', ')} } from '${options.preact ? 'preact/hooks' : 'react'}'` : ''} ${options.stylesType === 'twrnc' ? `import tw from 'twrnc';\n` : ''} ${componentHasStyles && options.stylesType === 'emotion' && options.format !== 'lite' ? `/** @jsx jsx */ import { jsx } from '@emotion/react'`.trim() : ''} ${(0, state_2.getReactVariantStateImportString)(hasState, options)} ${json.types && options.typescript ? json.types.join('\n') : ''} ${(0, render_imports_1.renderPreComponent)({ explicitImportFileExtension: options.explicitImportFileExtension, component: json, target: options.type === 'native' ? 'reactNative' : 'react', })} ${isForwardRef ? `const ${json.name} = forwardRef${forwardRefType}(` : ''}function ${json.name}(${componentArgs}) { ${(0, default_props_1.getDefaultProps)(json)} ${componentBody} }${isForwardRef ? ')' : ''} ${reactNativeStyles ? `const styles = StyleSheet.create(${json5_1.default.stringify(reactNativeStyles)});` : ''} ${styledComponentsCode !== null && styledComponentsCode !== void 0 ? styledComponentsCode : ''} ${isSubComponent ? '' : options.stateType === 'mobx' ? ` const observed${json.name} = observer(${json.name}); export default observed${json.name}; ` : `export default ${json.name};`} `; return (0, replace_new_lines_in_strings_1.stripNewlinesInStrings)(str); };