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