@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
330 lines (321 loc) • 13.7 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.componentToMitosis = exports.blockToMitosis = exports.DEFAULT_FORMAT = void 0;
const hooks_1 = require("../../constants/hooks");
const html_tags_1 = require("../../constants/html_tags");
const dedent_1 = require("../../helpers/dedent");
const event_handlers_1 = require("../../helpers/event-handlers");
const fast_clone_1 = require("../../helpers/fast-clone");
const get_components_1 = require("../../helpers/get-components");
const get_refs_1 = require("../../helpers/get-refs");
const get_state_object_string_1 = require("../../helpers/get-state-object-string");
const is_mitosis_node_1 = require("../../helpers/is-mitosis-node");
const is_root_text_node_1 = require("../../helpers/is-root-text-node");
const map_refs_1 = require("../../helpers/map-refs");
const render_imports_1 = require("../../helpers/render-imports");
const state_1 = require("../../helpers/state");
const plugins_1 = require("../../modules/plugins");
const mitosis_node_1 = require("../../types/mitosis-node");
const json5_1 = __importDefault(require("json5"));
const standalone_1 = require("prettier/standalone");
const react_1 = require("../react");
exports.DEFAULT_FORMAT = 'legacy';
// Special isValidAttributeName for Mitosis so we can allow for $ in names
const isValidAttributeName = (str) => {
return Boolean(str && /^[$a-z0-9\-_:]+$/i.test(str));
};
const isInvalidJsxAttributeName = (str) => {
let attr = str.trim();
if (attr.startsWith(':') || str.startsWith('@')) {
return true;
}
return false;
};
const blockToMitosis = (json, toMitosisOptions = {}, component, insideJsx) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
const options = {
format: exports.DEFAULT_FORMAT,
...toMitosisOptions,
};
if (options.format === 'react') {
return (0, react_1.blockToReact)(json, {
format: 'lite',
stateType: 'useState',
stylesType: 'emotion',
type: 'dom',
prettier: options.prettier,
}, component, insideJsx);
}
if ((0, mitosis_node_1.checkIsShowNode)(json)) {
const when = (_a = json.bindings.when) === null || _a === void 0 ? void 0 : _a.code;
const elseCase = json.meta.else;
if (options.nativeConditionals) {
const needsWrapper = json.children.length !== 1;
const renderChildren = `${needsWrapper ? '<>' : ''}
${json.children
.map((child) => (0, exports.blockToMitosis)(child, options, component, needsWrapper))
.join('\n')}
${needsWrapper ? '</>' : ''}`;
const renderElse = elseCase && (0, is_mitosis_node_1.isMitosisNode)(elseCase)
? (0, exports.blockToMitosis)(elseCase, options, component, false)
: 'null';
return `${insideJsx ? '{' : ''}(${when}) ? ${renderChildren} : ${renderElse}${insideJsx ? '}' : ''}`;
}
else {
const elseHandler = elseCase
? ` else={${(0, exports.blockToMitosis)(elseCase, options, component, false)}}`
: '';
return `<Show when={${when}}${elseHandler}>
${json.children.map((child) => (0, exports.blockToMitosis)(child, options, component, true)).join('\n')}
</Show>`;
}
}
if ((0, mitosis_node_1.checkIsForNode)(json)) {
const needsWrapper = json.children.length !== 1;
if (options.nativeLoops) {
const a = `${insideJsx ? '{' : ''}(${(_b = json.bindings.each) === null || _b === void 0 ? void 0 : _b.code}).map(
(${json.scope.forName || '_'}, ${json.scope.indexName || 'index'}) => (
${needsWrapper ? '<>' : ''}
${json.children
.map((child) => (0, exports.blockToMitosis)(child, options, component, needsWrapper))
.join('\n')}
${needsWrapper ? '</>' : ''}
))${insideJsx ? '}' : ''}`;
return a;
}
return `<For each={${(_c = json.bindings.each) === null || _c === void 0 ? void 0 : _c.code}}>
{(${json.scope.forName || '_'}, ${json.scope.indexName || 'index'}) =>
${needsWrapper ? '<>' : ''}
${json.children.map((child) => (0, exports.blockToMitosis)(child, options, component, needsWrapper))}}
${needsWrapper ? '</>' : ''}
</For>`;
}
if (json.properties._text) {
let text = json.properties._text
// Convert HTML <br> to JSX-valid <br />
.replace(/<br\s*>/g, '<br />');
let isInvalidJsx = text.includes('{') || text.includes('}');
if (text.includes('<') || text.includes('>')) {
// test if this can parse as jsx
try {
/**
* We intentionally use the typescript parser here because texts like ">" will crash
* in the typescript parser but will not crash in the babel parser. The Prettier
* formatting that is run after JSX is generated also uses the typescript parser,
* so we want to make sure that doesn't crash.
*/
(0, standalone_1.check)(`let _ = <>${text}</>;`, {
parser: 'typescript',
plugins: [
require('prettier/parser-typescript'), // To support running in browsers
],
});
isInvalidJsx = false;
}
catch (e) {
isInvalidJsx = true;
}
}
if (isInvalidJsx) {
text = text
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/{/g, '{')
.replace(/}/g, '}')
.replace(/&/g, '&');
}
if (insideJsx) {
return `${text}`;
}
else {
return `<>${text}</>`;
}
}
if ((_d = json.bindings._text) === null || _d === void 0 ? void 0 : _d.code) {
if (insideJsx) {
return `{${json.bindings._text.code}}`;
}
else {
return `${json.bindings._text.code}`;
}
}
let str = '';
str += `<${json.name} `;
for (const key in json.properties) {
if (isInvalidJsxAttributeName(key)) {
console.warn('Skipping invalid attribute name:', key);
continue;
}
const value = (json.properties[key] || '').replace(/"/g, '"').replace(/\n/g, '\\n');
if (!isValidAttributeName(key)) {
console.warn('Skipping invalid attribute name:', key);
}
else {
str += ` ${key}="${value}" `;
}
}
for (const key in json.bindings) {
if (isInvalidJsxAttributeName(key)) {
console.warn('Skipping invalid attribute name:', key);
continue;
}
const value = (_e = json.bindings[key]) === null || _e === void 0 ? void 0 : _e.code;
if (!value || ((_f = json.slots) === null || _f === void 0 ? void 0 : _f[key])) {
continue;
}
if (((_g = json.bindings[key]) === null || _g === void 0 ? void 0 : _g.type) === 'spread') {
str += ` {...(${(_h = json.bindings[key]) === null || _h === void 0 ? void 0 : _h.code})} `;
}
else if ((0, event_handlers_1.checkIsEvent)(key)) {
const { arguments: cusArgs = ['event'], async } = json.bindings[key];
const asyncKeyword = async ? 'async ' : '';
str += ` ${key}={${asyncKeyword}(${cusArgs.join(',')}) => ${value.replace(/\s*;$/, '')}} `;
}
else {
if (!isValidAttributeName(key)) {
console.warn('Skipping invalid attribute name:', key);
}
else {
str += ` ${key}={${value}} `;
}
}
}
for (const key in json.slots) {
const value = json.slots[key];
str += ` ${key}={`;
if (value.length > 1) {
str += '<>';
}
str += json.slots[key]
.map((item) => (0, exports.blockToMitosis)(item, options, component, insideJsx))
.join('\n');
if (value.length > 1) {
str += '</>';
}
str += `}`;
}
if (html_tags_1.SELF_CLOSING_HTML_TAGS.has(json.name)) {
return str + ' />';
}
// Self close by default if no children
if (!json.children.length) {
str += ' />';
return str;
}
str += '>';
if (json.children) {
str += json.children.map((item) => (0, exports.blockToMitosis)(item, options, component, true)).join('\n');
}
str += `</${json.name}>`;
return str;
};
exports.blockToMitosis = blockToMitosis;
const getRefsString = (json, refs = Array.from((0, get_refs_1.getRefs)(json))) => {
var _a, _b;
let str = '';
for (const ref of refs) {
const typeParameter = ((_a = json['refs'][ref]) === null || _a === void 0 ? void 0 : _a.typeParameter) || '';
const argument = ((_b = json['refs'][ref]) === null || _b === void 0 ? void 0 : _b.argument) || '';
str += `\nconst ${ref} = useRef${typeParameter ? `<${typeParameter}>` : ''}(${argument});`;
}
return str;
};
const mitosisCoreComponents = ['Show', 'For'];
const componentToMitosis = (toMitosisOptions = {}) => ({ component }) => {
var _a;
const options = {
format: exports.DEFAULT_FORMAT,
...toMitosisOptions,
};
if (options.format === 'react') {
return (0, react_1.componentToReact)({
format: 'lite',
stateType: 'useState',
stylesType: 'emotion',
prettier: options.prettier,
})({ component });
}
let json = (0, fast_clone_1.fastClone)(component);
if (options.plugins) {
json = (0, plugins_1.runPreJsonPlugins)({ json, plugins: options.plugins });
}
const domRefs = (0, get_refs_1.getRefs)(component);
// grab refs not used for bindings
const jsRefs = Object.keys(component.refs).filter((ref) => domRefs.has(ref));
const refs = [...jsRefs, ...Array.from(domRefs)];
(0, map_refs_1.mapRefs)(json, (refName) => {
return `${refName}${domRefs.has(refName) ? `.current` : ''}`;
});
const addWrapper = json.children.length !== 1 || (0, is_root_text_node_1.isRootTextNode)(json);
const components = Array.from((0, get_components_1.getComponents)(json));
const mitosisCoreComponents = [];
if (!options.nativeConditionals) {
mitosisCoreComponents.push('Show');
}
if (!options.nativeLoops) {
mitosisCoreComponents.push('For');
}
const mitosisComponents = components.filter((item) => mitosisCoreComponents.includes(item));
const otherComponents = components.filter((item) => !mitosisCoreComponents.includes(item));
if (options.plugins) {
json = (0, plugins_1.runPostJsonPlugins)({ json, plugins: options.plugins });
}
const hasState = (0, state_1.checkHasState)(component);
const needsMitosisCoreImport = Boolean(hasState || refs.length || mitosisComponents.length);
const stringifiedUseMetadata = json5_1.default.stringify(component.meta.useMetadata);
// TODO: smart only pull in imports as needed
let str = (0, dedent_1.dedent) `
${!needsMitosisCoreImport
? ''
: `import { ${!hasState ? '' : 'useStore, '} ${!refs.length ? '' : 'useRef, '} ${mitosisComponents.join(', ')} } from '../..';`}
${!otherComponents.length ? '' : `import { ${otherComponents.join(',')} } from '@components';`}
${json.types ? json.types.join('\n') : ''}
${(0, render_imports_1.renderPreComponent)({
explicitImportFileExtension: options.explicitImportFileExtension,
component: json,
target: 'mitosis',
})}
${stringifiedUseMetadata && stringifiedUseMetadata !== '{}'
? `${hooks_1.HOOKS.METADATA}(${stringifiedUseMetadata})`
: ''}
export default function ${component.name}(props) {
${!hasState ? '' : `const state = useStore(${(0, get_state_object_string_1.getStateObjectStringFromComponent)(json)});`}
${getRefsString(json, refs)}
${json.hooks.onMount.map((hook) => `onMount(() => { ${hook.code} })`)}
${!((_a = json.hooks.onUnMount) === null || _a === void 0 ? void 0 : _a.code) ? '' : `onUnMount(() => { ${json.hooks.onUnMount.code} })`}
${json.style ? `useStyle(\`${json.style}\`)` : ''}
return ${options.returnArray ? '[' : '('}${addWrapper ? '<>' : ''}
${json.children
.map((item) => (0, exports.blockToMitosis)(item, options, component, addWrapper))
.join('\n')}
${addWrapper ? '</>' : ''}${options.returnArray ? ']' : ')'}
}
`;
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: 'typescript',
plugins: [
require('prettier/parser-typescript'), // To support running in browsers
],
});
}
catch (err) {
if (process.env.NODE_ENV !== 'test') {
console.error('Format error for file:', str, JSON.stringify(json, null, 2));
}
throw err;
}
}
if (options.plugins) {
str = (0, plugins_1.runPostCodePlugins)({ json, code: str, plugins: options.plugins });
}
return str;
};
exports.componentToMitosis = componentToMitosis;
;