UNPKG

@builder.io/mitosis

Version:

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

662 lines (657 loc) 27.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.componentToBuilder = exports.blockToBuilder = void 0; const media_sizes_1 = require("../../constants/media-sizes"); const dedent_1 = require("../../helpers/dedent"); const fast_clone_1 = require("../../helpers/fast-clone"); const filter_empty_text_nodes_1 = require("../../helpers/filter-empty-text-nodes"); const get_state_object_string_1 = require("../../helpers/get-state-object-string"); const has_props_1 = require("../../helpers/has-props"); const is_component_1 = require("../../helpers/is-component"); const is_mitosis_node_1 = require("../../helpers/is-mitosis-node"); const is_upper_case_1 = require("../../helpers/is-upper-case"); const parsers_1 = require("../../helpers/parsers"); const remove_surrounding_block_1 = require("../../helpers/remove-surrounding-block"); const replace_identifiers_1 = require("../../helpers/replace-identifiers"); const state_1 = require("../../helpers/state"); const builder_1 = require("../../parsers/builder"); const symbol_processor_1 = require("../../symbols/symbol-processor"); const core_1 = require("@babel/core"); const generator_1 = __importDefault(require("@babel/generator")); const parser_1 = require("@babel/parser"); const json5_1 = __importDefault(require("json5")); const lodash_1 = require("lodash"); const legacy_1 = __importDefault(require("neotraverse/legacy")); const standalone_1 = require("prettier/standalone"); const on_mount_1 = require("../helpers/on-mount"); const omitMetaProperties = (obj) => (0, lodash_1.omitBy)(obj, (_value, key) => key.startsWith('$')); const builderBlockPrefixes = ['Amp', 'Core', 'Builder', 'Raw', 'Form']; const mapComponentName = (name) => { if (name === 'CustomCode') { return 'Custom Code'; } for (const prefix of builderBlockPrefixes) { if (name.startsWith(prefix)) { const suffix = name.replace(prefix, ''); const restOfName = suffix[0]; if (restOfName && (0, is_upper_case_1.isUpperCase)(restOfName)) { return `${prefix}:${name.replace(prefix, '')}`; } } } return name; }; const componentMappers = { // TODO: add back if this direction (blocks as children not prop) is desired ...(!builder_1.symbolBlocksAsChildren ? {} : { Symbol(node, options) { const child = node.children[0]; const symbolOptions = (node.bindings.symbol && json5_1.default.parse(node.bindings.symbol.code)) || {}; if (child) { (0, lodash_1.set)(symbolOptions, 'content.data.blocks', child.children.map((item) => (0, exports.blockToBuilder)(item, options))); } return el({ component: { name: 'Symbol', options: { // TODO: forward other symbol options symbol: symbolOptions, }, }, }, options); }, }), Columns(node, options) { const block = (0, exports.blockToBuilder)(node, options, { skipMapper: true }); const columns = block.children.map((item) => { var _a, _b; return ({ blocks: item.children, width: (_b = (_a = item.component) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.width, }); }); block.component.options.columns = columns; block.children = []; return block; }, Fragment(node, options) { const block = (0, exports.blockToBuilder)(node, options, { skipMapper: true }); block.component = { name: 'Core:Fragment' }; block.tagName = undefined; return block; }, PersonalizationContainer(node, options) { const block = (0, exports.blockToBuilder)(node, options, { skipMapper: true }); const variants = []; let defaultVariant = []; const validFakeNodeNames = [ 'Variant', 'PersonalizationOption', 'PersonalizationVariant', 'Personalization', ]; block.children.forEach((item) => { var _a; if (item.component && validFakeNodeNames.includes((_a = item.component) === null || _a === void 0 ? void 0 : _a.name)) { let query; if (item.component.options.query) { const optionsQuery = item.component.options.query; if (Array.isArray(optionsQuery)) { query = optionsQuery.map((q) => ({ '@type': '@builder.io/core:Query', ...q, })); } else { query = [ { '@type': '@builder.io/core:Query', ...optionsQuery, }, ]; } const newVariant = { ...item.component.options, query, blocks: item.children, }; variants.push(newVariant); } else if (item.children) { defaultVariant.push(...item.children); } } else { defaultVariant.push(item); } }); delete block.properties; delete block.bindings; block.component.options.variants = variants; block.children = defaultVariant; return block; }, For(_node, options) { var _a; const node = _node; const replaceIndexNode = (str) => (0, replace_identifiers_1.replaceNodes)({ code: str, nodeMaps: [ { from: core_1.types.identifier(target), to: core_1.types.memberExpression(core_1.types.identifier('state'), core_1.types.identifier('$index')), }, ], }); // rename `index` var to `state.$index` const target = node.scope.indexName || 'index'; const replaceIndex = (node) => { (0, legacy_1.default)(node).forEach(function (thing) { if (!(0, is_mitosis_node_1.isMitosisNode)(thing)) return; for (const [key, value] of Object.entries(thing.bindings)) { if (!value) continue; if (!value.code.includes(target)) continue; if (value.type === 'single' && value.bindingType === 'function') { try { const code = value.code; const programNode = (0, parsers_1.parseCodeToAst)(code); if (!programNode) continue; (0, core_1.traverse)(programNode, { Program(path) { if (path.scope.hasBinding(target)) return; const x = { id: core_1.types.identifier(target), init: core_1.types.identifier('PLACEHOLDER'), }; path.scope.push(x); path.scope.rename(target, 'state.$index'); path.traverse({ VariableDeclaration(p) { if (p.node.declarations.length === 1 && p.node.declarations[0].id === x.id) { p.remove(); } }, }); }, }); thing.bindings[key].code = (0, generator_1.default)(programNode).code; } catch (error) { console.error('Error processing function binding. Falling back to simple replacement.', error); thing.bindings[key].code = replaceIndexNode(value.code); } } else { thing.bindings[key].code = replaceIndexNode(value.code); } } }); return node; }; return el({ component: { name: 'Core:Fragment', }, repeat: { collection: (_a = node.bindings.each) === null || _a === void 0 ? void 0 : _a.code, itemName: node.scope.forName, }, children: node.children .filter(filter_empty_text_nodes_1.filterEmptyTextNodes) .map((node) => (0, exports.blockToBuilder)(replaceIndex(node), options)), }, options); }, Show(node, options) { var _a, _b, _c; const elseCase = node.meta.else; const children = node.children.filter(filter_empty_text_nodes_1.filterEmptyTextNodes); const showNode = children.length > 0 ? el({ // TODO: the reverse mapping for this component: { name: 'Core:Fragment', }, bindings: { show: (_a = node.bindings.when) === null || _a === void 0 ? void 0 : _a.code, }, children: children.map((node) => (0, exports.blockToBuilder)(node, options)), }, options) : undefined; const elseNode = elseCase && (0, filter_empty_text_nodes_1.filterEmptyTextNodes)(elseCase) ? el({ // TODO: the reverse mapping for this component: { name: 'Core:Fragment', }, bindings: { hide: (_b = node.bindings.when) === null || _b === void 0 ? void 0 : _b.code, }, children: [(0, exports.blockToBuilder)(elseCase, options)], }, options) : undefined; if (elseNode && showNode) { return el({ component: { name: 'Core:Fragment', }, children: [showNode, elseNode], }, options); } else if (showNode) { return showNode; } else if (elseNode) { return elseNode; } return el({ // TODO: the reverse mapping for this component: { name: 'Core:Fragment', }, bindings: { show: (_c = node.bindings.when) === null || _c === void 0 ? void 0 : _c.code, }, children: [], }, options); }, }; const el = (options, toBuilderOptions) => ({ '@type': '@builder.io/sdk:Element', ...(toBuilderOptions.includeIds && { id: 'builder-' + (0, symbol_processor_1.hashCodeAsString)(options), }), ...options, }); function tryFormat(code) { let str = code; try { str = (0, standalone_1.format)(str, { parser: 'babel', plugins: [ require('prettier/parser-babel'), // To support running in browsers ], }); } catch (err) { console.error('Format error for code:', str); throw err; } return str; } const processLocalizedValues = (element, node) => { if (node.localizedValues) { for (const [path, value] of Object.entries(node.localizedValues)) { (0, lodash_1.set)(element, path, value); } } return element; }; /** * Turns a stringified object into an object that can be looped over. * Since values in the stringified object could be JS expressions, all * values in the resulting object will remain strings. * @param input - The stringified object */ const parseJSObject = (input) => { var _a; const unparsed = []; let parsed = {}; try { const ast = (0, parser_1.parseExpression)(`(${input})`, { plugins: ['jsx', 'typescript'], sourceType: 'module', }); if (ast.type !== 'ObjectExpression') { return { parsed, unparsed: input }; } for (const prop of ast.properties) { /** * If the object includes spread or method, we stop. We can't really break the component into Key/Value * and the whole expression is considered dynamic. We return `false` to signify that. */ if (prop.type === 'ObjectMethod' || prop.type === 'SpreadElement') { if (!!prop.start && !!prop.end) { if (typeof input === 'string') { unparsed.push(input.slice(prop.start - 1, prop.end - 1)); } } continue; } /** * Ignore shorthand objects when processing incomplete objects. Otherwise we may * create identifiers unintentionally. * Example: When accounting for shorthand objects, "{ color" would become * { color: color } thus creating a "color" identifier that does not exist. */ if (prop.type === 'ObjectProperty') { if ((_a = prop.extra) === null || _a === void 0 ? void 0 : _a.shorthand) { if (typeof input === 'string') { unparsed.push(input.slice(prop.start - 1, prop.end - 1)); } continue; } let key = ''; if (prop.key.type === 'Identifier') { key = prop.key.name; } else if (prop.key.type === 'StringLiteral') { key = prop.key.value; } else { continue; } if (typeof input === 'string') { const [val, err] = extractValue(input, prop.value); if (err === null) { parsed[key] = val; } } } } return { parsed, unparsed: unparsed.length > 0 ? `{${unparsed.join('\n')}}` : undefined, }; } catch (err) { return { parsed, unparsed: unparsed.length > 0 ? `{${unparsed.join('\n')}}` : undefined, }; } }; const extractValue = (input, node) => { var _a, _b; const start = (_a = node === null || node === void 0 ? void 0 : node.loc) === null || _a === void 0 ? void 0 : _a.start; const end = (_b = node === null || node === void 0 ? void 0 : node.loc) === null || _b === void 0 ? void 0 : _b.end; const startIndex = start !== undefined && 'index' in start && typeof start['index'] === 'number' ? start['index'] : undefined; const endIndex = end !== undefined && 'index' in end && typeof end['index'] === 'number' ? end['index'] : undefined; if (startIndex === undefined || endIndex === undefined || node === null) { const err = `bad value: ${node}`; return [null, err]; } const value = input.slice(startIndex - 1, endIndex - 1); return [value, null]; }; /** * Maps and styles that are bound with dynamic values onto their respective * binding keys for Builder elements. This function also maps media queries * with dynamic values. * @param - bindings - The bindings object that has your styles. This param * will be modified in-place, and the old "style" key will be removed. */ const mapBoundStyles = (bindings) => { const styles = bindings['style']; if (!styles) { return; } const { parsed, unparsed } = parseJSObject(styles.code); for (const key in parsed) { const mediaQueryMatch = key.match(media_sizes_1.mediaQueryRegex); if (mediaQueryMatch) { const { parsed: mParsed } = parseJSObject(parsed[key]); const [_, pixelSize] = mediaQueryMatch; const size = media_sizes_1.sizes.getSizeForWidth(Number(pixelSize)); for (const mKey in mParsed) { bindings[`responsiveStyles.${size}.${mKey}`] = { code: mParsed[mKey], bindingType: 'expression', type: 'single', }; } } else { if (isGlobalStyle(key)) { console.warn(`The following bound styles are not supported by Builder JSON and have been removed: "${key}": ${parsed[key]} `); } else { bindings[`style.${key}`] = { code: parsed[key], bindingType: 'expression', type: 'single', }; } } } delete bindings['style']; // unparsed data could be something else such as a function call if (unparsed) { try { const ast = (0, parser_1.parseExpression)(`(${unparsed})`, { plugins: ['jsx', 'typescript'], sourceType: 'module', }); // style={state.getStyles()} if (ast.type === 'CallExpression') { bindings['style'] = { code: unparsed, bindingType: 'expression', type: 'single', }; } else { throw 'unsupported style'; } } catch (_a) { console.warn(`The following bound styles are invalid and have been removed: ${unparsed}`); } } }; function isGlobalStyle(key) { // These are mapped to their respective responsiveStyle and support bindings if (/max-width: (.*?)px/gm.exec(key)) { return false; } return ( // pseudo class key.startsWith('&:') || key.startsWith(':') || // @ rules key.startsWith('@')); } const blockToBuilder = (json, options = {}, _internalOptions = {}) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; const mapper = !_internalOptions.skipMapper && componentMappers[json.name]; if (mapper) { const element = mapper(json, options); return processLocalizedValues(element, json); } if (json.properties._text || ((_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code)) { const element = el({ tagName: 'span', bindings: { ...(((_b = json.bindings._text) === null || _b === void 0 ? void 0 : _b.code) ? { 'component.options.text': json.bindings._text.code, 'json.bindings._text.code': undefined, } : {}), }, component: { name: 'Text', options: { // Mitosis uses {} for bindings, but Builder expects {{}} so we need to convert text: (_c = json.properties._text) === null || _c === void 0 ? void 0 : _c.replace(/\{(.*?)\}/g, '{{$1}}'), }, }, }, options); return processLocalizedValues(element, json); } const thisIsComponent = (0, is_component_1.isComponent)(json); let bindings = json.bindings; const actions = {}; for (const key in bindings) { const eventBindingKeyRegex = /^on([A-Z])/; const firstCharMatchForEventBindingKey = (_d = key.match(eventBindingKeyRegex)) === null || _d === void 0 ? void 0 : _d[1]; if (firstCharMatchForEventBindingKey) { let actionBody = ((_e = bindings[key]) === null || _e === void 0 ? void 0 : _e.async) ? `(async () => ${(_f = bindings[key]) === null || _f === void 0 ? void 0 : _f.code})()` : (0, remove_surrounding_block_1.removeSurroundingBlock)((_g = bindings[key]) === null || _g === void 0 ? void 0 : _g.code); const eventIdentifier = (_j = (_h = bindings[key]) === null || _h === void 0 ? void 0 : _h.arguments) === null || _j === void 0 ? void 0 : _j[0]; if (typeof eventIdentifier === 'string' && eventIdentifier !== 'event') { actionBody = (0, replace_identifiers_1.replaceNodes)({ code: actionBody, nodeMaps: [{ from: core_1.types.identifier(eventIdentifier), to: core_1.types.identifier('event') }], }); } actions[key.replace(eventBindingKeyRegex, firstCharMatchForEventBindingKey.toLowerCase())] = actionBody; delete bindings[key]; } if (key === 'style') { mapBoundStyles(bindings); } } const builderBindings = {}; const componentOptions = omitMetaProperties(json.properties); if (thisIsComponent) { for (const key in bindings) { if (key === 'css') { continue; } const value = bindings[key]; const parsed = (0, lodash_1.attempt)(() => json5_1.default.parse(value === null || value === void 0 ? void 0 : value.code)); if (!(parsed instanceof Error)) { componentOptions[key] = parsed; } else { if (!((_k = json.slots) === null || _k === void 0 ? void 0 : _k[key])) { builderBindings[`component.options.${key}`] = bindings[key].code; } } } } for (const key in json.slots) { componentOptions[key] = json.slots[key].map((node) => (0, exports.blockToBuilder)(node, options)); } const hasCss = !!((_l = bindings.css) === null || _l === void 0 ? void 0 : _l.code); let responsiveStyles = { large: {}, }; if (hasCss) { const cssRules = json5_1.default.parse((_m = bindings.css) === null || _m === void 0 ? void 0 : _m.code); const cssRuleKeys = Object.keys(cssRules); for (const ruleKey of cssRuleKeys) { const mediaQueryMatch = ruleKey.match(media_sizes_1.mediaQueryRegex); if (mediaQueryMatch) { const [fullmatch, pixelSize] = mediaQueryMatch; const sizeForWidth = media_sizes_1.sizes.getSizeForWidth(Number(pixelSize)); const currentSizeStyles = responsiveStyles[sizeForWidth] || {}; responsiveStyles[sizeForWidth] = { ...currentSizeStyles, ...cssRules[ruleKey], }; } else { responsiveStyles.large = { ...responsiveStyles.large, [ruleKey]: cssRules[ruleKey], }; } } delete json.bindings.css; } const element = el({ tagName: thisIsComponent ? undefined : json.name, ...(hasCss && { responsiveStyles, }), layerName: json.properties.$name, ...(thisIsComponent && { component: { name: mapComponentName(json.name), options: componentOptions, }, }), code: { bindings: builderBindings, actions, }, properties: thisIsComponent ? undefined : omitMetaProperties(json.properties), bindings: thisIsComponent ? builderBindings : (0, lodash_1.omit)((0, lodash_1.mapValues)(bindings, (value) => value === null || value === void 0 ? void 0 : value.code), 'css'), actions, children: json.children .filter(filter_empty_text_nodes_1.filterEmptyTextNodes) .map((child) => (0, exports.blockToBuilder)(child, options)), }, options); return processLocalizedValues(element, json); }; exports.blockToBuilder = blockToBuilder; const componentToBuilder = (options = {}) => ({ component }) => { var _a, _b; const hasState = (0, state_1.checkHasState)(component); const result = (0, fast_clone_1.fastClone)({ data: { httpRequests: (_b = (_a = component === null || component === void 0 ? void 0 : component.meta) === null || _a === void 0 ? void 0 : _a.useMetadata) === null || _b === void 0 ? void 0 : _b.httpRequests, jsCode: tryFormat((0, dedent_1.dedent) ` ${!(0, has_props_1.hasProps)(component) ? '' : `var props = state;`} ${!hasState ? '' : `Object.assign(state, ${(0, get_state_object_string_1.getStateObjectStringFromComponent)(component)});`} ${(0, on_mount_1.stringifySingleScopeOnMount)(component)} `), tsCode: tryFormat((0, dedent_1.dedent) ` ${!(0, has_props_1.hasProps)(component) ? '' : `var props = state;`} ${!hasState ? '' : `useStore(${(0, get_state_object_string_1.getStateObjectStringFromComponent)(component)});`} ${!component.hooks.onMount.length ? '' : `onMount(() => { ${(0, on_mount_1.stringifySingleScopeOnMount)(component)} })`} `), cssCode: component === null || component === void 0 ? void 0 : component.style, blocks: component.children .filter(filter_empty_text_nodes_1.filterEmptyTextNodes) .map((child) => (0, exports.blockToBuilder)(child, options)), }, }); const subComponentMap = {}; for (const subComponent of component.subComponents) { const name = subComponent.name; subComponentMap[name] = (0, exports.componentToBuilder)(options)({ component: subComponent, }); } (0, legacy_1.default)([result, subComponentMap]).forEach(function (el) { var _a; if ((0, builder_1.isBuilderElement)(el)) { const value = subComponentMap[(_a = el.component) === null || _a === void 0 ? void 0 : _a.name]; if (value) { (0, lodash_1.set)(el, 'component.options.symbol.content', value); } if (el.bindings) { for (const [key, value] of Object.entries(el.bindings)) { if (value.match(/\n|;/)) { if (!el.code) { el.code = {}; } if (!el.code.bindings) { el.code.bindings = {}; } el.code.bindings[key] = value; el.bindings[key] = ` return ${value}`; } } } } }); return result; }; exports.componentToBuilder = componentToBuilder;