@tinkerlandai/indiana-iam 0.3.0 → 0.5.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 +81 -5
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +78 -36
- package/package.json +12 -14
- package/src/analyze.mjs +968 -146
- package/src/anchors.mjs +2 -0
- package/src/build-options.d.ts +1 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +56 -8
- package/src/build.mjs +3 -3
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/document-material.mjs +180 -0
- package/src/dom-payload-json.mjs +2 -2
- package/src/dom-payload.mjs +3 -3
- package/src/framework-routes.mjs +4 -3
- package/src/generate.mjs +107 -25
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +2 -1
- package/src/index.mjs +1 -1
- package/src/json.mjs +2 -3
- package/src/library-entry.mjs +9 -2
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/node-hashes.mjs +10 -9
- package/src/render-index.mjs +7 -7
- package/src/route-composition.mjs +5 -5
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +19 -7
- package/src/routing.mjs +7 -6
- package/src/schema-definition.mjs +13 -15
- package/src/selected-manifest.mjs +12 -0
- package/src/source-aliases.mjs +34 -0
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +53 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-css-modules.mjs +79 -0
- package/src/source-environment.mjs +26 -0
- package/src/source-files.mjs +8 -7
- package/src/source-flowbite.mjs +60 -0
- package/src/source-i18next.mjs +22 -0
- package/src/source-mantine.mjs +61 -0
- package/src/source-mdx.mjs +92 -0
- package/src/source-media.mjs +30 -0
- package/src/source-query-evidence.mjs +64 -0
- package/src/source-redirects.mjs +388 -0
- package/src/source-styles.mjs +9 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +102 -14
- package/src/svg-preview.mjs +23 -0
- package/src/tab-scenes.mjs +214 -0
- package/src/tanstack-routes.mjs +1 -1
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +634 -0
- package/src/validate.mjs +5 -0
- package/src/viewer-index.mjs +49 -0
package/README.md
CHANGED
|
@@ -47,6 +47,13 @@ not import merchant modules to execute them. Source enumeration and optional Git
|
|
|
47
47
|
the package starts no external commands and works without a Git executable. Developers' explicitly configured
|
|
48
48
|
framework callbacks remain part of their normal build.
|
|
49
49
|
|
|
50
|
+
Build integrations retain local compiled CSS when a stylesheet contains an external `@import`: the import is omitted
|
|
51
|
+
without making a network request and the manifest records the missing external styles. Inline SVG resources are
|
|
52
|
+
omitted by default during framework builds, with a coverage gap, so an unsupported form icon does not discard the
|
|
53
|
+
stylesheet. Set `omitSvgResources: false` to reject those resources and use the source-style fallback instead.
|
|
54
|
+
Standalone generation keeps its explicit `--omit-svg-resources` option. Compiled previews preserve hidden source
|
|
55
|
+
branches even when application CSS uses `display: flex` or `display: grid`.
|
|
56
|
+
|
|
50
57
|
## Vite, React Router and TanStack Router
|
|
51
58
|
|
|
52
59
|
```js
|
|
@@ -110,15 +117,56 @@ optional command for explicit source annotations. Next and Vite do not need that
|
|
|
110
117
|
Supported source: Next.js App Router and Pages Router, React Router / single-root React apps built with Vite,
|
|
111
118
|
and statically generated TanStack file-route trees registered with `createRouter({ routeTree })`.
|
|
112
119
|
React Router supports imported static path constants, inline `Component` functions and source-proven lazy import forwarding.
|
|
120
|
+
Data-router `route.lazy` objects support a static module's named `Component`, or a source-proven mapper that captures
|
|
121
|
+
the component export. Mapper factories, imports and business loaders are never executed. Unknown overrides remain gaps.
|
|
122
|
+
Markdown/MDX pages, layouts and local imports retain original routes and source coordinates.
|
|
123
|
+
The pinned Markdown/MDX parsers project headings, lists, GFM tables/tasks, code, links, images and bounded source JSX.
|
|
124
|
+
Merchant MDX compilers/plugins, ESM side effects, arbitrary expressions and active HTML are never executed.
|
|
125
|
+
Generated Markdown markup stays preview-only; custom MDX component mappings and plugin transforms remain gaps.
|
|
126
|
+
CSS Modules retain source-literal default imports, deterministic local preview class names,
|
|
127
|
+
scoped selectors and static declarations, including standard media/supports/container rules.
|
|
128
|
+
Global selectors, resource loads, composition, nested preprocessing and animation definitions stay explicit gaps;
|
|
129
|
+
merchant PostCSS plugins and configuration are never executed. Compiled CSS remains a separate build input.
|
|
130
|
+
Mantine core forms, headings, containers, tables, images and controlled Modal/Drawer visibility have an inert
|
|
131
|
+
semantic HTML profile. Source labels, ordinary form constraints and safe default values are retained;
|
|
132
|
+
password values stay redacted. Theme resolution, responsive object props, portal positioning, focus handling
|
|
133
|
+
and library callbacks remain unobserved. Boolean `useDisclosure` without lifecycle callbacks, controlled Tabs,
|
|
134
|
+
Drawer/Modal close controls and native Accordion folding have bounded local previews. This profile does not
|
|
135
|
+
import Mantine or run its hooks.
|
|
136
|
+
Async source helpers and TanStack queries can expose source-declared literal fixtures, including a source-proven
|
|
137
|
+
timer-only delay. IAM interprets their AST without executing functions, awaiting timers or invoking query callbacks.
|
|
138
|
+
Safe-integer locale formatting uses deterministic English grouping in the source preview.
|
|
139
|
+
Declared `initialData` retains its source initial state; loading/error/network outcomes otherwise stay unknown.
|
|
140
|
+
Only the known frontend `services/api` source path is included; framework API routes, server trees and credentials
|
|
141
|
+
stay outside the scan. Remote data, mutations, impure helpers and ambiguous bindings remain unobserved.
|
|
113
142
|
TanStack reads the registered generated tree, including pathless layouts, index routes and simple `$param` paths;
|
|
114
143
|
custom history/basepath and complex parameter patterns require separate adapters. Loaders, guards, retry/reload behavior
|
|
115
144
|
and runtime data remain unobserved. Unsupported optional React paths are isolated as gaps.
|
|
116
|
-
|
|
145
|
+
React Hook Form Controller callbacks retain their source JSX, including first-party wrappers; field values, validation
|
|
146
|
+
and submissions are not executed. Source-proven Radix overlays and React Transition Group visibility retain their
|
|
147
|
+
initial state. Supported Radix triggers and close controls simulate local state only; unknown controlled states stay
|
|
148
|
+
closed and unobserved. These adapters do not implement portal positioning, focus management or business callbacks.
|
|
149
|
+
Chart.js canvas wrappers and Recharts line/bar/area/pie declarations project bounded literal series as inert SVG.
|
|
150
|
+
Chart modules, effects, formatters and callbacks are not executed. Random/remote/oversized series show an explicit
|
|
151
|
+
unobserved-data placeholder; animation, library layout and tooltips remain outside this source projection.
|
|
152
|
+
Build hooks package only raster images referenced by projected JSX: relative source imports and public build URLs.
|
|
153
|
+
They never crawl unrelated assets or fetch URLs. Files must remain inside the root without symlinks; referenced
|
|
154
|
+
source image bytes participate in release identity. The existing 1 MiB/file, 6 MiB input and 128-asset bounds apply. Referenced rasters over 64 KiB use the bundled sharp 0.35.5 preview profile: auto-orient, fit inside 800×800, WebP quality 90, metadata stripped. Source bytes stay independently hashed; this lossy preview is explicitly reported.
|
|
155
|
+
Standalone generation keeps images unpackaged unless explicit public inputs are supplied.
|
|
156
|
+
Source-proven `useSyncExternalStore` matchMedia getters retain both typed viewport branches and follow the preview
|
|
157
|
+
viewport. Subscriptions are not run. UI preference cookies with an explicit source SSR guard use an empty-cookie
|
|
158
|
+
preview; auth/session cookies and arbitrary stores stay unobserved. Pure lazy local-state initializers are interpreted
|
|
159
|
+
under the same bounded rules; merchant callbacks and browser storage are never executed or read.
|
|
160
|
+
IAM has no configured page, node or total artifact byte limit. The exporter retains every supported route.
|
|
117
161
|
Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
|
|
118
162
|
and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
|
|
119
|
-
HTML/DOM hashes and targets. The document hash pins its encoded storage body; decoding is local and
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
HTML/DOM hashes and targets. Route navigation stores the exact interaction-array index instead of repeating its ID; validation checks node, route, destination and parameters. Source files retain their full hashes; component/element records cover reachable route compositions and shared fragments. Text node IDs that follow the original parent/index identity formula are derived locally instead of repeated; explicit exceptions remain stored, and every resolved source-map hash is checked. The document hash pins its encoded storage body; decoding is local and checks the exact declared size and hashes. Exported page HTML is expanded.
|
|
164
|
+
The standalone viewer embeds only its route directory and hash-pinned document references. It reads and expands
|
|
165
|
+
the selected document on demand, cancels superseded reads and retains one expanded page. Derived page files
|
|
166
|
+
come from the complete validated IAM; the complete JSON remains the source of release identity.
|
|
167
|
+
Serve the output directory over loopback HTTP to open `index.html`; browser `file://` restrictions prevent
|
|
168
|
+
on-demand reads of its document files. Serving the exported files does not run the App or its backend.
|
|
169
|
+
Use the matching 0.4 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
|
|
122
170
|
before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
|
|
123
171
|
viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
|
|
124
172
|
The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
|
|
@@ -151,7 +199,35 @@ The Console App map groups pages into areas, shows recorded navigation, and open
|
|
|
151
199
|
opening a page editor. Thumbnails are static source projections. They do not execute authentication, data loaders,
|
|
152
200
|
form submissions or application code.
|
|
153
201
|
|
|
154
|
-
|
|
202
|
+
The optional `routeTree.ui` index records source UI alternatives, controlled Base UI Tabs (including first-party
|
|
203
|
+
wrappers and context), and conditional redirect/return recipes. A recipe follows the actual React Router location
|
|
204
|
+
through a URL query writer to a matching query-consuming `Navigate`; a password input at the destination identifies
|
|
205
|
+
the login UI. The index never infers authentication from a route name or certifies a successful login. Dynamic
|
|
206
|
+
callbacks, unresolved registries and external data remain coverage gaps. Finite first-party component registries
|
|
207
|
+
retain their source labels and scope qualifiers in `ui.regions`; selecting an alternative is a preview assumption.
|
|
208
|
+
Local JSX aliases and invoked JSX render props can preserve page content without executing their modules or callbacks.
|
|
209
|
+
|
|
210
|
+
Unknown branches stay hidden in the admitted document. The shared `previewIamUiDocument` helper can select inert
|
|
211
|
+
source alternatives and typed local state for thumbnail inspection. It returns the selected conditions and source
|
|
212
|
+
locations. Controlled overlays with unknown `open` values stay closed until explicitly selected for inspection.
|
|
213
|
+
These preview choices never change render discriminators, editable targets, the manifest or runtime permissions.
|
|
214
|
+
`iamTabUiScenes` dispatches the recorded scalar Tab transitions to derive before/after preview states and directed
|
|
215
|
+
edges. It includes content controlled outside the Tab root within the same source component or overlay. Every edge
|
|
216
|
+
retains the transition, state writes and source locations. Missing collections remain gaps rather than sample rows.
|
|
217
|
+
Finite Tab-dependent literal collections use shared `ui.fragments` and concrete `ui.collections` mounts. Their
|
|
218
|
+
source element references, item counts, Tab cases and required local values remain pinned; derived rows are inert
|
|
219
|
+
and never become SDK edit targets. The Lucide adapter reads geometry from pinned `lucide-static` 1.34.0 JSON,
|
|
220
|
+
without loading merchant packages. Unknown filter combinations remain unobserved rather than reusing stale rows.
|
|
221
|
+
Finite source-derived Tab classes, search placeholders and accessible labels share `ui.attributeSets`; concrete
|
|
222
|
+
mounts in `ui.attributes` apply only the recorded presentation properties. Fragment class and element dictionaries
|
|
223
|
+
preserve their exact authored values. Repeated graph source locations and instance scope names use lossless
|
|
224
|
+
`sourcePool` / `scopePool` references; source hashes, state identities and per-instance state remain unchanged.
|
|
225
|
+
Unexecuted query status/result branches and unresolved scalar alternatives display explicit unobserved annotations;
|
|
226
|
+
preview does not select a loading, failed or empty result automatically. Literal empty source catalogs remain valid.
|
|
227
|
+
Static thumbnail crops can focus on the page body or a source node using a bounded viewport; they retain authored
|
|
228
|
+
descendants and never introduce scripts, frames or remote resources.
|
|
229
|
+
|
|
230
|
+
Optional static attributes preserve product intent that cannot be established from source dataflow alone:
|
|
155
231
|
|
|
156
232
|
| Attribute | Location | Meaning |
|
|
157
233
|
| -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
@@ -7,9 +7,18 @@
|
|
|
7
7
|
const model = document.querySelector('[data-iam-adapter="' + name + '"]');
|
|
8
8
|
if (model) root.className = model.getAttribute("class") || "";
|
|
9
9
|
}
|
|
10
|
-
let machine = null;
|
|
10
|
+
let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
|
|
11
11
|
const patches = new Map();
|
|
12
12
|
const node = id => [...document.querySelectorAll("[data-iam-node]")].find(e => e.dataset.iamNode === id);
|
|
13
|
+
function viewportValues(graph,routeKey,renderKey){
|
|
14
|
+
const values={};for(const variable of graph.variables??[]){
|
|
15
|
+
if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
|
|
16
|
+
const match=/^@media (\((?:max|min)-width: [1-9]\d{1,3}px\))$/.exec(variable.label);
|
|
17
|
+
if(match)values[variable.variableKey]=matchMedia(match[1]).matches;
|
|
18
|
+
}return values;
|
|
19
|
+
}
|
|
20
|
+
let viewportGraph=null,viewportRoute=null,viewportRender=null;
|
|
21
|
+
addEventListener('resize',()=>{if(machine&&viewportGraph){machine.preview(viewportValues(viewportGraph,viewportRoute,viewportRender));renderState();}});
|
|
13
22
|
function renderState() {
|
|
14
23
|
if (!machine) return;
|
|
15
24
|
for (const binding of machine.bindings()) {
|
|
@@ -19,6 +28,11 @@
|
|
|
19
28
|
else if (binding.property === "text") el.textContent = String(binding.value);
|
|
20
29
|
else el[binding.property] = binding.value;
|
|
21
30
|
}
|
|
31
|
+
const values=machine.snapshot();
|
|
32
|
+
for(const tab of activeTabs){const selected=tab.variableKey?values[tab.variableKey]:tab.initialValue;for(const item of tab.items)node(item.nodeId)?.setAttribute('aria-selected',String(item.value===selected));}
|
|
33
|
+
applySourceAttributes(document,sourceUi,sourceRenderKey,values);
|
|
34
|
+
applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
|
|
35
|
+
markUnobservedData(document,sourceUi,true);
|
|
22
36
|
for (const patch of patches.values()) applyPatch(patch);
|
|
23
37
|
}
|
|
24
38
|
function applyPatch(data) {
|
|
@@ -78,7 +92,10 @@
|
|
|
78
92
|
channel = data.nonce;
|
|
79
93
|
if (data.graph && typeof data.routeKey === "string") {
|
|
80
94
|
machine = createStateMachine(data.graph, data.routeKey, data.renderKey);
|
|
95
|
+
viewportGraph=data.graph;viewportRoute=data.routeKey;viewportRender=data.renderKey;machine.preview(viewportValues(data.graph,data.routeKey,data.renderKey));
|
|
81
96
|
const renderKey = data.renderKey ?? data.graph.routes.find(route => route.routeKey === data.routeKey)?.initialRenderKey;
|
|
97
|
+
sourceUi=data.graph.ui;sourceRenderKey=renderKey;
|
|
98
|
+
activeTabs = (data.graph.ui?.tabs??[]).filter(t=>t.routeKey===data.routeKey&&t.renderKey===renderKey);
|
|
82
99
|
activeTransitions = data.graph.transitions.filter(t => t.routeKey === data.routeKey && t.renderKey === renderKey);
|
|
83
100
|
renderState();
|
|
84
101
|
}
|
|
@@ -76,6 +76,177 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function checkedCollectionFragment(doc, fragment) {
|
|
80
|
+
if (
|
|
81
|
+
typeof fragment?.html !== "string" ||
|
|
82
|
+
fragment.html.length > 262144 ||
|
|
83
|
+
!Array.isArray(fragment.elementKeys) ||
|
|
84
|
+
!Array.isArray(fragment.sourceElements) ||
|
|
85
|
+
!Array.isArray(fragment.classNames) ||
|
|
86
|
+
fragment.sourceElements.length > 2000 ||
|
|
87
|
+
fragment.classNames.length > 128 ||
|
|
88
|
+
fragment.classNames.some((s) => typeof s !== "string" || s.length > 4000) ||
|
|
89
|
+
fragment.elementKeys.some(
|
|
90
|
+
(i) =>
|
|
91
|
+
!Number.isInteger(i) || i < 0 || i >= fragment.sourceElements.length,
|
|
92
|
+
) ||
|
|
93
|
+
fragment.elementKeys.length > 2000
|
|
94
|
+
)
|
|
95
|
+
throw Error("Invalid shared source fragment");
|
|
96
|
+
const template = doc.createElement("template");
|
|
97
|
+
template.innerHTML = fragment.html;
|
|
98
|
+
const nodes = [...template.content.querySelectorAll("*")],
|
|
99
|
+
allowed = new Set(
|
|
100
|
+
"div span p button input svg path circle rect line polyline polygon ellipse g title".split(
|
|
101
|
+
" ",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
if (nodes.length !== fragment.elementKeys.length)
|
|
105
|
+
throw Error("Shared source fragment mapping mismatch");
|
|
106
|
+
for (const node of nodes) {
|
|
107
|
+
if (node.hasAttribute("data-iam-fragment-class")) {
|
|
108
|
+
const raw = node.getAttribute("data-iam-fragment-class"),
|
|
109
|
+
index = Number(raw);
|
|
110
|
+
if (
|
|
111
|
+
String(index) !== raw ||
|
|
112
|
+
!Number.isInteger(index) ||
|
|
113
|
+
index < 0 ||
|
|
114
|
+
index >= fragment.classNames.length
|
|
115
|
+
)
|
|
116
|
+
throw Error("Invalid source fragment class");
|
|
117
|
+
node.setAttribute("class", fragment.classNames[index]);
|
|
118
|
+
node.removeAttribute("data-iam-fragment-class");
|
|
119
|
+
}
|
|
120
|
+
if (!allowed.has(node.tagName.toLowerCase()))
|
|
121
|
+
throw Error("Active shared source fragment element");
|
|
122
|
+
for (const attr of node.attributes)
|
|
123
|
+
if (
|
|
124
|
+
!/^(?:class|title|type|value|placeholder|disabled|hidden|checked|tabindex|width|height|viewBox|viewbox|xmlns|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|cx|cy|r|rx|ry|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-(?:slot|state|active|disabled|pressed))$/.test(
|
|
125
|
+
attr.name,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
throw Error("Active shared source fragment attribute: " + attr.name);
|
|
129
|
+
for (const key of ["fill", "stroke"])
|
|
130
|
+
if (
|
|
131
|
+
node.hasAttribute(key) &&
|
|
132
|
+
!/^(?:none|currentColor|transparent|#[a-f\d]{3,8})$/i.test(
|
|
133
|
+
node.getAttribute(key),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
throw Error("External source fragment paint");
|
|
137
|
+
if (
|
|
138
|
+
node.hasAttribute("xmlns") &&
|
|
139
|
+
node.getAttribute("xmlns") !== "http://www.w3.org/2000/svg"
|
|
140
|
+
)
|
|
141
|
+
throw Error("Invalid source fragment namespace");
|
|
142
|
+
}
|
|
143
|
+
return { content: template.content, nodes };
|
|
144
|
+
}
|
|
145
|
+
function applySourceAttributes(doc, ui, renderKey, values) {
|
|
146
|
+
const sets = new Map((ui?.attributeSets ?? []).map((s) => [s.setKey, s])),
|
|
147
|
+
nodes = new Map(
|
|
148
|
+
[...doc.querySelectorAll("[data-iam-node]")].map((n) => [
|
|
149
|
+
n.getAttribute("data-iam-node"),
|
|
150
|
+
n,
|
|
151
|
+
]),
|
|
152
|
+
);
|
|
153
|
+
for (const row of ui?.attributes ?? []) {
|
|
154
|
+
if (row.renderKey !== renderKey) continue;
|
|
155
|
+
const set = sets.get(row.setKey),
|
|
156
|
+
node = nodes.get(row.nodeId),
|
|
157
|
+
chosen = set?.cases.find((c) => c.value === values[row.variableKey]);
|
|
158
|
+
if (!node || !chosen) continue;
|
|
159
|
+
if (
|
|
160
|
+
!["class", "placeholder", "aria-label"].includes(set.property) ||
|
|
161
|
+
typeof chosen.text !== "string" ||
|
|
162
|
+
chosen.text.length > 4000
|
|
163
|
+
)
|
|
164
|
+
throw Error("Invalid source presentation attribute");
|
|
165
|
+
node.setAttribute(set.property, chosen.text);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function applySourceCollections(doc, ui, renderKey, values, enabled) {
|
|
169
|
+
const choices = [],
|
|
170
|
+
fragments = new Map((ui?.fragments ?? []).map((f) => [f.fragmentKey, f]));
|
|
171
|
+
for (const row of ui?.collections ?? []) {
|
|
172
|
+
if (row.renderKey !== renderKey) continue;
|
|
173
|
+
const root = [...doc.querySelectorAll("[data-iam-node]")].find(
|
|
174
|
+
(n) => n.getAttribute("data-iam-node") === row.nodeId,
|
|
175
|
+
);
|
|
176
|
+
if (!root) continue;
|
|
177
|
+
root.replaceChildren();
|
|
178
|
+
if (!enabled) continue;
|
|
179
|
+
if (root.parentElement?.closest("[hidden]")) continue;
|
|
180
|
+
const selected = row.cases.find((c) => c.value === values[row.variableKey]),
|
|
181
|
+
satisfied = row.requires.every((r) => values[r.variableKey] === r.value);
|
|
182
|
+
root.hidden = false;
|
|
183
|
+
root.setAttribute("inert", "");
|
|
184
|
+
if (!selected || !satisfied) {
|
|
185
|
+
root.textContent = "未观测 · 此筛选组合未重建";
|
|
186
|
+
root.setAttribute("data-iam-observation", "unobserved-collection");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const fragment = fragments.get(selected.fragmentKey);
|
|
190
|
+
if (!fragment) throw Error("Missing source collection fragment");
|
|
191
|
+
const parsed = checkedCollectionFragment(doc, fragment);
|
|
192
|
+
root.replaceChildren(parsed.content);
|
|
193
|
+
parsed.nodes.forEach((n, index) => {
|
|
194
|
+
n.setAttribute("data-iam-preview-only", "true");
|
|
195
|
+
n.setAttribute(
|
|
196
|
+
"data-iam-source-element",
|
|
197
|
+
fragment.sourceElements[fragment.elementKeys[index]],
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
choices.push({
|
|
201
|
+
collectionKey: row.collectionKey,
|
|
202
|
+
value: selected.value,
|
|
203
|
+
itemCount: fragment.itemCount,
|
|
204
|
+
source: row.source,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return choices;
|
|
208
|
+
}
|
|
209
|
+
function markUnobservedData(doc, ui, enabled, selections = new Map()) {
|
|
210
|
+
doc.querySelectorAll("[data-iam-preview-note]").forEach((n) => n.remove());
|
|
211
|
+
const unobserved = [];
|
|
212
|
+
if (!enabled) return unobserved;
|
|
213
|
+
const source = new Map(
|
|
214
|
+
(ui?.conditions ?? []).map((c) => [c.conditionKey, c]),
|
|
215
|
+
);
|
|
216
|
+
for (const root of doc.querySelectorAll(
|
|
217
|
+
'[data-iam-condition][data-iam-alternative="true"]',
|
|
218
|
+
)) {
|
|
219
|
+
const condition = source.get(root.getAttribute("data-iam-condition")),
|
|
220
|
+
id = root.getAttribute("data-iam-node");
|
|
221
|
+
if (!condition?.observation || selections.has(id)) continue;
|
|
222
|
+
const note = doc.createElement("div");
|
|
223
|
+
note.setAttribute("data-iam-preview-note", "unobserved");
|
|
224
|
+
note.className = "iam-observation-note";
|
|
225
|
+
note.setAttribute("role", "status");
|
|
226
|
+
note.textContent =
|
|
227
|
+
condition.observation === "remote" ? "远程数据未观测" : "数据未观测";
|
|
228
|
+
note.title = condition.expression;
|
|
229
|
+
root.before(note);
|
|
230
|
+
if (!note.closest("[hidden]"))
|
|
231
|
+
unobserved.push({
|
|
232
|
+
groupKey: id,
|
|
233
|
+
expression: condition.expression,
|
|
234
|
+
source: condition.source,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
for (const root of doc.querySelectorAll(
|
|
238
|
+
'[data-iam-observation="remote"],[data-iam-observation="unobserved"]',
|
|
239
|
+
)) {
|
|
240
|
+
root.hidden = false;
|
|
241
|
+
root.textContent = "未观测";
|
|
242
|
+
root.classList.add("iam-unobserved-value");
|
|
243
|
+
}
|
|
244
|
+
for (const field of doc.querySelectorAll("[data-iam-input-unobserved]")) {
|
|
245
|
+
field.setAttribute("placeholder", "未观测");
|
|
246
|
+
field.setAttribute("title", "此字段的数据未观测");
|
|
247
|
+
}
|
|
248
|
+
return unobserved;
|
|
249
|
+
}
|
|
79
250
|
/* Indiana UI simulation runtime 0.2.0. No merchant code, requests, authentication or purchases. */
|
|
80
251
|
(() => {
|
|
81
252
|
"use strict";
|
|
@@ -85,9 +256,18 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
85
256
|
const model = document.querySelector('[data-iam-adapter="' + name + '"]');
|
|
86
257
|
if (model) root.className = model.getAttribute("class") || "";
|
|
87
258
|
}
|
|
88
|
-
let machine = null;
|
|
259
|
+
let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
|
|
89
260
|
const patches = new Map();
|
|
90
261
|
const node = id => [...document.querySelectorAll("[data-iam-node]")].find(e => e.dataset.iamNode === id);
|
|
262
|
+
function viewportValues(graph,routeKey,renderKey){
|
|
263
|
+
const values={};for(const variable of graph.variables??[]){
|
|
264
|
+
if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
|
|
265
|
+
const match=/^@media (\((?:max|min)-width: [1-9]\d{1,3}px\))$/.exec(variable.label);
|
|
266
|
+
if(match)values[variable.variableKey]=matchMedia(match[1]).matches;
|
|
267
|
+
}return values;
|
|
268
|
+
}
|
|
269
|
+
let viewportGraph=null,viewportRoute=null,viewportRender=null;
|
|
270
|
+
addEventListener('resize',()=>{if(machine&&viewportGraph){machine.preview(viewportValues(viewportGraph,viewportRoute,viewportRender));renderState();}});
|
|
91
271
|
function renderState() {
|
|
92
272
|
if (!machine) return;
|
|
93
273
|
for (const binding of machine.bindings()) {
|
|
@@ -97,6 +277,11 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
97
277
|
else if (binding.property === "text") el.textContent = String(binding.value);
|
|
98
278
|
else el[binding.property] = binding.value;
|
|
99
279
|
}
|
|
280
|
+
const values=machine.snapshot();
|
|
281
|
+
for(const tab of activeTabs){const selected=tab.variableKey?values[tab.variableKey]:tab.initialValue;for(const item of tab.items)node(item.nodeId)?.setAttribute('aria-selected',String(item.value===selected));}
|
|
282
|
+
applySourceAttributes(document,sourceUi,sourceRenderKey,values);
|
|
283
|
+
applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
|
|
284
|
+
markUnobservedData(document,sourceUi,true);
|
|
100
285
|
for (const patch of patches.values()) applyPatch(patch);
|
|
101
286
|
}
|
|
102
287
|
function applyPatch(data) {
|
|
@@ -156,7 +341,10 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
156
341
|
channel = data.nonce;
|
|
157
342
|
if (data.graph && typeof data.routeKey === "string") {
|
|
158
343
|
machine = createStateMachine(data.graph, data.routeKey, data.renderKey);
|
|
344
|
+
viewportGraph=data.graph;viewportRoute=data.routeKey;viewportRender=data.renderKey;machine.preview(viewportValues(data.graph,data.routeKey,data.renderKey));
|
|
159
345
|
const renderKey = data.renderKey ?? data.graph.routes.find(route => route.routeKey === data.routeKey)?.initialRenderKey;
|
|
346
|
+
sourceUi=data.graph.ui;sourceRenderKey=renderKey;
|
|
347
|
+
activeTabs = (data.graph.ui?.tabs??[]).filter(t=>t.routeKey===data.routeKey&&t.renderKey===renderKey);
|
|
160
348
|
activeTransitions = data.graph.transitions.filter(t => t.routeKey === data.routeKey && t.renderKey === renderKey);
|
|
161
349
|
renderState();
|
|
162
350
|
}
|
package/assets/viewer.js
CHANGED
|
@@ -119,6 +119,11 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
119
119
|
}
|
|
120
120
|
document.body.dataset.panel = "canvas";
|
|
121
121
|
$("viewport").value = String(viewport);
|
|
122
|
+
const inlineDocuments=new Map(m.documents.map(document=>[document.documentKey,document]));
|
|
123
|
+
const localDocumentFiles=new Map((m.documentFiles??[]).map(file=>[file.documentKey,file]));
|
|
124
|
+
const directoryInteractions=m.interactions;
|
|
125
|
+
const expandedDocuments=new WeakSet();
|
|
126
|
+
let localRead=null;
|
|
122
127
|
const changes = new Map(), baseline = new Map(), originalValues = new Map(), localValues = new Map(), savedViews = new Map(), seededViews = new Set();
|
|
123
128
|
let baselineLoaded = false;
|
|
124
129
|
let pendingDocument=null,projectionReadOnly=false,retryOpen=null;
|
|
@@ -208,7 +213,7 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
208
213
|
$("commit").textContent = "";
|
|
209
214
|
$("page-count").textContent = m.pages.length;
|
|
210
215
|
$("summary").textContent =
|
|
211
|
-
`${m.pages.length} ${tr("页面")} · ${m.components.length} ${tr("组件")} · ${consoleContext?.catalog?.nodeCount??m.documents.reduce((n, d) => n + d.nodes.length, 0)} ${tr("DOM 节点")}`;
|
|
216
|
+
`${m.pages.length} ${tr("页面")} · ${m.summary?.components??m.components.length} ${tr("组件")} · ${consoleContext?.catalog?.nodeCount??m.summary?.nodes??m.documents.reduce((n, d) => n + d.nodes.length, 0)} ${tr("DOM 节点")}`;
|
|
212
217
|
$("gap-count").textContent = `${consoleContext?.catalog?.gapCount??m.coverage.gaps.length} ${iamEnglish ? "coverage gaps" : "项缺口已记录"}`;
|
|
213
218
|
$("coverage-description").textContent =
|
|
214
219
|
tr("这是从 App 源码生成的 HTML/CSS/JS 投影。未运行原应用;动态数据、鉴权状态、字体与第三方组件可能不完整。只有明确提取的界面状态可模拟。");
|
|
@@ -299,13 +304,49 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
299
304
|
"<\/script>" + (keyboard ? "<script>" + keyboard.js.replaceAll("</script", "<\\/script") + "<\/script>" : "") + "</body></html>"
|
|
300
305
|
);
|
|
301
306
|
}
|
|
302
|
-
function openPage(next, targetUrl = next.route, renderKey, historyMove=null) {
|
|
307
|
+
async function openPage(next, targetUrl = next.route, renderKey, historyMove=null) {
|
|
303
308
|
if (consoleContext) targetUrl = viewUrl(targetUrl);
|
|
304
309
|
retryOpen={pageKey:next.pageKey,targetUrl,renderKey,historyMove};
|
|
305
310
|
if(consoleContext)consoleContext.documentReady=false;
|
|
306
311
|
const nextRender=m.routeTree.renders.find(render=>render.routeKey===next.pageKey&&render.renderKey===(renderKey??m.routeTree.routes.find(route=>route.routeKey===next.pageKey).initialRenderKey));
|
|
307
312
|
if (!nextRender) throw Error("Unknown preview render");
|
|
308
|
-
|
|
313
|
+
let nextDocument=byId(m.documents,"documentKey",nextRender.documentKey);
|
|
314
|
+
if(!consoleContext || consoleContext.mode!=="documents"){
|
|
315
|
+
localRead?.abort();localRead=null;pendingDocument=null;
|
|
316
|
+
if(!nextDocument||!expandedDocuments.has(nextDocument)){
|
|
317
|
+
const request={pageKey:next.pageKey,renderKey:nextRender.renderKey};
|
|
318
|
+
const controller=new AbortController();localRead=controller;pendingDocument=request;
|
|
319
|
+
$("document-status").textContent=tr("正在打开页面资料…");$("export").disabled=true;
|
|
320
|
+
try{
|
|
321
|
+
const file=localDocumentFiles.get(nextRender.documentKey);
|
|
322
|
+
if(file){
|
|
323
|
+
if(!/^document_[a-f0-9]{64}$/.test(file.documentKey)||!/^[a-f0-9]{64}$/.test(file.sha256)||!Number.isSafeInteger(file.bytes)||file.bytes<1)throw Error("Invalid local document reference");
|
|
324
|
+
const response=await fetch(new URL('documents/'+file.documentKey+'/material.json',location.href),{signal:controller.signal});
|
|
325
|
+
if(!response.ok)throw Error("Local document unavailable");
|
|
326
|
+
const bytes=await response.arrayBuffer();
|
|
327
|
+
if(bytes.byteLength!==file.bytes)throw Error("Local document size mismatch");
|
|
328
|
+
const actual=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
329
|
+
if(actual!==file.sha256)throw Error("Local document digest mismatch");
|
|
330
|
+
const material=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes));
|
|
331
|
+
const stored=material.document;
|
|
332
|
+
if(stored.documentKey!==nextRender.documentKey||stored.pageKey!==next.pageKey||stored.hashes.document!==nextRender.documentHash)throw Error("Local document scope mismatch");
|
|
333
|
+
const expanded=await expandNodeHashes(stored,request);
|
|
334
|
+
if(pendingDocument!==request)return;
|
|
335
|
+
for(const key of ['elements','styles','assets','scripts','components','interactions'])m[key]=material[key];
|
|
336
|
+
activeGraph=material.routeTree;m.routeTree={...activeGraph,routes:m.routeTree.routes,renders:m.routeTree.renders};expandedDocuments.add(expanded);m.documents=[expanded];
|
|
337
|
+
}else{
|
|
338
|
+
const stored=inlineDocuments.get(nextRender.documentKey);if(!stored)throw Error("Missing local document");
|
|
339
|
+
const expanded=await expandNodeHashes(stored,request);
|
|
340
|
+
if(pendingDocument!==request)return;
|
|
341
|
+
expandedDocuments.add(expanded);m.documents=[expanded];
|
|
342
|
+
}
|
|
343
|
+
pendingDocument=null;localRead=null;nextDocument=m.documents[0];
|
|
344
|
+
}catch(error){
|
|
345
|
+
if(pendingDocument===request){pendingDocument=null;localRead=null;$("document-status").textContent=tr("页面资料校验失败,请重新打开此版本。");toast(tr("页面资料校验失败,请重新打开此版本。"));}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
309
350
|
if(!nextDocument&&consoleContext?.mode==="documents"){
|
|
310
351
|
pendingDocument={requestId:crypto.randomUUID(),pageKey:next.pageKey,renderKey:nextRender.renderKey,targetUrl,historyMove,documentKey:nextRender.documentKey,documentHash:nextRender.documentHash};
|
|
311
352
|
$("document-status").textContent=tr("正在打开页面资料…");$("export").disabled=true;
|
|
@@ -493,12 +534,12 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
493
534
|
function checkedDomPayload(document) {
|
|
494
535
|
const payload = document.domPayload;
|
|
495
536
|
if (!payload) return null;
|
|
496
|
-
if (document.html !== "" || !Array.isArray(document.nodes) || document.nodes.length || Object.keys(payload).sort().join(",") !== "bytes,data,encoding" || payload.encoding !== "gzip-json-v1" || !Number.isSafeInteger(payload.bytes) || payload.bytes < 1 ||
|
|
537
|
+
if (document.html !== "" || !Array.isArray(document.nodes) || document.nodes.length || Object.keys(payload).sort().join(",") !== "bytes,data,encoding" || payload.encoding !== "gzip-json-v1" || !Number.isSafeInteger(payload.bytes) || payload.bytes < 1 || typeof payload.data !== "string" || !payload.data.length || payload.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(payload.data)) throw Error("Invalid compressed DOM payload");
|
|
497
538
|
return payload;
|
|
498
539
|
}
|
|
499
540
|
function parsedDomPayload(text) {
|
|
500
541
|
const value = JSON.parse(text);
|
|
501
|
-
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" ||
|
|
542
|
+
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" || !value.html.isWellFormed() || !Array.isArray(value.nodes) || JSON.stringify(value) !== text) throw Error("Invalid expanded DOM payload");
|
|
502
543
|
return value;
|
|
503
544
|
}
|
|
504
545
|
async function documentDomBrowser(document) {
|
|
@@ -528,17 +569,17 @@ async function documentDomBrowser(document) {
|
|
|
528
569
|
}
|
|
529
570
|
/* END COMPRESSED DOM */
|
|
530
571
|
/* BEGIN HTML DOM DERIVATION */
|
|
531
|
-
function expandDomNodes(document, body) {
|
|
532
|
-
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash
|
|
572
|
+
function expandDomNodes(document, body, deriveTextId) {
|
|
573
|
+
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash)throw Error('Invalid HTML-derived DOM mode');
|
|
533
574
|
let index=0;const result=[];
|
|
534
|
-
function visit(node,parentId,depth){
|
|
575
|
+
function visit(node,parentId,depth,childIndex=0){
|
|
535
576
|
if(depth>128||![1,3].includes(node.nodeType))throw Error('Invalid HTML-derived DOM structure');
|
|
536
577
|
const ref=document.nodes[index++];
|
|
537
578
|
if(!ref||Object.keys(ref).some(key=>!['nodeId','elementKey','source','synthetic'].includes(key)))throw Error('Mixed HTML-derived DOM values');
|
|
538
|
-
const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId;
|
|
579
|
+
const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId??deriveTextId?.(document.pageKey,parentId,childIndex);
|
|
539
580
|
if(!/^node_[a-f0-9]{64}$/.test(nodeId??'')||element&&Object.hasOwn(ref,'nodeId')||Object.hasOwn(ref,'synthetic')&&ref.synthetic!==true)throw Error('HTML-derived DOM identity differs');
|
|
540
581
|
const full={...ref,nodeId,synthetic:ref.synthetic??false,parentId,kind:element?'element':'text',tag:element?node.tagName.toLowerCase():null,text:element?null:node.data,attributes:element?Object.fromEntries([...node.attributes].filter(a=>!a.name.startsWith('data-iam-')).map(a=>[a.name,a.value])):{},children:[]};
|
|
541
|
-
result.push(full);for(const child
|
|
582
|
+
result.push(full);for(const [i,child]of [...node.childNodes].entries())full.children.push(visit(child,nodeId,depth+1,i));return nodeId;
|
|
542
583
|
}
|
|
543
584
|
if(body.childNodes.length!==1||visit(body.firstChild,null,0)!==document.rootNodeId||index!==document.nodes.length)throw Error('HTML-derived DOM coverage differs');
|
|
544
585
|
return result;
|
|
@@ -553,8 +594,22 @@ function expandDomNodes(document, body) {
|
|
|
553
594
|
const {domPayload, ...plain} = document;
|
|
554
595
|
document = {...plain, ...decoded};
|
|
555
596
|
}
|
|
556
|
-
if(!Array.isArray(document.nodes)
|
|
557
|
-
const
|
|
597
|
+
if(!Array.isArray(document.nodes))throw Error('Node hash budget');
|
|
598
|
+
const body=new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body,textIds=new Map(),pendingText=[];
|
|
599
|
+
let preorder=0;
|
|
600
|
+
const collectText=(node,parentId,childIndex=0,depth=0)=>{
|
|
601
|
+
if(!node||depth>128)throw Error('DOM derivation budget');
|
|
602
|
+
const ref=document.nodes[preorder++];if(!ref)throw Error('Missing source node');
|
|
603
|
+
const id=node.nodeType===1?node.getAttribute('data-iam-node'):ref.nodeId;
|
|
604
|
+
if(node.nodeType===3&&!id)pendingText.push({parentId,index:childIndex,key:parentId+':'+childIndex});
|
|
605
|
+
for(const [i,child]of [...node.childNodes].entries())collectText(child,id,i,depth+1);
|
|
606
|
+
};
|
|
607
|
+
collectText(body.firstChild,null);if(preorder!==document.nodes.length)throw Error('Source node count differs');
|
|
608
|
+
for(let offset=0;offset<pendingText.length;offset+=64){
|
|
609
|
+
if(pendingDocument!==request)throw Error('Superseded document');
|
|
610
|
+
await Promise.all(pendingText.slice(offset,offset+64).map(async row=>textIds.set(row.key,'node_'+await hash('iam/identity/node/v1\0'+canonical({page:document.pageKey,parentId:row.parentId,index:row.index,kind:3})))));
|
|
611
|
+
}
|
|
612
|
+
const nodes=expandDomNodes(document,body,(_page,parentId,index)=>textIds.get(parentId+':'+index)),byId=new Map(nodes.map(node=>[node.nodeId,node]));
|
|
558
613
|
if(byId.size!==nodes.length)throw Error('Duplicate node');
|
|
559
614
|
for(let offset=0;offset<nodes.length;offset+=64){
|
|
560
615
|
if(pendingDocument!==request)throw Error('Superseded document');
|
|
@@ -581,12 +636,12 @@ function expandDomNodes(document, body) {
|
|
|
581
636
|
const request=pendingDocument;
|
|
582
637
|
try{
|
|
583
638
|
const projection=data.projection;
|
|
584
|
-
if(projection?.format!=="indiana-document-projection"||projection.schemaVersion!==2||projection.versionId!==consoleContext.versionId||projection.manifestDigest!==consoleContext.manifestDigest||projection.pageKey!==request.pageKey||projection.renderKey!==(request.renderKey??null)||projection.documentKey!==request.documentKey||projection.documentHash!==request.documentHash||typeof projection.payload!=="string"
|
|
639
|
+
if(projection?.format!=="indiana-document-projection"||projection.schemaVersion!==2||projection.versionId!==consoleContext.versionId||projection.manifestDigest!==consoleContext.manifestDigest||projection.pageKey!==request.pageKey||projection.renderKey!==(request.renderKey??null)||projection.documentKey!==request.documentKey||projection.documentHash!==request.documentHash||typeof projection.payload!=="string")throw Error("Projection scope mismatch");
|
|
585
640
|
const material=JSON.parse(projection.payload),document=material.document;
|
|
586
641
|
const {hashes,...body}=document;
|
|
587
642
|
if(document.documentKey!==request.documentKey||document.pageKey!==request.pageKey||hashes.document!==request.documentHash||await hash("iam/document/v1\0"+canonical(body))!==request.documentHash)throw Error("Projection document hash mismatch");
|
|
588
643
|
const provided = new Map();
|
|
589
|
-
if (!Array.isArray(material.resourceRefs)
|
|
644
|
+
if (!Array.isArray(material.resourceRefs)) throw Error("Invalid preview resources");
|
|
590
645
|
for (const kind of ["styles","assets","scripts"]) {
|
|
591
646
|
if (!Array.isArray(material[kind])) throw Error("Invalid preview resource kind");
|
|
592
647
|
for (const value of material[kind]) {
|
|
@@ -602,7 +657,6 @@ function expandDomNodes(document, body) {
|
|
|
602
657
|
if(!entry||entry.kind!==ref.kind)throw Error("Missing preview resource");
|
|
603
658
|
resolved.set(ref.hash,entry);
|
|
604
659
|
}
|
|
605
|
-
if([...resolved.values()].reduce((bytes,entry)=>bytes+entry.bytes,0)>8_388_608)throw Error("Preview resource byte budget");
|
|
606
660
|
for (const kind of ["styles","assets","scripts"]) material[kind]=[...resolved.values()].filter(entry=>entry.kind===kind).map(entry=>entry.value);
|
|
607
661
|
const script=document.scriptKey?material.scripts.find(script=>script.scriptKey===document.scriptKey):{js:document.js,sha256:hashes.js};
|
|
608
662
|
if(!script||!consoleContext.reviewedRuntimeHashes.includes(script.sha256)||await hash(script.js)!==script.sha256||document.scriptKey&&document.scriptKey!=="script_"+script.sha256)throw Error("Unreviewed preview runtime");
|
|
@@ -615,7 +669,7 @@ function expandDomNodes(document, body) {
|
|
|
615
669
|
if(consoleContext.catalog)m.routeTree={...material.routeTree,routes:consoleContext.catalog.routes,renders:consoleContext.catalog.renders};
|
|
616
670
|
const expanded=await expandNodeHashes(document,request);
|
|
617
671
|
if(pendingDocument!==request)return;
|
|
618
|
-
m.documents=[expanded];projectionReadOnly=projection.readOnly;pendingDocument=null;
|
|
672
|
+
expandedDocuments.add(expanded);m.documents=[expanded];projectionReadOnly=projection.readOnly;pendingDocument=null;
|
|
619
673
|
openPage(m.pages.find(page=>page.pageKey===request.pageKey),request.targetUrl,request.renderKey,request.historyMove);
|
|
620
674
|
}catch{if(pendingDocument===request){pendingDocument=null;projectionReadOnly=true;renderSaveCount();parent.postMessage({channel:"indiana-iam-console-v1",nonce:consoleContext.nonce,versionId:consoleContext.versionId,type:"error"},"*");}}
|
|
621
675
|
return;
|
|
@@ -790,9 +844,9 @@ function expandDomNodes(document, body) {
|
|
|
790
844
|
text("strong", p.route),
|
|
791
845
|
text(
|
|
792
846
|
"small",
|
|
793
|
-
p.componentKeys.length +
|
|
847
|
+
(p.componentCount??p.componentKeys.length) +
|
|
794
848
|
tr(" 组件 · ") +
|
|
795
|
-
p.stateKeys.length +
|
|
849
|
+
(p.stateCount??p.stateKeys.length) +
|
|
796
850
|
tr(" 状态候选"),
|
|
797
851
|
),
|
|
798
852
|
);
|
|
@@ -800,7 +854,7 @@ function expandDomNodes(document, body) {
|
|
|
800
854
|
const links = text("div", "", "map-targets"),
|
|
801
855
|
ids = [
|
|
802
856
|
...new Set(
|
|
803
|
-
|
|
857
|
+
directoryInteractions
|
|
804
858
|
.filter((i) => i.pageKey === p.pageKey && i.targetPageKey)
|
|
805
859
|
.map((i) => i.targetPageKey),
|
|
806
860
|
),
|
|
@@ -825,9 +879,9 @@ function expandDomNodes(document, body) {
|
|
|
825
879
|
format: "indiana-local-preview-changes",
|
|
826
880
|
schemaVersion: 1,
|
|
827
881
|
sourceGraphDigest: m.release.sourceGraphDigest,
|
|
828
|
-
manifestDocumentHashes: m.
|
|
829
|
-
documentKey:
|
|
830
|
-
hash:
|
|
882
|
+
manifestDocumentHashes: m.routeTree.renders.map((row) => ({
|
|
883
|
+
documentKey: row.documentKey,
|
|
884
|
+
hash: row.documentHash,
|
|
831
885
|
})),
|
|
832
886
|
changes: [...changes.values()],
|
|
833
887
|
scope: "local_preview_only",
|
|
@@ -845,19 +899,7 @@ function expandDomNodes(document, body) {
|
|
|
845
899
|
toast(tr("导出本地 JSON;未保存到服务器或应用到商户。"));
|
|
846
900
|
});
|
|
847
901
|
renderNav();
|
|
848
|
-
|
|
849
|
-
const initialRequest = {};
|
|
850
|
-
pendingDocument = initialRequest;
|
|
851
|
-
const expandedBytes = m.documents.reduce((total, document) => total + (document.domPayload?.bytes ?? new TextEncoder().encode(JSON.stringify({html:document.html,nodes:document.nodes})).byteLength), 0);
|
|
852
|
-
if (!Number.isSafeInteger(expandedBytes) || expandedBytes > 33_554_432) throw Error("Expanded DOM aggregate byte budget exceeded");
|
|
853
|
-
// Sequential decoding bounds peak memory for a multi-route artifact.
|
|
854
|
-
const expandedDocuments = [];
|
|
855
|
-
for (const document of m.documents) expandedDocuments.push(await expandNodeHashes(document, initialRequest));
|
|
856
|
-
m.documents = expandedDocuments;
|
|
857
|
-
pendingDocument = null;
|
|
858
|
-
renderNav();
|
|
859
|
-
}
|
|
860
|
-
openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
902
|
+
await openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
861
903
|
}
|
|
862
904
|
(async () => {
|
|
863
905
|
const data = JSON.parse(document.getElementById("iam-data").textContent);
|
|
@@ -894,7 +936,7 @@ function expandDomNodes(document, body) {
|
|
|
894
936
|
const model={format:"indiana-preview-material",schemaVersion:1,app:{name:catalog.appName},release:{sourceGraphDigest:catalog.sourceGraphDigest},pages:catalog.routes.map(route=>({pageKey:route.routeKey,route:route.path,title:route.path,documentKeys:[route.documentKey],stateKeys:[],componentKeys:[]})),documents:[],elements:[],components:[],interactions:[],styles:[],scripts:[],assets:[],coverage:{gaps:[]},routeTree:{schemaVersion:catalog.renderIndexVersion,routes:catalog.routes,renders:catalog.renders,variables:catalog.graph.variables.map(variable=>({variableKey:variable.id,routeKey:variable.routeId,renderKey:variable.renderKey})),states:[],transitions:[],bindings:[],targets:[],navigation:[]}};
|
|
895
937
|
await mountIamViewer(model,context);loading.hidden=true;document.querySelector(".workspace").hidden=false;return;
|
|
896
938
|
}
|
|
897
|
-
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength !== context.bytes
|
|
939
|
+
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength !== context.bytes) throw new Error("Invalid IAM bytes");
|
|
898
940
|
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", message.bytes))].map(b => b.toString(16).padStart(2,"0")).join("");
|
|
899
941
|
if (digest !== context.manifestDigest) throw new Error("IAM digest mismatch");
|
|
900
942
|
const manifest = JSON.parse(new TextDecoder("utf-8",{fatal:true}).decode(message.bytes));
|