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 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 `&nbsp;`
101
+ like you would in HTML, use `&#x00A0;` 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.
@@ -0,0 +1,2 @@
1
+ import { type PluginOption } from "vite";
2
+ export default function xhtml2shadow(): PluginOption;
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
+ }