diff --git a/src/compiler/transformers/decorators-to-static/prop-decorator.ts b/src/compiler/transformers/decorators-to-static/prop-decorator.ts index d951b49d8a6..2ab26084e2e 100644 --- a/src/compiler/transformers/decorators-to-static/prop-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/prop-decorator.ts @@ -98,6 +98,7 @@ const parsePropDecorator = ( const propMeta: d.ComponentCompilerStaticProperty = { type: typeStr, + attribute: getAttributeName(propName, propOptions), mutable: !!propOptions.mutable, complexType: getComplexType(typeChecker, prop, type, program), required: prop.exclamationToken !== undefined && propName !== 'mode', @@ -106,11 +107,12 @@ const parsePropDecorator = ( getter: ts.isGetAccessor(prop), setter: !!foundSetter, }; - if (ogPropName && ogPropName !== propName) propMeta.ogPropName = ogPropName; + if (ogPropName && ogPropName !== propName) { + propMeta.ogPropName = ogPropName; + } // prop can have an attribute if type is NOT "unknown" if (typeStr !== 'unknown') { - propMeta.attribute = getAttributeName(propName, propOptions); propMeta.reflect = getReflect(diagnostics, propDecorator, propOptions); } diff --git a/src/compiler/transformers/test/convert-decorators.spec.ts b/src/compiler/transformers/test/convert-decorators.spec.ts index 426f67b6908..018a0a22e7a 100644 --- a/src/compiler/transformers/test/convert-decorators.spec.ts +++ b/src/compiler/transformers/test/convert-decorators.spec.ts @@ -28,6 +28,7 @@ describe('convert-decorators', () => { return { "val": { "type": "string", + attribute: 'val', "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, @@ -35,7 +36,6 @@ describe('convert-decorators', () => { "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, - "attribute": "val", "reflect": false, "defaultValue": "\\"initial value\\"" } diff --git a/src/compiler/transformers/test/parse-props.spec.ts b/src/compiler/transformers/test/parse-props.spec.ts index 9d56d78da8a..397738c0890 100644 --- a/src/compiler/transformers/test/parse-props.spec.ts +++ b/src/compiler/transformers/test/parse-props.spec.ts @@ -213,6 +213,7 @@ describe('parse props', () => { `); expect(getStaticGetter(t.outputText, 'properties')).toEqual({ val: { + attribute: 'val', complexType: { references: {}, resolved: '{}', // TODO, needs to be string[] @@ -231,7 +232,7 @@ describe('parse props', () => { }, }); expect(t.property?.type).toBe('unknown'); - expect(t.property?.attribute).toBe(undefined); + expect(t.property?.attribute).toBe('val'); expect(t.property?.reflect).toBe(false); }); @@ -819,6 +820,7 @@ describe('parse props', () => { return { val: { type: 'string', + attribute: 'val', mutable: false, complexType: { original: 'string', resolved: 'string', references: {} }, required: false, @@ -826,12 +828,12 @@ describe('parse props', () => { docs: { tags: [], text: '' }, getter: false, setter: false, - attribute: 'val', reflect: false, defaultValue: \"'good'\", }, val2: { type: 'string', + attribute: 'val-2', mutable: false, complexType: { original: 'string', resolved: 'string', references: {} }, required: false, @@ -840,7 +842,6 @@ describe('parse props', () => { getter: false, setter: false, ogPropName: 'dynVal', - attribute: 'val-2', reflect: false, defaultValue: \"'nice'\", }, diff --git a/src/hydrate/platform/proxy-host-element.ts b/src/hydrate/platform/proxy-host-element.ts index 6db2e14c4d3..19ec218b6f4 100644 --- a/src/hydrate/platform/proxy-host-element.ts +++ b/src/hydrate/platform/proxy-host-element.ts @@ -41,46 +41,32 @@ export function proxyHostElement(elm: d.HostElement, cstr: d.ComponentConstructo members.forEach(([memberName, [memberFlags, metaAttributeName]]) => { if (memberFlags & MEMBER_FLAGS.Prop) { + // hyphenated attribute name const attributeName = metaAttributeName || memberName; - let attrValue = elm.getAttribute(attributeName); - - /** - * allow hydrate parameters that contain a simple object, e.g. - * ```ts - * import { renderToString } from 'component-library/hydrate'; - * await renderToString(`<car-detail car=${JSON.stringify({ year: 1234 })}></car-detail>`); - * ``` - */ - if ( - (attrValue?.startsWith('{') && attrValue.endsWith('}')) || - (attrValue?.startsWith('[') && attrValue.endsWith(']')) - ) { - try { - attrValue = JSON.parse(attrValue); - } catch (e) { - /* ignore */ - } - } - + // attribute value + const attrValue = elm.getAttribute(attributeName); + // property value + const propValue = (elm as any)[memberName]; + let attrPropVal: any; + // any existing getter/setter applied to class property const { get: origGetter, set: origSetter } = Object.getOwnPropertyDescriptor((cstr as any).prototype, memberName) || {}; - let attrPropVal: any; - if (attrValue != null) { + // incoming value from `an-attribute=....`. Convert from string to correct type attrPropVal = parsePropertyValue(attrValue, memberFlags); } - const ownValue = (elm as any)[memberName]; - if (ownValue !== undefined) { - attrPropVal = ownValue; - // we've got an actual value already set on the host element - // let's add that to our instance values and pull it off the element - // so the getter/setter kicks in instead, but still getting this value + if (propValue !== undefined) { + // incoming value set on the host element (e.g `element.aProp = ...`) + // let's add that to our instance values and pull it off the element. + // This allows any applied getter/setter to kick in instead whilst still getting this value + attrPropVal = propValue; delete (elm as any)[memberName]; } if (attrPropVal !== undefined) { + // value set via attribute/prop on the host element if (origSetter) { // we have an original setter, so let's set the value via that. origSetter.apply(elm, [attrPropVal]); diff --git a/src/hydrate/runner/index.ts b/src/hydrate/runner/index.ts index ca11c860a5d..6d8016ffbca 100644 --- a/src/hydrate/runner/index.ts +++ b/src/hydrate/runner/index.ts @@ -1,2 +1,3 @@ export { createWindowFromHtml } from './create-window'; export { hydrateDocument, renderToString, serializeDocumentToString, streamToString } from './render'; +export { deserializeProperty, serializeProperty } from '@utils'; diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index d3bcc2d954f..97bbf85a5bf 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -1,6 +1,7 @@ import { BUILD } from '@app-data'; import { plt, win } from '@platform'; -import { CMP_FLAGS } from '@utils'; +import { parsePropertyValue } from '@runtime'; +import { CMP_FLAGS, MEMBER_FLAGS } from '@utils'; import type * as d from '../declarations'; import { patchSlottedNode } from './dom-extras'; @@ -53,6 +54,24 @@ export const initializeClientHydrate = ( const vnode: d.VNode = newVNode(tagName, null); vnode.$elm$ = hostElm; + /** + * The following forEach loop attaches properties from the element's attributes to the VNode. + * This is used to hydrate the VNode with the initial values of the element's attributes. + */ + const members = Object.entries(hostRef.$cmpMeta$?.$members$ || {}); + members.forEach(([memberName, [memberFlags, metaAttributeName]]) => { + if (!(memberFlags & MEMBER_FLAGS.Prop)) { + return; + } + const attributeName = metaAttributeName || memberName; + const attrVal = hostElm.getAttribute(attributeName); + + if (attrVal !== null) { + const attrPropVal = parsePropertyValue(attrVal, memberFlags); + hostRef?.$instanceValues$?.set(memberName, attrPropVal); + } + }); + let scopeId: string; if (BUILD.scoped) { const cmpMeta = hostRef.$cmpMeta$; diff --git a/src/runtime/parse-property-value.ts b/src/runtime/parse-property-value.ts index 39d402399c8..53d53dc8941 100644 --- a/src/runtime/parse-property-value.ts +++ b/src/runtime/parse-property-value.ts @@ -1,5 +1,5 @@ import { BUILD } from '@app-data'; -import { isComplexType, MEMBER_FLAGS } from '@utils'; +import { deserializeProperty, isComplexType, MEMBER_FLAGS, SERIALIZED_PREFIX } from '@utils'; /** * Parse a new property value for a given property type. @@ -24,32 +24,71 @@ import { isComplexType, MEMBER_FLAGS } from '@utils'; * @param propType the type of the prop, expressed as a binary number * @returns the parsed/coerced value */ -export const parsePropertyValue = (propValue: any, propType: number): any => { - // ensure this value is of the correct prop type +export const parsePropertyValue = (propValue: unknown, propType: number): any => { + /** + * Allow hydrate parameters that contain a simple object, e.g. + * ```ts + * import { renderToString } from 'component-library/hydrate'; + * await renderToString(`<car-detail car=${JSON.stringify({ year: 1234 })}></car-detail>`); + * ``` + * @deprecated + */ + if ( + (BUILD.hydrateClientSide || BUILD.hydrateServerSide) && + typeof propValue === 'string' && + ((propValue.startsWith('{') && propValue.endsWith('}')) || (propValue.startsWith('[') && propValue.endsWith(']'))) + ) { + try { + propValue = JSON.parse(propValue); + return propValue; + } catch (e) { + /* ignore */ + } + } + + /** + * Allow hydrate parameters that contain a complex non-serialized values. + */ + if ( + (BUILD.hydrateClientSide || BUILD.hydrateServerSide) && + typeof propValue === 'string' && + propValue.startsWith(SERIALIZED_PREFIX) + ) { + propValue = deserializeProperty(propValue); + return propValue; + } if (propValue != null && !isComplexType(propValue)) { + /** + * ensure this value is of the correct prop type + */ if (BUILD.propBoolean && propType & MEMBER_FLAGS.Boolean) { - // per the HTML spec, any string value means it is a boolean true value - // but we'll cheat here and say that the string "false" is the boolean false + /** + * per the HTML spec, any string value means it is a boolean true value + * but we'll cheat here and say that the string "false" is the boolean false + */ return propValue === 'false' ? false : propValue === '' || !!propValue; } + /** + * force it to be a number + */ if (BUILD.propNumber && propType & MEMBER_FLAGS.Number) { - // force it to be a number - return parseFloat(propValue); + return typeof propValue === 'string' ? parseFloat(propValue) : typeof propValue === 'number' ? propValue : NaN; } + /** + * could have been passed as a number or boolean but we still want it as a string + */ if (BUILD.propString && propType & MEMBER_FLAGS.String) { - // could have been passed as a number or boolean - // but we still want it as a string return String(propValue); } - // redundant return here for better minification return propValue; } - // not sure exactly what type we want - // so no need to change to a different type + /** + * not sure exactly what type we want so no need to change to a different type + */ return propValue; }; diff --git a/src/runtime/test/hydrate-prop.spec.tsx b/src/runtime/test/hydrate-prop.spec.tsx index 30f2057b916..8906fdc7c4b 100644 --- a/src/runtime/test/hydrate-prop.spec.tsx +++ b/src/runtime/test/hydrate-prop.spec.tsx @@ -63,7 +63,7 @@ describe('hydrate prop types', () => { }); expect(serverHydrated.root).toEqualHtml(` - <cmp-a class="hydrated" boolean="false" clamped="11" class="hydrated" num="1" s-id="1" str="hello" accessor="1"> + <cmp-a class="hydrated" boolean="false" clamped="11" num="1" s-id="1" str="hello" accessor="1"> <!--r.1--> <!--t.1.0.0.0--> false-hello world world-201-101-10 diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index e9875b1eb3e..ea51e89dd75 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -985,13 +985,14 @@ export const renderVdom = (hostRef: d.HostRef, renderFnResults: d.VNode | d.VNod const hostElm = hostRef.$hostElement$; const cmpMeta = hostRef.$cmpMeta$; const oldVNode: d.VNode = hostRef.$vnode$ || newVNode(null, null); + const isHostElement = isHost(renderFnResults); // if `renderFnResults` is a Host node then we can use it directly. If not, // we need to call `h` again to wrap the children of our component in a // 'dummy' Host node (well, an empty vnode) since `renderVdom` assumes // implicitly that the top-level vdom node is 1) an only child and 2) // contains attrs that need to be set on the host element. - const rootVnode = isHost(renderFnResults) ? renderFnResults : h(null, null, renderFnResults as any); + const rootVnode = isHostElement ? renderFnResults : h(null, null, renderFnResults as any); hostTagName = hostElm.tagName; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b24af5ce26e..d29f4436e8f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -259,3 +259,36 @@ export const enum NODE_TYPES { DOCUMENT_FRAGMENT_NODE = 11, NOTATION_NODE = 12, } + +/** + * Represents a primitive type. + * Described in https://w3c.github.io/webdriver-bidi/#type-script-PrimitiveProtocolValue. + */ +export enum PrimitiveType { + Undefined = 'undefined', + Null = 'null', + String = 'string', + Number = 'number', + SpecialNumber = 'number', + Boolean = 'boolean', + BigInt = 'bigint', +} + +/** + * Represents a non-primitive type. + * Described in https://w3c.github.io/webdriver-bidi/#type-script-RemoteValue. + */ +export enum NonPrimitiveType { + Array = 'array', + Date = 'date', + Map = 'map', + Object = 'object', + RegularExpression = 'regexp', + Set = 'set', + Channel = 'channel', + Symbol = 'symbol', +} + +export const TYPE_CONSTANT = 'type'; +export const VALUE_CONSTANT = 'value'; +export const SERIALIZED_PREFIX = 'serialized:'; diff --git a/src/utils/format-component-runtime-meta.ts b/src/utils/format-component-runtime-meta.ts index 7981653d20f..9f9dbb62333 100644 --- a/src/utils/format-component-runtime-meta.ts +++ b/src/utils/format-component-runtime-meta.ts @@ -123,8 +123,16 @@ const formatFlags = (compilerProperty: d.ComponentCompilerProperty) => { return type; }; +/** + * We mainly add the alternative kebab-case attribute name because it might + * be used in an HTML environment (non JSX). Since we support hydration of + * complex types we provide a kebab-case attribute name for properties with + * these types. + */ +const kebabCaseSupportForTypes = ['string', 'unknown']; + const formatAttrName = (compilerProperty: d.ComponentCompilerProperty) => { - if (typeof compilerProperty.attribute === 'string') { + if (kebabCaseSupportForTypes.includes(typeof compilerProperty.attribute)) { // string attr name means we should observe this attribute if (compilerProperty.name === compilerProperty.attribute) { // property name and attribute name are the exact same diff --git a/src/utils/index.ts b/src/utils/index.ts index f817e38c951..53e2c2ffd02 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,6 +4,7 @@ export * from './format-component-runtime-meta'; export * from './helpers'; export * from './is-glob'; export * from './is-root-path'; +export * from './local-value'; export * from './logger/logger-rollup'; export * from './logger/logger-typescript'; export * from './logger/logger-utils'; @@ -12,8 +13,11 @@ export * from './output-target'; export * from './path'; export * from './query-nonce-meta-tag-content'; export * from './regular-expression'; +export * from './remote-value'; export * as result from './result'; +export * from './serialize'; export * from './sourcemaps'; +export * from './types'; export * from './url-paths'; export * from './util'; export * from './validation'; diff --git a/src/utils/local-value.ts b/src/utils/local-value.ts new file mode 100644 index 00000000000..683b9feea37 --- /dev/null +++ b/src/utils/local-value.ts @@ -0,0 +1,258 @@ +import { NonPrimitiveType, PrimitiveType, TYPE_CONSTANT, VALUE_CONSTANT } from './constants'; +import type { LocalValueParam, ScriptLocalValue, Serializeable } from './types'; + +/** + * Represents a local value with a specified type and optional value. + * Described in https://w3c.github.io/webdriver-bidi/#type-script-LocalValue + */ +export class LocalValue { + type: PrimitiveType | NonPrimitiveType; + value?: Serializeable | Serializeable[] | [Serializeable, Serializeable][]; + + constructor(type: PrimitiveType | NonPrimitiveType, value?: LocalValueParam) { + if (type === PrimitiveType.Undefined || type === PrimitiveType.Null) { + this.type = type; + } else { + this.type = type; + this.value = value; + } + } + + /** + * Creates a new LocalValue object with a string value. + * + * @param {string} value - The string value to be stored in the LocalValue object. + * @returns {LocalValue} - The created LocalValue object. + */ + static createStringValue(value: string) { + return new LocalValue(PrimitiveType.String, value); + } + + /** + * Creates a new LocalValue object with a number value. + * + * @param {number} value - The number value. + * @returns {LocalValue} - The created LocalValue object. + */ + static createNumberValue(value: number) { + return new LocalValue(PrimitiveType.Number, value); + } + + /** + * Creates a new LocalValue object with a special number value. + * + * @param {number} value - The value of the special number. + * @returns {LocalValue} - The created LocalValue object. + */ + static createSpecialNumberValue(value: number) { + if (Number.isNaN(value)) { + return new LocalValue(PrimitiveType.SpecialNumber, 'NaN'); + } + if (Object.is(value, -0)) { + return new LocalValue(PrimitiveType.SpecialNumber, '-0'); + } + if (value === Infinity) { + return new LocalValue(PrimitiveType.SpecialNumber, 'Infinity'); + } + if (value === -Infinity) { + return new LocalValue(PrimitiveType.SpecialNumber, '-Infinity'); + } + return new LocalValue(PrimitiveType.SpecialNumber, value); + } + + /** + * Creates a new LocalValue object with an undefined value. + * @returns {LocalValue} - The created LocalValue object. + */ + static createUndefinedValue() { + return new LocalValue(PrimitiveType.Undefined); + } + + /** + * Creates a new LocalValue object with a null value. + * @returns {LocalValue} - The created LocalValue object. + */ + static createNullValue() { + return new LocalValue(PrimitiveType.Null); + } + + /** + * Creates a new LocalValue object with a boolean value. + * + * @param {boolean} value - The boolean value. + * @returns {LocalValue} - The created LocalValue object. + */ + static createBooleanValue(value: boolean) { + return new LocalValue(PrimitiveType.Boolean, value); + } + + /** + * Creates a new LocalValue object with a BigInt value. + * + * @param {BigInt} value - The BigInt value. + * @returns {LocalValue} - The created LocalValue object. + */ + static createBigIntValue(value: bigint) { + return new LocalValue(PrimitiveType.BigInt, value.toString()); + } + + /** + * Creates a new LocalValue object with an array. + * + * @param {Array} value - The array. + * @returns {LocalValue} - The created LocalValue object. + */ + static createArrayValue(value: Array<unknown>) { + return new LocalValue(NonPrimitiveType.Array, value); + } + + /** + * Creates a new LocalValue object with date value. + * + * @param {string} value - The date. + * @returns {LocalValue} - The created LocalValue object. + */ + static createDateValue(value: Date) { + return new LocalValue(NonPrimitiveType.Date, value); + } + + /** + * Creates a new LocalValue object of map value. + * @param {Map} map - The map. + * @returns {LocalValue} - The created LocalValue object. + */ + static createMapValue(map: Map<unknown, unknown>) { + const value: [Serializeable, Serializeable][] = []; + Array.from(map.entries()).forEach(([key, val]) => { + value.push([LocalValue.getArgument(key), LocalValue.getArgument(val)]); + }); + return new LocalValue(NonPrimitiveType.Map, value); + } + + /** + * Creates a new LocalValue object from the passed object. + * + * @param object the object to create a LocalValue from + * @returns {LocalValue} - The created LocalValue object. + */ + static createObjectValue(object: Record<string | number | symbol, unknown>) { + const value: [Serializeable, Serializeable][] = []; + Object.entries(object).forEach(([key, val]) => { + value.push([key, LocalValue.getArgument(val)]); + }); + return new LocalValue(NonPrimitiveType.Object, value); + } + + /** + * Creates a new LocalValue object of regular expression value. + * + * @param {string} value - The value of the regular expression. + * @returns {LocalValue} - The created LocalValue object. + */ + static createRegularExpressionValue(value: { pattern: string; flags: string }) { + return new LocalValue(NonPrimitiveType.RegularExpression, value); + } + + /** + * Creates a new LocalValue object with the specified value. + * @param {Set} value - The value to be set. + * @returns {LocalValue} - The created LocalValue object. + */ + static createSetValue(value: ([unknown, unknown] | LocalValue)[]) { + return new LocalValue(NonPrimitiveType.Set, value); + } + + /** + * Creates a new LocalValue object with the given channel value + * + * @param {ChannelValue} value - The channel value. + * @returns {LocalValue} - The created LocalValue object. + */ + static createChannelValue(value: unknown) { + return new LocalValue(NonPrimitiveType.Channel, value); + } + + /** + * Creates a new LocalValue object with a Symbol value. + * + * @param {Symbol} symbol - The Symbol value + * @returns {LocalValue} - The created LocalValue object + */ + static createSymbolValue(symbol: Symbol) { + // Store the symbol description or 'Symbol()' if undefined + const description = symbol.description || 'Symbol()'; + return new LocalValue(NonPrimitiveType.Symbol, description); + } + + static getArgument(argument: unknown) { + const type = typeof argument; + switch (type) { + case PrimitiveType.String: + return LocalValue.createStringValue(argument as string); + case PrimitiveType.Number: + if (Number.isNaN(argument) || Object.is(argument, -0) || !Number.isFinite(argument)) { + return LocalValue.createSpecialNumberValue(argument as number); + } + + return LocalValue.createNumberValue(argument as number); + case PrimitiveType.Boolean: + return LocalValue.createBooleanValue(argument as boolean); + case PrimitiveType.BigInt: + return LocalValue.createBigIntValue(argument as bigint); + case PrimitiveType.Undefined: + return LocalValue.createUndefinedValue(); + case NonPrimitiveType.Symbol: + return LocalValue.createSymbolValue(argument as Symbol); + case NonPrimitiveType.Object: + if (argument === null) { + return LocalValue.createNullValue(); + } + if (argument instanceof Date) { + return LocalValue.createDateValue(argument); + } + if (argument instanceof Map) { + const map: ([unknown, unknown] | LocalValue)[] = []; + + argument.forEach((value, key) => { + const objectKey = typeof key === 'string' ? key : LocalValue.getArgument(key); + const objectValue = LocalValue.getArgument(value); + map.push([objectKey, objectValue]); + }); + return LocalValue.createMapValue(argument as Map<unknown, unknown>); + } + if (argument instanceof Set) { + const set: LocalValue[] = []; + argument.forEach((value) => { + set.push(LocalValue.getArgument(value)); + }); + return LocalValue.createSetValue(set); + } + if (argument instanceof Array) { + const arr: LocalValue[] = []; + argument.forEach((value) => { + arr.push(LocalValue.getArgument(value)); + }); + return LocalValue.createArrayValue(arr); + } + if (argument instanceof RegExp) { + return LocalValue.createRegularExpressionValue({ + pattern: argument.source, + flags: argument.flags, + }); + } + + return LocalValue.createObjectValue(argument as Record<string | number | symbol, unknown>); + } + + throw new Error(`Unsupported type: ${type}`); + } + + asMap() { + return { + [TYPE_CONSTANT]: this.type, + ...(!(this.type === PrimitiveType.Null || this.type === PrimitiveType.Undefined) + ? { [VALUE_CONSTANT]: this.value } + : {}), + } as ScriptLocalValue; + } +} diff --git a/src/utils/remote-value.ts b/src/utils/remote-value.ts new file mode 100644 index 00000000000..83223423d3f --- /dev/null +++ b/src/utils/remote-value.ts @@ -0,0 +1,120 @@ +import { NonPrimitiveType, PrimitiveType, TYPE_CONSTANT, VALUE_CONSTANT } from './constants'; +import type { ScriptListLocalValue, ScriptLocalValue, ScriptRegExpValue } from './types'; + +/** + * RemoteValue class for deserializing LocalValue serialized objects back into their original form + */ +export class RemoteValue { + /** + * Deserializes a LocalValue serialized object back to its original JavaScript representation + * + * @param serialized The serialized LocalValue object + * @returns The original JavaScript value/object + */ + static fromLocalValue(serialized: ScriptLocalValue): any { + const type = serialized[TYPE_CONSTANT]; + const value = VALUE_CONSTANT in serialized ? serialized[VALUE_CONSTANT] : undefined; + + switch (type) { + case PrimitiveType.String: + return value; + + case PrimitiveType.Boolean: + return value; + + case PrimitiveType.BigInt: + return BigInt(value as string); + + case PrimitiveType.Undefined: + return undefined; + + case PrimitiveType.Null: + return null; + + case PrimitiveType.Number: + if (value === 'NaN') return NaN; + if (value === '-0') return -0; + if (value === 'Infinity') return Infinity; + if (value === '-Infinity') return -Infinity; + return value; + + case NonPrimitiveType.Array: + return (value as ScriptLocalValue[]).map((item: ScriptLocalValue) => RemoteValue.fromLocalValue(item)); + + case NonPrimitiveType.Date: + return new Date(value as string); + + case NonPrimitiveType.Map: + const map = new Map(); + for (const [key, val] of value as unknown as [string, ScriptLocalValue][]) { + const deserializedKey = typeof key === 'object' && key !== null ? RemoteValue.fromLocalValue(key) : key; + const deserializedValue = RemoteValue.fromLocalValue(val); + map.set(deserializedKey, deserializedValue); + } + return map; + + case NonPrimitiveType.Object: + const obj: Record<string, any> = {}; + for (const [key, val] of value as unknown as [string, ScriptLocalValue][]) { + obj[key] = RemoteValue.fromLocalValue(val); + } + return obj; + + case NonPrimitiveType.RegularExpression: + const { pattern, flags } = value as ScriptRegExpValue; + return new RegExp(pattern, flags); + + case NonPrimitiveType.Set: + const set = new Set(); + for (const item of value as unknown as ScriptListLocalValue) { + set.add(RemoteValue.fromLocalValue(item)); + } + return set; + + case NonPrimitiveType.Symbol: + return Symbol(value as string); + + default: + throw new Error(`Unsupported type: ${type}`); + } + } + + /** + * Utility method to deserialize multiple LocalValues at once + * + * @param serializedValues Array of serialized LocalValue objects + * @returns Array of deserialized JavaScript values + */ + static fromLocalValueArray(serializedValues: ScriptLocalValue[]): any[] { + return serializedValues.map((value) => RemoteValue.fromLocalValue(value)); + } + + /** + * Verifies if the given object matches the structure of a serialized LocalValue + * + * @param obj Object to verify + * @returns boolean indicating if the object has LocalValue structure + */ + static isLocalValueObject(obj: any): boolean { + if (typeof obj !== 'object' || obj === null) { + return false; + } + + if (!obj.hasOwnProperty(TYPE_CONSTANT)) { + return false; + } + + const type = obj[TYPE_CONSTANT]; + const hasTypeProperty = Object.values({ ...PrimitiveType, ...NonPrimitiveType }).includes(type); + + if (!hasTypeProperty) { + return false; + } + + if (type !== PrimitiveType.Null && type !== PrimitiveType.Undefined) { + return obj.hasOwnProperty(VALUE_CONSTANT); + } + + return true; + } +} diff --git a/src/utils/serialize.ts b/src/utils/serialize.ts new file mode 100644 index 00000000000..e646f5dac35 --- /dev/null +++ b/src/utils/serialize.ts @@ -0,0 +1,35 @@ +import { SERIALIZED_PREFIX } from './constants'; +import { LocalValue } from './local-value'; +import { RemoteValue } from './remote-value'; + +/** + * Serialize a value to a string that can be deserialized later. + * @param {unknown} value - The value to serialize. + * @returns {string} A string that can be deserialized later. + */ +export function serializeProperty(value: unknown) { + /** + * If the value is a primitive type, return it as is. + */ + if ( + ['string', 'boolean', 'undefined'].includes(typeof value) || + (typeof value === 'number' && value !== Infinity && value !== -Infinity && !isNaN(value)) + ) { + return value as string | number | boolean; + } + + const arg = LocalValue.getArgument(value); + return (SERIALIZED_PREFIX + btoa(JSON.stringify(arg))) as string; +} + +/** + * Deserialize a value from a string that was serialized earlier. + * @param {string} value - The string to deserialize. + * @returns {unknown} The deserialized value. + */ +export function deserializeProperty(value: string) { + if (typeof value !== 'string' || !value.startsWith(SERIALIZED_PREFIX)) { + return value; + } + return RemoteValue.fromLocalValue(JSON.parse(atob(value.slice(SERIALIZED_PREFIX.length)))); +} diff --git a/src/utils/test/serialize.spec.ts b/src/utils/test/serialize.spec.ts new file mode 100644 index 00000000000..b37a2cdd035 --- /dev/null +++ b/src/utils/test/serialize.spec.ts @@ -0,0 +1,113 @@ +import { deserializeProperty, serializeProperty } from '../serialize'; + +describe('serialize', () => { + it('should serialize and deserialize a simple object', () => { + const toSerialize = { foo: 'bar' }; + expect(deserializeProperty(serializeProperty(toSerialize) as string)).toEqual(toSerialize); + }); + + it('should serialize and deserialize a simple array', () => { + const toSerialize = [1, 2, 3]; + expect(deserializeProperty(serializeProperty(toSerialize) as string)).toEqual(toSerialize); + }); + + describe('should serialize and deserialize special number values', () => { + const specialNumbers = [NaN, Infinity, -Infinity, -0]; + specialNumbers.forEach((num) => { + it(`should serialize and deserialize ${num}`, () => { + const serialized = serializeProperty(num) as string; + const deserialized = deserializeProperty(serialized); + if (Number.isNaN(num)) { + expect(Number.isNaN(deserialized)).toBe(true); + } else { + expect(Object.is(deserialized, num)).toBe(true); + } + }); + }); + }); + + it('should serialize and deserialize complex nested structures', () => { + const date = new Date('2024-01-01'); + const regex = /test/gi; + const toSerialize = { + array: [1, 'two', { three: 3 }], + date: date, + regex: regex, + nested: { + map: new Map([['key', 'value']]), + set: new Set([1, 2, 3]), + }, + }; + + const deserialized = deserializeProperty(serializeProperty(toSerialize) as string); + + expect(deserialized.array).toEqual([1, 'two', { three: 3 }]); + expect(deserialized.date).toEqual(date); + expect(deserialized.regex.source).toBe(regex.source); + expect(deserialized.regex.flags).toBe(regex.flags); + expect(deserialized.nested.map.get('key')).toBe('value'); + expect(Array.from(deserialized.nested.set)).toEqual([1, 2, 3]); + }); + + it('should serialize and deserialize Map with non-string keys', () => { + const map = new Map([ + [1, 'number key'], + [{ complex: 'key' }, 'object key'], + ['string', 'string key'], + ] as any); + + const deserialized = deserializeProperty(serializeProperty(map) as string); + expect(deserialized.get(1)).toBe('number key'); + expect(deserialized.get('string')).toBe('string key'); + // Note: object keys will be serialized/deserialized but won't be strictly equal + expect(Array.from(deserialized.entries()).length).toBe(3); + }); + + it('should serialize and deserialize Symbols', () => { + const symbolKey = Symbol('test'); + const obj = { + [symbolKey]: 'symbol value', + regularKey: Symbol('another'), + }; + + const deserialized = deserializeProperty(serializeProperty(obj) as string); + expect(deserialized.regularKey.description).toBe('another'); + }); + + it('should handle null and undefined values', () => { + const toSerialize: any = { + nullValue: null, + undefinedValue: undefined, + nested: { + nullValue: null, + undefinedValue: undefined, + }, + }; + + const deserialized = deserializeProperty(serializeProperty(toSerialize) as string); + expect(deserialized.nullValue).toBeNull(); + expect(deserialized.undefinedValue).toBeUndefined(); + expect(deserialized.nested.nullValue).toBeNull(); + expect(deserialized.nested.undefinedValue).toBeUndefined(); + }); + + it('should serialize and deserialize BigInt values', () => { + const toSerialize = { + bigInt: BigInt('9007199254740991'), + nested: { + bigInt: BigInt('1234567890'), + }, + }; + + const deserialized = deserializeProperty(serializeProperty(toSerialize) as string); + expect(deserialized.bigInt).toBe(BigInt('9007199254740991')); + expect(deserialized.nested.bigInt).toBe(BigInt('1234567890')); + }); + + it('should handle circular references gracefully', () => { + const circular: any = { foo: 'bar' }; + circular.self = circular; + + expect(() => serializeProperty(circular)).toThrow(); + }); +}); diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 00000000000..910ae5af3ea --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,95 @@ +export type Serializeable = string | number | boolean | unknown; +export type LocalValueParam = Serializeable | Serializeable[] | [Serializeable, Serializeable][]; + +export type ScriptLocalValue = + | ScriptPrimitiveProtocolValue + | ScriptArrayLocalValue + | ScriptDateLocalValue + | ScriptSymbolValue + | ScriptMapLocalValue + | ScriptObjectLocalValue + | ScriptRegExpLocalValue + | ScriptSetLocalValue; +export type ScriptListLocalValue = ScriptLocalValue[]; + +export interface ScriptArrayLocalValue { + type: 'array'; + value: ScriptListLocalValue; +} + +export interface ScriptDateLocalValue { + type: 'date'; + value: string; +} + +export type ScriptMappingLocalValue = (ScriptLocalValue | ScriptLocalValue)[]; + +export interface ScriptMapLocalValue { + type: 'map'; + value: ScriptMappingLocalValue; +} + +export interface ScriptObjectLocalValue { + type: 'object'; + value: ScriptMappingLocalValue; +} + +export interface ScriptRegExpValue { + pattern: string; + flags?: string; +} + +export interface ScriptRegExpLocalValue { + type: 'regexp'; + value: ScriptRegExpValue; +} + +export interface ScriptSetLocalValue { + type: 'set'; + value: ScriptListLocalValue; +} + +export type ScriptPreloadScript = string; +export type ScriptRealm = string; +export type ScriptPrimitiveProtocolValue = + | ScriptUndefinedValue + | ScriptNullValue + | ScriptStringValue + | ScriptNumberValue + | ScriptBooleanValue + | ScriptBigIntValue; + +export interface ScriptUndefinedValue { + type: 'undefined'; +} + +export interface ScriptNullValue { + type: 'null'; +} + +export interface ScriptStringValue { + type: 'string'; + value: string; +} + +export interface ScriptSymbolValue { + type: 'symbol'; + value: string; +} + +export type ScriptSpecialNumber = 'NaN' | '-0' | 'Infinity' | '-Infinity'; + +export interface ScriptNumberValue { + type: 'number'; + value: number | ScriptSpecialNumber; +} + +export interface ScriptBooleanValue { + type: 'boolean'; + value: boolean; +} + +export interface ScriptBigIntValue { + type: 'bigint'; + value: string; +} diff --git a/test/end-to-end/package-lock.json b/test/end-to-end/package-lock.json index a75adcf8f30..d77ceb794b2 100644 --- a/test/end-to-end/package-lock.json +++ b/test/end-to-end/package-lock.json @@ -25,7 +25,7 @@ }, "../..": { "name": "@stencil/core", - "version": "4.27.1", + "version": "4.28.2", "dev": true, "license": "MIT", "bin": { @@ -33,11 +33,12 @@ }, "devDependencies": { "@ionic/prettier-config": "^4.0.0", - "@rollup/plugin-commonjs": "21.1.0", + "@jridgewell/source-map": "^0.3.6", + "@rollup/plugin-commonjs": "28.0.2", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "9.0.0", - "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.3", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-replace": "6.0.2", + "@rollup/pluginutils": "5.1.4", "@types/eslint": "^8.4.6", "@types/exit": "^0.1.31", "@types/fs-extra": "^11.0.0", @@ -84,16 +85,16 @@ "node-fetch": "3.3.2", "open": "^9.0.0", "open-in-editor": "2.2.0", - "parse5": "7.1.2", + "parse5": "7.2.1", "pixelmatch": "5.3.0", "postcss": "^8.2.8", "prettier": "3.3.1", "prompts": "2.4.2", "puppeteer": "^24.1.0", "rimraf": "^6.0.1", - "rollup": "2.56.3", + "rollup": "4.34.9", "semver": "^7.3.7", - "terser": "5.31.1", + "terser": "5.37.0", "tsx": "^4.19.2", "typescript": "~5.5.4", "webpack": "^5.75.0", @@ -102,6 +103,16 @@ "engines": { "node": ">=16.0.0", "npm": ">=7.10.0" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9" } }, "node_modules/@ampproject/remapping": { @@ -5354,11 +5365,20 @@ "version": "file:../..", "requires": { "@ionic/prettier-config": "^4.0.0", - "@rollup/plugin-commonjs": "21.1.0", + "@jridgewell/source-map": "^0.3.6", + "@rollup/plugin-commonjs": "28.0.2", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "9.0.0", - "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.3", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-replace": "6.0.2", + "@rollup/pluginutils": "5.1.4", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", "@types/eslint": "^8.4.6", "@types/exit": "^0.1.31", "@types/fs-extra": "^11.0.0", @@ -5405,16 +5425,16 @@ "node-fetch": "3.3.2", "open": "^9.0.0", "open-in-editor": "2.2.0", - "parse5": "7.1.2", + "parse5": "7.2.1", "pixelmatch": "5.3.0", "postcss": "^8.2.8", "prettier": "3.3.1", "prompts": "2.4.2", "puppeteer": "^24.1.0", "rimraf": "^6.0.1", - "rollup": "2.56.3", + "rollup": "4.34.9", "semver": "^7.3.7", - "terser": "5.31.1", + "terser": "5.37.0", "tsx": "^4.19.2", "typescript": "~5.5.4", "webpack": "^5.75.0", diff --git a/test/end-to-end/src/car-detail/readme.md b/test/end-to-end/src/car-detail/readme.md index 9e96ff7d5a9..323bf9db021 100644 --- a/test/end-to-end/src/car-detail/readme.md +++ b/test/end-to-end/src/car-detail/readme.md @@ -9,7 +9,7 @@ | Property | Attribute | Description | Type | Default | | -------- | --------- | ----------- | --------- | ----------- | -| `car` | -- | | `CarData` | `undefined` | +| `car` | `car` | | `CarData` | `undefined` | ## Dependencies diff --git a/test/end-to-end/src/car-list/readme.md b/test/end-to-end/src/car-list/readme.md index 5c7b1d348cb..d79a51cc217 100644 --- a/test/end-to-end/src/car-list/readme.md +++ b/test/end-to-end/src/car-list/readme.md @@ -11,10 +11,10 @@ Component that helps display a list of cars ## Properties -| Property | Attribute | Description | Type | Default | -| ---------- | --------- | ----------- | ----------- | ----------- | -| `cars` | -- | | `CarData[]` | `undefined` | -| `selected` | -- | | `CarData` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ---------- | ---------- | ----------- | ----------- | ----------- | +| `cars` | `cars` | | `CarData[]` | `undefined` | +| `selected` | `selected` | | `CarData` | `undefined` | ## Events diff --git a/test/end-to-end/src/declarative-shadow-dom/readme.md b/test/end-to-end/src/declarative-shadow-dom/readme.md index c9cd4edb487..318e4a524f5 100644 --- a/test/end-to-end/src/declarative-shadow-dom/readme.md +++ b/test/end-to-end/src/declarative-shadow-dom/readme.md @@ -11,10 +11,10 @@ Component that helps display a list of cars ## Properties -| Property | Attribute | Description | Type | Default | -| ---------- | --------- | ----------- | ----------- | ----------- | -| `cars` | -- | | `CarData[]` | `undefined` | -| `selected` | -- | | `CarData` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ---------- | ---------- | ----------- | ----------- | ----------- | +| `cars` | `cars` | | `CarData[]` | `undefined` | +| `selected` | `selected` | | `CarData` | `undefined` | ## Events diff --git a/test/end-to-end/src/ssr-runtime-decorators/ssr-runtime-decorators.e2e.ts b/test/end-to-end/src/ssr-runtime-decorators/ssr-runtime-decorators.e2e.ts index 0b07cc01676..e9953983867 100644 --- a/test/end-to-end/src/ssr-runtime-decorators/ssr-runtime-decorators.e2e.ts +++ b/test/end-to-end/src/ssr-runtime-decorators/ssr-runtime-decorators.e2e.ts @@ -108,7 +108,7 @@ describe('different types of decorated properties and states render on both serv expect(await txt('decoratedState')).toBe('10'); }); - it('renders different values on different component instances ', async () => { + it('renders different values on different component instances', async () => { const doc = await renderToString(` <runtime-decorators></runtime-decorators> <runtime-decorators diff --git a/test/wdio/complex-properties/__snapshots__/cmp.test.tsx.snap b/test/wdio/complex-properties/__snapshots__/cmp.test.tsx.snap new file mode 100644 index 00000000000..6165cfe0c3c --- /dev/null +++ b/test/wdio/complex-properties/__snapshots__/cmp.test.tsx.snap @@ -0,0 +1,43 @@ +// Snapshot v1 + +exports[`complex-properties > should render complex properties 1`] = ` +"<complex-properties baz="serialized:eyJ0eXBlIjoibWFwIiwidmFsdWUiOltbeyJ0eXBlIjoic3RyaW5nIiwidmFsdWUiOiJmb28ifSx7InR5cGUiOiJvYmplY3QiLCJ2YWx1ZSI6W1sicXV4Iix7InR5cGUiOiJzeW1ib2wiLCJ2YWx1ZSI6InF1dXgifV1dfV1dfQ==" class="hydrated" corge="serialized:eyJ0eXBlIjoic2V0IiwidmFsdWUiOlt7InR5cGUiOiJvYmplY3QiLCJ2YWx1ZSI6W1siZm9vIix7InR5cGUiOiJvYmplY3QiLCJ2YWx1ZSI6W1siYmFyIix7InR5cGUiOiJzdHJpbmciLCJ2YWx1ZSI6ImZvbyJ9XV19XV19XX0=" foo="serialized:eyJ0eXBlIjoib2JqZWN0IiwidmFsdWUiOltbImJhciIseyJ0eXBlIjoibnVtYmVyIiwidmFsdWUiOjEyM31dLFsibG9vIix7InR5cGUiOiJhcnJheSIsInZhbHVlIjpbeyJ0eXBlIjoibnVtYmVyIiwidmFsdWUiOjF9LHsidHlwZSI6Im51bWJlciIsInZhbHVlIjoyfSx7InR5cGUiOiJudW1iZXIiLCJ2YWx1ZSI6M31dfV0sWyJxdXgiLHsidHlwZSI6Im9iamVjdCIsInZhbHVlIjpbWyJxdXV4Iix7InR5cGUiOiJzeW1ib2wiLCJ2YWx1ZSI6InF1dXgifV1dfV1dfQ==" grault="serialized:eyJ0eXBlIjoibnVtYmVyIiwidmFsdWUiOiJJbmZpbml0eSJ9" kids-names="serialized:eyJ0eXBlIjoiYXJyYXkiLCJ2YWx1ZSI6W3sidHlwZSI6InN0cmluZyIsInZhbHVlIjoiSm9obiJ9LHsidHlwZSI6InN0cmluZyIsInZhbHVlIjoiSmFuZSJ9LHsidHlwZSI6InN0cmluZyIsInZhbHVlIjoiSmltIn1dfQ==" quux="serialized:eyJ0eXBlIjoic2V0IiwidmFsdWUiOlt7InR5cGUiOiJzdHJpbmciLCJ2YWx1ZSI6ImZvbyJ9XX0=" s-id="1" waldo="serialized:eyJ0eXBlIjoibnVsbCJ9"> + <template shadowrootmode="open"> + <ul c-id="1.0.0.0"> + <li c-id="1.1.1.0"> + <!--t.1.2.2.0--> + this.foo.bar: 123 + </li> + <li c-id="1.3.1.1"> + <!--t.1.4.2.0--> + this.foo.loo: 1, 2, 3 + </li> + <li c-id="1.5.1.2"> + <!--t.1.6.2.0--> + this.foo.qux: symbol + </li> + <li c-id="1.7.1.3"> + <!--t.1.8.2.0--> + this.baz.get('foo'): symbol + </li> + <li c-id="1.9.1.4"> + <!--t.1.10.2.0--> + this.quux.has('foo'): true + </li> + <li c-id="1.11.1.5"> + <!--t.1.12.2.0--> + this.grault: true + </li> + <li c-id="1.13.1.6"> + <!--t.1.14.2.0--> + this.waldo: true + </li> + <li c-id="1.15.1.7"> + <!--t.1.16.2.0--> + this.kidsNames: John, Jane, Jim + </li> + </ul> + </template> + <!--r.1--> +</complex-properties>" +`; diff --git a/test/wdio/complex-properties/cmp.test.tsx b/test/wdio/complex-properties/cmp.test.tsx new file mode 100644 index 00000000000..25451a8a86c --- /dev/null +++ b/test/wdio/complex-properties/cmp.test.tsx @@ -0,0 +1,63 @@ +/// <reference types="../dist/components.d.ts" /> +import { render } from '@wdio/browser-runner/stencil'; +import { $, expect } from '@wdio/globals'; + +import { renderToString, serializeProperty } from '../hydrate/index.mjs'; + +const template = `<complex-properties + foo=${serializeProperty({ bar: 123, loo: [1, 2, 3], qux: { quux: Symbol('quux') } })} + baz=${serializeProperty(new Map([['foo', { qux: Symbol('quux') }]]))} + quux=${serializeProperty(new Set(['foo']))} + corge=${serializeProperty(new Set([{ foo: { bar: 'foo' } }]))} + grault=${serializeProperty(Infinity)} + waldo=${serializeProperty(null)} + kids-names=${serializeProperty(['John', 'Jane', 'Jim'])} +/>`; + +describe('complex-properties', () => { + it('should render complex properties', async () => { + const { html } = await renderToString(template, { + prettyHtml: true, + fullDocument: false, + }); + expect(html).toMatchSnapshot(); + }); + + it('can render component and update properties', async () => { + const { html } = await renderToString(template, { + fullDocument: false, + }); + + render({ html, components: [] }); + await expect($('complex-properties')).toHaveText( + [ + `this.foo.bar: 123`, + `this.foo.loo: 1, 2, 3`, + `this.foo.qux: symbol`, + `this.baz.get('foo'): symbol`, + `this.quux.has('foo'): true`, + `this.grault: true`, + `this.waldo: true`, + `this.kidsNames: John, Jane, Jim`, + ].join('\n'), + ); + }); + + it('can change a complex property and see it updated correctly', async () => { + const elm = document.querySelector('complex-properties') as HTMLComplexPropertiesElement; + elm.foo = { bar: '456', loo: [4, 5, 6], qux: { quux: Symbol('new quux') } }; + elm.kidsNames.push('Jill'); + await expect(elm).toHaveText( + [ + `this.foo.bar: 456`, + `this.foo.loo: 4, 5, 6`, + `this.foo.qux: symbol`, + `this.baz.get('foo'): symbol`, + `this.quux.has('foo'): true`, + `this.grault: true`, + `this.waldo: true`, + `this.kidsNames: John, Jane, Jim, Jill`, + ].join('\n'), + ); + }); +}); diff --git a/test/wdio/complex-properties/cmp.tsx b/test/wdio/complex-properties/cmp.tsx new file mode 100644 index 00000000000..158f2c0dc33 --- /dev/null +++ b/test/wdio/complex-properties/cmp.tsx @@ -0,0 +1,68 @@ +import { Component, h, Prop } from '@stencil/core'; + +@Component({ + tag: 'complex-properties', + shadow: true, +}) +export class ComplexProperties { + /** + * basic object + */ + @Prop() foo: { bar: string; loo: number[]; qux: { quux: symbol } }; + + /** + * map objects + */ + @Prop() baz: Map<string, { qux: symbol }>; + + /** + * set objects + */ + @Prop() quux: Set<string>; + + /** + * infinity + */ + @Prop() grault: typeof Infinity; + + /** + * null + */ + @Prop() waldo: null; + + /** + * basic array + */ + @Prop() kidsNames: string[]; + + render() { + return ( + <ul> + <li> + {`this.foo.bar`}: {this.foo.bar} + </li> + <li> + {`this.foo.loo`}: {this.foo.loo.join(', ')} + </li> + <li> + {`this.foo.qux`}: {typeof this.foo.qux.quux} + </li> + <li> + {`this.baz.get('foo')`}: {typeof this.baz.get('foo')?.qux} + </li> + <li> + {`this.quux.has('foo')`}: {this.quux.has('foo') ? 'true' : 'false'} + </li> + <li> + {`this.grault`}: {this.grault === Infinity ? 'true' : 'false'} + </li> + <li> + {`this.waldo`}: {this.waldo === null ? 'true' : 'false'} + </li> + <li> + {`this.kidsNames`}: {this.kidsNames?.join(', ')} + </li> + </ul> + ); + } +} diff --git a/test/wdio/declarative-shadow-dom/cmp.test.tsx b/test/wdio/declarative-shadow-dom/cmp.test.tsx index 0d04224debf..9ceb8a46bee 100644 --- a/test/wdio/declarative-shadow-dom/cmp.test.tsx +++ b/test/wdio/declarative-shadow-dom/cmp.test.tsx @@ -1,4 +1,5 @@ import { render } from '@wdio/browser-runner/stencil'; +import { $, expect } from '@wdio/globals'; import { renderToString } from '../hydrate/index.mjs'; @@ -11,7 +12,7 @@ describe('dsd-cmp', () => { }); expect(html).toContain('I am rendered on the Server!'); - render({ html }); + render({ html, components: [] }); await expect($('dsd-cmp')).toHaveText('I am rendered on the Client!'); }); }); diff --git a/test/wdio/package-lock.json b/test/wdio/package-lock.json index 90e4e5e4271..c7040981f1d 100644 --- a/test/wdio/package-lock.json +++ b/test/wdio/package-lock.json @@ -26,7 +26,7 @@ }, "../..": { "name": "@stencil/core", - "version": "4.27.1", + "version": "4.28.2", "dev": true, "license": "MIT", "bin": { @@ -34,11 +34,12 @@ }, "devDependencies": { "@ionic/prettier-config": "^4.0.0", - "@rollup/plugin-commonjs": "21.1.0", + "@jridgewell/source-map": "^0.3.6", + "@rollup/plugin-commonjs": "28.0.2", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "9.0.0", - "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.3", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-replace": "6.0.2", + "@rollup/pluginutils": "5.1.4", "@types/eslint": "^8.4.6", "@types/exit": "^0.1.31", "@types/fs-extra": "^11.0.0", @@ -85,16 +86,16 @@ "node-fetch": "3.3.2", "open": "^9.0.0", "open-in-editor": "2.2.0", - "parse5": "7.1.2", + "parse5": "7.2.1", "pixelmatch": "5.3.0", "postcss": "^8.2.8", "prettier": "3.3.1", "prompts": "2.4.2", "puppeteer": "^24.1.0", "rimraf": "^6.0.1", - "rollup": "2.56.3", + "rollup": "4.34.9", "semver": "^7.3.7", - "terser": "5.31.1", + "terser": "5.37.0", "tsx": "^4.19.2", "typescript": "~5.5.4", "webpack": "^5.75.0", @@ -103,6 +104,16 @@ "engines": { "node": ">=16.0.0", "npm": ">=7.10.0" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9" } }, "node_modules/@ampproject/remapping": { diff --git a/test/wdio/package.json b/test/wdio/package.json index 4f55ff8a066..7825b9d01d9 100644 --- a/test/wdio/package.json +++ b/test/wdio/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "scripts": { "build": "run-s build.no-external-runtime build.test-sibling build.main build.global-script build.prerender build.invisible-prehydration build.es2022", - "build.main": "node ../../bin/stencil build --debug --es5", + "build.main": "node ../../bin/stencil build --debug --es5 && cp src/components.d.ts dist/components.d.ts", "build.es2022": "node ../../bin/stencil build --debug --config stencil.config-es2022.ts", "build.global-script": "node ../../bin/stencil build --debug --es5 --config global-script.stencil.config.ts", "build.test-sibling": "cd test-sibling && npm run build",