@builder.io/mitosis
Version:
Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io
275 lines (274 loc) • 12.2 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseStateObjectToMitosisState = exports.mapStateIdentifiers = void 0;
const babel_transform_1 = require("../../helpers/babel-transform");
const capitalize_1 = require("../../helpers/capitalize");
const is_mitosis_node_1 = require("../../helpers/is-mitosis-node");
const process_code_1 = require("../../helpers/plugins/process-code");
const replace_identifiers_1 = require("../../helpers/replace-identifiers");
const core_1 = require("@babel/core");
const types_1 = require("@babel/types");
const function_1 = require("fp-ts/lib/function");
const legacy_1 = __importDefault(require("neotraverse/legacy"));
const helpers_1 = require("./helpers");
function mapStateIdentifiersInExpression(expression, stateProperties) {
const setExpressions = stateProperties.map((propertyName) => `set${(0, capitalize_1.capitalize)(propertyName)}`);
return (0, function_1.pipe)((0, babel_transform_1.babelTransformExpression)(expression, {
Identifier(path) {
if (stateProperties.includes(path.node.name)) {
if (
// ignore member expressions, as the `stateProperty` is going to be at the module scope.
!((0, types_1.isMemberExpression)(path.parent) && path.parent.property === path.node) &&
!((0, types_1.isOptionalMemberExpression)(path.parent) && path.parent.property === path.node) &&
// ignore declarations of that state property, e.g. `function foo() {}`
!(0, types_1.isDeclaration)(path.parent) &&
!(0, types_1.isFunctionDeclaration)(path.parent) &&
!((0, types_1.isFunctionExpression)(path.parent) && path.parent.id === path.node) &&
// ignore object keys
!((0, types_1.isObjectProperty)(path.parent) && path.parent.key === path.node)) {
let hasTypeParent = false;
path.findParent((parent) => {
if ((0, types_1.isTSType)(parent) || (0, types_1.isTSInterfaceBody)(parent)) {
hasTypeParent = true;
return true;
}
return false;
});
if (hasTypeParent) {
return;
}
const newExpression = (0, types_1.memberExpression)((0, types_1.identifier)('state'), (0, types_1.identifier)(path.node.name));
try {
path.replaceWith(newExpression);
}
catch (err) {
console.error(err);
// console.log('err: ', {
// from: generate(path.parent).code,
// fromChild: generate(path.node).code,
// to: newExpression,
// // err,
// });
}
}
}
},
CallExpression(path) {
if ((0, types_1.isIdentifier)(path.node.callee)) {
if (setExpressions.includes(path.node.callee.name)) {
// setFoo -> foo
const statePropertyName = (0, helpers_1.uncapitalize)(path.node.callee.name.slice(3));
// setFoo(...) -> state.foo = ...
path.replaceWith((0, types_1.assignmentExpression)('=', (0, types_1.identifier)(`state.${statePropertyName}`), path.node.arguments[0]));
}
}
},
}), (code) => code.trim());
}
const consolidateClassBindings = (item) => {
if (item.bindings.className) {
if (item.bindings.class) {
// TO-DO: it's too much work to merge 2 bindings, so just remove the old one for now.
item.bindings.class = item.bindings.className;
}
else {
item.bindings.class = item.bindings.className;
}
delete item.bindings.className;
}
if (item.properties.className) {
if (item.properties.class) {
item.properties.class = `${item.properties.class} ${item.properties.className}`;
}
else {
item.properties.class = item.properties.className;
}
delete item.properties.className;
}
if (item.properties.class && item.bindings.class) {
console.warn(`[${item.name}]: Ended up with both a property and binding for 'class'.`);
}
};
/**
* Convert state identifiers from React hooks format to the state.* format Mitosis needs
* e.g.
* text -> state.text
* setText(...) -> state.text = ...
*
* This also applies to components that use both useState and useStore.
* e.g.
* const [foo, setFoo] = useState(1)
* const store = useStore({
* bar() { return foo } // becomes bar() { return state.foo }
* })`
*/
function mapStateIdentifiers(json, stateProperties) {
const plugin = (0, process_code_1.createCodeProcessorPlugin)(() => (code) => mapStateIdentifiersInExpression(code, stateProperties));
plugin(json);
for (const key in json.targetBlocks) {
const targetBlock = json.targetBlocks[key];
for (const targetBlockKey of Object.keys(targetBlock)) {
const block = targetBlock[targetBlockKey];
if (block && 'code' in block) {
block.code = mapStateIdentifiersInExpression(block.code, stateProperties);
}
}
}
(0, legacy_1.default)(json).forEach(function (item) {
// only consolidate bindings for HTML tags, not custom components
// custom components are always PascalCase, e.g. MyComponent
// but HTML tags are lowercase, e.g. div
if ((0, is_mitosis_node_1.isMitosisNode)(item) && item.name.toLowerCase() === item.name) {
consolidateClassBindings(item);
}
});
}
exports.mapStateIdentifiers = mapStateIdentifiers;
/**
* Replaces `this.` with `state.` and trims code
* @param code origin code
*/
const getCleanedStateCode = (code) => {
return (0, replace_identifiers_1.replaceNodes)({
code,
nodeMaps: [
{
from: core_1.types.thisExpression(),
to: core_1.types.identifier('state'),
},
],
}).trim();
};
const processStateObjectSlice = (item) => {
if ((0, types_1.isObjectProperty)(item)) {
if ((0, types_1.isFunctionExpression)(item.value)) {
return {
code: getCleanedStateCode((0, helpers_1.parseCode)(item.value)),
type: 'function',
};
}
else if ((0, types_1.isArrowFunctionExpression)(item.value)) {
/**
* Arrow functions are normally converted to object methods to work around
* limitations with arrow functions in state in frameworks such as Svelte.
* However, this conversion does not work for async arrow functions due to
* how we handle parsing in `handleErrorOrExpression` for parsing
* expressions. That code does not detect async functions in order to apply
* its parsing workarounds. Even if it did, it does not consider async code
* when prefixing with "function". This would result in "function async foo()"
* which is not a valid function expression definition.
*/
// TODO ENG-7256 Find a way to do this without diverging code path
if (item.value.async) {
const func = (0, types_1.functionExpression)(item.key, item.value.params, item.value.body, false, true);
return {
code: (0, helpers_1.parseCode)(func).trim(),
type: 'function',
};
}
const n = (0, types_1.objectMethod)('method', item.key, item.value.params, item.value.body);
// Replace this. with state. to handle following
// const state = useStore({ _do: () => {this._active = !!id;}})
const code = getCleanedStateCode((0, helpers_1.parseCode)(n));
return {
code: code,
type: 'method',
};
}
else {
// Remove typescript types, e.g. from
// { foo: ('string' as SomeType) }
if ((0, types_1.isTSAsExpression)(item.value)) {
return {
code: getCleanedStateCode((0, helpers_1.parseCode)(item.value.expression)),
type: 'property',
propertyType: 'normal',
};
}
return {
code: getCleanedStateCode((0, helpers_1.parseCode)(item.value)),
type: 'property',
propertyType: 'normal',
};
}
}
else if ((0, types_1.isObjectMethod)(item)) {
// TODO ENG-7256 Find a way to do this without diverging code path
if (item.async) {
const func = (0, types_1.functionExpression)(item.key, item.params, item.body, false, true);
return {
code: (0, helpers_1.parseCode)(func).trim(),
type: 'function',
};
}
const method = (0, types_1.objectMethod)(item.kind, item.key, item.params, item.body, false, false, item.async);
const n = getCleanedStateCode((0, helpers_1.parseCode)({ ...method, returnType: null }));
const isGetter = item.kind === 'get';
return {
code: n,
type: isGetter ? 'getter' : 'method',
};
}
else {
throw new Error('Unexpected state value type', item);
}
};
const processDefaultPropsSlice = (item) => {
if ((0, types_1.isObjectProperty)(item)) {
if ((0, types_1.isFunctionExpression)(item.value) || (0, types_1.isArrowFunctionExpression)(item.value)) {
return {
code: (0, helpers_1.parseCode)(item.value),
type: 'method',
};
}
else {
// Remove typescript types, e.g. from
// { foo: ('string' as SomeType) }
if ((0, types_1.isTSAsExpression)(item.value)) {
return {
code: (0, helpers_1.parseCode)(item.value.expression),
type: 'property',
propertyType: 'normal',
};
}
return {
code: (0, helpers_1.parseCode)(item.value),
type: 'property',
propertyType: 'normal',
};
}
}
else if ((0, types_1.isObjectMethod)(item)) {
const n = (0, helpers_1.parseCode)({ ...item, returnType: null });
const isGetter = item.kind === 'get';
return {
code: n,
type: isGetter ? 'getter' : 'method',
};
}
else {
throw new Error('Unexpected state value type', item);
}
};
const parseStateObjectToMitosisState = (object, isState = true) => {
const state = {};
object.properties.forEach((x) => {
if ((0, types_1.isSpreadElement)(x)) {
throw new Error('Parse Error: Mitosis cannot consume spread element in state object: ' + x);
}
if ((0, types_1.isPrivateName)(x.key)) {
throw new Error('Parse Error: Mitosis cannot consume private name in state object: ' + x.key);
}
if (!(0, types_1.isIdentifier)(x.key) && !(0, types_1.isStringLiteral)(x.key)) {
throw new Error('Parse Error: Mitosis cannot consume non-identifier and non-string key in state object: ' +
x.key);
}
const keyName = (0, types_1.isStringLiteral)(x.key) ? x.key.value : x.key.name;
state[keyName] = isState ? processStateObjectSlice(x) : processDefaultPropsSlice(x);
});
return state;
};
exports.parseStateObjectToMitosisState = parseStateObjectToMitosisState;
;