project-graph-mcp 1.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/AGENT_ROLE.md +126 -0
- package/AGENT_ROLE_MINIMAL.md +54 -0
- package/CONFIGURATION.md +188 -0
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/package.json +46 -0
- package/references/symbiote-3x.md +834 -0
- package/rules/express-5.json +76 -0
- package/rules/fastify-5.json +75 -0
- package/rules/nestjs-10.json +88 -0
- package/rules/nextjs-15.json +87 -0
- package/rules/node-22.json +156 -0
- package/rules/react-18.json +87 -0
- package/rules/react-19.json +76 -0
- package/rules/symbiote-2x.json +158 -0
- package/rules/symbiote-3x.json +221 -0
- package/rules/typescript-5.json +69 -0
- package/rules/vue-3.json +79 -0
- package/src/cli-handlers.js +140 -0
- package/src/cli.js +83 -0
- package/src/complexity.js +223 -0
- package/src/custom-rules.js +583 -0
- package/src/dead-code.js +468 -0
- package/src/filters.js +226 -0
- package/src/framework-references.js +177 -0
- package/src/full-analysis.js +159 -0
- package/src/graph-builder.js +269 -0
- package/src/instructions.js +175 -0
- package/src/jsdoc-generator.js +214 -0
- package/src/large-files.js +162 -0
- package/src/mcp-server.js +375 -0
- package/src/outdated-patterns.js +295 -0
- package/src/parser.js +293 -0
- package/src/server.js +28 -0
- package/src/similar-functions.js +278 -0
- package/src/test-annotations.js +301 -0
- package/src/tool-defs.js +444 -0
- package/src/tools.js +240 -0
- package/src/undocumented.js +260 -0
- package/src/workspace.js +70 -0
- package/vendor/acorn.mjs +6145 -0
- package/vendor/walk.mjs +437 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
# Symbiote.js — AI Context Reference (v3.x)
|
|
2
|
+
|
|
3
|
+
> **Purpose**: Authoritative reference for AI code assistants. All information is derived from source code analysis of [symbiote.js](https://github.com/symbiotejs/symbiote.js).
|
|
4
|
+
> Current version: **3.2.0**. Zero dependencies. ~6 KB gzip.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Installation & Import
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
// NPM
|
|
12
|
+
import Symbiote, { html, css, PubSub, DICT } from '@symbiotejs/symbiote';
|
|
13
|
+
|
|
14
|
+
// CDN / HTTPS
|
|
15
|
+
import Symbiote, { html, css } from 'https://esm.run/@symbiotejs/symbiote';
|
|
16
|
+
|
|
17
|
+
// Individual module imports (tree-shaking)
|
|
18
|
+
import Symbiote from '@symbiotejs/symbiote/core/Symbiote.js';
|
|
19
|
+
import { PubSub } from '@symbiotejs/symbiote/core/PubSub.js';
|
|
20
|
+
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
|
|
21
|
+
import { html } from '@symbiotejs/symbiote/core/html.js';
|
|
22
|
+
import { css } from '@symbiotejs/symbiote/core/css.js';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Full export list (index.js)
|
|
26
|
+
`Symbiote` (default), `html`, `css`, `PubSub`, `DICT`
|
|
27
|
+
|
|
28
|
+
> **v3.2 change**: `UID`, `setNestedProp`, `applyStyles`, `applyAttributes`, `create`, `kebabToCamel`, `reassignDictionary` moved to `@symbiotejs/symbiote/utils`.
|
|
29
|
+
> `AppRouter` moved to `@symbiotejs/symbiote/core/AppRouter.js` since v3.0.
|
|
30
|
+
|
|
31
|
+
### Utils entry point (`@symbiotejs/symbiote/utils`)
|
|
32
|
+
```js
|
|
33
|
+
import { UID, create, applyStyles, applyAttributes, setNestedProp, kebabToCamel, reassignDictionary } from '@symbiotejs/symbiote/utils';
|
|
34
|
+
```
|
|
35
|
+
Individual deep imports also work: `@symbiotejs/symbiote/utils/UID.js`, etc.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Component Basics
|
|
40
|
+
|
|
41
|
+
Symbiote extends `HTMLElement`. Every component is a Custom Element.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
class MyComponent extends Symbiote {
|
|
45
|
+
// Initial reactive state (key-value pairs)
|
|
46
|
+
init$ = {
|
|
47
|
+
name: 'World',
|
|
48
|
+
count: 0,
|
|
49
|
+
onBtnClick: () => {
|
|
50
|
+
this.$.count++;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Called once after init$ is processed but BEFORE template is rendered
|
|
55
|
+
initCallback() {}
|
|
56
|
+
|
|
57
|
+
// Called once AFTER template is rendered and attached to DOM
|
|
58
|
+
renderCallback() {
|
|
59
|
+
// Safe to access this.ref, this.$, DOM children here
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Called when element is disconnected and readyToDestroy is true
|
|
63
|
+
destroyCallback() {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Template — assigned via static property SETTER, outside the class body
|
|
67
|
+
MyComponent.template = html`
|
|
68
|
+
<h1>Hello {{name}}!</h1>
|
|
69
|
+
<p>Count: {{count}}</p>
|
|
70
|
+
<button ${{onclick: 'onBtnClick'}}>Increment</button>
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
// Register Custom Element tag
|
|
74
|
+
MyComponent.reg('my-component');
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> **CRITICAL**: `template` is a **static property setter** on the `Symbiote` class, not a regular static class field.
|
|
78
|
+
> You **MUST** assign it **outside** the class body: `MyComponent.template = html\`...\``.
|
|
79
|
+
> Using `static template = html\`...\`` inside the class declaration **will NOT work**.
|
|
80
|
+
> Templates are plain HTML strings, NOT JSX. Use the `html` tagged template literal.
|
|
81
|
+
|
|
82
|
+
### Usage in HTML
|
|
83
|
+
```html
|
|
84
|
+
<my-component></my-component>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Template Binding Syntax
|
|
90
|
+
|
|
91
|
+
Use the `html` tagged template literal for ergonomic binding syntax. It supports **two interpolation modes**:
|
|
92
|
+
|
|
93
|
+
- **`Object`** → converted to `bind="prop:key;"` attribute (reactive binding)
|
|
94
|
+
- **`string` / `number`** → concatenated as-is (native interpolation, useful for SSR page shells)
|
|
95
|
+
|
|
96
|
+
This dual-mode design means `html` works for both component templates and full-page SSR output — no separate "server-only template" function is needed.
|
|
97
|
+
|
|
98
|
+
### Text node binding
|
|
99
|
+
```html
|
|
100
|
+
<div>{{propName}}</div>
|
|
101
|
+
```
|
|
102
|
+
Binds `propName` from component state to the text content of a text node. Works inside any element. Multiple bindings in one text node are supported: `{{first}} - {{second}}`.
|
|
103
|
+
|
|
104
|
+
### Property binding (element's own properties)
|
|
105
|
+
```html
|
|
106
|
+
<button ${{onclick: 'handlerName'}}>Click</button>
|
|
107
|
+
<div ${{textContent: 'myProp'}}></div>
|
|
108
|
+
```
|
|
109
|
+
The `${{key: 'value'}}` interpolation creates a `bind="key:value;"` attribute. Keys are DOM element property names. Values are component state property names (strings).
|
|
110
|
+
|
|
111
|
+
**Class property fallback (v3.1+):** When a binding key is not found in `init$`, Symbiote falls back to own class properties (checked via `Object.hasOwn`). This works for ALL properties, not just `on*` handlers. Functions are auto-bound to the component instance. Inherited `HTMLElement` properties are never picked up.
|
|
112
|
+
```js
|
|
113
|
+
class MyComp extends Symbiote {
|
|
114
|
+
// Approach 1: state property (arrow function)
|
|
115
|
+
init$ = { onClick: () => console.log('clicked') };
|
|
116
|
+
|
|
117
|
+
// Approach 2: class property (fallback for any binding)
|
|
118
|
+
label = 'Click me';
|
|
119
|
+
onSubmit() { console.log('submitted'); }
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Nested property binding
|
|
124
|
+
```html
|
|
125
|
+
<div ${{'style.color': 'colorProp'}}>Text</div>
|
|
126
|
+
```
|
|
127
|
+
Dot notation navigates nested properties on the element.
|
|
128
|
+
|
|
129
|
+
### Direct child component state binding
|
|
130
|
+
```html
|
|
131
|
+
<child-component ${{'$.innerProp': 'parentProp'}}></child-component>
|
|
132
|
+
```
|
|
133
|
+
The `$.` prefix accesses the child component's `$` state proxy directly.
|
|
134
|
+
|
|
135
|
+
### Attribute binding (`@` prefix)
|
|
136
|
+
```html
|
|
137
|
+
<div ${{'@hidden': 'isHidden'}}>Content</div>
|
|
138
|
+
<input ${{'@disabled': 'isDisabled'}}>
|
|
139
|
+
<div ${{'@data-value': 'myValue'}}></div>
|
|
140
|
+
```
|
|
141
|
+
The `@` prefix means "bind to HTML attribute" (not DOM property). For boolean attributes: `true` → attribute present, `false` → attribute removed. `@` is for binding syntax only, do NOT use it as a regular HTML attribute prefix.
|
|
142
|
+
|
|
143
|
+
### Type casting (`!` / `!!`)
|
|
144
|
+
```html
|
|
145
|
+
<div ${{'@hidden': '!showContent'}}>...</div> <!-- inverted boolean -->
|
|
146
|
+
<div ${{'@contenteditable': '!!hasText'}}>...</div> <!-- double inversion = cast to boolean -->
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Loose-coupling alternative (plain HTML, no JS context needed)
|
|
150
|
+
```html
|
|
151
|
+
<div bind="textContent: myProp"></div>
|
|
152
|
+
<div bind="onclick: handler; @hidden: !flag"></div>
|
|
153
|
+
```
|
|
154
|
+
This is the raw form. The `html` helper generates it automatically.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Property Token Prefixes
|
|
159
|
+
|
|
160
|
+
Prefixes control which data context a binding resolves to:
|
|
161
|
+
|
|
162
|
+
| Prefix | Meaning | Example | Description |
|
|
163
|
+
|--------|---------|---------|-------------|
|
|
164
|
+
| _(none)_ | Local state | `{{count}}` | Current component's local context |
|
|
165
|
+
| `^` | Parent inherited | `{{^parentProp}}` | Walk up DOM ancestry to find nearest component that has this prop |
|
|
166
|
+
| `*` | Shared context | `{{*sharedProp}}` | Shared context scoped by `ctx` attribute or CSS `--ctx` |
|
|
167
|
+
| `/` | Named context | `{{APP/myProp}}` | Global named context identified by key before `/` |
|
|
168
|
+
| `--` | CSS Data | `${{textContent: '--my-css-var'}}` | Read value from CSS custom property |
|
|
169
|
+
| `+` | Computed | (in init$) `'+sum': () => ...` | Function recalculated when local dependencies change (auto-tracked) |
|
|
170
|
+
|
|
171
|
+
### Examples in init$
|
|
172
|
+
```js
|
|
173
|
+
init$ = {
|
|
174
|
+
localProp: 'hello', // local
|
|
175
|
+
'*sharedProp': 'shared value', // shared context
|
|
176
|
+
'APP/globalProp': 42, // named context "APP"
|
|
177
|
+
'+computed': () => this.$.a + this.$.b, // local computed (auto-tracked)
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Computed properties (v3.x)
|
|
182
|
+
|
|
183
|
+
Computed props use the `+` prefix and are auto-tracked: dependencies are recorded when the function executes.
|
|
184
|
+
|
|
185
|
+
**Local computed** — reacts to local state changes automatically:
|
|
186
|
+
```js
|
|
187
|
+
init$ = {
|
|
188
|
+
a: 1,
|
|
189
|
+
b: 2,
|
|
190
|
+
'+sum': () => this.$.a + this.$.b, // auto-tracks 'a' and 'b'
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Cross-context computed** — reacts to external named context changes via explicit deps:
|
|
195
|
+
```js
|
|
196
|
+
init$ = {
|
|
197
|
+
local: 0,
|
|
198
|
+
'+total': {
|
|
199
|
+
deps: ['GAME/score'],
|
|
200
|
+
fn: () => this.$['GAME/score'] + this.$.local,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> **NOTE**: Computed values are recalculated asynchronously (via `queueMicrotask`), so subscribers are notified in the next microtask, not inline during `pub()`.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## State Management API
|
|
211
|
+
|
|
212
|
+
### `$` proxy (read/write state)
|
|
213
|
+
```js
|
|
214
|
+
this.$.count = 10; // publish
|
|
215
|
+
let val = this.$.count; // read
|
|
216
|
+
this.$['APP/prop'] = 'x'; // named context
|
|
217
|
+
this.$['^parentProp'] = 5; // parent context
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `set$(kvObj, forcePrimitives?)` — bulk update
|
|
221
|
+
```js
|
|
222
|
+
this.set$({ name: 'Jane', count: 5 });
|
|
223
|
+
// forcePrimitives=true → triggers callbacks even if value unchanged (for primitives)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### `sub(prop, handler, init?)` — subscribe to changes
|
|
227
|
+
```js
|
|
228
|
+
this.sub('count', (val) => {
|
|
229
|
+
console.log('count changed:', val);
|
|
230
|
+
});
|
|
231
|
+
// init defaults to true (handler called immediately with current value)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `add(prop, val, rewrite?)` — add property to context
|
|
235
|
+
### `add$(obj, rewrite?)` — bulk add
|
|
236
|
+
|
|
237
|
+
### `has(prop)` — check if property exists in context
|
|
238
|
+
### `notify(prop)` — force notification to all subscribers
|
|
239
|
+
|
|
240
|
+
> **WARNING**: Property keys with nested dots (`prop.sub`) are NOT supported as state keys.
|
|
241
|
+
> Use flat names: `propSub` instead of `prop.sub`.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## PubSub (Standalone State Management)
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
import { PubSub } from '@symbiotejs/symbiote';
|
|
249
|
+
|
|
250
|
+
// Register a named global context
|
|
251
|
+
const ctx = PubSub.registerCtx({
|
|
252
|
+
userName: 'Anonymous',
|
|
253
|
+
score: 0,
|
|
254
|
+
}, 'GAME'); // 'GAME' is the context key
|
|
255
|
+
|
|
256
|
+
// Read/write from any component
|
|
257
|
+
this.$['GAME/userName'] = 'Player 1';
|
|
258
|
+
console.log(this.$['GAME/score']);
|
|
259
|
+
|
|
260
|
+
// Subscribe from any component
|
|
261
|
+
this.sub('GAME/score', (val) => {
|
|
262
|
+
console.log('Score:', val);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Direct PubSub API
|
|
266
|
+
ctx.pub('score', 100);
|
|
267
|
+
ctx.read('score');
|
|
268
|
+
ctx.sub('score', callback);
|
|
269
|
+
ctx.multiPub({ score: 100, userName: 'Hero' });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### PubSub static methods
|
|
273
|
+
- `PubSub.registerCtx(schema, uid?)` → `PubSub` instance
|
|
274
|
+
- `PubSub.getCtx(uid, notify?)` → `PubSub` instance or null
|
|
275
|
+
- `PubSub.deleteCtx(uid)`
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Shared Context (`*` prefix)
|
|
280
|
+
|
|
281
|
+
Components grouped by the `ctx` HTML attribute (or `--ctx` CSS custom property) share a data context. Properties with `*` prefix are read/written in this shared context — inspired by native HTML `name` attribute grouping (like radio button groups):
|
|
282
|
+
|
|
283
|
+
```html
|
|
284
|
+
<upload-btn ctx="gallery"></upload-btn>
|
|
285
|
+
<file-list ctx="gallery"></file-list>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
class UploadBtn extends Symbiote {
|
|
290
|
+
init$ = {
|
|
291
|
+
'*files': [],
|
|
292
|
+
onUpload: () => {
|
|
293
|
+
this.$['*files'] = [...this.$['*files'], newFile];
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class FileList extends Symbiote {
|
|
299
|
+
init$ = {
|
|
300
|
+
'*files': [], // same shared prop — first-registered value wins
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Both components access the same `*files` state — no parent component, no prop drilling, no global store. Just set `ctx="gallery"` in HTML and use `*`-prefixed properties.
|
|
306
|
+
|
|
307
|
+
### Context name resolution (first match wins)
|
|
308
|
+
1. `ctx="name"` HTML attribute
|
|
309
|
+
2. `--ctx` CSS custom property (inherited from ancestors)
|
|
310
|
+
3. No match → `*` props are **silently skipped** (dev mode warns)
|
|
311
|
+
|
|
312
|
+
> **WARNING**: `*` properties require an explicit `ctx` attribute or `--ctx` CSS variable. Without one, the shared context is not created and `*` props have no effect.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Lifecycle & Instance Properties
|
|
317
|
+
|
|
318
|
+
### Lifecycle callbacks (override in subclass)
|
|
319
|
+
| Method | When called |
|
|
320
|
+
|--------|------------|
|
|
321
|
+
| `initCallback()` | Once, after state initialized, before render (if `pauseRender=true`) or normally after render |
|
|
322
|
+
| `renderCallback()` | Once, after template is rendered and attached to DOM |
|
|
323
|
+
| `destroyCallback()` | On disconnect, after 100ms delay, only if `readyToDestroy=true` |
|
|
324
|
+
|
|
325
|
+
### Constructor flags (set in constructor or as class fields)
|
|
326
|
+
| Property | Default | Description |
|
|
327
|
+
|----------|---------|-------------|
|
|
328
|
+
| `pauseRender` | `false` | Skip automatic rendering; call `this.render()` manually later |
|
|
329
|
+
| `renderShadow` | `false` | Render template into Shadow DOM |
|
|
330
|
+
| `readyToDestroy` | `true` | Allow cleanup on disconnect |
|
|
331
|
+
| `processInnerHtml` | `false` | Process existing inner HTML with template processors |
|
|
332
|
+
| `ssrMode` | `false` | **Client-only.** Hydrate server-rendered HTML: skips template injection, attaches bindings to existing DOM. Supports both light DOM and Declarative Shadow DOM. Ignored when `__SYMBIOTE_SSR` is active (server side) |
|
|
333
|
+
| `allowCustomTemplate` | `false` | Allow `use-template="#selector"` attribute |
|
|
334
|
+
| `isVirtual` | `false` | Replace element with its template fragment |
|
|
335
|
+
| `allowTemplateInits` | `true` | Auto-add props found in template but not in init$ |
|
|
336
|
+
|
|
337
|
+
### Instance properties (available after render)
|
|
338
|
+
- `this.ref` — object map of `ref`-attributed elements
|
|
339
|
+
- `this.initChildren` — array of original child nodes (before template render)
|
|
340
|
+
- `this.$` — state proxy
|
|
341
|
+
- `this.allSubs` — Set of all subscriptions (for cleanup)
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Exit Animation (`animateOut`)
|
|
345
|
+
|
|
346
|
+
`animateOut(el)` sets `[leaving]` attribute, waits for CSS `transitionend`, then removes the element. If no CSS transition is defined, removes immediately. Available as standalone import or `Symbiote.animateOut`.
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
import { animateOut } from '@symbiotejs/symbiote';
|
|
350
|
+
// or: Symbiote.animateOut(el)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### CSS pattern
|
|
354
|
+
|
|
355
|
+
```css
|
|
356
|
+
my-item {
|
|
357
|
+
opacity: 1;
|
|
358
|
+
transform: translateY(0);
|
|
359
|
+
transition: opacity 0.3s, transform 0.3s;
|
|
360
|
+
|
|
361
|
+
/* Enter (CSS-native, no JS needed): */
|
|
362
|
+
@starting-style {
|
|
363
|
+
opacity: 0;
|
|
364
|
+
transform: translateY(20px);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Exit (triggered by animateOut): */
|
|
368
|
+
&[leaving] {
|
|
369
|
+
opacity: 0;
|
|
370
|
+
transform: translateY(-10px);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Itemize integration
|
|
376
|
+
|
|
377
|
+
Both itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Styling
|
|
382
|
+
|
|
383
|
+
### rootStyles (Light DOM, adopted stylesheets)
|
|
384
|
+
```js
|
|
385
|
+
MyComponent.rootStyles = css`
|
|
386
|
+
my-component {
|
|
387
|
+
display: block;
|
|
388
|
+
color: var(--text-color);
|
|
389
|
+
}
|
|
390
|
+
`;
|
|
391
|
+
```
|
|
392
|
+
Styles are added to the closest document root via `adoptedStyleSheets`. Use the custom tag name as selector.
|
|
393
|
+
|
|
394
|
+
### shadowStyles (Shadow DOM, auto-creates shadow root)
|
|
395
|
+
```js
|
|
396
|
+
MyComponent.shadowStyles = css`
|
|
397
|
+
:host {
|
|
398
|
+
display: block;
|
|
399
|
+
}
|
|
400
|
+
button {
|
|
401
|
+
color: red;
|
|
402
|
+
}
|
|
403
|
+
`;
|
|
404
|
+
```
|
|
405
|
+
Setting `shadowStyles` automatically creates a Shadow Root and uses `adoptedStyleSheets`.
|
|
406
|
+
|
|
407
|
+
### addRootStyles / addShadowStyles (append additional sheets)
|
|
408
|
+
```js
|
|
409
|
+
MyComponent.addRootStyles(anotherSheet);
|
|
410
|
+
MyComponent.addShadowStyles(anotherSheet);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### `css` tag function
|
|
414
|
+
Returns a `CSSStyleSheet` instance (constructable stylesheet). Supports processors:
|
|
415
|
+
```js
|
|
416
|
+
css.useProcessor((txt) => txt.replaceAll('$accent', '#ff0'));
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Element References
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
MyComponent.template = html`
|
|
425
|
+
<input ${{ref: 'nameInput'}}>
|
|
426
|
+
<button ${{ref: 'submitBtn', onclick: 'onSubmit'}}>Submit</button>
|
|
427
|
+
`;
|
|
428
|
+
|
|
429
|
+
// In renderCallback:
|
|
430
|
+
this.ref.nameInput.focus();
|
|
431
|
+
this.ref.submitBtn.disabled = true;
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Alternative HTML syntax: `<div ref="myRef"></div>`
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Itemize API (Dynamic Lists)
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
class MyList extends Symbiote {
|
|
442
|
+
init$ = {
|
|
443
|
+
items: [
|
|
444
|
+
{ name: 'Alice', role: 'Admin' },
|
|
445
|
+
{ name: 'Bob', role: 'User' },
|
|
446
|
+
],
|
|
447
|
+
onItemClick: (e) => {
|
|
448
|
+
console.log('clicked');
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
MyList.template = html`
|
|
454
|
+
<ul ${{itemize: 'items'}}>
|
|
455
|
+
<template>
|
|
456
|
+
<li>
|
|
457
|
+
<span>{{name}}</span> - <span>{{role}}</span>
|
|
458
|
+
<button ${{onclick: '^onItemClick'}}>Click</button>
|
|
459
|
+
</li>
|
|
460
|
+
</template>
|
|
461
|
+
</ul>
|
|
462
|
+
`;
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
> **CRITICAL**: Inside itemize templates, items are full Symbiote components with their own state scope.
|
|
466
|
+
> - `{{name}}` — item's own property
|
|
467
|
+
> - `${{onclick: 'handler'}}` — binds to the item component's own method/property
|
|
468
|
+
> - `${{onclick: '^handler'}}` — use `^` prefix to reach the **parent** component's property
|
|
469
|
+
> - Failure to use `^` for parent handlers will result in broken event bindings
|
|
470
|
+
|
|
471
|
+
### Custom item component
|
|
472
|
+
```html
|
|
473
|
+
<div ${{itemize: 'items', 'item-tag': 'my-item'}}></div>
|
|
474
|
+
```
|
|
475
|
+
Then define `my-item` as a separate Symbiote component.
|
|
476
|
+
|
|
477
|
+
### Data formats
|
|
478
|
+
- **Array**: `[{prop: val}, ...]` — items rendered in order
|
|
479
|
+
- **Object**: `{key1: {prop: val}, ...}` — items get `_KEY_` property added
|
|
480
|
+
|
|
481
|
+
### Updating lists
|
|
482
|
+
Assign new array to trigger re-render:
|
|
483
|
+
```js
|
|
484
|
+
this.$.items = [...newItems]; // triggers update
|
|
485
|
+
```
|
|
486
|
+
Existing items are updated in-place via `set$`, new items appended, excess removed.
|
|
487
|
+
|
|
488
|
+
### Itemize class property fallback (v3.2)
|
|
489
|
+
The `itemize` data source property supports class property fallback, consistent with other template processors:
|
|
490
|
+
```js
|
|
491
|
+
class MyList extends Symbiote {
|
|
492
|
+
items = [{ name: 'Alice' }, { name: 'Bob' }]; // no init$ needed
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Keyed itemize processor (optional, v3.0+)
|
|
497
|
+
Drop-in replacement with reference-equality fast paths and key-based reconciliation. Up to **3×** faster for appends, **32×** for no-ops:
|
|
498
|
+
```js
|
|
499
|
+
import { itemizeProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor-keyed.js';
|
|
500
|
+
import { itemizeProcessor as defaultProcessor } from '@symbiotejs/symbiote/core/itemizeProcessor.js';
|
|
501
|
+
|
|
502
|
+
class BigList extends Symbiote {
|
|
503
|
+
constructor() {
|
|
504
|
+
super();
|
|
505
|
+
this.templateProcessors.delete(defaultProcessor);
|
|
506
|
+
this.templateProcessors = new Set([itemizeProcessor, ...this.templateProcessors]);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Slots (Light DOM)
|
|
514
|
+
|
|
515
|
+
Slots work without Shadow DOM (processed by `slotProcessor`). Import and add manually since v2.x:
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
import { slotProcessor } from '@symbiotejs/symbiote/core/slotProcessor.js';
|
|
519
|
+
|
|
520
|
+
class MyWrapper extends Symbiote {
|
|
521
|
+
constructor() {
|
|
522
|
+
super();
|
|
523
|
+
this.templateProcessors.add(slotProcessor);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
MyWrapper.template = html`
|
|
528
|
+
<header><slot name="header"></slot></header>
|
|
529
|
+
<main><slot></slot></main>
|
|
530
|
+
`;
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Usage:
|
|
534
|
+
```html
|
|
535
|
+
<my-wrapper>
|
|
536
|
+
<h1 slot="header">Title</h1>
|
|
537
|
+
<p>Default slot content</p>
|
|
538
|
+
</my-wrapper>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## Server-Side Rendering (SSR)
|
|
544
|
+
|
|
545
|
+
Import `node/SSR.js` to render components to HTML strings on the server. Requires `linkedom` (optional peer dependency).
|
|
546
|
+
|
|
547
|
+
### Basic usage — `processHtml`
|
|
548
|
+
|
|
549
|
+
```js
|
|
550
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
551
|
+
|
|
552
|
+
await SSR.init(); // patches globals with linkedom env
|
|
553
|
+
await import('./my-component.js'); // component reg() works normally
|
|
554
|
+
|
|
555
|
+
let html = await SSR.processHtml('<div><my-component></my-component></div>');
|
|
556
|
+
// => '<div><my-component><style>...</style><template shadowrootmode="open">...</template>content</my-component></div>'
|
|
557
|
+
|
|
558
|
+
SSR.destroy(); // cleanup globals
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`processHtml` takes any HTML string, renders all Symbiote components found within, and returns the processed HTML. If `SSR.init()` was already called, it reuses the existing environment; otherwise it auto-initializes (and auto-destroys after).
|
|
562
|
+
|
|
563
|
+
### Advanced — `renderToString` / `renderToStream`
|
|
564
|
+
|
|
565
|
+
```js
|
|
566
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
567
|
+
|
|
568
|
+
await SSR.init();
|
|
569
|
+
await import('./my-component.js');
|
|
570
|
+
|
|
571
|
+
let html = SSR.renderToString('my-component', { title: 'Hello' });
|
|
572
|
+
// => '<my-component title="Hello"><h1>Hello</h1></my-component>'
|
|
573
|
+
|
|
574
|
+
SSR.destroy();
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### API
|
|
578
|
+
|
|
579
|
+
| Method | Description |
|
|
580
|
+
|--------|-------------|
|
|
581
|
+
| `SSR.init()` | `async` — creates linkedom document, polyfills CSSStyleSheet/NodeFilter/MutationObserver/adoptedStyleSheets, patches globals |
|
|
582
|
+
| `SSR.processHtml(html)` | `async` — parses HTML string, renders all custom elements, returns processed HTML. Auto-inits if needed |
|
|
583
|
+
| `SSR.renderToString(tagName, attrs?)` | Creates element, triggers `connectedCallback`, serializes to HTML string |
|
|
584
|
+
| `SSR.renderToStream(tagName, attrs?)` | Async generator — yields HTML chunks. Same output as `renderToString`, but streamed for lower TTFB |
|
|
585
|
+
| `SSR.destroy()` | Removes global patches, cleans up document |
|
|
586
|
+
|
|
587
|
+
### Styles in SSR output
|
|
588
|
+
|
|
589
|
+
- **rootStyles** → `<style>` tag as first child of the component (light DOM, deduplicated per constructor)
|
|
590
|
+
- **shadowStyles** → `<style>` inside the Declarative Shadow DOM `<template>`
|
|
591
|
+
- Both are supported simultaneously on the same component
|
|
592
|
+
|
|
593
|
+
### Streaming usage
|
|
594
|
+
|
|
595
|
+
```js
|
|
596
|
+
import http from 'node:http';
|
|
597
|
+
import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
|
|
598
|
+
|
|
599
|
+
await SSR.init();
|
|
600
|
+
import './my-app.js';
|
|
601
|
+
|
|
602
|
+
http.createServer(async (req, res) => {
|
|
603
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
604
|
+
res.write('<!DOCTYPE html><html><body>');
|
|
605
|
+
for await (let chunk of SSR.renderToStream('my-app')) {
|
|
606
|
+
res.write(chunk);
|
|
607
|
+
}
|
|
608
|
+
res.end('</body></html>');
|
|
609
|
+
}).listen(3000);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Shadow DOM output
|
|
613
|
+
|
|
614
|
+
Shadow components produce Declarative Shadow DOM markup with styles inlined. Light DOM content is preserved alongside the DSD template:
|
|
615
|
+
```html
|
|
616
|
+
<my-shadow>
|
|
617
|
+
<style>my-shadow { display: block; }</style>
|
|
618
|
+
<template shadowrootmode="open">
|
|
619
|
+
<style>:host { color: red; }</style>
|
|
620
|
+
<h1>Content</h1>
|
|
621
|
+
<slot></slot>
|
|
622
|
+
</template>
|
|
623
|
+
Light DOM content here
|
|
624
|
+
</my-shadow>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### SSR context detection
|
|
628
|
+
|
|
629
|
+
`SSR.init()` sets `globalThis.__SYMBIOTE_SSR = true`. This is separate from the instance `ssrMode` flag:
|
|
630
|
+
|
|
631
|
+
| Flag | Scope | Purpose |
|
|
632
|
+
|------|-------|-------|
|
|
633
|
+
| `__SYMBIOTE_SSR` | Server (global) | Preserves binding attributes (`bind`, `ref`, `itemize`) in HTML output. Bypasses `ssrMode` effects |
|
|
634
|
+
| `ssrMode` | Client (instance) | Skips template injection, hydrates existing DOM with bindings |
|
|
635
|
+
|
|
636
|
+
### Hydration flow
|
|
637
|
+
|
|
638
|
+
1. **Server**: `SSR.processHtml()` / `SSR.renderToString()` produces HTML with `bind=` / `itemize=` attributes preserved
|
|
639
|
+
2. **Client**: component with `ssrMode = true` skips template injection, attaches bindings to pre-rendered DOM
|
|
640
|
+
3. State mutations on client update DOM reactively
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Routing (AppRouter)
|
|
645
|
+
|
|
646
|
+
### Path-based routing (recommended)
|
|
647
|
+
```js
|
|
648
|
+
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
|
|
649
|
+
|
|
650
|
+
const routerCtx = AppRouter.initRoutingCtx('R', {
|
|
651
|
+
home: { pattern: '/', title: 'Home', default: true },
|
|
652
|
+
user: { pattern: '/users/:id', title: 'User Profile' },
|
|
653
|
+
settings: { pattern: '/settings', title: 'Settings' },
|
|
654
|
+
notFound: { pattern: '/404', title: 'Not Found', error: true },
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Navigate programmatically
|
|
658
|
+
AppRouter.navigate('user', { id: '42' });
|
|
659
|
+
// URL becomes: /users/42
|
|
660
|
+
|
|
661
|
+
// React to route changes in any component
|
|
662
|
+
this.sub('R/route', (route) => console.log('Route:', route));
|
|
663
|
+
this.sub('R/options', (opts) => console.log('Params:', opts)); // { id: '42' }
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Query-string routing (legacy/alternative)
|
|
667
|
+
```js
|
|
668
|
+
// Routes WITHOUT `pattern` use query-string mode automatically
|
|
669
|
+
const routerCtx = AppRouter.initRoutingCtx('R', {
|
|
670
|
+
home: { title: 'Home', default: true },
|
|
671
|
+
about: { title: 'About' },
|
|
672
|
+
});
|
|
673
|
+
AppRouter.navigate('about', { section: 'team' });
|
|
674
|
+
// URL becomes: ?about§ion=team
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Route guards
|
|
678
|
+
```js
|
|
679
|
+
// Register guard — runs before every navigation
|
|
680
|
+
let unsub = AppRouter.beforeRoute((to, from) => {
|
|
681
|
+
if (!isAuth && to.route === 'settings') {
|
|
682
|
+
return 'login'; // redirect
|
|
683
|
+
}
|
|
684
|
+
// return false to cancel, nothing to proceed
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
unsub(); // remove guard
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Lazy loaded routes
|
|
691
|
+
```js
|
|
692
|
+
AppRouter.initRoutingCtx('R', {
|
|
693
|
+
settings: {
|
|
694
|
+
pattern: '/settings',
|
|
695
|
+
title: 'Settings',
|
|
696
|
+
load: () => import('./pages/settings.js'), // loaded once, cached
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### AppRouter API
|
|
702
|
+
- `AppRouter.initRoutingCtx(ctxName, routingMap)` → PubSub
|
|
703
|
+
- `AppRouter.navigate(route, options?)` — navigate and dispatch event
|
|
704
|
+
- `AppRouter.reflect(route, options?)` — update URL without triggering event
|
|
705
|
+
- `AppRouter.notify()` — read URL, run guards, lazy load, dispatch event
|
|
706
|
+
- `AppRouter.beforeRoute(fn)` — register guard, returns unsubscribe fn
|
|
707
|
+
- `AppRouter.setRoutingMap(map)` — extend routes
|
|
708
|
+
- `AppRouter.readAddressBar()` → `{ route, options }`
|
|
709
|
+
- `AppRouter.setSeparator(char)` — default `&` (query-string mode)
|
|
710
|
+
- `AppRouter.setDefaultTitle(title)`
|
|
711
|
+
- `AppRouter.removePopstateListener()`
|
|
712
|
+
- Mode auto-detected: routes with `pattern` → path-based, without → query-string
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Attribute Binding
|
|
717
|
+
|
|
718
|
+
```js
|
|
719
|
+
class MyComponent extends Symbiote {
|
|
720
|
+
init$ = {
|
|
721
|
+
'@name': '', // reads from HTML attribute `name` automatically
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
MyComponent.bindAttributes({
|
|
725
|
+
'value': 'inputValue', // maps HTML attr `value` → state prop `inputValue`
|
|
726
|
+
});
|
|
727
|
+
// observedAttributes is auto-populated
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## CSS Data Binding
|
|
733
|
+
|
|
734
|
+
Read CSS custom property values into component state:
|
|
735
|
+
|
|
736
|
+
```js
|
|
737
|
+
class MyComponent extends Symbiote {
|
|
738
|
+
cssInit$ = {
|
|
739
|
+
'--accent-color': '#ff0', // fallback value
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Or in template:
|
|
745
|
+
```html
|
|
746
|
+
<div ${{textContent: '--my-css-prop'}}>...</div>
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
Update with: `this.updateCssData()` / `this.dropCssDataCache()`.
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## Component Registration
|
|
754
|
+
|
|
755
|
+
```js
|
|
756
|
+
// Explicit tag name
|
|
757
|
+
MyComponent.reg('my-component');
|
|
758
|
+
|
|
759
|
+
// Auto-generated tag (sym-1, sym-2, ...)
|
|
760
|
+
MyComponent.reg();
|
|
761
|
+
|
|
762
|
+
// Alias registration (creates a subclass)
|
|
763
|
+
MyComponent.reg('my-alias', true);
|
|
764
|
+
|
|
765
|
+
// Get tag name (auto-registers if needed)
|
|
766
|
+
const tag = MyComponent.is; // 'my-component'
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
## Utilities
|
|
772
|
+
|
|
773
|
+
```js
|
|
774
|
+
import { UID } from '@symbiotejs/symbiote';
|
|
775
|
+
UID.generate('XXXXX-XXX'); // e.g. 'aB3kD-z9Q'
|
|
776
|
+
|
|
777
|
+
import { create, applyStyles, applyAttributes } from '@symbiotejs/symbiote';
|
|
778
|
+
let el = create({ tag: 'div', attributes: { id: 'x' }, styles: { color: 'red' }, children: [...] });
|
|
779
|
+
|
|
780
|
+
import { reassignDictionary } from '@symbiotejs/symbiote';
|
|
781
|
+
reassignDictionary({ BIND_ATTR: 'data-bind' }); // customize internal attribute names
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## Security (Trusted Types)
|
|
787
|
+
|
|
788
|
+
Template `innerHTML` writes use a Trusted Types policy when the API is available:
|
|
789
|
+
|
|
790
|
+
```js
|
|
791
|
+
// Symbiote creates a passthrough policy automatically:
|
|
792
|
+
// trustedTypes.createPolicy('symbiote', { createHTML: (s) => s })
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
This makes Symbiote compatible with strict CSP headers:
|
|
796
|
+
```
|
|
797
|
+
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types symbiote
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
No sanitization is performed — templates are developer-authored, not user input. The policy name is `'symbiote'`.
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Dev Mode
|
|
805
|
+
|
|
806
|
+
Enable verbose warnings during development:
|
|
807
|
+
```js
|
|
808
|
+
Symbiote.devMode = true;
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Always-on** (regardless of `devMode`):
|
|
812
|
+
- `[Symbiote]` prefixed warnings for PubSub errors, duplicate tags, type mismatches, router issues
|
|
813
|
+
- `this` in template interpolation error (`html` tag detects `${this.x}` usage)
|
|
814
|
+
|
|
815
|
+
**Dev-only** (`devMode = true`):
|
|
816
|
+
- Unresolved binding keys — warns when a template binding auto-initializes to `null` (likely typo)
|
|
817
|
+
- `*prop` without `ctx` attribute or `--ctx` CSS variable — warns that shared context won't be created
|
|
818
|
+
- `*prop` conflict — warns when a later component tries to set a different initial value for the same shared prop
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
## Common Mistakes to Avoid
|
|
823
|
+
|
|
824
|
+
1. **DON'T** use `this` in template strings — templates are decoupled from component context
|
|
825
|
+
2. **DON'T** nest property keys with dots in state: `'obj.prop'` won't work as a state key
|
|
826
|
+
3. **DON'T** forget `^` prefix when referencing **parent** component properties from itemize items
|
|
827
|
+
4. **DON'T** use `@` prefix directly in HTML — it's only for binding syntax (`${{'@attr': 'prop'}}`)
|
|
828
|
+
5. **DON'T** treat `init$` as a regular object — it's processed at connection time
|
|
829
|
+
6. **DON'T** define `template` inside the class body (`static template = html\`...\`` won't work) — it's a static property **setter**, assign it outside: `MyComponent.template = html\`...\``. Same applies to `rootStyles` and `shadowStyles`.
|
|
830
|
+
7. **DON'T** expect Shadow DOM by default — use `renderShadow = true` or `shadowStyles` to opt in
|
|
831
|
+
8. **DON'T** wrap Custom Elements in extra divs — the custom tag IS the wrapper
|
|
832
|
+
9. **DON'T** use CSS frameworks (Tailwind, etc.) — use native CSS with custom properties
|
|
833
|
+
10. **DON'T** use `require()` — ESM only (import/export)
|
|
834
|
+
11. **DON'T** use `*prop` without `ctx` attribute or `--ctx` CSS variable — shared context won't be created
|