Skip to content

Commit be22d4d

Browse files
authored
Merge pull request #536 from bendemboski/wait-for-transform
Support waitFor()-type modifiers in async arrow transform
2 parents dd2fbde + d3e4ee9 commit be22d4d

File tree

2 files changed

+148
-31
lines changed

2 files changed

+148
-31
lines changed

lib/babel-plugin-transform-ember-concurrency-async-tasks.js

+70-30
Original file line numberDiff line numberDiff line change
@@ -78,68 +78,108 @@ const TransformAsyncMethodsIntoGeneratorMethods = {
7878
}
7979

8080
// Thus far, we've established that value is `myTask = task(...)`.
81-
// Now we need to check if the last argument is an async ArrowFunctionExpress
81+
// Now we need to check if the last argument is an async ArrowFunctionExpression,
82+
// possibly wrapped in other modifier functions such as `waitFor()`
8283

83-
const maybeAsyncArrowPath = path.get(
84+
// If there are modifier functions applied, this will capture the
85+
// top-level one
86+
let rootModifierPath;
87+
88+
let maybeAsyncArrowPath = path.get(
8489
`value.arguments.${value.arguments.length - 1}`
8590
);
86-
if (!maybeAsyncArrowPath && !maybeAsyncArrowPath.node) {
87-
return;
88-
}
89-
const maybeAsyncArrow = maybeAsyncArrowPath.node;
90-
if (
91-
maybeAsyncArrow &&
92-
maybeAsyncArrow.type === 'ArrowFunctionExpression' &&
93-
maybeAsyncArrow.async
94-
) {
95-
convertFunctionExpressionIntoGenerator(
96-
maybeAsyncArrowPath,
97-
state,
98-
factoryFunctionName
99-
);
91+
while (maybeAsyncArrowPath && maybeAsyncArrowPath.node) {
92+
const maybeAsyncArrow = maybeAsyncArrowPath.node;
93+
94+
if (
95+
maybeAsyncArrow.type === 'ArrowFunctionExpression' &&
96+
maybeAsyncArrow.async
97+
) {
98+
// It's an async arrow function, so convert it
99+
convertFunctionExpressionIntoGenerator(
100+
maybeAsyncArrowPath,
101+
rootModifierPath,
102+
state,
103+
factoryFunctionName
104+
);
105+
break;
106+
} else if (maybeAsyncArrow.type === 'CallExpression') {
107+
// It's a call expression, so save it as the modifier functions root
108+
// if we don't already have one and then traverse into it
109+
rootModifierPath = rootModifierPath || maybeAsyncArrowPath;
110+
maybeAsyncArrowPath = maybeAsyncArrowPath.get('arguments.0');
111+
} else {
112+
break;
113+
}
100114
}
101115
}
102116
},
103117
};
104118

