@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
273 lines (266 loc) • 13.1 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.componentToVue = void 0;
const apply_meta_tagname_1 = require("../../helpers/apply-meta-tagname");
const babel_transform_1 = require("../../helpers/babel-transform");
const bindings_1 = require("../../helpers/bindings");
const dedent_1 = require("../../helpers/dedent");
const fast_clone_1 = require("../../helpers/fast-clone");
const get_props_1 = require("../../helpers/get-props");
const is_mitosis_node_1 = require("../../helpers/is-mitosis-node");
const map_refs_1 = require("../../helpers/map-refs");
const merge_options_1 = require("../../helpers/merge-options");
const on_event_1 = require("../../helpers/on-event");
const process_code_1 = require("../../helpers/plugins/process-code");
const process_http_requests_1 = require("../../helpers/process-http-requests");
const render_imports_1 = require("../../helpers/render-imports");
const replace_identifiers_1 = require("../../helpers/replace-identifiers");
const slots_1 = require("../../helpers/slots");
const strip_meta_properties_1 = require("../../helpers/strip-meta-properties");
const collect_css_1 = require("../../helpers/styles/collect-css");
const function_1 = require("fp-ts/lib/function");
const lodash_1 = require("lodash");
const legacy_1 = __importDefault(require("neotraverse/legacy"));
const standalone_1 = require("prettier/standalone");
const plugins_1 = require("../../modules/plugins");
const functions_1 = require("../helpers/functions");
const blocks_1 = require("./blocks");
const compositionApi_1 = require("./compositionApi");
const helpers_1 = require("./helpers");
const optionsApi_1 = require("./optionsApi");
// Transform <foo.bar key="value" /> to <component :is="foo.bar" key="value" />
function processDynamicComponents(json, _options) {
(0, legacy_1.default)(json).forEach((node) => {
if ((0, is_mitosis_node_1.isMitosisNode)(node)) {
if (node.name.includes('.')) {
node.bindings.is = (0, bindings_1.createSingleBinding)({ code: node.name });
node.name = 'component';
}
}
});
}
function processForKeys(json, _options) {
(0, legacy_1.default)(json).forEach((node) => {
if ((0, is_mitosis_node_1.isMitosisNode)(node)) {
if (node.name === 'For') {
const firstChild = node.children[0];
if (firstChild && firstChild.bindings.key) {
node.bindings.key = firstChild.bindings.key;
delete firstChild.bindings.key;
}
}
}
});
}
/**
* This plugin handle `onUpdate` code that watches dependencies.
* We need to apply this workaround to be able to watch specific dependencies in Vue 2: https://stackoverflow.com/a/45853349
*
* We add a `computed` property for the dependencies, and a matching `watch` function for the `onUpdate` code
*/
const onUpdatePlugin = (options) => ({
json: {
post: (component) => {
if (component.hooks.onUpdate) {
component.hooks.onUpdate
.filter((hook) => { var _a; return (_a = hook.deps) === null || _a === void 0 ? void 0 : _a.length; })
.forEach((hook, index) => {
var _a;
const code = `get ${(0, helpers_1.getOnUpdateHookName)(index)} () {
return {
${(_a = hook.deps) === null || _a === void 0 ? void 0 : _a.slice(1, -1).split(',').map((dep, k) => {
const val = dep.trim();
return `${k}: ${val}`;
}).join(',')}
}
}`;
component.state[(0, helpers_1.getOnUpdateHookName)(index)] = {
code,
type: 'getter',
};
});
}
},
},
});
const BASE_OPTIONS = {
api: 'options',
defineComponent: true,
casing: 'pascal',
};
const componentToVue = (userOptions) => ({ component: _component, path }) => {
var _a, _b, _c, _d, _e, _f;
// Make a copy we can safely mutate, similar to babel's toolchain can be used
let component = (0, fast_clone_1.fastClone)(_component);
const options = (0, merge_options_1.initializeOptions)({
target: 'vue',
component,
defaults: BASE_OPTIONS,
userOptions: userOptions,
});
if (options.api === 'composition') {
options.asyncComponentImports = false;
}
options.plugins.unshift((0, on_event_1.processOnEventHooksPlugin)(), ...(options.api === 'options' ? [onUpdatePlugin] : []), ...(options.api === 'composition' ? [functions_1.FUNCTION_HACK_PLUGIN] : []), (0, process_code_1.CODE_PROCESSOR_PLUGIN)((codeType) => {
if (options.api === 'composition') {
switch (codeType) {
case 'hooks':
return (code) => (0, helpers_1.processBinding)({ code, options, json: component });
case 'state':
return (code) => (0, helpers_1.processBinding)({ code, options, json: component });
case 'bindings':
return (0, function_1.flow)(
// Strip types from any JS code that ends up in the template, because Vue does not support TS code in templates.
babel_transform_1.convertTypeScriptToJS, (code) => (0, helpers_1.processBinding)({ code, options, json: component, codeType }));
case 'context-set':
return (code) => (0, helpers_1.processBinding)({ code, options, json: component, preserveGetter: true });
case 'hooks-deps':
return (0, replace_identifiers_1.replaceStateIdentifier)(null);
case 'properties':
case 'dynamic-jsx-elements':
case 'hooks-deps-array':
case 'types':
return (c) => c;
}
}
else {
switch (codeType) {
case 'hooks':
return (code) => (0, helpers_1.processBinding)({ code, options, json: component });
case 'bindings':
return (0, function_1.flow)(
// Strip types from any JS code that ends up in the template, because Vue does not support TS code in templates.
babel_transform_1.convertTypeScriptToJS, (code) => (0, helpers_1.processBinding)({ code, options, json: component, codeType }));
case 'properties':
case 'dynamic-jsx-elements':
case 'hooks-deps':
case 'hooks-deps-array':
case 'types':
return (c) => c;
case 'state':
return (c) => (0, helpers_1.processBinding)({ code: c, options, json: component });
case 'context-set':
return (code) => (0, helpers_1.processBinding)({
code,
options,
json: component,
thisPrefix: '_this',
preserveGetter: true,
});
}
}
}));
(0, process_http_requests_1.processHttpRequests)(component);
processDynamicComponents(component, options);
processForKeys(component, options);
component = (0, plugins_1.runPreJsonPlugins)({ json: component, plugins: options.plugins });
if (options.api === 'options') {
(0, map_refs_1.mapRefs)(component, (refName) => `this.$refs.${refName}`);
}
// need to run this before we process the component's code
const props = Array.from((0, get_props_1.getProps)(component));
const elementProps = props.filter((prop) => !(0, slots_1.isSlotProperty)(prop));
const slotsProps = props.filter((prop) => (0, slots_1.isSlotProperty)(prop));
component = (0, plugins_1.runPostJsonPlugins)({ json: component, plugins: options.plugins });
const css = (0, collect_css_1.collectCss)(component, {
prefix: (_b = (_a = options.cssNamespace) === null || _a === void 0 ? void 0 : _a.call(options)) !== null && _b !== void 0 ? _b : undefined,
});
// Apply the meta tagName to the component BEFORE we strip the meta properties
(0, apply_meta_tagname_1.applyMetaTagName)(component);
(0, strip_meta_properties_1.stripMetaProperties)(component);
const templateStrBody = component.children
.map((item) => (0, blocks_1.blockToVue)(item, options, { isRootNode: true }))
.join('\n');
const template = options.casing === 'kebab'
? (0, helpers_1.renameMitosisComponentsToKebabCase)(templateStrBody)
: templateStrBody;
const onUpdateWithDeps = ((_c = component.hooks.onUpdate) === null || _c === void 0 ? void 0 : _c.filter((hook) => { var _a; return (_a = hook.deps) === null || _a === void 0 ? void 0 : _a.length; })) || [];
const onUpdateWithoutDeps = ((_d = component.hooks.onUpdate) === null || _d === void 0 ? void 0 : _d.filter((hook) => { var _a; return !((_a = hook.deps) === null || _a === void 0 ? void 0 : _a.length); })) || [];
const getterKeys = Object.keys((0, lodash_1.pickBy)(component.state, (i) => (i === null || i === void 0 ? void 0 : i.type) === 'getter'));
// import from vue
let vueImports = [];
if (options.asyncComponentImports) {
vueImports.push('defineAsyncComponent');
}
if (options.api === 'options' && options.defineComponent) {
vueImports.push('defineComponent');
}
if (options.api === 'composition') {
onUpdateWithDeps.length && vueImports.push('watch');
component.hooks.onMount.length && vueImports.push('onMounted');
((_e = component.hooks.onUnMount) === null || _e === void 0 ? void 0 : _e.code) && vueImports.push('onUnmounted');
onUpdateWithoutDeps.length && vueImports.push('onUpdated');
(0, lodash_1.size)(getterKeys) && vueImports.push('computed');
(0, lodash_1.size)(component.context.set) && vueImports.push('provide');
(0, lodash_1.size)(component.context.get) && vueImports.push('inject');
(0, lodash_1.size)(Object.keys(component.state).filter((key) => { var _a; return ((_a = component.state[key]) === null || _a === void 0 ? void 0 : _a.type) === 'property'; })) + (0, lodash_1.size)(component.refs) && vueImports.push('ref');
(0, lodash_1.size)(slotsProps) && vueImports.push('useSlots');
}
const tsLangAttribute = options.typescript ? `lang='ts'` : '';
let str = (0, dedent_1.dedent) `
${template.trim().length > 0
? `<template>
${template}
</template>`
: ''}
<script ${options.api === 'composition' ? 'setup' : ''} ${tsLangAttribute}>
${vueImports.length ? `import { ${(0, lodash_1.uniq)(vueImports).sort().join(', ')} } from "vue"` : ''}
${(0, render_imports_1.renderPreComponent)({
explicitImportFileExtension: options.explicitImportFileExtension,
component,
target: 'vue',
asyncComponentImports: options.asyncComponentImports,
})}
${(options.typescript && ((_f = component.types) === null || _f === void 0 ? void 0 : _f.join('\n'))) || ''}
${options.api === 'composition'
? (0, compositionApi_1.generateCompositionApiScript)(component, options, template, elementProps, onUpdateWithDeps, onUpdateWithoutDeps)
: (0, optionsApi_1.generateOptionsApiScript)(component, options, path, template, elementProps, onUpdateWithDeps, onUpdateWithoutDeps)}
</script>
${!css.trim().length
? ''
: `<style scoped>
${css}
</style>`}
`;
str = (0, plugins_1.runPreCodePlugins)({
json: component,
code: str,
plugins: options.plugins,
options: { json: component },
});
if (true || options.prettier !== false) {
try {
str = (0, standalone_1.format)(str, {
parser: 'vue',
plugins: [
// To support running in browsers
require('prettier/parser-typescript'),
require('prettier/parser-html'),
require('prettier/parser-postcss'),
require('prettier/parser-babel'),
],
});
}
catch (err) {
console.warn('Could not prettify', { string: str }, err);
}
}
str = (0, plugins_1.runPostCodePlugins)({ json: component, code: str, plugins: options.plugins });
for (const pattern of removePatterns) {
str = str.replace(pattern, '').trim();
}
str = str.replace(/<script(.*)>\n?<\/script>/g, '').trim();
return str;
};
exports.componentToVue = componentToVue;
// Remove unused artifacts like empty script or style tags
const removePatterns = [
`<script>
export default {};
</script>`,
`<style>
</style>`,
];
;