Skip to content

Conversation

@rax-it
Copy link
Contributor

@rax-it rax-it commented Oct 21, 2025

Details

This change introduces a transformOption: experimentalErrorRecoveryMode which also sets babel parsers errorRecovery flag.
This changes lwc compilers, specifically babel-plugin-component behavior from

Throw at FIRST Error --> Collect as MANY errors as we can in a single invocation

While in theory we might still get errors in subsequent compile requests as some validation paths were skipped because we couldn't continue in a meaningful way.

These errors are then put in-to a bag of errors, {metadata: { lwcErrors: CompilerError[]}}, upon completion of the transform process our transformer looks for presence of these errors and throws an AggregateError.

Currently, only scriptTransformer is updated to utilize this behavior with other transformers (html, css) to follow suit.
experimentalErrorRecoveryMode defaults to false hence there should be no observable changes in current compilation scenarios.

Does this pull request introduce a breaking change?

  • 😮‍💨 No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • 🤞 No, it does not introduce an observable change.

GUS work item

W-19992686

@rax-it rax-it marked this pull request as ready for review October 23, 2025 19:56
@rax-it rax-it requested a review from a team as a code owner October 23, 2025 19:56

expect(() => {
transformSync(actual, 'foo.js', TRANSFORM_OPTIONS);
}).toThrowAggregateError(['LWC1107: Invalid property name "dataInvalidProperty".']);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}).toThrowAggregateError(['LWC1107: Invalid property name "dataInvalidProperty".']);
}).toThrow(aggregatErrorMatcher(['LWC1107: Invalid property name "dataInvalidProperty".']));

We can use asymmetric matchers to avoid having to a whole new thing.

function aggregateErrorMatcher(messages: string[]) {
    return expect.objectContaining({
        name: 'AggregateError',
        message: 'Multiple errors occurred during compilation.',
        errors: messages.map((msg) => {
            return expect.objectContaining({
                name: 'Error',
                message: expect.stringContaining(msg),
            });
        }),
    });
}

Copy link
Contributor

@jye-sf jye-sf left a comment

Choose a reason for hiding this comment

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

What's the reasoning for putting this as its own separate lwcErrors vs the errors parameter?

name = (expressionPath.node.callee as types.V8IntrinsicIdentifier).name as LwcDecoratorName;
} else {
throw generateInvalidDecoratorError(decoratorPath, state);
//TODO: Add a fallback assignment
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify what you mean by fallback assignment here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My bad, I missed to address this todo, updated!

},
})!;

const lwcErrors = extractLwcErrors(result);
Copy link
Contributor

Choose a reason for hiding this comment

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

See question above about this being a separate function. Feels like you could combine the code below with the code in the function and get cleaner conditional blocks?

Copy link
Contributor

Choose a reason for hiding this comment

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

Potentially even combine the code after the try/catch block here too.

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 think I was overthinking this a bit, simplified and updated

const lwcErrors = extractLwcErrors(result);

if (!experimentalErrorRecoveryMode || lwcErrors.length === 0) {
return {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I personally prefer having the return statement at the end even if it means result isn't a const. It forces a cleaner separation between the 'successful' codepath and the 'error' code paths, and doesn't hide the main successful return value in the middle.

errors.push(...lwcErrors.map((diagnostic) => CompilerError.from(diagnostic)));
} catch (e) {
// If we are here in errorRecoveryMode then it's most likely that we have run into
// an unforeseen error
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean we don't want to try to aggregate any 'thrown' errors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should only happen when babel.transformSync call throws, compiler-plugin errors should not cause the compilation itself to fail. This will be an instance of Error and we don't have our "lwcErrors" bag of errors , hence nothing to Aggregate this error to.

This also means that when running in errorRecoveryMode compiler can now throw 2 types of errors CompilerError and an AggregateError but I think that might me fine, unless we need to converge them to a single error type for some reason.
Let me know if you have any thoughts here?

): types.ObjectProperty[] {
const properties = wireConfig.get('properties');

// Should only occurs in error recovery mode when config validation has already failed
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Should only occurs in error recovery mode when config validation has already failed
// Should only occur in error recovery mode when config validation has already failed

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, what does this mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This essentially means that we have reached a path that we shouldn't have reached given the existing constraints/validations in place.

Ex: we reach wire.transfrom only when we have successfully gathered metadata, now say if we failed to identify the decorator type, instead of throwing we just log that error and continue to parse but this causes an issue here where we are expecting wireConfig.get('properties') to be an Array but instead it could be anything like string, undefined.

So to prevent compilation from breaking unexpectedly we just return an skip processing these props in errorRecoveryMode since we know this path already has known issues and is going to fail.

@rax-it
Copy link
Contributor Author

rax-it commented Oct 29, 2025

What's the reasoning for putting this as its own separate lwcErrors vs the errors parameter?

Metadata that gets passed around in babel doesn't have any keys , I just gave it an explicit name to avoid any conflict/confusion if we update metadata to have extra key/values :)

@rax-it rax-it requested a review from jye-sf October 29, 2025 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants