smol.js 0.1.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/README.md +205 -0
- package/dist/index.d.ts +285 -0
- package/dist/smol.js +432 -0
- package/dist/smol.umd.js +1 -0
- package/dist/vite-plugin-smol-templates.d.ts +20 -0
- package/dist/vite-plugin-smol-templates.js +73 -0
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +4 -0
- package/package.json +48 -0
- package/src/component.ts +154 -0
- package/src/css.ts +25 -0
- package/src/html-module.d.ts +16 -0
- package/src/html.ts +178 -0
- package/src/hydrate-client.ts +17 -0
- package/src/hydrate.ts +102 -0
- package/src/index.ts +33 -0
- package/src/service.ts +67 -0
- package/src/signal.ts +89 -0
- package/src/ssr.ts +158 -0
- package/src/state.ts +67 -0
- package/src/types.ts +91 -0
- package/src/vite-plugin-smol-templates.ts +85 -0
- package/src/vite.ts +4 -0
package/dist/smol.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
function html(strings, ...values) {
|
|
2
|
+
return {
|
|
3
|
+
strings,
|
|
4
|
+
values,
|
|
5
|
+
_isTemplateResult: true
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function isTemplateResult(value) {
|
|
9
|
+
return value && value._isTemplateResult === true;
|
|
10
|
+
}
|
|
11
|
+
function renderToString(template) {
|
|
12
|
+
let result = "";
|
|
13
|
+
for (let i = 0; i < template.strings.length; i++) {
|
|
14
|
+
result += template.strings[i];
|
|
15
|
+
if (i < template.values.length) {
|
|
16
|
+
const value = template.values[i];
|
|
17
|
+
result += stringifyValue(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
function stringifyValue(value) {
|
|
23
|
+
if (value == null) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
if (isTemplateResult(value)) {
|
|
27
|
+
return renderToString(value);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return value.map(stringifyValue).join("");
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "boolean") {
|
|
33
|
+
return value ? "" : "";
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "function") {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
return escapeHtml(String(value));
|
|
39
|
+
}
|
|
40
|
+
function escapeHtml(unsafe) {
|
|
41
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
42
|
+
}
|
|
43
|
+
function render(template, container) {
|
|
44
|
+
const existingStyles = Array.from(container.querySelectorAll("style"));
|
|
45
|
+
let html2 = "";
|
|
46
|
+
const markers = [];
|
|
47
|
+
let markerIndex = 0;
|
|
48
|
+
for (let i = 0; i < template.strings.length; i++) {
|
|
49
|
+
const str = template.strings[i];
|
|
50
|
+
html2 += str;
|
|
51
|
+
if (i < template.values.length) {
|
|
52
|
+
const value = template.values[i];
|
|
53
|
+
const lastPart = str.trim();
|
|
54
|
+
if (lastPart.endsWith("@click=") || lastPart.endsWith("@change=") || lastPart.includes("@")) {
|
|
55
|
+
markers.push({ index: markerIndex, value, type: "event" });
|
|
56
|
+
html2 += `"__smol_event_${markerIndex}__"`;
|
|
57
|
+
markerIndex++;
|
|
58
|
+
} else if (lastPart.endsWith("?disabled=") || lastPart.endsWith("?checked=") || lastPart.includes("?")) {
|
|
59
|
+
markers.push({ index: markerIndex, value, type: "boolean" });
|
|
60
|
+
html2 += `"__smol_bool_${markerIndex}__"`;
|
|
61
|
+
markerIndex++;
|
|
62
|
+
} else {
|
|
63
|
+
html2 += stringifyValue(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
container.innerHTML = html2;
|
|
68
|
+
existingStyles.forEach((style) => {
|
|
69
|
+
container.insertBefore(style, container.firstChild);
|
|
70
|
+
});
|
|
71
|
+
markers.forEach((marker) => {
|
|
72
|
+
if (marker.type === "event") {
|
|
73
|
+
const markerAttr = `__smol_event_${marker.index}__`;
|
|
74
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
75
|
+
let node;
|
|
76
|
+
while (node = walker.nextNode()) {
|
|
77
|
+
const element = node;
|
|
78
|
+
for (const attr of Array.from(element.attributes)) {
|
|
79
|
+
if (attr.value === markerAttr) {
|
|
80
|
+
const eventName = attr.name.replace("@", "");
|
|
81
|
+
element.removeAttribute(attr.name);
|
|
82
|
+
if (typeof marker.value === "function") {
|
|
83
|
+
element.addEventListener(eventName, marker.value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (marker.type === "boolean") {
|
|
89
|
+
const markerAttr = `__smol_bool_${marker.index}__`;
|
|
90
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
91
|
+
let node;
|
|
92
|
+
while (node = walker.nextNode()) {
|
|
93
|
+
const element = node;
|
|
94
|
+
for (const attr of Array.from(element.attributes)) {
|
|
95
|
+
if (attr.value === markerAttr) {
|
|
96
|
+
const attrName = attr.name.replace("?", "");
|
|
97
|
+
element.removeAttribute(attr.name);
|
|
98
|
+
if (marker.value) {
|
|
99
|
+
element.setAttribute(attrName, "");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function smolComponent(config) {
|
|
108
|
+
const {
|
|
109
|
+
tag,
|
|
110
|
+
mode = "open",
|
|
111
|
+
observedAttributes = [],
|
|
112
|
+
styles = "",
|
|
113
|
+
template,
|
|
114
|
+
connected,
|
|
115
|
+
disconnected,
|
|
116
|
+
attributeChanged
|
|
117
|
+
} = config;
|
|
118
|
+
if (!tag.includes("-")) {
|
|
119
|
+
throw new Error(`Custom element tag names must contain a hyphen: "${tag}"`);
|
|
120
|
+
}
|
|
121
|
+
class SmolCustomElement extends HTMLElement {
|
|
122
|
+
static get observedAttributes() {
|
|
123
|
+
return observedAttributes;
|
|
124
|
+
}
|
|
125
|
+
constructor() {
|
|
126
|
+
super();
|
|
127
|
+
if (!this.shadowRoot) {
|
|
128
|
+
this.attachShadow({ mode });
|
|
129
|
+
}
|
|
130
|
+
if (styles && this.shadowRoot) {
|
|
131
|
+
const styleElement = document.createElement("style");
|
|
132
|
+
styleElement.textContent = styles;
|
|
133
|
+
this.shadowRoot.appendChild(styleElement);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
connectedCallback() {
|
|
137
|
+
if (connected) {
|
|
138
|
+
connected.call(this);
|
|
139
|
+
}
|
|
140
|
+
this.render();
|
|
141
|
+
}
|
|
142
|
+
disconnectedCallback() {
|
|
143
|
+
if (disconnected) {
|
|
144
|
+
disconnected.call(this);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
148
|
+
if (this.isConnected) {
|
|
149
|
+
this.render();
|
|
150
|
+
}
|
|
151
|
+
if (attributeChanged) {
|
|
152
|
+
attributeChanged.call(this, name, oldValue, newValue);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Render the component template
|
|
157
|
+
*/
|
|
158
|
+
render() {
|
|
159
|
+
if (!template || !this.shadowRoot) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const ctx = {
|
|
163
|
+
emit: this.emit.bind(this),
|
|
164
|
+
render: this.render.bind(this),
|
|
165
|
+
// Add element as context (for accessing attributes, etc.)
|
|
166
|
+
element: this
|
|
167
|
+
};
|
|
168
|
+
const result = template.call(this, ctx);
|
|
169
|
+
if (!result) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (typeof result === "string") {
|
|
173
|
+
this.shadowRoot.innerHTML = result;
|
|
174
|
+
} else if (isTemplateResult(result)) {
|
|
175
|
+
render(result, this.shadowRoot);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Emit a custom event
|
|
180
|
+
*/
|
|
181
|
+
emit(name, detail) {
|
|
182
|
+
this.dispatchEvent(new CustomEvent(name, {
|
|
183
|
+
detail,
|
|
184
|
+
bubbles: true,
|
|
185
|
+
composed: true
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
SmolCustomElement._smolConfig = config;
|
|
190
|
+
SmolCustomElement._smolTag = tag;
|
|
191
|
+
if (!customElements.get(tag)) {
|
|
192
|
+
customElements.define(tag, SmolCustomElement);
|
|
193
|
+
}
|
|
194
|
+
return SmolCustomElement;
|
|
195
|
+
}
|
|
196
|
+
const serviceRegistry = /* @__PURE__ */ new Map();
|
|
197
|
+
function smolService(config) {
|
|
198
|
+
const { name, factory, singleton = true } = config;
|
|
199
|
+
if (singleton) {
|
|
200
|
+
if (serviceRegistry.has(name)) {
|
|
201
|
+
return serviceRegistry.get(name);
|
|
202
|
+
}
|
|
203
|
+
const instance = factory();
|
|
204
|
+
serviceRegistry.set(name, instance);
|
|
205
|
+
return instance;
|
|
206
|
+
}
|
|
207
|
+
return factory();
|
|
208
|
+
}
|
|
209
|
+
function inject(name) {
|
|
210
|
+
if (!serviceRegistry.has(name)) {
|
|
211
|
+
throw new Error(`Service "${name}" not found. Did you forget to create it with smolService()?`);
|
|
212
|
+
}
|
|
213
|
+
return serviceRegistry.get(name);
|
|
214
|
+
}
|
|
215
|
+
function clearServices() {
|
|
216
|
+
serviceRegistry.clear();
|
|
217
|
+
}
|
|
218
|
+
function smolSignal(initialValue) {
|
|
219
|
+
let _value = initialValue;
|
|
220
|
+
const _subscribers = /* @__PURE__ */ new Set();
|
|
221
|
+
const signal = {
|
|
222
|
+
get value() {
|
|
223
|
+
return _value;
|
|
224
|
+
},
|
|
225
|
+
set value(newValue) {
|
|
226
|
+
if (_value !== newValue) {
|
|
227
|
+
_value = newValue;
|
|
228
|
+
_subscribers.forEach((fn) => fn(_value));
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
subscribe(fn) {
|
|
232
|
+
_subscribers.add(fn);
|
|
233
|
+
return () => {
|
|
234
|
+
_subscribers.delete(fn);
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
_subscribers
|
|
238
|
+
};
|
|
239
|
+
return signal;
|
|
240
|
+
}
|
|
241
|
+
function computed(fn) {
|
|
242
|
+
const signal = smolSignal(fn());
|
|
243
|
+
return signal;
|
|
244
|
+
}
|
|
245
|
+
function effect(fn) {
|
|
246
|
+
fn();
|
|
247
|
+
return () => {
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function smolState(initialValue) {
|
|
251
|
+
const _subscribers = /* @__PURE__ */ new Set();
|
|
252
|
+
const notify = () => {
|
|
253
|
+
_subscribers.forEach((fn) => fn());
|
|
254
|
+
};
|
|
255
|
+
const data = new Proxy(initialValue, {
|
|
256
|
+
set(target, property, value) {
|
|
257
|
+
const oldValue = target[property];
|
|
258
|
+
if (oldValue !== value) {
|
|
259
|
+
target[property] = value;
|
|
260
|
+
notify();
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
},
|
|
264
|
+
deleteProperty(target, property) {
|
|
265
|
+
if (property in target) {
|
|
266
|
+
delete target[property];
|
|
267
|
+
notify();
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
const state = {
|
|
273
|
+
data,
|
|
274
|
+
subscribe(fn) {
|
|
275
|
+
_subscribers.add(fn);
|
|
276
|
+
return () => {
|
|
277
|
+
_subscribers.delete(fn);
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
_subscribers
|
|
281
|
+
};
|
|
282
|
+
return state;
|
|
283
|
+
}
|
|
284
|
+
function css(strings, ...values) {
|
|
285
|
+
let result = "";
|
|
286
|
+
for (let i = 0; i < strings.length; i++) {
|
|
287
|
+
result += strings[i];
|
|
288
|
+
if (i < values.length) {
|
|
289
|
+
result += String(values[i]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return result.trim();
|
|
293
|
+
}
|
|
294
|
+
function renderComponentToString(ComponentClass, attributes = {}) {
|
|
295
|
+
const instance = new ComponentClass();
|
|
296
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
297
|
+
instance.setAttribute(key, value);
|
|
298
|
+
});
|
|
299
|
+
const tagName = ComponentClass._smolTag || "unknown-element";
|
|
300
|
+
const config = ComponentClass._smolConfig;
|
|
301
|
+
const styles = (config == null ? void 0 : config.styles) || "";
|
|
302
|
+
if (config == null ? void 0 : config.connected) {
|
|
303
|
+
config.connected.call(instance);
|
|
304
|
+
}
|
|
305
|
+
let templateHTML = "";
|
|
306
|
+
if (config == null ? void 0 : config.template) {
|
|
307
|
+
const ctx = {
|
|
308
|
+
emit: instance.emit.bind(instance),
|
|
309
|
+
render: () => {
|
|
310
|
+
},
|
|
311
|
+
element: instance
|
|
312
|
+
};
|
|
313
|
+
const result = config.template.call(instance, ctx);
|
|
314
|
+
if (typeof result === "string") {
|
|
315
|
+
templateHTML = result;
|
|
316
|
+
} else if (result && result._isTemplateResult) {
|
|
317
|
+
templateHTML = renderToString(result);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (globalThis.document && templateHTML) {
|
|
321
|
+
const template = document.createElement("template");
|
|
322
|
+
template.innerHTML = templateHTML;
|
|
323
|
+
expandNestedComponents(template.content);
|
|
324
|
+
templateHTML = template.innerHTML;
|
|
325
|
+
}
|
|
326
|
+
const shadowContent = `
|
|
327
|
+
${styles ? `<style>${styles}</style>` : ""}
|
|
328
|
+
${templateHTML}
|
|
329
|
+
`.trim();
|
|
330
|
+
const attrString = Object.entries(attributes).map(([key, value]) => `${key}="${escapeAttr(value)}"`).join(" ");
|
|
331
|
+
return `
|
|
332
|
+
<${tagName}${attrString ? " " + attrString : ""}>
|
|
333
|
+
<template shadowrootmode="open">
|
|
334
|
+
${shadowContent}
|
|
335
|
+
</template>
|
|
336
|
+
</${tagName}>
|
|
337
|
+
`.trim();
|
|
338
|
+
}
|
|
339
|
+
function escapeAttr(value) {
|
|
340
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
341
|
+
}
|
|
342
|
+
function ssr(components) {
|
|
343
|
+
return components.map(({ component, attributes }) => renderComponentToString(component, attributes)).join("\n");
|
|
344
|
+
}
|
|
345
|
+
function expandNestedComponents(node) {
|
|
346
|
+
const children = Array.from(node.childNodes);
|
|
347
|
+
children.forEach((child) => {
|
|
348
|
+
if (child.nodeType === 1) {
|
|
349
|
+
const element = child;
|
|
350
|
+
const tagName = element.tagName.toLowerCase();
|
|
351
|
+
if (globalThis.customElements && tagName.includes("-")) {
|
|
352
|
+
const ComponentClass = customElements.get(tagName);
|
|
353
|
+
if (ComponentClass) {
|
|
354
|
+
const attributes = {};
|
|
355
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
356
|
+
attributes[attr.name] = attr.value;
|
|
357
|
+
});
|
|
358
|
+
const rendered = renderComponentToString(ComponentClass, attributes);
|
|
359
|
+
const temp = document.createElement("div");
|
|
360
|
+
temp.innerHTML = rendered;
|
|
361
|
+
const newChild = temp.firstElementChild;
|
|
362
|
+
if (newChild) {
|
|
363
|
+
element.replaceWith(newChild);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
expandNestedComponents(element);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function hydrateComponent(element) {
|
|
373
|
+
if (!element.shadowRoot) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
element._hydrated = true;
|
|
377
|
+
attachEventListeners(element);
|
|
378
|
+
}
|
|
379
|
+
function attachEventListeners(element) {
|
|
380
|
+
if (!element.shadowRoot) return;
|
|
381
|
+
const walker = document.createTreeWalker(
|
|
382
|
+
element.shadowRoot,
|
|
383
|
+
NodeFilter.SHOW_ELEMENT
|
|
384
|
+
);
|
|
385
|
+
const elementsWithEvents = [];
|
|
386
|
+
let node;
|
|
387
|
+
while (node = walker.nextNode()) {
|
|
388
|
+
const el = node;
|
|
389
|
+
const events = /* @__PURE__ */ new Map();
|
|
390
|
+
for (const attr of Array.from(el.attributes)) {
|
|
391
|
+
if (attr.name.startsWith("data-smol-event-")) {
|
|
392
|
+
const eventName = attr.name.replace("data-smol-event-", "");
|
|
393
|
+
events.set(eventName, attr.value);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (events.size > 0) {
|
|
397
|
+
elementsWithEvents.push({ element: el, events });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
elementsWithEvents.forEach(({ element: el, events }) => {
|
|
401
|
+
events.forEach((handlerId, eventName) => {
|
|
402
|
+
console.warn(`Hydration: Found event listener ${eventName} but handler lookup not implemented`);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function hydrateAll() {
|
|
407
|
+
const allElements = document.querySelectorAll("*");
|
|
408
|
+
allElements.forEach((element) => {
|
|
409
|
+
if (element.tagName.includes("-") && element.shadowRoot) {
|
|
410
|
+
hydrateComponent(element);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
export {
|
|
415
|
+
clearServices,
|
|
416
|
+
computed,
|
|
417
|
+
css,
|
|
418
|
+
effect,
|
|
419
|
+
html,
|
|
420
|
+
hydrateAll,
|
|
421
|
+
hydrateComponent,
|
|
422
|
+
inject,
|
|
423
|
+
isTemplateResult,
|
|
424
|
+
render,
|
|
425
|
+
renderComponentToString,
|
|
426
|
+
renderToString,
|
|
427
|
+
smolComponent,
|
|
428
|
+
smolService,
|
|
429
|
+
smolSignal,
|
|
430
|
+
smolState,
|
|
431
|
+
ssr
|
|
432
|
+
};
|
package/dist/smol.umd.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Smol={})}(this,function(t){"use strict";function e(t){return t&&!0===t.t}function n(t){let e="";for(let n=0;n<t.strings.length;n++)e+=t.strings[n],n<t.values.length&&(e+=o(t.values[n]));return e}function o(t){return null==t?"":e(t)?n(t):Array.isArray(t)?t.map(o).join(""):"boolean"==typeof t||"function"==typeof t?"":String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function s(t,e){const n=Array.from(e.querySelectorAll("style"));let s="";const i=[];let r=0;for(let c=0;c<t.strings.length;c++){const e=t.strings[c];if(s+=e,c<t.values.length){const n=t.values[c],u=e.trim();u.endsWith("@click=")||u.endsWith("@change=")||u.includes("@")?(i.push({index:r,value:n,type:"event"}),s+=`"__smol_event_${r}__"`,r++):u.endsWith("?disabled=")||u.endsWith("?checked=")||u.includes("?")?(i.push({index:r,value:n,type:"boolean"}),s+=`"__smol_bool_${r}__"`,r++):s+=o(n)}}e.innerHTML=s,n.forEach(t=>{e.insertBefore(t,e.firstChild)}),i.forEach(t=>{if("event"===t.type){const n=`__smol_event_${t.index}__`,o=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);let s;for(;s=o.nextNode();){const e=s;for(const o of Array.from(e.attributes))if(o.value===n){const n=o.name.replace("@","");e.removeAttribute(o.name),"function"==typeof t.value&&e.addEventListener(n,t.value)}}}else if("boolean"===t.type){const n=`__smol_bool_${t.index}__`,o=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);let s;for(;s=o.nextNode();){const e=s;for(const o of Array.from(e.attributes))if(o.value===n){const n=o.name.replace("?","");e.removeAttribute(o.name),t.value&&e.setAttribute(n,"")}}}})}const i=new Map;function r(t){let e=t;const n=new Set;return{get value(){return e},set value(t){e!==t&&(e=t,n.forEach(t=>t(e)))},subscribe:t=>(n.add(t),()=>{n.delete(t)}),o:n}}function c(t,e={}){const o=new t;Object.entries(e).forEach(([t,e])=>{o.setAttribute(t,e)});const s=t.i||"unknown-element",i=t.u,r=(null==i?void 0:i.styles)||"";(null==i?void 0:i.connected)&&i.connected.call(o);let c="";if(null==i?void 0:i.template){const t={emit:o.emit.bind(o),render:()=>{},element:o},e=i.template.call(o,t);"string"==typeof e?c=e:e&&e.t&&(c=n(e))}if(globalThis.document&&c){const t=document.createElement("template");t.innerHTML=c,u(t.content),c=t.innerHTML}const l=`\n ${r?`<style>${r}</style>`:""}\n ${c}\n `.trim(),f=Object.entries(e).map(([t,e])=>`${t}="${function(t){return t.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}(e)}"`).join(" ");return`\n<${s}${f?" "+f:""}>\n <template shadowrootmode="open">\n ${l}\n </template>\n</${s}>\n `.trim()}function u(t){Array.from(t.childNodes).forEach(t=>{if(1===t.nodeType){const e=t,n=e.tagName.toLowerCase();if(globalThis.customElements&&n.includes("-")){const t=customElements.get(n);if(t){const n={};Array.from(e.attributes).forEach(t=>{n[t.name]=t.value});const o=c(t,n),s=document.createElement("div");s.innerHTML=o;const i=s.firstElementChild;if(i)return void e.replaceWith(i)}}u(e)}})}function l(t){t.shadowRoot&&(t.l=!0,function(t){if(!t.shadowRoot)return;const e=document.createTreeWalker(t.shadowRoot,NodeFilter.SHOW_ELEMENT),n=[];let o;for(;o=e.nextNode();){const t=o,e=new Map;for(const n of Array.from(t.attributes))if(n.name.startsWith("data-smol-event-")){const t=n.name.replace("data-smol-event-","");e.set(t,n.value)}e.size>0&&n.push({element:t,events:e})}n.forEach(({element:t,events:e})=>{e.forEach((t,e)=>{})})}(t))}t.clearServices=function(){i.clear()},t.computed=function(t){return r(t())},t.css=function(t,...e){let n="";for(let o=0;o<t.length;o++)n+=t[o],o<e.length&&(n+=String(e[o]));return n.trim()},t.effect=function(t){return t(),()=>{}},t.html=function(t,...e){return{strings:t,values:e,t:!0}},t.hydrateAll=function(){document.querySelectorAll("*").forEach(t=>{t.tagName.includes("-")&&t.shadowRoot&&l(t)})},t.hydrateComponent=l,t.inject=function(t){if(!i.has(t))throw new Error(`Service "${t}" not found. Did you forget to create it with smolService()?`);return i.get(t)},t.isTemplateResult=e,t.render=s,t.renderComponentToString=c,t.renderToString=n,t.smolComponent=function(t){const{tag:n,mode:o="open",observedAttributes:i=[],styles:r="",template:c,connected:u,disconnected:l,attributeChanged:f}=t;if(!n.includes("-"))throw new Error(`Custom element tag names must contain a hyphen: "${n}"`);class a extends HTMLElement{static get observedAttributes(){return i}constructor(){if(super(),this.shadowRoot||this.attachShadow({mode:o}),r&&this.shadowRoot){const t=document.createElement("style");t.textContent=r,this.shadowRoot.appendChild(t)}}connectedCallback(){u&&u.call(this),this.render()}disconnectedCallback(){l&&l.call(this)}attributeChangedCallback(t,e,n){this.isConnected&&this.render(),f&&f.call(this,t,e,n)}render(){if(!c||!this.shadowRoot)return;const t={emit:this.emit.bind(this),render:this.render.bind(this),element:this},n=c.call(this,t);n&&("string"==typeof n?this.shadowRoot.innerHTML=n:e(n)&&s(n,this.shadowRoot))}emit(t,e){this.dispatchEvent(new CustomEvent(t,{detail:e,bubbles:!0,composed:!0}))}}return a.u=t,a.i=n,customElements.get(n)||customElements.define(n,a),a},t.smolService=function(t){const{name:e,factory:n,singleton:o=!0}=t;if(o){if(i.has(e))return i.get(e);const t=n();return i.set(e,t),t}return n()},t.smolSignal=r,t.smolState=function(t){const e=new Set,n=()=>{e.forEach(t=>t())};return{data:new Proxy(t,{set:(t,e,o)=>(t[e]!==o&&(t[e]=o,n()),!0),deleteProperty:(t,e)=>(e in t&&(delete t[e],n()),!0)}),subscribe:t=>(e.add(t),()=>{e.delete(t)}),o:e}},t.ssr=function(t){return t.map(({component:t,attributes:e})=>c(t,e)).join("\n")},Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* Vite plugin for importing HTML templates as smol.js template functions
|
|
4
|
+
*
|
|
5
|
+
* This plugin allows you to write your component templates in separate .html files
|
|
6
|
+
* and import them into your components.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import template from './my-component.html?smol';
|
|
11
|
+
*
|
|
12
|
+
* smolComponent({
|
|
13
|
+
* tag: 'my-component',
|
|
14
|
+
* template(ctx) {
|
|
15
|
+
* return template(html);
|
|
16
|
+
* }
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function smolTemplatePlugin(): Plugin;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Vite plugin for importing HTML templates as smol.js template functions
|
|
4
|
+
*
|
|
5
|
+
* This plugin allows you to write your component templates in separate .html files
|
|
6
|
+
* and import them into your components.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import template from './my-component.html?smol';
|
|
11
|
+
*
|
|
12
|
+
* smolComponent({
|
|
13
|
+
* tag: 'my-component',
|
|
14
|
+
* template(ctx) {
|
|
15
|
+
* return template(html);
|
|
16
|
+
* }
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function smolTemplatePlugin() {
|
|
21
|
+
return {
|
|
22
|
+
name: 'vite-plugin-smol-templates',
|
|
23
|
+
enforce: 'pre',
|
|
24
|
+
async resolveId(id, importer) {
|
|
25
|
+
// Only handle .html files with ?smol query
|
|
26
|
+
if (id.includes('.html?smol')) {
|
|
27
|
+
const cleanId = id.replace('?smol', '');
|
|
28
|
+
const resolved = await this.resolve(cleanId, importer);
|
|
29
|
+
if (resolved) {
|
|
30
|
+
return resolved.id + '?smol';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
},
|
|
35
|
+
load(id) {
|
|
36
|
+
// Check if this is an HTML template with ?smol query
|
|
37
|
+
if (!id.includes('.html?smol')) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// Remove query parameter to get the actual file path
|
|
41
|
+
const filePath = id.replace(/\?smol$/, '');
|
|
42
|
+
try {
|
|
43
|
+
// Read the HTML file content
|
|
44
|
+
const htmlContent = readFileSync(filePath, 'utf-8');
|
|
45
|
+
// Transform the HTML into a JavaScript module
|
|
46
|
+
// The template will be a function that takes the html tagged template function
|
|
47
|
+
// and returns a TemplateResult
|
|
48
|
+
// We need to preserve ${} interpolations as actual template literal placeholders
|
|
49
|
+
// while escaping backticks in the HTML content
|
|
50
|
+
const escapedContent = htmlContent
|
|
51
|
+
.replace(/\\/g, '\\\\') // Escape backslashes
|
|
52
|
+
.replace(/`/g, '\\`'); // Escape backticks
|
|
53
|
+
// Create a function that uses 'with' to execute the template literal
|
|
54
|
+
// We use new Function to avoid strict mode limitations on 'with'
|
|
55
|
+
// and to allow dynamic variable resolution from the context
|
|
56
|
+
const templateBody = `with(context) { return html\`${escapedContent}\`; }`;
|
|
57
|
+
const code = `
|
|
58
|
+
export default function(html, context = {}) {
|
|
59
|
+
return new Function('html', 'context', ${JSON.stringify(templateBody)})(html, context);
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
return {
|
|
63
|
+
code,
|
|
64
|
+
map: null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.error(`Failed to load HTML template: ${filePath}\n${error}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { smolTemplatePlugin } from './vite-plugin-smol-templates.js';
|
package/dist/vite.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smol.js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal Web Component library with zero dependencies",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/smol.umd.js",
|
|
7
|
+
"module": "./dist/smol.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/smol.js",
|
|
12
|
+
"require": "./dist/smol.umd.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./vite": {
|
|
16
|
+
"import": "./dist/vite.js",
|
|
17
|
+
"types": "./dist/vite.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "vite build && npm run build:types && npm run build:vite",
|
|
26
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
27
|
+
"build:vite": "tsc src/vite.ts src/vite-plugin-smol-templates.ts --outDir dist --module ESNext --target ES2020 --moduleResolution bundler --declaration",
|
|
28
|
+
"test": "vitest",
|
|
29
|
+
"dev": "vite build --watch"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"web-components",
|
|
33
|
+
"custom-elements",
|
|
34
|
+
"shadow-dom",
|
|
35
|
+
"ssr",
|
|
36
|
+
"lightweight"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.0.0",
|
|
42
|
+
"terser": "^5.44.1",
|
|
43
|
+
"typescript": "^5.0.0",
|
|
44
|
+
"vite": "^5.0.0",
|
|
45
|
+
"vite-plugin-dts": "^3.0.0",
|
|
46
|
+
"vitest": "^1.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|