├── .gitignore ├── src ├── _implementation │ ├── default_implementation.ts │ ├── util.ts │ ├── feature_detect.ts │ ├── mutation_observer.ts │ ├── queryselectorall.ts │ └── manual_walk.ts ├── template-shadowroot.ts └── test │ └── template-shadowroot_test.ts ├── web-test-runner.config.mjs ├── .github └── workflows │ └── test.yml ├── tsconfig.json ├── rollup.config.js ├── bench ├── tach.json ├── many_flat.html └── many_deep.html ├── README.md ├── package.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules/ 3 | .npm 4 | .vscode 5 | 6 | # build output 7 | /template-shadowroot.* 8 | /test/ 9 | /_implementation/ 10 | -------------------------------------------------------------------------------- /src/_implementation/default_implementation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2020 Google LLC 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | // TODO(rictic): do more robust benchmarks and pick a winner. 8 | export {hydrateShadowRoots} from './manual_walk.js'; 9 | -------------------------------------------------------------------------------- /src/template-shadowroot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | import {hasNativeDeclarativeShadowRoots} from './_implementation/feature_detect.js'; 8 | import {hydrateShadowRoots} from './_implementation/default_implementation.js'; 9 | 10 | export {hasNativeDeclarativeShadowRoots, hydrateShadowRoots}; 11 | -------------------------------------------------------------------------------- /web-test-runner.config.mjs: -------------------------------------------------------------------------------- 1 | import { playwrightLauncher } from '@web/test-runner-playwright'; 2 | 3 | export default { 4 | concurrency: 10, 5 | nodeResolve: true, 6 | watch: false, 7 | files: './test/*_test.js', 8 | browsers: [ 9 | playwrightLauncher({ product: 'chromium' }), 10 | playwrightLauncher({ product: 'firefox' }), 11 | playwrightLauncher({ product: 'webkit' }), 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /src/_implementation/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | export const hasNoParentElement = 8 | (e: Element|DocumentFragment): e is DocumentFragment => 9 | e.parentElement === null; 10 | export const isTemplate = (e: Node): e is HTMLTemplateElement => 11 | (e as Partial).tagName === 'TEMPLATE'; 12 | export const isElement = (e: Node): e is HTMLElement => 13 | e.nodeType === Node.ELEMENT_NODE; 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 18 15 | 16 | - name: NPM install 17 | run: npm ci 18 | 19 | - name: Build 20 | run: npm run build 21 | 22 | - name: Install playwright browsers 23 | run: npx playwright install-deps 24 | 25 | - name: Test 26 | run: npm test 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "esnext", 5 | "lib": ["es2019", "dom"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "inlineSources": true, 10 | "outDir": "./", 11 | "rootDir": "./src", 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "moduleResolution": "node", 18 | "esModuleInterop": true 19 | }, 20 | "include": ["src/**/*.ts"], 21 | "exclude": [] 22 | } 23 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {terser} from 'rollup-plugin-terser'; 2 | import sourcemaps from 'rollup-plugin-sourcemaps'; 3 | 4 | export default { 5 | input: 'template-shadowroot.js', 6 | plugins: [ 7 | terser({ 8 | module: true, 9 | warnings: true, 10 | // Prevent Terser from removing the generated `var TemplateShadowRoot = 11 | // ...;` (where `TemplateShadowRoot` is intended for users and never used 12 | // in the bundle itself). 13 | compress: { 14 | top_retain: ['TemplateShadowRoot'], 15 | }, 16 | mangle: { 17 | reserved: ['TemplateShadowRoot'], 18 | }, 19 | }), 20 | sourcemaps(), 21 | ], 22 | output: { 23 | file: 'template-shadowroot.min.js', 24 | format: 'iife', 25 | name: 'TemplateShadowRoot', 26 | sourcemap: true, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /bench/tach.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Polymer/tachometer/master/config.schema.json", 3 | "timeout": 0.001, 4 | "sampleSize": 25, 5 | "benchmarks": [ 6 | { 7 | "measurement": "global", 8 | "browser": "chrome-headless", 9 | "expand": [ 10 | { 11 | "url": "./bench/many_flat.html?manualWalk", 12 | "name": "flat tree: manual walk" 13 | }, 14 | { 15 | "url": "./bench/many_flat.html?querySelector", 16 | "name": "flat tree: querySelectorAll" 17 | }, 18 | { 19 | "url": "./bench/many_deep.html?manualWalk", 20 | "name": "deep tree: manual walk" 21 | }, 22 | { 23 | "url": "./bench/many_deep.html?querySelector", 24 | "name": "deep tree: querySelectorAll" 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/_implementation/feature_detect.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2020 Google LLC 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | // lib.dom.ts is out of date, so declare our own parseFromString here. 8 | interface DOMParser { 9 | parseFromString(string: string, type: DOMParserSupportedType, options?: { 10 | includeShadowRoots: boolean; 11 | }): Document; 12 | } 13 | 14 | // This isn't ideal. Setting .innerHTML is not compatible with some 15 | // TrustedTypes CSP policies. Discussion at: 16 | // https://github.com/mfreed7/declarative-shadow-dom/issues/3 17 | let hasNative: boolean|undefined; 18 | export function hasNativeDeclarativeShadowRoots(): boolean { 19 | if (hasNative === undefined) { 20 | const html = `
`; 21 | const fragment = (new DOMParser() as DOMParser).parseFromString(html, 'text/html', { 22 | includeShadowRoots: true 23 | }); 24 | hasNative = !!fragment.querySelector('div')?.shadowRoot; 25 | } 26 | return hasNative; 27 | } 28 | -------------------------------------------------------------------------------- /src/_implementation/mutation_observer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2020 Google LLC 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | import { hydrateShadowRoots } from './default_implementation.js'; 8 | 9 | import { hasNativeDeclarativeShadowRoots } from './feature_detect.js'; 10 | 11 | export function transformShadowRoots(within: Node = document) { 12 | if (hasNativeDeclarativeShadowRoots()) { 13 | return { mutationObserver: undefined, cleanup() {} }; // do nothing 14 | } 15 | const mutationObserver = new MutationObserver((mutations, _) => { 16 | for (const mutation of mutations) { 17 | // TODO(rictic): test with streamed HTML that pauses in the middle 18 | // of a