From 8f92b11c1940b86b460c2f3a574208b88e1bbecd Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Tue, 13 Aug 2024 17:09:18 -0700 Subject: [PATCH 1/9] fix(runtime): place scoped component styles after preconnect links but before custom styles (#5938) * fix(runtime): place scoped component styles after preconnect links but before custom styles * prettier * make styles appear for custom styles if there are no preconnect links * prettier * prettier --- src/runtime/styles.ts | 21 ++++++- .../src/miscellaneous/renderToString.e2e.ts | 58 +++++++++++++++++-- test/end-to-end/tsconfig.json | 2 +- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index b705d7a0ee3..d12404843ea 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -92,10 +92,27 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet } /** - * attach styles at the end of the head tag if we render shadow components + * attach styles at the end of the head tag if we render scoped components */ if (!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)) { - styleContainerNode.append(styleElm); + if (styleContainerNode.nodeName === 'HEAD') { + /** + * if the page contains preconnect links, we want to insert the styles + * after the last preconnect link to ensure the styles are preloaded + */ + const preconnectLinks = styleContainerNode.querySelectorAll('link[rel=preconnect]'); + const referenceNode = + preconnectLinks.length > 0 + ? preconnectLinks[preconnectLinks.length - 1].nextSibling + : document.querySelector('style'); + styleContainerNode.insertBefore(styleElm, referenceNode); + } else if ('host' in styleContainerNode) { + /** + * if a scoped component is used within a shadow root, we want to insert the styles + * at the beginning of the shadow root node + */ + styleContainerNode.prepend(styleElm, null); + } } /** diff --git a/test/end-to-end/src/miscellaneous/renderToString.e2e.ts b/test/end-to-end/src/miscellaneous/renderToString.e2e.ts index f6601be948c..1125850d033 100644 --- a/test/end-to-end/src/miscellaneous/renderToString.e2e.ts +++ b/test/end-to-end/src/miscellaneous/renderToString.e2e.ts @@ -49,11 +49,16 @@ describe('renderToString', () => { ); }); - it('puts style last in the head tag', async () => { + it('puts style after preconnect links in the head tag', async () => { const { html } = await renderToString( ` + @@ -72,11 +77,52 @@ describe('renderToString', () => { { fullDocument: true, serializeShadowRoot: false }, ); + /** + * expect the scoped component styles to be injected after the preconnect link + */ expect(html).toContain( - '
`, + ); + }); + + it('puts styles before any custom styles', async () => { + const { html } = await renderToString( + ` + + + + + +
+
+ +
+
+ + + + `, + { fullDocument: true, serializeShadowRoot: false }, + ); + + /** + * expect the scoped component styles to be injected before custom styles + */ + expect(html.replaceAll(/\n[ ]*/g, '')).toContain( + '.selected.sc-scoped-car-list{font-weight:bold;background:rgb(255, 255, 210)} ', ); }); @@ -109,8 +155,8 @@ describe('renderToString', () => { /** * renders hydration styles and custom link tag within the head tag */ - expect(html).toContain( - '
', ); }); }); diff --git a/test/end-to-end/tsconfig.json b/test/end-to-end/tsconfig.json index c67ece8da0c..83de5031222 100644 --- a/test/end-to-end/tsconfig.json +++ b/test/end-to-end/tsconfig.json @@ -12,7 +12,7 @@ "jsxFragmentFactory": "Fragment", "lib": [ "dom", - "es2017" + "es2021" ], "module": "esnext", "moduleResolution": "node", From 37a0aaf176db2ad620fad18a3ddc1e64764c237c Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Wed, 14 Aug 2024 08:15:46 -0700 Subject: [PATCH 2/9] fix(compiler): verify parent node when validating component members (#5942) --- src/compiler/transformers/static-to-meta/component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compiler/transformers/static-to-meta/component.ts b/src/compiler/transformers/static-to-meta/component.ts index 15dcf6cb143..5abeaf7c8b1 100644 --- a/src/compiler/transformers/static-to-meta/component.ts +++ b/src/compiler/transformers/static-to-meta/component.ts @@ -201,6 +201,7 @@ const validateComponentMembers = (node: ts.Node) => { /** * the parent node is a class declaration */ + node.parent && ts.isClassDeclaration(node.parent) ) { const propName = node.name.escapedText; From 970c5d25fba3b82df262a154980cc0f25fdd315c Mon Sep 17 00:00:00 2001 From: Tanner Reits <47483144+tanner-reits@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:32:40 -0400 Subject: [PATCH 3/9] fix(runtime): update call to `prepend` to remove `null` node (#5946) --- src/runtime/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index d12404843ea..ad1dc3b269d 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -105,13 +105,13 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet preconnectLinks.length > 0 ? preconnectLinks[preconnectLinks.length - 1].nextSibling : document.querySelector('style'); - styleContainerNode.insertBefore(styleElm, referenceNode); + (styleContainerNode as HTMLElement).insertBefore(styleElm, referenceNode); } else if ('host' in styleContainerNode) { /** * if a scoped component is used within a shadow root, we want to insert the styles * at the beginning of the shadow root node */ - styleContainerNode.prepend(styleElm, null); + (styleContainerNode as HTMLElement).prepend(styleElm); } } From 0e261d653b03fd55a975f4e56e2fae258c3dcd88 Mon Sep 17 00:00:00 2001 From: Tanner Reits <47483144+tanner-reits@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:36:11 -0400 Subject: [PATCH 4/9] fix(compiler): default `asyncLoading` build conditional to `true` (#5941) Fixes: #3580 STENCIL-565 --- src/app-data/index.ts | 2 +- .../cmp-child.tsx | 18 ++++++++++ .../cmp-parent.tsx | 18 ++++++++++ .../cmp-util.ts | 6 ++++ .../cmp.test.tsx | 35 +++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx create mode 100644 test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx create mode 100644 test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts create mode 100644 test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx diff --git a/src/app-data/index.ts b/src/app-data/index.ts index 33c39e13853..7cb73f320c8 100644 --- a/src/app-data/index.ts +++ b/src/app-data/index.ts @@ -105,7 +105,7 @@ export const BUILD: BuildConditionals = { devTools: false, shadowDelegatesFocus: true, initializeNextTick: false, - asyncLoading: false, + asyncLoading: true, asyncQueue: false, transformTagName: false, attachStyles: true, diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx new file mode 100644 index 00000000000..cf4fb3ce233 --- /dev/null +++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx @@ -0,0 +1,18 @@ +import { Component, h } from '@stencil/core'; + +import { createAndAppendElement } from './cmp-util.js'; + +@Component({ + tag: 'custom-elements-hierarchy-lifecycle-child', + shadow: true, +}) +export class CustomElementsHierarchyLifecycleChild { + async componentDidLoad(): Promise { + createAndAppendElement('DID LOAD CHILD'); + return Promise.resolve(); + } + + render() { + return

CHILD CONTENT

; + } +} diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx new file mode 100644 index 00000000000..d9131aa1646 --- /dev/null +++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx @@ -0,0 +1,18 @@ +import { Component, h } from '@stencil/core'; + +import { createAndAppendElement } from './cmp-util.js'; + +@Component({ + tag: 'custom-elements-hierarchy-lifecycle-parent', + shadow: true, +}) +export class CustomElementsHierarchyLifecycleParent { + async componentDidLoad(): Promise { + createAndAppendElement('DID LOAD PARENT'); + return Promise.resolve(); + } + + render() { + return ; + } +} diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts new file mode 100644 index 00000000000..e47bb48baf1 --- /dev/null +++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts @@ -0,0 +1,6 @@ +export const createAndAppendElement = (text: string) => { + const p = document.createElement('p'); + p.textContent = text; + + document.body.appendChild(p); +}; diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx b/test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx new file mode 100644 index 00000000000..c864a91dd92 --- /dev/null +++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx @@ -0,0 +1,35 @@ +import { Fragment, h } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; + +import { defineCustomElement as defineCustomElementChildCmp } from '../test-components/custom-elements-hierarchy-lifecycle-child.js'; +import { defineCustomElement as defineCustomElementParentCmp } from '../test-components/custom-elements-hierarchy-lifecycle-parent.js'; + +describe('custom-elements-hierarchy-lifecycle', () => { + before(() => { + defineCustomElementChildCmp(); + defineCustomElementParentCmp(); + }); + + it('should call componentDidLoad in the child before the parent', async () => { + expect(customElements.get('custom-elements-hierarchy-lifecycle-child')).toBeDefined(); + expect(customElements.get('custom-elements-hierarchy-lifecycle-parent')).toBeDefined(); + + render({ + template: () => ( + <> + + + ), + }); + + const elm = document.querySelector('custom-elements-hierarchy-lifecycle-parent'); + expect(elm.shadowRoot).toBeDefined(); + + await browser.waitUntil(() => Boolean(elm.shadowRoot.querySelector('custom-elements-hierarchy-lifecycle-child'))); + + expect(Array.from(document.querySelectorAll('p')).map((r) => r.textContent)).toEqual([ + 'DID LOAD CHILD', + 'DID LOAD PARENT', + ]); + }); +}); From 0f42656f00a84be52e1c2497159c27cbfb0fba2a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 16 Aug 2024 09:16:07 -0700 Subject: [PATCH 5/9] fix(compiler): prefer `localName` over `originalName` by running an empty check on `originalName` (#5943) * fix(compiler): properly resolve type imports * prettier --- src/compiler/types/generate-app-types.ts | 4 +++- src/compiler/types/tests/generate-app-types.spec.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/compiler/types/generate-app-types.ts b/src/compiler/types/generate-app-types.ts index bff21008d3c..09643f214ee 100644 --- a/src/compiler/types/generate-app-types.ts +++ b/src/compiler/types/generate-app-types.ts @@ -108,7 +108,9 @@ const generateComponentTypesFile = ( return `{ ${typeData .sort(sortImportNames) .map((td) => { - if (td.originalName === td.importName) { + if (td.originalName === '') { + return `${td.localName}`; + } else if (td.originalName === td.importName) { return `${td.originalName}`; } else { return `${td.originalName} as ${td.importName}`; diff --git a/src/compiler/types/tests/generate-app-types.spec.ts b/src/compiler/types/tests/generate-app-types.spec.ts index 234202f6dd9..813fb5c7298 100644 --- a/src/compiler/types/tests/generate-app-types.spec.ts +++ b/src/compiler/types/tests/generate-app-types.spec.ts @@ -1747,6 +1747,11 @@ declare module "@stencil/core" { location: 'import', path: '@utils', }, + Fragment: { + location: 'import', + path: '@stencil/core', + id: '', + }, }, }, }), @@ -1767,7 +1772,9 @@ declare module "@stencil/core" { */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { MyType as UserImplementedPropType } from "@utils"; +import { Fragment } from "@stencil/core"; export { MyType as UserImplementedPropType } from "@utils"; +export { Fragment } from "@stencil/core"; export namespace Components { /** * docs From 7e9fa60d7692e630134618f1186386c0cc0b3a29 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 16 Aug 2024 15:00:29 -0700 Subject: [PATCH 6/9] fix(runtime): only use setter if existing (#5947) * fix(runtime): only use setter if existing * prettier --- src/mock-doc/element.ts | 6 ++++++ src/runtime/vdom/set-accessor.ts | 6 +++++- src/runtime/vdom/test/set-accessor.spec.ts | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mock-doc/element.ts b/src/mock-doc/element.ts index 81576b18ac4..4d4cf8a38eb 100644 --- a/src/mock-doc/element.ts +++ b/src/mock-doc/element.ts @@ -126,6 +126,12 @@ patchPropAttributes( }, ); +Object.defineProperty(MockButtonElement.prototype, 'form', { + get(this: MockElement) { + return this.hasAttribute('form') ? this.getAttribute('form') : null; + }, +}); + export class MockImageElement extends MockHTMLElement { constructor(ownerDocument: any) { super(ownerDocument, 'img'); diff --git a/src/runtime/vdom/set-accessor.ts b/src/runtime/vdom/set-accessor.ts index 950fad94fe2..c4e0aa6f20c 100644 --- a/src/runtime/vdom/set-accessor.ts +++ b/src/runtime/vdom/set-accessor.ts @@ -136,7 +136,11 @@ export const setAccessor = ( if (memberName === 'list') { isProp = false; } else if (oldValue == null || (elm as any)[memberName] != n) { - (elm as any)[memberName] = n; + if (typeof (elm as any).__lookupSetter__(memberName) === 'function') { + (elm as any)[memberName] = n; + } else { + elm.setAttribute(memberName, n); + } } } else { (elm as any)[memberName] = newValue; diff --git a/src/runtime/vdom/test/set-accessor.spec.ts b/src/runtime/vdom/test/set-accessor.spec.ts index ff99235244b..9f9bb4529f3 100644 --- a/src/runtime/vdom/test/set-accessor.spec.ts +++ b/src/runtime/vdom/test/set-accessor.spec.ts @@ -886,4 +886,16 @@ describe('setAccessor for standard html elements', () => { expect(elm.style.cssText).toEqual('margin: 30px; color: orange;'); }); }); + + it('uses setAttribute if element has not setter', () => { + const elm = document.createElement('button'); + const spy = jest.spyOn(elm, 'setAttribute'); + setAccessor(elm, 'form', undefined, 'some-form', false, 0); + expect(spy.mock.calls).toEqual([['form', 'some-form']]); + + const elm2 = document.createElement('button'); + const spy2 = jest.spyOn(elm2, 'setAttribute'); + setAccessor(elm2, 'textContent', undefined, 'some-content', false, 0); + expect(spy2.mock.calls).toEqual([]); + }); }); From ae19d7ad736ee1ae4989a4d0ed08a607ea208b78 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Sat, 17 Aug 2024 13:09:17 -0700 Subject: [PATCH 7/9] fix(runtime): have fallback for style setting (#5948) --- src/runtime/styles.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index ad1dc3b269d..cedf3559d0a 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -112,6 +112,8 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet * at the beginning of the shadow root node */ (styleContainerNode as HTMLElement).prepend(styleElm); + } else { + styleContainerNode.append(styleElm); } } From 307b2420443ccd9902e29668227dcd45ec968997 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:51:49 -0700 Subject: [PATCH 8/9] chore(deps): update dependency eslint-plugin-jsdoc to v50.2.2 (#5949) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5862d038101..c2a22cd18c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1157,15 +1157,15 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", - "integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.48.0.tgz", + "integrity": "sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==", "dev": true, "license": "MIT", "dependencies": { "comment-parser": "1.4.1", "esquery": "^1.6.0", - "jsdoc-type-pratt-parser": "~4.0.0" + "jsdoc-type-pratt-parser": "~4.1.0" }, "engines": { "node": ">=16" @@ -5429,16 +5429,16 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.0.1.tgz", - "integrity": "sha512-UayhAysIk1Du8InV27WMbV4AMSJSu60+bekmeuGK2OUy4QJSFPr1srYT6AInykGkmMdRuHfDX6Q0tJEr8BtDtg==", + "version": "50.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.2.2.tgz", + "integrity": "sha512-i0ZMWA199DG7sjxlzXn5AeYZxpRfMJjDPUl7lL9eJJX8TPRoIaxJU4ys/joP5faM5AXE1eqW/dslCj3uj4Nqpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.46.0", + "@es-joy/jsdoccomment": "~0.48.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", - "debug": "^4.3.5", + "debug": "^4.3.6", "escape-string-regexp": "^4.0.0", "espree": "^10.1.0", "esquery": "^1.6.0", @@ -8638,7 +8638,9 @@ "license": "MIT" }, "node_modules/jsdoc-type-pratt-parser": { - "version": "4.0.0", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, "license": "MIT", "engines": { From 958c826ff384e13079056ecbf452aa3e7d120806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 19 Aug 2024 20:59:59 +0200 Subject: [PATCH 9/9] fix(types): expose missing `JSX.Element` on `h` (#5944) --- src/declarations/stencil-public-runtime.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/declarations/stencil-public-runtime.ts b/src/declarations/stencil-public-runtime.ts index 85caf89d29e..5916ea13c37 100644 --- a/src/declarations/stencil-public-runtime.ts +++ b/src/declarations/stencil-public-runtime.ts @@ -619,6 +619,8 @@ export declare namespace h { export function h(sel: any, data: VNodeData | null, children: VNode): VNode; export namespace JSX { + interface Element extends LocalJSX.Element {} + interface IntrinsicElements extends LocalJSX.IntrinsicElements, JSXBase.IntrinsicElements { [tagName: string]: any; }