@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
JavaScript
"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, '"').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(/"/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