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