@archie/devtools 0.0.47 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/{chunk-P5XMHBFS.mjs → chunk-THKKWDMJ.mjs} +98 -2
- package/dist/client/client.js +160 -2
- package/dist/client/client.mjs +160 -2
- package/dist/collection-binding.js +1 -0
- package/dist/collection-binding.mjs +1 -1
- package/dist/index.d.mts +92 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +98 -2
- package/dist/index.mjs +1 -1
- package/dist/inspector.standalone.js +625 -90
- package/package.json +4 -2
|
@@ -1725,10 +1725,96 @@
|
|
|
1725
1725
|
// asking blindly (and treating silence as "still unknown", never
|
|
1726
1726
|
// as "reported empty") is the correct posture.
|
|
1727
1727
|
capabilitiesQuery: true,
|
|
1728
|
+
// vid-identity migration (protocol 1.x, `@8base-archie/editor-protocol`):
|
|
1729
|
+
// this script reads `data-vid`/`meta.vid` as the node's identity
|
|
1730
|
+
// (`resolveNodeVid` below) and assembles a `NodeMeta` object for
|
|
1731
|
+
// `ELEMENT_SELECTED`, and it emits `REFUSED` instead of silently
|
|
1732
|
+
// accepting a drag/drop this package's own policy would reject.
|
|
1733
|
+
// Hosts MUST feature-detect all three: a frozen pre-migration
|
|
1734
|
+
// inspector has none of them, and its `ELEMENT_SELECTED` has no
|
|
1735
|
+
// `vid`/`nodeMeta` fields at all.
|
|
1736
|
+
vidIdentity: true,
|
|
1737
|
+
nodeMeta: true,
|
|
1738
|
+
refusals: true,
|
|
1728
1739
|
},
|
|
1729
1740
|
}
|
|
1730
1741
|
}
|
|
1731
1742
|
|
|
1743
|
+
// `@8base-archie/editor-protocol`'s `PROTOCOL_VERSION` (semver,
|
|
1744
|
+
// major-only compatibility). This script cannot `import` the shared
|
|
1745
|
+
// package (vanilla IIFE, CDN-served) — keep this literal in sync BY
|
|
1746
|
+
// HAND with that package's `src/version.ts` whenever the wire shape of
|
|
1747
|
+
// `RuntimeMessage`/`NodeMeta`/`Op` changes. Reported as a SEPARATE key
|
|
1748
|
+
// from the existing numeric `protocolVersion` above (never overwritten
|
|
1749
|
+
// — archie-app's `runtime-compatibility.ts` parses that field as a
|
|
1750
|
+
// number and compares it with strict equality; colliding the two would
|
|
1751
|
+
// silently break every existing compatibility check).
|
|
1752
|
+
var EDITOR_PROTOCOL_VERSION = "1.0.0"
|
|
1753
|
+
|
|
1754
|
+
// vid-identity migration: `/^[a-z0-9]{6,8}$/`, mirrors
|
|
1755
|
+
// `@8base-archie/editor-protocol`'s `VID_PATTERN` — kept in parity by
|
|
1756
|
+
// hand for the same reason as `EDITOR_PROTOCOL_VERSION` above.
|
|
1757
|
+
var VID_PATTERN = /^[a-z0-9]{6,8}$/
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* This node's `vid` — the fiber-resolved (usage-projected) meta's
|
|
1761
|
+
* `vid` first, the DOM `data-vid` attribute next, or null. Mirrors
|
|
1762
|
+
* `node-key.ts`'s `getNodeKeyFromElement` precedence for the vid-only
|
|
1763
|
+
* portion of it (this script has no legacy-nodeId/runtime-key
|
|
1764
|
+
* fallback to offer here — callers that need a key for every element,
|
|
1765
|
+
* vid or not, keep using their own `inspectorSource` resolution).
|
|
1766
|
+
*/
|
|
1767
|
+
function resolveNodeVid(el, meta) {
|
|
1768
|
+
if (meta && typeof meta.vid === "string" && VID_PATTERN.test(meta.vid)) {
|
|
1769
|
+
return meta.vid
|
|
1770
|
+
}
|
|
1771
|
+
if (el && typeof el.getAttribute === "function") {
|
|
1772
|
+
var domVid = el.getAttribute("data-vid")
|
|
1773
|
+
if (domVid && VID_PATTERN.test(domVid)) return domVid
|
|
1774
|
+
}
|
|
1775
|
+
return null
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/**
|
|
1779
|
+
* Assemble this node's `NodeMeta` (`@8base-archie/editor-protocol`)
|
|
1780
|
+
* from its `EditorMeta`, or null when there is no `vid` to key it by.
|
|
1781
|
+
* Mirrors `fiber-bridge.ts`'s `getNodeMeta` — kept in parity by hand,
|
|
1782
|
+
* same reason as `EDITOR_PROTOCOL_VERSION` above (this script cannot
|
|
1783
|
+
* import the TS module or run its `NodeMetaSchema.safeParse`).
|
|
1784
|
+
*/
|
|
1785
|
+
function resolveNodeMeta(el, meta) {
|
|
1786
|
+
var vid = resolveNodeVid(el, meta)
|
|
1787
|
+
if (!vid) return null
|
|
1788
|
+
var nodeMeta = {
|
|
1789
|
+
vid: vid,
|
|
1790
|
+
file: (meta && meta.file) || "",
|
|
1791
|
+
dynamicType: (meta && meta.dynamicType) || "STATIC",
|
|
1792
|
+
isKitOrigin: !!(meta && (meta.isKitOrigin || isTemplateFrame(meta))),
|
|
1793
|
+
isChrome: !!(meta && meta.isChrome),
|
|
1794
|
+
containerKind: (meta && meta.containerKind) || "none",
|
|
1795
|
+
}
|
|
1796
|
+
if (meta && meta.componentName) nodeMeta.component = meta.componentName
|
|
1797
|
+
if (meta && meta.lockReason) nodeMeta.lockReason = meta.lockReason
|
|
1798
|
+
return nodeMeta
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/**
|
|
1802
|
+
* Emit `REFUSED` (vid-identity migration) instead of letting a
|
|
1803
|
+
* drag/drop the runtime's own policy would reject proceed silently —
|
|
1804
|
+
* the gap `docs/deploy-order.md`-adjacent decision this capability bit
|
|
1805
|
+
* documents: "today the drop is accepted and the host discards it
|
|
1806
|
+
* after the fact; that must stop."
|
|
1807
|
+
*/
|
|
1808
|
+
function refuseNode(code, cause, vid) {
|
|
1809
|
+
postMessageToParent({
|
|
1810
|
+
source: "archie-devtools",
|
|
1811
|
+
type: "REFUSED",
|
|
1812
|
+
code: code,
|
|
1813
|
+
cause: cause,
|
|
1814
|
+
vid: vid || undefined,
|
|
1815
|
+
})
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1732
1818
|
/**
|
|
1733
1819
|
* Emit the capabilities payload as `CAPABILITIES_RESULT`.
|
|
1734
1820
|
*
|
|
@@ -1745,6 +1831,7 @@
|
|
|
1745
1831
|
reason: typeof reason === "string" && reason !== "" ? reason : null,
|
|
1746
1832
|
version: handshake.version,
|
|
1747
1833
|
protocolVersion: handshake.protocolVersion,
|
|
1834
|
+
editorProtocolVersion: EDITOR_PROTOCOL_VERSION,
|
|
1748
1835
|
capabilities: handshake.capabilities,
|
|
1749
1836
|
})
|
|
1750
1837
|
}
|
|
@@ -1755,6 +1842,7 @@
|
|
|
1755
1842
|
timestamp: Date.now(),
|
|
1756
1843
|
version: initialHandshake.version,
|
|
1757
1844
|
protocolVersion: initialHandshake.protocolVersion,
|
|
1845
|
+
editorProtocolVersion: EDITOR_PROTOCOL_VERSION,
|
|
1758
1846
|
capabilities: initialHandshake.capabilities,
|
|
1759
1847
|
})
|
|
1760
1848
|
|
|
@@ -3069,25 +3157,254 @@
|
|
|
3069
3157
|
return result
|
|
3070
3158
|
}
|
|
3071
3159
|
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3160
|
+
var CSS_INLINE_TIMEOUT_MS = 2000
|
|
3161
|
+
var CSS_URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g
|
|
3162
|
+
|
|
3163
|
+
/** Resolves once `promise` settles, or once `ms` elapses — whichever
|
|
3164
|
+
* first — falling back to `fallbackValue` instead of rejecting. Used
|
|
3165
|
+
* to bound best-effort work (font/url inlining) that must never block
|
|
3166
|
+
* the capture past a short budget. */
|
|
3167
|
+
function withTimeout(promise, ms, fallbackValue) {
|
|
3168
|
+
return new Promise(function (resolve) {
|
|
3169
|
+
var settled = false
|
|
3170
|
+
var timer = setTimeout(function () {
|
|
3171
|
+
if (settled) return
|
|
3172
|
+
settled = true
|
|
3173
|
+
resolve(fallbackValue)
|
|
3174
|
+
}, ms)
|
|
3175
|
+
promise.then(
|
|
3176
|
+
function (value) {
|
|
3177
|
+
if (settled) return
|
|
3178
|
+
settled = true
|
|
3179
|
+
clearTimeout(timer)
|
|
3180
|
+
resolve(value)
|
|
3181
|
+
},
|
|
3182
|
+
function () {
|
|
3183
|
+
if (settled) return
|
|
3184
|
+
settled = true
|
|
3185
|
+
clearTimeout(timer)
|
|
3186
|
+
resolve(fallbackValue)
|
|
3187
|
+
}
|
|
3188
|
+
)
|
|
3189
|
+
})
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// Duck-typed against CSSOM's numeric `type` (MEDIA_RULE=4, IMPORT_RULE=3,
|
|
3193
|
+
// FONT_FACE_RULE=5) because CSSMediaRule/CSSImportRule/CSSFontFaceRule
|
|
3194
|
+
// are not guaranteed to exist in every test DOM (e.g. jsdom).
|
|
3195
|
+
function isMediaRule(rule) {
|
|
3196
|
+
if (typeof CSSMediaRule !== "undefined" && rule instanceof CSSMediaRule) {
|
|
3197
|
+
return true
|
|
3198
|
+
}
|
|
3199
|
+
return !!(rule && rule.media && rule.cssRules && rule.type === 4)
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
function isImportRule(rule) {
|
|
3203
|
+
if (typeof CSSImportRule !== "undefined" && rule instanceof CSSImportRule) {
|
|
3204
|
+
return true
|
|
3089
3205
|
}
|
|
3090
|
-
return
|
|
3206
|
+
return !!(rule && rule.styleSheet && rule.type === 3)
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function isFontFaceRule(rule) {
|
|
3210
|
+
if (typeof CSSFontFaceRule !== "undefined" && rule instanceof CSSFontFaceRule) {
|
|
3211
|
+
return true
|
|
3212
|
+
}
|
|
3213
|
+
return !!(rule && rule.type === 5)
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
/** The at-rule keyword of `rule.cssText` (e.g. "layer", "supports"),
|
|
3217
|
+
* lowercased. Used where a rule's numeric `.type` is unreliable — the
|
|
3218
|
+
* CSSOM spec never assigned legacy type constants to rules added
|
|
3219
|
+
* after `@supports` (`@layer`, `@container`, `@starting-style`, ...),
|
|
3220
|
+
* so they all report `type === 0`. */
|
|
3221
|
+
function ruleAtKeyword(rule) {
|
|
3222
|
+
var text = rule && rule.cssText ? rule.cssText : ""
|
|
3223
|
+
var match = /^\s*@(?:-webkit-|-moz-|-ms-|-o-)?([a-zA-Z-]+)/.exec(text)
|
|
3224
|
+
return match ? match[1].toLowerCase() : ""
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
/** A rule that carries its own declarations/content and must always be
|
|
3228
|
+
* pushed verbatim, never treated as a group to recurse into — even
|
|
3229
|
+
* though some of these also expose `.cssRules`:
|
|
3230
|
+
* - `CSSStyleRule` (type 1) exposes `.cssRules` in real browsers for
|
|
3231
|
+
* CSS nesting (an empty list on an un-nested rule); treating that
|
|
3232
|
+
* as a grouping rule renders `.foo {}` with the declarations
|
|
3233
|
+
* dropped. Nested `@media` inside a nested style rule is a rare
|
|
3234
|
+
* case and is left as-is inside `cssText` rather than recursed.
|
|
3235
|
+
* - `@keyframes` / `@page` also expose `.cssRules`, but their nested
|
|
3236
|
+
* rules are keyframe selectors / margin boxes, not media-sensitive
|
|
3237
|
+
* content.
|
|
3238
|
+
* `.style` existing is a reliable signal across all of these (and
|
|
3239
|
+
* `CSSKeyframeRule`, type 8) even where `.type` duck-typing can't be
|
|
3240
|
+
* trusted. */
|
|
3241
|
+
function isStyleLikeLeaf(rule) {
|
|
3242
|
+
if (typeof CSSStyleRule !== "undefined" && rule instanceof CSSStyleRule) {
|
|
3243
|
+
return true
|
|
3244
|
+
}
|
|
3245
|
+
if (
|
|
3246
|
+
rule &&
|
|
3247
|
+
(rule.type === 1 || rule.type === 6 || rule.type === 7 || rule.type === 8)
|
|
3248
|
+
) {
|
|
3249
|
+
return true
|
|
3250
|
+
}
|
|
3251
|
+
return !!(rule && rule.style)
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
var GROUPING_AT_KEYWORDS = {
|
|
3255
|
+
layer: true,
|
|
3256
|
+
supports: true,
|
|
3257
|
+
container: true,
|
|
3258
|
+
scope: true,
|
|
3259
|
+
"starting-style": true,
|
|
3260
|
+
document: true,
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
/** Positive test for "safe to recurse into and rebuild": only at-rules
|
|
3264
|
+
* whose sole job is grouping other rules under a condition/namespace
|
|
3265
|
+
* (`@layer`, `@supports`, `@container`, `@scope`, `@starting-style`,
|
|
3266
|
+
* `@document`), and only when they actually carry nested rules — a
|
|
3267
|
+
* `@layer a, b;` statement form has no `cssRules` and falls through
|
|
3268
|
+
* to the verbatim leaf branch instead. Everything else defaults to
|
|
3269
|
+
* verbatim, rather than recursing by default, precisely because
|
|
3270
|
+
* `CSSStyleRule` also exposes `cssRules` (see `isStyleLikeLeaf`). */
|
|
3271
|
+
function isGroupingRule(rule) {
|
|
3272
|
+
if (!rule || !rule.cssRules) return false
|
|
3273
|
+
if (typeof CSSStyleRule !== "undefined" && rule instanceof CSSStyleRule) {
|
|
3274
|
+
return false
|
|
3275
|
+
}
|
|
3276
|
+
return !!GROUPING_AT_KEYWORDS[ruleAtKeyword(rule)]
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
/** The rule's header text up to (not including) its opening brace,
|
|
3280
|
+
* e.g. `@layer utilities ` or `@supports (display: grid) `. Used to
|
|
3281
|
+
* rebuild a grouping at-rule around its flattened children. */
|
|
3282
|
+
function headerTextOf(rule) {
|
|
3283
|
+
var text = rule && rule.cssText ? rule.cssText : ""
|
|
3284
|
+
var braceIndex = text.indexOf("{")
|
|
3285
|
+
return braceIndex === -1 ? text : text.slice(0, braceIndex)
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
/** Renders a node produced by `flattenStyleRules` back into CSS text.
|
|
3289
|
+
* A "slot" defers to `pieces[index]`, which a font-face/cross-origin
|
|
3290
|
+
* inlining job may still be mutating when this runs — see
|
|
3291
|
+
* `collectCaptureCss`'s `CSS_INLINE_TIMEOUT_MS` race. */
|
|
3292
|
+
function renderNode(node, pieces) {
|
|
3293
|
+
if (typeof node === "string") return node
|
|
3294
|
+
if (node.kind === "slot") return pieces[node.index]
|
|
3295
|
+
if (node.kind === "group") {
|
|
3296
|
+
return node.header + "{" + renderNodes(node.children, pieces) + "}"
|
|
3297
|
+
}
|
|
3298
|
+
return ""
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function renderNodes(nodes, pieces) {
|
|
3302
|
+
var texts = []
|
|
3303
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
3304
|
+
texts.push(renderNode(nodes[i], pieces))
|
|
3305
|
+
}
|
|
3306
|
+
return texts.join("\n")
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
/**
|
|
3310
|
+
* Flattens `rules` into a tree of plain-string leaves, font-face "slot"
|
|
3311
|
+
* placeholders (resolved later by URL-inlining — see `pieces` in
|
|
3312
|
+
* `collectCaptureCss`), and group nodes that preserve a non-media
|
|
3313
|
+
* at-rule's own semantics while still flattening any `@media` nested
|
|
3314
|
+
* inside it. This last case matters in practice: Tailwind v4 emits
|
|
3315
|
+
* responsive utilities as `@layer utilities { @media (min-width: ...)
|
|
3316
|
+
* { ... } }`, so a `@layer`/`@supports`/`@container` wrapper has to be
|
|
3317
|
+
* rebuilt around its recursed children rather than pushed as opaque
|
|
3318
|
+
* `cssText` — otherwise the nested `@media` inside it would still be
|
|
3319
|
+
* evaluated against the tiny rasterized SVG instead of the live
|
|
3320
|
+
* viewport.
|
|
3321
|
+
*
|
|
3322
|
+
* In a rasterized SVG, `@media` is evaluated against the SVG image's
|
|
3323
|
+
* own (tiny) intrinsic size, not the live viewport — so a non-matching
|
|
3324
|
+
* query is dropped up front and a matching one is inlined (unwrapped),
|
|
3325
|
+
* using the live `window.matchMedia` as the source of truth. `@import`
|
|
3326
|
+
* is followed into its target sheet the same way; a cross-origin
|
|
3327
|
+
* import throws on `.styleSheet.cssRules` and is dropped, same as any
|
|
3328
|
+
* other unreachable cross-origin CSS. A rule with no `cssRules` at all
|
|
3329
|
+
* (e.g. a plain style rule, or `@layer a, b;`'s statement form) is
|
|
3330
|
+
* pushed as a plain string leaf.
|
|
3331
|
+
*/
|
|
3332
|
+
function flattenStyleRules(rules, sheetHref, fetchCache, enhancementJobs, pieces) {
|
|
3333
|
+
var nodes = []
|
|
3334
|
+
if (!rules) return nodes
|
|
3335
|
+
for (var i = 0; i < rules.length; i++) {
|
|
3336
|
+
;(function (rule) {
|
|
3337
|
+
if (isMediaRule(rule)) {
|
|
3338
|
+
var conditionText =
|
|
3339
|
+
rule.conditionText || (rule.media && rule.media.mediaText) || ""
|
|
3340
|
+
var matches = true
|
|
3341
|
+
try {
|
|
3342
|
+
matches = window.matchMedia(conditionText).matches
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
matches = true
|
|
3345
|
+
}
|
|
3346
|
+
if (matches) {
|
|
3347
|
+
var mediaChildren = flattenStyleRules(
|
|
3348
|
+
rule.cssRules,
|
|
3349
|
+
sheetHref,
|
|
3350
|
+
fetchCache,
|
|
3351
|
+
enhancementJobs,
|
|
3352
|
+
pieces
|
|
3353
|
+
)
|
|
3354
|
+
for (var m = 0; m < mediaChildren.length; m++) {
|
|
3355
|
+
nodes.push(mediaChildren[m])
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
} else if (isImportRule(rule)) {
|
|
3359
|
+
try {
|
|
3360
|
+
var importedChildren = flattenStyleRules(
|
|
3361
|
+
rule.styleSheet.cssRules,
|
|
3362
|
+
rule.href || sheetHref,
|
|
3363
|
+
fetchCache,
|
|
3364
|
+
enhancementJobs,
|
|
3365
|
+
pieces
|
|
3366
|
+
)
|
|
3367
|
+
for (var k = 0; k < importedChildren.length; k++) {
|
|
3368
|
+
nodes.push(importedChildren[k])
|
|
3369
|
+
}
|
|
3370
|
+
} catch (error) {
|
|
3371
|
+
// cross-origin @import — best-effort, same as other
|
|
3372
|
+
// unreachable cross-origin CSS.
|
|
3373
|
+
}
|
|
3374
|
+
} else if (isFontFaceRule(rule)) {
|
|
3375
|
+
var index = pieces.length
|
|
3376
|
+
pieces.push(rule.cssText)
|
|
3377
|
+
enhancementJobs.push(
|
|
3378
|
+
inlineCssUrls(
|
|
3379
|
+
rule.cssText,
|
|
3380
|
+
sheetHref || document.baseURI,
|
|
3381
|
+
fetchCache
|
|
3382
|
+
).then(function (text) {
|
|
3383
|
+
pieces[index] = text
|
|
3384
|
+
})
|
|
3385
|
+
)
|
|
3386
|
+
nodes.push({ kind: "slot", index: index })
|
|
3387
|
+
} else if (isStyleLikeLeaf(rule)) {
|
|
3388
|
+
nodes.push(rule.cssText)
|
|
3389
|
+
} else if (isGroupingRule(rule)) {
|
|
3390
|
+
var groupChildren = flattenStyleRules(
|
|
3391
|
+
rule.cssRules,
|
|
3392
|
+
sheetHref,
|
|
3393
|
+
fetchCache,
|
|
3394
|
+
enhancementJobs,
|
|
3395
|
+
pieces
|
|
3396
|
+
)
|
|
3397
|
+
nodes.push({
|
|
3398
|
+
kind: "group",
|
|
3399
|
+
header: headerTextOf(rule),
|
|
3400
|
+
children: groupChildren,
|
|
3401
|
+
})
|
|
3402
|
+
} else {
|
|
3403
|
+
nodes.push(rule.cssText)
|
|
3404
|
+
}
|
|
3405
|
+
})(rules[i])
|
|
3406
|
+
}
|
|
3407
|
+
return nodes
|
|
3091
3408
|
}
|
|
3092
3409
|
|
|
3093
3410
|
function fetchAsDataUrl(url) {
|
|
@@ -3108,6 +3425,168 @@
|
|
|
3108
3425
|
})
|
|
3109
3426
|
}
|
|
3110
3427
|
|
|
3428
|
+
function fetchAsDataUrlCors(url) {
|
|
3429
|
+
return fetch(url, { mode: "cors" })
|
|
3430
|
+
.then(function (response) {
|
|
3431
|
+
if (!response.ok) throw new Error("bad status")
|
|
3432
|
+
return response.blob()
|
|
3433
|
+
})
|
|
3434
|
+
.then(function (blob) {
|
|
3435
|
+
return new Promise(function (resolve, reject) {
|
|
3436
|
+
var reader = new FileReader()
|
|
3437
|
+
reader.onload = function () {
|
|
3438
|
+
resolve(String(reader.result))
|
|
3439
|
+
}
|
|
3440
|
+
reader.onerror = reject
|
|
3441
|
+
reader.readAsDataURL(blob)
|
|
3442
|
+
})
|
|
3443
|
+
})
|
|
3444
|
+
.catch(function () {
|
|
3445
|
+
return null
|
|
3446
|
+
})
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
function collectCssUrls(cssText) {
|
|
3450
|
+
var urls = []
|
|
3451
|
+
var seen = {}
|
|
3452
|
+
var match
|
|
3453
|
+
CSS_URL_RE.lastIndex = 0
|
|
3454
|
+
while ((match = CSS_URL_RE.exec(cssText))) {
|
|
3455
|
+
var raw = match[2]
|
|
3456
|
+
if (!raw || raw.indexOf("data:") === 0 || seen[raw]) continue
|
|
3457
|
+
seen[raw] = true
|
|
3458
|
+
urls.push(raw)
|
|
3459
|
+
}
|
|
3460
|
+
return urls
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
/** Replaces every `url(...)` in `cssText` with a `data:` URL, fetched
|
|
3464
|
+
* cross-origin and deduped via `fetchCache` (shared across calls so
|
|
3465
|
+
* the same font file is only ever fetched once). A URL that fails to
|
|
3466
|
+
* resolve or fetch is left as-is rather than aborting the rest. */
|
|
3467
|
+
function inlineCssUrls(cssText, baseHref, fetchCache) {
|
|
3468
|
+
var urls = collectCssUrls(cssText)
|
|
3469
|
+
if (!urls.length) return Promise.resolve(cssText)
|
|
3470
|
+
|
|
3471
|
+
var resolved = {}
|
|
3472
|
+
var jobs = []
|
|
3473
|
+
for (var i = 0; i < urls.length; i++) {
|
|
3474
|
+
;(function (raw) {
|
|
3475
|
+
var absolute
|
|
3476
|
+
try {
|
|
3477
|
+
absolute = new URL(raw, baseHref).href
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
return
|
|
3480
|
+
}
|
|
3481
|
+
if (!fetchCache[absolute]) {
|
|
3482
|
+
fetchCache[absolute] = fetchAsDataUrlCors(absolute)
|
|
3483
|
+
}
|
|
3484
|
+
jobs.push(
|
|
3485
|
+
fetchCache[absolute].then(function (dataUrl) {
|
|
3486
|
+
resolved[absolute] = dataUrl
|
|
3487
|
+
})
|
|
3488
|
+
)
|
|
3489
|
+
})(urls[i])
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
return Promise.all(jobs).then(function () {
|
|
3493
|
+
return cssText.replace(CSS_URL_RE, function (full, quote, raw) {
|
|
3494
|
+
if (!raw || raw.indexOf("data:") === 0) return full
|
|
3495
|
+
var absolute
|
|
3496
|
+
try {
|
|
3497
|
+
absolute = new URL(raw, baseHref).href
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
return full
|
|
3500
|
+
}
|
|
3501
|
+
var dataUrl = resolved[absolute]
|
|
3502
|
+
return dataUrl ? "url(" + dataUrl + ")" : full
|
|
3503
|
+
})
|
|
3504
|
+
})
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/**
|
|
3508
|
+
* CSS text for the capture clone: same-origin sheets have their
|
|
3509
|
+
* `@media` flattened against the LIVE viewport (see `flattenStyleRules`)
|
|
3510
|
+
* and `:root` rewritten to `html`, because inside the rasterized SVG
|
|
3511
|
+
* the root element is the `<svg>`, not `<html>`. Cross-origin sheets
|
|
3512
|
+
* throw on `cssRules`, so their raw text is fetched instead and every
|
|
3513
|
+
* `url(...)` inlined as a `data:` URL (same-origin `@font-face` rules
|
|
3514
|
+
* get the same treatment); this fetching work is best-effort and
|
|
3515
|
+
* bounded by `CSS_INLINE_TIMEOUT_MS` so a slow or unreachable font
|
|
3516
|
+
* never delays the capture — rules still pending when the budget runs
|
|
3517
|
+
* out simply keep their original (uninlined) `url(...)`.
|
|
3518
|
+
*/
|
|
3519
|
+
function collectCaptureCss() {
|
|
3520
|
+
var fetchCache = {}
|
|
3521
|
+
var pieces = []
|
|
3522
|
+
var enhancementJobs = []
|
|
3523
|
+
var topNodes = []
|
|
3524
|
+
|
|
3525
|
+
for (var i = 0; i < document.styleSheets.length; i++) {
|
|
3526
|
+
;(function (sheet) {
|
|
3527
|
+
var rules = null
|
|
3528
|
+
try {
|
|
3529
|
+
rules = sheet.cssRules
|
|
3530
|
+
} catch (error) {
|
|
3531
|
+
rules = null
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
if (rules) {
|
|
3535
|
+
var nodes = flattenStyleRules(
|
|
3536
|
+
rules,
|
|
3537
|
+
sheet.href,
|
|
3538
|
+
fetchCache,
|
|
3539
|
+
enhancementJobs,
|
|
3540
|
+
pieces
|
|
3541
|
+
)
|
|
3542
|
+
for (var n = 0; n < nodes.length; n++) topNodes.push(nodes[n])
|
|
3543
|
+
return
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
if (!sheet.href) return
|
|
3547
|
+
|
|
3548
|
+
var index = pieces.length
|
|
3549
|
+
pieces.push("")
|
|
3550
|
+
topNodes.push({ kind: "slot", index: index })
|
|
3551
|
+
enhancementJobs.push(
|
|
3552
|
+
fetch(sheet.href, { mode: "cors" })
|
|
3553
|
+
.then(function (response) {
|
|
3554
|
+
if (!response.ok) throw new Error("bad status")
|
|
3555
|
+
return response.text()
|
|
3556
|
+
})
|
|
3557
|
+
.then(function (text) {
|
|
3558
|
+
return inlineCssUrls(text, sheet.href, fetchCache)
|
|
3559
|
+
})
|
|
3560
|
+
.then(function (text) {
|
|
3561
|
+
// Best-effort only: this raw fetched text is not run back
|
|
3562
|
+
// through flattenStyleRules, so any @media/@layer nesting
|
|
3563
|
+
// inside a blocked cross-origin sheet is left unflattened.
|
|
3564
|
+
pieces[index] = text
|
|
3565
|
+
})
|
|
3566
|
+
.catch(function () {
|
|
3567
|
+
// cross-origin sheet unreachable — left blank, same as
|
|
3568
|
+
// the previous best-effort behavior.
|
|
3569
|
+
})
|
|
3570
|
+
)
|
|
3571
|
+
})(document.styleSheets[i])
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
return withTimeout(
|
|
3575
|
+
Promise.all(enhancementJobs),
|
|
3576
|
+
CSS_INLINE_TIMEOUT_MS,
|
|
3577
|
+
null
|
|
3578
|
+
).then(function () {
|
|
3579
|
+
return renderNodes(topNodes, pieces).replace(/:root\b/g, "html")
|
|
3580
|
+
})
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// Test hook only — lets inspector.annotation.test.ts assert on the
|
|
3584
|
+
// collected CSS text directly, since the full capture pipeline needs a
|
|
3585
|
+
// browser Image decoder no test DOM provides.
|
|
3586
|
+
if (window.__ARCHIE_INSPECTOR__) {
|
|
3587
|
+
window.__ARCHIE_INSPECTOR__.collectCaptureCss = collectCaptureCss
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3111
3590
|
/** Rasterizing an SVG cannot fetch subresources, so same-origin <img>
|
|
3112
3591
|
* sources are inlined first. Failures leave the image blank rather
|
|
3113
3592
|
* than aborting the whole capture. */
|
|
@@ -3141,15 +3620,22 @@
|
|
|
3141
3620
|
|
|
3142
3621
|
/**
|
|
3143
3622
|
* Rasterizes the CURRENT viewport into a PNG data URL via an
|
|
3144
|
-
* `<svg><foreignObject>` round-trip. Best-effort by construction:
|
|
3145
|
-
*
|
|
3146
|
-
*
|
|
3147
|
-
* the drawing
|
|
3623
|
+
* `<svg><foreignObject>` round-trip. Best-effort by construction:
|
|
3624
|
+
* cross-origin images and <canvas> pixels do not survive it, and web
|
|
3625
|
+
* fonts survive only within `CSS_INLINE_TIMEOUT_MS`. The host composes
|
|
3626
|
+
* the result with the user's drawing, and falls back to the drawing
|
|
3627
|
+
* alone when this rejects.
|
|
3148
3628
|
*/
|
|
3149
3629
|
function captureViewportDataUrl(pixelRatio) {
|
|
3150
3630
|
var width = window.innerWidth
|
|
3151
3631
|
var height = window.innerHeight
|
|
3152
3632
|
var ratio = Math.min(Math.max(Number(pixelRatio) || 1, 1), 2)
|
|
3633
|
+
// The rasterized SVG's root element is the `<svg>` itself, not
|
|
3634
|
+
// `<html>`, so `rem` would otherwise resolve against the UA default
|
|
3635
|
+
// instead of the live page's root font-size.
|
|
3636
|
+
var rootFontSize = window.getComputedStyle(
|
|
3637
|
+
document.documentElement
|
|
3638
|
+
).fontSize
|
|
3153
3639
|
|
|
3154
3640
|
var clone = document.documentElement.cloneNode(true)
|
|
3155
3641
|
var strip = clone.querySelectorAll(
|
|
@@ -3159,75 +3645,79 @@
|
|
|
3159
3645
|
if (strip[i].parentNode) strip[i].parentNode.removeChild(strip[i])
|
|
3160
3646
|
}
|
|
3161
3647
|
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
settled =
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
var canvas = document.createElement("canvas")
|
|
3213
|
-
canvas.width = Math.round(width * ratio)
|
|
3214
|
-
canvas.height = Math.round(height * ratio)
|
|
3215
|
-
var context = canvas.getContext("2d")
|
|
3216
|
-
context.fillStyle = "#ffffff"
|
|
3217
|
-
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
3218
|
-
context.drawImage(image, 0, 0, canvas.width, canvas.height)
|
|
3219
|
-
finish(resolve, canvas.toDataURL("image/png"))
|
|
3220
|
-
} catch (error) {
|
|
3221
|
-
finish(reject, error)
|
|
3648
|
+
return Promise.all([collectCaptureCss(), inlineImages(clone)]).then(
|
|
3649
|
+
function (results) {
|
|
3650
|
+
var styleEl = document.createElement("style")
|
|
3651
|
+
styleEl.textContent = results[0]
|
|
3652
|
+
var head = clone.querySelector("head")
|
|
3653
|
+
if (head) head.appendChild(styleEl)
|
|
3654
|
+
else clone.insertBefore(styleEl, clone.firstChild)
|
|
3655
|
+
|
|
3656
|
+
var markup = new XMLSerializer().serializeToString(clone)
|
|
3657
|
+
var svg =
|
|
3658
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
|
|
3659
|
+
width +
|
|
3660
|
+
'" height="' +
|
|
3661
|
+
height +
|
|
3662
|
+
'" style="font-size:' +
|
|
3663
|
+
rootFontSize +
|
|
3664
|
+
'">' +
|
|
3665
|
+
'<foreignObject x="0" y="0" width="' +
|
|
3666
|
+
width +
|
|
3667
|
+
'" height="' +
|
|
3668
|
+
height +
|
|
3669
|
+
'">' +
|
|
3670
|
+
'<div xmlns="http://www.w3.org/1999/xhtml" style="width:' +
|
|
3671
|
+
width +
|
|
3672
|
+
"px;transform:translate(" +
|
|
3673
|
+
-window.scrollX +
|
|
3674
|
+
"px," +
|
|
3675
|
+
-window.scrollY +
|
|
3676
|
+
'px)">' +
|
|
3677
|
+
markup +
|
|
3678
|
+
"</div></foreignObject></svg>"
|
|
3679
|
+
|
|
3680
|
+
return new Promise(function (resolve, reject) {
|
|
3681
|
+
var image = new Image()
|
|
3682
|
+
// An SVG the browser refuses to decode can fire NEITHER onload nor
|
|
3683
|
+
// onerror (malformed serialization, blocked subresource). Without
|
|
3684
|
+
// this guard the host would sit on the request until its own
|
|
3685
|
+
// timeout with no diagnostic; the deadline turns that into an
|
|
3686
|
+
// explicit VIEWPORT_CAPTURE_FAILED.
|
|
3687
|
+
var settled = false
|
|
3688
|
+
var deadline = setTimeout(function () {
|
|
3689
|
+
if (settled) return
|
|
3690
|
+
settled = true
|
|
3691
|
+
reject(new Error("svg rasterization timed out"))
|
|
3692
|
+
}, ANNOTATION_CAPTURE_TIMEOUT_MS)
|
|
3693
|
+
var finish = function (fn, value) {
|
|
3694
|
+
if (settled) return
|
|
3695
|
+
settled = true
|
|
3696
|
+
clearTimeout(deadline)
|
|
3697
|
+
fn(value)
|
|
3222
3698
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3699
|
+
image.onload = function () {
|
|
3700
|
+
try {
|
|
3701
|
+
var canvas = document.createElement("canvas")
|
|
3702
|
+
canvas.width = Math.round(width * ratio)
|
|
3703
|
+
canvas.height = Math.round(height * ratio)
|
|
3704
|
+
var context = canvas.getContext("2d")
|
|
3705
|
+
context.fillStyle = "#ffffff"
|
|
3706
|
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
3707
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height)
|
|
3708
|
+
finish(resolve, canvas.toDataURL("image/png"))
|
|
3709
|
+
} catch (error) {
|
|
3710
|
+
finish(reject, error)
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
image.onerror = function () {
|
|
3714
|
+
finish(reject, new Error("svg rasterization failed"))
|
|
3715
|
+
}
|
|
3716
|
+
image.src =
|
|
3717
|
+
"data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
|
|
3718
|
+
})
|
|
3719
|
+
}
|
|
3720
|
+
)
|
|
3231
3721
|
}
|
|
3232
3722
|
|
|
3233
3723
|
function extractMetadata(element) {
|
|
@@ -3279,6 +3769,12 @@
|
|
|
3279
3769
|
// this script's version. The slot string is opaque here; validating it
|
|
3280
3770
|
// against kit-runtime's `slot-vocabulary` is the host's job.
|
|
3281
3771
|
editorMeta: editorMeta ? editorMeta.meta : null,
|
|
3772
|
+
// vid-identity migration — `vid`/`nodeMeta` derived from the SAME
|
|
3773
|
+
// meta as `editorMeta` above (`resolveNodeVid`/`resolveNodeMeta`,
|
|
3774
|
+
// mirroring `node-key.ts`/`fiber-bridge.ts`'s `getNodeMeta`). Both
|
|
3775
|
+
// are null for a legacy node with no `data-vid`.
|
|
3776
|
+
vid: resolveNodeVid(element, editorMeta ? editorMeta.meta : null),
|
|
3777
|
+
nodeMeta: resolveNodeMeta(element, editorMeta ? editorMeta.meta : null),
|
|
3282
3778
|
// ARCH-883 F2 — the two things the host cannot derive from
|
|
3283
3779
|
// `editorMeta` alone (SDD §2.9):
|
|
3284
3780
|
//
|
|
@@ -3817,6 +4313,45 @@
|
|
|
3817
4313
|
return
|
|
3818
4314
|
}
|
|
3819
4315
|
|
|
4316
|
+
// `data-vid` identity migration — the DnD runtime's `drag-started`/
|
|
4317
|
+
// `drag-refused` events (`useDragLifecycle.ts`, `host-bridge.ts`)
|
|
4318
|
+
// otherwise die inside the iframe: nothing relayed them to the host.
|
|
4319
|
+
// Reuses `refuseNode` for `REFUSED` so the wire shape is the SAME
|
|
4320
|
+
// one produced by every other refusal path in this file (never a
|
|
4321
|
+
// second, drifting literal) and validates against
|
|
4322
|
+
// `RuntimeMessageSchema` (`@8base-archie/editor-protocol`).
|
|
4323
|
+
if (detail.type === "drag-refused") {
|
|
4324
|
+
refuseNode(detail.code, detail.cause, detail.vid)
|
|
4325
|
+
return
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
if (detail.type === "drag-started") {
|
|
4329
|
+
// `DragStartMessageSchema.targetVid` is REQUIRED (not optional) —
|
|
4330
|
+
// unlike `REFUSED.vid`. A legacy (no `data-vid`) page's dragged
|
|
4331
|
+
// node has no vid to report, so the message cannot be made to
|
|
4332
|
+
// satisfy the schema; skip the relay rather than send a shape
|
|
4333
|
+
// that fails validation on the host. `droppable` is filtered to
|
|
4334
|
+
// well-formed vids for the same reason — the projection's own ids
|
|
4335
|
+
// can be legacy `file:line:col:componentName` strings, which the
|
|
4336
|
+
// schema's `z.array(Vid)` would reject.
|
|
4337
|
+
if (typeof detail.targetVid !== "string" || !VID_PATTERN.test(detail.targetVid)) {
|
|
4338
|
+
veTrace("runtime-event:drag-started:skipped-no-vid", {})
|
|
4339
|
+
return
|
|
4340
|
+
}
|
|
4341
|
+
var droppableVids = Array.isArray(detail.droppable)
|
|
4342
|
+
? detail.droppable.filter(function (id) {
|
|
4343
|
+
return typeof id === "string" && VID_PATTERN.test(id)
|
|
4344
|
+
})
|
|
4345
|
+
: []
|
|
4346
|
+
postMessageToParent({
|
|
4347
|
+
source: "archie-devtools",
|
|
4348
|
+
type: "DRAG_START",
|
|
4349
|
+
targetVid: detail.targetVid,
|
|
4350
|
+
droppable: droppableVids,
|
|
4351
|
+
})
|
|
4352
|
+
return
|
|
4353
|
+
}
|
|
4354
|
+
|
|
3820
4355
|
// ARCH-1367 F6 — attach the drop container's editor meta chain to a
|
|
3821
4356
|
// container descriptor ({selector, nodeId, …}). Shared by the
|
|
3822
4357
|
// `element-inserted` relay AND the `arrange-session-changed` relay:
|