@tinkerlandai/indiana-iam 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,33 @@ 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
+ MDX pages/layouts need a separate adapter and explicitly report incomplete route coverage.
113
123
  TanStack reads the registered generated tree, including pathless layouts, index routes and simple `$param` paths;
114
124
  custom history/basepath and complex parameter patterns require separate adapters. Loaders, guards, retry/reload behavior
115
125
  and runtime data remain unobserved. Unsupported optional React paths are isolated as gaps.
126
+ React Hook Form Controller callbacks retain their source JSX, including first-party wrappers; field values, validation
127
+ and submissions are not executed. Source-proven Radix overlays and React Transition Group visibility retain their
128
+ initial state. Supported Radix triggers and close controls simulate local state only; unknown controlled states stay
129
+ closed and unobserved. These adapters do not implement portal positioning, focus management or business callbacks.
130
+ Chart.js canvas wrappers and Recharts line/bar/area/pie declarations project bounded literal series as inert SVG.
131
+ Chart modules, effects, formatters and callbacks are not executed. Random/remote/oversized series show an explicit
132
+ unobserved-data placeholder; animation, library layout and tooltips remain outside this source projection.
133
+ Build hooks package only raster images referenced by projected JSX: relative source imports and public build URLs.
134
+ They never crawl unrelated assets or fetch URLs. Files must remain inside the root without symlinks; referenced
135
+ 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.
136
+ Standalone generation keeps images unpackaged unless explicit public inputs are supplied.
137
+ Source-proven `useSyncExternalStore` matchMedia getters retain both typed viewport branches and follow the preview
138
+ viewport. Subscriptions are not run. UI preference cookies with an explicit source SSR guard use an empty-cookie
139
+ preview; auth/session cookies and arbitrary stores stay unobserved. Pure lazy local-state initializers are interpreted
140
+ under the same bounded rules; merchant callbacks and browser storage are never executed or read.
116
141
  The complete validated IAM must fit within 8 MiB; oversized projections fail without emitting a truncated artifact.
117
142
  Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
118
143
  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 bounded to
144
+ 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 bounded to
120
145
  8 MiB per payload and 32 MiB in aggregate. The final IAM file limit stays 8 MiB. Exported page HTML is expanded.
121
- Use the matching 0.3 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
146
+ Use the matching 0.4 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
122
147
  before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
123
148
  viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
124
149
  The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
@@ -151,7 +176,35 @@ The Console App map groups pages into areas, shows recorded navigation, and open
151
176
  opening a page editor. Thumbnails are static source projections. They do not execute authentication, data loaders,
152
177
  form submissions or application code.
153
178
 
154
- Optional static attributes preserve product intent that cannot be established from a URL alone:
179
+ The optional `routeTree.ui` index records source UI alternatives, controlled Base UI Tabs (including first-party
180
+ wrappers and context), and conditional redirect/return recipes. A recipe follows the actual React Router location
181
+ through a URL query writer to a matching query-consuming `Navigate`; a password input at the destination identifies
182
+ the login UI. The index never infers authentication from a route name or certifies a successful login. Dynamic
183
+ callbacks, unresolved registries and external data remain coverage gaps. Finite first-party component registries
184
+ retain their source labels and scope qualifiers in `ui.regions`; selecting an alternative is a preview assumption.
185
+ Local JSX aliases and invoked JSX render props can preserve page content without executing their modules or callbacks.
186
+
187
+ Unknown branches stay hidden in the admitted document. The shared `previewIamUiDocument` helper can select inert
188
+ source alternatives and typed local state for thumbnail inspection. It returns the selected conditions and source
189
+ locations. Controlled overlays with unknown `open` values stay closed until explicitly selected for inspection.
190
+ These preview choices never change render discriminators, editable targets, the manifest or runtime permissions.
191
+ `iamTabUiScenes` dispatches the recorded scalar Tab transitions to derive before/after preview states and directed
192
+ edges. It includes content controlled outside the Tab root within the same source component or overlay. Every edge
193
+ retains the transition, state writes and source locations. Missing collections remain gaps rather than sample rows.
194
+ Finite Tab-dependent literal collections use shared `ui.fragments` and concrete `ui.collections` mounts. Their
195
+ source element references, item counts, Tab cases and required local values remain pinned; derived rows are inert
196
+ and never become SDK edit targets. The Lucide adapter reads geometry from pinned `lucide-static` 1.34.0 JSON,
197
+ without loading merchant packages. Unknown filter combinations remain unobserved rather than reusing stale rows.
198
+ Finite source-derived Tab classes, search placeholders and accessible labels share `ui.attributeSets`; concrete
199
+ mounts in `ui.attributes` apply only the recorded presentation properties. Fragment class and element dictionaries
200
+ preserve their exact authored values. Repeated graph source locations and instance scope names use lossless
201
+ `sourcePool` / `scopePool` references; source hashes, state identities and per-instance state remain unchanged.
202
+ Unexecuted query status/result branches and unresolved scalar alternatives display explicit unobserved annotations;
203
+ preview does not select a loading, failed or empty result automatically. Literal empty source catalogs remain valid.
204
+ Static thumbnail crops can focus on the page body or a source node using a bounded viewport; they retain authored
205
+ descendants and never introduce scripts, frames or remote resources.
206
+
207
+ Optional static attributes preserve product intent that cannot be established from source dataflow alone:
155
208
 
