Skip to content
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: support async plugin's pre/post #16862

Merged
merged 3 commits into from
Oct 24, 2024

Conversation

timofei-iatsenko
Copy link
Contributor

@timofei-iatsenko timofei-iatsenko commented Sep 26, 2024

Q                       A
Fixed Issues? Fixes #16860
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? Yes
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes? No
License MIT

Enable support of async pre/post functions in plugins + update tests. As discussed here #16860

I'm also wondering of adding a isAsync flag for pre/post functions arguments so plugins could adjust it's own logic based on that. However i don't know how exactly to shape this signature, my proposition is:

  pre?: (this: S, file: File, ctx: {isAsync: boolean}) => void | Promise<void>;
  post?: (this: S, file: File, ctx: {isAsync: boolean}) => void | Promise<void>;

Let me know what you think, maybe there is a better place for that.

Comment on lines 6 to +7
await wait(50);
this.file.ast.program.body[0].value = "success"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking that promise is actually awaited

@babel-bot
Copy link
Collaborator

babel-bot commented Sep 26, 2024

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/58233

@nicolo-ribaudo nicolo-ribaudo added the PR: New Feature 🚀 A type of pull request used for our changelog categories label Sep 30, 2024
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good -- can you also ad a test with an async pre, which transforms the AST in such a way that makes sure that it's being awaited before running the visitors?

@nicolo-ribaudo nicolo-ribaudo added this to the v7.26.0 milestone Sep 30, 2024
@timofei-iatsenko
Copy link
Contributor Author

@nicolo-ribaudo there is one, it reassigns variable in the closure. If pre would not be awaited, visitor will print "failure" instead of "success".

I also have question regarding typings. Should we update typings of pre/post to reflect that they are supporting Async result?
I tried to change them, but this caused a lot of TS errors, and i honestly don't know how to solve them properly in this codebase without big changes.

@nicolo-ribaudo
Copy link
Member

I also have question regarding typings. Should we update typings of pre/post to reflect that they are supporting Async result?
I tried to change them, but this caused a lot of TS errors, and i honestly don't know how to solve them properly in this codebase without big changes.

I tried changing them in packages/babel-core/src/config/validation/plugins.ts, and the type failure actually uncovered a bug!

