@ariel-salgado/vite-plugin-shadow-dom 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.mjs +8 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Ariel Salgado (https://github.com/ariel-salgado)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @ariel-salgado/vite-plugin-shadow-dom
|
|
2
|
+
|
|
3
|
+
A Vite plugin that wraps your app inside a [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), providing true style and DOM encapsulation with no changes to your application code.
|
|
4
|
+
|
|
5
|
+
> **Note:** This plugin is designed and tested for vanilla HTML, JavaScript, and TypeScript projects. Framework support has not been tested.
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
When embedding a Vite app inside a third-party page or a legacy document, the host page's styles inevitably bleed into your app — and yours leak out.
|
|
10
|
+
Shadow DOM is the platform's native solution to this problem. This plugin handles all the wiring so your app runs in a fully isolated shadow tree without you having to restructure anything.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
The plugin finds the element with `id="app"` in your HTML, moves it into a `<template>` tag, replaces it with a shadow host `<div>`, and injects a bootstrap `<script>` that attaches a shadow root and stamps the template into it at runtime.
|
|
15
|
+
|
|
16
|
+
Everything else in `<body>` — any headers, footers, or scripts outside `#app` — is left exactly where it is in the regular document.
|
|
17
|
+
|
|
18
|
+
Given this input:
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<body>
|
|
22
|
+
<div id="app">
|
|
23
|
+
<h1>Hello</h1>
|
|
24
|
+
</div>
|
|
25
|
+
<script type="module" src="/src/main.ts"></script>
|
|
26
|
+
</body>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The plugin produces:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<body>
|
|
33
|
+
<div id="shadow-host" style="display:block;width:100%;height:100%"></div>
|
|
34
|
+
|
|
35
|
+
<template id="shadow-template">
|
|
36
|
+
<div id="app">
|
|
37
|
+
<h1>Hello</h1>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script type="module">
|
|
42
|
+
const host = document.getElementById('shadow-host');
|
|
43
|
+
const shadow = host.attachShadow({ mode: 'open', delegatesFocus: true, serializable: true });
|
|
44
|
+
|
|
45
|
+
const tpl = document.getElementById('shadow-template');
|
|
46
|
+
shadow.appendChild(tpl.content.cloneNode(true));
|
|
47
|
+
|
|
48
|
+
window['__shadowRoot'] = shadow;
|
|
49
|
+
|
|
50
|
+
// Patch document query methods to search the shadow root first
|
|
51
|
+
for (const m of ['getElementById', 'querySelector', 'querySelectorAll']) {
|
|
52
|
+
const orig = document[m].bind(document);
|
|
53
|
+
document[m] = (...args) => {
|
|
54
|
+
const r = shadow[m](...args);
|
|
55
|
+
return r != null && (!('length' in r) || r.length > 0) ? r : orig(...args);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mirror dev-mode <style> injections into the shadow root
|
|
60
|
+
new MutationObserver(mutations => {
|
|
61
|
+
for (const { addedNodes } of mutations)
|
|
62
|
+
for (const node of addedNodes)
|
|
63
|
+
if (node.nodeName === 'STYLE') shadow.appendChild(node.cloneNode(true));
|
|
64
|
+
}).observe(document.head, { childList: true });
|
|
65
|
+
|
|
66
|
+
const link = document.createElement('link');
|
|
67
|
+
link.rel = 'stylesheet';
|
|
68
|
+
link.href = '/assets/index.css';
|
|
69
|
+
shadow.appendChild(link);
|
|
70
|
+
|
|
71
|
+
import('/assets/index.js');
|
|
72
|
+
</script>
|
|
73
|
+
</body>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# npm
|
|
80
|
+
npm install -D @ariel-salgado/vite-plugin-shadow-dom
|
|
81
|
+
|
|
82
|
+
# pnpm
|
|
83
|
+
pnpm add -D @ariel-salgado/vite-plugin-shadow-dom
|
|
84
|
+
|
|
85
|
+
# bun
|
|
86
|
+
bun add -D @ariel-salgado/vite-plugin-shadow-dom
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Requires Vite >= 7.0.0**
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { shadowDOM } from '@ariel-salgado/vite-plugin-shadow-dom';
|
|
95
|
+
// vite.config.ts
|
|
96
|
+
import { defineConfig } from 'vite';
|
|
97
|
+
|
|
98
|
+
export default defineConfig({
|
|
99
|
+
plugins: [shadowDOM()],
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
With options:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { shadowDOM } from '@ariel-salgado/vite-plugin-shadow-dom';
|
|
107
|
+
|
|
108
|
+
export default defineConfig({
|
|
109
|
+
plugins: [
|
|
110
|
+
shadowDOM({
|
|
111
|
+
mode: 'closed',
|
|
112
|
+
cssStrategy: 'constructable',
|
|
113
|
+
formatOutput: false,
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See the [documentation](./docs/plugin-options.md) for a full description of every option.
|
|
120
|
+
|
|
121
|
+
## Dev mode
|
|
122
|
+
|
|
123
|
+
The plugin runs in both dev and production.
|
|
124
|
+
During development, Vite injects CSS as `<style>` elements directly into `document.head` at runtime rather than emitting `<link>` tags. The bootstrap script installs a `MutationObserver` on `document.head` that automatically clones any `<style>` tag into the shadow root as it appears, so hot module replacement and style updates work without any extra configuration.
|
|
125
|
+
|
|
126
|
+
## CSS scoping
|
|
127
|
+
|
|
128
|
+
Built CSS files are injected into the shadow root as `<link>` tags (or as `CSSStyleSheet` objects when `cssStrategy: 'constructable'` is set), so all your styles are scoped to the shadow tree. CSS files are also kept in `<head>`, which ensures document-level selectors like `:root` and `body` continue to work as expected.
|
|
129
|
+
|
|
130
|
+
## Closed mode
|
|
131
|
+
|
|
132
|
+
When `mode: 'closed'`, `element.shadowRoot` returns `null` from outside the shadow tree. The plugin exposes the shadow root on `window.__shadowRoot` before importing your app's JS so it is always accessible regardless of mode.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Access the shadow root directly if needed
|
|
136
|
+
const shadow = window.__shadowRoot;
|
|
137
|
+
const app = shadow.getElementById('app');
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The window property name is configurable via the `shadowRootGlobal` option.
|
package/dist/index.mjs
CHANGED
|
@@ -26,7 +26,8 @@ const DEFAULT_PLUGIN_OPTIONS = {
|
|
|
26
26
|
serializable: true,
|
|
27
27
|
appId: "app",
|
|
28
28
|
shadowRootGlobal: "__shadowRoot",
|
|
29
|
-
patchDocument: true
|
|
29
|
+
patchDocument: true,
|
|
30
|
+
formatOutput: true
|
|
30
31
|
};
|
|
31
32
|
const DOCUMENT_METHODS_TO_PATCH = [
|
|
32
33
|
"getElementById",
|
|
@@ -307,10 +308,10 @@ function build_exclude_predicate(exclude) {
|
|
|
307
308
|
if (typeof exclude === "function") return exclude;
|
|
308
309
|
return (filename) => exclude.some((pattern) => filename.includes(pattern));
|
|
309
310
|
}
|
|
310
|
-
function resolve_format_option(
|
|
311
|
-
if (
|
|
312
|
-
if (
|
|
313
|
-
return
|
|
311
|
+
function resolve_format_option(options) {
|
|
312
|
+
if (options === false) return false;
|
|
313
|
+
if (options === true || options === void 0) return {};
|
|
314
|
+
return options;
|
|
314
315
|
}
|
|
315
316
|
function resolve_options(options) {
|
|
316
317
|
return {
|
|
@@ -333,11 +334,12 @@ function shadowDOM(options = {}) {
|
|
|
333
334
|
}
|
|
334
335
|
},
|
|
335
336
|
generateBundle(_, bundle) {
|
|
337
|
+
if (resolved.formatOutput === false) return;
|
|
336
338
|
for (const filename of Object.keys(bundle)) {
|
|
337
339
|
const chunk = bundle[filename];
|
|
338
340
|
if (chunk.type === "asset" && filename.endsWith(".html")) {
|
|
339
341
|
const source = chunk.source;
|
|
340
|
-
chunk.source = format_html(source);
|
|
342
|
+
chunk.source = format_html(source, resolved.formatOutput);
|
|
341
343
|
}
|
|
342
344
|
}
|
|
343
345
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/process/format.ts","../src/process/html.ts","../src/dom/patch.ts","../src/process/inject.ts","../src/process/transform.ts","../src/plugin.ts"],"sourcesContent":["import type { ResolvedOptions } from './types';\nimport type { HTMLBeautifyOptions } from 'js-beautify';\n\nexport const VOID_TAGS = new Set([\n\t'area',\n\t'base',\n\t'br',\n\t'col',\n\t'embed',\n\t'hr',\n\t'img',\n\t'input',\n\t'link',\n\t'meta',\n\t'param',\n\t'source',\n\t'track',\n\t'wbr',\n]);\n\nexport const DEFAULT_PLUGIN_OPTIONS: Partial<ResolvedOptions> = {\n\tmode: 'open',\n\tcssStrategy: 'link',\n\thostId: 'shadow-host',\n\ttemplateId: 'shadow-template',\n\tdelegatesFocus: true,\n\tserializable: true,\n\tappId: 'app',\n\tshadowRootGlobal: '__shadowRoot',\n\tpatchDocument: true,\n};\n\nexport const DOCUMENT_METHODS_TO_PATCH = [\n\t'getElementById',\n\t'querySelector',\n\t'querySelectorAll',\n];\n\nexport const DEFAULT_BEAUTIFY_OPTIONS: HTMLBeautifyOptions = {\n\tend_with_newline: true,\n\teol: '\\n',\n\tindent_with_tabs: true,\n\tindent_size: 4,\n\twrap_line_length: 0,\n\tindent_inner_html: true,\n\tmax_preserve_newlines: 0,\n\tpreserve_newlines: false,\n\textra_liners: [],\n};\n","import type { HTMLBeautifyOptions } from 'js-beautify';\n\nimport beautify from 'js-beautify';\n\nimport { DEFAULT_BEAUTIFY_OPTIONS } from '../constants';\n\nexport function format_html(html: string, options?: HTMLBeautifyOptions): string {\n\treturn beautify.html(html, { ...DEFAULT_BEAUTIFY_OPTIONS, ...options });\n}\n","import type { BodySlice, ElementSlice, ExtractedAssets } from '../types';\n\nimport { VOID_TAGS } from '../constants';\n\n/**\n * Strips all Vite-injected <link rel=\"stylesheet\"> and <script type=\"module\" src=\"...\">\n * tags from HTML, collecting their URLs for manual injection into the shadow root.\n * Non-stylesheet links (favicon, preload, etc.) and inline module scripts are preserved.\n */\nexport function extract_assets(html: string): ExtractedAssets {\n\tconst css_hrefs: string[] = [];\n\tconst js_srcs: string[] = [];\n\n\thtml.replace(/<link\\b[^>]*>/g, (tag) => {\n\t\tif (get_attr(tag, 'rel') === 'stylesheet') {\n\t\t\tconst href = get_attr(tag, 'href');\n\t\t\tif (href)\n\t\t\t\tcss_hrefs.push(href);\n\t\t}\n\t\treturn tag;\n\t});\n\n\thtml = html.replace(/<script\\b[^>]*><\\/script>/g, (tag) => {\n\t\tif (get_attr(tag, 'type') === 'module') {\n\t\t\tconst src = get_attr(tag, 'src');\n\t\t\tif (src)\n\t\t\t\tjs_srcs.push(src);\n\t\t\treturn '';\n\t\t}\n\t\treturn tag;\n\t});\n\n\treturn { html, css_hrefs, js_srcs };\n}\n\n/**\n * Splits HTML around the <body> element using index-based slicing.\n * Immune to </body> strings inside scripts or templates.\n */\nexport function slice_body(html: string): BodySlice | null {\n\tconst body_open = html.indexOf('<body');\n\tif (body_open === -1)\n\t\treturn null;\n\n\tconst tag_end = html.indexOf('>', body_open);\n\tif (tag_end === -1)\n\t\treturn null;\n\n\tconst body_start = tag_end + 1;\n\tconst body_end = html.lastIndexOf('</body>');\n\tif (body_end === -1)\n\t\treturn null;\n\n\treturn {\n\t\tbefore: html.slice(0, body_start),\n\t\tcontent: html.slice(body_start, body_end),\n\t\tafter: html.slice(body_end),\n\t};\n}\n\n/**\n * Finds and extracts an element by id from an HTML string using nesting-aware scanning.\n * Supports any depth of same-tag nesting. Only supports id= attribute selectors.\n * Returns null if the element is not found or the HTML is malformed.\n */\nexport function find_element_by_id(html: string, id: string): ElementSlice | null {\n\tconst id_re = new RegExp(`<(\\\\w+)\\\\b[^>]*\\\\bid=\"${id}\"[^>]*>`, 'i');\n\tconst match = id_re.exec(html);\n\tif (!match)\n\t\treturn null;\n\n\tconst tag_name = match[1].toLowerCase();\n\tconst tag_start = match.index;\n\tconst opening_end = tag_start + match[0].length;\n\n\tif (VOID_TAGS.has(tag_name) || match[0].endsWith('/>')) {\n\t\treturn {\n\t\t\tbefore: html.slice(0, tag_start),\n\t\t\telement: match[0],\n\t\t\tafter: html.slice(opening_end),\n\t\t};\n\t}\n\n\tconst open_seq = `<${tag_name}`;\n\tconst close_seq = `</${tag_name}`;\n\n\tlet pos = opening_end;\n\tlet depth = 1;\n\n\twhile (depth > 0 && pos < html.length) {\n\t\tconst next_open = html.indexOf(open_seq, pos);\n\n\t\tlet next_close = html.indexOf(close_seq, pos);\n\t\twhile (next_close !== -1) {\n\t\t\tconst c = html[next_close + close_seq.length];\n\t\t\tif (c === '>' || c === ' ' || c === '\\n' || c === '\\t' || c === '\\r')\n\t\t\t\tbreak;\n\t\t\tnext_close = html.indexOf(close_seq, next_close + 1);\n\t\t}\n\n\t\tif (next_close === -1)\n\t\t\treturn null; // malformed HTML\n\n\t\tif (next_open !== -1 && next_open < next_close) {\n\t\t\tconst char_after = html[next_open + open_seq.length];\n\t\t\tif (\n\t\t\t\tchar_after === '>'\n\t\t\t\t|| char_after === ' '\n\t\t\t\t|| char_after === '\\n'\n\t\t\t\t|| char_after === '\\t'\n\t\t\t\t|| char_after === '\\r'\n\t\t\t\t|| char_after === '/'\n\t\t\t) {\n\t\t\t\tdepth++;\n\t\t\t\tpos = next_open + open_seq.length;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tpos = next_open + 1;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tdepth--;\n\t\t\tconst close_end = html.indexOf('>', next_close) + 1;\n\t\t\tif (close_end === 0)\n\t\t\t\treturn null;\n\t\t\tif (depth === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tbefore: html.slice(0, tag_start),\n\t\t\t\t\telement: html.slice(tag_start, close_end),\n\t\t\t\t\tafter: html.slice(close_end),\n\t\t\t\t};\n\t\t\t}\n\t\t\tpos = close_end;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Extracts the value of a named attribute from an HTML opening tag string.\n * Order-independent — works regardless of attribute position in the tag.\n */\nexport function get_attr(tag: string, attr: string): string | undefined {\n\treturn tag.match(new RegExp(`\\\\b${attr}=\"([^\"]+)\"`))?.[1];\n}\n","import { DOCUMENT_METHODS_TO_PATCH } from '../constants';\n\n/**\n * Generates the document-patching snippet injected into the bootstrap script.\n *\n * Iterates over the query methods that ShadowRoot implements, replacing each\n * on `document` with a version that searches the shadow root first and falls\n * back to the original document method when nothing is found in the shadow.\n */\nexport function build_document_patch(global_name: string): string {\n\treturn `\n\t\tconst __shadow = window['${global_name}'];\n\n\t\tfor (const m of ${JSON.stringify(DOCUMENT_METHODS_TO_PATCH)}) {\n\t\t\tconst orig = document[m].bind(document);\n\t\t\tdocument[m] = (...args) => {\n\t\t\t\tconst r = __shadow[m](...args);\n\t\t\t\treturn r != null && (!('length' in r) || r.length > 0)\n\t\t\t\t? r\n\t\t\t\t: orig(...args);\n\t\t\t};\n\t\t}\n `.trim();\n}\n\n/**\n * Generates a MutationObserver snippet that clones any <style> tag added\n * to document.head into the shadow root.\n *\n * This is required in dev mode where Vite injects CSS as runtime <style>\n * elements rather than emitting <link> tags. Without this, styles from\n * `import './style.css'` in the app entry never reach the shadow tree.\n *\n * Safe to include in production too — no <style> tags are injected there\n * so the observer fires zero times and has no cost.\n */\nexport function build_style_observer(global_name: string): string {\n\treturn `\n\t\tconst __style_target = window['${global_name}'];\n\t\tconst __existing_styles = document.head.querySelectorAll('style');\n\n\t\tfor (const s of __existing_styles) {\n\t\t\t__style_target.appendChild(s.cloneNode(true));\n\t\t}\n\n\t\tnew MutationObserver(mutations => {\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tfor (const node of mutation.addedNodes) {\n\t\t\t\t\tif (node.nodeName === 'STYLE') {\n\t\t\t\t\t\t__style_target.appendChild(node.cloneNode(true));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}).observe(document.head, { childList: true });\n\t`.trim();\n}\n","import type { ResolvedOptions } from '../types.js';\n\nimport { build_document_patch, build_style_observer } from '../dom/patch.js';\n\nexport function build_bootstrap_script(\n\tcss_hrefs: string[],\n\tjs_srcs: string[],\n\topts: ResolvedOptions,\n): string {\n\tconst css_block = css_hrefs.length > 0\n\t\t? opts.cssStrategy === 'constructable'\n\t\t\t? build_constructable_css(css_hrefs)\n\t\t\t: build_link_css(css_hrefs)\n\t\t: '';\n\n\tconst js_block = js_srcs.map(src => ` import('${src}');`).join('\\n');\n\tconst patch_block = opts.patchDocument ? build_document_patch(opts.shadowRootGlobal) : '';\n\tconst style_observer = build_style_observer(opts.shadowRootGlobal);\n\n\treturn `\n\t\t<script type=\"module\">\n\t\t\tconst host = document.getElementById('${opts.hostId}');\n\n\t\t\tconst shadow = host.attachShadow({\n\t\t\t mode: '${opts.mode}',\n\t\t\t delegatesFocus: ${opts.delegatesFocus},\n\t\t\t serializable: ${opts.serializable},\n\t\t\t});\n\n\t\t\tconst tpl = document.getElementById('${opts.templateId}');\n\n\t\t\tshadow.appendChild(tpl.content.cloneNode(true));\n\n\t\t\twindow['${opts.shadowRootGlobal}'] = shadow;\n\n\t\t\t${patch_block}\n\t\t\t${style_observer}\n\t\t\t${css_block}\n\t\t\t${js_block}\n\t\t</script>\n\t`;\n}\n\nfunction build_link_css(hrefs: string[]): string {\n\treturn hrefs.map(href => `\n\t\tconst link = document.createElement('link');\n\t\tlink.rel = 'stylesheet';\n\t\tlink.href = '${href}';\n\t\tshadow.appendChild(link);\n\t`).join('\\n');\n}\n\nfunction build_constructable_css(hrefs: string[]): string {\n\tconst fetches = hrefs.map((href, i) => `\n\t\tconst res_${i} = await fetch('${href}');\n\t\tconst sheet_${i} = new CSSStyleSheet();\n\t\tawait sheet_${i}.replace(await res_${i}.text());\n\t`).join('\\n');\n\n\tconst sheet_refs = hrefs.map((_, i) => `sheet_${i}`).join(', ');\n\n\treturn `\n\t\t(async () => {\n\t\t\t${fetches}\n\t\t\tshadow.adoptedStyleSheets = [${sheet_refs}];\n\t\t})();\n\t`;\n}\n","import type { ResolvedOptions } from '../types.js';\n\nimport { extract_assets, find_element_by_id, slice_body } from './html.js';\nimport { build_bootstrap_script } from './inject.js';\n\n/**\n * Full HTML transformation pipeline.\n *\n * When appId is set (default: 'app'):\n * - Finds the element with that id in the body\n * - Replaces it in-place with the shadow host div\n * - Appends the template and bootstrap script at the end of body\n * - Everything else in the body remains untouched\n *\n * Returns the original HTML unchanged if the target element cannot be found.\n */\nexport function transform_html(html: string, opts: ResolvedOptions): string {\n\tconst { html: stripped, css_hrefs, js_srcs } = extract_assets(html);\n\n\tconst slice = find_element_by_id(stripped, opts.appId);\n\tif (!slice)\n\t\treturn html;\n\n\tconst body_slice = slice_body(`${slice.before}PLACEHOLDER${slice.after}`);\n\tif (!body_slice)\n\t\treturn html;\n\n\tconst script = build_bootstrap_script(css_hrefs, js_srcs, opts);\n\n\tconst injection = `\n\t\t<div id=\"${opts.hostId}\" style=\"display: contents; width: 100%; height: 100%;\"></div>\n\n\t\t<template id=\"${opts.templateId}\">\n\t\t ${slice.element.trim()}\n\t\t</template>\n\n\t\t${script}\n\t`;\n\n\tconst body_with_host = body_slice.content.replace('PLACEHOLDER', injection);\n\n\treturn body_slice.before + body_with_host + body_slice.after;\n}\n","import type { ResolvedOptions, ShadowDOMOptions } from './types.js';\nimport type { Plugin } from 'vite';\n\nimport { DEFAULT_PLUGIN_OPTIONS } from './constants.js';\nimport { format_html } from './process/format.js';\nimport { transform_html } from './process/transform.js';\n\nfunction build_exclude_predicate(\n\texclude: ShadowDOMOptions['exclude'],\n): (filename: string) => boolean {\n\tif (!exclude)\n\t\treturn () => false;\n\tif (typeof exclude === 'function')\n\t\treturn exclude;\n\treturn (filename: string) => exclude.some(pattern => filename.includes(pattern));\n}\n\nfunction resolve_format_option(\n\toption: ShadowDOMOptions['formatOutput'],\n): ResolvedOptions['formatOutput'] {\n\tif (option === false)\n\t\treturn false;\n\tif (option === true || option === undefined)\n\t\treturn {};\n\treturn option;\n}\n\nfunction resolve_options(options: ShadowDOMOptions): ResolvedOptions {\n\tconst merged = { ...DEFAULT_PLUGIN_OPTIONS, ...options } as ResolvedOptions;\n\n\treturn {\n\t\t...merged,\n\t\texclude: build_exclude_predicate(options.exclude),\n\t\tformatOutput: resolve_format_option(options.formatOutput),\n\t};\n}\n\nexport function shadowDOM(options: ShadowDOMOptions = {}): Plugin {\n\tconst resolved = resolve_options(options);\n\n\treturn {\n\t\tname: '@ariel-salgado/vite-plugin-shadow-dom',\n\t\tenforce: 'post',\n\t\ttransformIndexHtml: {\n\t\t\torder: 'post',\n\t\t\thandler(html, ctx) {\n\t\t\t\tif ((resolved.exclude as ((filename: string) => boolean))(ctx.filename))\n\t\t\t\t\treturn html;\n\t\t\t\treturn transform_html(html, resolved);\n\t\t\t},\n\t\t},\n\t\tgenerateBundle(_, bundle) {\n\t\t\tfor (const filename of Object.keys(bundle)) {\n\t\t\t\tconst chunk = bundle[filename];\n\n\t\t\t\tif (chunk.type === 'asset' && filename.endsWith('.html')) {\n\t\t\t\t\tconst source = chunk.source as string;\n\t\t\t\t\tchunk.source = format_html(source);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/process/format.ts","../src/process/html.ts","../src/dom/patch.ts","../src/process/inject.ts","../src/process/transform.ts","../src/plugin.ts"],"sourcesContent":["import type { ResolvedOptions } from './types';\nimport type { HTMLBeautifyOptions } from 'js-beautify';\n\nexport const VOID_TAGS = new Set([\n\t'area',\n\t'base',\n\t'br',\n\t'col',\n\t'embed',\n\t'hr',\n\t'img',\n\t'input',\n\t'link',\n\t'meta',\n\t'param',\n\t'source',\n\t'track',\n\t'wbr',\n]);\n\nexport const DEFAULT_PLUGIN_OPTIONS: Partial<ResolvedOptions> = {\n\tmode: 'open',\n\tcssStrategy: 'link',\n\thostId: 'shadow-host',\n\ttemplateId: 'shadow-template',\n\tdelegatesFocus: true,\n\tserializable: true,\n\tappId: 'app',\n\tshadowRootGlobal: '__shadowRoot',\n\tpatchDocument: true,\n\tformatOutput: true,\n};\n\nexport const DOCUMENT_METHODS_TO_PATCH = [\n\t'getElementById',\n\t'querySelector',\n\t'querySelectorAll',\n];\n\nexport const DEFAULT_BEAUTIFY_OPTIONS: HTMLBeautifyOptions = {\n\tend_with_newline: true,\n\teol: '\\n',\n\tindent_with_tabs: true,\n\tindent_size: 4,\n\twrap_line_length: 0,\n\tindent_inner_html: true,\n\tmax_preserve_newlines: 0,\n\tpreserve_newlines: false,\n\textra_liners: [],\n};\n","import type { HTMLBeautifyOptions } from 'js-beautify';\n\nimport beautify from 'js-beautify';\n\nimport { DEFAULT_BEAUTIFY_OPTIONS } from '../constants';\n\nexport function format_html(html: string, options?: HTMLBeautifyOptions): string {\n\treturn beautify.html(html, { ...DEFAULT_BEAUTIFY_OPTIONS, ...options });\n}\n","import type { BodySlice, ElementSlice, ExtractedAssets } from '../types';\n\nimport { VOID_TAGS } from '../constants';\n\n/**\n * Strips all Vite-injected <link rel=\"stylesheet\"> and <script type=\"module\" src=\"...\">\n * tags from HTML, collecting their URLs for manual injection into the shadow root.\n * Non-stylesheet links (favicon, preload, etc.) and inline module scripts are preserved.\n */\nexport function extract_assets(html: string): ExtractedAssets {\n\tconst css_hrefs: string[] = [];\n\tconst js_srcs: string[] = [];\n\n\thtml.replace(/<link\\b[^>]*>/g, (tag) => {\n\t\tif (get_attr(tag, 'rel') === 'stylesheet') {\n\t\t\tconst href = get_attr(tag, 'href');\n\t\t\tif (href)\n\t\t\t\tcss_hrefs.push(href);\n\t\t}\n\t\treturn tag;\n\t});\n\n\thtml = html.replace(/<script\\b[^>]*><\\/script>/g, (tag) => {\n\t\tif (get_attr(tag, 'type') === 'module') {\n\t\t\tconst src = get_attr(tag, 'src');\n\t\t\tif (src)\n\t\t\t\tjs_srcs.push(src);\n\t\t\treturn '';\n\t\t}\n\t\treturn tag;\n\t});\n\n\treturn { html, css_hrefs, js_srcs };\n}\n\n/**\n * Splits HTML around the <body> element using index-based slicing.\n * Immune to </body> strings inside scripts or templates.\n */\nexport function slice_body(html: string): BodySlice | null {\n\tconst body_open = html.indexOf('<body');\n\tif (body_open === -1)\n\t\treturn null;\n\n\tconst tag_end = html.indexOf('>', body_open);\n\tif (tag_end === -1)\n\t\treturn null;\n\n\tconst body_start = tag_end + 1;\n\tconst body_end = html.lastIndexOf('</body>');\n\tif (body_end === -1)\n\t\treturn null;\n\n\treturn {\n\t\tbefore: html.slice(0, body_start),\n\t\tcontent: html.slice(body_start, body_end),\n\t\tafter: html.slice(body_end),\n\t};\n}\n\n/**\n * Finds and extracts an element by id from an HTML string using nesting-aware scanning.\n * Supports any depth of same-tag nesting. Only supports id= attribute selectors.\n * Returns null if the element is not found or the HTML is malformed.\n */\nexport function find_element_by_id(html: string, id: string): ElementSlice | null {\n\tconst id_re = new RegExp(`<(\\\\w+)\\\\b[^>]*\\\\bid=\"${id}\"[^>]*>`, 'i');\n\tconst match = id_re.exec(html);\n\tif (!match)\n\t\treturn null;\n\n\tconst tag_name = match[1].toLowerCase();\n\tconst tag_start = match.index;\n\tconst opening_end = tag_start + match[0].length;\n\n\tif (VOID_TAGS.has(tag_name) || match[0].endsWith('/>')) {\n\t\treturn {\n\t\t\tbefore: html.slice(0, tag_start),\n\t\t\telement: match[0],\n\t\t\tafter: html.slice(opening_end),\n\t\t};\n\t}\n\n\tconst open_seq = `<${tag_name}`;\n\tconst close_seq = `</${tag_name}`;\n\n\tlet pos = opening_end;\n\tlet depth = 1;\n\n\twhile (depth > 0 && pos < html.length) {\n\t\tconst next_open = html.indexOf(open_seq, pos);\n\n\t\tlet next_close = html.indexOf(close_seq, pos);\n\t\twhile (next_close !== -1) {\n\t\t\tconst c = html[next_close + close_seq.length];\n\t\t\tif (c === '>' || c === ' ' || c === '\\n' || c === '\\t' || c === '\\r')\n\t\t\t\tbreak;\n\t\t\tnext_close = html.indexOf(close_seq, next_close + 1);\n\t\t}\n\n\t\tif (next_close === -1)\n\t\t\treturn null; // malformed HTML\n\n\t\tif (next_open !== -1 && next_open < next_close) {\n\t\t\tconst char_after = html[next_open + open_seq.length];\n\t\t\tif (\n\t\t\t\tchar_after === '>'\n\t\t\t\t|| char_after === ' '\n\t\t\t\t|| char_after === '\\n'\n\t\t\t\t|| char_after === '\\t'\n\t\t\t\t|| char_after === '\\r'\n\t\t\t\t|| char_after === '/'\n\t\t\t) {\n\t\t\t\tdepth++;\n\t\t\t\tpos = next_open + open_seq.length;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tpos = next_open + 1;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tdepth--;\n\t\t\tconst close_end = html.indexOf('>', next_close) + 1;\n\t\t\tif (close_end === 0)\n\t\t\t\treturn null;\n\t\t\tif (depth === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tbefore: html.slice(0, tag_start),\n\t\t\t\t\telement: html.slice(tag_start, close_end),\n\t\t\t\t\tafter: html.slice(close_end),\n\t\t\t\t};\n\t\t\t}\n\t\t\tpos = close_end;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Extracts the value of a named attribute from an HTML opening tag string.\n * Order-independent — works regardless of attribute position in the tag.\n */\nexport function get_attr(tag: string, attr: string): string | undefined {\n\treturn tag.match(new RegExp(`\\\\b${attr}=\"([^\"]+)\"`))?.[1];\n}\n","import { DOCUMENT_METHODS_TO_PATCH } from '../constants';\n\n/**\n * Generates the document-patching snippet injected into the bootstrap script.\n *\n * Iterates over the query methods that ShadowRoot implements, replacing each\n * on `document` with a version that searches the shadow root first and falls\n * back to the original document method when nothing is found in the shadow.\n */\nexport function build_document_patch(global_name: string): string {\n\treturn `\n\t\tconst __shadow = window['${global_name}'];\n\n\t\tfor (const m of ${JSON.stringify(DOCUMENT_METHODS_TO_PATCH)}) {\n\t\t\tconst orig = document[m].bind(document);\n\t\t\tdocument[m] = (...args) => {\n\t\t\t\tconst r = __shadow[m](...args);\n\t\t\t\treturn r != null && (!('length' in r) || r.length > 0)\n\t\t\t\t? r\n\t\t\t\t: orig(...args);\n\t\t\t};\n\t\t}\n `.trim();\n}\n\n/**\n * Generates a MutationObserver snippet that clones any <style> tag added\n * to document.head into the shadow root.\n *\n * This is required in dev mode where Vite injects CSS as runtime <style>\n * elements rather than emitting <link> tags. Without this, styles from\n * `import './style.css'` in the app entry never reach the shadow tree.\n *\n * Safe to include in production too — no <style> tags are injected there\n * so the observer fires zero times and has no cost.\n */\nexport function build_style_observer(global_name: string): string {\n\treturn `\n\t\tconst __style_target = window['${global_name}'];\n\t\tconst __existing_styles = document.head.querySelectorAll('style');\n\n\t\tfor (const s of __existing_styles) {\n\t\t\t__style_target.appendChild(s.cloneNode(true));\n\t\t}\n\n\t\tnew MutationObserver(mutations => {\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tfor (const node of mutation.addedNodes) {\n\t\t\t\t\tif (node.nodeName === 'STYLE') {\n\t\t\t\t\t\t__style_target.appendChild(node.cloneNode(true));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}).observe(document.head, { childList: true });\n\t`.trim();\n}\n","import type { ResolvedOptions } from '../types.js';\n\nimport { build_document_patch, build_style_observer } from '../dom/patch.js';\n\nexport function build_bootstrap_script(\n\tcss_hrefs: string[],\n\tjs_srcs: string[],\n\topts: ResolvedOptions,\n): string {\n\tconst css_block = css_hrefs.length > 0\n\t\t? opts.cssStrategy === 'constructable'\n\t\t\t? build_constructable_css(css_hrefs)\n\t\t\t: build_link_css(css_hrefs)\n\t\t: '';\n\n\tconst js_block = js_srcs.map(src => ` import('${src}');`).join('\\n');\n\tconst patch_block = opts.patchDocument ? build_document_patch(opts.shadowRootGlobal) : '';\n\tconst style_observer = build_style_observer(opts.shadowRootGlobal);\n\n\treturn `\n\t\t<script type=\"module\">\n\t\t\tconst host = document.getElementById('${opts.hostId}');\n\n\t\t\tconst shadow = host.attachShadow({\n\t\t\t mode: '${opts.mode}',\n\t\t\t delegatesFocus: ${opts.delegatesFocus},\n\t\t\t serializable: ${opts.serializable},\n\t\t\t});\n\n\t\t\tconst tpl = document.getElementById('${opts.templateId}');\n\n\t\t\tshadow.appendChild(tpl.content.cloneNode(true));\n\n\t\t\twindow['${opts.shadowRootGlobal}'] = shadow;\n\n\t\t\t${patch_block}\n\t\t\t${style_observer}\n\t\t\t${css_block}\n\t\t\t${js_block}\n\t\t</script>\n\t`;\n}\n\nfunction build_link_css(hrefs: string[]): string {\n\treturn hrefs.map(href => `\n\t\tconst link = document.createElement('link');\n\t\tlink.rel = 'stylesheet';\n\t\tlink.href = '${href}';\n\t\tshadow.appendChild(link);\n\t`).join('\\n');\n}\n\nfunction build_constructable_css(hrefs: string[]): string {\n\tconst fetches = hrefs.map((href, i) => `\n\t\tconst res_${i} = await fetch('${href}');\n\t\tconst sheet_${i} = new CSSStyleSheet();\n\t\tawait sheet_${i}.replace(await res_${i}.text());\n\t`).join('\\n');\n\n\tconst sheet_refs = hrefs.map((_, i) => `sheet_${i}`).join(', ');\n\n\treturn `\n\t\t(async () => {\n\t\t\t${fetches}\n\t\t\tshadow.adoptedStyleSheets = [${sheet_refs}];\n\t\t})();\n\t`;\n}\n","import type { ResolvedOptions } from '../types.js';\n\nimport { extract_assets, find_element_by_id, slice_body } from './html.js';\nimport { build_bootstrap_script } from './inject.js';\n\n/**\n * Full HTML transformation pipeline.\n *\n * When appId is set (default: 'app'):\n * - Finds the element with that id in the body\n * - Replaces it in-place with the shadow host div\n * - Appends the template and bootstrap script at the end of body\n * - Everything else in the body remains untouched\n *\n * Returns the original HTML unchanged if the target element cannot be found.\n */\nexport function transform_html(html: string, opts: ResolvedOptions): string {\n\tconst { html: stripped, css_hrefs, js_srcs } = extract_assets(html);\n\n\tconst slice = find_element_by_id(stripped, opts.appId);\n\tif (!slice)\n\t\treturn html;\n\n\tconst body_slice = slice_body(`${slice.before}PLACEHOLDER${slice.after}`);\n\tif (!body_slice)\n\t\treturn html;\n\n\tconst script = build_bootstrap_script(css_hrefs, js_srcs, opts);\n\n\tconst injection = `\n\t\t<div id=\"${opts.hostId}\" style=\"display: contents; width: 100%; height: 100%;\"></div>\n\n\t\t<template id=\"${opts.templateId}\">\n\t\t ${slice.element.trim()}\n\t\t</template>\n\n\t\t${script}\n\t`;\n\n\tconst body_with_host = body_slice.content.replace('PLACEHOLDER', injection);\n\n\treturn body_slice.before + body_with_host + body_slice.after;\n}\n","import type { ResolvedOptions, ShadowDOMOptions } from './types.js';\nimport type { Plugin } from 'vite';\n\nimport { DEFAULT_PLUGIN_OPTIONS } from './constants.js';\nimport { format_html } from './process/format.js';\nimport { transform_html } from './process/transform.js';\n\nfunction build_exclude_predicate(\n\texclude: ShadowDOMOptions['exclude'],\n): (filename: string) => boolean {\n\tif (!exclude)\n\t\treturn () => false;\n\tif (typeof exclude === 'function')\n\t\treturn exclude;\n\treturn (filename: string) => exclude.some(pattern => filename.includes(pattern));\n}\n\nfunction resolve_format_option(\n\toptions: ShadowDOMOptions['formatOutput'],\n): ResolvedOptions['formatOutput'] {\n\tif (options === false)\n\t\treturn false;\n\tif (options === true || options === undefined)\n\t\treturn {};\n\treturn options;\n}\n\nfunction resolve_options(options: ShadowDOMOptions): ResolvedOptions {\n\tconst merged = { ...DEFAULT_PLUGIN_OPTIONS, ...options } as ResolvedOptions;\n\n\treturn {\n\t\t...merged,\n\t\texclude: build_exclude_predicate(options.exclude),\n\t\tformatOutput: resolve_format_option(options.formatOutput),\n\t};\n}\n\nexport function shadowDOM(options: ShadowDOMOptions = {}): Plugin {\n\tconst resolved = resolve_options(options);\n\n\treturn {\n\t\tname: '@ariel-salgado/vite-plugin-shadow-dom',\n\t\tenforce: 'post',\n\t\ttransformIndexHtml: {\n\t\t\torder: 'post',\n\t\t\thandler(html, ctx) {\n\t\t\t\tif (resolved.exclude(ctx.filename))\n\t\t\t\t\treturn html;\n\t\t\t\treturn transform_html(html, resolved);\n\t\t\t},\n\t\t},\n\t\tgenerateBundle(_, bundle) {\n\t\t\tif (resolved.formatOutput === false)\n\t\t\t\treturn;\n\n\t\t\tfor (const filename of Object.keys(bundle)) {\n\t\t\t\tconst chunk = bundle[filename];\n\t\t\t\tif (chunk.type === 'asset' && filename.endsWith('.html')) {\n\t\t\t\t\tconst source = chunk.source as string;\n\t\t\t\t\tchunk.source = format_html(\n\t\t\t\t\t\tsource,\n\t\t\t\t\t\tresolved.formatOutput,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ariel-salgado/vite-plugin-shadow-dom",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
5
|
-
"description": "Vite plugin that isolates
|
|
4
|
+
"version": "0.0.4",
|
|
5
|
+
"description": "Vite plugin that isolates your app into a Shadow DOM — with zero changes to your application code",
|
|
6
6
|
"author": "Ariel Salgado <ariel.salgado.acevedo@gmail.com> (https://github.com/ariel-salgado/)",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://github.com/ariel-salgado/vite-plugins/packages/vite-plugin-shadow-dom#readme",
|
|
8
|
+
"homepage": "https://github.com/ariel-salgado/vite-plugins/tree/main/packages/vite-plugin-shadow-dom#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/ariel-salgado/vite-plugins.git"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
|
-
"vite"
|
|
14
|
+
"vite",
|
|
15
|
+
"vite-plugin",
|
|
16
|
+
"shadow-dom",
|
|
17
|
+
"encapsulation",
|
|
18
|
+
"css-isolation",
|
|
19
|
+
"dom-isolation"
|
|
15
20
|
],
|
|
16
21
|
"exports": {
|
|
17
22
|
".": "./dist/index.mjs",
|