@ornery/web-components 2.0.0 → 4.0.0
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/.github/workflows/publish.yml +25 -0
- package/esbuild.js +2 -0
- package/html.d.ts +7 -0
- package/index.js +7 -17
- package/jestLoader.js +5 -3
- package/loader.js +1 -2
- package/package.json +26 -18
- package/rollup.js +2 -0
- package/rspack.js +2 -0
- package/src/bind-events.js +2 -2
- package/src/context-binding.js +1 -1
- package/src/data-manager.js +2 -2
- package/src/event-map.js +1 -1
- package/src/i18n.js +5 -8
- package/src/index.js +5 -7
- package/src/loaders/attrs-parser.js +2 -2
- package/src/loaders/html-loader.js +107 -131
- package/src/plugin.js +14 -0
- package/src/setup-connect.js +1 -1
- package/src/utils.js +4 -4
- package/templates.js +2 -7
- package/tests/fixtures/list.html +10 -0
- package/tests/fixtures/list.scss +12 -0
- package/tests/loader.smoke.test.mjs +127 -0
- package/vite.js +2 -0
- package/webpack.js +2 -0
- package/src/loaders/rules.js +0 -17
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: '20'
|
|
19
|
+
registry-url: 'https://registry.npmjs.org'
|
|
20
|
+
|
|
21
|
+
- run: npm install
|
|
22
|
+
|
|
23
|
+
- run: npm publish --access public
|
|
24
|
+
env:
|
|
25
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/esbuild.js
ADDED
package/html.d.ts
ADDED
package/index.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
module.exports = {
|
|
10
|
-
ContextBinding,
|
|
11
|
-
DataManager,
|
|
12
|
-
EventMap,
|
|
13
|
-
bindEvents,
|
|
14
|
-
setupConnect,
|
|
15
|
-
...i18n,
|
|
16
|
-
...utils,
|
|
17
|
-
};
|
|
1
|
+
export { default as ContextBinding } from './src/context-binding.js';
|
|
2
|
+
export { default as DataManager } from './src/data-manager.js';
|
|
3
|
+
export { default as EventMap } from './src/event-map.js';
|
|
4
|
+
export { I18n } from './src/i18n.js';
|
|
5
|
+
export { default as bindEvents } from './src/bind-events.js';
|
|
6
|
+
export { default as setupConnect } from './src/setup-connect.js';
|
|
7
|
+
export * from './src/utils.js';
|
package/jestLoader.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { transformHTML } from './src/loaders/html-loader.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
export default {
|
|
4
|
+
processAsync: async (src, filename) => ({
|
|
5
|
+
code: await transformHTML(src, filename),
|
|
6
|
+
}),
|
|
5
7
|
};
|
package/loader.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
module.exports = HtmlLoader;
|
|
1
|
+
export { default } from './webpack.js';
|
package/package.json
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornery/web-components",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "WebComponents html loader and optional runtime mixins to enable creation of custom HTML elements using es6 template literal syntax in *.html files.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./templates": "./templates.js",
|
|
10
|
+
"./loader": "./loader.js",
|
|
11
|
+
"./vite": "./vite.js",
|
|
12
|
+
"./webpack": "./webpack.js",
|
|
13
|
+
"./rollup": "./rollup.js",
|
|
14
|
+
"./esbuild": "./esbuild.js",
|
|
15
|
+
"./rspack": "./rspack.js",
|
|
16
|
+
"./html.d.ts": "./html.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"types": "./html.d.ts",
|
|
6
19
|
"scripts": {
|
|
7
|
-
"docs": "jsdoc2md src/loaders/*.js src/*.js > README.md"
|
|
20
|
+
"docs": "jsdoc2md src/loaders/*.js src/*.js > README.md",
|
|
21
|
+
"test": "node --test tests/*.test.mjs"
|
|
8
22
|
},
|
|
9
23
|
"author": "timothyswt@gmail.com",
|
|
10
24
|
"license": "MIT",
|
|
11
25
|
"dependencies": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"loader-utils": "^1.1.0",
|
|
17
|
-
"node-sass": "^6.0.0",
|
|
18
|
-
"sass": "^1.34.0"
|
|
26
|
+
"fastparse": "^1.1.2",
|
|
27
|
+
"html-minifier-terser": "^7.2.0",
|
|
28
|
+
"sass": "^1.86.0",
|
|
29
|
+
"unplugin": "^3.0.0"
|
|
19
30
|
},
|
|
20
31
|
"devDependencies": {
|
|
21
|
-
"eslint": "^
|
|
22
|
-
"eslint-config-
|
|
23
|
-
"eslint-
|
|
24
|
-
"
|
|
25
|
-
"jsdoc": "^
|
|
26
|
-
"
|
|
27
|
-
},
|
|
28
|
-
"peerDependencies": {
|
|
29
|
-
"node-sass": "^4.13.1"
|
|
32
|
+
"eslint": "^9.0.0",
|
|
33
|
+
"eslint-config-google": "^0.14.0",
|
|
34
|
+
"eslint-plugin-jsdoc": "^50.0.0",
|
|
35
|
+
"jsdoc": "^4.0.0",
|
|
36
|
+
"jsdoc-to-markdown": "^9.0.0",
|
|
37
|
+
"jsdom": "^29.1.1"
|
|
30
38
|
}
|
|
31
39
|
}
|
package/rollup.js
ADDED
package/rspack.js
ADDED
package/src/bind-events.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import {template} from './utils.js';
|
|
2
2
|
/**
|
|
3
3
|
* @param {HTMLElement} root The root element to find all elements from.
|
|
4
4
|
* @param {Object} context the context object for finding functions to bind against. default is the root element
|
|
@@ -96,4 +96,4 @@ const bindEvents = (root, context = root) => {
|
|
|
96
96
|
return root;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
export default bindEvents;
|
package/src/context-binding.js
CHANGED
package/src/data-manager.js
CHANGED
package/src/event-map.js
CHANGED
package/src/i18n.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import DataManager from './data-manager.js';
|
|
2
|
+
import {template, getFromObj, toLowerMap} from './utils.js';
|
|
3
3
|
|
|
4
|
-
if (typeof HTMLElement === 'undefined') {
|
|
5
|
-
|
|
6
|
-
HTMLElement = class {};
|
|
4
|
+
if (typeof globalThis.HTMLElement === 'undefined') {
|
|
5
|
+
globalThis.HTMLElement = class {};
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -246,6 +245,4 @@ class I18n {
|
|
|
246
245
|
}
|
|
247
246
|
}
|
|
248
247
|
|
|
249
|
-
|
|
250
|
-
I18n,
|
|
251
|
-
};
|
|
248
|
+
export { I18n };
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import ContextBinding from './context-binding';
|
|
2
|
-
import DataManager from './data-manager';
|
|
3
|
-
import EventMap from './event-map';
|
|
4
|
-
|
|
5
|
-
export * from './
|
|
1
|
+
import ContextBinding from './context-binding.js';
|
|
2
|
+
import DataManager from './data-manager.js';
|
|
3
|
+
import EventMap from './event-map.js';
|
|
4
|
+
export * from './utils.js';
|
|
5
|
+
export * from './i18n.js';
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
ContextBinding,
|
|
9
9
|
DataManager,
|
|
10
10
|
EventMap,
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
export default HTMLLoader;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import Parser from 'fastparse';
|
|
2
2
|
|
|
3
3
|
const attrsParser = new Parser({
|
|
4
4
|
outside: {
|
|
@@ -41,7 +41,7 @@ const attrsParser = new Parser({
|
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
export default (html, tagAttr) => {
|
|
45
45
|
return attrsParser.parse('outside', html, {
|
|
46
46
|
currentTag: null,
|
|
47
47
|
results: [],
|
|
@@ -1,91 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const {compile} = require('es6-templates');
|
|
1
|
+
import { minify } from 'html-minifier-terser';
|
|
2
|
+
import attrParse from './attrs-parser.js';
|
|
3
|
+
import * as sass from 'sass';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
8
6
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
7
|
+
const RUNTIME_HELPERS = `
|
|
8
|
+
const __idRe = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
9
|
+
const __reserved = new Set(['props', 'arguments', 'this']);
|
|
10
|
+
function __render(__p, __body) {
|
|
11
|
+
const keys = Object.keys(__p).filter((k) => __idRe.test(k) && !__reserved.has(k));
|
|
12
|
+
const fn = new Function('props', ...keys, 'return \`' + __body + '\`');
|
|
13
|
+
return fn.apply(__p, [__p, ...keys.map((k) => __p[k])]);
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
11
16
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
useShortDoctype: true,
|
|
28
|
-
keepClosingSlash: true,
|
|
29
|
-
removeScriptTypeAttributes: true,
|
|
30
|
-
removeStyleTypeAttributes: true,
|
|
31
|
-
}, ...query, ...config};
|
|
17
|
+
const defaultOptions = {
|
|
18
|
+
minimize: true,
|
|
19
|
+
removeComments: true,
|
|
20
|
+
collapseWhitespace: true,
|
|
21
|
+
exportAsEs6Default: true,
|
|
22
|
+
attributes: [],
|
|
23
|
+
interpolate: false,
|
|
24
|
+
urlRoot: '',
|
|
25
|
+
removeCommentsFromCDATA: true,
|
|
26
|
+
removeCDATASectionsFromCDATA: true,
|
|
27
|
+
conservativeCollapse: true,
|
|
28
|
+
useShortDoctype: true,
|
|
29
|
+
keepClosingSlash: true,
|
|
30
|
+
removeScriptTypeAttributes: true,
|
|
31
|
+
removeStyleTypeAttributes: true,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @
|
|
36
|
-
* @param {String} content
|
|
37
|
-
* @
|
|
38
|
-
* @
|
|
35
|
+
* @function transformHTML
|
|
36
|
+
* @param {String} content the HTML source content
|
|
37
|
+
* @param {String} id the file path of the HTML file being transformed
|
|
38
|
+
* @param {Object} options plugin options
|
|
39
|
+
* @return {Promise<String>} the HTML content wrapped as an ESM module function
|
|
40
|
+
* @description Transforms an HTML file into an ES module that exports a function.
|
|
39
41
|
* That function takes a single argument (p shorthand for "props").
|
|
40
|
-
* Also provides sass support by incliding a `link` tag in your html file to the scss file.
|
|
41
42
|
*
|
|
42
43
|
* We use the builtin DOMParser to parse the HTML template to reduce runtime dependencies.
|
|
43
|
-
*
|
|
44
|
-
* it into the DOMParser.parseFromString fn.
|
|
45
|
-
*
|
|
46
|
-
* The IIFE ends with fn.call(p, p) which ensures that the es6 template context supports
|
|
44
|
+
* The function ends with fn.call(p, p) which ensures that the es6 template context supports
|
|
47
45
|
* both "this" and "props" within the template.
|
|
48
46
|
*
|
|
49
|
-
* ${this.myValue} and ${props.myValue} are treated identically and can be used
|
|
50
|
-
* For on*="" HTML5 event attributes, the loader replaces any ES6 syntax before babel conversion
|
|
51
|
-
* to the es6 template literal. This way, the interaction between the on* events and the ContextBinding mixin
|
|
52
|
-
* does not break. @see ContextBinding for more details.
|
|
47
|
+
* ${this.myValue} and ${props.myValue} are treated identically and can be used interchangeably.
|
|
53
48
|
*
|
|
54
|
-
* @example @lang js <caption>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
rules: [
|
|
58
|
-
{
|
|
59
|
-
// set this to match the paths of the html files you want to import as functions.
|
|
60
|
-
test: /web-components\/.+\.html$/,
|
|
61
|
-
exclude: /node_modules/,
|
|
62
|
-
use: [{
|
|
63
|
-
loader: '@ornery/web-components/loader',
|
|
64
|
-
options: {
|
|
65
|
-
minimize: true,
|
|
66
|
-
removeComments: true,
|
|
67
|
-
collapseWhitespace: true,
|
|
68
|
-
exportAsEs6Default: true,
|
|
69
|
-
attrs: false,
|
|
70
|
-
interpolate: false
|
|
71
|
-
}
|
|
72
|
-
}]
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
}
|
|
76
|
-
}
|
|
49
|
+
* @example @lang js <caption>vite.config.js</caption>
|
|
50
|
+
* import { defineConfig } from 'vite';
|
|
51
|
+
* import webComponents from '@ornery/web-components/vite';
|
|
77
52
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
53
|
+
* export default defineConfig({
|
|
54
|
+
* plugins: [
|
|
55
|
+
* webComponents({ include: /\.html$/ })
|
|
56
|
+
* ]
|
|
57
|
+
* });
|
|
82
58
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* margin: .5rem;
|
|
86
|
-
* }
|
|
87
|
-
* }
|
|
59
|
+
* @example @lang js <caption>webpack.config.js</caption>
|
|
60
|
+
* import webComponents from '@ornery/web-components/webpack';
|
|
88
61
|
*
|
|
62
|
+
* export default {
|
|
63
|
+
* plugins: [
|
|
64
|
+
* webComponents({ include: /\.html$/ })
|
|
65
|
+
* ]
|
|
66
|
+
* };
|
|
89
67
|
*
|
|
90
68
|
* @example @lang html <caption>example.html</caption>
|
|
91
69
|
* <link src="./example.scss" />
|
|
@@ -95,23 +73,6 @@ const getLoaderConfig = function(context) {
|
|
|
95
73
|
* </ul>
|
|
96
74
|
*
|
|
97
75
|
* @example @lang js
|
|
98
|
-
* // becomes converted into:
|
|
99
|
-
* const {bindEvents, setupConnect} = require("@ornery/web-components/templates");
|
|
100
|
-
* module.exports = (p = {})=> {
|
|
101
|
-
* const parsed = new DOMParser().parseFromString(function(props){
|
|
102
|
-
* return "<style>.example-list{padding: 0;margin: 0;} .example-list .example-list-item{line-height: 1rem;margin: .5rem;}</style>" +
|
|
103
|
-
* "<h3>" + this.headerText + "</h3>" +
|
|
104
|
-
* "<ul>" +
|
|
105
|
-
* this.items.map(function(item){return "<li>" + item + "</li>"; })
|
|
106
|
-
* .join("") +
|
|
107
|
-
* "</ul>"
|
|
108
|
-
* }.call(p, p), 'text/html');
|
|
109
|
-
*
|
|
110
|
-
* const elements = [...parsed.head.children, ...bindEvents(parsed.body, p).childNodes];
|
|
111
|
-
* return setupConnect(elements, p)
|
|
112
|
-
* }
|
|
113
|
-
*
|
|
114
|
-
* @example @lang js
|
|
115
76
|
* import listTemplate from './example.html';
|
|
116
77
|
* const fruits = ["apple", "orange", "banana"];
|
|
117
78
|
*
|
|
@@ -119,23 +80,11 @@ const getLoaderConfig = function(context) {
|
|
|
119
80
|
* headerText: "List of fruits.",
|
|
120
81
|
* items: fruits
|
|
121
82
|
* });
|
|
122
|
-
*
|
|
123
|
-
* console.log(compiledDOMNodeArray.length) // 2
|
|
124
|
-
* console.log(compiledDOMNodeArray[0].tagName) // "h3"
|
|
125
|
-
* console.log(compiledDOMNodeArray[0].innerHTML) // "List of fruits."
|
|
126
|
-
* console.log(compiledDOMNodeArray[1].tagName) // "ul"
|
|
127
|
-
* console.log(compiledDOMNodeArray[1].children[0].tagName) // "li"
|
|
128
|
-
* console.log(compiledDOMNodeArray[1].children[0].innerHTML) // "apple"
|
|
129
|
-
* console.log(compiledDOMNodeArray[1].children[1].tagName) // "li"
|
|
130
|
-
* console.log(compiledDOMNodeArray[1].children[1].innerHTML) // "orange"
|
|
131
|
-
* console.log(compiledDOMNodeArray[1].children[2].tagName) // "li"
|
|
132
|
-
* console.log(compiledDOMNodeArray[1].children[2].innerHTML) // "banana"
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
83
|
*/
|
|
137
|
-
|
|
138
|
-
const config =
|
|
84
|
+
export async function transformHTML(content, id, options = {}) {
|
|
85
|
+
const config = { ...defaultOptions, ...options };
|
|
86
|
+
const fileDir = path.dirname(id);
|
|
87
|
+
|
|
139
88
|
const links = attrParse(content, function(tag, attr) {
|
|
140
89
|
const res = config.attributes.find(function(a) {
|
|
141
90
|
if (a.charAt(0) === ':') {
|
|
@@ -150,16 +99,8 @@ const htmlLoader = function(content) {
|
|
|
150
99
|
const data = {};
|
|
151
100
|
content = [content];
|
|
152
101
|
links.forEach(function(link) {
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
if (link.value.indexOf('mailto:') > -1 ) return;
|
|
102
|
+
if (link.value.indexOf('mailto:') > -1) return;
|
|
156
103
|
|
|
157
|
-
const uri = url.parse(link.value);
|
|
158
|
-
if (uri.hash !== null && uri.hash !== undefined) {
|
|
159
|
-
uri.hash = null;
|
|
160
|
-
link.value = uri.format();
|
|
161
|
-
link.length = link.value.length;
|
|
162
|
-
}
|
|
163
104
|
let ident;
|
|
164
105
|
while (data[ident]) {
|
|
165
106
|
ident = '~~~HTMLLINK~~~' + Math.random() + Math.random() + '~~~';
|
|
@@ -172,25 +113,60 @@ const htmlLoader = function(content) {
|
|
|
172
113
|
});
|
|
173
114
|
content.reverse();
|
|
174
115
|
content = content.join('');
|
|
175
|
-
const linkregex = /^\s*<link.+href="(.+?s*css)".*>$/gmi;
|
|
176
|
-
const imports = [...content.matchAll(linkregex)];
|
|
177
116
|
content = content.replace(/on(\w+?)=["']\$\{(.+?)\}.*?["']/gmi, (m, cg1, cg2) => `on${cg1}="${cg2}"`);
|
|
178
|
-
content =
|
|
117
|
+
content = await minify(content, { ...config });
|
|
179
118
|
content = content.replace(/\\"/g, '\\\\"');
|
|
180
119
|
content = content.replace(/\\'/g, '\\\\\'');
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
120
|
+
|
|
121
|
+
const linkregex = /<link[^>]+href=["']([^"']+?\.s?css)["'][^>]*>/gi;
|
|
122
|
+
const imports = [...content.matchAll(linkregex)];
|
|
123
|
+
const styleContent = imports.map((m) => {
|
|
124
|
+
const filePath = path.resolve(fileDir, m[1]);
|
|
125
|
+
if (fs.existsSync(filePath)) {
|
|
126
|
+
content = content.replace(m[0], '');
|
|
127
|
+
return sass.compile(filePath).css;
|
|
190
128
|
}
|
|
191
129
|
return '';
|
|
192
|
-
}).join('\n')
|
|
193
|
-
|
|
130
|
+
}).join('\n');
|
|
131
|
+
|
|
132
|
+
content = escapeBackticksOutsideInterpolation(content);
|
|
133
|
+
const stylePrefix = styleContent ? `<style>${styleContent.replace(/`/g, '\\`')}</style>` : '';
|
|
134
|
+
const body = JSON.stringify(stylePrefix + content);
|
|
135
|
+
return `import {bindEvents, setupConnect} from "@ornery/web-components/templates";
|
|
136
|
+
${RUNTIME_HELPERS}
|
|
137
|
+
export default (p = {}) => {
|
|
138
|
+
const parsed = new DOMParser().parseFromString(__render(p, ${body}), 'text/html');
|
|
139
|
+
const elements = [...parsed.head.children, ...bindEvents(parsed.body, p).childNodes];
|
|
140
|
+
return setupConnect(elements, p);
|
|
194
141
|
};
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function escapeBackticksOutsideInterpolation(str) {
|
|
146
|
+
let out = '';
|
|
147
|
+
let i = 0;
|
|
148
|
+
while (i < str.length) {
|
|
149
|
+
const ch = str[i];
|
|
150
|
+
if (ch === '$' && str[i + 1] === '{') {
|
|
151
|
+
let depth = 1;
|
|
152
|
+
let j = i + 2;
|
|
153
|
+
while (j < str.length && depth > 0) {
|
|
154
|
+
if (str[j] === '{') depth++;
|
|
155
|
+
else if (str[j] === '}') depth--;
|
|
156
|
+
if (depth === 0) break;
|
|
157
|
+
j++;
|
|
158
|
+
}
|
|
159
|
+
out += str.slice(i, j + 1);
|
|
160
|
+
i = j + 1;
|
|
161
|
+
} else if (ch === '`') {
|
|
162
|
+
out += '\\`';
|
|
163
|
+
i++;
|
|
164
|
+
} else {
|
|
165
|
+
out += ch;
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
195
171
|
|
|
196
|
-
|
|
172
|
+
export default transformHTML;
|
package/src/plugin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createUnplugin } from 'unplugin';
|
|
2
|
+
import { transformHTML } from './loaders/html-loader.js';
|
|
3
|
+
|
|
4
|
+
export const unplugin = createUnplugin((options = {}) => ({
|
|
5
|
+
name: 'ornery-web-components',
|
|
6
|
+
transformInclude(id) {
|
|
7
|
+
return (options.include || /\.html$/).test(id);
|
|
8
|
+
},
|
|
9
|
+
transform(code, id) {
|
|
10
|
+
return transformHTML(code, id, options);
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
export default unplugin;
|
package/src/setup-connect.js
CHANGED
package/src/utils.js
CHANGED
|
@@ -28,7 +28,7 @@ const getFromObj = (path, obj = {}) => {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
const thisRegex = /^
|
|
31
|
+
const thisRegex = /^(this|props)\./i;
|
|
32
32
|
const nestedES6 = /\$\{.*(\$\{(.+?)\}).*\}/g;
|
|
33
33
|
const es6Regex = /\$\{(.+?)\}/g;
|
|
34
34
|
/**
|
|
@@ -45,10 +45,10 @@ const stripES6 = function(expr, context) {
|
|
|
45
45
|
let matchArr;
|
|
46
46
|
while (matchArr = nestedES6.exec(result)) {
|
|
47
47
|
const [, outerMatch, key] = matchArr;
|
|
48
|
-
const replacement = getFromObj(key, context);
|
|
48
|
+
const replacement = getFromObj(key.replace(thisRegex, ''), context);
|
|
49
49
|
result = stripES6(result.replace(outerMatch, replacement).trim(), context);
|
|
50
50
|
}
|
|
51
|
-
return result.replace(es6Regex, (match, $1)=> getFromObj($1, context));
|
|
51
|
+
return result.replace(es6Regex, (match, $1) => getFromObj($1.replace(thisRegex, ''), context));
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
/**
|
|
@@ -277,7 +277,7 @@ const toLowerMap = (obj = {}) => {
|
|
|
277
277
|
}, {});
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
-
|
|
280
|
+
export {
|
|
281
281
|
getFromObj,
|
|
282
282
|
template,
|
|
283
283
|
stripES6,
|
package/templates.js
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
module.exports = {
|
|
5
|
-
bindEvents,
|
|
6
|
-
setupConnect,
|
|
7
|
-
};
|
|
1
|
+
export { default as bindEvents } from './src/bind-events.js';
|
|
2
|
+
export { default as setupConnect } from './src/setup-connect.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<link href="./list.scss" />
|
|
2
|
+
<h3 id="list-title">${this.headerText}</h3>
|
|
3
|
+
<p id="subtitle-props">${props.subtitle}</p>
|
|
4
|
+
<p id="subtitle-bare">${subtitle}</p>
|
|
5
|
+
<ul class="example-list">
|
|
6
|
+
${this.items.map((item) => `<li class="example-list-item">${item}</li>`).join("")}
|
|
7
|
+
</ul>
|
|
8
|
+
<button id="add-this" onclick="${this.onAdd}">Add via this</button>
|
|
9
|
+
<button id="add-props" onclick="${props.onAdd}">Add via props</button>
|
|
10
|
+
<button id="add-bare" onclick="${onAdd}">Add via bare</button>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
|
+
import { JSDOM } from 'jsdom';
|
|
7
|
+
|
|
8
|
+
import vitePlugin from '@ornery/web-components/vite';
|
|
9
|
+
import webpackPlugin from '@ornery/web-components/webpack';
|
|
10
|
+
import rollupPlugin from '@ornery/web-components/rollup';
|
|
11
|
+
import esbuildPlugin from '@ornery/web-components/esbuild';
|
|
12
|
+
import rspackPlugin from '@ornery/web-components/rspack';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const fixture = path.join(__dirname, 'fixtures', 'list.html');
|
|
16
|
+
|
|
17
|
+
// Exercise the bundler plugin's transform hook the same way Vite/Webpack would.
|
|
18
|
+
async function runTransform(id) {
|
|
19
|
+
const plugin = vitePlugin();
|
|
20
|
+
const code = await readFile(id, 'utf8');
|
|
21
|
+
assert.equal(plugin.transformInclude(id), true, 'plugin opts into .html files');
|
|
22
|
+
const result = await plugin.transform.call({}, code, id);
|
|
23
|
+
return typeof result === 'string' ? result : result.code;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('@ornery/web-components loader', () => {
|
|
27
|
+
let dom;
|
|
28
|
+
before(() => {
|
|
29
|
+
dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
30
|
+
globalThis.window = dom.window;
|
|
31
|
+
globalThis.document = dom.window.document;
|
|
32
|
+
globalThis.DOMParser = dom.window.DOMParser;
|
|
33
|
+
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
34
|
+
globalThis.Node = dom.window.Node;
|
|
35
|
+
globalThis.Element = dom.window.Element;
|
|
36
|
+
globalThis.Event = dom.window.Event;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('transforms an HTML file into an ESM module that matches the documented shape', async () => {
|
|
40
|
+
const html = await readFile(fixture, 'utf8');
|
|
41
|
+
const out = await runTransform(fixture);
|
|
42
|
+
|
|
43
|
+
assert.match(
|
|
44
|
+
out,
|
|
45
|
+
/import \{bindEvents, setupConnect\} from "@ornery\/web-components\/templates"/,
|
|
46
|
+
'output imports bindEvents/setupConnect from the public entry',
|
|
47
|
+
);
|
|
48
|
+
assert.match(out, /export default \(p = \{\}\)\s*=>/, 'default-exports a props-taking function');
|
|
49
|
+
assert.match(out, /new DOMParser\(\)\.parseFromString/, 'uses DOMParser for template assembly');
|
|
50
|
+
assert.match(out, /<style>/, 'inlines a <style> tag');
|
|
51
|
+
|
|
52
|
+
assert.match(out, /\.example-list/, 'compiled SCSS selector is present');
|
|
53
|
+
assert.match(out, /\.example-list-item\s*\{[^}]*color:\s*#2563eb/, 'SCSS was compiled to CSS with resolved $accent');
|
|
54
|
+
|
|
55
|
+
assert.doesNotMatch(out, /\$accent/, 'SCSS variables are resolved');
|
|
56
|
+
assert.doesNotMatch(out, /<link[^>]+\.scss/, 'link tag is stripped after SCSS inline');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('exposes instantiable plugins for every supported bundler', () => {
|
|
60
|
+
for (const factory of [vitePlugin, webpackPlugin, rollupPlugin, esbuildPlugin, rspackPlugin]) {
|
|
61
|
+
const plugin = factory({});
|
|
62
|
+
assert.ok(plugin, `${factory.name || 'plugin'} returned a value`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('evaluating the module and calling it with props renders nodes, styles, and wires events', async () => {
|
|
67
|
+
const out = await runTransform(fixture);
|
|
68
|
+
|
|
69
|
+
// Write the compiled module inside the repo so Node can resolve
|
|
70
|
+
// `@ornery/web-components/templates` through the symlinked node_modules entry
|
|
71
|
+
// — exactly what a consumer project would see after `npm install`.
|
|
72
|
+
const outDir = path.join(__dirname, '.compiled');
|
|
73
|
+
await rm(outDir, { recursive: true, force: true });
|
|
74
|
+
await mkdir(outDir, { recursive: true });
|
|
75
|
+
const modFile = path.join(outDir, 'list.compiled.mjs');
|
|
76
|
+
await writeFile(modFile, out, 'utf8');
|
|
77
|
+
const mod = await import(pathToFileURL(modFile).href);
|
|
78
|
+
|
|
79
|
+
assert.equal(typeof mod.default, 'function', 'default export is a function');
|
|
80
|
+
|
|
81
|
+
let clicks = 0;
|
|
82
|
+
const props = {
|
|
83
|
+
headerText: 'List of fruits.',
|
|
84
|
+
subtitle: 'Fresh picks',
|
|
85
|
+
items: ['apple', 'orange', 'banana'],
|
|
86
|
+
onAdd() { clicks++; },
|
|
87
|
+
};
|
|
88
|
+
const result = mod.default(props);
|
|
89
|
+
|
|
90
|
+
assert.ok(Array.isArray(result), 'returns an array-like node list');
|
|
91
|
+
assert.equal(typeof result.connect, 'function', 'node list carries a connect() function (setupConnect)');
|
|
92
|
+
|
|
93
|
+
const host = document.createElement('div');
|
|
94
|
+
host.append(...result);
|
|
95
|
+
|
|
96
|
+
const title = host.querySelector('#list-title');
|
|
97
|
+
assert.ok(title, 'rendered header');
|
|
98
|
+
assert.equal(title.textContent, 'List of fruits.', 'interpolated ${this.headerText}');
|
|
99
|
+
|
|
100
|
+
// Per docs: ${this.X}, ${props.X}, and ${X} are interchangeable.
|
|
101
|
+
assert.equal(host.querySelector('#subtitle-props').textContent, 'Fresh picks', '${props.subtitle} interpolates');
|
|
102
|
+
assert.equal(host.querySelector('#subtitle-bare').textContent, 'Fresh picks', '${subtitle} (bare) interpolates');
|
|
103
|
+
|
|
104
|
+
const items = host.querySelectorAll('.example-list-item');
|
|
105
|
+
assert.equal(items.length, 3, 'rendered all mapped items');
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
Array.from(items, (li) => li.textContent),
|
|
108
|
+
['apple', 'orange', 'banana'],
|
|
109
|
+
'each item value interpolated inside the mapped template',
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
for (const id of ['add-this', 'add-props', 'add-bare']) {
|
|
113
|
+
const button = host.querySelector('#' + id);
|
|
114
|
+
assert.ok(button, `rendered ${id}`);
|
|
115
|
+
assert.equal(button.getAttribute('onclick'), null, `onclick stripped on ${id}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
host.querySelector('#add-this').dispatchEvent(new dom.window.Event('click'));
|
|
119
|
+
host.querySelector('#add-props').dispatchEvent(new dom.window.Event('click'));
|
|
120
|
+
host.querySelector('#add-bare').dispatchEvent(new dom.window.Event('click'));
|
|
121
|
+
assert.equal(clicks, 3, '${this.fn}, ${props.fn}, and ${fn} all bind the same handler');
|
|
122
|
+
|
|
123
|
+
const style = Array.from(host.children).find((n) => n.tagName === 'STYLE');
|
|
124
|
+
assert.ok(style, '<style> landed in the connected output');
|
|
125
|
+
assert.match(style.textContent, /\.example-list-item/, 'compiled CSS reached the DOM');
|
|
126
|
+
});
|
|
127
|
+
});
|
package/vite.js
ADDED
package/webpack.js
ADDED
package/src/loaders/rules.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
|
-
module.exports = [
|
|
4
|
-
{
|
|
5
|
-
test: /\.scss$/,
|
|
6
|
-
use: [
|
|
7
|
-
'css-loader', // translates CSS into CommonJS
|
|
8
|
-
'sass-loader', // compiles Sass to CSS, using Node Sass by default
|
|
9
|
-
],
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
test: /\.html$/,
|
|
13
|
-
use: [{
|
|
14
|
-
loader: path.resolve(__dirname, '../loaders/html-loader.js'),
|
|
15
|
-
}],
|
|
16
|
-
},
|
|
17
|
-
];
|