Skip to content

Commit a37c754

Browse files
authored
feat(ssr): compiler error location (#5114)
1 parent 47c46eb commit a37c754

File tree

4 files changed

+138
-15
lines changed

4 files changed

+138
-15
lines changed

packages/@lwc/ssr-compiler/src/__tests__/compilation.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
import path from 'node:path';
22
import { describe, test, expect } from 'vitest';
3+
import { CompilerError } from '@lwc/errors';
34
import { compileComponentForSSR } from '../index';
45

6+
expect.addSnapshotSerializer({
7+
test(val) {
8+
return val instanceof CompilerError;
9+
},
10+
serialize(val: CompilerError, config, indentation, depth, refs, printer) {
11+
return printer(
12+
{
13+
message: val.message,
14+
location: val.location,
15+
filename: val.filename,
16+
},
17+
config,
18+
indentation,
19+
depth,
20+
refs
21+
);
22+
},
23+
});
24+
525
describe('component compilation', () => {
626
test('implicit templates imports do not use full file paths', () => {
727
const src = `
@@ -31,4 +51,79 @@ describe('component compilation', () => {
3151
const { code } = compileComponentForSSR(src, filename, {});
3252
expect(code).toContain('import tmpl from "./component.html"');
3353
});
54+
55+
describe('wire decorator', () => {
56+
test('error when using @wire and @track together', () => {
57+
const src = `import { track, wire, LightningElement } from "lwc";
58+
import { getFoo } from "data-service";
59+
export default class Test extends LightningElement {
60+
@track
61+
@wire(getFoo, { key1: "$prop1", key2: ["fixed", "array"] })
62+
wiredWithTrack;
63+
}
64+
`;
65+
expect(() => compileComponentForSSR(src, 'test.js', {}))
66+
.toThrowErrorMatchingInlineSnapshot(`
67+
{
68+
"filename": "test.js",
69+
"location": {
70+
"column": 2,
71+
"length": 59,
72+
"line": 5,
73+
"start": 156,
74+
},
75+
"message": "LWC1095: @wire method or property cannot be used with @track",
76+
}
77+
`);
78+
});
79+
test('throws when wired method is combined with @api', () => {
80+
const src = `import { api, wire, LightningElement } from "lwc";
81+
import { getFoo } from "data-service";
82+
export default class Test extends LightningElement {
83+
@api
84+
@wire(getFoo, { key1: "$prop1", key2: ["fixed"] })
85+
wiredWithApi() {}
86+
}
87+
`;
88+
89+
expect(() => compileComponentForSSR(src, 'test.js', {}))
90+
.toThrowErrorMatchingInlineSnapshot(`
91+
{
92+
"filename": "test.js",
93+
"location": {
94+
"column": 2,
95+
"length": 50,
96+
"line": 5,
97+
"start": 152,
98+
},
99+
"message": "LWC1095: @wire method or property cannot be used with @api",
100+
}
101+
`);
102+
});
103+
test('throws when computed property is expression', () => {
104+
const src = `import { wire, LightningElement } from "lwc";
105+
import { getFoo } from "data-service";
106+
const symbol = Symbol.for("key");
107+
export default class Test extends LightningElement {
108+
// accidentally an array expression = oops!
109+
@wire(getFoo, { [[symbol]]: "$prop1", key2: ["fixed", "array"] })
110+
wiredFoo;
111+
}
112+
`;
113+
114+
expect(() => compileComponentForSSR(src, 'test.js', {}))
115+
.toThrowErrorMatchingInlineSnapshot(`
116+
{
117+
"filename": "test.js",
118+
"location": {
119+
"column": 2,
120+
"length": 9,
121+
"line": 7,
122+
"start": 288,
123+
},
124+
"message": "LWC1200: Computed property in @wire config must be a constant or primitive literal.",
125+
}
126+
`);
127+
});
128+
});
34129
});

packages/@lwc/ssr-compiler/src/compile-js/errors.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* SPDX-License-Identifier: MIT
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
66
*/
7-
import { generateErrorMessage, type LWCErrorInfo } from '@lwc/errors';
7+
import { type LWCErrorInfo, generateCompilerError } from '@lwc/errors';
8+
import type { Node } from 'estree';
89

910
// This type extracts the arguments in a string. Example: "Error {0} {1}" -> [string, string]
1011
type ExtractArguments<
@@ -18,8 +19,23 @@ type ExtractArguments<
1819
: Args; // No `N` found, nothing more to check
1920

2021
export function generateError<const T extends LWCErrorInfo>(
22+
node: Node,
2123
error: T,
22-
...args: ExtractArguments<T['message']>
23-
): Error {
24-
return new Error(generateErrorMessage(error, args));
24+
...messageArgs: ExtractArguments<T['message']>
25+
) {
26+
return generateCompilerError(error, {
27+
messageArgs,
28+
origin: node.loc
29+
? {
30+
filename: node.loc.source || undefined,
31+
location: {
32+
line: node.loc.start.line,
33+
column: node.loc.start.column,
34+
...(node.range
35+
? { start: node.range[0], length: node.range[1] - node.range[0] }
36+
: {}),
37+
},
38+
}
39+
: undefined,
40+
});
2541
}

packages/@lwc/ssr-compiler/src/compile-js/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,20 +225,20 @@ function validateUniqueDecorator(decorators: EsDecorator[]) {
225225

226226
const expressions = decorators.map(({ expression }) => expression);
227227

228-
const hasWire = expressions.some(
228+
const wire = expressions.find(
229229
(expr) => is.callExpression(expr) && is.identifier(expr.callee, { name: 'wire' })
230230
);
231231

232-
const hasApi = expressions.some((expr) => is.identifier(expr, { name: 'api' }));
232+
const api = expressions.find((expr) => is.identifier(expr, { name: 'api' }));
233233

234-
if (hasWire && hasApi) {
235-
throw generateError(DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api');
234+
if (wire && api) {
235+
throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api');
236236
}
237237

238-
const hasTrack = expressions.some((expr) => is.identifier(expr, { name: 'track' }));
238+
const track = expressions.find((expr) => is.identifier(expr, { name: 'track' }));
239239

240-
if ((hasWire || hasApi) && hasTrack) {
241-
throw generateError(DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track');
240+
if (wire && track) {
241+
throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track');
242242
}
243243
}
244244

@@ -252,6 +252,9 @@ export default function compileJS(
252252
let ast = parseModule(src, {
253253
module: true,
254254
next: true,
255+
loc: true,
256+
source: filename,
257+
ranges: true,
255258
}) as EsProgram;
256259

257260
const state: ComponentMetaState = {

packages/@lwc/ssr-compiler/src/compile-js/wire.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function getWireParams(
4444
const { decorators } = node;
4545

4646
if (decorators.length > 1) {
47-
throw generateError(DecoratorErrors.ONE_WIRE_DECORATOR_ALLOWED);
47+
throw generateError(node, DecoratorErrors.ONE_WIRE_DECORATOR_ALLOWED);
4848
}
4949

5050
// validate the parameters
@@ -94,7 +94,10 @@ function validateWireId(
9494

9595
// This is not the exact same validation done in @lwc/babel-plugin-component but it accomplishes the same thing
9696
if (path.scope?.getBinding(wireAdapterVar)?.kind !== 'module') {
97-
throw generateError(DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL);
97+
throw generateError(
98+
path.node!,
99+
DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL
100+
);
98101
}
99102
}
100103

@@ -129,9 +132,15 @@ function validateWireConfig(
129132
continue;
130133
}
131134
} else if (is.templateLiteral(key)) {
132-
throw generateError(DecoratorErrors.COMPUTED_PROPERTY_CANNOT_BE_TEMPLATE_LITERAL);
135+
throw generateError(
136+
path.node!,
137+
DecoratorErrors.COMPUTED_PROPERTY_CANNOT_BE_TEMPLATE_LITERAL
138+
);
133139
}
134-
throw generateError(DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL);
140+
throw generateError(
141+
path.node!,
142+
DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL
143+
);
135144
}
136145
}
137146

0 commit comments

Comments
 (0)