From 9ba090114b13609249da9829c553d320108a8723 Mon Sep 17 00:00:00 2001 From: James Tu Date: Thu, 14 Nov 2024 14:09:12 -0800 Subject: [PATCH] refactor: use estemplate instead of builder and address pr feedback --- .../babel-plugin-component/src/component.ts | 5 +- .../@lwc/engine-core/src/framework/api.ts | 2 +- .../dynamic-component-invalid-ctor/error.txt | 2 +- packages/@lwc/shared/src/custom-element.ts | 20 +++++ packages/@lwc/shared/src/index.ts | 1 + .../src/compile-js/generate-markup.ts | 9 +-- .../@lwc/ssr-compiler/src/compile-js/index.ts | 7 +- .../transformers/lwc-component.ts | 81 +++++-------------- packages/@lwc/ssr-compiler/src/index.ts | 4 +- 9 files changed, 56 insertions(+), 75 deletions(-) create mode 100644 packages/@lwc/shared/src/custom-element.ts diff --git a/packages/@lwc/babel-plugin-component/src/component.ts b/packages/@lwc/babel-plugin-component/src/component.ts index 7debbf4bd2..7e6c20e056 100644 --- a/packages/@lwc/babel-plugin-component/src/component.ts +++ b/packages/@lwc/babel-plugin-component/src/component.ts @@ -9,7 +9,7 @@ import * as types from '@babel/types'; import { addDefault, addNamed } from '@babel/helper-module-imports'; import { NodePath } from '@babel/traverse'; import { Visitor } from '@babel/core'; -import { getAPIVersionFromNumber } from '@lwc/shared'; +import { generateCustomElementTagName, getAPIVersionFromNumber } from '@lwc/shared'; import { COMPONENT_NAME_KEY, LWC_PACKAGE_ALIAS, @@ -48,8 +48,7 @@ function needsComponentRegistration(path: DeclarationPath) { function getComponentRegisteredName(t: BabelTypes, state: LwcBabelPluginPass) { const { namespace, name } = state.opts; - const kebabCasedName = name?.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - const componentName = namespace && kebabCasedName ? `${namespace}-${kebabCasedName}` : ''; + const componentName = generateCustomElementTagName(namespace, name); return t.stringLiteral(componentName); } diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 401e826c95..1c4f2c4454 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -667,7 +667,7 @@ function dc( if (!isComponentConstructor(Ctor)) { throw new Error( - `Invalid constructor ${toString(Ctor)} is not a LightningElement constructor.` + `Invalid constructor "${toString(Ctor)}" is not a LightningElement constructor.` ); } diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/dynamic-component-invalid-ctor/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/dynamic-component-invalid-ctor/error.txt index 8a32627e40..b5c01fd6d7 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/dynamic-component-invalid-ctor/error.txt +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/dynamic-component-invalid-ctor/error.txt @@ -1 +1 @@ -Invalid constructor frankenstein is not a LightningElement constructor. \ No newline at end of file +Invalid constructor "frankenstein" is not a LightningElement constructor. \ No newline at end of file diff --git a/packages/@lwc/shared/src/custom-element.ts b/packages/@lwc/shared/src/custom-element.ts new file mode 100644 index 0000000000..8d74ffb8cc --- /dev/null +++ b/packages/@lwc/shared/src/custom-element.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024, salesforce.com, 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 + */ + +/** + * Generates a custom element tag name given a namespace and component name. + * Based on the LWC file system requirements, component names come from the file system name which is + * camel cased. The component's name will be converted to kebab case when the tag name is produced. + * + * @param namespace component namespace + * @param name component name + * @returns component tag name + */ +export function generateCustomElementTagName(namespace: string = '', name: string = '') { + const kebabCasedName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + return `${namespace}-${kebabCasedName}`; +} diff --git a/packages/@lwc/shared/src/index.ts b/packages/@lwc/shared/src/index.ts index d752c05f91..5447d335d9 100644 --- a/packages/@lwc/shared/src/index.ts +++ b/packages/@lwc/shared/src/index.ts @@ -20,5 +20,6 @@ export * from './overridable-hooks'; export * from './static-part-tokens'; export * from './style'; export * from './signals'; +export * from './custom-element'; export { assert }; 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 1593ec6bdf..de710ef817 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts @@ -10,7 +10,6 @@ import { is, builders as b } from 'estree-toolkit'; import { esTemplate } from '../estemplate'; import { isIdentOrRenderCall } from '../estree/validators'; import { bImportDeclaration, bImportDefaultDeclaration } from '../estree/builders'; -import { TransformOptions } from '../shared'; import { bWireAdaptersPlumbing } from './wire'; import type { @@ -30,8 +29,7 @@ type RenderCallExpression = SimpleCallExpression & { }; const bGenerateMarkup = esTemplate` - export async function* generateMarkup(tagName, props, attrs, slotted, parent, scopeToken) { - tagName = tagName ?? ${/*component tag name*/ is.literal} + export async function* generateMarkup(tagName = ${/*component tag name*/ is.literal}, props, attrs, slotted, parent, scopeToken) { attrs = attrs ?? Object.create(null); props = props ?? Object.create(null); props = __filterProperties( @@ -89,17 +87,16 @@ const bAssignGenerateMarkupToComponentClass = esTemplate` export function addGenerateMarkupExport( program: Program, state: ComponentMetaState, - options: TransformOptions, + tagName: string, filename: string ) { const { hasRenderMethod, privateFields, publicFields, tmplExplicitImports } = state; - const { namespace, name } = options; // The default tag name represents the component name that's passed to the transformer. // This is needed to generate markup for dynamic components which are invoked through // the generateMarkup function on the constructor. // At the time of generation, the invoker does not have reference to its tag name to pass as an argument. - const defaultTagName = b.literal(`${namespace}-${name}`); + const defaultTagName = b.literal(tagName); const classIdentifier = b.identifier(state.lwcClassName!); const renderCall = hasRenderMethod ? (b.callExpression( diff --git a/packages/@lwc/ssr-compiler/src/compile-js/index.ts b/packages/@lwc/ssr-compiler/src/compile-js/index.ts index 3ef2131659..f2ef0e4d15 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/index.ts @@ -19,7 +19,7 @@ import { catalogWireAdapters } from './wire'; import { removeDecoratorImport } from './remove-decorator-import'; import type { Identifier as EsIdentifier, Program as EsProgram, Expression } from 'estree'; import type { Visitors, ComponentMetaState } from './types'; -import type { CompilationMode, TransformOptions } from '../shared'; +import type { CompilationMode } from '../shared'; import type { PropertyDefinition as DecoratatedPropertyDefinition, MethodDefinition as DecoratatedMethodDefinition, @@ -153,7 +153,7 @@ const visitors: Visitors = { export default function compileJS( src: string, filename: string, - options: TransformOptions, + tagName: string, compilationMode: CompilationMode ) { let ast = parseModule(src, { @@ -190,8 +190,7 @@ export default function compileJS( }; } - // Add the tag name here, either export tag name as additional export or just attach it to the class - addGenerateMarkupExport(ast, state, options, filename); + addGenerateMarkupExport(ast, state, tagName, filename); assignGenerateMarkupToComponent(ast, state); if (compilationMode === 'async' || compilationMode === 'sync') { diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/lwc-component.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/lwc-component.ts index 1455f9385f..be9e6b46b7 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/lwc-component.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/lwc-component.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { builders as b, is } from 'estree-toolkit'; +import { is } from 'estree-toolkit'; import { isUndefined } from '@lwc/shared'; import { Transformer } from '../types'; import { expressionIrToEs } from '../expression'; @@ -15,56 +15,25 @@ import type { LwcComponent as IrLwcComponent, Expression as IrExpression, } from '@lwc/template-compiler'; -import type { BlockStatement as EsBlockStatement, Expression, Statement } from 'estree'; +import type { + IfStatement as EsIfStatement, + VariableDeclaration as EsVariableDeclaration, +} from 'estree'; + +const bDynamicComponentConstructorDeclaration = esTemplate` + const Ctor = '${/*lwcIs attribute value*/ is.expression}'; +`; const bYieldFromDynamicComponentConstructorGenerator = esTemplateWithYield` - { + if (Ctor) { + if (typeof Ctor !== 'function' || !(Ctor.prototype instanceof LightningElement)) { + throw new Error(\`Invalid constructor "\${String(Ctor)}" is not a LightningElement constructor.\`) + } const childProps = __cloneAndDeepFreeze(${/* child props */ is.objectExpression}); const childAttrs = ${/* child attrs */ is.objectExpression}; - yield* ${/*component ctor*/ is.expression}[SYMBOL__GENERATE_MARKUP](null, childProps, childAttrs); - } -`; - -const bThrowErrorForInvalidConstructor = esTemplate` - { - throw new Error(\`Invalid constructor \${String(${/*component ctor*/ is.expression})} is not a LightningElement constructor.\`) + yield* Ctor[SYMBOL__GENERATE_MARKUP](null, childProps, childAttrs); } -`; - -function bIfLwcIsExpressionDefined(lwcIsExpression: Expression, consequent: Statement) { - // instance.lwcIsValue !== undefined && instance.lwcIsValue !== null - const lwcIsExpressionDefined = b.logicalExpression( - '&&', - b.binaryExpression('!==', lwcIsExpression, b.identifier('undefined')), - b.binaryExpression('!==', lwcIsExpression, b.identifier('null')) - ); - - return b.ifStatement(lwcIsExpressionDefined, b.blockStatement([consequent])); -} - -function bifLwcIsExpressionTypeCorrect( - lwcIsExpression: Expression, - consequent: Statement, - alternate: Statement -) { - // typeof instance.lwcIsValue === 'function' - const typeComparison = b.binaryExpression( - '===', - b.unaryExpression('typeof', lwcIsExpression), - b.literal('function') - ); - - // instance.lwcIsValue.prototype instanceof LightningElement - const protoComparison = b.binaryExpression( - 'instanceof', - b.memberExpression(lwcIsExpression, b.identifier('prototype')), - b.identifier('LightningElement') - ); - - const comparison = b.logicalExpression('&&', typeComparison, protoComparison); - - return b.ifStatement(comparison, consequent, alternate); -} +`; export const LwcComponent: Transformer = function LwcComponent(node, cxt) { const { directives } = node; @@ -81,20 +50,14 @@ export const LwcComponent: Transformer = function LwcComponent(n 'import:cloneAndDeepFreeze' ); - // The template compiler has validation to prevent lwcIs.value from being a literal - const lwcIsExpression = expressionIrToEs(lwcIs.value as IrExpression, cxt); return [ - bIfLwcIsExpressionDefined( - lwcIsExpression, - bifLwcIsExpressionTypeCorrect( - lwcIsExpression, - bYieldFromDynamicComponentConstructorGenerator( - getChildAttrsOrProps(node.properties, cxt), - getChildAttrsOrProps(node.attributes, cxt), - lwcIsExpression - ), - bThrowErrorForInvalidConstructor(lwcIsExpression) - ) + bDynamicComponentConstructorDeclaration( + // The template compiler has validation to prevent lwcIs.value from being a literal + expressionIrToEs(lwcIs.value as IrExpression, cxt) + ), + bYieldFromDynamicComponentConstructorGenerator( + getChildAttrsOrProps(node.properties, cxt), + getChildAttrsOrProps(node.attributes, cxt) ), ]; } else { diff --git a/packages/@lwc/ssr-compiler/src/index.ts b/packages/@lwc/ssr-compiler/src/index.ts index e0d8bf7259..2a6211e7dd 100644 --- a/packages/@lwc/ssr-compiler/src/index.ts +++ b/packages/@lwc/ssr-compiler/src/index.ts @@ -5,6 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ +import { generateCustomElementTagName } from '@lwc/shared'; import compileJS from './compile-js'; import compileTemplate from './compile-template'; import type { CompilationMode, TransformOptions } from './shared'; @@ -22,7 +23,8 @@ export function compileComponentForSSR( options: TransformOptions, mode: CompilationMode = 'asyncYield' ): CompilationResult { - const { code } = compileJS(src, filename, options, mode); + const tagName = generateCustomElementTagName(options.namespace, options.name); + const { code } = compileJS(src, filename, tagName, mode); return { code, map: undefined }; }