UNPKG

@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
"use strict"; 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;