@ariel-salgado/vite-plugin-shadow-dom 0.0.1
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/dist/dom.d.mts +12 -0
- package/dist/dom.d.mts.map +1 -0
- package/dist/dom.mjs +16 -0
- package/dist/dom.mjs.map +1 -0
- package/dist/index.d.mts +75 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +280 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/dist/dom.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/dom/shadow.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Client-side helper for apps running inside a shadow DOM.
|
|
4
|
+
*
|
|
5
|
+
* When the shadow root mode is 'closed', element.shadowRoot returns null
|
|
6
|
+
* and document.getElementById / querySelector cannot find shadow DOM elements.
|
|
7
|
+
* Use getShadowRoot() as your document substitute to query the shadow tree.
|
|
8
|
+
*/
|
|
9
|
+
declare function getShadowRoot(globalName?: string): ShadowRoot | Document;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { getShadowRoot };
|
|
12
|
+
//# sourceMappingURL=dom.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.d.mts","names":[],"sources":["../src/dom/shadow.ts"],"mappings":";;AAOA;;;;;;iBAAgB,aAAA,CAAc,UAAA,YAA8B,UAAA,GAAa,QAAA"}
|
package/dist/dom.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/dom/shadow.ts
|
|
2
|
+
/**
|
|
3
|
+
* Client-side helper for apps running inside a shadow DOM.
|
|
4
|
+
*
|
|
5
|
+
* When the shadow root mode is 'closed', element.shadowRoot returns null
|
|
6
|
+
* and document.getElementById / querySelector cannot find shadow DOM elements.
|
|
7
|
+
* Use getShadowRoot() as your document substitute to query the shadow tree.
|
|
8
|
+
*/
|
|
9
|
+
function getShadowRoot(globalName = "__shadowRoot") {
|
|
10
|
+
if (typeof window === "undefined") return {};
|
|
11
|
+
return window[globalName] ?? document;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { getShadowRoot };
|
|
16
|
+
//# sourceMappingURL=dom.mjs.map
|
package/dist/dom.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.mjs","names":[],"sources":["../src/dom/shadow.ts"],"sourcesContent":["/**\n * Client-side helper for apps running inside a shadow DOM.\n *\n * When the shadow root mode is 'closed', element.shadowRoot returns null\n * and document.getElementById / querySelector cannot find shadow DOM elements.\n * Use getShadowRoot() as your document substitute to query the shadow tree.\n */\nexport function getShadowRoot(globalName = '__shadowRoot'): ShadowRoot | Document {\n\tif (typeof window === 'undefined')\n\t\treturn {} as Document;\n\n\treturn ((window as unknown as Record<string, unknown>)[globalName] as ShadowRoot | undefined) ?? document;\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,cAAc,aAAa,gBAAuC;AACjF,KAAI,OAAO,WAAW,YACrB,QAAO,EAAE;AAEV,QAAS,OAA8C,eAA0C"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
type ShadowMode = 'open' | 'closed';
|
|
5
|
+
type CSSStrategy = 'link' | 'constructable';
|
|
6
|
+
interface ShadowDOMOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Shadow root mode.
|
|
9
|
+
* 'closed' prevents external JS from accessing internals via element.shadowRoot.
|
|
10
|
+
* Your app should use getShadowRoot() from the client entry instead.
|
|
11
|
+
* @default 'open'
|
|
12
|
+
*/
|
|
13
|
+
mode?: ShadowMode;
|
|
14
|
+
/**
|
|
15
|
+
* How Vite-built CSS is injected into the shadow root.
|
|
16
|
+
* 'constructable' avoids FOUC but requires an async fetch.
|
|
17
|
+
* @default 'link'
|
|
18
|
+
*/
|
|
19
|
+
cssStrategy?: CSSStrategy;
|
|
20
|
+
/**
|
|
21
|
+
* id of the generated shadow host element.
|
|
22
|
+
* @default 'shadow-host'
|
|
23
|
+
*/
|
|
24
|
+
hostId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* id of the <template> holding original content.
|
|
27
|
+
* @default 'shadow-template'
|
|
28
|
+
*/
|
|
29
|
+
templateId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Forwards focus events across the shadow boundary.
|
|
32
|
+
* Set true if the shadow content has inputs, buttons, or other focusable elements.
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
delegatesFocus?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Allows the shadow root to be serialized via getHTML() and inspected in DevTools.
|
|
38
|
+
* Disable only if you need strict serialization prevention.
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
serializable?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The id of the element inside <body> to isolate into the shadow DOM.
|
|
44
|
+
* Everything else in <body> stays in the regular document.
|
|
45
|
+
* @default 'app'
|
|
46
|
+
*/
|
|
47
|
+
appId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* HTML files to skip. Matched against the absolute file path.
|
|
50
|
+
* - string[]: skips files whose path includes any of the strings
|
|
51
|
+
* - function: skips files where the predicate returns true
|
|
52
|
+
*/
|
|
53
|
+
exclude?: string[] | ((filename: string) => boolean);
|
|
54
|
+
/**
|
|
55
|
+
* Window property name used to expose the shadow root before JS is imported.
|
|
56
|
+
* Lets your app call getShadowRoot() to access shadow DOM elements,
|
|
57
|
+
* which is required when mode is 'closed'.
|
|
58
|
+
* @default '__shadowRoot'
|
|
59
|
+
*/
|
|
60
|
+
shadowRootGlobal?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Patches document.getElementById, document.querySelector, and
|
|
63
|
+
* document.querySelectorAll to search the shadow root first.
|
|
64
|
+
* This lets app code like document.querySelector('#app') work
|
|
65
|
+
* without any modification regardless of shadow mode.
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
patchDocument?: boolean;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/plugin.d.ts
|
|
72
|
+
declare function shadowDOM(options?: ShadowDOMOptions): Plugin;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { type CSSStrategy, type ShadowDOMOptions, type ShadowMode, shadowDOM };
|
|
75
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"mappings":";;;KAAY,UAAA;AAAA,KACA,WAAA;AAAA,UAEK,gBAAA;EAHL;;;;;AACZ;EASC,IAAA,GAAO,UAAA;;;;AAPR;;EAcC,WAAA,GAAc,WAAA;EAAW;;;;EAMzB,MAAA;EAAA;;;;EAMA,UAAA;EA4BA;;;;;EArBA,cAAA;;;;ACXD;;EDkBC,YAAA;EClBgE;;;;;EDyBhE,KAAA;;;;;;EAOA,OAAA,gBAAuB,QAAA;;;;;;;EAQvB,gBAAA;;;;;;;;EASA,aAAA;AAAA;;;iBCjDe,SAAA,CAAU,OAAA,GAAS,gBAAA,GAAwB,MAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
const VOID_TAGS = new Set([
|
|
3
|
+
"area",
|
|
4
|
+
"base",
|
|
5
|
+
"br",
|
|
6
|
+
"col",
|
|
7
|
+
"embed",
|
|
8
|
+
"hr",
|
|
9
|
+
"img",
|
|
10
|
+
"input",
|
|
11
|
+
"link",
|
|
12
|
+
"meta",
|
|
13
|
+
"param",
|
|
14
|
+
"source",
|
|
15
|
+
"track",
|
|
16
|
+
"wbr"
|
|
17
|
+
]);
|
|
18
|
+
const DEFAULT_PLUGIN_OPTIONS = {
|
|
19
|
+
mode: "open",
|
|
20
|
+
cssStrategy: "link",
|
|
21
|
+
hostId: "shadow-host",
|
|
22
|
+
templateId: "shadow-template",
|
|
23
|
+
delegatesFocus: true,
|
|
24
|
+
serializable: true,
|
|
25
|
+
appId: "app",
|
|
26
|
+
shadowRootGlobal: "__shadowRoot",
|
|
27
|
+
patchDocument: true
|
|
28
|
+
};
|
|
29
|
+
const DOCUMENT_METHODS_TO_PATCH = [
|
|
30
|
+
"getElementById",
|
|
31
|
+
"querySelector",
|
|
32
|
+
"querySelectorAll"
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/process/html.ts
|
|
37
|
+
/**
|
|
38
|
+
* Strips all Vite-injected <link rel="stylesheet"> and <script type="module" src="...">
|
|
39
|
+
* tags from HTML, collecting their URLs for manual injection into the shadow root.
|
|
40
|
+
* Non-stylesheet links (favicon, preload, etc.) and inline module scripts are preserved.
|
|
41
|
+
*/
|
|
42
|
+
function extract_assets(html) {
|
|
43
|
+
const css_hrefs = [];
|
|
44
|
+
const js_srcs = [];
|
|
45
|
+
html = html.replace(/<link\b[^>]*>/g, (tag) => {
|
|
46
|
+
if (get_attr(tag, "rel") === "stylesheet") {
|
|
47
|
+
const href = get_attr(tag, "href");
|
|
48
|
+
if (href) css_hrefs.push(href);
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
return tag;
|
|
52
|
+
});
|
|
53
|
+
html = html.replace(/<script\b[^>]*><\/script>/g, (tag) => {
|
|
54
|
+
if (get_attr(tag, "type") === "module") {
|
|
55
|
+
const src = get_attr(tag, "src");
|
|
56
|
+
if (src) js_srcs.push(src);
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
return tag;
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
html,
|
|
63
|
+
css_hrefs,
|
|
64
|
+
js_srcs
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Splits HTML around the <body> element using index-based slicing.
|
|
69
|
+
* Immune to </body> strings inside scripts or templates.
|
|
70
|
+
*/
|
|
71
|
+
function slice_body(html) {
|
|
72
|
+
const body_open = html.indexOf("<body");
|
|
73
|
+
if (body_open === -1) return null;
|
|
74
|
+
const tag_end = html.indexOf(">", body_open);
|
|
75
|
+
if (tag_end === -1) return null;
|
|
76
|
+
const body_start = tag_end + 1;
|
|
77
|
+
const body_end = html.lastIndexOf("</body>");
|
|
78
|
+
if (body_end === -1) return null;
|
|
79
|
+
return {
|
|
80
|
+
before: html.slice(0, body_start),
|
|
81
|
+
content: html.slice(body_start, body_end),
|
|
82
|
+
after: html.slice(body_end)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Finds and extracts an element by id from an HTML string using nesting-aware scanning.
|
|
87
|
+
* Supports any depth of same-tag nesting. Only supports id= attribute selectors.
|
|
88
|
+
* Returns null if the element is not found or the HTML is malformed.
|
|
89
|
+
*/
|
|
90
|
+
function find_element_by_id(html, id) {
|
|
91
|
+
const match = new RegExp(`<(\\w+)\\b[^>]*\\bid="${id}"[^>]*>`, "i").exec(html);
|
|
92
|
+
if (!match) return null;
|
|
93
|
+
const tag_name = match[1].toLowerCase();
|
|
94
|
+
const tag_start = match.index;
|
|
95
|
+
const opening_end = tag_start + match[0].length;
|
|
96
|
+
if (VOID_TAGS.has(tag_name) || match[0].endsWith("/>")) return {
|
|
97
|
+
before: html.slice(0, tag_start),
|
|
98
|
+
element: match[0],
|
|
99
|
+
after: html.slice(opening_end)
|
|
100
|
+
};
|
|
101
|
+
const open_seq = `<${tag_name}`;
|
|
102
|
+
const close_seq = `</${tag_name}`;
|
|
103
|
+
let pos = opening_end;
|
|
104
|
+
let depth = 1;
|
|
105
|
+
while (depth > 0 && pos < html.length) {
|
|
106
|
+
const next_open = html.indexOf(open_seq, pos);
|
|
107
|
+
let next_close = html.indexOf(close_seq, pos);
|
|
108
|
+
while (next_close !== -1) {
|
|
109
|
+
const c = html[next_close + close_seq.length];
|
|
110
|
+
if (c === ">" || c === " " || c === "\n" || c === " " || c === "\r") break;
|
|
111
|
+
next_close = html.indexOf(close_seq, next_close + 1);
|
|
112
|
+
}
|
|
113
|
+
if (next_close === -1) return null;
|
|
114
|
+
if (next_open !== -1 && next_open < next_close) {
|
|
115
|
+
const char_after = html[next_open + open_seq.length];
|
|
116
|
+
if (char_after === ">" || char_after === " " || char_after === "\n" || char_after === " " || char_after === "\r" || char_after === "/") {
|
|
117
|
+
depth++;
|
|
118
|
+
pos = next_open + open_seq.length;
|
|
119
|
+
} else pos = next_open + 1;
|
|
120
|
+
} else {
|
|
121
|
+
depth--;
|
|
122
|
+
const close_end = html.indexOf(">", next_close) + 1;
|
|
123
|
+
if (close_end === 0) return null;
|
|
124
|
+
if (depth === 0) return {
|
|
125
|
+
before: html.slice(0, tag_start),
|
|
126
|
+
element: html.slice(tag_start, close_end),
|
|
127
|
+
after: html.slice(close_end)
|
|
128
|
+
};
|
|
129
|
+
pos = close_end;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Extracts the value of a named attribute from an HTML opening tag string.
|
|
136
|
+
* Order-independent — works regardless of attribute position in the tag.
|
|
137
|
+
*/
|
|
138
|
+
function get_attr(tag, attr) {
|
|
139
|
+
return tag.match(new RegExp(`\\b${attr}="([^"]+)"`))?.[1];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/dom/patch.ts
|
|
144
|
+
/**
|
|
145
|
+
* Generates the document-patching snippet injected into the bootstrap script.
|
|
146
|
+
*
|
|
147
|
+
* Iterates over the query methods that ShadowRoot implements, replacing each
|
|
148
|
+
* on `document` with a version that searches the shadow root first and falls
|
|
149
|
+
* back to the original document method when nothing is found in the shadow.
|
|
150
|
+
*
|
|
151
|
+
* Adding support for a new method is a one-line change to PATCHED_METHODS.
|
|
152
|
+
*/
|
|
153
|
+
function build_document_patch(global_name) {
|
|
154
|
+
return `
|
|
155
|
+
const __shadow = window['${global_name}'];
|
|
156
|
+
|
|
157
|
+
for (const m of ${JSON.stringify(DOCUMENT_METHODS_TO_PATCH)}) {
|
|
158
|
+
const orig = document[m].bind(document);
|
|
159
|
+
document[m] = (...args) => {
|
|
160
|
+
const r = __shadow[m](...args);
|
|
161
|
+
return r != null && (!('length' in r) || r.length > 0)
|
|
162
|
+
? r
|
|
163
|
+
: orig(...args);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
`.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/process/inject.ts
|
|
171
|
+
function build_bootstrap_script(css_hrefs, js_srcs, opts) {
|
|
172
|
+
const css_block = css_hrefs.length > 0 ? opts.cssStrategy === "constructable" ? build_constructable_css(css_hrefs) : build_link_css(css_hrefs) : "";
|
|
173
|
+
const js_block = js_srcs.map((src) => ` import('${src}');`).join("\n");
|
|
174
|
+
const patch_block = opts.patchDocument ? build_document_patch(opts.shadowRootGlobal) : "";
|
|
175
|
+
return `
|
|
176
|
+
<script type="module">
|
|
177
|
+
const host = document.getElementById('${opts.hostId}');
|
|
178
|
+
const shadow = host.attachShadow({
|
|
179
|
+
mode: '${opts.mode}',
|
|
180
|
+
delegatesFocus: ${opts.delegatesFocus},
|
|
181
|
+
serializable: ${opts.serializable},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const tpl = document.getElementById('${opts.templateId}');
|
|
185
|
+
shadow.appendChild(tpl.content.cloneNode(true));
|
|
186
|
+
|
|
187
|
+
window['${opts.shadowRootGlobal}'] = shadow;
|
|
188
|
+
|
|
189
|
+
${patch_block}
|
|
190
|
+
${css_block}
|
|
191
|
+
${js_block}
|
|
192
|
+
<\/script>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
function build_link_css(hrefs) {
|
|
196
|
+
return hrefs.map((href) => `
|
|
197
|
+
const link = document.createElement('link');
|
|
198
|
+
link.rel = 'stylesheet';
|
|
199
|
+
link.href = '${href}';
|
|
200
|
+
shadow.appendChild(link);
|
|
201
|
+
`).join("\n");
|
|
202
|
+
}
|
|
203
|
+
function build_constructable_css(hrefs) {
|
|
204
|
+
return `
|
|
205
|
+
(async () => {
|
|
206
|
+
${hrefs.map((href, i) => `
|
|
207
|
+
const res_${i} = await fetch('${href}');
|
|
208
|
+
const sheet_${i} = new CSSStyleSheet();
|
|
209
|
+
await sheet_${i}.replace(await res_${i}.text());
|
|
210
|
+
`).join("\n")}
|
|
211
|
+
shadow.adoptedStyleSheets = [${hrefs.map((_, i) => `sheet_${i}`).join(", ")}];
|
|
212
|
+
})();
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/process/transform.ts
|
|
218
|
+
/**
|
|
219
|
+
* Full HTML transformation pipeline.
|
|
220
|
+
*
|
|
221
|
+
* When appId is set (default: 'app'):
|
|
222
|
+
* - Finds the element with that id in the body
|
|
223
|
+
* - Replaces it in-place with the shadow host div
|
|
224
|
+
* - Appends the template and bootstrap script at the end of body
|
|
225
|
+
* - Everything else in the body remains untouched
|
|
226
|
+
*
|
|
227
|
+
* Returns the original HTML unchanged if the target element cannot be found.
|
|
228
|
+
*/
|
|
229
|
+
function transform_html(html, opts) {
|
|
230
|
+
const { html: stripped, css_hrefs, js_srcs } = extract_assets(html);
|
|
231
|
+
const slice = find_element_by_id(stripped, opts.appId);
|
|
232
|
+
if (!slice) return html;
|
|
233
|
+
const body_slice = slice_body(`${slice.before}PLACEHOLDER${slice.after}`);
|
|
234
|
+
if (!body_slice) return html;
|
|
235
|
+
const script = build_bootstrap_script(css_hrefs, js_srcs, opts);
|
|
236
|
+
const injection = `
|
|
237
|
+
<div id="${opts.hostId}"></div>
|
|
238
|
+
|
|
239
|
+
<template id="${opts.templateId}">
|
|
240
|
+
${slice.element.trim()}
|
|
241
|
+
</template>
|
|
242
|
+
|
|
243
|
+
${script}
|
|
244
|
+
`;
|
|
245
|
+
const body_with_host = body_slice.content.replace("PLACEHOLDER", injection);
|
|
246
|
+
return body_slice.before + body_with_host + body_slice.after;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/plugin.ts
|
|
251
|
+
function build_exclude_predicate(exclude) {
|
|
252
|
+
if (!exclude) return () => false;
|
|
253
|
+
if (typeof exclude === "function") return exclude;
|
|
254
|
+
return (filename) => exclude.some((pattern) => filename.includes(pattern));
|
|
255
|
+
}
|
|
256
|
+
function resolve_options(options) {
|
|
257
|
+
return {
|
|
258
|
+
...DEFAULT_PLUGIN_OPTIONS,
|
|
259
|
+
...options,
|
|
260
|
+
exclude: build_exclude_predicate(options.exclude)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function shadowDOM(options = {}) {
|
|
264
|
+
const resolved = resolve_options(options);
|
|
265
|
+
return {
|
|
266
|
+
name: "@ariel-salgado/vite-plugin-shadow-dom",
|
|
267
|
+
enforce: "post",
|
|
268
|
+
transformIndexHtml: {
|
|
269
|
+
order: "post",
|
|
270
|
+
handler(html, ctx) {
|
|
271
|
+
if (resolved.exclude(ctx.filename)) return html;
|
|
272
|
+
return transform_html(html, resolved);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
export { shadowDOM };
|
|
280
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.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';\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","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 = html.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\treturn '';\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 *\n * Adding support for a new method is a one-line change to PATCHED_METHODS.\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","import type { ResolvedOptions } from '../types.js';\n\nimport { build_document_patch } 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\n\t\t= css_hrefs.length > 0\n\t\t\t? opts.cssStrategy === 'constructable'\n\t\t\t\t? build_constructable_css(css_hrefs)\n\t\t\t\t: build_link_css(css_hrefs)\n\t\t\t: '';\n\n\tconst js_block = js_srcs.map(src => ` import('${src}');`).join('\\n');\n\n\tconst patch_block = opts.patchDocument\n\t\t? build_document_patch(opts.shadowRootGlobal)\n\t\t: '';\n\n\treturn `\n\t\t<script type=\"module\">\n\t\t\tconst host = document.getElementById('${opts.hostId}');\n\t\t\tconst shadow = host.attachShadow({\n\t\t\t\tmode: '${opts.mode}',\n\t\t\t\tdelegatesFocus: ${opts.delegatesFocus},\n\t\t\t\tserializable: ${opts.serializable},\n\t\t\t});\n\n\t\t\tconst tpl = document.getElementById('${opts.templateId}');\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${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}\"></div>\n\n\t\t<template id=\"${opts.templateId}\">\n\t\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 { 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_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};\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};\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ariel-salgado/vite-plugin-shadow-dom",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Vite plugin that isolates page content into a Shadow DOM",
|
|
6
|
+
"author": "Ariel Salgado <ariel.salgado.acevedo@gmail.com> (https://github.com/ariel-salgado/)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/ariel-salgado/vite-plugins/packages/vite-plugin-shadow-dom#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ariel-salgado/vite-plugins.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vite"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.mjs",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.mjs",
|
|
24
|
+
"module": "./dist/index.mjs",
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsdown",
|
|
31
|
+
"dev": "tsdown --watch",
|
|
32
|
+
"test": "vitest",
|
|
33
|
+
"release": "sh ../../scripts/release.sh",
|
|
34
|
+
"prepublishOnly": "bun run build"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vite": "^7.0.0 || ^8.0.0-0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
41
|
+
"tsdown": "^0.21.0-beta.2",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vite": "^8.0.0-beta.15",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
45
|
+
}
|
|
46
|
+
}
|