diff --git a/.changeset/clean-chicken-retire.md b/.changeset/clean-chicken-retire.md new file mode 100644 index 0000000000..c3beac8f04 --- /dev/null +++ b/.changeset/clean-chicken-retire.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +[core] make scoped-elements ssr-compatible diff --git a/package-lock.json b/package-lock.json index ca35254d8f..596463e3f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@changesets/cli": "^2.27.9", "@custom-elements-manifest/analyzer": "^0.10.3", "@custom-elements-manifest/to-markdown": "^0.1.0", + "@lit-labs/testing": "^0.2.5", "@open-wc/building-rollup": "^2.2.3", "@open-wc/eslint-config": "^12.0.3", "@open-wc/scoped-elements": "^3.0.5", @@ -3399,12 +3400,271 @@ "resolved": "packages/ui", "link": true }, + "node_modules/@lit-labs/ssr": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.2.2.tgz", + "integrity": "sha512-He5TzeNPM9ECmVpgXRYmVlz0UA5YnzHlT43kyLi2Lu6mUidskqJVonk9W5K699+2DKhoXp8Ra4EJmHR6KrcW1Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-client": "^1.1.7", + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "@parse5/tools": "^0.3.0", + "@types/node": "^16.0.0", + "enhanced-resolve": "^5.10.0", + "lit": "^3.1.2", + "lit-element": "^4.0.4", + "lit-html": "^3.1.2", + "node-fetch": "^3.2.8", + "parse5": "^7.1.1" + }, + "engines": { + "node": ">=13.9.0" + } + }, + "node_modules/@lit-labs/ssr-client": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz", + "integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit": "^3.1.2", + "lit-html": "^3.1.2" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", "license": "BSD-3-Clause" }, + "node_modules/@lit-labs/ssr/node_modules/@types/node": { + "version": "16.18.116", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.116.tgz", + "integrity": "sha512-mLigUvhoaADRewggiby+XfAAFOUOMCm/SwL5DAJ+CMUGjSLIGMsJVN7BOKftuQSHGjUmS/W7hVht8fcNbi/MRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lit-labs/ssr/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@lit-labs/ssr/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@lit-labs/testing": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@lit-labs/testing/-/testing-0.2.5.tgz", + "integrity": "sha512-VVYPhnpYhTgmZ3pWGQV8ZN/c81/aUlxSya+G94pNhlAiKUqsAwJZAkQCEZLncF8WHWg9jhas3eswxe9G3oQr1Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr": "^3.1.8", + "@lit-labs/ssr-client": "^1.1.4", + "@web/test-runner-commands": "^0.6.1", + "@webcomponents/template-shadowroot": "^0.1.0", + "lit": "^2.0.0 || ^3.0.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/@web/browser-logs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.2.6.tgz", + "integrity": "sha512-CNjNVhd4FplRY8PPWIAt02vAowJAVcOoTNrR/NNb/o9pka7yI9qdjpWrWhEbPr2pOXonWb52AeAgdK66B8ZH7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "errorstacks": "^2.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/@web/dev-server-core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.4.1.tgz", + "integrity": "sha512-KdYwejXZwIZvb6tYMCqU7yBiEOPfKLQ3V9ezqqEz8DA9V9R3oQWaowckvCpFB9IxxPfS/P8/59OkdzGKQjcIUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^1.3.1", + "chokidar": "^3.4.3", + "clone": "^2.1.2", + "es-module-lexer": "^1.0.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^5.0.0", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^6.0.0", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.4.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/@web/parse5-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.3.1.tgz", + "integrity": "sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/@web/test-runner-commands": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.6.6.tgz", + "integrity": "sha512-2DcK/+7f8QTicQpGFq/TmvKHDK/6Zald6rn1zqRlmj3pcH8fX6KHNVMU60Za9QgAKdorMBPfd8dJwWba5otzdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.10.29", + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/@web/test-runner-core": { + "version": "0.10.29", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.10.29.tgz", + "integrity": "sha512-0/ZALYaycEWswHhpyvl5yqo0uIfCmZe8q14nGPi1dMmNiqLcHjyFGnuIiLexI224AW74ljHcHllmDlXK9FUKGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/babel__code-frame": "^7.0.2", + "@types/co-body": "^6.1.0", + "@types/convert-source-map": "^2.0.0", + "@types/debounce": "^1.2.0", + "@types/istanbul-lib-coverage": "^2.0.3", + "@types/istanbul-reports": "^3.0.0", + "@web/browser-logs": "^0.2.6", + "@web/dev-server-core": "^0.4.1", + "chokidar": "^3.4.3", + "cli-cursor": "^3.1.0", + "co-body": "^6.1.0", + "convert-source-map": "^2.0.0", + "debounce": "^1.2.0", + "dependency-graph": "^0.11.0", + "globby": "^11.0.1", + "ip": "^1.1.5", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.0.2", + "log-update": "^4.0.0", + "nanocolors": "^0.2.1", + "nanoid": "^3.1.25", + "open": "^8.0.2", + "picomatch": "^2.2.2", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@lit-labs/testing/node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lit-labs/testing/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@lit-labs/testing/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@lit-labs/testing/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lit-labs/testing/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/@lit/reactive-element": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", @@ -4134,6 +4394,16 @@ "win32" ] }, + "node_modules/@parse5/tools": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", + "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7896,6 +8166,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@webcomponents/template-shadowroot": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@webcomponents/template-shadowroot/-/template-shadowroot-0.1.0.tgz", + "integrity": "sha512-ry84Vft6xtRBbd4M/ptRodbOLodV5AD15TYhyRghCRgIcJJKmYmJ2v2BaaWxygENwh6Uq3zTfGPmlckKT/GXsQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@webcomponents/webcomponentsjs": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz", @@ -11741,6 +12018,20 @@ } } }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -25400,6 +25691,16 @@ "node": ">=8" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tar-fs": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", @@ -28036,7 +28337,7 @@ "license": "MIT" }, "packages-node/providence-analytics": { - "version": "0.16.8", + "version": "0.17.0", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.7", @@ -28295,7 +28596,7 @@ }, "packages/ui": { "name": "@lion/ui", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "dependencies": { "@bundled-es-modules/message-format": "^6.2.4", diff --git a/package.json b/package.json index 11198a5cd4..fedf7ccf65 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@changesets/cli": "^2.27.9", "@custom-elements-manifest/analyzer": "^0.10.3", "@custom-elements-manifest/to-markdown": "^0.1.0", + "@lit-labs/testing": "^0.2.5", "@open-wc/building-rollup": "^2.2.3", "@open-wc/eslint-config": "^12.0.3", "@open-wc/scoped-elements": "^3.0.5", diff --git a/packages/ui/components/core/src/ScopedElementsMixin.js b/packages/ui/components/core/src/ScopedElementsMixin.js index 24ac8fb4ec..a41f6e9f61 100644 --- a/packages/ui/components/core/src/ScopedElementsMixin.js +++ b/packages/ui/components/core/src/ScopedElementsMixin.js @@ -1,6 +1,31 @@ -/* +import { dedupeMixin } from '@open-wc/dedupe-mixin'; +import { adoptStyles, isServer } from 'lit'; +import { ScopedElementsMixin as OpenWcLitScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; + +/** + * @typedef {import('../../form-core/types/validate/ValidateMixinTypes.js').ScopedElementsMap} ScopedElementsMap + * @typedef {import('@open-wc/dedupe-mixin').Constructor} ScopedElementsHostConstructor + * @typedef {import('@open-wc/scoped-elements/lit-element.js').ScopedElementsHost} ScopedElementsHost + * @typedef {import('./types.js').ScopedElementsHostV2Constructor} ScopedElementsHostV2Constructor + * @typedef {import('@open-wc/dedupe-mixin').Constructor} LitElementConstructor + * @typedef {import('lit').CSSResultOrNative} CSSResultOrNative + * @typedef {typeof import('lit').LitElement} TypeofLitElement + * @typedef {import('lit').LitElement} LitElement + */ + +export function supportsScopedRegistry() { + return Boolean( + // @ts-expect-error + globalThis.ShadowRoot?.prototype.createElement && globalThis.ShadowRoot?.prototype.importNode, + ); +} + +/** * This file is combination of '@open-wc/scoped-elements@v3/lit-element.js' and '@open-wc/scoped-elements@v3/html-element.js'. * Then on top of those, some code from '@open-wc/scoped-elements@v2' is brought to to make polyfill not mandatory. + * This can be a great help for ssr scenarios, allowing elements to be consumed without needing knowledge about internall + * consumption. + * (N.B. at this point in time, this is limited to the scenario where there's one version of lion on the page). * * ## Considerations * In its current state, the [scoped-custom-element-registry](https://github.com/webcomponents/polyfills/tree/master/packages/scoped-custom-element-registry) draft spec has uncertainties: @@ -21,29 +46,7 @@ * This can be beneficial for performance, bundle size, ease of use and SSR capabilities. * * We will keep a close eye on developments in spec and polyfill, and will re-evaluate the scoped-elements approach when the time is right. - */ - -import { dedupeMixin } from '@open-wc/dedupe-mixin'; -import { adoptStyles } from 'lit'; -import { ScopedElementsMixin as OpenWcLitScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; - -/** - * @typedef {import('@open-wc/scoped-elements/lit-element.js').ScopedElementsHost} ScopedElementsHost - * @typedef {import('../../form-core/types/validate/ValidateMixinTypes.js').ScopedElementsMap} ScopedElementsMap - * @typedef {import('lit').CSSResultOrNative} CSSResultOrNative - * @typedef {import('lit').LitElement} LitElement - * @typedef {typeof import('lit').LitElement} TypeofLitElement - * @typedef {import('@open-wc/dedupe-mixin').Constructor} LitElementConstructor - * @typedef {import('@open-wc/dedupe-mixin').Constructor} ScopedElementsHostConstructor - * @typedef {import('./types.js').ScopedElementsHostV2Constructor} ScopedElementsHostV2Constructor - */ - -const supportsScopedRegistry = Boolean( - // @ts-expect-error - ShadowRoot.prototype.createElement && ShadowRoot.prototype.importNode, -); - -/** + * * @template {LitElementConstructor} T * @param {T} superclass * @return {T & ScopedElementsHostConstructor & ScopedElementsHostV2Constructor} @@ -51,8 +54,29 @@ const supportsScopedRegistry = Boolean( const ScopedElementsMixinImplementation = superclass => /** @type {ScopedElementsHost} */ class ScopedElementsHost extends OpenWcLitScopedElementsMixin(superclass) { + constructor() { + super(); + + if (isServer) { + // We are on the server: this means we can't support scoped registries... + // So we must treat it like the "no-polyfill scenario", that registers scoped + // elements used for internal composition on the global registry. + // On the client that would happen in connectedCallback, so we do it here... + // N.B. keep in mind that this does not work when we have multiple element (versions) + // with the same name. (like multiple versions of lion extension layers). + // If we want to support this, we must re-introduce the shim-behavior of ScopedElementsMixin v1 + // to make this work with ssr as well. + // @ts-expect-error + this.registry = customElements; + // @ts-expect-error + for (const [name, klass] of Object.entries(this.constructor.scopedElements || {})) { + this.defineScopedElement(name, klass); + } + } + } + createScopedElement(/** @type {string} */ tagName) { - const root = supportsScopedRegistry ? this.shadowRoot : document; + const root = supportsScopedRegistry() ? this.shadowRoot : document; // @ts-expect-error polyfill to support createElement on shadowRoot is loaded return root.createElement(tagName); } @@ -61,12 +85,12 @@ const ScopedElementsMixinImplementation = superclass => * Defines a scoped element. * * @param {string} tagName - * @param {typeof HTMLElement} klass + * @param {typeof HTMLElement} classToBeRegistered */ - defineScopedElement(tagName, klass) { - // @ts-ignore + defineScopedElement(tagName, classToBeRegistered) { const registeredClass = this.registry.get(tagName); - if (registeredClass && supportsScopedRegistry === false && registeredClass !== klass) { + const isAlreadyRegistered = registeredClass && registeredClass === classToBeRegistered; + if (isAlreadyRegistered && !supportsScopedRegistry()) { // eslint-disable-next-line no-console console.error( [ @@ -80,10 +104,8 @@ const ScopedElementsMixinImplementation = superclass => ); } if (!registeredClass) { - // @ts-ignore - return this.registry.define(tagName, klass); + return this.registry.define(tagName, classToBeRegistered); } - // @ts-ignore return this.registry.get(tagName); } @@ -92,12 +114,12 @@ const ScopedElementsMixinImplementation = superclass => * @returns {ShadowRoot} */ attachShadow(options) { - // @ts-ignore + // @ts-expect-error const { scopedElements } = /** @type {typeof ScopedElementsHost} */ (this.constructor); const shouldCreateRegistry = !this.registry || - // @ts-ignore + // @ts-expect-error (this.registry === this.constructor.__registry && !Object.prototype.hasOwnProperty.call(this.constructor, '__registry')); @@ -108,8 +130,7 @@ const ScopedElementsMixinImplementation = superclass => * This is important specifically for superclasses/inheritance */ if (shouldCreateRegistry) { - // @ts-ignore - this.registry = supportsScopedRegistry ? new CustomElementRegistry() : customElements; + this.registry = supportsScopedRegistry() ? new CustomElementRegistry() : customElements; for (const [tagName, klass] of Object.entries(scopedElements ?? {})) { this.defineScopedElement(tagName, klass); } @@ -131,7 +152,7 @@ const ScopedElementsMixinImplementation = superclass => ); const createdRoot = this.attachShadow(shadowRootOptions); - if (supportsScopedRegistry) { + if (supportsScopedRegistry()) { // @ts-expect-error this.renderOptions.creationScope = createdRoot; } diff --git a/packages/ui/components/core/test/ScopedElementsMixin.test.js b/packages/ui/components/core/test/ScopedElementsMixin.test.js new file mode 100644 index 0000000000..bbebe37367 --- /dev/null +++ b/packages/ui/components/core/test/ScopedElementsMixin.test.js @@ -0,0 +1,131 @@ +import { expect, fixture } from '@open-wc/testing'; +import { + ssrNonHydratedFixture, + ssrHydratedFixture, + csrFixture, +} from '@lit-labs/testing/fixtures.js'; +import { LitElement, html } from 'lit'; +import sinon from 'sinon'; +import { browserDetection } from '../src/browserDetection.js'; + +import { ScopedElementsMixin, supportsScopedRegistry } from '../src/ScopedElementsMixin.js'; + +const hasRealScopedRegistrySupport = supportsScopedRegistry(); +const originalShadowRootProps = { + // @ts-expect-error + createElement: globalThis.ShadowRoot?.prototype.createElement, + // @ts-expect-error + importNode: globalThis.ShadowRoot?.prototype.importNode, +}; + +// Even though the polyfill might be loaded in this test or we run it in a browser supporting these features, +// we mock "no support", so that `supportsScopedRegistry()` returns false inside ScopedElementsMixin.. +function mockNoRegistrySupport() { + // Are we on a server or do we have no polyfill? Nothing to be done here... + if (!hasRealScopedRegistrySupport) return; + + // This will be enough to make the `supportsScopedRegistry()` check fail inside ScopedElementsMixin and bypass scoped registries + globalThis.ShadowRoot = globalThis.ShadowRoot || { prototype: {} }; + // @ts-expect-error + globalThis.ShadowRoot.prototype.createElement = null; +} + +mockNoRegistrySupport.restore = () => { + // Are we on a server or do we have no polyfill? Nothing to be done here... + if (!hasRealScopedRegistrySupport) return; + + // @ts-expect-error + globalThis.ShadowRoot.prototype.createElement = originalShadowRootProps.createElement; + // @ts-expect-error + globalThis.ShadowRoot.prototype.importNode = originalShadowRootProps.importNode; +}; + +class ScopedElementsChild extends LitElement { + render() { + return html`I'm a child`; + } +} + +class ScopedElementsHost extends ScopedElementsMixin(LitElement) { + static scopedElements = { 'scoped-elements-child': ScopedElementsChild }; + + render() { + return html``; + } +} +customElements.define('scoped-elements-host', ScopedElementsHost); + +describe('ScopedElementsMixin', () => { + it('renders child elements correctly (that were not registered yet on global registry)', async () => { + // customElements.define('scoped-elements-child', ScopedElementsChild); + for (const _fixture of [csrFixture, ssrNonHydratedFixture, ssrHydratedFixture]) { + const el = await _fixture(html``, { + // we must provide modules atm + modules: ['./ssr-definitions/ScopedElementsHost.define.js'], + }); + + // Wait for FF support + if (!browserDetection.isFirefox) { + expect( + el.shadowRoot?.querySelector('scoped-elements-child')?.shadowRoot?.innerHTML, + ).to.contain("I'm a child"); + } + + // @ts-expect-error + expect(el.registry.get('scoped-elements-child')).to.not.be.undefined; + } + }); + + describe('When scoped registries are supported', () => { + it('registers elements on local registry', async () => { + const ceDefineSpy = sinon.spy(customElements, 'define'); + + const el = /** @type {ScopedElementsHost} */ ( + await fixture(html``) + ); + + // @ts-expect-error + expect(el.registry.get('scoped-elements-child')).to.equal(ScopedElementsChild); + expect(el.registry).to.not.equal(customElements); + expect(ceDefineSpy.calledWith('scoped-elements-child')).to.be.false; + + ceDefineSpy.restore(); + }); + }); + + describe('When scoped registries are not supported', () => { + class ScopedElementsChildNoReg extends LitElement { + render() { + return html`I'm a child`; + } + } + + class ScopedElementsHostNoReg extends ScopedElementsMixin(LitElement) { + static scopedElements = { 'scoped-elements-child-no-reg': ScopedElementsChildNoReg }; + + render() { + return html``; + } + } + before(() => { + mockNoRegistrySupport(); + customElements.define('scoped-elements-host-no-reg', ScopedElementsHostNoReg); + }); + + after(() => { + mockNoRegistrySupport.restore(); + }); + + it('registers elements', async () => { + const ceDefineSpy = sinon.spy(customElements, 'define'); + + const el = /** @type {ScopedElementsHostNoReg} */ ( + await fixture(html``) + ); + + expect(el.registry).to.equal(customElements); + expect(ceDefineSpy.calledWith('scoped-elements-child-no-reg')).to.be.true; + ceDefineSpy.restore(); + }); + }); +}); diff --git a/packages/ui/components/core/test/ssr-definitions/ScopedElementsHost.define.js b/packages/ui/components/core/test/ssr-definitions/ScopedElementsHost.define.js new file mode 100644 index 0000000000..da56cf6299 --- /dev/null +++ b/packages/ui/components/core/test/ssr-definitions/ScopedElementsHost.define.js @@ -0,0 +1 @@ +// ... empty file needed for '@lit-labs/testing/fixtures.js' diff --git a/packages/ui/components/localize/src/LocalizeManager.js b/packages/ui/components/localize/src/LocalizeManager.js index ed01a2e59c..129855af85 100644 --- a/packages/ui/components/localize/src/LocalizeManager.js +++ b/packages/ui/components/localize/src/LocalizeManager.js @@ -13,7 +13,7 @@ import isLocalizeESModule from './isLocalizeESModule.js'; /** * We can't access `window.document.documentElement` on the server, * so we write to and read from this object on the server. - * N.B.: for now, the goal is to make LocalizeManager not crash on the server, and localizaion happens on the client. + * N.B.: for now, the goal is to make LocalizeManager not crash on the server, and let localization happen on the client. * In the future, we might want to look into more advanced SSR of localized messages */ const documentElement = isServer diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs index df5d7f4b73..79b37f85a0 100644 --- a/web-test-runner.config.mjs +++ b/web-test-runner.config.mjs @@ -1,5 +1,6 @@ import fs from 'fs'; import { playwrightLauncher } from '@web/test-runner-playwright'; +import { litSsrPlugin } from '@lit-labs/testing/web-test-runner-ssr-plugin.js'; const devMode = process.argv.includes('--dev-mode'); @@ -60,4 +61,5 @@ export default { name: pkg.name, files: `${pkg.path}/**/*.test.js`, })), + plugins: [litSsrPlugin()], };