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/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# smol.js
|
|
2
|
+
|
|
3
|
+
> Minimal Web Component library with zero dependencies
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Zero dependencies** - Only ~3KB gzipped
|
|
8
|
+
- ✅ **Standards-based** - Uses native Web Components
|
|
9
|
+
- ✅ **TypeScript-first** - Full type safety
|
|
10
|
+
- ✅ **Reactivity** - Fine-grained signals and state
|
|
11
|
+
- ✅ **SSR ready** - Server-side rendering support
|
|
12
|
+
- ✅ **Framework-agnostic** - Works with anything
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install smol.js
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Basic Component
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { smolComponent, html, smolSignal } from 'smol.js';
|
|
26
|
+
|
|
27
|
+
smolComponent({
|
|
28
|
+
tag: 'my-counter',
|
|
29
|
+
|
|
30
|
+
// Scoped CSS
|
|
31
|
+
styles: css`
|
|
32
|
+
button { padding: 0.5rem; }
|
|
33
|
+
`,
|
|
34
|
+
|
|
35
|
+
// Component Logic
|
|
36
|
+
connected() {
|
|
37
|
+
this.count = smolSignal(0);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Template with automated reactivity
|
|
41
|
+
template(ctx) {
|
|
42
|
+
const count = this.count.value;
|
|
43
|
+
|
|
44
|
+
return html`
|
|
45
|
+
<button @click=${() => this.count.value++}>
|
|
46
|
+
Count: ${count}
|
|
47
|
+
</button>
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Core Concepts
|
|
54
|
+
|
|
55
|
+
### Components (`smolComponent`)
|
|
56
|
+
|
|
57
|
+
Creates a standard native Web Component.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
smolComponent({
|
|
61
|
+
tag: 'user-card',
|
|
62
|
+
|
|
63
|
+
// Define attributes to watch for changes
|
|
64
|
+
observedAttributes: ['username'],
|
|
65
|
+
|
|
66
|
+
// Lifecycle methods
|
|
67
|
+
connected() { console.log('Mounted'); },
|
|
68
|
+
disconnected() { console.log('Unmounted'); },
|
|
69
|
+
|
|
70
|
+
// React to attribute changes
|
|
71
|
+
attributeChanged(name, oldVal, newVal) {
|
|
72
|
+
// ...
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Render template
|
|
76
|
+
template(ctx) {
|
|
77
|
+
return html`<div>Hello ${ctx.element.getAttribute('username')}</div>`;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Reactivity (`smolSignal`, `computed`, `effect`)
|
|
83
|
+
|
|
84
|
+
Fine-grained reactivity system.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Create a signal
|
|
88
|
+
const count = smolSignal(0);
|
|
89
|
+
|
|
90
|
+
// Create a computed value (updates automatically)
|
|
91
|
+
const double = computed(() => count.value * 2);
|
|
92
|
+
|
|
93
|
+
// Run a side effect when signals change
|
|
94
|
+
effect(() => {
|
|
95
|
+
console.log(`Count is ${count.value}, double is ${double.value}`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
count.value++; // Logs: "Count is 1, double is 2"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Services (`smolService`)
|
|
102
|
+
|
|
103
|
+
Singleton services for global state and logic.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Define service
|
|
107
|
+
export const authService = smolService({
|
|
108
|
+
name: 'auth',
|
|
109
|
+
factory: () => {
|
|
110
|
+
const user = smolSignal(null);
|
|
111
|
+
return {
|
|
112
|
+
user,
|
|
113
|
+
login: (name) => user.value = name
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Use in component
|
|
119
|
+
import { authService } from './auth.service';
|
|
120
|
+
|
|
121
|
+
smolComponent({
|
|
122
|
+
// ...
|
|
123
|
+
template(ctx) {
|
|
124
|
+
const user = authService.user.value;
|
|
125
|
+
return html`
|
|
126
|
+
<div>User: ${user}</div>
|
|
127
|
+
<button @click=${() => authService.login('Alice')}>Login</button>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Templates
|
|
134
|
+
|
|
135
|
+
### External Templates (`.html?smol`)
|
|
136
|
+
|
|
137
|
+
You can separate your HTML and CSS into files using the Vite plugin.
|
|
138
|
+
|
|
139
|
+
**vite.config.ts**:
|
|
140
|
+
```typescript
|
|
141
|
+
import { smolTemplatePlugin } from 'smol.js/vite';
|
|
142
|
+
export default {
|
|
143
|
+
plugins: [smolTemplatePlugin()]
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**my-cmp.ts**:
|
|
148
|
+
```typescript
|
|
149
|
+
import template from './my-cmp.html?smol';
|
|
150
|
+
import styles from './my-cmp.css?inline';
|
|
151
|
+
|
|
152
|
+
smolComponent({
|
|
153
|
+
tag: 'my-cmp',
|
|
154
|
+
styles,
|
|
155
|
+
template(ctx) {
|
|
156
|
+
// Variables used in HTML must be available in context or locals
|
|
157
|
+
return template(html, this);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**my-cmp.html**:
|
|
163
|
+
```html
|
|
164
|
+
<div>
|
|
165
|
+
<!-- 'count' refers to this.count from the component instance -->
|
|
166
|
+
Count: ${count.value}
|
|
167
|
+
</div>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Hydration
|
|
171
|
+
|
|
172
|
+
For Client-Side Hydration of SSR content:
|
|
173
|
+
|
|
174
|
+
**main.ts**:
|
|
175
|
+
```typescript
|
|
176
|
+
// Initializes hydration for all components
|
|
177
|
+
import 'smol.js/src/hydrate-client';
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Or manually:
|
|
181
|
+
```typescript
|
|
182
|
+
import { hydrateAll } from 'smol.js';
|
|
183
|
+
|
|
184
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
185
|
+
hydrateAll();
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### `html`
|
|
192
|
+
Tagged template literal for defining HTML structure.
|
|
193
|
+
|
|
194
|
+
### `css`
|
|
195
|
+
Tagged template literal for defining styles.
|
|
196
|
+
|
|
197
|
+
### `smolState(obj)`
|
|
198
|
+
Creates a deeply reactive object proxy.
|
|
199
|
+
|
|
200
|
+
### `inject(service)`
|
|
201
|
+
Retrieves a service instance (mostly used internally or for testing).
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clear all services (useful for testing)
|
|
3
|
+
*/
|
|
4
|
+
export declare function clearServices(): void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a computed signal that derives its value from other signals
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const count = smolSignal(0);
|
|
12
|
+
* const doubled = computed(() => count.value * 2);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function computed<T>(fn: () => T): Signal<T>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tagged template literal for CSS styles
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const styles = css`
|
|
23
|
+
* :host {
|
|
24
|
+
* display: block;
|
|
25
|
+
* }
|
|
26
|
+
* `;
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function css(strings: TemplateStringsArray, ...values: any[]): string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create an effect that runs when signals change
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const count = smolSignal(0);
|
|
37
|
+
*
|
|
38
|
+
* effect(() => {
|
|
39
|
+
* console.log('Count is:', count.value);
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function effect(fn: () => void): () => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Tagged template literal for HTML templates
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const template = html`<div>${value}</div>`;
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function html(strings: TemplateStringsArray, ...values: any[]): TemplateResult;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hydrate all smol components on the page
|
|
57
|
+
*
|
|
58
|
+
* Call this after the page loads to hydrate all server-rendered components.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hydrateAll(): void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Hydrate a server-rendered component
|
|
64
|
+
*
|
|
65
|
+
* This attaches event listeners and sets up reactivity
|
|
66
|
+
* without re-rendering the component.
|
|
67
|
+
*/
|
|
68
|
+
export declare function hydrateComponent(element: SmolElement): void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Inject a service by name
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const api = inject<ApiService>('ApiService');
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function inject<T = any>(name: string): T;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a value is a TemplateResult
|
|
82
|
+
*/
|
|
83
|
+
export declare function isTemplateResult(value: any): value is TemplateResult;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a template to a DOM node
|
|
87
|
+
* This handles event listeners, attributes, and text content
|
|
88
|
+
* IMPORTANT: Preserves existing <style> elements in shadow DOM
|
|
89
|
+
*/
|
|
90
|
+
export declare function render(template: TemplateResult, container: HTMLElement | ShadowRoot): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render a component to an HTML string with Declarative Shadow DOM
|
|
94
|
+
* This is used for server-side rendering (SSR)
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const html = renderComponentToString(MyCounter, { initialCount: 0 });
|
|
99
|
+
* // Returns: <my-counter><template shadowrootmode="open">...</template></my-counter>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function renderComponentToString(ComponentClass: typeof HTMLElement, attributes?: Record<string, string>): string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render a template result to an HTML string
|
|
106
|
+
* Used for SSR or initial rendering
|
|
107
|
+
*/
|
|
108
|
+
export declare function renderToString(template: TemplateResult): string;
|
|
109
|
+
|
|
110
|
+
export declare interface Signal<T> {
|
|
111
|
+
/** Get the current value */
|
|
112
|
+
get value(): T;
|
|
113
|
+
/** Set a new value */
|
|
114
|
+
set value(newValue: T);
|
|
115
|
+
/** Subscribe to value changes */
|
|
116
|
+
subscribe(fn: (value: T) => void): () => void;
|
|
117
|
+
/** Internal subscribers */
|
|
118
|
+
_subscribers: Set<(value: T) => void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a custom Web Component
|
|
123
|
+
*
|
|
124
|
+
* This function creates a custom element class and automatically registers it.
|
|
125
|
+
* It handles Shadow DOM, lifecycle callbacks, and template rendering.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* // my-button.html
|
|
130
|
+
* // <button><slot>Button</slot></button>
|
|
131
|
+
*
|
|
132
|
+
* // my-button.css
|
|
133
|
+
* // button { padding: 0.5rem 1rem; }
|
|
134
|
+
*
|
|
135
|
+
* import { smolComponent, html } from 'smol.js';
|
|
136
|
+
* import styles from './my-button.css?inline';
|
|
137
|
+
* import template from './my-button.html?smol';
|
|
138
|
+
*
|
|
139
|
+
* smolComponent({
|
|
140
|
+
* tag: 'my-button',
|
|
141
|
+
* observedAttributes: ['variant'],
|
|
142
|
+
*
|
|
143
|
+
* styles,
|
|
144
|
+
*
|
|
145
|
+
* template(ctx) {
|
|
146
|
+
* return template(html);
|
|
147
|
+
* }
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export declare function smolComponent(config: SmolComponentConfig): typeof HTMLElement;
|
|
152
|
+
|
|
153
|
+
export declare interface SmolComponentConfig {
|
|
154
|
+
/** The custom element tag name (must contain a hyphen) */
|
|
155
|
+
tag: string;
|
|
156
|
+
/** Shadow DOM mode */
|
|
157
|
+
mode?: 'open' | 'closed';
|
|
158
|
+
/** List of attributes to observe for changes */
|
|
159
|
+
observedAttributes?: string[];
|
|
160
|
+
/** Component styles (use css`` tagged template) */
|
|
161
|
+
styles?: string;
|
|
162
|
+
/** Template function that returns HTML */
|
|
163
|
+
template?: (this: SmolElement, ctx: SmolContext) => string | TemplateResult;
|
|
164
|
+
/** Lifecycle: element connected to DOM */
|
|
165
|
+
connected?: (this: SmolElement) => void;
|
|
166
|
+
/** Lifecycle: element disconnected from DOM */
|
|
167
|
+
disconnected?: (this: SmolElement) => void;
|
|
168
|
+
/** Lifecycle: observed attribute changed */
|
|
169
|
+
attributeChanged?: (this: SmolElement, name: string, oldValue: string | null, newValue: string | null) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export declare interface SmolContext {
|
|
173
|
+
/** Emit a custom event */
|
|
174
|
+
emit: (name: string, detail?: any) => void;
|
|
175
|
+
/** Trigger a re-render */
|
|
176
|
+
render: () => void;
|
|
177
|
+
/** Access to element properties */
|
|
178
|
+
[key: string]: any;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export declare interface SmolElement extends HTMLElement {
|
|
182
|
+
/** Trigger a re-render of the component */
|
|
183
|
+
render(): void;
|
|
184
|
+
/** Emit a custom event from the component */
|
|
185
|
+
emit(name: string, detail?: any): void;
|
|
186
|
+
/** Access to the shadow root */
|
|
187
|
+
readonly shadowRoot: ShadowRoot;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create a service (singleton by default)
|
|
192
|
+
*
|
|
193
|
+
* Services provide shared functionality across components.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* const ApiService = smolService({
|
|
198
|
+
* name: 'ApiService',
|
|
199
|
+
* factory: () => ({
|
|
200
|
+
* async fetchData(url: string) {
|
|
201
|
+
* const res = await fetch(url);
|
|
202
|
+
* return res.json();
|
|
203
|
+
* }
|
|
204
|
+
* })
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* // Use in component:
|
|
208
|
+
* const api = inject('ApiService');
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export declare function smolService<T>(config: SmolServiceConfig<T>): T;
|
|
212
|
+
|
|
213
|
+
export declare interface SmolServiceConfig<T> {
|
|
214
|
+
/** Unique service name */
|
|
215
|
+
name: string;
|
|
216
|
+
/** Factory function to create the service */
|
|
217
|
+
factory: () => T;
|
|
218
|
+
/** Whether this is a singleton (default: true) */
|
|
219
|
+
singleton?: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a reactive signal
|
|
224
|
+
*
|
|
225
|
+
* Signals are lightweight reactive primitives that notify subscribers when their value changes.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* const count = smolSignal(0);
|
|
230
|
+
*
|
|
231
|
+
* // Subscribe to changes
|
|
232
|
+
* count.subscribe((value) => console.log('Count:', value));
|
|
233
|
+
*
|
|
234
|
+
* // Update value
|
|
235
|
+
* count.value = 1; // logs "Count: 1"
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export declare function smolSignal<T>(initialValue: T): Signal<T>;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create a reactive state object using Proxy
|
|
242
|
+
*
|
|
243
|
+
* Unlike signals, state objects are reactive objects that track changes to any property.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* const state = smolState({
|
|
248
|
+
* count: 0,
|
|
249
|
+
* name: 'John'
|
|
250
|
+
* });
|
|
251
|
+
*
|
|
252
|
+
* state.subscribe(() => {
|
|
253
|
+
* console.log('State changed:', state.data);
|
|
254
|
+
* });
|
|
255
|
+
*
|
|
256
|
+
* state.data.count++; // triggers subscribers
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export declare function smolState<T extends object>(initialValue: T): State<T>;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Server-side render utility
|
|
263
|
+
* Renders multiple components to HTML
|
|
264
|
+
*/
|
|
265
|
+
export declare function ssr(components: Array<{
|
|
266
|
+
component: typeof HTMLElement;
|
|
267
|
+
attributes?: Record<string, string>;
|
|
268
|
+
}>): string;
|
|
269
|
+
|
|
270
|
+
export declare interface State<T extends object> {
|
|
271
|
+
/** The proxied state object */
|
|
272
|
+
data: T;
|
|
273
|
+
/** Subscribe to any state change */
|
|
274
|
+
subscribe(fn: () => void): () => void;
|
|
275
|
+
/** Internal subscribers */
|
|
276
|
+
_subscribers: Set<() => void>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export declare interface TemplateResult {
|
|
280
|
+
strings: TemplateStringsArray;
|
|
281
|
+
values: any[];
|
|
282
|
+
_isTemplateResult: true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { }
|