105119
function convertFunctionExpressionIntoGenerator(
106-
path,
120+
taskFnPath,
121+
rootModifierPath,
107122
state,
108123
factoryFunctionName
109124
) {
110-
if (path && path.node.async) {
111-
if (isArrowFunctionExpression(path)) {
125+
if (taskFnPath && taskFnPath.node.async) {
126+
if (isArrowFunctionExpression(taskFnPath)) {
112127
// At this point we have something that looks like
113128
//
114129
// foo = task(this?, {}?, async () => {})
115130
//
131+
// or (if there are modifier functions applied)
132+
//
133+
// foo = task(this?, {}?, modifier1(modifier2(async () => {})))
134+
//
116135
// and we need to convert it to
117136
//
118137
// foo = buildTask(contextFn, options | null, taskName, bufferPolicyName?)
119138
//
120139
// where conextFn is
121140
//
122141
// () => ({ context: this, generator: function * () { ... } })
142+
//
143+
// or (if there are modifier functions applied)
144+
//
145+
// () => ({ context: this, generator: modifier1(modifier2(function * () { ... } })))
146+
147+
// Before we start moving things around, let's save off the task()
148+
// CallExpression path
149+
const taskPath = (rootModifierPath || taskFnPath).parentPath;
123150

124-
// Replace the async arrow fn with a generator fn
125-
let asyncArrowFnBody = path.node.body;
151+
// Transform the async arrow task function into a generator function
152+
// (we'll do the actual transformation of `await`s into `yield`s below)
153+
let asyncArrowFnBody = taskFnPath.node.body;
126154
if (asyncArrowFnBody.type !== 'BlockStatement') {
127155
// Need to convert `async () => expr` with `async () => { return expr }`
128156
asyncArrowFnBody = blockStatement([returnStatement(asyncArrowFnBody)]);
129157
}
130158

131159
const taskGeneratorFn = functionExpression(
132-
path.node.id,
133-
path.node.params,
160+
taskFnPath.node.id,
161+
taskFnPath.node.params,
134162
asyncArrowFnBody,
135163
true
136164
);
137165

166+
// Replace the async arrow task function with the generator function
167+
// in-place in the tree (and update `taskFnPath` to point to the new,
168+
// generator, task function)
169+
taskFnPath = taskFnPath.replaceWith(taskGeneratorFn)[0];
170+
138171
const contextFn = arrowFunctionExpression(
139172
[],
140173
objectExpression([
141174
objectProperty(identifier('context'), thisExpression()),
142-
objectProperty(identifier('generator'), taskGeneratorFn),
175+
objectProperty(
176+
identifier('generator'),
177+
// We've swapped out the task fn for a generator function, possibly
178+
// inside some modifier functions. Now we want to move that whole
179+
// tree, including any modifier functions, into this generator
180+
// property.
181+
(rootModifierPath || taskFnPath).node
182+
),
143183
])
144184
);
145185

@@ -152,15 +192,15 @@ function convertFunctionExpressionIntoGenerator(
152192
);
153193
}
154194

155-
const originalArgs = path.parentPath.node.arguments;
195+
const originalArgs = taskPath.node.arguments;
156196

157197
// task(this, async() => {}) was the original API, but we don't actually
158198
// need the `this` arg (we determine the `this` context from the contextFn async arrow fn)
159199
if (originalArgs[0] && originalArgs[0].type === 'ThisExpression') {
160200
originalArgs.shift();
161201
}
162202

163-
const taskName = extractTaskNameFromClassProperty(path);
203+
const taskName = extractTaskNameFromClassProperty(taskPath);
164204
let optionsOrNull;
165205

166206
// remaining args should either be [options, async () => {}] or [async () => {}]
@@ -192,7 +232,7 @@ function convertFunctionExpressionIntoGenerator(
192232
]
193233
);
194234

195-
let newPath = path.parentPath.replaceWith(buildTaskCall)[0];
235+
let newPath = taskPath.replaceWith(buildTaskCall)[0];
196236
newPath.traverse({
197237
FunctionExpression(path) {
198238
if (!path.node.generator) {
@@ -224,11 +264,11 @@ const TransformAwaitIntoYield = {
224264
* in this method we extract the name from the ClassProperty assignment so that we can pass it in
225265
* to the options hash when constructing the Task.
226266
*
227-
* @param {babel.NodePath<babel.types.ArrowFunctionExpression>} asyncArrowFnPath
267+
* @param {babel.NodePath<babel.types.CallExpression>} taskPath
228268
* @returns {string | null}
229269
*/
230-
function extractTaskNameFromClassProperty(asyncArrowFnPath) {
231-
const maybeClassPropertyPath = asyncArrowFnPath.parentPath.parentPath;
270+
function extractTaskNameFromClassProperty(taskPath) {
271+
const maybeClassPropertyPath = taskPath.parentPath;
232272
if (
233273
maybeClassPropertyPath &&
234274
maybeClassPropertyPath.node.type === 'ClassProperty'

tests/integration/async-arrow-task-test.js

+78-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { module, test } from 'qunit';
22
import { setupRenderingTest } from 'ember-qunit';
33
// eslint-disable-next-line ember/no-computed-properties-in-native-classes
44
import { computed, set } from '@ember/object';
5-
import { click, render, settled } from '@ember/test-helpers';
5+
import {
6+
click,
7+
getSettledState,
8+
render,
9+
settled,
10+
waitUntil,
11+
} from '@ember/test-helpers';
12+
import { waitFor } from '@ember/test-waiters';
613
import { hbs } from 'ember-cli-htmlbars';
714
import {
815
task,
@@ -142,6 +149,76 @@ module('Integration | async-arrow-task', function (hooks) {
142149
await finishTest(assert);
143150
});
144151

152+
test('modifiers', async function (assert) {
153+
let modifier1Called = false;
154+
let modifier2Called = false;
155+
156+
function modifier1(fn) {
157+
return function* (...args) {
158+
modifier1Called = true;
159+
yield* fn(args);
160+
};
161+
}
162+
function modifier2(fn) {
163+
return function* (...args) {
164+
modifier2Called = true;
165+
yield* fn(args);
166+
};
167+
}
168+
169+
this.owner.register(
170+
'component:test-async-arrow-task',
171+
class extends TestComponent {
172+
myTask = task(
173+
this,
174+
modifier1(
175+
modifier2(async (arg) => {
176+
return arg;
177+
})
178+
)
179+
);
180+
}
181+
);
182+
183+
await render(hbs`<TestAsyncArrowTask />`);
184+
await click('button#start');
185+
assert.true(modifier1Called);
186+
assert.true(modifier2Called);
187+
});
188+
189+
test('waitFor modifier', async function (assert) {
190+
assert.expect(9);
191+
192+
let { promise, resolve } = defer();
193+
194+
this.owner.register(
195+
'component:test-async-arrow-task',
196+
class extends TestComponent {
197+
myTask = task(
198+
this,
199+
waitFor(async (arg) => {
200+
set(this, 'resolved', await promise);
201+
assert.strictEqual(this.myTask.name, 'myTask');
202+
return arg;
203+
})
204+
);
205+
}
206+
);
207+
208+
await render(hbs`<TestAsyncArrowTask />`);
209+
click('button#start');
210+
await waitUntil(() => this.element.textContent.includes('Running!'));
211+
212+
assert.true(getSettledState().hasPendingWaiters);
213+
214+
resolve('Wow!');
215+
await settled();
216+
217+
assert.false(getSettledState().hasPendingWaiters);
218+
219+
await finishTest(assert);
220+
});
221+
145222
test('dropTask and other shorthand tasks (with `this` arg)', async function (assert) {
146223
assert.expect(13);
147224

0 commit comments

Comments
 (0)