diff --git a/packages/@lwc/babel-plugin-component/src/constants.ts b/packages/@lwc/babel-plugin-component/src/constants.ts index f8be05db9f..0f4e07d703 100644 --- a/packages/@lwc/babel-plugin-component/src/constants.ts +++ b/packages/@lwc/babel-plugin-component/src/constants.ts @@ -5,25 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -// This set is for attributes that have a camel cased property name -// For example, div.tabIndex. -// We do not want users to define @api properties with these names -// Because the template will never call them. It'll always call the camel -// cased version. -const AMBIGUOUS_PROP_SET = new Map([ - ['bgcolor', 'bgColor'], - ['accesskey', 'accessKey'], - ['contenteditable', 'contentEditable'], - ['tabindex', 'tabIndex'], - ['maxlength', 'maxLength'], - ['maxvalue', 'maxValue'], -]); - -// This set is for attributes that can never be defined -// by users on their components. -// We throw for these. -const DISALLOWED_PROP_SET = new Set(['is', 'class', 'slot', 'style']); - const LWC_PACKAGE_ALIAS = 'lwc'; const LWC_PACKAGE_EXPORTS = { @@ -55,9 +36,7 @@ const API_VERSION_KEY = 'apiVersion'; const COMPONENT_CLASS_ID = '__lwc_component_class_internal'; export { - AMBIGUOUS_PROP_SET, DECORATOR_TYPES, - DISALLOWED_PROP_SET, LWC_PACKAGE_ALIAS, LWC_PACKAGE_EXPORTS, LWC_COMPONENT_PROPERTIES, diff --git a/packages/@lwc/babel-plugin-component/src/decorators/api/validate.ts b/packages/@lwc/babel-plugin-component/src/decorators/api/validate.ts index 7ffdff4139..a021f05d6e 100644 --- a/packages/@lwc/babel-plugin-component/src/decorators/api/validate.ts +++ b/packages/@lwc/babel-plugin-component/src/decorators/api/validate.ts @@ -5,13 +5,9 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { DecoratorErrors } from '@lwc/errors'; +import { AMBIGUOUS_PROP_SET, DISALLOWED_PROP_SET } from '@lwc/shared'; import { generateError } from '../../utils'; -import { - AMBIGUOUS_PROP_SET, - DECORATOR_TYPES, - DISALLOWED_PROP_SET, - LWC_PACKAGE_EXPORTS, -} from '../../constants'; +import { DECORATOR_TYPES, LWC_PACKAGE_EXPORTS } from '../../constants'; import { isApiDecorator } from './shared'; import type { types, NodePath } from '@babel/core'; import type { LwcBabelPluginPass } from '../../types'; diff --git a/packages/@lwc/shared/src/html-attributes.ts b/packages/@lwc/shared/src/html-attributes.ts index ac37b419fc..c35367767d 100644 --- a/packages/@lwc/shared/src/html-attributes.ts +++ b/packages/@lwc/shared/src/html-attributes.ts @@ -194,3 +194,26 @@ export function kebabCaseToCamelCase(attrName: string): string { return result; } + +/** + * This set is for attributes that have a camel cased property name + * For example, div.tabIndex. + * We do not want users to define `@api` properties with these names + * Because the template will never call them. It'll always call the camel + * cased version. + */ +export const AMBIGUOUS_PROP_SET = /*@__PURE__@*/ new Map([ + ['bgcolor', 'bgColor'], + ['accesskey', 'accessKey'], + ['contenteditable', 'contentEditable'], + ['tabindex', 'tabIndex'], + ['maxlength', 'maxLength'], + ['maxvalue', 'maxValue'], +]); + +/** + * This set is for attributes that can never be defined + * by users on their components. + * We throw for these. + */ +export const DISALLOWED_PROP_SET = /*@__PURE__@*/ new Set(['is', 'class', 'slot', 'style']); diff --git a/packages/@lwc/ssr-compiler/src/__tests__/api-decorator.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/api-decorator.spec.ts new file mode 100644 index 0000000000..dc03c08f75 --- /dev/null +++ b/packages/@lwc/ssr-compiler/src/__tests__/api-decorator.spec.ts @@ -0,0 +1,168 @@ +import { describe, test, expect } from 'vitest'; +import { compileComponentForSSR } from '../index'; + +const compile = + (src: string, filename = 'test.js') => + () => { + return compileComponentForSSR(src, filename, {}); + }; + +describe('thows error', () => { + test('combined with @track', () => { + const src = /* js */ ` + import { api, track, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @track + @api + apiWithTrack = "foo"; + } + `; + expect(compile(src)).toThrow(`LWC1093: @api method or property cannot be used with @track`); + }); + + describe('conflicting api properties', () => { + test.for([ + [ + 'getter/setter', + /* js */ ` + @api foo = 1; + _internal = 1; + @api + get foo() { + return "foo"; + } + set foo(val) { + this._internal = val; + }`, + ], + [ + 'method', + /* js */ ` + @api foo = 1; + @api foo() { + return "foo"; + }`, + ], + ])(`%s`, ([, body]) => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + ${body} + } + `; + expect(compile(src)).toThrow(`LWC1096: Duplicate @api property "foo".`); + }); + }); + + test('default value is true', () => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api publicProp = true; + } + `; + expect(compile(src)).toThrow(`LWC1099: Boolean public property must default to false.`); + }); + + test('computed api getters and setters', () => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api + set [x](value) {} + get [x]() {} + } + `; + expect(compile(src)).toThrow( + `LWC1106: @api cannot be applied to a computed property, getter, setter or method.` + ); + }); + + test('property name prefixed with data', () => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api dataFooBar; + } + `; + expect(compile(src)).toThrow( + `LWC1107: Invalid property name "dataFooBar". Properties starting with "data" are reserved attributes.` + ); + }); + + test('property name prefixed with on', () => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api onChangeHandler; + } + `; + expect(compile(src)).toThrow( + `LWC1108: Invalid property name "onChangeHandler". Properties starting with "on" are reserved for event handlers` + ); + }); + + describe('property name is ambiguous', () => { + test.for([ + ['bgcolor', 'bgColor'], + ['accesskey', 'accessKey'], + ['contenteditable', 'contentEditable'], + ['tabindex', 'tabIndex'], + ['maxlength', 'maxLength'], + ['maxvalue', 'maxValue'], + ] as [prop: string, suggestion: string][])('%s', ([prop, suggestion]) => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api ${prop}; + } + `; + expect(compile(src)).toThrow( + `LWC1109: Ambiguous attribute name "${prop}". "${prop}" will never be called from template because its corresponding property is camel cased. Consider renaming to "${suggestion}"` + ); + }); + }); + + describe('disallowed props', () => { + test.for(['class', 'is', 'slot', 'style'])('%s', (prop) => { + const src = /* js */ ` + import { api, LightningElement } from 'lwc' + export default class Test extends LightningElement { + @api ${prop} + } + `; + expect(compile(src)).toThrow( + `LWC1110: Invalid property name "${prop}". "${prop}" is a reserved attribute.` + ); + }); + }); + + test('property name is part', () => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api part; + } + `; + expect(compile(src)).toThrow( + `LWC1111: Invalid property name "part". "part" is a future reserved attribute for web components.` + ); + }); + + test('both getter and a setter', () => { + const src = /* js */ ` + import { api, LightningElement } from "lwc"; + export default class Test extends LightningElement { + @api get something() { + return this.s; + } + @api set something(value) { + this.s = value; + } + } + `; + expect(compile(src)).toThrow( + `LWC1112: @api get something and @api set something detected in class declaration. Only one of the two needs to be decorated with @api.` + ); + }); +}); diff --git a/packages/@lwc/ssr-compiler/src/compile-js/decorators/api/index.ts b/packages/@lwc/ssr-compiler/src/compile-js/decorators/api/index.ts new file mode 100644 index 0000000000..bbb791b887 --- /dev/null +++ b/packages/@lwc/ssr-compiler/src/compile-js/decorators/api/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import type { Decorator, Identifier } from 'estree'; + +export function isApiDecorator(decorator: Decorator | undefined): decorator is Decorator & { + expression: Identifier & { + name: 'api'; + }; +} { + return decorator?.expression.type === 'Identifier' && decorator.expression.name === 'api'; +} diff --git a/packages/@lwc/ssr-compiler/src/compile-js/decorators/api/validate.ts b/packages/@lwc/ssr-compiler/src/compile-js/decorators/api/validate.ts new file mode 100644 index 0000000000..ed4e5762e6 --- /dev/null +++ b/packages/@lwc/ssr-compiler/src/compile-js/decorators/api/validate.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import { DecoratorErrors } from '@lwc/errors'; +import { DISALLOWED_PROP_SET, AMBIGUOUS_PROP_SET } from '@lwc/shared'; +import { generateError } from '../../errors'; +import { type ComponentMetaState } from '../../types'; +import type { Identifier, MethodDefinition, PropertyDefinition } from 'estree'; + +export type ApiMethodDefinition = MethodDefinition & { + key: Identifier; +}; +export type ApiPropertyDefinition = PropertyDefinition & { + key: Identifier; +}; + +export type ApiDefinition = ApiPropertyDefinition | ApiMethodDefinition; + +function validateName(definition: ApiDefinition) { + if (definition.computed) { + throw generateError(definition, DecoratorErrors.PROPERTY_CANNOT_BE_COMPUTED); + } + + const propertyName = definition.key.name; + + switch (true) { + case propertyName === 'part': + throw generateError( + definition, + DecoratorErrors.PROPERTY_NAME_PART_IS_RESERVED, + propertyName + ); + case propertyName.startsWith('on'): + throw generateError( + definition, + DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_ON, + propertyName + ); + case propertyName.startsWith('data') && propertyName.length > 4: + throw generateError( + definition, + DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_DATA, + propertyName + ); + case DISALLOWED_PROP_SET.has(propertyName): + throw generateError( + definition, + DecoratorErrors.PROPERTY_NAME_IS_RESERVED, + propertyName + ); + case AMBIGUOUS_PROP_SET.has(propertyName): + throw generateError( + definition, + DecoratorErrors.PROPERTY_NAME_IS_AMBIGUOUS, + propertyName, + AMBIGUOUS_PROP_SET.get(propertyName)! + ); + } +} + +function validatePropertyValue(property: ApiPropertyDefinition) { + if (property.value && property.value.type === 'Literal' && property.value.value === true) { + throw generateError(property, DecoratorErrors.INVALID_BOOLEAN_PUBLIC_PROPERTY); + } +} + +function validatePropertyUnique(node: ApiPropertyDefinition, state: ComponentMetaState) { + if (state.publicFields.has(node.key.name)) { + throw generateError(node, DecoratorErrors.DUPLICATE_API_PROPERTY, node.key.name); + } +} + +export function validateApiProperty(node: ApiPropertyDefinition, state: ComponentMetaState) { + validatePropertyUnique(node, state); + validateName(node); + validatePropertyValue(node); +} + +function validateUniqueMethod(node: ApiMethodDefinition, state: ComponentMetaState) { + const field = state.publicFields.get(node.key.name); + + if (!field) { + return; + } + + if ( + field.type === 'MethodDefinition' && + (field.kind === 'get' || field.kind === 'set') && + (node.kind === 'get' || node.kind === 'set') + ) { + throw generateError( + node, + DecoratorErrors.SINGLE_DECORATOR_ON_SETTER_GETTER_PAIR, + node.key.name + ); + } + + throw generateError(node, DecoratorErrors.DUPLICATE_API_PROPERTY, node.key.name); +} + +export function validateApiMethod(node: ApiMethodDefinition, state: ComponentMetaState) { + validateUniqueMethod(node, state); + validateName(node); +} diff --git a/packages/@lwc/ssr-compiler/src/compile-js/decorators/index.ts b/packages/@lwc/ssr-compiler/src/compile-js/decorators/index.ts new file mode 100644 index 0000000000..c89f7441d3 --- /dev/null +++ b/packages/@lwc/ssr-compiler/src/compile-js/decorators/index.ts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import { DecoratorErrors } from '@lwc/errors'; +import { generateError } from '../errors'; +import { isApiDecorator } from './api'; +import { isTrackDecorator } from './track'; +import { isWireDecorator } from './wire'; +import type { Decorator as EsDecorator } from 'estree'; + +export function validateUniqueDecorator(decorators: EsDecorator[]) { + if (decorators.length < 2) { + return; + } + + const wire = decorators.find(isWireDecorator); + const api = decorators.find(isApiDecorator); + const track = decorators.find(isTrackDecorator); + + if (wire) { + if (api) { + throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api'); + } + + if (track) { + throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track'); + } + } + + if (api && track) { + throw generateError(api, DecoratorErrors.API_AND_TRACK_DECORATOR_CONFLICT); + } +} + +// function validateUniqueDecorator(decorators: EsDecorator[]) { +// if (decorators.length < 2) { +// return; +// } + +// const expressions = decorators.map(({ expression }) => expression); + +// const wire = expressions.find( +// (expr) => is.callExpression(expr) && is.identifier(expr.callee, { name: 'wire' }) +// ); + +// const api = expressions.find((expr) => is.identifier(expr, { name: 'api' })); + +// if (wire && api) { +// throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api'); +// } + +// const track = expressions.find((expr) => is.identifier(expr, { name: 'track' })); + +// if (wire && track) { +// throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track'); +// } +// } diff --git a/packages/@lwc/ssr-compiler/src/compile-js/decorators/track.ts b/packages/@lwc/ssr-compiler/src/compile-js/decorators/track.ts new file mode 100644 index 0000000000..d72f9f4156 --- /dev/null +++ b/packages/@lwc/ssr-compiler/src/compile-js/decorators/track.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import type { Decorator, Identifier } from 'estree'; + +export function isTrackDecorator( + decorator: Decorator | undefined +): decorator is Decorator & { expression: Identifier & { name: 'track' } } { + return decorator?.expression.type === 'Identifier' && decorator.expression.name === 'track'; +} diff --git a/packages/@lwc/ssr-compiler/src/compile-js/wire.ts b/packages/@lwc/ssr-compiler/src/compile-js/decorators/wire.ts similarity index 93% rename from packages/@lwc/ssr-compiler/src/compile-js/wire.ts rename to packages/@lwc/ssr-compiler/src/compile-js/decorators/wire.ts index 69d68ac899..b733ace220 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/wire.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/decorators/wire.ts @@ -8,8 +8,8 @@ import { is, builders as b } from 'estree-toolkit'; import { produce } from 'immer'; import { DecoratorErrors } from '@lwc/errors'; -import { esTemplate } from '../estemplate'; -import { generateError } from './errors'; +import { esTemplate } from '../../estemplate'; +import { generateError } from '../errors'; import type { NodePath } from 'estree-toolkit'; import type { @@ -22,8 +22,10 @@ import type { MemberExpression, Property, BlockStatement, + Decorator, + CallExpression, } from 'estree'; -import type { ComponentMetaState, WireAdapter } from './types'; +import type { ComponentMetaState, WireAdapter } from '../types'; interface NoSpreadObjectExpression extends Omit { properties: Property[]; @@ -217,3 +219,13 @@ export function bWireAdaptersPlumbing(adapters: WireAdapter[]): BlockStatement[] return bWireAdapterPlumbing(adapterId, actionUponNewValue, config); }); } + +export function isWireDecorator(decorator: Decorator | undefined): decorator is Decorator & { + expression: CallExpression & { callee: Identifier & { name: 'wire' } }; +} { + return ( + is.callExpression(decorator?.expression) && + is.identifier(decorator.expression.callee) && + decorator.expression.callee.name === 'wire' + ); +} diff --git a/packages/@lwc/ssr-compiler/src/compile-js/errors.ts b/packages/@lwc/ssr-compiler/src/compile-js/errors.ts index b63846225c..b333e30f30 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/errors.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/errors.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { type LWCErrorInfo, generateCompilerError } from '@lwc/errors'; -import type { Node } from 'estree'; +import type { BaseNodeWithoutComments } from 'estree'; // This type extracts the arguments in a string. Example: "Error {0} {1}" -> [string, string] type ExtractArguments< @@ -19,7 +19,7 @@ type ExtractArguments< : Args; // No `N` found, nothing more to check export function generateError( - node: Node, + node: BaseNodeWithoutComments, error: T, ...messageArgs: ExtractArguments ) { diff --git a/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts b/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts index f7be0e745c..63099c7bc2 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts @@ -9,7 +9,7 @@ import { parse as pathParse } from 'node:path'; import { is, builders as b } from 'estree-toolkit'; import { esTemplate } from '../estemplate'; import { bImportDeclaration } from '../estree/builders'; -import { bWireAdaptersPlumbing } from './wire'; +import { bWireAdaptersPlumbing } from './decorators/wire'; import type { Program, Statement, IfStatement } from 'estree'; import type { ComponentMetaState } from './types'; @@ -139,8 +139,8 @@ export function addGenerateMarkupFunction( ); program.body.push( ...bGenerateMarkup( - b.arrayExpression(publicFields.map(b.literal)), - b.arrayExpression(privateFields.map(b.literal)), + b.arrayExpression([...publicFields.keys()].map(b.literal)), + b.arrayExpression([...privateFields].map(b.literal)), defaultTagName, classIdentifier, connectWireAdapterCode diff --git a/packages/@lwc/ssr-compiler/src/compile-js/index.ts b/packages/@lwc/ssr-compiler/src/compile-js/index.ts index a6386dcadf..f1489db0da 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/index.ts @@ -9,24 +9,28 @@ import { generate } from 'astring'; import { traverse, builders as b, is } from 'estree-toolkit'; import { parseModule } from 'meriyah'; -import { DecoratorErrors } from '@lwc/errors'; import { transmogrify } from '../transmogrify'; import { ImportManager } from '../imports'; import { replaceLwcImport, replaceNamedLwcExport, replaceAllLwcExport } from './lwc-import'; import { catalogTmplImport } from './catalog-tmpls'; import { catalogStaticStylesheets, catalogAndReplaceStyleImports } from './stylesheets'; import { addGenerateMarkupFunction } from './generate-markup'; -import { catalogWireAdapters } from './wire'; +import { catalogWireAdapters, isWireDecorator } from './decorators/wire'; +import { validateApiProperty, validateApiMethod } from './decorators/api/validate'; +import { isApiDecorator } from './decorators/api'; import { removeDecoratorImport } from './remove-decorator-import'; -import { generateError } from './errors'; + +import { type Visitors, type ComponentMetaState } from './types'; +import { validateUniqueDecorator } from './decorators'; import type { ComponentTransformOptions } from '../shared'; import type { Identifier as EsIdentifier, Program as EsProgram, - Decorator as EsDecorator, + PropertyDefinition as EsPropertyDefinition, + MethodDefinition as EsMethodDefinition, + Identifier, } from 'estree'; -import type { Visitors, ComponentMetaState } from './types'; import type { CompilationMode } from '@lwc/shared'; const visitors: Visitors = { @@ -93,24 +97,21 @@ const visitors: Visitors = { }, PropertyDefinition(path, state) { const node = path.node; - if (!is.identifier(node?.key)) { + + if (!isKeyIdentifier(node)) { return; } const { decorators } = node; validateUniqueDecorator(decorators); - const decoratedExpression = decorators?.[0]?.expression; - if (is.identifier(decoratedExpression) && decoratedExpression.name === 'api') { - state.publicFields.push(node.key.name); - } else if ( - is.callExpression(decoratedExpression) && - is.identifier(decoratedExpression.callee) && - decoratedExpression.callee.name === 'wire' - ) { + if (isApiDecorator(decorators[0])) { + validateApiProperty(node, state); + state.publicFields.set(node.key.name, node); + } else if (isWireDecorator(decorators[0])) { catalogWireAdapters(path, state); - state.privateFields.push(node.key.name); + state.privateFields.add(node.key.name); } else { - state.privateFields.push(node.key.name); + state.privateFields.add(node.key.name); } if ( @@ -127,10 +128,9 @@ const visitors: Visitors = { }, MethodDefinition(path, state) { const node = path.node; - if (!is.identifier(node?.key)) { + if (!isKeyIdentifier(node)) { return; } - // If we mutate any class-methods that are piped through this compiler, then we'll be // inadvertently mutating things like Wire adapters. if (!state.isLWC) { @@ -139,13 +139,10 @@ const visitors: Visitors = { const { decorators } = node; validateUniqueDecorator(decorators); - // The real type is a subset of `Expression`, which doesn't work with the `is` validators - const decoratedExpression = decorators?.[0]?.expression; - if ( - is.callExpression(decoratedExpression) && - is.identifier(decoratedExpression.callee) && - decoratedExpression.callee.name === 'wire' - ) { + if (isApiDecorator(decorators[0])) { + validateApiMethod(node, state); + state.publicFields.set(node.key.name, node); + } else if (isWireDecorator(decorators[0])) { // Getters and setters are methods in the AST, but treated as properties by @wire // Note that this means that their implementations are ignored! if (node.kind === 'get' || node.kind === 'set') { @@ -218,30 +215,6 @@ const visitors: Visitors = { }, }; -function validateUniqueDecorator(decorators: EsDecorator[]) { - if (decorators.length < 2) { - return; - } - - const expressions = decorators.map(({ expression }) => expression); - - const wire = expressions.find( - (expr) => is.callExpression(expr) && is.identifier(expr.callee, { name: 'wire' }) - ); - - const api = expressions.find((expr) => is.identifier(expr, { name: 'api' })); - - if (wire && api) { - throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api'); - } - - const track = expressions.find((expr) => is.identifier(expr, { name: 'track' })); - - if (wire && track) { - throw generateError(wire, DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track'); - } -} - export default function compileJS( src: string, filename: string, @@ -269,8 +242,8 @@ export default function compileJS( tmplExplicitImports: null, cssExplicitImports: null, staticStylesheetIds: null, - publicFields: [], - privateFields: [], + publicFields: new Map(), + privateFields: new Set(), wireAdapters: [], experimentalDynamicComponent: options.experimentalDynamicComponent, importManager: new ImportManager(), @@ -297,3 +270,9 @@ export default function compileJS( code: generate(ast, {}), }; } + +function isKeyIdentifier( + node: T | undefined | null +): node is T & { key: Identifier } { + return node?.key.type === 'Identifier'; +} diff --git a/packages/@lwc/ssr-compiler/src/compile-js/types.ts b/packages/@lwc/ssr-compiler/src/compile-js/types.ts index 6ff7723f08..98949a7764 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/types.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/types.ts @@ -5,9 +5,9 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ +import { type traverse } from 'estree-toolkit'; import type { ImportManager } from '../imports'; import type { ComponentTransformOptions } from '../shared'; -import type { traverse } from 'estree-toolkit'; import type { Identifier, MemberExpression, @@ -49,9 +49,12 @@ export interface ComponentMetaState { // the set of variable names associated with explicitly imported CSS files staticStylesheetIds: Set | null; // the public (`@api`-annotated) fields of the component class - publicFields: Array; + publicFields: Map< + string, + (MethodDefinition & { key: Identifier }) | (PropertyDefinition & { key: Identifier }) + >; // the private fields of the component class - privateFields: Array; + privateFields: Set; // indicates whether the LightningElement has any wired props wireAdapters: WireAdapter[]; // dynamic imports configuration