UNPKG

@builder.io/mitosis

Version:

Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io

1,178 lines (1,138 loc) 52.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.componentToCustomElement = exports.componentToHtml = 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_prop_functions_1 = require("../../helpers/get-prop-functions"); const get_props_1 = require("../../helpers/get-props"); const get_props_ref_1 = require("../../helpers/get-props-ref"); const get_refs_1 = require("../../helpers/get-refs"); const get_state_object_string_1 = require("../../helpers/get-state-object-string"); const has_bindings_text_1 = require("../../helpers/has-bindings-text"); const has_component_1 = require("../../helpers/has-component"); const has_props_1 = require("../../helpers/has-props"); const has_stateful_dom_1 = require("../../helpers/has-stateful-dom"); const is_html_attribute_1 = require("../../helpers/is-html-attribute"); 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 for_1 = require("../../helpers/nodes/for"); const remove_surrounding_block_1 = require("../../helpers/remove-surrounding-block"); const render_imports_1 = require("../../helpers/render-imports"); 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 core_1 = require("@babel/core"); const function_1 = require("fp-ts/lib/function"); const lodash_1 = require("lodash"); const legacy_1 = __importDefault(require("neotraverse/legacy")); const is_children_1 = __importDefault(require("../../helpers/is-children")); const standalone_1 = require("prettier/standalone"); const on_mount_1 = require("../helpers/on-mount"); const isAttribute = (key) => { return /-/.test(key); }; const ATTRIBUTE_KEY_EXCEPTIONS_MAP = { class: 'className', innerHtml: 'innerHTML', }; const updateKeyIfException = (key) => { var _a; return (_a = ATTRIBUTE_KEY_EXCEPTIONS_MAP[key]) !== null && _a !== void 0 ? _a : key; }; const generateSetElementAttributeCode = (key, tagName, useValue, options, meta = {}) => { var _a, _b; if ((_a = options === null || options === void 0 ? void 0 : options.experimental) === null || _a === void 0 ? void 0 : _a.props) { return (_b = options === null || options === void 0 ? void 0 : options.experimental) === null || _b === void 0 ? void 0 : _b.props(key, useValue, options); } const isKey = key === 'key'; const ignoreKey = /^(innerHTML|key|class|value)$/.test(key); const isTextarea = key === 'value' && tagName === 'textarea'; const isDataSet = /^data-/.test(key); const isComponent = Boolean(meta === null || meta === void 0 ? void 0 : meta.component); const isHtmlAttr = (0, is_html_attribute_1.isHtmlAttribute)(key, tagName); const setAttr = !ignoreKey && (isHtmlAttr || !isTextarea || isAttribute(key)); return [ // is html attribute or dash-case setAttr ? `;el.setAttribute("${key}", ${useValue});` : '', // not attr or dataset or html attr !setAttr || !(isHtmlAttr || isDataSet || !isComponent || isKey) ? `el.${updateKeyIfException((0, lodash_1.camelCase)(key))} = ${useValue};` : '', // is component but not html attribute isComponent && !isHtmlAttr ? // custom-element is created but we're in the middle of the update loop ` if (el.props) { ;el.props.${(0, lodash_1.camelCase)(key)} = ${useValue}; if (el.update) { ;el.update(); } } else { ;el.props = {}; ;el.props.${(0, lodash_1.camelCase)(key)} = ${useValue}; } ` : '', ].join('\n'); }; const addUpdateAfterSet = (json, options) => { (0, legacy_1.default)(json).forEach(function (item) { var _a; if ((0, is_mitosis_node_1.isMitosisNode)(item)) { for (const key in item.bindings) { const value = (_a = item.bindings[key]) === null || _a === void 0 ? void 0 : _a.code; if (value) { const newValue = addUpdateAfterSetInCode(value, options); if (newValue !== value) { item.bindings[key].code = newValue; } } } } }); }; const getChildComponents = (json, options) => { const childComponents = []; json.imports.forEach(({ imports }) => { Object.keys(imports).forEach((key) => { if (imports[key] === 'default') { childComponents.push(key); } }); }); return childComponents; }; const getScopeVars = (parentScopeVars, value) => { return parentScopeVars.filter((scopeVar) => { if (typeof value === 'boolean') { return value; } const checkVar = new RegExp('(\\.\\.\\.|,| |;|\\(|^|!)' + scopeVar + '(\\.|,| |;|\\)|$)', 'g'); return checkVar.test(value); }); }; const addScopeVars = (parentScopeVars, value, fn) => { return `${getScopeVars(parentScopeVars, value) .map((scopeVar) => { return fn(scopeVar); }) .join('\n')}`; }; const mappers = { Fragment: (json, options, blockOptions) => { return json.children.map((item) => blockToHtml(item, options, blockOptions)).join('\n'); }, }; const getId = (json, options) => { const name = json.properties.$name ? (0, dash_case_1.dashCase)(json.properties.$name) : /^h\d$/.test(json.name || '') // don't dashcase h1 into h-1 ? json.name : (0, dash_case_1.dashCase)(json.name || 'div'); const newNameNum = (options.namesMap[name] || 0) + 1; options.namesMap[name] = newNameNum; return `${name}${options.prefix ? `-${options.prefix}` : ''}${name !== json.name && newNameNum === 1 ? '' : `-${newNameNum}`}`; }; const createGlobalId = (name, options) => { const newNameNum = (options.namesMap[name] || 0) + 1; options.namesMap[name] = newNameNum; return `${name}${options.prefix ? `-${options.prefix}` : ''}-${newNameNum}`; }; const deprecatedStripStateAndPropsRefs = (code, { context, contextVars, domRefs, includeProps, includeState, outputVars, replaceWith, stateVars, }) => { return (0, function_1.pipe)((0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(code, { includeProps, includeState, replaceWith, }), (newCode) => (0, strip_state_and_props_refs_1.DO_NOT_USE_VARS_TRANSFORMS)(newCode, { context, contextVars, domRefs, outputVars, stateVars, })); }; // TODO: overloaded function const updateReferencesInCode = (code, options, blockOptions = {}) => { var _a, _b; const contextVars = blockOptions.contextVars || []; const context = (blockOptions === null || blockOptions === void 0 ? void 0 : blockOptions.context) || 'this.'; if ((_a = options === null || options === void 0 ? void 0 : options.experimental) === null || _a === void 0 ? void 0 : _a.updateReferencesInCode) { return (_b = options === null || options === void 0 ? void 0 : options.experimental) === null || _b === void 0 ? void 0 : _b.updateReferencesInCode(code, options, { stripStateAndPropsRefs: deprecatedStripStateAndPropsRefs, }); } if (options.format === 'class') { return (0, function_1.pipe)((0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(code, { includeProps: false, includeState: true, replaceWith: context + 'state.', }), (newCode) => (0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(newCode, { // TODO: replace with `this.` and add setters that call this.update() includeProps: true, includeState: false, replaceWith: context + 'props.', }), (newCode) => (0, strip_state_and_props_refs_1.DO_NOT_USE_CONTEXT_VARS_TRANSFORMS)({ code: newCode, context, contextVars })); } return code; }; const addOnChangeJs = (id, options, code) => { if (!options.onChangeJsById[id]) { options.onChangeJsById[id] = ''; } options.onChangeJsById[id] += code; }; // TODO: spread support const blockToHtml = (json, options, blockOptions = {}) => { var _a, _b, _c, _d, _e, _f, _g, _h; const ComponentName = blockOptions.ComponentName; const scopeVars = (blockOptions === null || blockOptions === void 0 ? void 0 : blockOptions.scopeVars) || []; const childComponents = (blockOptions === null || blockOptions === void 0 ? void 0 : blockOptions.childComponents) || []; const hasData = Object.keys(json.bindings).length; const hasDomState = /input|textarea|select/.test(json.name); let elId = ''; if (hasData) { elId = getId(json, options); json.properties['data-el'] = elId; } if (hasDomState) { json.properties['data-dom-state'] = createGlobalId((ComponentName ? ComponentName + '-' : '') + json.name, options); } if (mappers[json.name]) { return mappers[json.name](json, options, blockOptions); } if ((0, is_children_1.default)({ node: json })) { return `<slot></slot>`; } if (json.properties._text) { return json.properties._text; } if ((_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code) { // TO-DO: textContent might be better performance-wise addOnChangeJs(elId, options, ` ${addScopeVars(scopeVars, json.bindings._text.code, (scopeVar) => `const ${scopeVar} = ${options.format === 'class' ? 'this.' : ''}getScope(el, "${scopeVar}");`)} ${options.format === 'class' ? 'this.' : ''}renderTextNode(el, ${json.bindings._text.code});`); return `<template data-el="${elId}"><!-- ${(_b = json.bindings._text) === null || _b === void 0 ? void 0 : _b.code} --></template>`; } let str = ''; if ((0, mitosis_node_1.checkIsForNode)(json)) { const forArguments = (0, for_1.getForArguments)(json); const localScopeVars = [...scopeVars, ...forArguments]; const argsStr = forArguments.map((arg) => `"${arg}"`).join(','); addOnChangeJs(elId, options, // TODO: be smarter about rendering, deleting old items and adding new ones by // querying dom potentially ` let array = ${(_c = json.bindings.each) === null || _c === void 0 ? void 0 : _c.code}; ${options.format === 'class' ? 'this.' : ''}renderLoop(el, array, ${argsStr}); `); // TODO: decide on how to handle this... str += ` <template data-el="${elId}">`; if (json.children) { str += json.children .map((item) => blockToHtml(item, options, { ...blockOptions, scopeVars: localScopeVars, })) .join('\n'); } str += '</template>'; } else if (json.name === 'Show') { const whenCondition = ((_d = json.bindings.when) === null || _d === void 0 ? void 0 : _d.code).replace(/;$/, ''); addOnChangeJs(elId, options, ` ${addScopeVars(scopeVars, whenCondition, (scopeVar) => `const ${scopeVar} = ${options.format === 'class' ? 'this.' : ''}getScope(el, "${scopeVar}");`)} const whenCondition = ${whenCondition}; if (whenCondition) { ${options.format === 'class' ? 'this.' : ''}showContent(el) } `); str += `<template data-el="${elId}">`; if (json.children) { str += json.children.map((item) => blockToHtml(item, options, blockOptions)).join('\n'); } str += '</template>'; } else { const component = childComponents.find((impName) => impName === json.name); const elSelector = component ? (0, lodash_1.kebabCase)(json.name) : json.name; str += `<${elSelector} `; // For now, spread is not supported // if (json.bindings._spread === '_spread') { // str += ` // {% for _attr in ${json.bindings._spread} %} // {{ _attr[0] }}="{{ _attr[1] }}" // {% endfor %} // `; // } for (const key in json.properties) { if (key === 'innerHTML') { continue; } if (key.startsWith('$')) { continue; } const value = (json.properties[key] || '').replace(/"/g, '&quot;').replace(/\n/g, '\\n'); str += ` ${key}="${value}" `; } // batch all local vars within the bindings let batchScopeVars = {}; let injectOnce = false; let startInjectVar = '%%START_VARS%%'; for (const key in json.bindings) { if (((_e = json.bindings[key]) === null || _e === void 0 ? void 0 : _e.type) === 'spread' || key === 'css') { continue; } const value = (_f = json.bindings[key]) === null || _f === void 0 ? void 0 : _f.code; const cusArg = ((_g = json.bindings[key]) === null || _g === void 0 ? void 0 : _g.arguments) || ['event']; // TODO: proper babel transform to replace. Util for this const useValue = value; if ((0, event_handlers_1.checkIsEvent)(key)) { let event = key.replace('on', '').toLowerCase(); const fnName = (0, lodash_1.camelCase)(`on-${elId}-${event}`); const codeContent = (0, remove_surrounding_block_1.removeSurroundingBlock)(updateReferencesInCode(useValue, options, blockOptions)); const asyncKeyword = ((_h = json.bindings[key]) === null || _h === void 0 ? void 0 : _h.async) ? 'async ' : ''; options.js += ` // Event handler for '${event}' event on ${elId} ${options.format === 'class' ? `this.${fnName} = ${asyncKeyword}(${cusArg.join(',')}) => {` : `${asyncKeyword}function ${fnName} (${cusArg.join(',')}) {`} ${addScopeVars(scopeVars, codeContent, (scopeVar) => `const ${scopeVar} = ${options.format === 'class' ? 'this.' : ''}getScope(event.currentTarget, "${scopeVar}");`)} ${codeContent} } `; const fnIdentifier = `${options.format === 'class' ? 'this.' : ''}${fnName}`; addOnChangeJs(elId, options, ` ;el.removeEventListener('${event}', ${fnIdentifier}); ;el.addEventListener('${event}', ${fnIdentifier}); `); } else if (key === 'ref') { str += ` data-ref="${ComponentName}-${useValue}" `; } else { if (key === 'style') { addOnChangeJs(elId, options, ` ${addScopeVars(scopeVars, useValue, (scopeVar) => `const ${scopeVar} = ${options.format === 'class' ? 'this.' : ''}getScope(el, "${scopeVar}");`)} ;Object.assign(el.style, ${useValue});`); } else { // gather all local vars to inject later getScopeVars(scopeVars, useValue).forEach((key) => { // unique keys batchScopeVars[key] = true; }); addOnChangeJs(elId, options, ` ${injectOnce ? '' : startInjectVar} ${generateSetElementAttributeCode(key, elSelector, useValue, options, { component, })} `); if (!injectOnce) { injectOnce = true; } } } } // batch inject local vars in the beginning of the function block const codeBlock = options.onChangeJsById[elId]; const testInjectVar = new RegExp(startInjectVar); if (codeBlock && testInjectVar.test(codeBlock)) { const localScopeVars = Object.keys(batchScopeVars); options.onChangeJsById[elId] = codeBlock.replace(startInjectVar, ` ${addScopeVars(localScopeVars, true, (scopeVar) => `const ${scopeVar} = ${options.format === 'class' ? 'this.' : ''}getScope(el, "${scopeVar}");`)} `); } if (html_tags_1.SELF_CLOSING_HTML_TAGS.has(json.name)) { return str + ' />'; } str += '>'; if (json.children) { str += json.children.map((item) => blockToHtml(item, options, blockOptions)).join('\n'); } if (json.properties.innerHTML) { // Maybe put some kind of safety here for broken HTML such as no close tag str += htmlDecode(json.properties.innerHTML); } str += `</${elSelector}>`; } return str; }; function addUpdateAfterSetInCode(code = '', options, useString = options.format === 'class' ? 'this.update' : 'update') { let updates = 0; return (0, babel_transform_1.babelTransformExpression)(code, { AssignmentExpression(path) { var _a, _b; const { node } = path; if (core_1.types.isMemberExpression(node.left)) { if (core_1.types.isIdentifier(node.left.object)) { // TODO: utillity to properly trace this reference to the beginning if (node.left.object.name === 'state') { // TODO: ultimately do updates by property, e.g. updateName() // that updates any attributes dependent on name, etcç let parent = path; // `_temp = ` assignments are created sometimes when we insertAfter // for simple expressions. this causes us to re-process the same expression // in an infinite loop while ((parent = parent.parentPath)) { if (core_1.types.isAssignmentExpression(parent.node) && core_1.types.isIdentifier(parent.node.left) && parent.node.left.name.startsWith('_temp')) { return; } } // Uncomment to debug infinite loops: // if (updates++ > 1000) { // console.error('Infinite assignment detected'); // return; // } if ((_a = options === null || options === void 0 ? void 0 : options.experimental) === null || _a === void 0 ? void 0 : _a.addUpdateAfterSetInCode) { useString = (_b = options === null || options === void 0 ? void 0 : options.experimental) === null || _b === void 0 ? void 0 : _b.addUpdateAfterSetInCode(useString, options, { node, code, types: core_1.types, }); } path.insertAfter(core_1.types.callExpression(core_1.types.identifier(useString), [])); } } } }, }); } const htmlDecode = (html) => html.replace(/&quot;/gi, '"'); // TODO: props support via custom elements const componentToHtml = (_options = {}) => ({ component }) => { var _a, _b, _c, _d, _e, _f, _g; const options = (0, merge_options_1.initializeOptions)({ target: 'html', component, defaults: { ..._options, onChangeJsById: {}, js: '', namesMap: {}, format: 'script', }, }); let json = (0, fast_clone_1.fastClone)(component); if (options.plugins) { json = (0, plugins_1.runPreJsonPlugins)({ json, plugins: options.plugins }); } addUpdateAfterSet(json, options); const componentHasProps = (0, has_props_1.hasProps)(json); const hasLoop = (0, has_component_1.hasComponent)('For', json); const hasShow = (0, has_component_1.hasComponent)('Show', json); const hasTextBinding = (0, has_bindings_text_1.hasBindingsText)(json); if (options.plugins) { json = (0, plugins_1.runPostJsonPlugins)({ json, plugins: options.plugins }); } const css = (0, collect_css_1.collectCss)(json, { prefix: options.prefix, }); let str = json.children.map((item) => blockToHtml(item, options)).join('\n'); if (css.trim().length) { str += `<style>${css}</style>`; } const hasChangeListeners = Boolean(Object.keys(options.onChangeJsById).length); const hasGeneratedJs = Boolean(options.js.trim().length); if (hasChangeListeners || hasGeneratedJs || json.hooks.onMount.length || hasLoop) { // TODO: collectJs helper for here and liquid str += ` <script> (() => { const state = ${(0, get_state_object_string_1.getStateObjectStringFromComponent)(json, { valueMapper: (value) => addUpdateAfterSetInCode(updateReferencesInCode(value, options), options), })}; ${componentHasProps ? `let props = {};` : ''} let context = null; let nodesToDestroy = []; let pendingUpdate = false; ${!((_b = (_a = json.hooks) === null || _a === void 0 ? void 0 : _a.onInit) === null || _b === void 0 ? void 0 : _b.code) ? '' : 'let onInitOnce = false;'} function destroyAnyNodes() { // destroy current view template refs before rendering again nodesToDestroy.forEach(el => el.remove()); nodesToDestroy = []; } ${!hasChangeListeners ? '' : ` // Function to update data bindings and loops // call update() when you mutate state and need the updates to reflect // in the dom function update() { if (pendingUpdate === true) { return; } pendingUpdate = true; ${Object.keys(options.onChangeJsById) .map((key) => { const value = options.onChangeJsById[key]; if (!value) { return ''; } return ` document.querySelectorAll("[data-el='${key}']").forEach((el) => { ${value} }); `; }) .join('\n\n')} destroyAnyNodes(); ${!((_c = json.hooks.onUpdate) === null || _c === void 0 ? void 0 : _c.length) ? '' : ` ${json.hooks.onUpdate.reduce((code, hook) => { code += addUpdateAfterSetInCode(updateReferencesInCode(hook.code, options), options); return code + '\n'; }, '')} `} pendingUpdate = false; } ${options.js} // Update with initial state on first load update(); `} ${!((_e = (_d = json.hooks) === null || _d === void 0 ? void 0 : _d.onInit) === null || _e === void 0 ? void 0 : _e.code) ? '' : ` if (!onInitOnce) { ${updateReferencesInCode(addUpdateAfterSetInCode((_g = (_f = json.hooks) === null || _f === void 0 ? void 0 : _f.onInit) === null || _g === void 0 ? void 0 : _g.code, options), options)} onInitOnce = true; } `} ${!json.hooks.onMount.length ? '' : // TODO: make prettier by grabbing only the function body ` // onMount ${updateReferencesInCode(addUpdateAfterSetInCode((0, on_mount_1.stringifySingleScopeOnMount)(json), options), options)} `} ${!hasShow ? '' : ` function showContent(el) { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement/content // grabs the content of a node that is between <template> tags // iterates through child nodes to register all content including text elements // attaches the content after the template const elementFragment = el.content.cloneNode(true); const children = Array.from(elementFragment.childNodes) children.forEach(child => { if (el?.scope) { child.scope = el.scope; } if (el?.context) { child.context = el.context; } nodesToDestroy.push(child); }); el.after(elementFragment); } `} ${!hasTextBinding ? '' : ` // Helper text DOM nodes function renderTextNode(el, text) { const textNode = document.createTextNode(text); if (el?.scope) { textNode.scope = el.scope } if (el?.context) { child.context = el.context; } el.after(textNode); nodesToDestroy.push(el.nextSibling); } `} ${!hasLoop ? '' : ` // Helper to render loops function renderLoop(template, array, itemName, itemIndex, collectionName) { const collection = []; for (let [index, value] of array.entries()) { const elementFragment = template.content.cloneNode(true); const children = Array.from(elementFragment.childNodes) const localScope = {}; let scope = localScope; if (template?.scope) { const getParent = { get(target, prop, receiver) { if (prop in target) { return target[prop]; } if (prop in template.scope) { return template.scope[prop]; } return target[prop]; } }; scope = new Proxy(localScope, getParent); } children.forEach((child) => { if (itemName !== undefined) { scope[itemName] = value; } if (itemIndex !== undefined) { scope[itemIndex] = index; } if (collectionName !== undefined) { scope[collectionName] = array; } child.scope = scope; if (template.context) { child.context = template.context; } this.nodesToDestroy.push(child); collection.unshift(child); }); collection.forEach(child => template.after(child)); } } function getScope(el, name) { do { let value = el?.scope?.[name] if (value !== undefined) { return value } } while ((el = el.parentNode)); } `} })() </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.componentToHtml = componentToHtml; // TODO: props support via custom elements const componentToCustomElement = (_options = {}) => ({ component }) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12; const ComponentName = component.name; const kebabName = (0, lodash_1.kebabCase)(ComponentName); const options = (0, merge_options_1.initializeOptions)({ target: 'customElement', component, defaults: { prefix: kebabName, ..._options, onChangeJsById: {}, js: '', namesMap: {}, format: 'class', }, }); let json = (0, fast_clone_1.fastClone)(component); if (options.plugins) { json = (0, plugins_1.runPreJsonPlugins)({ json, plugins: options.plugins }); } const [forwardProp, hasPropRef] = (0, get_props_ref_1.getPropsRef)(json, true); const contextVars = Object.keys(((_a = json === null || json === void 0 ? void 0 : json.context) === null || _a === void 0 ? void 0 : _a.get) || {}); const childComponents = getChildComponents(json, options); const componentHasProps = (0, has_props_1.hasProps)(json); const componentHasStatefulDom = (0, has_stateful_dom_1.hasStatefulDom)(json); const props = (0, get_props_1.getProps)(json); // prevent jsx props from showing up as @Input if (hasPropRef) { props.delete(forwardProp); } const outputs = (0, get_prop_functions_1.getPropFunctions)(json); const domRefs = (0, get_refs_1.getRefs)(json); const jsRefs = Object.keys(json.refs).filter((ref) => !domRefs.has(ref)); (0, map_refs_1.mapRefs)(json, (refName) => `self._${refName}`); const context = contextVars.map((variableName) => { var _a, _b, _c; const token = (_a = json === null || json === void 0 ? void 0 : json.context) === null || _a === void 0 ? void 0 : _a.get[variableName].name; if ((_b = options === null || options === void 0 ? void 0 : options.experimental) === null || _b === void 0 ? void 0 : _b.htmlContext) { return (_c = options === null || options === void 0 ? void 0 : options.experimental) === null || _c === void 0 ? void 0 : _c.htmlContext(variableName, token); } return `this.${variableName} = this.getContext(this._root, ${token})`; }); const setContext = []; for (const key in json.context.set) { const { name, value, ref } = json.context.set[key]; setContext.push({ name, value, ref }); } addUpdateAfterSet(json, options); const hasContext = context.length; const hasLoop = (0, has_component_1.hasComponent)('For', json); const hasScope = hasLoop; const hasShow = (0, has_component_1.hasComponent)('Show', json); if (options.plugins) { json = (0, plugins_1.runPostJsonPlugins)({ json, plugins: options.plugins }); } let css = ''; if ((_b = options === null || options === void 0 ? void 0 : options.experimental) === null || _b === void 0 ? void 0 : _b.css) { css = (_c = options === null || options === void 0 ? void 0 : options.experimental) === null || _c === void 0 ? void 0 : _c.css(json, options, { collectCss: collect_css_1.collectCss, prefix: options.prefix, }); } else { css = (0, collect_css_1.collectCss)(json, { prefix: options.prefix, }); } (0, strip_meta_properties_1.stripMetaProperties)(json); let html = json.children .map((item) => blockToHtml(item, options, { childComponents, props, outputs, ComponentName, contextVars, })) .join('\n'); if ((_d = options === null || options === void 0 ? void 0 : options.experimental) === null || _d === void 0 ? void 0 : _d.childrenHtml) { html = (_e = options === null || options === void 0 ? void 0 : options.experimental) === null || _e === void 0 ? void 0 : _e.childrenHtml(html, kebabName, json, options); } if ((_f = options === null || options === void 0 ? void 0 : options.experimental) === null || _f === void 0 ? void 0 : _f.cssHtml) { html += (_g = options === null || options === void 0 ? void 0 : options.experimental) === null || _g === void 0 ? void 0 : _g.cssHtml(css); } else if (css.length) { html += `<style>${css}</style>`; } if (options.prettier !== false) { try { html = (0, standalone_1.format)(html, { parser: 'html', htmlWhitespaceSensitivity: 'ignore', plugins: [ // To support running in browsers require('prettier/parser-html'), require('prettier/parser-postcss'), require('prettier/parser-babel'), require('prettier/parser-typescript'), ], }); html = html.trim().replace(/\n/g, '\n '); } catch (err) { console.warn('Could not prettify', { string: html }, err); } } let str = ` ${json.types ? json.types.join('\n') : ''} ${(0, render_imports_1.renderPreComponent)({ explicitImportFileExtension: options.explicitImportFileExtension, component: json, target: 'customElement', })} /** * Usage: * * <${kebabName}></${kebabName}> * */ class ${ComponentName} extends ${((_h = options === null || options === void 0 ? void 0 : options.experimental) === null || _h === void 0 ? void 0 : _h.classExtends) ? (_j = options === null || options === void 0 ? void 0 : options.experimental) === null || _j === void 0 ? void 0 : _j.classExtends(json, options) : 'HTMLElement'} { ${Array.from(domRefs) .map((ref) => { return ` get _${ref}() { return this._root.querySelector("[data-ref='${ComponentName}-${ref}']") } `; }) .join('\n')} get _root() { return this.shadowRoot || this; } constructor() { super(); const self = this; ${ // TODO: more than one context not injector setContext.length === 1 && ((_k = setContext === null || setContext === void 0 ? void 0 : setContext[0]) === null || _k === void 0 ? void 0 : _k.ref) ? `this.context = ${setContext[0].ref}` : ''} ${!((_m = (_l = json.hooks) === null || _l === void 0 ? void 0 : _l.onInit) === null || _m === void 0 ? void 0 : _m.code) ? '' : 'this.onInitOnce = false;'} this.state = ${(0, get_state_object_string_1.getStateObjectStringFromComponent)(json, { valueMapper: (value) => (0, function_1.pipe)((0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(addUpdateAfterSetInCode(value, options, 'self.update'), { includeProps: false, includeState: true, // TODO: if it's an arrow function it's this.state. replaceWith: 'self.state.', }), (newCode) => (0, strip_state_and_props_refs_1.stripStateAndPropsRefs)(newCode, { // TODO: replace with `this.` and add setters that call this.update() includeProps: true, includeState: false, replaceWith: 'self.props.', }), (code) => (0, strip_state_and_props_refs_1.DO_NOT_USE_CONTEXT_VARS_TRANSFORMS)({ code, contextVars, // correctly ref the class not state object context: 'self.', })), })}; if (!this.props) { this.props = {}; } ${!componentHasProps ? '' : ` this.componentProps = [${Array.from(props) .map((prop) => `"${prop}"`) .join(',')}]; `} ${!((_o = json.hooks.onUpdate) === null || _o === void 0 ? void 0 : _o.length) ? '' : ` this.updateDeps = [${(_p = json.hooks.onUpdate) === null || _p === void 0 ? void 0 : _p.map((hook) => updateReferencesInCode((hook === null || hook === void 0 ? void 0 : hook.deps) || '[]', options)).join(',')}]; `} // used to keep track of all nodes created by show/for this.nodesToDestroy = []; // batch updates this.pendingUpdate = false; ${((_q = options === null || options === void 0 ? void 0 : options.experimental) === null || _q === void 0 ? void 0 : _q.componentConstructor) ? (_r = options === null || options === void 0 ? void 0 : options.experimental) === null || _r === void 0 ? void 0 : _r.componentConstructor(json, options) : ''} ${options.js} ${jsRefs .map((ref) => { var _a; // const typeParameter = json['refs'][ref]?.typeParameter || ''; const argument = ((_a = json['refs'][ref]) === null || _a === void 0 ? void 0 : _a.argument) || 'null'; return `this._${ref} = ${argument}`; }) .join('\n')} if (${(_s = json.meta.useMetadata) === null || _s === void 0 ? void 0 : _s.isAttachedToShadowDom}) { this.attachShadow({ mode: 'open' }) } } ${!((_t = json.hooks.onUnMount) === null || _t === void 0 ? void 0 : _t.code) ? '' : ` disconnectedCallback() { ${((_u = options === null || options === void 0 ? void 0 : options.experimental) === null || _u === void 0 ? void 0 : _u.disconnectedCallback) ? (_v = options === null || options === void 0 ? void 0 : options.experimental) === null || _v === void 0 ? void 0 : _v.disconnectedCallback(json, options) : ` // onUnMount ${updateReferencesInCode(addUpdateAfterSetInCode(json.hooks.onUnMount.code, options), options, { contextVars, })} this.destroyAnyNodes(); // clean up nodes when component is destroyed ${!((_x = (_w = json.hooks) === null || _w === void 0 ? void 0 : _w.onInit) === null || _x === void 0 ? void 0 : _x.code) ? '' : 'this.onInitOnce = false;'} `} } `} destroyAnyNodes() { // destroy current view template refs before rendering again this.nodesToDestroy.forEach(el => el.remove()); this.nodesToDestroy = []; } connectedCallback() { ${context.join('\n')} ${!componentHasProps ? '' : ` this.getAttributeNames().forEach((attr) => { const jsVar = attr.replace(/-/g, ''); const regexp = new RegExp(jsVar, 'i'); this.componentProps.forEach(prop => { if (regexp.test(prop)) { const attrValue = this.getAttribute(attr); if (this.props[prop] !== attrValue) { this.props[prop] = attrValue; } } }); }); `} ${((_y = options === null || options === void 0 ? void 0 : options.experimental) === null || _y === void 0 ? void 0 : _y.connectedCallbackUpdate) ? (_z = options === null || options === void 0 ? void 0 : options.experimental) === null || _z === void 0 ? void 0 : _z.connectedCallbackUpdate(json, html, options) : ` this._root.innerHTML = \` ${html}\`; this.pendingUpdate = true; ${!((_1 = (_0 = json.hooks) === null || _0 === void 0 ? void 0 : _0.onInit) === null || _1 === void 0 ? void 0 : _1.code) ? '' : 'this.onInit();'} this.render(); this.onMount(); this.pendingUpdate = false; this.update(); `} } ${!((_3 = (_2 = json.hooks) === null || _2 === void 0 ? void 0 : _2.onInit) === null || _3 === void 0 ? void 0 : _3.code) ? '' : ` onInit() { ${!((_5 = (_4 = json.hooks) === null || _4 === void 0 ? void 0 : _4.onInit) === null || _5 === void 0 ? void 0 : _5.code) ? '' : ` if (!this.onInitOnce) { ${updateReferencesInCode(addUpdateAfterSetInCode((_7 = (_6 = json.hooks) === null || _6 === void 0 ? void 0 : _6.onInit) === null || _7 === void 0 ? void 0 : _7.code, options), options, { contextVars, })} this.onInitOnce = true; }`} } `} ${!hasShow ? '' : ` showContent(el) { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement/content // grabs the content of a node that is between <template> tags // iterates through child nodes to register all content including text elements // attaches the content after the template const elementFragment = el.content.cloneNode(true); const children = Array.from(elementFragment.childNodes) children.forEach(child => { if (el?.scope) { child.scope = el.scope; } if (el?.context) { child.context = el.context; } this.nodesToDestroy.push(child); }); el.after(elementFragment); }`} ${!((_8 = options === null || options === void 0 ? void 0 : options.experimental) === null || _8 === void 0 ? void 0 : _8.attributeChangedCallback) ? '' : ` attributeChangedCallback(name, oldValue, newValue) { ${(_9 = options === null || options === void 0 ? void 0 : options.experimental) === null || _9 === void 0 ? void 0 : _9.attributeChangedCallback(['name', 'oldValue', 'newValue'], json, options)} } `} onMount() { ${!json.hooks.onMount.length ? '' : // TODO: make prettier by grabbing only the function body ` // onMount ${updateReferencesInCode(addUpdateAfterSetInCode((0, on_mount_1.stringifySingleScopeOnMount)(json), options), options, { contextVars, })} `} } onUpdate() { ${!((_10 = json.hooks.onUpdate) === null || _10 === void 0 ? void 0 : _10.length) ? '' : ` const self = this; ${json.hooks.onUpdate.reduce((code, hook, index) => { // create check update if (hook === null || hook === void 0 ? void 0 : hook.deps) { code += ` ;(function (__prev, __next) { const __hasChange = __prev.find((val, index) => val !== __next[index]); if (__hasChange !== undefined) { ${updateReferencesInCode(hook.code, options, { contextVars, context: 'self.', })} self.updateDeps[${index}] = __next; } }(self.updateDeps[${index}], ${updateReferencesInCode((hook === null || hook === void 0 ? void 0 : hook.deps) || '[]', options, { contextVars, context: 'self.', })})); `; } else { code += ` ${updateReferencesInCode(hook.code, options, { contextVars, context: 'self.', })} `; } return code + '\n'; }, '')} `} } update() { if (this.pendingUpdate === true) { return; } this.pendingUpdate = true; this.render(); this.onUpdate(); this.pendingUpdate = false; } render() { ${!componentHasStatefulDom ? '' : ` // grab previous input state const preStateful = this.getStateful(this._root); const preValues = this.prepareHydrate(preStateful); `} // re-rendering needs to ensure that all nodes generated by for/show are refreshed this.destroyAnyNodes(); this.updateBindings(); ${!componentHasStatefulDom ? '' : ` // hydrate input state if (preValues.length) { const nextStateful = this.getStateful(this._root); this.hydrateDom(preValues, nextStateful); } `} } ${!componentHasStatefulDom ? '' : ` getStateful(el) { const stateful = el.querySelectorAll('[data-dom-state]'); return stateful ? Array.from(stateful) : []; } prepareHydrate(stateful) { return stateful.map(el => { return { id: el.dataset.domState, value: el.value, active: document.activeElement === el, selectionStart: el.selectionStart }; }); } hydrateDom(preValues, stateful) { return stateful.map((el, index) => { const prev = preValues.find((prev) => el.dataset.domState === prev.id); if (prev) { el.value = prev.value; if (prev.active) { el.focus(); el.selectionStart = prev.selectionStart; } } }); } `} updateBindings() { ${Object.keys(options.onChangeJsById) .map((key) => { var _a, _b, _c, _d, _e, _f, _g; const value = options.onChangeJsById[key]; if (!value) { return ''; } let code = ''; if ((_a = options === null || options === void 0 ? void 0 : options.experimental) === null || _a === void 0 ? void 0 : _a.updateBindings) { key = (_c = (_b = options === null || options === void 0 ? void 0 : options.experimental) === null || _b === void 0 ? void 0 : _b.updateBindings) === null || _c === void 0 ? void 0 : _c.key(key, value, options); code = (_e = (_d = options === null || options === void 0 ? void 0 : options.experimental) === null || _d === void 0 ? void 0 : _d.updateBindings) === null || _e === void 0 ? void 0 : _e.code(key, value, options); } else { code = updateReferencesInCode(value, options, { contextVars, }); } return ` ${((_f = options === null || options === void 0 ? void 0 : options.experimental) === null || _f === void 0 ? void 0 : _f.generateQuerySelectorAll) ? ` ${(_g = options === null || options === void 0 ? void 0 : options.experimental) === null || _g === void 0 ? void 0 : _g.generateQuerySelectorAll(key, code)} ` : ` this._root.querySelectorAll("[data-el='${key}']").forEach((el) => { ${code} }) `} `; }) .join('\n\n')} } // Helper to render content renderTextNode(el, text) { const textNode = document.createTextNode(text); if (el?.scope) { textNode.scope = el.scope; } if (el?.context) { textNode.context = el.context; } el.after(textNode); this.nodesToDestroy.push(el.nextSibling); } ${!hasContext ? '' : ` // get Context Helper getContext(el, token) { do { let value; if (el?.context?.get) { value = el.context.get(token); } else if (el?.context?.[token]) { value = el.context[token]; } if (value !== undefined) { return value; } } while ((el = el.parentNode)); } `} ${!hasScope ? '' : ` // scope helper getScope(el, name) { do { let value = el?.scope?.[name] if (value !== undefined) { return value } } while ((el = el.parentNode)); } `} ${!hasLoop ? '' : ` // Helper to render loops renderLoop(template, array, itemName, itemIndex, collectionName) { const collection = []; for (let [index, value] of array.entries()) { const elementFragment = template.content.cloneNode(true); const children = Array.from(elementFragment.childNodes) const localScope = {}; let scope = localScope; if (template?.scope) { const getParent = { get(target, prop, receiver) { if (prop in target) { return target[prop]; } if (prop in template.scope) { return template.scope[prop]; } return target[prop]; } }; scope = new P