diff --git a/.changeset/great-phones-exist.md b/.changeset/great-phones-exist.md
new file mode 100644
index 0000000000..39f826e2df
--- /dev/null
+++ b/.changeset/great-phones-exist.md
@@ -0,0 +1,5 @@
+---
+'@builder.io/mitosis': patch
+---
+
+respect async with anonymous arrow function in state
diff --git a/packages/core/src/__tests__/__snapshots__/alpine.test.ts.snap b/packages/core/src/__tests__/__snapshots__/alpine.test.ts.snap
index 6789407d19..2f6f8d96c9 100644
--- a/packages/core/src/__tests__/__snapshots__/alpine.test.ts.snap
+++ b/packages/core/src/__tests__/__snapshots__/alpine.test.ts.snap
@@ -2629,6 +2629,26 @@ exports[`Alpine.js > jsx > Javascript Test > spreadProps 1`] = `
"
`;
+exports[`Alpine.js > jsx > Javascript Test > store-async-function 1`] = `
+"
+
+"
+`;
+
exports[`Alpine.js > jsx > Javascript Test > string-literal-store 1`] = `
"
+"
+`;
+
exports[`Alpine.js > jsx > Typescript Test > string-literal-store 1`] = `
"
+
+"
+`;
+
exports[`Svelte > jsx > Javascript Test > string-literal-store 1`] = `
"
+
+"
+`;
+
exports[`Svelte > jsx > Typescript Test > string-literal-store 1`] = `
""
+`;
+
exports[`Vue > jsx > Javascript Test > string-literal-store 1`] = `
"
{{ foo }}
@@ -6375,6 +6393,24 @@ exports[`Vue > jsx > Typescript Test > spreadProps 1`] = `
"
`;
+exports[`Vue > jsx > Typescript Test > store-async-function 1`] = `
+"
+
+
+
+"
+`;
+
exports[`Vue > jsx > Typescript Test > string-literal-store 1`] = `
"
{{ foo }}
diff --git a/packages/core/src/__tests__/__snapshots__/vue.test.ts.snap b/packages/core/src/__tests__/__snapshots__/vue.test.ts.snap
index 63ddbf3a48..22a0078619 100644
--- a/packages/core/src/__tests__/__snapshots__/vue.test.ts.snap
+++ b/packages/core/src/__tests__/__snapshots__/vue.test.ts.snap
@@ -3573,6 +3573,32 @@ export default defineComponent({
"
`;
+exports[`Vue > jsx > Javascript Test > store-async-function 1`] = `
+"
+
+
+
+"
+`;
+
exports[`Vue > jsx > Javascript Test > string-literal-store 1`] = `
"
{{ foo }}
@@ -7938,6 +7964,32 @@ export default defineComponent({
"
`;
+exports[`Vue > jsx > Typescript Test > store-async-function 1`] = `
+"
+
+
+
+"
+`;
+
exports[`Vue > jsx > Typescript Test > string-literal-store 1`] = `
"
{{ foo }}
diff --git a/packages/core/src/__tests__/__snapshots__/webcomponent.test.ts.snap b/packages/core/src/__tests__/__snapshots__/webcomponent.test.ts.snap
index cb68f5aa59..96b21c8b83 100644
--- a/packages/core/src/__tests__/__snapshots__/webcomponent.test.ts.snap
+++ b/packages/core/src/__tests__/__snapshots__/webcomponent.test.ts.snap
@@ -14324,6 +14324,104 @@ customElements.define(\\"my-basic-component\\", MyBasicComponent);
"
`;
+exports[`webcomponent > jsx > Javascript Test > store-async-function 1`] = `
+"/**
+ * Usage:
+ *
+ *
+ *
+ */
+class StringLiteralStore extends HTMLElement {
+ get _root() {
+ return this.shadowRoot || this;
+ }
+
+ constructor() {
+ super();
+ const self = this;
+
+ this.state = {
+ arrowFunction: async function arrowFunction() {
+ return Promise.resolve();
+ },
+ namedFunction: async function namedFunction() {
+ return Promise.resolve();
+ },
+ fetchUsers: async function fetchUsers() {
+ return Promise.resolve();
+ },
+ };
+ if (!this.props) {
+ this.props = {};
+ }
+
+ // used to keep track of all nodes created by show/for
+ this.nodesToDestroy = [];
+ // batch updates
+ this.pendingUpdate = false;
+
+ if (undefined) {
+ this.attachShadow({ mode: \\"open\\" });
+ }
+ }
+
+ destroyAnyNodes() {
+ // destroy current view template refs before rendering again
+ this.nodesToDestroy.forEach((el) => el.remove());
+ this.nodesToDestroy = [];
+ }
+
+ connectedCallback() {
+ this._root.innerHTML = \`
+ \`;
+ this.pendingUpdate = true;
+
+ this.render();
+ this.onMount();
+ this.pendingUpdate = false;
+ this.update();
+ }
+
+ onMount() {}
+
+ onUpdate() {}
+
+ update() {
+ if (this.pendingUpdate === true) {
+ return;
+ }
+ this.pendingUpdate = true;
+ this.render();
+ this.onUpdate();
+ this.pendingUpdate = false;
+ }
+
+ render() {
+ // re-rendering needs to ensure that all nodes generated by for/show are refreshed
+ this.destroyAnyNodes();
+ this.updateBindings();
+ }
+
+ updateBindings() {}
+
+ // 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);
+ }
+}
+
+customElements.define(\\"string-literal-store\\", StringLiteralStore);
+"
+`;
+
exports[`webcomponent > jsx > Javascript Test > string-literal-store 1`] = `
"/**
* Usage:
@@ -30090,6 +30188,104 @@ customElements.define(\\"my-basic-component\\", MyBasicComponent);
"
`;
+exports[`webcomponent > jsx > Typescript Test > store-async-function 1`] = `
+"/**
+ * Usage:
+ *
+ *
+ *
+ */
+class StringLiteralStore extends HTMLElement {
+ get _root() {
+ return this.shadowRoot || this;
+ }
+
+ constructor() {
+ super();
+ const self = this;
+
+ this.state = {
+ arrowFunction: async function arrowFunction() {
+ return Promise.resolve();
+ },
+ namedFunction: async function namedFunction() {
+ return Promise.resolve();
+ },
+ fetchUsers: async function fetchUsers() {
+ return Promise.resolve();
+ },
+ };
+ if (!this.props) {
+ this.props = {};
+ }
+
+ // used to keep track of all nodes created by show/for
+ this.nodesToDestroy = [];
+ // batch updates
+ this.pendingUpdate = false;
+
+ if (undefined) {
+ this.attachShadow({ mode: \\"open\\" });
+ }
+ }
+
+ destroyAnyNodes() {
+ // destroy current view template refs before rendering again
+ this.nodesToDestroy.forEach((el) => el.remove());
+ this.nodesToDestroy = [];
+ }
+
+ connectedCallback() {
+ this._root.innerHTML = \`
+ \`;
+ this.pendingUpdate = true;
+
+ this.render();
+ this.onMount();
+ this.pendingUpdate = false;
+ this.update();
+ }
+
+ onMount() {}
+
+ onUpdate() {}
+
+ update() {
+ if (this.pendingUpdate === true) {
+ return;
+ }
+ this.pendingUpdate = true;
+ this.render();
+ this.onUpdate();
+ this.pendingUpdate = false;
+ }
+
+ render() {
+ // re-rendering needs to ensure that all nodes generated by for/show are refreshed
+ this.destroyAnyNodes();
+ this.updateBindings();
+ }
+
+ updateBindings() {}
+
+ // 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);
+ }
+}
+
+customElements.define(\\"string-literal-store\\", StringLiteralStore);
+"
+`;
+
exports[`webcomponent > jsx > Typescript Test > string-literal-store 1`] = `
"/**
* Usage:
diff --git a/packages/core/src/__tests__/data/store-async-function.raw.tsx b/packages/core/src/__tests__/data/store-async-function.raw.tsx
new file mode 100644
index 0000000000..7e4abb2cbe
--- /dev/null
+++ b/packages/core/src/__tests__/data/store-async-function.raw.tsx
@@ -0,0 +1,17 @@
+import { useStore } from '@builder.io/mitosis';
+
+export default function StringLiteralStore() {
+ const state = useStore({
+ arrowFunction: async () => {
+ return Promise.resolve();
+ },
+ namedFunction: async function namedFunction() {
+ return Promise.resolve();
+ },
+ async fetchUsers() {
+ return Promise.resolve();
+ },
+ });
+
+ return ;
+}
diff --git a/packages/core/src/__tests__/test-generator.ts b/packages/core/src/__tests__/test-generator.ts
index 22cf9a8d59..9d73d956e3 100644
--- a/packages/core/src/__tests__/test-generator.ts
+++ b/packages/core/src/__tests__/test-generator.ts
@@ -130,6 +130,7 @@ const inputToTextInputRN = getRawFile('./data/react-native/text-input.raw.tsx');
const StringLiteralStore = getRawFile('./data/string-literal-store.raw.tsx');
const StringLiteralStoreKebab = getRawFile('./data/string-literal-store-kebab.raw.tsx');
+const StoreAsyncFunction = getRawFile('./data/store-async-function.raw.tsx');
/**
* Use TestsWithFailFor when you want to write a test that you know will fail
@@ -246,6 +247,7 @@ const BASIC_TESTS: Tests = {
useTarget,
signalsOnUpdate,
getterState,
+ 'store-async-function': StoreAsyncFunction,
'string-literal-store': StringLiteralStore,
'string-literal-store-kebab': {
file: StringLiteralStoreKebab,
diff --git a/packages/core/src/generators/qwik/helpers/state.ts b/packages/core/src/generators/qwik/helpers/state.ts
index f64ddb171f..7909a7053a 100644
--- a/packages/core/src/generators/qwik/helpers/state.ts
+++ b/packages/core/src/generators/qwik/helpers/state.ts
@@ -68,21 +68,24 @@ function emitStateMethods(
case 'method':
case 'function':
let code = stateValue.code;
- let prefixIdx = 0;
- if (stateValue.type === 'function') {
- prefixIdx += 'function '.length;
+ const isAsync = code.startsWith('async');
+ if (!isAsync) {
+ let prefixIdx = 0;
+ if (stateValue.type === 'function') {
+ prefixIdx += 'function '.length;
+ }
+ code = code.substring(prefixIdx);
+ code = convertMethodToFunction(code, methodMap, lexicalArgs).replace(
+ '(',
+ `(${lexicalArgs.join(',')},`,
+ );
}
- code = code.substring(prefixIdx);
- code = convertMethodToFunction(code, methodMap, lexicalArgs).replace(
- '(',
- `(${lexicalArgs.join(',')},`,
- );
const functionName = code.split(/\(/)[0];
if (!file.options.isTypeScript) {
// Erase type information
code = convertTypeScriptToJS(code);
}
- file.exportConst(functionName, 'function ' + code, true);
+ file.exportConst(isAsync ? key : functionName, isAsync ? code : 'function ' + code, true);
continue;
case 'property':
diff --git a/packages/core/src/generators/react/generator.ts b/packages/core/src/generators/react/generator.ts
index 6f69f74f00..b4ea1a5065 100644
--- a/packages/core/src/generators/react/generator.ts
+++ b/packages/core/src/generators/react/generator.ts
@@ -447,7 +447,8 @@ const _componentToReact = (
keyPrefix: 'const',
valueMapper: (code, type, _, key) => {
if (type === 'getter') return `${key} = function ${code.replace('get ', '')}`;
- if (type === 'function') return `${key} = function ${code}`;
+ if (type === 'function')
+ return code.startsWith('async') ? code : `${key} = function ${code}`;
return code;
},
})
diff --git a/packages/core/src/parsers/jsx/state.ts b/packages/core/src/parsers/jsx/state.ts
index c92dcfb722..d9f9a0fcdc 100644
--- a/packages/core/src/parsers/jsx/state.ts
+++ b/packages/core/src/parsers/jsx/state.ts
@@ -12,11 +12,13 @@ import { NodePath } from '@babel/core';
import {
BlockStatement,
Expression,
+ Identifier,
Node,
ObjectExpression,
ObjectMethod,
ObjectProperty,
assignmentExpression,
+ functionExpression,
identifier,
isArrowFunctionExpression,
isDeclaration,
@@ -178,13 +180,40 @@ const processStateObjectSlice = (item: ObjectMethod | ObjectProperty): StateValu
type: 'function',
};
} else if (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 = functionExpression(
+ item.key as Identifier,
+ item.value.params,
+ item.value.body as BlockStatement,
+ false,
+ true,
+ );
+
+ return {
+ code: parseCode(func).trim(),
+ type: 'function',
+ };
+ }
const n = objectMethod(
'method',
item.key as Expression,
item.value.params,
item.value.body as BlockStatement,
);
+
const code = parseCode(n).trim();
+
return {
code: code,
type: 'method',
@@ -206,6 +235,22 @@ const processStateObjectSlice = (item: ObjectMethod | ObjectProperty): StateValu
};
}
} else if (isObjectMethod(item)) {
+ // TODO ENG-7256 Find a way to do this without diverging code path
+ if (item.async) {
+ const func = functionExpression(
+ item.key as Identifier,
+ item.params,
+ item.body as BlockStatement,
+ false,
+ true,
+ );
+
+ return {
+ code: parseCode(func).trim(),
+ type: 'function',
+ };
+ }
+
const n = parseCode({ ...item, returnType: null }).trim();
const isGetter = item.kind === 'get';