fix(core): make scoped-elements ssr-compatible

fix(core): ssr-support scopedElements
This commit is contained in:
Thijs Louisse 2024-10-30 18:28:29 +01:00 committed by Thijs Louisse
parent fb3bdd6a17
commit 2c38a919bc
8 changed files with 502 additions and 40 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
[core] make scoped-elements ssr-compatible

305
package-lock.json generated
View file

@ -17,6 +17,7 @@
"@changesets/cli": "^2.27.9", "@changesets/cli": "^2.27.9",
"@custom-elements-manifest/analyzer": "^0.10.3", "@custom-elements-manifest/analyzer": "^0.10.3",
"@custom-elements-manifest/to-markdown": "^0.1.0", "@custom-elements-manifest/to-markdown": "^0.1.0",
"@lit-labs/testing": "^0.2.5",
"@open-wc/building-rollup": "^2.2.3", "@open-wc/building-rollup": "^2.2.3",
"@open-wc/eslint-config": "^12.0.3", "@open-wc/eslint-config": "^12.0.3",
"@open-wc/scoped-elements": "^3.0.5", "@open-wc/scoped-elements": "^3.0.5",
@ -3399,12 +3400,271 @@
"resolved": "packages/ui", "resolved": "packages/ui",
"link": true "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": { "node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==",
"license": "BSD-3-Clause" "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": { "node_modules/@lit/reactive-element": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
@ -4134,6 +4394,16 @@
"win32" "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": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -7896,6 +8166,13 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": { "node_modules/@webcomponents/webcomponentsjs": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz", "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": { "node_modules/enquirer": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
@ -25400,6 +25691,16 @@
"node": ">=8" "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": { "node_modules/tar-fs": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
@ -28036,7 +28337,7 @@
"license": "MIT" "license": "MIT"
}, },
"packages-node/providence-analytics": { "packages-node/providence-analytics": {
"version": "0.16.8", "version": "0.17.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/traverse": "^7.25.7", "@babel/traverse": "^7.25.7",
@ -28295,7 +28596,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@lion/ui", "name": "@lion/ui",
"version": "0.8.0", "version": "0.8.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@bundled-es-modules/message-format": "^6.2.4", "@bundled-es-modules/message-format": "^6.2.4",

View file

@ -44,6 +44,7 @@
"@changesets/cli": "^2.27.9", "@changesets/cli": "^2.27.9",
"@custom-elements-manifest/analyzer": "^0.10.3", "@custom-elements-manifest/analyzer": "^0.10.3",
"@custom-elements-manifest/to-markdown": "^0.1.0", "@custom-elements-manifest/to-markdown": "^0.1.0",
"@lit-labs/testing": "^0.2.5",
"@open-wc/building-rollup": "^2.2.3", "@open-wc/building-rollup": "^2.2.3",
"@open-wc/eslint-config": "^12.0.3", "@open-wc/eslint-config": "^12.0.3",
"@open-wc/scoped-elements": "^3.0.5", "@open-wc/scoped-elements": "^3.0.5",

View file

@ -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<ScopedElementsHost>} 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<LitElement>} 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'. * 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. * 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 * ## 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: * 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. * 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. * 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<LitElement>} LitElementConstructor
* @typedef {import('@open-wc/dedupe-mixin').Constructor<ScopedElementsHost>} ScopedElementsHostConstructor
* @typedef {import('./types.js').ScopedElementsHostV2Constructor} ScopedElementsHostV2Constructor
*/
const supportsScopedRegistry = Boolean(
// @ts-expect-error
ShadowRoot.prototype.createElement && ShadowRoot.prototype.importNode,
);
/**
* @template {LitElementConstructor} T * @template {LitElementConstructor} T
* @param {T} superclass * @param {T} superclass
* @return {T & ScopedElementsHostConstructor & ScopedElementsHostV2Constructor} * @return {T & ScopedElementsHostConstructor & ScopedElementsHostV2Constructor}
@ -51,8 +54,29 @@ const supportsScopedRegistry = Boolean(
const ScopedElementsMixinImplementation = superclass => const ScopedElementsMixinImplementation = superclass =>
/** @type {ScopedElementsHost} */ /** @type {ScopedElementsHost} */
class ScopedElementsHost extends OpenWcLitScopedElementsMixin(superclass) { 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) { 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 // @ts-expect-error polyfill to support createElement on shadowRoot is loaded
return root.createElement(tagName); return root.createElement(tagName);
} }
@ -61,12 +85,12 @@ const ScopedElementsMixinImplementation = superclass =>
* Defines a scoped element. * Defines a scoped element.
* *
* @param {string} tagName * @param {string} tagName
* @param {typeof HTMLElement} klass * @param {typeof HTMLElement} classToBeRegistered
*/ */
defineScopedElement(tagName, klass) { defineScopedElement(tagName, classToBeRegistered) {
// @ts-ignore
const registeredClass = this.registry.get(tagName); 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 // eslint-disable-next-line no-console
console.error( console.error(
[ [
@ -80,10 +104,8 @@ const ScopedElementsMixinImplementation = superclass =>
); );
} }
if (!registeredClass) { if (!registeredClass) {
// @ts-ignore return this.registry.define(tagName, classToBeRegistered);
return this.registry.define(tagName, klass);
} }
// @ts-ignore
return this.registry.get(tagName); return this.registry.get(tagName);
} }
@ -92,12 +114,12 @@ const ScopedElementsMixinImplementation = superclass =>
* @returns {ShadowRoot} * @returns {ShadowRoot}
*/ */
attachShadow(options) { attachShadow(options) {
// @ts-ignore // @ts-expect-error
const { scopedElements } = /** @type {typeof ScopedElementsHost} */ (this.constructor); const { scopedElements } = /** @type {typeof ScopedElementsHost} */ (this.constructor);
const shouldCreateRegistry = const shouldCreateRegistry =
!this.registry || !this.registry ||
// @ts-ignore // @ts-expect-error
(this.registry === this.constructor.__registry && (this.registry === this.constructor.__registry &&
!Object.prototype.hasOwnProperty.call(this.constructor, '__registry')); !Object.prototype.hasOwnProperty.call(this.constructor, '__registry'));
@ -108,8 +130,7 @@ const ScopedElementsMixinImplementation = superclass =>
* This is important specifically for superclasses/inheritance * This is important specifically for superclasses/inheritance
*/ */
if (shouldCreateRegistry) { if (shouldCreateRegistry) {
// @ts-ignore this.registry = supportsScopedRegistry() ? new CustomElementRegistry() : customElements;
this.registry = supportsScopedRegistry ? new CustomElementRegistry() : customElements;
for (const [tagName, klass] of Object.entries(scopedElements ?? {})) { for (const [tagName, klass] of Object.entries(scopedElements ?? {})) {
this.defineScopedElement(tagName, klass); this.defineScopedElement(tagName, klass);
} }
@ -131,7 +152,7 @@ const ScopedElementsMixinImplementation = superclass =>
); );
const createdRoot = this.attachShadow(shadowRootOptions); const createdRoot = this.attachShadow(shadowRootOptions);
if (supportsScopedRegistry) { if (supportsScopedRegistry()) {
// @ts-expect-error // @ts-expect-error
this.renderOptions.creationScope = createdRoot; this.renderOptions.creationScope = createdRoot;
} }

View file

@ -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`<span>I'm a child</span>`;
}
}
class ScopedElementsHost extends ScopedElementsMixin(LitElement) {
static scopedElements = { 'scoped-elements-child': ScopedElementsChild };
render() {
return html`<scoped-elements-child></scoped-elements-child>`;
}
}
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`<scoped-elements-host></scoped-elements-host>`, {
// 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("<span>I'm a child</span>");
}
// @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`<scoped-elements-host></scoped-elements-host>`)
);
// @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`<span>I'm a child</span>`;
}
}
class ScopedElementsHostNoReg extends ScopedElementsMixin(LitElement) {
static scopedElements = { 'scoped-elements-child-no-reg': ScopedElementsChildNoReg };
render() {
return html`<scoped-elements-child-no-reg></scoped-elements-child-no-reg>`;
}
}
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`<scoped-elements-host-no-reg></scoped-elements-host-no-reg>`)
);
expect(el.registry).to.equal(customElements);
expect(ceDefineSpy.calledWith('scoped-elements-child-no-reg')).to.be.true;
ceDefineSpy.restore();
});
});
});

View file

@ -0,0 +1 @@
// ... empty file needed for '@lit-labs/testing/fixtures.js'

View file

@ -13,7 +13,7 @@ import isLocalizeESModule from './isLocalizeESModule.js';
/** /**
* We can't access `window.document.documentElement` on the server, * We can't access `window.document.documentElement` on the server,
* so we write to and read from this object 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 * In the future, we might want to look into more advanced SSR of localized messages
*/ */
const documentElement = isServer const documentElement = isServer

View file

@ -1,5 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import { playwrightLauncher } from '@web/test-runner-playwright'; 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'); const devMode = process.argv.includes('--dev-mode');
@ -60,4 +61,5 @@ export default {
name: pkg.name, name: pkg.name,
files: `${pkg.path}/**/*.test.js`, files: `${pkg.path}/**/*.test.js`,
})), })),
plugins: [litSsrPlugin()],
}; };