Where we do chain(inherits.pre, plugin.pre), we are calling the two pre functions one after the other without awaiting if the first one is async. I think something like this should work (but I didn't test it):

function chainMaybeAsync<Args extends any[]>(
  a: undefined | ((...args: Args) => void | Promise<void>),
  b: undefined | ((...args: Args) => void | Promise<void>),
) {
  if (!a) return b;
  if (!b) return a;

  return function (this: unknown, ...args: Args) {
    const res = a.apply(this, args);
    if (res != null && typeof (res as Promise<void>).then === "function") {
      return (res as Promise<void>).then(() => b.apply(this, args));
    }
    return b.apply(this, args);
  };
}

You can add a test with a plugin A that has plugin B in its inherits, plugin B's pre is async, and plugin A's pre must run after it.

@timofei-iatsenko
Copy link
Contributor Author

@nicolo-ribaudo i changed types for pre/post and fixed chaining. Also added two more tests for pre/post with inheritance.

The pipelines are failing for unknown reason. Could you check what's wrong?

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just needed a rebase.

I left a minor comment, but this looks good now!

return {
inherits: pluginC,
async post() {
await wait(50);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make pluginC async and keep pluginB sync? So we have async-sync-async, testing both combinations :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, test for pre uses sync-async-async and test for post uses async-sync-async.

@timofei-iatsenko
Copy link
Contributor Author

@nicolo-ribaudo what about passing isAsync to the plugin's authors? It could be passed directly to the pre/post function as a parameter:

pre?: (this: S, file: File, ctx: {isAsync: boolean}) => void | Promise<void>;
post?: (this: S, file: File, ctx: {isAsync: boolean}) => void | Promise<void>;

Or might be as a property on a PluginPass object?

I'm afraid it would be a bit tricky to implement no-breaking logic for the plugins without that flag.

@nicolo-ribaudo
Copy link
Member

Whops sorry, I missed the question. I would prefer to expose it on PluginPass.

@timofei-iatsenko
Copy link
Contributor Author

@nicolo-ribaudo i've added a new property isAsync on a BabelPass object. Corresponding tests for the functionality added.

Comment on lines +10 to 17
/**
* The working directory that Babel's programmatic options are loaded
* relative to.
*/
cwd: string;

// The absolute path of the file being compiled.
/** The absolute path of the file being compiled. */
filename: string | void;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With /** style of comment it's a jsdoc description which could be used as quick documentation in IDE.

file: File,
key?: string | null,
options?: Options,
isAsync?: boolean,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've added isAsync as optional argument to the last position, so it shouldn't be a breaking change to anyone who uses PluginPass constructor directly. However, i would like to add it as required argument after the file. Let me know if that change is acceptable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only export PluginPass as a type and not as a constructor (see packages/babel-core/src/index.ts), so it should be fine to add it as a required property here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you can just remove the ?s from the other arguments since they are always always passed. It's probably a leftover from when we converted the repo from Flow to TS.

@@ -308,7 +338,7 @@ describe("asynchronicity", () => {
});

describe("misc", () => {
it("unknown preset in config file does not trigget unhandledRejection if caught", async () => {
it("unknown preset in config file does not trigger unhandledRejection if caught", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed a typo

@nicolo-ribaudo nicolo-ribaudo added the PR: Needs Review A pull request awaiting more approvals label Oct 23, 2024
fn.apply(this, args);
function chainMaybeAsync<Args extends any[], R extends void | Promise<void>>(
a: undefined | ((...args: Args) => R),
b: undefined | ((...args: Args) => R),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we assume a and b share the same return type, however, it is also possible that a is sync while b is async. I suggest we remove the R constraint and type them as (...args: Args) => void | Promise<void>. We can probably get rid of the final type cast as well.

For situations where both a and b are sync, we can add a type overload so that the chained function is still sync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current typing is fine:

  • if a and b are sync/undefined, R is void and thus chainMaybeAsync returns (...args: Args) => void.
  • if a is sync/undefined and b is async/undefined, R is void | Promise<void> and thus chainMaybeAsync returns (...args: Args) => void | Promise<void>. Note that the void | is correct, because if b is undefined then chainMaybeAsync returns a which returns void.
  • if a and b are async/undefined, R is Promise<void> and thus chainMaybeAsync returns (...args: Args) => Promise<void>.

Also note that this function is only called when a and b are either both sync, or both void | Promise<void>. We don't use it in mixed-type cases.

Also, a problem with a type overload is that every function is assignable to () => void, so it would also cover the case returning a promise:
https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXzAAsotUBZKATwCMQBBAZ0vQB46YBzB+EADwxCpg3KKkoBtALoA+ABQAoePCgAueGlCJSIYPAA+8WbIB0p2FzXsuASngBeafABuOLMGsAaRfGpqNILVQdfUMTM04GSwjbB2dXdy9rNTDjc0j4KwYYxxc3AG55UEhYBBR0bDwCYlIKGnomVkyefkFhZTEpOW9VdSEA7V0DI1NUiKibexz4kIAFGBwAWywGEBZc4GlPb19ezQGQ4fCLDOjJuLdZ+aWVtfjNxOSRtPGss-XLxeXV9ekC+ULwNA4PAICAMEgAIzJbLnQbwOafG4-P6g8Fwbh2KokchUWiMZhgWSICEeSHWArwAD0lIAegB+IA

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the mixed-type cases, for example, if inherits.pre is a sync function and plugin.pre is an async function, then the merged pre will be an async function.

Like you said, every function is assignable to () => void, so the type checking can't help very much here. I am fine with leaving it as-is.

@nicolo-ribaudo nicolo-ribaudo removed the PR: Needs Review A pull request awaiting more approvals label Oct 24, 2024
@nicolo-ribaudo nicolo-ribaudo merged commit 0216853 into babel:main Oct 24, 2024
54 checks passed
@timofei-iatsenko
Copy link
Contributor Author

Thanks for your reviews and helps on this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: core PR: New Feature 🚀 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support async pre/post in plugins
4 participants