sygnal 6.2.0 → 6.4.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/CHANGELOG.md +112 -0
- package/README.md +3 -1
- package/dist/ai.cjs.js +36 -0
- package/dist/ai.cjs.js.map +1 -1
- package/dist/ai.esm.js +1 -1
- package/dist/astro/index.cjs.js +69 -3
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +69 -3
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3 -3
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3 -3
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +211 -11
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +211 -11
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/agent.md +7 -1
- package/dist/guide/ai-cards.md +319 -0
- package/dist/guide/ai-chat.md +4 -0
- package/dist/guide/ai-compose.md +881 -0
- package/dist/guide/ai-decisions.md +16 -0
- package/dist/guide/http.md +1 -0
- package/dist/guide/mcp-apps.md +59 -4
- package/dist/index.cjs.js +5704 -350
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +647 -10
- package/dist/index.esm.js +5694 -350
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vike/onRenderClient.cjs.js +3 -2
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +3 -2
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +69 -3
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +69 -3
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +7 -5
- package/package.json +2 -1
- package/src/ai.d.ts +642 -7
- package/src/ai.ts +6 -0
- package/src/collection.ts +2 -1
- package/src/core/instance.ts +6 -3
- package/src/core/markers/lazy.ts +7 -3
- package/src/core/markers/portal.ts +5 -2
- package/src/core/markers/suspense.ts +3 -2
- package/src/core/markers/transition.ts +6 -3
- package/src/core/registry.ts +9 -5
- package/src/cycle/dom/IsolateModule.ts +25 -22
- package/src/extra/ai/chat/behavior.ts +354 -81
- package/src/extra/ai/chat/driver.ts +7 -4
- package/src/extra/ai/compose/agent.ts +55 -0
- package/src/extra/ai/compose/ask.ts +147 -0
- package/src/extra/ai/compose/behavior.ts +183 -0
- package/src/extra/ai/compose/catalog.ts +211 -0
- package/src/extra/ai/compose/composed.ts +61 -0
- package/src/extra/ai/compose/decisions.ts +1649 -0
- package/src/extra/ai/compose/diag.ts +33 -0
- package/src/extra/ai/compose/index.ts +23 -0
- package/src/extra/ai/compose/layout.ts +441 -0
- package/src/extra/ai/compose/llm.ts +1120 -0
- package/src/extra/ai/compose/render.ts +159 -0
- package/src/extra/ai/compose/testing.ts +71 -0
- package/src/extra/ai/compose/types.ts +159 -0
- package/src/extra/ai/decide.ts +97 -1
- package/src/extra/ai/index.ts +6 -1
- package/src/extra/diagnostics/checks/dom.ts +10 -2
- package/src/extra/diagnostics/checks/next.ts +103 -3
- package/src/extra/diagnostics/checks/persist.ts +4 -2
- package/src/extra/diagnostics/checks/wiring.ts +14 -6
- package/src/extra/diagnostics/codes.ts +59 -0
- package/src/extra/fetchDriver.ts +58 -27
- package/src/extra/lens.ts +15 -0
- package/src/extra/pwa.ts +3 -1
- package/src/extra/reducers.ts +10 -4
- package/src/extra/ssr.ts +3 -3
- package/src/extra/testing.ts +2 -0
- package/src/extra/widget.ts +3 -3
- package/src/index.d.ts +50 -2
- package/src/index.ts +8 -0
- package/src/lazy.ts +3 -2
- package/src/portal.ts +5 -2
- package/src/pragma/index.ts +2 -1
- package/src/suspense.ts +5 -2
- package/src/switchable.ts +2 -1
- package/src/transition.ts +5 -2
- package/src/vike/onRenderClient.ts +3 -2
- package/src/vite/plugin.ts +63 -2
package/src/index.d.ts
CHANGED
|
@@ -156,7 +156,7 @@ type EventNextFunction = (action: string, data?: any, delay?: number) => void
|
|
|
156
156
|
|
|
157
157
|
/** Payload function for `event()` / `emit()`: receives the reducer arguments, returns the event data. */
|
|
158
158
|
export type EventPayloadFunction<TYPE extends string = string, STATE = any, DATA = any> =
|
|
159
|
-
(state: STATE, data: DATA, next: EventNextFunction, props: any) => EventPayload<TYPE>
|
|
159
|
+
(state: STATE, data: DATA, next: EventNextFunction, props: any) => EventPayload<TYPE> | ABORT
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Remaining arguments of `event()` / `emit()` when the payload is a static value.
|
|
@@ -1977,7 +1977,8 @@ export function emit<TYPE extends EventName>(
|
|
|
1977
1977
|
* }
|
|
1978
1978
|
*
|
|
1979
1979
|
* The payload is either a function `(state, data, next, props) => payload` or a static value:
|
|
1980
|
-
* `event('RESET')`, `event('SET_MODE', 'dark')`.
|
|
1980
|
+
* `event('RESET')`, `event('SET_MODE', 'dark')`. A payload function that returns `ABORT` sends
|
|
1981
|
+
* nothing: `event('SAVED', (state) => state.dirty ? { id: state.id } : ABORT)`.
|
|
1981
1982
|
*
|
|
1982
1983
|
* With a `SygnalEvents` registry, `type` must be a registered name and the payload must match
|
|
1983
1984
|
* its type. When used inside a model, the payload function's `state` and `data` parameters are
|
|
@@ -2358,6 +2359,21 @@ export type FetchRequest = string | {
|
|
|
2358
2359
|
* schema's (possibly transformed) value. A failure is an error with `issues`
|
|
2359
2360
|
*/
|
|
2360
2361
|
validate?: StandardSchemaLike;
|
|
2362
|
+
/**
|
|
2363
|
+
* D472: send these sub-requests instead, in parallel (each inherits this request's fields:
|
|
2364
|
+
* `url`, `headers`, `init`, ...; its own `json` / `body` / `parse` win), and deliver ONE reply:
|
|
2365
|
+
* `combine(bodies, requests)`. One failure fails the request (once). The test fake
|
|
2366
|
+
* (renderComponent) answers the request as one, by its own `url` / `json`. `decide()` makes these
|
|
2367
|
+
*/
|
|
2368
|
+
batch?: ReadonlyArray<{ url?: string; method?: string; headers?: Record<string, string>; query?: Record<string, unknown>; json?: unknown; body?: BodyInit; parse?: 'auto' | 'json' | 'text' | ((response: Response) => any) }>;
|
|
2369
|
+
/** D472: merges the parsed bodies of `batch` / `split` parts into the reply (default: the array) */
|
|
2370
|
+
combine?: (bodies: any[], requests: any[]) => any;
|
|
2371
|
+
/**
|
|
2372
|
+
* D472: a request (or a `batch` part) that fails with an HTTP error is sent again as the
|
|
2373
|
+
* requests this returns (≥ 2, in parallel, merged by `combine`), at most 3 times deep; null keeps
|
|
2374
|
+
* the error. `decide()` halves its questions on a 400 / 413 that names a size limit
|
|
2375
|
+
*/
|
|
2376
|
+
split?: (request: any, error: Error & { status?: number; body?: any }) => any[] | null | undefined;
|
|
2361
2377
|
/** Your own fields (an id, ...): not sent, returned on the reply's `request` */
|
|
2362
2378
|
[appData: string]: any;
|
|
2363
2379
|
} | {
|
|
@@ -2990,6 +3006,12 @@ export interface ServiceWorkerCommand {
|
|
|
2990
3006
|
|
|
2991
3007
|
export interface ServiceWorkerOptions {
|
|
2992
3008
|
scope?: string;
|
|
3009
|
+
/**
|
|
3010
|
+
* `false`: the driver registers no service worker, ignores commands and emits nothing, so the
|
|
3011
|
+
* app keeps its `SW` wiring where a worker must not run: `enabled: import.meta.env.PROD`.
|
|
3012
|
+
* Default `true`.
|
|
3013
|
+
*/
|
|
3014
|
+
enabled?: boolean;
|
|
2993
3015
|
}
|
|
2994
3016
|
|
|
2995
3017
|
export function makeServiceWorkerDriver(
|
|
@@ -3708,7 +3730,9 @@ declare global {
|
|
|
3708
3730
|
namespace JSX {
|
|
3709
3731
|
interface IntrinsicElements {
|
|
3710
3732
|
[elemName: string]: any;
|
|
3733
|
+
/** @deprecated since 6.3.0, removed in the next major: `import { Collection } from 'sygnal'` and write `<Collection …>` (SYG426) */
|
|
3711
3734
|
collection: CollectionProps<any>;
|
|
3735
|
+
/** @deprecated since 6.3.0, removed in the next major: `import { Switchable } from 'sygnal'` and write `<Switchable …>` (SYG426) */
|
|
3712
3736
|
switchable: SwitchableProps<any>;
|
|
3713
3737
|
slot: SlotProps;
|
|
3714
3738
|
}
|
|
@@ -3721,6 +3745,15 @@ declare global {
|
|
|
3721
3745
|
children: {};
|
|
3722
3746
|
}
|
|
3723
3747
|
|
|
3748
|
+
/**
|
|
3749
|
+
* `key` on any tag (D456): on a component, its identity among its parent's children (before
|
|
3750
|
+
* its `id` prop, then its position): a keyed instance survives a move or a reorder, and a new
|
|
3751
|
+
* key makes a new instance. Never a prop of the component.
|
|
3752
|
+
*/
|
|
3753
|
+
interface IntrinsicAttributes {
|
|
3754
|
+
key?: string | number | null;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3724
3757
|
/**
|
|
3725
3758
|
* A component's view receives `state: STATE` and `context`, but in JSX the parent passes
|
|
3726
3759
|
* an optional `state` slice name or lens (`<Editor state="editor" />`, `<Panel />`).
|
|
@@ -3754,6 +3787,21 @@ export type { ChatState, ChatStatus, ChatActions, ChatDone, ChatOptions } from '
|
|
|
3754
3787
|
// PLAN-6 M-2 / M-3 (3-M)
|
|
3755
3788
|
export { answers, commandBar } from './ai.d'
|
|
3756
3789
|
export type { AnswerPick, AnswerPicks, CommandBarState, CommandBarStatus, CommandBarActions, CommandBarOptions, CommandBarResult, CommandBarUnsure } from './ai.d'
|
|
3790
|
+
// PLAN-9 (A): catalogs, the compose behavior, composeWith, the layout utilities
|
|
3791
|
+
export { defineCatalog, compose, composeWith, fromTree, toTree, removeNode } from './ai.d'
|
|
3792
|
+
// PLAN-9 H: chat client tools and cards
|
|
3793
|
+
export { openToolCalls, askCard } from './ai.d'
|
|
3794
|
+
export { Composed } from './ai.d'
|
|
3795
|
+
export type { ChatClientTool, ChatAskOptions, ChatCard, ChatCardStatus, ChatToolPart, ComposedProps } from './ai.d'
|
|
3796
|
+
export type { Catalog, CatalogDecl, CatalogPartDecl, CatalogContainerDecl, CatalogParam, CatalogParamValue, CatalogParamsOf, CatalogRecord, CatalogPart, CatalogContainer, CatalogOption, CatalogTree, CatalogTreeNode, ComposeTree, ContainerView, Layout, LayoutNode, ComposeRenderOptions, ComposeRenderable, ComposeBackend, ComposeNames, ComposeState, ComposeStatus, ComposeActions, ComposeOptions, ComposeAnswer } from './ai.d'
|
|
3797
|
+
|
|
3798
|
+
/**
|
|
3799
|
+
* PLAN-9 (D463): a `{ get, set }` lens to the element of the array `state[field]` whose `idKey`
|
|
3800
|
+
* (default 'id') is `id`: a record part's state (`state: (node) => byId('projects',
|
|
3801
|
+
* node.params.id)`) or any child's `state={byId('todos', 3)}`. `get` is undefined once the element
|
|
3802
|
+
* is gone; `set` then leaves the state unchanged.
|
|
3803
|
+
*/
|
|
3804
|
+
export function byId<T = any, S = any>(field: string, id: unknown, idKey?: string): { get(state: S): T | undefined; set(state: S, value: T): S }
|
|
3757
3805
|
// PLAN-6 X-1 (3-X)
|
|
3758
3806
|
export { makeMcpAppDriver } from './ai.d'
|
|
3759
3807
|
export type { McpAppDriverOptions, McpAppSource, McpAppRequest, McpAppError, McpToolResult, McpHostContext, McpContentBlock, McpAppDisplayMode } from './ai.d'
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,14 @@ export { anthropicMessages, agui, fromAISDK, strictSchemas } from './extra/ai/in
|
|
|
68
68
|
export { makeMcpAppDriver } from './extra/ai/index'
|
|
69
69
|
// A-3 (3-A3, experimental, D291)
|
|
70
70
|
export { formTool } from './extra/ai/index'
|
|
71
|
+
// PLAN-9 (A): catalogs, the compose behavior, composeWith, the layout utilities
|
|
72
|
+
export { defineCatalog, compose, composeWith, fromTree, toTree, removeNode } from './extra/ai/index'
|
|
73
|
+
// PLAN-9 H: chat client tools and cards
|
|
74
|
+
export { openToolCalls, askCard } from './extra/ai/index'
|
|
75
|
+
// PLAN-9 M-1: <Composed catalog from /> (documented import: 'sygnal/ai')
|
|
76
|
+
export { Composed } from './extra/ai/index'
|
|
77
|
+
// PLAN-9 (D463): a lens to an array element by id
|
|
78
|
+
export { byId } from './extra/lens'
|
|
71
79
|
|
|
72
80
|
// export dom helper functions (h, makeDOMDriver, etc.)
|
|
73
81
|
export * from './cycle/dom/index'
|
package/src/lazy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// the core's resolver for lazy components, registered
|
|
2
|
-
import './core/markers/lazy';
|
|
1
|
+
// the core's resolver for lazy components (D157), registered by the first lazy() call (G-751)
|
|
2
|
+
import {enableLazy} from './core/markers/lazy';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* PLAN-5 B-4 (D103): `when` defers the import: 'visible' starts it when a placeholder enters the
|
|
@@ -24,6 +24,7 @@ export interface LazyOptions {
|
|
|
24
24
|
const WHEN = 'data-sygnal-when';
|
|
25
25
|
|
|
26
26
|
export function lazy(loadFn: () => Promise<any>, options?: LazyOptions): any {
|
|
27
|
+
enableLazy();
|
|
27
28
|
let cachedComponent: any = null;
|
|
28
29
|
let loadError: any = null;
|
|
29
30
|
const g: any = globalThis, when = options?.when, ph = options?.placeholderHeight;
|
package/src/portal.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {h} from './cycle/dom/snabbdom';
|
|
2
|
-
// the core's handler for this marker, registered
|
|
3
|
-
import
|
|
2
|
+
// the core's handler for this marker (D157), registered when a tag uses it (G-751: the pragma calls
|
|
3
|
+
// `$reg`; not on import, so a bundle that never uses the marker drops the handler)
|
|
4
|
+
import {enablePortal as reg} from './core/markers/portal';
|
|
4
5
|
|
|
5
6
|
const Portal = (props: any) => {
|
|
7
|
+
reg();
|
|
6
8
|
const {children, ...sanitizedProps} = props;
|
|
7
9
|
return h('portal', {props: sanitizedProps}, children);
|
|
8
10
|
};
|
|
9
11
|
(Portal as any).componentName = 'portal';
|
|
10
12
|
(Portal as any).preventInstantiation = true;
|
|
13
|
+
(Portal as any).$reg = reg;
|
|
11
14
|
|
|
12
15
|
export {Portal};
|
|
13
16
|
export default Portal;
|
package/src/pragma/index.ts
CHANGED
|
@@ -322,7 +322,8 @@ export const createElementWithModules = (modules: Record<string, any>) => {
|
|
|
322
322
|
if (typeof text === 'undefined') plain = !!(flatten(children, kids = []) & +plain)
|
|
323
323
|
// G-492: a custom element (bit 8) routes list as a prop, in its own cache
|
|
324
324
|
const d = data ? sanitizeData(data, modules, isComponent ? croutes : t & 8 ? xroutes : routes, isComponent || t & 8 && 2) : {}
|
|
325
|
-
|
|
325
|
+
// G-751: a marker's core handler registers on first use (`$reg`), not when its module loads
|
|
326
|
+
if (fn) d[fn.preventInstantiation ? 'm' : 'c'] = fn, fn.$reg?.()
|
|
326
327
|
const key = k !== undefined ? k : data ? data.key : undefined
|
|
327
328
|
const vnode = plain ? new (Plain as any)(sel, d, kids, text, key) : { sel, data: d, children: kids, text, elm: undefined, key }
|
|
328
329
|
if (t & 2) applySvg(vnode)
|
package/src/suspense.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {h} from './cycle/dom/snabbdom';
|
|
2
|
-
// the core's handler for this marker, registered
|
|
3
|
-
import
|
|
2
|
+
// the core's handler for this marker (D157), registered when a tag uses it (G-751: the pragma calls
|
|
3
|
+
// `$reg`; not on import, so a bundle that never uses the marker drops the handler)
|
|
4
|
+
import {enableSuspense as reg} from './core/markers/suspense';
|
|
4
5
|
|
|
5
6
|
const Suspense = (props: any) => {
|
|
7
|
+
reg();
|
|
6
8
|
const {children, ...sanitizedProps} = props;
|
|
7
9
|
return h('suspense', {props: sanitizedProps}, children);
|
|
8
10
|
};
|
|
9
11
|
(Suspense as any).componentName = 'suspense';
|
|
10
12
|
(Suspense as any).preventInstantiation = true;
|
|
13
|
+
(Suspense as any).$reg = reg;
|
|
11
14
|
|
|
12
15
|
export {Suspense};
|
|
13
16
|
export default Suspense;
|
package/src/switchable.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {h} from './cycle/dom/snabbdom';
|
|
2
|
-
// the core's
|
|
2
|
+
// the core's host for this marker, registered on import (D157): unlike Portal, Transition and
|
|
3
|
+
// Suspense (G-751), not on first use, because the lowercase tag works without an import (docs)
|
|
3
4
|
import './core/hosts/switchable';
|
|
4
5
|
|
|
5
6
|
/** `<Switchable of={{ a: A, b: B }} current={state.tab} />`: a marker vnode the core's Switchable host renders */
|
package/src/transition.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {h} from './cycle/dom/snabbdom';
|
|
2
|
-
// the core's handler for this marker, registered
|
|
3
|
-
import
|
|
2
|
+
// the core's handler for this marker (D157), registered when a tag uses it (G-751: the pragma calls
|
|
3
|
+
// `$reg`; not on import, so a bundle that never uses the marker drops the handler)
|
|
4
|
+
import {enableTransition as reg} from './core/markers/transition';
|
|
4
5
|
|
|
5
6
|
const Transition = (props: any) => {
|
|
7
|
+
reg();
|
|
6
8
|
const {children, ...sanitizedProps} = props;
|
|
7
9
|
return h('transition', {props: sanitizedProps}, children);
|
|
8
10
|
};
|
|
9
11
|
(Transition as any).componentName = 'transition';
|
|
10
12
|
(Transition as any).preventInstantiation = true;
|
|
13
|
+
(Transition as any).$reg = reg;
|
|
11
14
|
|
|
12
15
|
export {Transition};
|
|
13
16
|
export default Transition;
|
|
@@ -105,7 +105,8 @@ function componentVNode(comp: any, key: string, stateField: any, children: any[]
|
|
|
105
105
|
children,
|
|
106
106
|
text: undefined,
|
|
107
107
|
elm: undefined,
|
|
108
|
-
key:
|
|
108
|
+
// no vnode key: it would be the instance identity (D456); `id` is (G-207)
|
|
109
|
+
key: undefined,
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -132,7 +133,7 @@ function pageChildVNode(pageState: any, stateField: any = 'page'): any {
|
|
|
132
133
|
children: [],
|
|
133
134
|
text: undefined,
|
|
134
135
|
elm: undefined,
|
|
135
|
-
key:
|
|
136
|
+
key: undefined,
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
package/src/vite/plugin.ts
CHANGED
|
@@ -117,6 +117,12 @@
|
|
|
117
117
|
* /__sygnal/mcp (./mcp.ts: streamable HTTP, loopback / local Host / local Origin only)
|
|
118
118
|
* and the dev entries also import 'virtual:sygnal/mcp', which connects the page over the
|
|
119
119
|
* HMR channel (installMcpBridge from 'sygnal/devtools'). Never in `vite build` or Vitest.
|
|
120
|
+
* 11. SYG103 view words (dev server with diagnostics on; never `vite build`, SSR or Vitest): a
|
|
121
|
+
* module that sets `X.intent` or `X.uses` gets a tail registering, for each such X, the
|
|
122
|
+
* words of its string literals and of the relative modules it imports, in
|
|
123
|
+
* `globalThis.__SYGNAL_VIEW_TOKENS__`. The runtime SYG103 check then takes a selector drawn
|
|
124
|
+
* by a view helper (`{state.open && panel()}`) as conditionally rendered, as it does for
|
|
125
|
+
* markup in the view function itself (see viewTokensTail).
|
|
120
126
|
*
|
|
121
127
|
* Why not Vite's `define`? Vite's dependency optimizer does not apply user
|
|
122
128
|
* `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
|
|
@@ -483,8 +489,17 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
483
489
|
transform(this: any, code: string, id: string, opts?: { ssr?: boolean }) {
|
|
484
490
|
// SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
|
|
485
491
|
const adapters = adapterImports(this, code, id)
|
|
486
|
-
|
|
487
|
-
|
|
492
|
+
const result = adapters
|
|
493
|
+
? checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts))
|
|
494
|
+
: devTransform.call(this, code, id, opts)
|
|
495
|
+
// 11. the words SYG103 may take as conditionally rendered markup (dev server, diagnostics on)
|
|
496
|
+
const tokens = isServe && !isVitest && devOn && !opts?.ssr ? viewTokensTail(this, code, id) : null
|
|
497
|
+
if (!tokens) return result
|
|
498
|
+
return Promise.all([result, tokens]).then(([r, tail]: [any, string]) => {
|
|
499
|
+
if (!tail) return r
|
|
500
|
+
if (r == null) return withSourcemap(code, [], tail, id)
|
|
501
|
+
return typeof r == 'string' ? r + tail : { ...r, code: r.code + tail }
|
|
502
|
+
})
|
|
488
503
|
},
|
|
489
504
|
}
|
|
490
505
|
|
|
@@ -572,6 +587,52 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
|
|
|
572
587
|
}
|
|
573
588
|
}
|
|
574
589
|
|
|
590
|
+
// 11. SYG103 (the dev runtime check of an intent selector that never matched) lets a selector
|
|
591
|
+
// through as conditionally rendered when its words appear in the component's view; the runtime
|
|
592
|
+
// can read only the view function's own source. A component module (one that sets `X.intent` or
|
|
593
|
+
// `X.uses`) gets, in dev, a tail registering for each such component the words of its string
|
|
594
|
+
// literals and those of the relative modules it imports, where view helpers live
|
|
595
|
+
// (globalThis.__SYGNAL_VIEW_TOKENS__, a WeakMap read by checks/dom.ts).
|
|
596
|
+
const COMPONENT_WITH_SELECTORS_RE = /(?:^|[^\w$.])([A-Z][\w$]*)\.(?:intent|uses)\s*=(?!=)/g
|
|
597
|
+
const STRING_RE = /(['"])((?:\\.|(?!\1)[^\\\n])*)\1|`((?:\\[\s\S]|[^\\`])*)`/g
|
|
598
|
+
const RELATIVE_IMPORT_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"])(\.{1,2}\/[^'"]+)\1/g
|
|
599
|
+
|
|
600
|
+
// A string that is itself a selector ('.save', '#id', 'form.ask', '[data-x]') or a path / URL: its
|
|
601
|
+
// words aren't markup (the intent's own DOM.click('.svae') would otherwise vouch for its typo)
|
|
602
|
+
const NOT_MARKUP_RE = /(?:^|[\s>+~,(])[\w-]*[.#[][\w-]|^\s*(?:\/|[\w+-]+:\/\/)/
|
|
603
|
+
|
|
604
|
+
/** the class / id / tag-like words in a module's string and template literals (comments skipped) */
|
|
605
|
+
function stringWords(code: string, into: Set<string>): void {
|
|
606
|
+
for (const m of blankOut(code, false).matchAll(STRING_RE)) {
|
|
607
|
+
const text = m[2] ?? m[3] ?? ''
|
|
608
|
+
if (NOT_MARKUP_RE.test(text)) continue
|
|
609
|
+
for (const w of text.match(/[A-Za-z_][\w-]*/g) || []) into.add(w)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function viewTokensTail(ctx: any, code: string, id: string): Promise<string> | null {
|
|
614
|
+
const file = cleanId(id)
|
|
615
|
+
if (!/\.[jt]sx?$/.test(file) || id.includes('node_modules') || id.startsWith('\0')) return null
|
|
616
|
+
const names = new Set<string>()
|
|
617
|
+
for (const m of blankOut(code, true).matchAll(COMPONENT_WITH_SELECTORS_RE)) names.add(m[1])
|
|
618
|
+
if (!names.size) return null
|
|
619
|
+
const words = new Set<string>()
|
|
620
|
+
stringWords(code, words)
|
|
621
|
+
const specs = new Set(Array.from(blankOut(code, false).matchAll(RELATIVE_IMPORT_RE), m => m[2]))
|
|
622
|
+
return Promise.all([...specs].map(async (spec) => {
|
|
623
|
+
try {
|
|
624
|
+
const r = await ctx.resolve(spec, id)
|
|
625
|
+
const dep = r && !r.external && cleanId(r.id)
|
|
626
|
+
if (!dep || !/\.[cm]?[jt]sx?$/.test(dep) || dep.includes('node_modules')) return
|
|
627
|
+
ctx.addWatchFile?.(dep)
|
|
628
|
+
stringWords(await fs.promises.readFile(dep, 'utf8'), words)
|
|
629
|
+
} catch (_) { /* unreadable: its words are just not added */ }
|
|
630
|
+
})).then(() => {
|
|
631
|
+
const comps = [...names].map(n => `typeof ${n} == 'function' && ${n}`).join(', ')
|
|
632
|
+
return `\n;(() => { try { const m = globalThis.__SYGNAL_VIEW_TOKENS__ ||= new WeakMap(), t = ${JSON.stringify([...words].join(' '))}; for (const c of [${comps}]) if (c) m.set(c, t) } catch (_) {} })();\n`
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
575
636
|
// PLAN-5 W-2 (D209): the adapter entries and the optional peer dependencies each one imports
|
|
576
637
|
// (the Zag packages pinned together: they depend on each other's exact versions)
|
|
577
638
|
const ZAG = '~1.45.0'
|