UNPKG

@builder.io/mitosis

Version:

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

202 lines (200 loc) 9.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.componentToAlpine = exports.isValidAlpineBinding = exports.checkIsComponentNode = void 0; const html_tags_1 = require("../../constants/html_tags"); const babel_transform_1 = require("../../helpers/babel-transform"); const dash_case_1 = require("../../helpers/dash-case"); const event_handlers_1 = require("../../helpers/event-handlers"); const fast_clone_1 = require("../../helpers/fast-clone"); const get_refs_1 = require("../../helpers/get-refs"); const get_state_object_string_1 = require("../../helpers/get-state-object-string"); const merge_options_1 = require("../../helpers/merge-options"); const remove_surrounding_block_1 = require("../../helpers/remove-surrounding-block"); const replace_identifiers_1 = require("../../helpers/replace-identifiers"); const strip_meta_properties_1 = require("../../helpers/strip-meta-properties"); const strip_state_and_props_refs_1 = require("../../helpers/strip-state-and-props-refs"); const collect_css_1 = require("../../helpers/styles/collect-css"); const plugins_1 = require("../../modules/plugins"); const mitosis_node_1 = require("../../types/mitosis-node"); const lodash_1 = require("lodash"); const standalone_1 = require("prettier/standalone"); const render_mount_hook_1 = require("./render-mount-hook"); const render_update_hooks_1 = require("./render-update-hooks"); const checkIsComponentNode = (node) => node.name === '@builder.io/mitosis/component'; exports.checkIsComponentNode = checkIsComponentNode; /** * Test if the binding expression would be likely to generate * valid or invalid liquid. If we generate invalid liquid tags * Shopify will reject our PUT to update the template */ const isValidAlpineBinding = (str = '') => { return true; /* const strictMatches = Boolean( // Test for our `context.shopify.liquid.*(expression), which // we regex out later to transform back into valid liquid expressions str.match(/(context|ctx)\s*(\.shopify\s*)?\.liquid\s*\./), ); return ( strictMatches || // Test is the expression is simple and would map to Shopify bindings // Test for our `context.shopify.liquid.*(expression), which // e.g. `state.product.price` -> `{{product.price}} // we regex out later to transform back into valid liquid expressions Boolean(str.match(/^[a-z0-9_\.\s]+$/i)) ); */ }; exports.isValidAlpineBinding = isValidAlpineBinding; const removeOnFromEventName = (str) => str.replace(/^on/, ''); const removeTrailingSemicolon = (str) => str.replace(/;$/, ''); const trim = (str) => str.trim(); const replaceInputRefs = (0, lodash_1.curry)((json, str) => { (0, get_refs_1.getRefs)(json).forEach((value) => { str = str.replaceAll(value, `this.$refs.${value}`); }); return str; }); const replaceStateWithThis = (str) => str.replaceAll('state.', 'this.'); const getStateObjectString = (json) => (0, lodash_1.flow)(get_state_object_string_1.getStateObjectStringFromComponent, trim, replaceInputRefs(json), (0, render_mount_hook_1.renderMountHook)(json), (0, render_update_hooks_1.renderUpdateHooks)(json), replaceStateWithThis, // cleanup bad regexes that result in malformed JSON strings that start with `{,` (x) => (x.startsWith('{,') ? x.replace('{,', '{') : x))(json); const bindEventHandlerKey = (0, lodash_1.flowRight)(dash_case_1.dashCase, removeOnFromEventName); const bindEventHandlerValue = (0, lodash_1.flowRight)((x) => (0, replace_identifiers_1.replaceIdentifiers)({ code: x, from: 'event', to: '$event', }), removeTrailingSemicolon, trim, remove_surrounding_block_1.removeSurroundingBlock, strip_state_and_props_refs_1.stripStateAndPropsRefs); const bindEventHandler = ({ useShorthandSyntax }) => (eventName, code) => { const bind = useShorthandSyntax ? '@' : 'x-on:'; return ` ${bind}${bindEventHandlerKey(eventName)}="${bindEventHandlerValue(code).trim()}"`; }; const mappers = { For: (json, options) => { var _a, _b, _c; return !((0, mitosis_node_1.checkIsForNode)(json) && (0, exports.isValidAlpineBinding)((_a = json.bindings.each) === null || _a === void 0 ? void 0 : _a.code) && (0, exports.isValidAlpineBinding)(json.scope.forName)) ? '' : `<template x-for="${json.scope.forName} in ${(0, strip_state_and_props_refs_1.stripStateAndPropsRefs)((_b = json.bindings.each) === null || _b === void 0 ? void 0 : _b.code)}"> ${((_c = json.children) !== null && _c !== void 0 ? _c : []).map((item) => blockToAlpine(item, options)).join('\n')} </template>`; }, Fragment: (json, options) => blockToAlpine({ ...json, name: 'div' }, options), Show: (json, options) => { var _a, _b, _c; return !(0, exports.isValidAlpineBinding)((_a = json.bindings.when) === null || _a === void 0 ? void 0 : _a.code) ? '' : `<template x-if="${(0, strip_state_and_props_refs_1.stripStateAndPropsRefs)((_b = json.bindings.when) === null || _b === void 0 ? void 0 : _b.code)}"> ${((_c = json.children) !== null && _c !== void 0 ? _c : []).map((item) => blockToAlpine(item, options)).join('\n')} </template>`; }, }; // TODO: spread support const blockToAlpine = (json, options = {}) => { var _a, _b; if (mappers[json.name]) { return mappers[json.name](json, options); } // TODO: Add support for `{props.children}` bindings if (json.properties._text) { return json.properties._text; } if ((_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code) { return (0, exports.isValidAlpineBinding)(json.bindings._text.code) ? `<span x-html="${(0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(json.bindings._text.code)}"></span>` : ''; } let str = `<${json.name} `; /* // Copied from the liquid generator. Not sure what it does. if ( json.bindings._spread?.code === '_spread' && isValidAlpineBinding(json.bindings._spread.code) ) { str += ` <template x-for="_attr in ${json.bindings._spread.code}"> {{ _attr[0] }}="{{ _attr[1] }}" </template> `; } */ for (const key in json.properties) { const value = json.properties[key]; str += ` ${key}="${value}" `; } for (const key in json.bindings) { if (key === '_spread' || key === 'css') { continue; } const { code: value, type: bindingType } = json.bindings[key]; // TODO: proper babel transform to replace. Util for this const useValue = (0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(value); if ((0, event_handlers_1.checkIsEvent)(key)) { str += bindEventHandler(options)(key, value); } else if (key === 'ref') { str += ` x-ref="${useValue}"`; } else if ((0, exports.isValidAlpineBinding)(useValue)) { const bind = options.useShorthandSyntax && bindingType !== 'spread' ? ':' : 'x-bind:'; str += ` ${bind}${bindingType === 'spread' ? '' : key}="${useValue}" `.replace(':=', '='); } } return html_tags_1.SELF_CLOSING_HTML_TAGS.has(json.name) ? `${str} />` : `${str}>${((_b = json.children) !== null && _b !== void 0 ? _b : []).map((item) => blockToAlpine(item, options)).join('\n')}</${json.name}>`; }; const componentToAlpine = (_options = {}) => ({ component }) => { const options = (0, merge_options_1.initializeOptions)({ target: 'alpine', component, defaults: _options }); let json = (0, fast_clone_1.fastClone)(component); if (options.plugins) { json = (0, plugins_1.runPreJsonPlugins)({ json, plugins: options.plugins }); } const css = (0, collect_css_1.collectCss)(json); (0, strip_meta_properties_1.stripMetaProperties)(json); if (options.plugins) { json = (0, plugins_1.runPostJsonPlugins)({ json, plugins: options.plugins }); } const componentName = (0, lodash_1.camelCase)(json.name) || 'MyComponent'; const stateObjectString = getStateObjectString(json); // Set x-data on root element json.children[0].properties['x-data'] = options.inlineState ? stateObjectString : `${componentName}()`; if ((0, render_update_hooks_1.hasRootUpdateHook)(json)) { json.children[0].properties['x-effect'] = 'onUpdate'; } let str = css.trim().length ? `<style>${css}</style>` : ''; str += json.children.map((item) => blockToAlpine(item, options)).join('\n'); if (!options.inlineState) { str += `<script> ${(0, babel_transform_1.babelTransformCode)(`document.addEventListener('alpine:init', () => { Alpine.data('${componentName}', () => (${stateObjectString})) })`)} </script>`; } 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: 'html', htmlWhitespaceSensitivity: 'ignore', plugins: [ // To support running in browsers require('prettier/parser-html'), require('prettier/parser-postcss'), require('prettier/parser-babel'), ], }); } catch (err) { console.warn('Could not prettify', { string: str }, err); } } if (options.plugins) { str = (0, plugins_1.runPostCodePlugins)({ json, code: str, plugins: options.plugins }); } return str; }; exports.componentToAlpine = componentToAlpine;