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