-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(transformer): async code transformation #9889
Conversation
Still very early stage, but already noticing |
Super exciting!
Yeah, I expected that. Almost all the logic will be identical, just cache lookups and the transforms themselves. One thing is that multiple processes will probably want to transform the same files. Not a big deal in sync code, but for async code we'll quickly end up transpiling the same file a bunch of time, wasting a lot of CPU. DOes not have to be solved in this PR, but having some sort of mutex or lock per file name might make sense? |
@@ -390,6 +491,135 @@ export default class ScriptTransformer { | |||
}; | |||
} | |||
|
|||
// TODO: replace third argument with TransformOptions in Jest 26 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we might wanna look into landing all the jest 26 changes first, not sure... Would delay this PR so probably not
Yes that makes sense. A way to implement per file name lock might be shared module map that tracks file name, whether being transpiled, and cached transpile result? |
Yeah, something like that 🙂 Again, not something we need in a first iteration, but something to keep in mind |
Considering the following case: a file is associated with a If code cache is shared between processes in the future, might be able to reduce this kind of error? (When the file was transformed asynchronously and cached before the sync call) |
Yes.
I don't think we should do that, it would make tests passing dependent on execution order (or warm cache), which we don't want |
ca916bf
to
f85e570
Compare
Hi @SimenB, can you take a look and see it the changes are on the right direction? Also, a bit curious why tsc complaints about |
Try a rebase, I've had weird warnings like that before that a rebase took care of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently on mobile, so a bit painful to review. Skimmed over and I like how this is shaping up! 👍
return transformer; | ||
} | ||
|
||
transform = await import(transformPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is to import
ESM we need to add this gile to the blacklist in babel.config.js in the root of the repo.
options: CacheKeyOptions, | ||
) => Promise<string>; | ||
|
||
process: ( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
packages/jest-transform/src/types.ts
Outdated
options?: TransformOptions, | ||
) => TransformedSource; | ||
|
||
processAsync?: ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not optional
3ea5f17
to
d4f36fa
Compare
Regarding tsc Update: there is |
Codecov Report
@@ Coverage Diff @@
## master #9889 +/- ##
==========================================
+ Coverage 64.14% 64.31% +0.16%
==========================================
Files 307 307
Lines 13375 13444 +69
Branches 3260 3281 +21
==========================================
+ Hits 8580 8647 +67
- Misses 4089 4090 +1
- Partials 706 707 +1
Continue to review full report at Codecov.
|
Hi @SimenB:
|
No particular reason. If it works I'm happy to merge a PR migrating |
I was wrong. When we use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finally 😅
I think this looks really good, thanks for sticking with it @ychi! Beyond the nits I've left, I wonder (as mentioned) if we should look at solving #11081 at the same time. We can probably punt on it since it'd just streamline done in #11163loading
transformers and is not related to the async transformation itself.
@ychi can you add a changelog entry and merge in (or rebase, whatever you prefer) master?
content, | ||
options, | ||
); | ||
const sourceMapPath: Config.Path | null = cacheFilePath + '.map'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this cannot be null
- it's a const
and you assign a concatenated string
null + '.map'
"null.map"
The current code sets it to null
if we don't have a source map - that seems sensible. I think that condition is missing here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, that setting to null
statement now happens in _buildTransformResult
. Removed the null
type here (and the one in transformSource
)
@@ -26,14 +26,20 @@ import { | |||
} from 'jest-util'; | |||
import handlePotentialSyntaxError from './enhanceUnexpectedTokenMessage'; | |||
import shouldInstrument from './shouldInstrument'; | |||
// eslint somehow complains "There should be no empty line between import groups" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also if you remove the new newline on line 42?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing that new newline works, thank you!
this._getTransformer(filename) || {}; | ||
let transformerCacheKey = undefined; | ||
|
||
if (transformer && typeof transformer.getCacheKey === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (transformer && typeof transformer.getCacheKey === 'function') { | |
if (typeof transformer?.getCacheKey === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
transformerConfig, | ||
}, | ||
); | ||
} else if (transformer && typeof transformer.getCacheKey === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} else if (transformer && typeof transformer.getCacheKey === 'function') { | |
} else if (typeof transformer?.getCacheKey === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, removed the duplication and using await getCacheKey
now
(await this._getTransformerAsync(filename)) || {}; | ||
let transformerCacheKey = undefined; | ||
|
||
if (transformer && typeof transformer.getCacheKeyAsync === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (transformer && typeof transformer.getCacheKeyAsync === 'function') { | |
if (typeof transformer?.getCacheKeyAsync === 'function') { |
return cached; | ||
} | ||
|
||
let transformer: AsyncTransformer = await import(transformPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should have a mutex here. Just a Map<Config.Path, Promise<void>>
where the Path
is transformPath
and we can await
that above the this._transformCache.get
call. If we get a cache miss, we add a promise to the map there. that way the cache will be populated once.
checking if the map has a promise before await
ing will avoid the extra tick as well.
(make sure the promise we stick in the cache resolve to something empty so we don't keep a reference to something we don't need)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modified, the Promise only gets resolved after cache has been set. A bit curious whether we can make cache and the mutex single source of truth while allowing transformer sync loading to work as-is 🤔
@@ -191,7 +313,7 @@ export default class ScriptTransformer { | |||
return cached; | |||
} | |||
|
|||
let transformer: Transformer = require(transformPath); | |||
let transformer: SyncTransformer = require(transformPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we might need to refactor to load all transformers in some warmup step later so people can write their transformers in ESM (which cannot be require
d). Not sure if we should stick the refactor in here as well... Thoughts?
(To be clear, the transformer itself can be written in ESM but still expose sync transformations. Concrete example: #11081. While that talks about doing the import()
in an async thing before using and the transformer itself being CJS, I think people will expect to be able to write ESM directly)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented this in #11163
if (typeof processed === 'string') { | ||
transformed.code = processed; | ||
} else if (processed != null && typeof processed.code === 'string') { | ||
transformed = processed; | ||
} else { | ||
throw new TypeError( | ||
"Jest: a transform's `process` function must return a string, " + | ||
'or an object with `code` key containing this string.', | ||
'or an object with `code` key containing this string. ' + | ||
"It's `processAsync` function must return that in a promise.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"It's `processAsync` function must return that in a promise.", | |
"It's `processAsync` function must return a Promise resolving to it.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
@@ -371,6 +470,9 @@ export default class ScriptTransformer { | |||
if (map) { | |||
const sourceMapContent = | |||
typeof map === 'string' ? map : JSON.stringify(map); | |||
|
|||
invariant(sourceMapPath, 'We should always have default sourceMapPath'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this can ever not be set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add this because sourceMapPath
might be set to null, making the type Config.Path | null
in this function.
Another approach might be declaring a new local variable for return?
Let me try to think through the mutex part this weekend and see if I can get ahead of the race😂 |
cd91c8d
to
141b722
Compare
@SimenB: feedback addressed and added changelog. |
CHANGELOG.md
Outdated
@@ -2,6 +2,8 @@ | |||
|
|||
### Features | |||
|
|||
-`[jest-transform]` Async code transformations ([#9889](https://github.com/facebook/jest/pull/9889)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be sorted alphabetically
return null; | ||
} | ||
|
||
if (this._transformCacheMutex.has(transformPath)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes in #11163 will make sure all transformers are loaded before starting any transformation at all. So we don't actually need this anymore 😅
I wonder if we should have a mutex on the transformation itself, though? I.e. on the cache key... I'll have a play with that when I test this PR together with the changes in jest-runtime
to make use of it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just remembered we briefly touched on this idea 😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah, there you go! I agree with past me - both in that we should explore it and that we don't necessarily have to do anything about it before landing this PR
OK, landed the other one. Sorry about the conflicts 🙈 I think the changes over there makes the changes in this PR a bit cleaner though, as it centralizes "load transformer", and for async transformation we only need to deal with async cache key and async transformation. Tell me if you want help resolving the conflicts |
@SimenB Some help would be wonderful😆 I am relocating in the coming week, available time might be fragmented. If you (or anyone) wants to chime in, please feel free. Otherwise I can come back to this in mid- to late-March. 🙂 |
Squashed, removed the mutex, and rebased except for the very latest commit. That one is a bit more hairy since we have a bunch of tests which assert we throw error about missing |
6a62fb2
to
7707816
Compare
@ychi I think this came out correctly. The tests were a bit hairy, but I think I got it 😅 |
Deploy preview for jestjs ready! Built without sensitive environment variables with commit fc92ed8 |
I've played with using this in the runtime, and I think we should be good to go 👍 Need to add some docs, but I can do that now. EDIT: actually, docs should come with the change in |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
Support async code transformation as mentioned in #9504
Test plan
Test cases added