156
209
  | Attribute | Location | Meaning |
157
210
  | -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
@@ -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
@@ -528,17 +528,17 @@ async function documentDomBrowser(document) {
528
528
  }
529
529
  /* END COMPRESSED DOM */
530
530
  /* BEGIN HTML DOM DERIVATION */
531
- function expandDomNodes(document, body) {
531
+ function expandDomNodes(document, body, deriveTextId) {
532
532
  if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash||document.nodes.length>12000)throw Error('Invalid HTML-derived DOM mode');
533
533
  let index=0;const result=[];
534
- function visit(node,parentId,depth){
534
+ function visit(node,parentId,depth,childIndex=0){
535
535
  if(depth>128||![1,3].includes(node.nodeType))throw Error('Invalid HTML-derived DOM structure');
536
536
  const ref=document.nodes[index++];
537
537
  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;
538
+ const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId??deriveTextId?.(document.pageKey,parentId,childIndex);
539
539
  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
540
  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 of node.childNodes)full.children.push(visit(child,nodeId,depth+1));return nodeId;
541
+ result.push(full);for(const [i,child]of [...node.childNodes].entries())full.children.push(visit(child,nodeId,depth+1,i));return nodeId;
542
542
  }
543
543
  if(body.childNodes.length!==1||visit(body.firstChild,null,0)!==document.rootNodeId||index!==document.nodes.length)throw Error('HTML-derived DOM coverage differs');
544
544
  return result;
@@ -554,7 +554,21 @@ function expandDomNodes(document, body) {
554
554
  document = {...plain, ...decoded};
555
555
  }
556
556
  if(!Array.isArray(document.nodes)||document.nodes.length>12000)throw Error('Node hash budget');
557
- const nodes=expandDomNodes(document,new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body),byId=new Map(nodes.map(node=>[node.nodeId,node]));
557
+ const body=new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body,textIds=new Map(),pendingText=[];
558
+ let preorder=0;
559
+ const collectText=(node,parentId,childIndex=0,depth=0)=>{
560
+ if(!node||depth>128||preorder>=12000)throw Error('DOM derivation budget');
561
+ const ref=document.nodes[preorder++];if(!ref)throw Error('Missing source node');
562
+ const id=node.nodeType===1?node.getAttribute('data-iam-node'):ref.nodeId;
563
+ if(node.nodeType===3&&!id)pendingText.push({parentId,index:childIndex,key:parentId+':'+childIndex});
564
+ for(const [i,child]of [...node.childNodes].entries())collectText(child,id,i,depth+1);
565
+ };
566
+ collectText(body.firstChild,null);if(preorder!==document.nodes.length)throw Error('Source node count differs');
567
+ for(let offset=0;offset<pendingText.length;offset+=64){
568
+ if(pendingDocument!==request)throw Error('Superseded document');
569
+ 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})))));
570
+ }
571
+ const nodes=expandDomNodes(document,body,(_page,parentId,index)=>textIds.get(parentId+':'+index)),byId=new Map(nodes.map(node=>[node.nodeId,node]));
558
572
  if(byId.size!==nodes.length)throw Error('Duplicate node');
559
573
  for(let offset=0;offset<nodes.length;offset+=64){
560
574
  if(pendingDocument!==request)throw Error('Superseded document');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinkerlandai/indiana-iam",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Build-time Indiana App Manifest (IAM) generator for Next.js and Vite apps: scans source after the production build and writes the IAM and its release descriptor.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -32,7 +32,8 @@
32
32
  },
33
33
  "./next": {
34
34
  "types": "./src/next.d.ts",
35
- "import": "./src/next.mjs"
35
+ "import": "./src/next.mjs",
36
+ "require": "./src/next.cjs"
36
37
  },
37
38
  "./vite": {
38
39
  "types": "./src/vite.d.ts",
@@ -46,21 +47,12 @@
46
47
  "ajv": "6.15.0",
47
48
  "css-tree": "3.2.1",
48
49
  "jsdom": "26.1.0",
50
+ "lucide-static": "1.34.0",
49
51
  "magic-string": "0.30.21",
50
52
  "pako": "3.0.2",
53
+ "sharp": "0.35.5",
51
54
  "tailwind-merge": "3.7.0",
52
55
  "tailwindcss": "4.3.3",
53
56
  "typescript": "5.9.3"
54
- },
55
- "peerDependencies": {
56
- "sharp": "0.35.5"
57
- },
58
- "peerDependenciesMeta": {
59
- "sharp": {
60
- "optional": true
61
- }
62
- },
63
- "devDependencies": {
64
- "sharp": "0.35.5"
65
57
  }
66
58
  }