UNPKG

@builder.io/mitosis

Version:

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

335 lines (325 loc) 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.componentToSwift = void 0; const event_handlers_1 = require("../../helpers/event-handlers"); const legacy_1 = __importDefault(require("neotraverse/legacy")); 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 generic_format_1 = require("../../helpers/generic-format"); const get_state_object_string_1 = require("../../helpers/get-state-object-string"); const get_styles_1 = require("../../helpers/get-styles"); const is_children_1 = __importDefault(require("../../helpers/is-children")); const is_mitosis_node_1 = require("../../helpers/is-mitosis-node"); const state_1 = require("../../helpers/state"); const try_prettier_format_1 = require("../../helpers/try-prettier-format"); const mitosis_node_1 = require("../../types/mitosis-node"); const scrolls = (json) => { var _a; return ((_a = (0, get_styles_1.getStyles)(json)) === null || _a === void 0 ? void 0 : _a.overflow) === 'auto'; }; const mappers = { Fragment: (json, options) => { return `${json.children.map((item) => blockToSwift(item, options)).join('\n')}`; }, link: () => '', Image: (json, options) => { var _a; return (`Image(${processBinding((_a = json.bindings.image) === null || _a === void 0 ? void 0 : _a.code, options) || `"${json.properties.image}"`})` + getStyleString(json, options) + getActionsString(json, options)); }, input: (json, options) => { var _a, _b; const name = json.properties.$name; let str = `TextField(${json.bindings.placeholder ? processBinding((_a = json.bindings.placeholder) === null || _a === void 0 ? void 0 : _a.code, options) : json.properties.placeholder ? JSON.stringify(json.bindings.placeholder.code) : '""'}, text: $${name})` + getStyleString(json, options) + getActionsString(json, options); if (json.bindings.onChange) { str += ` .onChange(of: ${name}) { ${name} in ${processBinding(wrapAction(`var event = { target: { value: "\\(${name})" } }; ${(_b = json.bindings.onChange) === null || _b === void 0 ? void 0 : _b.code}`), options)} }`; } return str; }, }; const blockToSwift = (json, options) => { var _a, _b; if (mappers[json.name]) { return mappers[json.name](json, options); } // TODO: Add support for `{props.children}` bindings // Right now we return an empty string because the generated code // is very likely wrong. if ((0, is_children_1.default)({ node: json })) { return '/* `props.children` is not supported yet for SwiftUI */'; } if (json.properties._text) { if (!json.properties._text.trim().length) { return ''; } return `Text("${json.properties._text.trim().replace(/\s+/g, ' ')}")`; } if (json.bindings._text) { return `Text(${processBinding(json.bindings._text.code, options)}.toString())`; } let str = ''; const children = json.children.filter(filter_empty_text_nodes_1.filterEmptyTextNodes); const style = (0, get_styles_1.getStyles)(json); // TODO: do as preprocess step and do more mappings of dom attributes to special // Image, TextField, etc component props const name = json.name === 'input' ? 'TextField' : json.name === 'img' ? 'Image' : json.name[0].toLowerCase() === json.name[0] ? scrolls(json) ? 'ScrollView' : (style === null || style === void 0 ? void 0 : style.display) === 'flex' && style.flexDirection !== 'column' ? 'HStack' : 'VStack' : json.name; if (name === 'TextField') { const placeholder = json.properties.placeholder; delete json.properties.placeholder; json.properties._ = placeholder || ''; } if ((0, mitosis_node_1.checkIsForNode)(json)) { str += `ForEach(${processBinding((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code, options)}, id: \\.self) { ${json.scope.forName} in ${children .map((item) => blockToSwift(item, options)) .join('\n')} }`; } else if (json.name === 'Show') { str += `if ${processBinding((_b = json.bindings.when) === null || _b === void 0 ? void 0 : _b.code, options)} { ${children.map((item) => blockToSwift(item, options)).join('\n')} }`; } else { str += `${name}(`; for (const key in json.properties) { if (key === 'class' || key === 'className') { continue; } // TODO: binding mappings // const value = json.properties[key]; // str += ` ${key}: "${(value as string).replace(/"/g, '"')}", `; console.warn(`Unsupported property "${key}"`); } for (const key in json.bindings) { if ( // TODO: implement spread, ref, more css key === '_spread' || key === 'ref' || key === 'css' || key === 'class' || key === 'className') { continue; } if ((0, event_handlers_1.checkIsEvent)(key)) { if (key === 'onClick') { continue; } else { // TODO: other event mappings console.warn(`Unsupported event binding "${key}"`); } } else { console.warn(`Unsupported binding "${key}"`); // TODO: need binding mappings // str += ` ${key}: ${processBinding(value, options)}, `; } } str += `)`; str += ` {`; if (json.children) { str += json.children.map((item) => blockToSwift(item, options)).join('\n'); } str += `}`; str += getStyleString(json, options); str += getActionsString(json, options); } return str; }; const wrapAction = (str) => `(() => { ${str} })()`; function getActionsString(json, options) { let str = ''; if (json.bindings.onClick) { str += `\n.onTapGesture { ${processBinding(wrapAction(json.bindings.onClick.code), options)} }`; } return str; } function getStyleString(node, options) { const style = (0, get_styles_1.getStyles)(node); let str = ''; for (const key in style) { let useKey = key; const rawValue = style[key]; let value = `"${rawValue}"`; if (['padding', 'margin'].includes(key)) { // TODO: throw error if calc() value = parseFloat(rawValue); str += `\n.${useKey}(${value})`; } else if (key === 'color') { useKey = 'foregroundColor'; // TODO: convert to RBG and use Color(red: ..., ....) } else { console.warn(`Styling key "${key}" is not supported`); } } return str; } function getJsSource(json, options) { const str = `const state = new Proxy(${(0, get_state_object_string_1.getStateObjectStringFromComponent)(json)}, { set: (target, key, value) => { const returnVal = Reflect.set(target, key, value); update(); return returnVal; } });`; if (options.prettier === false) { return str.trim(); } else { return (0, try_prettier_format_1.tryPrettierFormat)(str, 'typescript').trim(); } } const processBinding = (str, options) => { // Use triple quotes for multiline strings or strings including '"' if (str.includes('\n') || str.includes('"')) { return `eval(code: """ ${str} """)`; } // Use double quotes for simple strings return `eval(code: "${str}")`; }; function componentHasDynamicData(json) { const hasState = (0, state_1.checkHasState)(json); if (hasState) { return true; } let found = false; (0, legacy_1.default)(json).forEach(function (node) { if ((0, is_mitosis_node_1.isMitosisNode)(node)) { if (Object.keys(node.bindings).filter((item) => item !== 'css').length) { found = true; this.stop(); } } }); return found; } function mapDataForSwiftCompatability(json) { let inputIndex = 0; json.meta.inputNames = json.meta.inputNames || []; (0, legacy_1.default)(json).forEach(function (node) { var _a; if ((0, is_mitosis_node_1.isMitosisNode)(node)) { if (node.name === 'input') { if (!Object.keys(node.bindings).filter((item) => item !== 'css').length) { return; } if (!node.properties.$name) { node.properties.$name = `input${++inputIndex}`; } json.meta.inputNames[node.properties.$name] = ((_a = node.bindings.value) === null || _a === void 0 ? void 0 : _a.code) || ''; } } }); } function getInputBindings(json, options) { let str = ''; const inputNames = json.meta.inputNames; if (!inputNames) { return str; } for (const item in inputNames) { str += `\n@State private var ${item}: String = ""`; } return str; } const componentToSwift = (options = {}) => ({ component }) => { const json = (0, fast_clone_1.fastClone)(component); mapDataForSwiftCompatability(json); const hasDyanmicData = componentHasDynamicData(json); let children = json.children.map((item) => blockToSwift(item, options)).join('\n'); const hasInputNames = Object.keys(json.meta.inputNames || {}).length > 0; let str = (0, dedent_1.dedent) ` import SwiftUI ${!hasDyanmicData ? '' : `import JavaScriptCore final class UpdateTracker: ObservableObject { @Published var value = 0; func update() { value += 1 } } `} struct ${component.name}: View { ${!hasDyanmicData ? '' : ` @ObservedObject var updateTracker = UpdateTracker() private var jsContext = JSContext() ${getInputBindings(json, options)} func eval(code: String) -> JSValue! { return jsContext?.evaluateScript(code) } ${!hasInputNames ? '' : ` func setComputedState() { ${Object.keys(json.meta.inputNames || {}) .map((item) => { return `${item} = ${processBinding(json.meta.inputNames[item], options)}.toString()!`; }) .join('\n')} }`} init() { let jsSource = """ ${getJsSource(json, options)} """ jsContext?.exceptionHandler = { context, exception in print("JS Error: \\(exception!)") } let updateRef = updateTracker.update let updateFn : @convention(block) () -> Void = { updateRef() } jsContext?.setObject(updateFn, forKeyedSubscript: "update" as NSString) jsContext?.evaluateScript(jsSource) } `.trim()} var body: some View { VStack { ${children} }${!hasInputNames ? '' : ` .onAppear { setComputedState() } `} } } `; if (options.prettier !== false) { str = (0, generic_format_1.format)(str); } return str; }; exports.componentToSwift = componentToSwift;