Skip to content

Commit cbff7f0

Browse files
authored
Await member expr (#337)
Lets you await parts of a member expression.
1 parent 3b1be6c commit cbff7f0

File tree

3 files changed

+129
-8
lines changed

3 files changed

+129
-8
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@alloy-js/typescript"
5+
---
6+
7+
Add `await` prop to MemberExpression.Part to allow awaiting the value of that part.

packages/typescript/src/components/MemberExpression.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ interface PartDescriptorBase {
3535
nullish: boolean;
3636
type: boolean;
3737
args?: Children[];
38+
/**
39+
* Whether to await the value that results from this part of the member expression.
40+
*/
41+
await?: boolean;
3842
}
3943

4044
type PartDescriptor = PartDescriptorWithId | PartDescriptorWithIndex;
@@ -47,7 +51,8 @@ type PartDescriptor = PartDescriptorWithId | PartDescriptorWithIndex;
4751
* * **refkey**: a refkey for a symbol whose name becomes the identifier
4852
* * **symbol**: a symbol whose name becomes the identifier part
4953
* * **args**: create a method call with the given args
50-
* * **children**: arbitrary contents for the identifier part.
54+
* * **children**: arbitrary contents for the identifier part
55+
* * **await**: whether to await the value that results from this part of the member expression
5156
*
5257
* Each part can have a nullish prop, which indicates that the part may be null
5358
* or undefined.
@@ -88,6 +93,10 @@ export function MemberExpression(props: MemberExpressionProps): Children {
8893
}
8994

9095
const isCallChain = computed(() => {
96+
for (const part of parts) {
97+
if (part.await) return false;
98+
}
99+
91100
let callCount = 0;
92101
for (const part of parts) {
93102
if (part.args !== undefined) callCount++;
@@ -247,6 +256,7 @@ function createPartDescriptorFromProps(
247256
(symbolSource.value as TSOutputSymbol).isTypeSymbol
248257
);
249258
}),
259+
await: computed(() => partProps.await),
250260
};
251261

252262
return reactive(part);
@@ -389,27 +399,47 @@ function formatCallChain(parts: PartDescriptor[]): Children {
389399

390400
function formatNonCallChain(parts: PartDescriptor[]): Children {
391401
return computed(() => {
392-
const expression: Children[] = [];
402+
let expression: Children = [];
393403

394404
for (let i = 0; i < parts.length; i++) {
395405
const part = parts[i];
396406
const base = isIdPartDescriptor(part) ? part.id : part.index;
397407
if (i === 0) {
398-
expression.push(base);
408+
(expression as Children[]).push(base);
399409
} else {
400410
// Determine if we should use nullish operator from previous part
401411
const prevPart = parts[i - 1];
412+
let partExpr;
402413

403414
if (prevPart.type) {
404-
expression.push(formatArrayAccess(prevPart, part));
415+
partExpr = formatArrayAccess(prevPart, part);
405416
} else if (part.args !== undefined) {
406417
// For parts with only args (no name), append function call directly with appropriate nullish operator
407-
expression.push(formatCallExpr(prevPart, part));
418+
partExpr = formatCallExpr(prevPart, part);
408419
} else if (part.accessStyle === "dot") {
409-
expression.push(formatDotAccess(prevPart, part));
420+
partExpr = formatDotAccess(prevPart, part);
410421
} else {
411422
// bracket notation - don't include the dot
412-
expression.push(formatArrayAccess(prevPart, part));
423+
partExpr = formatArrayAccess(prevPart, part);
424+
}
425+
426+
if (Array.isArray(expression)) {
427+
expression.push(partExpr);
428+
} else {
429+
expression = (
430+
<>
431+
{expression}
432+
{partExpr}
433+
</>
434+
);
435+
}
436+
}
437+
438+
if (part.await) {
439+
if (i < parts.length - 1) {
440+
expression = <>(await {expression})</>;
441+
} else {
442+
expression = <>await {expression}</>;
413443
}
414444
}
415445
}
@@ -524,6 +554,11 @@ export interface MemberExpressionPartProps {
524554
*/
525555
args?: Children[] | boolean;
526556

557+
/**
558+
* Whether to await the value that results from this part of the member expression.
559+
*/
560+
await?: boolean;
561+
527562
/**
528563
* This part is nullish. Subsequent parts use conditional access operators.
529564
*/
@@ -543,7 +578,8 @@ export interface MemberExpressionPartProps {
543578
* * **refkey**: A refkey for a symbol whose name becomes the identifier
544579
* * **symbol**: a symbol whose name becomes the identifier part
545580
* * **args**: create a method call with the given args
546-
* * **children**: arbitrary contents for the identifier part.
581+
* * **children**: arbitrary contents for the identifier part
582+
* * **await**: whether to await the value that results from this part of the member expression
547583
*
548584
* Each part can have a nullish prop, which indicates that the part may be null
549585
* or undefined.

packages/typescript/test/member-expression.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,3 +760,81 @@ describe("formatting", () => {
760760
});
761761
});
762762
});
763+
764+
describe("with await", () => {
765+
it("renders member expression with await", () => {
766+
expect(
767+
toSourceText(
768+
<MemberExpression>
769+
<MemberExpression.Part id="foo" />
770+
<MemberExpression.Part id="bar" />
771+
<MemberExpression.Part args await />
772+
<MemberExpression.Part id="baz" />
773+
</MemberExpression>,
774+
),
775+
).toBe(d`
776+
(await foo.bar()).baz
777+
`);
778+
});
779+
780+
it("renders member expression with await at the end", () => {
781+
expect(
782+
toSourceText(
783+
<MemberExpression>
784+
<MemberExpression.Part id="foo" />
785+
<MemberExpression.Part id="bar" />
786+
<MemberExpression.Part args await />
787+
</MemberExpression>,
788+
),
789+
).toBe(d`
790+
await foo.bar()
791+
`);
792+
});
793+
794+
it("renders member expression with await in the middle of property access", () => {
795+
expect(
796+
toSourceText(
797+
<MemberExpression>
798+
<MemberExpression.Part id="foo" />
799+
<MemberExpression.Part id="bar" await />
800+
<MemberExpression.Part id="baz" />
801+
</MemberExpression>,
802+
),
803+
).toBe(d`
804+
(await foo.bar).baz
805+
`);
806+
});
807+
808+
it("renders member expression with multiple awaits", () => {
809+
expect(
810+
toSourceText(
811+
<MemberExpression>
812+
<MemberExpression.Part id="foo" />
813+
<MemberExpression.Part id="bar" await />
814+
<MemberExpression.Part id="baz" await />
815+
<MemberExpression.Part id="qux" />
816+
</MemberExpression>,
817+
),
818+
).toBe(d`
819+
(await (await foo.bar).baz).qux
820+
`);
821+
});
822+
823+
it("renders member expression with await and call chain (disables formatting)", () => {
824+
expect(
825+
toSourceText(
826+
<MemberExpression>
827+
<MemberExpression.Part id="foo" />
828+
<MemberExpression.Part id="bar" />
829+
<MemberExpression.Part args />
830+
<MemberExpression.Part id="baz" />
831+
<MemberExpression.Part args await />
832+
<MemberExpression.Part id="qux" />
833+
<MemberExpression.Part args />
834+
</MemberExpression>,
835+
),
836+
).toBe(d`
837+
(await foo.bar().baz()).qux()
838+
`);
839+
});
840+
});

0 commit comments

Comments
 (0)