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 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
@@ -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 { }