@forgecart/designer-runtime 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/README.md +166 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-cjs/package.json +3 -0
- package/dist/loader-cjs/turbo-loader.d.ts +13 -0
- package/dist/loader-cjs/turbo-loader.js +190 -0
- package/dist/loader-cjs/turbo-loader.js.map +1 -0
- package/dist/protocol/element-reference-formatter.d.ts +28 -0
- package/dist/protocol/element-reference-formatter.js +60 -0
- package/dist/protocol/element-reference-formatter.js.map +1 -0
- package/dist/protocol/element.types.d.ts +75 -0
- package/dist/protocol/element.types.js +7 -0
- package/dist/protocol/element.types.js.map +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.js +6 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/message-codec.d.ts +50 -0
- package/dist/protocol/message-codec.js +209 -0
- package/dist/protocol/message-codec.js.map +1 -0
- package/dist/protocol/message.types.d.ts +122 -0
- package/dist/protocol/message.types.js +2 -0
- package/dist/protocol/message.types.js.map +1 -0
- package/dist/protocol/protocol.constant.d.ts +30 -0
- package/dist/protocol/protocol.constant.js +31 -0
- package/dist/protocol/protocol.constant.js.map +1 -0
- package/dist/runtime/descriptor/element-descriptor-extractor.d.ts +38 -0
- package/dist/runtime/descriptor/element-descriptor-extractor.js +190 -0
- package/dist/runtime/descriptor/element-descriptor-extractor.js.map +1 -0
- package/dist/runtime/designer-runtime.d.ts +85 -0
- package/dist/runtime/designer-runtime.js +321 -0
- package/dist/runtime/designer-runtime.js.map +1 -0
- package/dist/runtime/guest-messenger.d.ts +37 -0
- package/dist/runtime/guest-messenger.js +41 -0
- package/dist/runtime/guest-messenger.js.map +1 -0
- package/dist/runtime/handshake-gate.d.ts +35 -0
- package/dist/runtime/handshake-gate.js +81 -0
- package/dist/runtime/handshake-gate.js.map +1 -0
- package/dist/runtime/highlight/highlight-overlay.d.ts +25 -0
- package/dist/runtime/highlight/highlight-overlay.js +80 -0
- package/dist/runtime/highlight/highlight-overlay.js.map +1 -0
- package/dist/runtime/region/dom-hit-tester.d.ts +28 -0
- package/dist/runtime/region/dom-hit-tester.js +45 -0
- package/dist/runtime/region/dom-hit-tester.js.map +1 -0
- package/dist/runtime/region/element-filter.d.ts +60 -0
- package/dist/runtime/region/element-filter.js +146 -0
- package/dist/runtime/region/element-filter.js.map +1 -0
- package/dist/runtime/region/region-sampler.d.ts +41 -0
- package/dist/runtime/region/region-sampler.js +100 -0
- package/dist/runtime/region/region-sampler.js.map +1 -0
- package/dist/runtime/region/spray-region-resolver.d.ts +53 -0
- package/dist/runtime/region/spray-region-resolver.js +113 -0
- package/dist/runtime/region/spray-region-resolver.js.map +1 -0
- package/dist/util/geometry.util.d.ts +29 -0
- package/dist/util/geometry.util.js +68 -0
- package/dist/util/geometry.util.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @forgecart/designer-runtime
|
|
2
|
+
|
|
3
|
+
The guest half of ForgeCart's visual editor. It ships inside tenant Next.js
|
|
4
|
+
storefronts, boots **dormant**, and is activated exclusively over the iframe
|
|
5
|
+
`postMessage` API by the dashboard host. Once active it resolves spray-stroke
|
|
6
|
+
regions into DOM element descriptors (with `file:line:col` source attribution
|
|
7
|
+
stamped at build time), renders highlight overlays, and reports scroll /
|
|
8
|
+
resize / navigation so the host can keep its canvas in sync.
|
|
9
|
+
|
|
10
|
+
Three entrypoints:
|
|
11
|
+
|
|
12
|
+
| Subpath | Environment | Contents |
|
|
13
|
+
|---------|-------------|----------|
|
|
14
|
+
| `.` | Browser (ESM) | `installDesignerRuntime(): () => void` — the storefront mounts this once |
|
|
15
|
+
| `./protocol` | Anywhere (ESM, DOM-free) | Wire types, `MessageCodec`, `ElementReferenceFormatter`, protocol constants — the dashboard host and the agent import **only** this |
|
|
16
|
+
| `./turbo-loader` | Node (CJS) | The Turbopack/webpack loader that stamps `data-fc-source` onto host JSX elements in development |
|
|
17
|
+
|
|
18
|
+
The browser entrypoints have **zero runtime dependencies**; the loader
|
|
19
|
+
subpath depends on `@babel/parser` + `magic-string` only.
|
|
20
|
+
|
|
21
|
+
## Wire protocol (v1, namespace `fc-designer`)
|
|
22
|
+
|
|
23
|
+
Every message carries the envelope `{ ns: 'fc-designer', v: 1 }`; anything
|
|
24
|
+
else on the message bus (payment SDKs, analytics, HMR) is foreign traffic and
|
|
25
|
+
ignored silently. All coordinates are **iframe-viewport CSS pixels** — the
|
|
26
|
+
guest is zoom-agnostic by construction.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
GUEST boots dormant ── READY (targetOrigin '*', empty payload) ──▶ HOST
|
|
30
|
+
HOST ── HELLO { nonce, protocolVersion } ──▶ GUEST nonce-gated, pins event.origin
|
|
31
|
+
GUEST ── HELLO_ACK { capabilities, url, viewport } ──▶ HOST state: handshaken
|
|
32
|
+
HOST ── ACTIVATE ──▶ GUEST ── ACTIVATED ──▶ HOST state: active
|
|
33
|
+
HOST ── REGION_SELECT { requestId, stroke, radius, maxElements } ──▶ GUEST
|
|
34
|
+
GUEST ── REGION_RESOLVED { requestId, elements, regionBounds, truncated } ──▶ HOST
|
|
35
|
+
HOST ── HIGHLIGHT_SET { refIds } / HIGHLIGHT_CLEAR ──▶ GUEST
|
|
36
|
+
HOST ── ELEMENT_BOUNDS_QUERY { requestId, refIds } ──▶ GUEST ── ELEMENT_BOUNDS ──▶ HOST
|
|
37
|
+
HOST ── PING ──▶ GUEST ── PONG { url, scroll } ──▶ HOST liveness
|
|
38
|
+
GUEST ── SCROLL / RESIZE (rAF-throttled), NAVIGATED ──▶ HOST while active
|
|
39
|
+
HOST ── DEACTIVATE ──▶ GUEST ── DEACTIVATED ──▶ HOST state: handshaken
|
|
40
|
+
GUEST ── ERROR { code, requestId, detail } ──▶ HOST post-handshake only
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Error codes: `INVALID_MESSAGE`, `NOT_ACTIVE`, `REGION_TOO_LARGE`,
|
|
44
|
+
`UNKNOWN_REF`, `INTERNAL` — a wire-level string union; the host maps them to
|
|
45
|
+
its own UX.
|
|
46
|
+
|
|
47
|
+
### Activation security
|
|
48
|
+
|
|
49
|
+
- **Inert outside an iframe** — `window.parent === window` makes
|
|
50
|
+
`installDesignerRuntime` a no-op. The dormant footprint is exactly one
|
|
51
|
+
`message` listener and zero DOM nodes.
|
|
52
|
+
- **Capability nonce** — the host appends `#fcd=<uuid>` to the preview URL.
|
|
53
|
+
The fragment never reaches the server; the guest parks the nonce in
|
|
54
|
+
`sessionStorage` (survives iframe reloads) and strips only the `fcd` key
|
|
55
|
+
from the hash. Without the nonce, every HELLO is ignored silently.
|
|
56
|
+
- **Origin pinning** — a HELLO is accepted only from `window.parent` with a
|
|
57
|
+
real (non-`'null'`) origin and the exact nonce; the first success pins
|
|
58
|
+
`event.origin` and every later post targets that origin. A re-HELLO must
|
|
59
|
+
come from the pinned origin.
|
|
60
|
+
- **Reload recovery** — an iframe reload boots a fresh runtime that posts a
|
|
61
|
+
fresh READY beacon; the host re-handshakes and the nonce is still in
|
|
62
|
+
`sessionStorage`.
|
|
63
|
+
|
|
64
|
+
### Region resolution
|
|
65
|
+
|
|
66
|
+
`REGION_SELECT` rasterizes the stroke into grid cells (cell size
|
|
67
|
+
`max(8, radius/2)`, budget 600 cells with coarsening back-off; padded bbox
|
|
68
|
+
beyond 4× the viewport area → `REGION_TOO_LARGE`), stack hit-tests every
|
|
69
|
+
cell center, drops non-meaningful elements (document chrome, invisible,
|
|
70
|
+
zero-area, > 85%-viewport wrappers, Next dev chrome), collapses
|
|
71
|
+
ancestor/descendant stacks by coverage, ranks by coverage then area, caps at
|
|
72
|
+
`min(maxElements ?? 12, 25)`, and stamps survivors with `data-fc-ref` for
|
|
73
|
+
later `HIGHLIGHT_SET` / `ELEMENT_BOUNDS_QUERY`. Stamps accumulate per
|
|
74
|
+
activation and are stripped on `DEACTIVATE`.
|
|
75
|
+
|
|
76
|
+
## Template integration (tenant storefront)
|
|
77
|
+
|
|
78
|
+
`next.config.js` — run the stamping loader before SWC (Next 15.1 key;
|
|
79
|
+
renamed `turbopack.rules` in 15.3). `next build` ignores it on 15.1 and the
|
|
80
|
+
loader additionally self-gates on `NODE_ENV === 'development'`, so production
|
|
81
|
+
output is untouched:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
module.exports = {
|
|
85
|
+
experimental: {
|
|
86
|
+
turbo: {
|
|
87
|
+
rules: {
|
|
88
|
+
'src/**/*.{tsx,jsx}': {
|
|
89
|
+
loaders: [{ loader: '@forgecart/designer-runtime/turbo-loader' }],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Mount the runtime last in the root layout's `<body>` via a `'use client'`
|
|
98
|
+
component so it survives App Router client navigations:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
'use client';
|
|
102
|
+
import { useEffect } from 'react';
|
|
103
|
+
|
|
104
|
+
export function ForgecartDesigner(): null {
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (process.env.NODE_ENV !== 'development') return; // DCE'd from prod bundles
|
|
107
|
+
if (window.parent === window) return;
|
|
108
|
+
let dispose: (() => void) | undefined;
|
|
109
|
+
let cancelled = false;
|
|
110
|
+
void import('@forgecart/designer-runtime').then((mod) => {
|
|
111
|
+
if (cancelled) return;
|
|
112
|
+
dispose = mod.installDesignerRuntime();
|
|
113
|
+
});
|
|
114
|
+
return () => {
|
|
115
|
+
cancelled = true;
|
|
116
|
+
dispose?.();
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`installDesignerRuntime` is idempotent across React StrictMode's double
|
|
124
|
+
effect: a second install while a runtime is live returns the live dispose,
|
|
125
|
+
and disposing clears the singleton.
|
|
126
|
+
|
|
127
|
+
## Host integration (dashboard)
|
|
128
|
+
|
|
129
|
+
Import **only** `@forgecart/designer-runtime/protocol`. The host:
|
|
130
|
+
|
|
131
|
+
1. appends `#fcd=<crypto.randomUUID()>` to the preview URL before setting the
|
|
132
|
+
iframe `src`;
|
|
133
|
+
2. listens for `READY`, then posts `HELLO { nonce, protocolVersion }` to the
|
|
134
|
+
iframe's content window — and re-handshakes whenever a fresh `READY`
|
|
135
|
+
arrives (iframe reload);
|
|
136
|
+
3. validates every inbound message with `MessageCodec.parseGuestMessage` and
|
|
137
|
+
checks `event.source` is the iframe's content window;
|
|
138
|
+
4. correlates request/response pairs by `requestId` with a timeout;
|
|
139
|
+
5. serializes selections for the first message of an anchored AI thread via
|
|
140
|
+
`ElementReferenceFormatter` (`<fc-selection>{json}</fc-selection>`).
|
|
141
|
+
|
|
142
|
+
## Contingency: jsx-dev-runtime fallback
|
|
143
|
+
|
|
144
|
+
If a future Next/Turbopack release breaks `experimental.turbo.rules` for JS
|
|
145
|
+
loaders, the documented fallback is intercepting `jsx-dev-runtime`'s `jsxDEV`
|
|
146
|
+
source argument to inject the same attribute at render time. It is kept as a
|
|
147
|
+
contingency only — it is fragile against React 19.2 dropping `jsxDEV` source
|
|
148
|
+
args, while the loader approach owns its parse and survives React upgrades.
|
|
149
|
+
|
|
150
|
+
## Develop, build, publish
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
nx run designer-runtime:test # jsdom unit + loader integration suite
|
|
154
|
+
nx run designer-runtime:typecheck # lib + loader + spec tsconfigs
|
|
155
|
+
nx run designer-runtime:build # tsc ESM lib → dist/, tsc CJS loader → dist/loader-cjs/
|
|
156
|
+
nx run designer-runtime:publish # npm publish --access public (runs build first)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The build stamps `dist/loader-cjs/package.json` with `{ "type": "commonjs" }`
|
|
160
|
+
so the loader subpath `require()`s correctly from this otherwise
|
|
161
|
+
`"type": "module"` package.
|
|
162
|
+
|
|
163
|
+
Publish order for real workspace pods: publish this package first (the
|
|
164
|
+
pod-image template warmup `npm install`s it through the Nexus npm proxy),
|
|
165
|
+
then bump + commit the storefront template / CLI, then rebuild the
|
|
166
|
+
workspace-pod image.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boots the designer runtime inside a tenant storefront. Inert unless the
|
|
3
|
+
* page is actually embedded (`window.parent !== window`) — on a top-level
|
|
4
|
+
* page this returns a no-op dispose and registers nothing. When embedded,
|
|
5
|
+
* the runtime boots dormant (one `message` listener, a READY beacon, zero
|
|
6
|
+
* DOM nodes) and waits for the dashboard's nonce-gated HELLO.
|
|
7
|
+
*
|
|
8
|
+
* @returns a dispose function that fully tears the runtime down.
|
|
9
|
+
*/
|
|
10
|
+
export declare function installDesignerRuntime(): () => void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MessageCodec } from './protocol/message-codec.js';
|
|
2
|
+
import { ElementDescriptorExtractor } from './runtime/descriptor/element-descriptor-extractor.js';
|
|
3
|
+
import { DesignerRuntime } from './runtime/designer-runtime.js';
|
|
4
|
+
import { GuestMessenger } from './runtime/guest-messenger.js';
|
|
5
|
+
import { HandshakeGate } from './runtime/handshake-gate.js';
|
|
6
|
+
import { HighlightOverlay } from './runtime/highlight/highlight-overlay.js';
|
|
7
|
+
import { DomHitTester } from './runtime/region/dom-hit-tester.js';
|
|
8
|
+
import { ElementFilter } from './runtime/region/element-filter.js';
|
|
9
|
+
import { RegionSampler } from './runtime/region/region-sampler.js';
|
|
10
|
+
import { SprayRegionResolver } from './runtime/region/spray-region-resolver.js';
|
|
11
|
+
/**
|
|
12
|
+
* Module-level singleton handle. React StrictMode runs every effect twice
|
|
13
|
+
* (mount → cleanup → mount), and an App Router layout can re-render the
|
|
14
|
+
* mounting component without a real page load — so idempotency cannot live
|
|
15
|
+
* in component state. The module instance is the one thing shared across
|
|
16
|
+
* those mounts: a second install while a runtime is live returns the live
|
|
17
|
+
* dispose, and disposing clears the slot so the next install boots fresh.
|
|
18
|
+
*/
|
|
19
|
+
let liveDispose = null;
|
|
20
|
+
const NOOP_DISPOSE = () => undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Boots the designer runtime inside a tenant storefront. Inert unless the
|
|
23
|
+
* page is actually embedded (`window.parent !== window`) — on a top-level
|
|
24
|
+
* page this returns a no-op dispose and registers nothing. When embedded,
|
|
25
|
+
* the runtime boots dormant (one `message` listener, a READY beacon, zero
|
|
26
|
+
* DOM nodes) and waits for the dashboard's nonce-gated HELLO.
|
|
27
|
+
*
|
|
28
|
+
* @returns a dispose function that fully tears the runtime down.
|
|
29
|
+
*/
|
|
30
|
+
export function installDesignerRuntime() {
|
|
31
|
+
if (typeof window === 'undefined' || window.parent === window)
|
|
32
|
+
return NOOP_DISPOSE;
|
|
33
|
+
if (liveDispose !== null)
|
|
34
|
+
return liveDispose;
|
|
35
|
+
const runtime = buildRuntime(window);
|
|
36
|
+
runtime.install();
|
|
37
|
+
const dispose = () => {
|
|
38
|
+
if (liveDispose !== dispose)
|
|
39
|
+
return;
|
|
40
|
+
liveDispose = null;
|
|
41
|
+
runtime.dispose();
|
|
42
|
+
};
|
|
43
|
+
liveDispose = dispose;
|
|
44
|
+
return dispose;
|
|
45
|
+
}
|
|
46
|
+
/** Composition root: wires the runtime object graph for a live window. */
|
|
47
|
+
function buildRuntime(win) {
|
|
48
|
+
const doc = win.document;
|
|
49
|
+
return new DesignerRuntime(win, new MessageCodec(), new HandshakeGate(win), new GuestMessenger(win.parent), new SprayRegionResolver(new RegionSampler(), new DomHitTester(doc), new ElementFilter(win), new ElementDescriptorExtractor(win), doc), new HighlightOverlay(doc));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AAClG,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAEhF;;;;;;;GAOG;AACH,IAAI,WAAW,GAAwB,IAAI,CAAC;AAE5C,MAAM,YAAY,GAAG,GAAS,EAAE,CAAC,SAAS,CAAC;AAE3C;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB;IACpC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACnF,IAAI,WAAW,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAE7C,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACrC,OAAO,CAAC,OAAO,EAAE,CAAC;IAElB,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,WAAW,KAAK,OAAO;YAAE,OAAO;QACpC,WAAW,GAAG,IAAI,CAAC;QACnB,OAAO,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC,CAAC;IACF,WAAW,GAAG,OAAO,CAAC;IACtB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,0EAA0E;AAC1E,SAAS,YAAY,CAAC,GAAW;IAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC;IACzB,OAAO,IAAI,eAAe,CACxB,GAAG,EACH,IAAI,YAAY,EAAE,EAClB,IAAI,aAAa,CAAC,GAAG,CAAC,EACtB,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,EAC9B,IAAI,mBAAmB,CACrB,IAAI,aAAa,EAAE,EACnB,IAAI,YAAY,CAAC,GAAG,CAAC,EACrB,IAAI,aAAa,CAAC,GAAG,CAAC,EACtB,IAAI,0BAA0B,CAAC,GAAG,CAAC,EACnC,GAAG,CACJ,EACD,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAC1B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Minimal surface of the webpack/Turbopack loader context this loader uses. */
|
|
2
|
+
interface TurboLoaderContext {
|
|
3
|
+
/** Absolute path of the module being loaded. */
|
|
4
|
+
resourcePath: string;
|
|
5
|
+
/** Absolute path of the project root; empty/undefined falls back to ''. */
|
|
6
|
+
rootContext?: string;
|
|
7
|
+
/** Marks the loader async and returns the completion callback. */
|
|
8
|
+
async(): (error: Error | null, content?: string, sourceMap?: object) => void;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: ((this: TurboLoaderContext, source: string, map?: object) => void) & {
|
|
11
|
+
DATA_ATTRIBUTE: string;
|
|
12
|
+
};
|
|
13
|
+
export = _default;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const parser_1 = require("@babel/parser");
|
|
7
|
+
const magic_string_1 = __importDefault(require("magic-string"));
|
|
8
|
+
/**
|
|
9
|
+
* The attribute stamped on host JSX elements, value
|
|
10
|
+
* `<project-relative-file>:<line>:<col>:<ComponentName>`.
|
|
11
|
+
*
|
|
12
|
+
* This is a deliberate local duplicate of the protocol's `SOURCE_ATTRIBUTE`
|
|
13
|
+
* constant: the loader compiles standalone to CommonJS (`dist/loader-cjs/`,
|
|
14
|
+
* rootDir `src/loader`) and must not import from `src/protocol`, which
|
|
15
|
+
* belongs to the ESM browser build. The turbo-loader spec asserts the two
|
|
16
|
+
* constants are equal, so they cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
const DATA_ATTRIBUTE = 'data-fc-source';
|
|
19
|
+
/**
|
|
20
|
+
* Stamps `data-fc-source="<file>:<line>:<col>:<Component>"` onto every host
|
|
21
|
+
* (lowercase) JSX element of one TSX/JSX module via magic-string, tracking
|
|
22
|
+
* the enclosing component name through a hand-rolled recursive AST walk —
|
|
23
|
+
* no `@babel/traverse`, keeping the loader's dependency surface at
|
|
24
|
+
* `@babel/parser` + `magic-string`.
|
|
25
|
+
*/
|
|
26
|
+
class JsxSourceStamper {
|
|
27
|
+
source;
|
|
28
|
+
relativePath;
|
|
29
|
+
magic;
|
|
30
|
+
stampCount = 0;
|
|
31
|
+
constructor(source, relativePath) {
|
|
32
|
+
this.source = source;
|
|
33
|
+
this.relativePath = relativePath;
|
|
34
|
+
this.magic = new magic_string_1.default(source);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Runs the stamping pass. Returns null when the module is left unchanged —
|
|
38
|
+
* either nothing needed stamping, or the module did not parse. A parse
|
|
39
|
+
* failure must never break the dev server: the file may be mid-edit under
|
|
40
|
+
* hot reload, and an unstamped module only costs source attribution, so
|
|
41
|
+
* the deliberate strategy is to pass every unparseable module through
|
|
42
|
+
* untouched.
|
|
43
|
+
*/
|
|
44
|
+
stamp() {
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = (0, parser_1.parse)(this.source, {
|
|
48
|
+
sourceType: 'module',
|
|
49
|
+
plugins: ['typescript', 'jsx'],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!this.isAstNode(parsed))
|
|
56
|
+
return null;
|
|
57
|
+
this.visit(parsed, null);
|
|
58
|
+
if (this.stampCount === 0)
|
|
59
|
+
return null;
|
|
60
|
+
return { code: this.magic.toString(), map: this.magic.generateMap({ hires: true }) };
|
|
61
|
+
}
|
|
62
|
+
visit(node, componentName) {
|
|
63
|
+
const enclosing = this.enclosingComponentName(node) ?? componentName;
|
|
64
|
+
if (node.type === 'JSXOpeningElement')
|
|
65
|
+
this.stampIfHostElement(node, enclosing);
|
|
66
|
+
for (const key of Object.keys(node)) {
|
|
67
|
+
if (key === 'loc')
|
|
68
|
+
continue;
|
|
69
|
+
this.visitChild(node[key], enclosing);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
visitChild(value, componentName) {
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
for (const item of value) {
|
|
75
|
+
if (this.isAstNode(item))
|
|
76
|
+
this.visit(item, componentName);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (this.isAstNode(value))
|
|
81
|
+
this.visit(value, componentName);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A node opens a component scope when it is a `function Component()`
|
|
85
|
+
* declaration or a `const Component = () => ...` / `function` initializer
|
|
86
|
+
* — in both cases the identifier must be capitalized, React's own
|
|
87
|
+
* component convention.
|
|
88
|
+
*/
|
|
89
|
+
enclosingComponentName(node) {
|
|
90
|
+
if (node.type === 'FunctionDeclaration') {
|
|
91
|
+
return this.capitalizedIdentifierName(node.id);
|
|
92
|
+
}
|
|
93
|
+
if (node.type !== 'VariableDeclarator')
|
|
94
|
+
return null;
|
|
95
|
+
if (!this.isAstNode(node.init))
|
|
96
|
+
return null;
|
|
97
|
+
if (node.init.type !== 'ArrowFunctionExpression' && node.init.type !== 'FunctionExpression') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return this.capitalizedIdentifierName(node.id);
|
|
101
|
+
}
|
|
102
|
+
capitalizedIdentifierName(value) {
|
|
103
|
+
if (!this.isAstNode(value) || value.type !== 'Identifier')
|
|
104
|
+
return null;
|
|
105
|
+
const name = value.name;
|
|
106
|
+
if (typeof name !== 'string' || !/^[A-Z]/.test(name))
|
|
107
|
+
return null;
|
|
108
|
+
return name;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Stamps a JSXOpeningElement when it is a host element — a lowercase
|
|
112
|
+
* `JSXIdentifier` tag (`<div>`, `<my-widget>`); components, fragments,
|
|
113
|
+
* member expressions (`<Foo.Bar>`) and namespaced names stay untouched —
|
|
114
|
+
* and does not already carry the attribute (idempotency under double
|
|
115
|
+
* application). Line is 1-based and column converted to 1-based, both
|
|
116
|
+
* addressing the opening element's `<`.
|
|
117
|
+
*/
|
|
118
|
+
stampIfHostElement(node, componentName) {
|
|
119
|
+
const name = node.name;
|
|
120
|
+
if (!this.isAstNode(name) || name.type !== 'JSXIdentifier')
|
|
121
|
+
return;
|
|
122
|
+
if (typeof name.name !== 'string' || !/^[a-z]/.test(name.name))
|
|
123
|
+
return;
|
|
124
|
+
if (this.hasDataAttribute(node))
|
|
125
|
+
return;
|
|
126
|
+
const position = this.startPosition(node);
|
|
127
|
+
const insertAt = name.end;
|
|
128
|
+
if (position === null || typeof insertAt !== 'number')
|
|
129
|
+
return;
|
|
130
|
+
const value = `${this.relativePath}:${String(position.line)}:${String(position.column + 1)}:${componentName ?? ''}`;
|
|
131
|
+
this.magic.appendLeft(insertAt, ` ${DATA_ATTRIBUTE}="${value.replace(/"/g, '')}"`);
|
|
132
|
+
this.stampCount++;
|
|
133
|
+
}
|
|
134
|
+
hasDataAttribute(openingElement) {
|
|
135
|
+
const attributes = openingElement.attributes;
|
|
136
|
+
if (!Array.isArray(attributes))
|
|
137
|
+
return false;
|
|
138
|
+
return attributes.some((attribute) => this.isDataAttribute(attribute));
|
|
139
|
+
}
|
|
140
|
+
isDataAttribute(attribute) {
|
|
141
|
+
if (!this.isAstNode(attribute) || attribute.type !== 'JSXAttribute')
|
|
142
|
+
return false;
|
|
143
|
+
const name = attribute.name;
|
|
144
|
+
return this.isAstNode(name) && name.type === 'JSXIdentifier' && name.name === DATA_ATTRIBUTE;
|
|
145
|
+
}
|
|
146
|
+
startPosition(node) {
|
|
147
|
+
const loc = node.loc;
|
|
148
|
+
if (typeof loc !== 'object' || loc === null || !('start' in loc))
|
|
149
|
+
return null;
|
|
150
|
+
const start = loc.start;
|
|
151
|
+
if (typeof start !== 'object' || start === null)
|
|
152
|
+
return null;
|
|
153
|
+
if (!('line' in start) || !('column' in start))
|
|
154
|
+
return null;
|
|
155
|
+
const { line, column } = start;
|
|
156
|
+
if (typeof line !== 'number' || typeof column !== 'number')
|
|
157
|
+
return null;
|
|
158
|
+
return { line, column };
|
|
159
|
+
}
|
|
160
|
+
isAstNode(value) {
|
|
161
|
+
return (typeof value === 'object' &&
|
|
162
|
+
value !== null &&
|
|
163
|
+
'type' in value &&
|
|
164
|
+
typeof value.type === 'string');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Turbopack/webpack loader entry. Registered in the storefront template via
|
|
169
|
+
* `experimental.turbo.rules` so it runs before SWC compiles each TSX/JSX
|
|
170
|
+
* module — identically across the RSC, SSR and client layers, so server and
|
|
171
|
+
* client markup carry the same attributes and hydration never mismatches.
|
|
172
|
+
* Self-gates on `NODE_ENV === 'development'`: production builds pass every
|
|
173
|
+
* module through untouched.
|
|
174
|
+
*/
|
|
175
|
+
const turboLoader = function (source, map) {
|
|
176
|
+
const callback = this.async();
|
|
177
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
178
|
+
callback(null, source, map);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const relativePath = (0, node_path_1.relative)(this.rootContext ?? '', this.resourcePath).replace(/\\/g, '/');
|
|
182
|
+
const result = new JsxSourceStamper(source, relativePath).stamp();
|
|
183
|
+
if (result === null) {
|
|
184
|
+
callback(null, source, map);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
callback(null, result.code, result.map);
|
|
188
|
+
};
|
|
189
|
+
module.exports = Object.assign(turboLoader, { DATA_ATTRIBUTE });
|
|
190
|
+
//# sourceMappingURL=turbo-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turbo-loader.js","sourceRoot":"","sources":["../../src/loader/turbo-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ElementDescriptor } from './element.types.js';
|
|
2
|
+
/** Opens the machine-readable selection block inside a chat message. */
|
|
3
|
+
export declare const SELECTION_BLOCK_OPEN = "<fc-selection>";
|
|
4
|
+
/** Closes the machine-readable selection block inside a chat message. */
|
|
5
|
+
export declare const SELECTION_BLOCK_CLOSE = "</fc-selection>";
|
|
6
|
+
/**
|
|
7
|
+
* Serializes resolved element descriptors into the `<fc-selection>` text
|
|
8
|
+
* block that anchors a visual-editor thread. The block rides verbatim in the
|
|
9
|
+
* first chat message of the thread (the agent pipeline persists and
|
|
10
|
+
* rehydrates message text untouched), so this formatter is the single
|
|
11
|
+
* definition of the format for both the dashboard host and the agent.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ElementReferenceFormatter {
|
|
14
|
+
/**
|
|
15
|
+
* Builds the selection block for one spray selection. Stamped elements
|
|
16
|
+
* carry a `file:line:col` source pointer plus the enclosing component;
|
|
17
|
+
* unstamped elements fall back to their body-rooted DOM path.
|
|
18
|
+
*/
|
|
19
|
+
toReferenceBlock(descriptors: readonly ElementDescriptor[], pageUrl: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Prepends the selection block to the user's text — the exact message
|
|
22
|
+
* shape the dashboard sends as the first message of an anchored thread.
|
|
23
|
+
*/
|
|
24
|
+
buildSelectionMessage(block: string, userText: string): string;
|
|
25
|
+
private toReferenceEntry;
|
|
26
|
+
private formatSource;
|
|
27
|
+
private formatDomPath;
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from './protocol.constant.js';
|
|
2
|
+
/** Opens the machine-readable selection block inside a chat message. */
|
|
3
|
+
export const SELECTION_BLOCK_OPEN = '<fc-selection>';
|
|
4
|
+
/** Closes the machine-readable selection block inside a chat message. */
|
|
5
|
+
export const SELECTION_BLOCK_CLOSE = '</fc-selection>';
|
|
6
|
+
/**
|
|
7
|
+
* Serializes resolved element descriptors into the `<fc-selection>` text
|
|
8
|
+
* block that anchors a visual-editor thread. The block rides verbatim in the
|
|
9
|
+
* first chat message of the thread (the agent pipeline persists and
|
|
10
|
+
* rehydrates message text untouched), so this formatter is the single
|
|
11
|
+
* definition of the format for both the dashboard host and the agent.
|
|
12
|
+
*/
|
|
13
|
+
export class ElementReferenceFormatter {
|
|
14
|
+
/**
|
|
15
|
+
* Builds the selection block for one spray selection. Stamped elements
|
|
16
|
+
* carry a `file:line:col` source pointer plus the enclosing component;
|
|
17
|
+
* unstamped elements fall back to their body-rooted DOM path.
|
|
18
|
+
*/
|
|
19
|
+
toReferenceBlock(descriptors, pageUrl) {
|
|
20
|
+
const payload = {
|
|
21
|
+
version: PROTOCOL_VERSION,
|
|
22
|
+
pageUrl,
|
|
23
|
+
elements: descriptors.map((descriptor) => this.toReferenceEntry(descriptor)),
|
|
24
|
+
};
|
|
25
|
+
return `${SELECTION_BLOCK_OPEN}${JSON.stringify(payload)}${SELECTION_BLOCK_CLOSE}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Prepends the selection block to the user's text — the exact message
|
|
29
|
+
* shape the dashboard sends as the first message of an anchored thread.
|
|
30
|
+
*/
|
|
31
|
+
buildSelectionMessage(block, userText) {
|
|
32
|
+
return `${block}\n${userText}`;
|
|
33
|
+
}
|
|
34
|
+
toReferenceEntry(descriptor) {
|
|
35
|
+
return {
|
|
36
|
+
ref: descriptor.refId,
|
|
37
|
+
tag: descriptor.tagName,
|
|
38
|
+
source: this.formatSource(descriptor),
|
|
39
|
+
component: descriptor.source?.componentName ?? null,
|
|
40
|
+
text: descriptor.text,
|
|
41
|
+
role: descriptor.role,
|
|
42
|
+
id: descriptor.id,
|
|
43
|
+
classes: descriptor.classList,
|
|
44
|
+
attributes: descriptor.attributes,
|
|
45
|
+
domPath: this.formatDomPath(descriptor.domPath),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
formatSource(descriptor) {
|
|
49
|
+
if (descriptor.source === null)
|
|
50
|
+
return null;
|
|
51
|
+
const { file, line, column } = descriptor.source;
|
|
52
|
+
return `${file}:${String(line)}:${String(column)}`;
|
|
53
|
+
}
|
|
54
|
+
formatDomPath(segments) {
|
|
55
|
+
return segments
|
|
56
|
+
.map((segment) => `${segment.tag}:nth-of-type(${String(segment.nthOfType)})`)
|
|
57
|
+
.join(' > ');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=element-reference-formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-reference-formatter.js","sourceRoot":"","sources":["../../src/protocol/element-reference-formatter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,wEAAwE;AACxE,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AAErD,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAuBvD;;;;;;GAMG;AACH,MAAM,OAAO,yBAAyB;IACpC;;;;OAIG;IACH,gBAAgB,CAAC,WAAyC,EAAE,OAAe;QACzE,MAAM,OAAO,GAA0B;YACrC,OAAO,EAAE,gBAAgB;YACzB,OAAO;YACP,QAAQ,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;SAC7E,CAAC;QACF,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,qBAAqB,EAAE,CAAC;IACrF,CAAC;IAED;;;OAGG;IACH,qBAAqB,CAAC,KAAa,EAAE,QAAgB;QACnD,OAAO,GAAG,KAAK,KAAK,QAAQ,EAAE,CAAC;IACjC,CAAC;IAEO,gBAAgB,CAAC,UAA6B;QACpD,OAAO;YACL,GAAG,EAAE,UAAU,CAAC,KAAK;YACrB,GAAG,EAAE,UAAU,CAAC,OAAO;YACvB,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC;YACrC,SAAS,EAAE,UAAU,CAAC,MAAM,EAAE,aAAa,IAAI,IAAI;YACnD,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,OAAO,EAAE,UAAU,CAAC,SAAS;YAC7B,UAAU,EAAE,UAAU,CAAC,UAAU;YACjC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC;SAChD,CAAC;IACJ,CAAC;IAEO,YAAY,CAAC,UAA6B;QAChD,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC5C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC;QACjD,OAAO,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IACrD,CAAC;IAEO,aAAa,CAAC,QAAmC;QACvD,OAAO,QAAQ;aACZ,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,gBAAgB,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC;aAC5E,IAAI,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC;CACF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM-free element/geometry shapes that cross the iframe wire. All
|
|
3
|
+
* coordinates are iframe-viewport CSS pixels — the guest is zoom-agnostic by
|
|
4
|
+
* construction; the host owns world/zoom transforms.
|
|
5
|
+
*/
|
|
6
|
+
/** A point in iframe-viewport CSS pixels. */
|
|
7
|
+
export interface Point {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
/** An axis-aligned rectangle in iframe-viewport CSS pixels. */
|
|
12
|
+
export interface Rect {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
/** The iframe's visual viewport size in CSS pixels. */
|
|
19
|
+
export interface Viewport {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
/** The iframe document's scroll offset in CSS pixels. */
|
|
24
|
+
export interface ScrollPosition {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
}
|
|
28
|
+
/** One `tag:nth-of-type(n)` step of an element's DOM path (body-rooted). */
|
|
29
|
+
export interface DomPathSegment {
|
|
30
|
+
tag: string;
|
|
31
|
+
nthOfType: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A parsed `data-fc-source` stamp pointing back at the JSX opening element
|
|
35
|
+
* in the storefront source tree. `componentName` is null when the stamping
|
|
36
|
+
* loader could not determine an enclosing component.
|
|
37
|
+
*/
|
|
38
|
+
export interface SourceRef {
|
|
39
|
+
file: string;
|
|
40
|
+
line: number;
|
|
41
|
+
column: number;
|
|
42
|
+
componentName: string | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Everything the host (and ultimately the AI agent) needs to know about one
|
|
46
|
+
* element resolved from a spray region. `refId` addresses the element in
|
|
47
|
+
* later HIGHLIGHT_SET / ELEMENT_BOUNDS_QUERY calls via its `data-fc-ref`
|
|
48
|
+
* attribute; it is only stable until DEACTIVATE strips the stamps.
|
|
49
|
+
*/
|
|
50
|
+
export interface ElementDescriptor {
|
|
51
|
+
refId: string;
|
|
52
|
+
tagName: string;
|
|
53
|
+
id: string | null;
|
|
54
|
+
classList: string[];
|
|
55
|
+
domPath: DomPathSegment[];
|
|
56
|
+
source: SourceRef | null;
|
|
57
|
+
text: string | null;
|
|
58
|
+
role: string | null;
|
|
59
|
+
attributes: Record<string, string>;
|
|
60
|
+
bounds: Rect;
|
|
61
|
+
coverage: number;
|
|
62
|
+
childCount: number;
|
|
63
|
+
}
|
|
64
|
+
/** What the guest runtime can do for this page, reported in HELLO_ACK. */
|
|
65
|
+
export interface GuestCapabilities {
|
|
66
|
+
/** True when at least one `data-fc-source` stamp exists in the document. */
|
|
67
|
+
stamping: boolean;
|
|
68
|
+
/** True when the highlight overlay is available. */
|
|
69
|
+
highlight: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** Per-ref answer of an ELEMENT_BOUNDS_QUERY; `bounds` is null for stale refs. */
|
|
72
|
+
export interface ElementBoundsEntry {
|
|
73
|
+
refId: string;
|
|
74
|
+
bounds: Rect | null;
|
|
75
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM-free element/geometry shapes that cross the iframe wire. All
|
|
3
|
+
* coordinates are iframe-viewport CSS pixels — the guest is zoom-agnostic by
|
|
4
|
+
* construction; the host owns world/zoom transforms.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=element.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.types.js","sourceRoot":"","sources":["../../src/protocol/element.types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kCAAkC,CAAC"}
|