vite-plugin-xhtml2shadow 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/README.md +119 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +132 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# vite-plugin-xhtml2shadow
|
|
2
|
+
|
|
3
|
+
A vite plugin that parses XHTML files into JavaScript functions
|
|
4
|
+
that produce a shadow root with that HTML.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
You should install this plugin as a dev tool. To use the plugin, you
|
|
9
|
+
must also install the `dubc-client-global-css` package as a non-dev
|
|
10
|
+
dependecy:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -D vite-plugin-xhtml2shadow
|
|
14
|
+
npm i dubc-client-global-css
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example usage
|
|
18
|
+
|
|
19
|
+
You can now create web components that reference markup instead of
|
|
20
|
+
using cumbersome JavaScript APIs:
|
|
21
|
+
|
|
22
|
+
**login-form.xhtml**:
|
|
23
|
+
```xhtml
|
|
24
|
+
<form>
|
|
25
|
+
<label>
|
|
26
|
+
Email
|
|
27
|
+
<input name="email" type="email" placeholder="Email"/>
|
|
28
|
+
</label>
|
|
29
|
+
<label>
|
|
30
|
+
Password
|
|
31
|
+
<input name="password" type="password" placeholder="Password"/>
|
|
32
|
+
</label>
|
|
33
|
+
<button type="submit">Submit</button>
|
|
34
|
+
</form>
|
|
35
|
+
<style>
|
|
36
|
+
form {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-flow: column nowrap;
|
|
39
|
+
}
|
|
40
|
+
/* ...and so on */
|
|
41
|
+
</style>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**login-form.ts**:
|
|
45
|
+
```TypeScript
|
|
46
|
+
import build from "./login-form.xhtml"
|
|
47
|
+
|
|
48
|
+
class LoginForm extends HTMLElement {
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super()
|
|
52
|
+
build(this)
|
|
53
|
+
const submit = this.shadowRoot!.querySelector("button")!
|
|
54
|
+
submit.onclick = () => {
|
|
55
|
+
// perform login here
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
customElements.define("login-form", LoginForm)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Stylesheets
|
|
64
|
+
|
|
65
|
+
Style elements are not added directly to the shadow root's HTML.
|
|
66
|
+
Instead, the style rules are added to a `CSSStyleSheet` singleton,
|
|
67
|
+
which the shadow root then adopts. This prevents the creation of
|
|
68
|
+
multiple duplicate style sheets if the component is created many times.
|
|
69
|
+
|
|
70
|
+
Additionally, you can set a global style sheet via the
|
|
71
|
+
`dubc-client-global-css` package's `setCSS` method:
|
|
72
|
+
|
|
73
|
+
```TypeScript
|
|
74
|
+
import { setCSS } from "dubc-client-global-css"
|
|
75
|
+
import css from "/src/global.css?raw"
|
|
76
|
+
|
|
77
|
+
const globalCSS = new CSSStyleSheet()
|
|
78
|
+
globalCSS.replaceSync(css)
|
|
79
|
+
setCSS(globalCSS)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Every shadow root created by this plugin will then adopt the global
|
|
83
|
+
style sheet, allowing you to re-use design across your entire app.
|
|
84
|
+
|
|
85
|
+
## XHTML notes
|
|
86
|
+
|
|
87
|
+
Why does this plugin use `.xhtml` files and not `.html` files? There
|
|
88
|
+
are two reasons.
|
|
89
|
+
|
|
90
|
+
First, `vite` already gives special handling to `.html` files, so
|
|
91
|
+
this plugin doesn't interfere with that.
|
|
92
|
+
|
|
93
|
+
Second, HTML5 doesn't allow self-closing tags for custom elements,
|
|
94
|
+
but XHTML does. If you're used to using frameworks like React,
|
|
95
|
+
you know that self-closing tags that represent self-contained components
|
|
96
|
+
make the markup much easier to read.
|
|
97
|
+
|
|
98
|
+
Since XHTML is XML, and XML doesn't support a lot of named entities,
|
|
99
|
+
you'll need to escape their unicode values instead. In practice this
|
|
100
|
+
issue only comes up with non-breaking spaces. Instead of using ` `
|
|
101
|
+
like you would in HTML, use ` ` instead.
|
|
102
|
+
|
|
103
|
+
Note that although you _authoring_ your markup as XHTML, the plugin
|
|
104
|
+
produces JavaScript that creates HTML5 elements.
|
|
105
|
+
|
|
106
|
+
## Server-Side Rendering
|
|
107
|
+
|
|
108
|
+
If the passed-in element already has a shadow root, the functions
|
|
109
|
+
produced by this plugin do nothing, just silently return. This is
|
|
110
|
+
done to support web components that were rendered by a server via Declarative Shadow DOM.
|
|
111
|
+
|
|
112
|
+
## Viteness
|
|
113
|
+
|
|
114
|
+
This plugin is vite-specific because it relies on vite's ability to
|
|
115
|
+
import static assets as URLs. This is done for `href` and `src`
|
|
116
|
+
attributes that reference a relative URL or an absolute URL that
|
|
117
|
+
begins with `/`. Doing so allows you to specify an asset in your
|
|
118
|
+
`/src` directory via its path, but the plugin will output the correct
|
|
119
|
+
hashed URL when building with vite.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { JSDOM } from "jsdom";
|
|
2
|
+
import CleanCSS from "clean-css";
|
|
3
|
+
export default function xhtml2shadow() {
|
|
4
|
+
return {
|
|
5
|
+
name: "xhtml2shadow",
|
|
6
|
+
transform: {
|
|
7
|
+
filter: {
|
|
8
|
+
id: /\.(xhtml)$/,
|
|
9
|
+
},
|
|
10
|
+
handler(src) {
|
|
11
|
+
return {
|
|
12
|
+
code: compile(src),
|
|
13
|
+
map: null,
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function compile(src) {
|
|
20
|
+
const parser = new Parser(src);
|
|
21
|
+
parser.go();
|
|
22
|
+
let code = `import {getCSS} from "dubc-client-global-css";`;
|
|
23
|
+
code += parser.style.s;
|
|
24
|
+
code += parser.code.s;
|
|
25
|
+
code += "}";
|
|
26
|
+
return code;
|
|
27
|
+
}
|
|
28
|
+
class Parser {
|
|
29
|
+
jsdom;
|
|
30
|
+
code = new SB("v");
|
|
31
|
+
style = new SB("s");
|
|
32
|
+
parsed;
|
|
33
|
+
constructor(src) {
|
|
34
|
+
src = "<div>" + src + "</div>";
|
|
35
|
+
this.jsdom = new JSDOM();
|
|
36
|
+
const parser = new this.jsdom.window.DOMParser();
|
|
37
|
+
const parsed = parser.parseFromString(src, "application/xhtml+xml");
|
|
38
|
+
const n = parsed.querySelector("parsererror");
|
|
39
|
+
if (n !== null) {
|
|
40
|
+
throw new Error(n.textContent);
|
|
41
|
+
}
|
|
42
|
+
this.code.write(`export default function xhtml(el){`);
|
|
43
|
+
this.code.write(`if(el.shadowRoot!==null)return;`);
|
|
44
|
+
this.code.write(`const sh=el.attachShadow({mode:"open",delegatesFocus:true});`); // TODO, more options
|
|
45
|
+
this.code.write(`sh.adoptedStyleSheets.push(getCSS());`);
|
|
46
|
+
this.parsed = parsed;
|
|
47
|
+
}
|
|
48
|
+
go() {
|
|
49
|
+
for (const x of this.parsed.documentElement.childNodes) {
|
|
50
|
+
const v = this.#node(x);
|
|
51
|
+
if (v !== undefined)
|
|
52
|
+
this.code.write(`sh.append(${v});`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
#nodeId(node) {
|
|
56
|
+
if (node instanceof this.jsdom.window.Comment)
|
|
57
|
+
return;
|
|
58
|
+
if (node instanceof this.jsdom.window.Text)
|
|
59
|
+
return this.#text(node);
|
|
60
|
+
if (node instanceof this.jsdom.window.Element)
|
|
61
|
+
return this.#element(node);
|
|
62
|
+
}
|
|
63
|
+
#node(node) {
|
|
64
|
+
const id = this.#nodeId(node);
|
|
65
|
+
if (id === undefined)
|
|
66
|
+
return;
|
|
67
|
+
for (const x of node.childNodes) {
|
|
68
|
+
const childId = this.#node(x);
|
|
69
|
+
if (childId !== undefined)
|
|
70
|
+
this.code.write(`${id}.append(${childId});`);
|
|
71
|
+
}
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
#text(text) {
|
|
75
|
+
const s = text.textContent;
|
|
76
|
+
if (s === null)
|
|
77
|
+
return;
|
|
78
|
+
if (s.trim() === "")
|
|
79
|
+
return;
|
|
80
|
+
const v = this.code.next();
|
|
81
|
+
this.code.write(`const ${v} = document.createTextNode(${esc(s)});`);
|
|
82
|
+
return v;
|
|
83
|
+
}
|
|
84
|
+
#element(el) {
|
|
85
|
+
if (el.tagName === "style")
|
|
86
|
+
return this.#style(el);
|
|
87
|
+
const tag = el.tagName.toLowerCase();
|
|
88
|
+
const v = this.code.next();
|
|
89
|
+
this.code.write(`const ${v}=document.createElement(${esc(tag)});`);
|
|
90
|
+
for (const x of el.attributes) {
|
|
91
|
+
let { name, value } = x;
|
|
92
|
+
if (name === "src" || name === "href") {
|
|
93
|
+
if (value.startsWith(".") || value.startsWith("/")) {
|
|
94
|
+
const imp = this.style.next();
|
|
95
|
+
this.style.write(`import ${imp} from ${esc(value)};`);
|
|
96
|
+
value = imp;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
value = esc(value);
|
|
101
|
+
}
|
|
102
|
+
this.code.write(`${v}.setAttribute(${esc(name)}, ${value});`);
|
|
103
|
+
}
|
|
104
|
+
return v;
|
|
105
|
+
}
|
|
106
|
+
#style(el) {
|
|
107
|
+
const v = this.style.next();
|
|
108
|
+
const css = new CleanCSS().minify(el.textContent ?? "");
|
|
109
|
+
this.style.write(`const ${v}=new CSSStyleSheet();`);
|
|
110
|
+
this.style.write(`${v}.replaceSync(${esc(css.styles)});`);
|
|
111
|
+
this.code.write(`sh.adoptedStyleSheets.push(${v});`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
class SB {
|
|
115
|
+
prefix;
|
|
116
|
+
s = "";
|
|
117
|
+
#id = 0;
|
|
118
|
+
next() {
|
|
119
|
+
this.#id++;
|
|
120
|
+
return this.prefix + this.#id;
|
|
121
|
+
}
|
|
122
|
+
constructor(prefix) {
|
|
123
|
+
this.prefix = prefix;
|
|
124
|
+
}
|
|
125
|
+
write(s) {
|
|
126
|
+
this.s += s;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function esc(s) {
|
|
130
|
+
return JSON.stringify(s);
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-xhtml2shadow",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Converts .xhtml files into JavaScript code that produces a shadow root with that HTML.",
|
|
6
|
+
"keywords": ["vite-plugin"],
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"README.md",
|
|
11
|
+
"dist/index.js",
|
|
12
|
+
"dist/index.d.ts"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepare": "tsc",
|
|
17
|
+
"test": "vitest --coverage"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/p-jack/dubc.git"
|
|
22
|
+
},
|
|
23
|
+
"author": "Paul Jack",
|
|
24
|
+
"license": "BSD-3-Clause",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/p-jack/dubc/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/p-jack/dubc#readme",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/clean-css": "^4.2.11",
|
|
31
|
+
"@types/jsdom": "^28.0.3",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
33
|
+
"typescript": "^6.0.3",
|
|
34
|
+
"vitest": "^4.1.7"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"clean-css": "^5.3.3",
|
|
38
|
+
"jsdom": "^29.1.1",
|
|
39
|
+
"vite": "^8"
|
|
40
|
+
}
|
|
41
|
+
}
|