From cf1a1b222c485f9dcc235c9ef52aae8bbc67f3b8 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Wed, 8 Jan 2025 17:30:33 -0300 Subject: [PATCH] feat(ssr): compiler error location --- .../src/__tests__/compilation.spec.ts | 95 +++++++++++++++++++ .../ssr-compiler/src/compile-js/errors.ts | 24 ++++- .../@lwc/ssr-compiler/src/compile-js/index.ts | 17 ++-- .../@lwc/ssr-compiler/src/compile-js/wire.ts | 17 +++- 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/__tests__/compilation.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/compilation.spec.ts index 95e75a688b..53c3eccc45 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/compilation.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/compilation.spec.ts @@ -1,7 +1,27 @@ import path from 'node:path'; import { describe, test, expect } from 'vitest'; +import { CompilerError } from '@lwc/errors'; import { compileComponentForSSR } from '../index'; +expect.addSnapshotSerializer({ + test(val) { + return val instanceof CompilerError; + }, + serialize(val: CompilerError, config, indentation, depth, refs, printer) { + return printer( + { + message: val.message, + location: val.location, + filename: val.filename, + }, + config, + indentation, + depth, + refs + ); + }, +}); + describe('component compilation', () => { test('implicit templates imports do not use full file paths', () => { const src = ` @@ -31,4 +51,79 @@ describe('component compilation', () => { const { code } = compileComponentForSSR(src, filename, {}); expect(code).toContain('import tmpl from "./component.html"'); }); + + describe('wire decorator', () => { + test('error when using @wire and @track together', () => { + const src = `import { track, wire, LightningElement } from "lwc"; +import { getFoo } from "data-service"; +export default class Test extends LightningElement { + @track + @wire(getFoo, { key1: "$prop1", key2: ["fixed", "array"] }) + wiredWithTrack; +} +`; + expect(() => compileComponentForSSR(src, 'test.js', {})) + .toThrowErrorMatchingInlineSnapshot(` + { + "filename": "test.js", + "location": { + "column": 2, + "length": 59, + "line": 5, + "start": 156, + }, + "message": "LWC1095: @wire method or property cannot be used with @track", + } + `); + }); + test('throws when wired method is combined with @api', () => { + const src = `import { api, wire, LightningElement } from "lwc"; +import { getFoo } from "data-service"; +export default class Test extends LightningElement { + @api + @wire(getFoo, { key1: "$prop1", key2: ["fixed"] }) + wiredWithApi() {} +} +`; + + expect(() => compileComponentForSSR(src, 'test.js', {})) + .toThrowErrorMatchingInlineSnapshot(` + { + "filename": "test.js", + "location": { + "column": 2, + "length": 50, + "line": 5, + "start": 152, + }, + "message": "LWC1095: @wire method or property cannot be used with @api", + } + `); + }); + test('throws when computed property is expression', () => { + const src = `import { wire, LightningElement } from "lwc"; +import { getFoo } from "data-service"; +const symbol = Symbol.for("key"); +export default class Test extends LightningElement { + // accidentally an array expression = oops! + @wire(getFoo, { [[symbol]]: "$prop1", key2: ["fixed", "array"] }) + wiredFoo; +} +`; + + expect(() => compileComponentForSSR(src, 'test.js', {})) + .toThrowErrorMatchingInlineSnapshot(` + { + "filename": "test.js", + "location": { + "column": 2, + "length": 9, + "line": 7, + "start": 288, + }, + "message": "LWC1200: Computed property in @wire config must be a constant or primitive literal.", + } + `); + }); + }); }); diff --git a/packages/@lwc/ssr-compiler/src/compile-js/errors.ts b/packages/@lwc/ssr-compiler/src/compile-js/errors.ts index 1a57ad5e4b..b63846225c 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/errors.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/errors.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { generateErrorMessage, type LWCErrorInfo } from '@lwc/errors'; +import { type LWCErrorInfo, generateCompilerError } from '@lwc/errors'; +import type { Node } from 'estree'; // This type extracts the arguments in a string. Example: "Error {0} {1}" -> [string, string] type ExtractArguments< @@ -18,8 +19,23 @@ type ExtractArguments< : Args; // No `N` found, nothing more to check export function generateError( + node: Node, error: T, - ...args: ExtractArguments -): Error { - return new Error(generateErrorMessage(error, args)); + ...messageArgs: ExtractArguments +) { + return generateCompilerError(error, { + messageArgs, + origin: node.loc + ? { + filename: node.loc.source || undefined, + location: { + line: node.loc.start.line, + column: node.loc.start.column, + ...(node.range + ? { start: node.range[0], length: node.range[1] - node.range[0] } + : {}), + }, + } + : undefined, + }); } diff --git a/packages/@lwc/ssr-compiler/src/compile-js/index.ts b/packages/@lwc/ssr-compiler/src/compile-js/index.ts index 431f91c209..a6386dcadf 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/index.ts @@ -225,20 +225,20 @@ function validateUniqueDecorator(decorators: EsDecorator[]) { const expressions = decorators.map(({ expression }) => expression); - const hasWire = expressions.some( + const wire = expressions.find( (expr) => is.callExpression(expr) && is.identifier(expr.callee, { name: 'wire' }) ); - const hasApi = expressions.some((expr) => is.identifier(expr, { name: 'api' })); + const api = expressions.find((expr) => is.identifier(expr, { name: 'api' })); - if (hasWire && hasApi) { - throw generateError(DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api'); + if (wire && api) { + throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api'); } - const hasTrack = expressions.some((expr) => is.identifier(expr, { name: 'track' })); + const track = expressions.find((expr) => is.identifier(expr, { name: 'track' })); - if ((hasWire || hasApi) && hasTrack) { - throw generateError(DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track'); + if (wire && track) { + throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track'); } } @@ -252,6 +252,9 @@ export default function compileJS( let ast = parseModule(src, { module: true, next: true, + loc: true, + source: filename, + ranges: true, }) as EsProgram; const state: ComponentMetaState = { diff --git a/packages/@lwc/ssr-compiler/src/compile-js/wire.ts b/packages/@lwc/ssr-compiler/src/compile-js/wire.ts index 189153e16e..69d68ac899 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/wire.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/wire.ts @@ -44,7 +44,7 @@ function getWireParams( const { decorators } = node; if (decorators.length > 1) { - throw generateError(DecoratorErrors.ONE_WIRE_DECORATOR_ALLOWED); + throw generateError(node, DecoratorErrors.ONE_WIRE_DECORATOR_ALLOWED); } // validate the parameters @@ -94,7 +94,10 @@ function validateWireId( // This is not the exact same validation done in @lwc/babel-plugin-component but it accomplishes the same thing if (path.scope?.getBinding(wireAdapterVar)?.kind !== 'module') { - throw generateError(DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL); + throw generateError( + path.node!, + DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL + ); } } @@ -129,9 +132,15 @@ function validateWireConfig( continue; } } else if (is.templateLiteral(key)) { - throw generateError(DecoratorErrors.COMPUTED_PROPERTY_CANNOT_BE_TEMPLATE_LITERAL); + throw generateError( + path.node!, + DecoratorErrors.COMPUTED_PROPERTY_CANNOT_BE_TEMPLATE_LITERAL + ); } - throw generateError(DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL); + throw generateError( + path.node!, + DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL + ); } }