@ornery/web-components 1.1.8 → 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 +51 -52
- package/src/event-map.js +1 -1
- package/src/i18n.js +87 -187
- 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 +46 -36
- 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
|
@@ -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
|
@@ -14,21 +14,21 @@
|
|
|
14
14
|
*
|
|
15
15
|
* result == 'bar';
|
|
16
16
|
*/
|
|
17
|
-
const keyRegexp = /^[\w
|
|
17
|
+
const keyRegexp = /^[\w-]+(\.[\w-]+)+$/g;
|
|
18
18
|
const getFromObj = (path, obj = {}) => {
|
|
19
19
|
path = path && path.trim();
|
|
20
|
-
if (path != null){
|
|
20
|
+
if (path != null) {
|
|
21
21
|
if (obj[path] != null) {
|
|
22
22
|
return obj[path];
|
|
23
23
|
} else if (keyRegexp.test(path)) {
|
|
24
24
|
return path.split('.').reduce((res, key) => res[key] != null ? res[key] : path, obj);
|
|
25
|
-
}
|
|
25
|
+
}
|
|
26
26
|
}
|
|
27
27
|
return path;
|
|
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
|
/**
|
|
@@ -44,11 +44,11 @@ const stripES6 = function(expr, context) {
|
|
|
44
44
|
let result = expr.replace(thisRegex, '');
|
|
45
45
|
let matchArr;
|
|
46
46
|
while (matchArr = nestedES6.exec(result)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const [, outerMatch, key] = matchArr;
|
|
48
|
+
const replacement = getFromObj(key.replace(thisRegex, ''), context);
|
|
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
|
/**
|
|
@@ -104,12 +104,12 @@ const template = stripES6;
|
|
|
104
104
|
const arrayParser = (val, key, params) => {
|
|
105
105
|
let current = params[key];
|
|
106
106
|
if (current) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
if (!Array.isArray(current)) {
|
|
108
|
+
current = [current];
|
|
109
|
+
}
|
|
110
|
+
current.push(val);
|
|
111
111
|
} else {
|
|
112
|
-
|
|
112
|
+
current = val;
|
|
113
113
|
}
|
|
114
114
|
return current;
|
|
115
115
|
};
|
|
@@ -144,12 +144,12 @@ const toParams = (str, options = {}) => {
|
|
|
144
144
|
const queryString = parts[1] || '';
|
|
145
145
|
const params = {};
|
|
146
146
|
queryString.split('&').forEach((val) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const innerParts = val.split('=');
|
|
148
|
+
if (innerParts.length !== 2) return;
|
|
149
|
+
const paramKey = decodeURIComponent(innerParts[0]);
|
|
150
|
+
const paramVal = decodeURIComponent(innerParts[1]);
|
|
151
|
+
const parser = options[paramKey] || (() => paramVal);
|
|
152
|
+
params[paramKey] = arrayParser(parser(paramVal, paramKey, params), paramKey, params);
|
|
153
153
|
});
|
|
154
154
|
return params;
|
|
155
155
|
};
|
|
@@ -176,11 +176,11 @@ const toParams = (str, options = {}) => {
|
|
|
176
176
|
const toSearch = (options) => {
|
|
177
177
|
const filtered = Object.entries(options).filter((ent) => !!ent[1]);
|
|
178
178
|
return encodeURI(`?${filtered.map((ent) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
if (Array.isArray(ent[1])) {
|
|
180
|
+
return ent[1].map((val) => [ent[0], val].join('=')).join('&');
|
|
181
|
+
} else {
|
|
182
|
+
return ent.join('=');
|
|
183
|
+
}
|
|
184
184
|
}).join('&')}`);
|
|
185
185
|
};
|
|
186
186
|
|
|
@@ -210,13 +210,13 @@ const toSearch = (options) => {
|
|
|
210
210
|
const prefixKeys = (obj, prefix) => {
|
|
211
211
|
let keys = [];
|
|
212
212
|
if (Array.isArray(obj)) {
|
|
213
|
-
|
|
213
|
+
keys = obj.map((val, i) => i);
|
|
214
214
|
} else {
|
|
215
|
-
|
|
215
|
+
keys = Object.keys(obj);
|
|
216
216
|
}
|
|
217
217
|
return Object.assign(
|
|
218
218
|
{},
|
|
219
|
-
...keys.map((key) => ({[prefix + key]: obj[key]}))
|
|
219
|
+
...keys.map((key) => ({[prefix + key]: obj[key]})),
|
|
220
220
|
);
|
|
221
221
|
};
|
|
222
222
|
|
|
@@ -257,18 +257,27 @@ const toDataAttrs = (obj) => {
|
|
|
257
257
|
return prefixKeys(obj, 'data-');
|
|
258
258
|
};
|
|
259
259
|
const HTMLEncodable = /[\u00A0-\u9999<>]/g;
|
|
260
|
-
const encodeHTML = (stringVal =
|
|
260
|
+
const encodeHTML = (stringVal = '') => stringVal.replace(HTMLEncodable, (i) => `&#${i.charCodeAt(0)};`);
|
|
261
261
|
|
|
262
262
|
const withClosing = /<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi;
|
|
263
263
|
const selfClosing = /(<([^>]+)\/>)/ig;
|
|
264
|
-
const shouldEncode = (str) =>
|
|
265
|
-
return (str || '')
|
|
266
|
-
.replace(withClosing, '')
|
|
267
|
-
.replace(selfClosing, '')
|
|
268
|
-
.trim();
|
|
269
|
-
}
|
|
264
|
+
const shouldEncode = (str) => (str || '').replace(withClosing, '').replace(selfClosing, '').trim();
|
|
270
265
|
|
|
271
|
-
|
|
266
|
+
const toLowerMap = (obj = {}) => {
|
|
267
|
+
if (Array.isArray(obj)) {
|
|
268
|
+
return obj.map(toLowerMap);
|
|
269
|
+
}
|
|
270
|
+
if (typeof obj === 'string') {
|
|
271
|
+
return obj.toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
return Object.entries(obj).reduce((acc, [key, val]) => {
|
|
274
|
+
const lck = key.toLowerCase();
|
|
275
|
+
acc[lck] = toLowerMap(val);
|
|
276
|
+
return acc;
|
|
277
|
+
}, {});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export {
|
|
272
281
|
getFromObj,
|
|
273
282
|
template,
|
|
274
283
|
stripES6,
|
|
@@ -278,5 +287,6 @@ module.exports = {
|
|
|
278
287
|
prefixKeys,
|
|
279
288
|
toDataAttrs,
|
|
280
289
|
shouldEncode,
|
|
281
|
-
encodeHTML
|
|
290
|
+
encodeHTML,
|
|
291
|
+
toLowerMap,
|
|
282
292
|
};
|
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
|
-
];
|