@tinkerlandai/indiana-iam 0.2.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 +127 -5
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +74 -7
- package/package.json +6 -13
- package/src/analyze.mjs +897 -123
- package/src/anchors.mjs +5 -0
- package/src/build-options.d.ts +6 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +49 -6
- package/src/build.mjs +13 -7
- package/src/cli.mjs +10 -4
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/dom-payload-browser.mjs +28 -0
- package/src/dom-payload-json.mjs +12 -0
- package/src/dom-payload.mjs +50 -0
- package/src/framework-routes.mjs +44 -89
- package/src/generate.mjs +20 -7
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +13 -1
- package/src/index.mjs +1 -1
- package/src/integrity.mjs +4 -2
- package/src/library-entry.mjs +5 -0
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/next.mjs +4 -3
- package/src/node-hashes.mjs +13 -9
- package/src/render-index.mjs +6 -4
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +15 -3
- package/src/routing.mjs +1 -1
- package/src/schema-definition.mjs +8 -1
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +38 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-entry.mjs +50 -0
- package/src/source-environment.mjs +26 -0
- package/src/source-media.mjs +21 -0
- package/src/source-query-evidence.mjs +64 -0
- package/src/source-redirects.mjs +388 -0
- package/src/source-resolution.mjs +268 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +39 -11
- package/src/tab-scenes.mjs +214 -0
- package/src/tanstack-routes.mjs +143 -0
- package/src/transform.mjs +2 -2
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +645 -0
- package/src/upload.mjs +28 -6
- package/src/validate.mjs +19 -4
- package/src/vite.d.ts +4 -1
- package/src/vite.mjs +25 -10
package/src/analyze.mjs
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import {referencedRasterSources} from "./source-media.mjs";
|
|
3
|
+
import {uiEnvironmentPrimitive} from "./source-environment.mjs";
|
|
4
|
+
import {sourceChart} from "./source-charts.mjs";
|
|
2
5
|
import path from "node:path";
|
|
3
|
-
import { automaticSourceAnchors, anchorAttributes } from "./anchors.mjs";
|
|
6
|
+
import { automaticSourceAnchors, anchorAttributes, sourceScriptKind } from "./anchors.mjs";
|
|
4
7
|
import { sourceFiles } from "./source-files.mjs";
|
|
5
8
|
import { dependency } from "./dependencies.mjs";
|
|
6
9
|
import { assembleRouteTree, readVariables } from "./route-tree.mjs";
|
|
10
|
+
import { sourceEntry } from "./source-entry.mjs";
|
|
7
11
|
import { frameworkRoutes, externalImport } from "./framework-routes.mjs";
|
|
8
|
-
import {
|
|
12
|
+
import { sourceResolution } from "./source-resolution.mjs";
|
|
13
|
+
import { routeIndexDigest, refreshRenderHashes, flattenComposition } from "./render-index.mjs";
|
|
9
14
|
import { storeDocumentNodes } from "./node-hashes.mjs";
|
|
10
15
|
import { staticQuery } from "./static-query.mjs";
|
|
16
|
+
import { attachUiStructure } from './ui-structure.mjs';
|
|
17
|
+
import { componentRegistry } from './component-registry.mjs';
|
|
18
|
+
import { sourceRedirects } from './source-redirects.mjs';
|
|
19
|
+
import {internGraphSources} from './graph-sources.mjs';
|
|
20
|
+
import {collectionStateReads,sourceCollectionFragment} from './source-collections.mjs';
|
|
21
|
+
import {sourceQueryEvidence} from './source-query-evidence.mjs';
|
|
22
|
+
import {lucideSource,lucideGeometry,LUCIDE_SOURCE,LUCIDE_SOURCE_VERSION} from './lucide-source.mjs';
|
|
11
23
|
import {
|
|
12
24
|
publicRoute,
|
|
13
25
|
assertRoutes,
|
|
@@ -27,6 +39,27 @@ const { JSDOM } = dependency("jsdom").value;
|
|
|
27
39
|
const UNKNOWN = Symbol("unknown");
|
|
28
40
|
import { sensitiveName, redactLiteral, privateUrl } from "./privacy.mjs";
|
|
29
41
|
const isUnknown = (value) => value === UNKNOWN;
|
|
42
|
+
const COMPONENT=Symbol('source-component');
|
|
43
|
+
const TRANSLATION=Symbol('source-translation-catalog');
|
|
44
|
+
const UI_SETTER=Symbol('source-ui-setter');
|
|
45
|
+
const UI_PRIMITIVE=Symbol('source-ui-primitive');
|
|
46
|
+
|
|
47
|
+
function uiReturnExpression(fn) {
|
|
48
|
+
if(!fn?.body||!ts.isBlock(fn.body))return returnExpression(fn);
|
|
49
|
+
const returned=statement=>ts.isReturnStatement(statement)?statement.expression:ts.isBlock(statement)&&statement.statements.every(s=>ts.isVariableStatement(s)||ts.isReturnStatement(s))?returned(statement.statements.at(-1)):null;
|
|
50
|
+
function sequence(statements) {
|
|
51
|
+
for(let i=0;i<statements.length;i++) {
|
|
52
|
+
const statement=statements[i];
|
|
53
|
+
if(ts.isReturnStatement(statement))return statement.expression;
|
|
54
|
+
if(ts.isIfStatement(statement)) {
|
|
55
|
+
const yes=returned(statement.thenStatement),no=statement.elseStatement?returned(statement.elseStatement):sequence(statements.slice(i+1));
|
|
56
|
+
if(yes&&no)return {kind:ts.SyntaxKind.ConditionalExpression,condition:statement.expression,whenTrue:yes,whenFalse:no};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return returnExpression(fn);
|
|
60
|
+
}
|
|
61
|
+
return sequence([...fn.body.statements]);
|
|
62
|
+
}
|
|
30
63
|
// React numeric styles use px except for unitless properties and CSS variables.
|
|
31
64
|
const unitlessStyles = new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth".split(" "));
|
|
32
65
|
function staticStyle(value, unsupported) {
|
|
@@ -140,7 +173,8 @@ function functionFrom(node) {
|
|
|
140
173
|
if (
|
|
141
174
|
ts.isFunctionDeclaration(node) ||
|
|
142
175
|
ts.isArrowFunction(node) ||
|
|
143
|
-
ts.isFunctionExpression(node)
|
|
176
|
+
ts.isFunctionExpression(node) ||
|
|
177
|
+
ts.isMethodDeclaration(node)
|
|
144
178
|
)
|
|
145
179
|
return node;
|
|
146
180
|
if (ts.isCallExpression(node))
|
|
@@ -156,6 +190,7 @@ export async function analyze({
|
|
|
156
190
|
maxFiles = 5000,
|
|
157
191
|
maxBytes = 100_000_000,
|
|
158
192
|
navigationPaths = [],
|
|
193
|
+
entry,
|
|
159
194
|
automaticAnchors = false,
|
|
160
195
|
}) {
|
|
161
196
|
root = await fs.realpath(root);
|
|
@@ -167,6 +202,8 @@ export async function analyze({
|
|
|
167
202
|
components = new Map(),
|
|
168
203
|
assets = new Map();
|
|
169
204
|
const cssSources = [];
|
|
205
|
+
const jsonResources=new Map(),translationResources=new Set();
|
|
206
|
+
const presentation={conditions:[],tabs:[],navigation:[],regions:[],collections:[],fragments:[],attributes:[],attributeSets:[]},collectionTemplates=new Map();
|
|
170
207
|
const ui = { variables: [], bindings: [], transitions: [], uncovered: [] };
|
|
171
208
|
const uiVariableKeys = new Set();
|
|
172
209
|
let byteCount = 0;
|
|
@@ -196,6 +233,7 @@ export async function analyze({
|
|
|
196
233
|
sha256: sha256(bytes),
|
|
197
234
|
bytes: bytes.length,
|
|
198
235
|
});
|
|
236
|
+
if(file.endsWith('.json')){try{jsonResources.set(file,JSON.parse(text));}catch{/* Non-resource JSON stays opaque. */}continue;}
|
|
199
237
|
if (file.endsWith(".css")) {
|
|
200
238
|
gap(
|
|
201
239
|
file.endsWith(".module.css")
|
|
@@ -207,13 +245,17 @@ export async function analyze({
|
|
|
207
245
|
if (!file.endsWith(".module.css")) cssSources.push({ file, text });
|
|
208
246
|
continue;
|
|
209
247
|
}
|
|
210
|
-
if (!/\.[cm]?[jt]sx?$/.test(file))
|
|
248
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) {
|
|
249
|
+
if (/^(?:src\/)?app\/(?:.*\/)?(?:page|layout)\.mdx$/.test(file) || /^(?:src\/)?pages\/.+\.mdx$/.test(file))
|
|
250
|
+
gap("ROUTE_MDX_UNSUPPORTED", file, "MDX route/layout source requires a dedicated adapter; its page and layout coverage is incomplete");
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
211
253
|
const source = ts.createSourceFile(
|
|
212
254
|
file,
|
|
213
255
|
text,
|
|
214
256
|
ts.ScriptTarget.Latest,
|
|
215
257
|
true,
|
|
216
|
-
|
|
258
|
+
sourceScriptKind(file, ts),
|
|
217
259
|
);
|
|
218
260
|
const module = {
|
|
219
261
|
file,
|
|
@@ -250,6 +292,7 @@ export async function analyze({
|
|
|
250
292
|
}
|
|
251
293
|
if (ts.isFunctionDeclaration(statement) && (statement.name||statement.modifiers?.some(m=>m.kind===ts.SyntaxKind.DefaultKeyword)))
|
|
252
294
|
module.bindings.set(statement.name?.text||'$default', statement);
|
|
295
|
+
if (ts.isEnumDeclaration(statement)) module.bindings.set(statement.name.text, statement);
|
|
253
296
|
if (ts.isVariableStatement(statement))
|
|
254
297
|
for (const d of statement.declarationList.declarations)
|
|
255
298
|
if (ts.isIdentifier(d.name) && d.initializer)
|
|
@@ -333,6 +376,52 @@ export async function analyze({
|
|
|
333
376
|
.find(Boolean) || null
|
|
334
377
|
);
|
|
335
378
|
}
|
|
379
|
+
function jsonResource(from,specifier){
|
|
380
|
+
let file=specifier.startsWith('.')?path.posix.normalize(path.posix.join(path.posix.dirname(from),specifier)):null;
|
|
381
|
+
if(!file)for(const [pattern,values] of Object.entries(alias)){const prefix=pattern.split('*')[0];if(pattern.includes('*')&&specifier.startsWith(prefix)){file=values[0].replace('*',specifier.slice(prefix.length)).replace(/^\.\//,'');break;}}
|
|
382
|
+
return file&&!file.startsWith('../')?jsonResources.get(file):undefined;
|
|
383
|
+
}
|
|
384
|
+
function contextKey(module,name){
|
|
385
|
+
const ref=binding(module,name),call=ref&&unwrap(ref.node);
|
|
386
|
+
const primitive=call&&ts.isCallExpression(call)&&externalImport(ref.module,call.expression.getText(ref.module.source));
|
|
387
|
+
return primitive?.specifier==='react'&&primitive.name==='createContext'?ref.module.file+'#'+ref.name:null;
|
|
388
|
+
}
|
|
389
|
+
function sourceContextCall(node,module,ctx){
|
|
390
|
+
const imported=externalImport(module,node.expression.getText(module.source));
|
|
391
|
+
if(imported?.specifier==='react'&&imported.name==='useContext'&&ts.isIdentifier(node.arguments[0]))return ctx.__iamContexts?.get(contextKey(module,node.arguments[0].text))??UNKNOWN;
|
|
392
|
+
const ref=ts.isIdentifier(node.expression)&&binding(module,node.expression.text),fn=ref&&functionFrom(ref.node);
|
|
393
|
+
if(!fn?.body||!ts.isBlock(fn.body))return UNKNOWN;
|
|
394
|
+
const result=unwrap(returnExpression(fn));
|
|
395
|
+
if(!result||!ts.isIdentifier(result))return UNKNOWN;
|
|
396
|
+
for(const statement of fn.body.statements)if(ts.isVariableStatement(statement))for(const decl of statement.declarationList.declarations){
|
|
397
|
+
const call=unwrap(decl.initializer),primitive=call&&ts.isCallExpression(call)&&externalImport(ref.module,call.expression.getText(ref.module.source));
|
|
398
|
+
if(primitive?.specifier==='react'&&primitive.name==='useContext'&&ts.isIdentifier(decl.name)&&decl.name.text===result.text&&ts.isIdentifier(call.arguments[0]))return ctx.__iamContexts?.get(contextKey(ref.module,call.arguments[0].text))??UNKNOWN;
|
|
399
|
+
if(primitive?.specifier==='react-i18next'&&primitive.name==='useTranslation'&&ts.isObjectBindingPattern(decl.name)&&decl.name.elements.some(e=>(e.propertyName?.text||e.name.text)==='t'&&e.name.text===result.text)){
|
|
400
|
+
const catalogs=[...ref.module.imports.values()].filter(i=>i.specifier.endsWith('.json')).map(i=>jsonResource(ref.module.file,i.specifier)).filter(Boolean);
|
|
401
|
+
if(catalogs.length===1){translationResources.add(catalogs[0]);return {[TRANSLATION]:catalogs[0]};}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return UNKNOWN;
|
|
405
|
+
}
|
|
406
|
+
function basePrimitive(module,tag,seen=new Set()){
|
|
407
|
+
if(seen.has(module.file+'#'+tag)||seen.size>8)return null;seen.add(module.file+'#'+tag);
|
|
408
|
+
const [name,part,extra]=tag.split('.'),ref=module.imports.get(name);
|
|
409
|
+
const imported=externalImport(module,tag);
|
|
410
|
+
const radixFamilies={'dialog':'Dialog','alert-dialog':'AlertDialog','popover':'Popover','dropdown-menu':'Menu','context-menu':'Menu','tooltip':'Tooltip','hover-card':'Popover','tabs':'Tabs','collapsible':'Collapsible'};
|
|
411
|
+
const radix=imported?.specifier?.match(/^@radix-ui\/react-([a-z-]+)$/)?.[1];
|
|
412
|
+
if(radixFamilies[radix]) {
|
|
413
|
+
const family=radixFamilies[radix],prefix=radix.split('-').map(v=>v[0].toUpperCase()+v.slice(1)).join('');
|
|
414
|
+
const exported=imported.name===prefix?'Root':imported.name.startsWith(prefix)?imported.name.slice(prefix.length):imported.name;
|
|
415
|
+
const mapped=family==='Tabs'?({Trigger:'Tab',Content:'Panel'}[exported]??exported):({Content:'Popup',Overlay:'Backdrop',Sub:'SubmenuRoot',SubTrigger:'SubmenuTrigger',SubContent:'Popup'}[exported]??exported);
|
|
416
|
+
return {family,part:mapped};
|
|
417
|
+
}
|
|
418
|
+
if(imported?.specifier==='react-transition-group'&&['Transition','CSSTransition'].includes(imported.name))return {family:'Transition',part:'Root'};
|
|
419
|
+
const families={'@base-ui/react/tabs':'Tabs','@base-ui/react/popover':'Popover','@base-ui/react/dialog':'Dialog','@base-ui/react/alert-dialog':'AlertDialog','@base-ui/react/menu':'Menu','@base-ui/react/tooltip':'Tooltip'};
|
|
420
|
+
if(!extra&&part&&ref&&families[ref.specifier]===ref.name)return {family:ref.name,part};
|
|
421
|
+
if(!part){const target=binding(module,tag),node=target&&unwrap(target.node);if(node&&ts.isPropertyAccessExpression(node))return basePrimitive(target.module,node.getText(target.module.source),seen);}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
function tabsPrimitive(module,tag){const primitive=basePrimitive(module,tag);return primitive?.family==='Tabs'&&['Root','List','Tab','Panel','Indicator'].includes(primitive.part)?primitive.part:null;}
|
|
336
425
|
function binding(module, name, seen=new Set()) {
|
|
337
426
|
if(!name||seen.size>=16||seen.has(module.file+'#'+name))return null;seen.add(module.file+'#'+name);
|
|
338
427
|
if (module.bindings.has(name))
|
|
@@ -347,11 +436,10 @@ export async function analyze({
|
|
|
347
436
|
: target.exports.get(imported.name) || imported.name;
|
|
348
437
|
return binding(target,key,seen);
|
|
349
438
|
}
|
|
350
|
-
const componentDefs = new Map();
|
|
351
|
-
|
|
352
|
-
for (const [symbol, node] of module.bindings) {
|
|
439
|
+
const componentDefs = new Map(),componentsByFunction=new WeakMap();
|
|
440
|
+
function registerComponent(module, symbol, node) {
|
|
353
441
|
const fn = functionFrom(node);
|
|
354
|
-
if (!fn)
|
|
442
|
+
if (!fn) return null;
|
|
355
443
|
let hasJsx = false;
|
|
356
444
|
function visit(n) {
|
|
357
445
|
if (
|
|
@@ -363,7 +451,7 @@ export async function analyze({
|
|
|
363
451
|
ts.forEachChild(n, visit);
|
|
364
452
|
}
|
|
365
453
|
visit(fn);
|
|
366
|
-
if (!hasJsx)
|
|
454
|
+
if (!hasJsx) return null;
|
|
367
455
|
const key = identity("component", { file: module.file, symbol }),
|
|
368
456
|
ordinals = new Map();
|
|
369
457
|
let ordinal = 0;
|
|
@@ -373,7 +461,7 @@ export async function analyze({
|
|
|
373
461
|
ts.forEachChild(n, number);
|
|
374
462
|
}
|
|
375
463
|
number(fn);
|
|
376
|
-
const def = { module, symbol, fn, key, ordinals };
|
|
464
|
+
const def = { module, symbol, fn, key, ordinals };componentsByFunction.set(fn,def);
|
|
377
465
|
let returns = 0;
|
|
378
466
|
function countReturns(item) {
|
|
379
467
|
if (
|
|
@@ -403,12 +491,21 @@ export async function analyze({
|
|
|
403
491
|
source: loc,
|
|
404
492
|
dependencies: [],
|
|
405
493
|
});
|
|
406
|
-
|
|
407
|
-
|
|
494
|
+
return def;
|
|
495
|
+
}
|
|
496
|
+
for (const module of modules.values())
|
|
497
|
+
for (const [symbol, node] of module.bindings) registerComponent(module, symbol, node);
|
|
498
|
+
const resolution = sourceResolution({ ts, resolveModule, resolveBinding: binding });
|
|
499
|
+
const componentBinding = (module, name) => {
|
|
500
|
+
const ref = resolution.component(module, name);
|
|
501
|
+
if (ref?.wrapped) gap("LAZY_WRAPPER_UNOBSERVED", module.file, name + ": only the statically forwarded successful module is projected; retry/reload behavior is not executed");
|
|
502
|
+
return ref;
|
|
503
|
+
};
|
|
504
|
+
function collectComponentDependencies(def) {
|
|
408
505
|
function collectDependencies(node) {
|
|
409
506
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
410
507
|
const opening = ts.isJsxElement(node) ? node.openingElement : node,
|
|
411
|
-
ref =
|
|
508
|
+
ref = componentBinding(def.module, opening.tagName.getText(def.module.source)),
|
|
412
509
|
target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
|
|
413
510
|
if (
|
|
414
511
|
target &&
|
|
@@ -420,6 +517,20 @@ export async function analyze({
|
|
|
420
517
|
}
|
|
421
518
|
collectDependencies(def.fn);
|
|
422
519
|
}
|
|
520
|
+
for (const def of componentDefs.values()) collectComponentDependencies(def);
|
|
521
|
+
const inlineNodes = new WeakMap(), inlineCounts = new Map();
|
|
522
|
+
function inlineComponent(module, node, route) {
|
|
523
|
+
if (inlineNodes.has(node)) return inlineNodes.get(node);
|
|
524
|
+
const group = module.file + "#" + route, occurrence = inlineCounts.get(group) ?? 0;
|
|
525
|
+
inlineCounts.set(group, occurrence + 1);
|
|
526
|
+
const symbol = "$route_" + hashObject("iam/inline-route-component/v1", { route, occurrence }).slice(0, 24);
|
|
527
|
+
module.bindings.set(symbol, node);
|
|
528
|
+
const def = registerComponent(module, symbol, node);
|
|
529
|
+
if (def) collectComponentDependencies(def);
|
|
530
|
+
const target = { file: module.file, symbol };
|
|
531
|
+
inlineNodes.set(node, target);
|
|
532
|
+
return target;
|
|
533
|
+
}
|
|
423
534
|
function sourceLocation(module, node, symbol) {
|
|
424
535
|
const loc = module.source.getLineAndCharacterOfPosition(
|
|
425
536
|
node.getStart(module.source),
|
|
@@ -432,6 +543,17 @@ export async function analyze({
|
|
|
432
543
|
fileHash: module.hash,
|
|
433
544
|
};
|
|
434
545
|
}
|
|
546
|
+
function sourceComponent(node,module){
|
|
547
|
+
const imported=ts.isIdentifier(node)&&externalImport(module,node.text);if(imported?.specifier==='@radix-ui/react-slot'&&['Slot','Root'].includes(imported.name))return {[UI_PRIMITIVE]:imported};if(imported?.specifier==='lucide-react'){const icon=lucideSource(imported.name);if(icon)return icon;}
|
|
548
|
+
const ref=ts.isIdentifier(node)?binding(module,node.text):null;const def=componentsByFunction.get(node)??(ref&&componentDefs.get(ref.module.file+'#'+ref.name));
|
|
549
|
+
return def?{[COMPONENT]:def}:null;
|
|
550
|
+
}
|
|
551
|
+
function environment(node,module,context){
|
|
552
|
+
const value=uiEnvironmentPrimitive({ts,node,module,binding,imported:externalImport,evaluate:evalExpression});
|
|
553
|
+
if(value?.kind==='empty-preference'){gap('UI_PREFERENCE_EMPTY_PREVIEW',module.file,'Source-declared SSR fallback for a UI preference; preview starts without cookies, never reads merchant browser state.');return [undefined];}
|
|
554
|
+
return value;
|
|
555
|
+
}
|
|
556
|
+
function query(node,module,context){return staticQuery({ts,node,module,context,binding,unknown:UNKNOWN,redact:redactLiteral,sensitive:sensitiveName,reference:sourceComponent,isReference:v=>Boolean(v?.[COMPONENT]||v?.[LUCIDE_SOURCE]||v?.[UI_PRIMITIVE]),imported:externalImport,project:(n,m,c)=>{const v=environment(n,m,c);return Array.isArray(v)?v:null;}});}
|
|
435
557
|
function evalExpression(
|
|
436
558
|
node,
|
|
437
559
|
module,
|
|
@@ -442,6 +564,7 @@ export async function analyze({
|
|
|
442
564
|
node = unwrap(node);
|
|
443
565
|
if (!node || depth > 30) return UNKNOWN;
|
|
444
566
|
const ev = (n) => evalExpression(n, module, context, stack, depth + 1);
|
|
567
|
+
const component=!(ts.isIdentifier(node)&&Object.hasOwn(context,node.text))&&sourceComponent(node,module);if(component)return component;
|
|
445
568
|
if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
|
|
446
569
|
return ts.isNumericLiteral(node) ? Number(node.text) : redactLiteral(node.text);
|
|
447
570
|
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
@@ -449,7 +572,14 @@ export async function analyze({
|
|
|
449
572
|
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
450
573
|
if (ts.isIdentifier(node)) {
|
|
451
574
|
if (Object.hasOwn(context, node.text)) return context[node.text];
|
|
575
|
+
const variableKey=context.__iamVariableKeys?.[context.__iamSetters?.[node.text]];
|
|
576
|
+
if(variableKey)return {[UI_SETTER]:{variableKey,value:{op:'event',key:'value'},payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType}}};
|
|
452
577
|
if (node.text === "undefined") return undefined;
|
|
578
|
+
const image=module.imports.get(node.text);
|
|
579
|
+
if(image?.name==='default'&&/^\.\.?\/[^?#]+\.(?:png|jpe?g|webp|gif)$/i.test(image.specifier)){
|
|
580
|
+
const file=path.posix.normalize(path.posix.join(path.posix.dirname(module.file),image.specifier));
|
|
581
|
+
if(!file.startsWith('../')&&!file.split('/').some(p=>p.startsWith('.'))&&!sensitiveName(file))return '/__iam_source__/'+file;
|
|
582
|
+
}
|
|
453
583
|
const token = module.file + "#" + node.text;
|
|
454
584
|
if (stack.has(token)) return UNKNOWN;
|
|
455
585
|
const ref = binding(module, node.text);
|
|
@@ -460,12 +590,16 @@ export async function analyze({
|
|
|
460
590
|
if (ts.isArrayLiteralExpression(node)) return node.elements.map(ev);
|
|
461
591
|
if (ts.isObjectLiteralExpression(node)) {
|
|
462
592
|
const obj = {};
|
|
593
|
+
const fields=new Map();
|
|
463
594
|
for (const prop of node.properties) {
|
|
464
595
|
if (ts.isPropertyAssignment(prop))
|
|
465
596
|
obj[prop.name.getText(module.source).replace(/^['"]|['"]$/g, "")] =
|
|
466
597
|
ev(prop.initializer);
|
|
467
|
-
else if (ts.isShorthandPropertyAssignment(prop))
|
|
598
|
+
else if (ts.isShorthandPropertyAssignment(prop)){
|
|
468
599
|
obj[prop.name.text] = ev(prop.name);
|
|
600
|
+
const variable=context.__iamVariableKeys?.[prop.name.text],inherited=context.__iamPropBindings?.get(prop.name.text);
|
|
601
|
+
if(variable||inherited)fields.set(prop.name.text,inherited||{op:'var',key:variable});
|
|
602
|
+
}
|
|
469
603
|
else if (ts.isSpreadAssignment(prop)) {
|
|
470
604
|
const v = ev(prop.expression);
|
|
471
605
|
if (isUnknown(v)) return UNKNOWN;
|
|
@@ -473,6 +607,7 @@ export async function analyze({
|
|
|
473
607
|
Object.assign(obj, v);
|
|
474
608
|
}
|
|
475
609
|
}
|
|
610
|
+
if(fields.size)objectBindings.set(obj,fields);
|
|
476
611
|
return obj;
|
|
477
612
|
}
|
|
478
613
|
if (
|
|
@@ -526,6 +661,12 @@ export async function analyze({
|
|
|
526
661
|
return isUnknown(a) ? UNKNOWN : (a ?? b);
|
|
527
662
|
if (isUnknown(a) || isUnknown(b)) return UNKNOWN;
|
|
528
663
|
if (op === ts.SyntaxKind.PlusToken) return a + b;
|
|
664
|
+
if(op === ts.SyntaxKind.MinusToken && typeof a==='number' && typeof b==='number')return a-b;
|
|
665
|
+
if(['number','string'].includes(typeof a)&&typeof a===typeof b){
|
|
666
|
+
if(op===ts.SyntaxKind.GreaterThanToken)return a>b;if(op===ts.SyntaxKind.GreaterThanEqualsToken)return a>=b;
|
|
667
|
+
if(op===ts.SyntaxKind.LessThanToken)return a<b;if(op===ts.SyntaxKind.LessThanEqualsToken)return a<=b;
|
|
668
|
+
}
|
|
669
|
+
if(op===ts.SyntaxKind.InKeyword&&typeof a==='string'&&b&&typeof b==='object'&&!['__proto__','constructor','prototype'].includes(a))return Object.hasOwn(b,a);
|
|
529
670
|
if (
|
|
530
671
|
[
|
|
531
672
|
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
@@ -543,9 +684,21 @@ export async function analyze({
|
|
|
543
684
|
}
|
|
544
685
|
if (ts.isCallExpression(node)) {
|
|
545
686
|
const callee = node.expression.getText(module.source);
|
|
687
|
+
const projected=environment(node,module,context);if(Array.isArray(projected))return projected[0];
|
|
688
|
+
const renderCallback=ev(node.expression)?.iamRenderProp;
|
|
689
|
+
if(renderCallback)return {iamRenderInvocation:{callback:renderCallback,args:node.arguments.map(ev),caller:context}};
|
|
690
|
+
const primitive=externalImport(module,callee);
|
|
691
|
+
if(['react-router','react-router-dom'].includes(primitive?.specifier)&&primitive.name==='useLocation'&&typeof context.__iamRoutePath==='string')return {pathname:context.__iamRoutePath,search:UNKNOWN,hash:UNKNOWN};
|
|
692
|
+
const translation=ts.isIdentifier(node.expression)&&context[node.expression.text]?.[TRANSLATION];
|
|
693
|
+
if(translation){const key=ev(node.arguments[0]);if(typeof key!=='string')return UNKNOWN;const value=key.split('.').reduce((value,key)=>value&&Object.hasOwn(Object(value),key)?value[key]:undefined,translation);return typeof value==='string'&&!/\{\{/.test(value)?redactLiteral(value):UNKNOWN;}
|
|
694
|
+
if(primitive?.specifier==='react'&&primitive.name==='useMemo'&&node.arguments.length===2){
|
|
695
|
+
const fn=node.arguments[0];if(ts.isArrowFunction(fn)&&!fn.modifiers?.length&&!fn.parameters.length&&!ts.isBlock(fn.body))return ev(fn.body);
|
|
696
|
+
}
|
|
697
|
+
const contextValue=sourceContextCall(node,module,context);
|
|
698
|
+
if(!isUnknown(contextValue))return contextValue;
|
|
546
699
|
if (/(?:^|\.)useState$/.test(callee))
|
|
547
700
|
return {
|
|
548
|
-
stateInitial: node.arguments.length ? ev(node.arguments[0]) : null,
|
|
701
|
+
stateInitial: node.arguments.length ? (ts.isArrowFunction(node.arguments[0])&&node.arguments[0].parameters.length===0 ? query({kind:ts.SyntaxKind.CallExpression,expression:node.arguments[0],arguments:[]},module,context) : ev(node.arguments[0])) : null,
|
|
549
702
|
};
|
|
550
703
|
if (callee === "cn" || callee === "clsx") {
|
|
551
704
|
let valid=true;
|
|
@@ -580,6 +733,7 @@ export async function analyze({
|
|
|
580
733
|
([key, values]) =>
|
|
581
734
|
values[isUnknown((props || {})[key]) ? options.defaultVariants?.[key] : (props || {})[key] ?? options.defaultVariants?.[key]],
|
|
582
735
|
),
|
|
736
|
+
...(options?.compoundVariants??[]).flatMap(row=>Object.entries(row).filter(([key])=>!['class','className'].includes(key)).every(([key,value])=>{const actual=Object.hasOwn(props??{},key)&&props[key]!==undefined?props[key]:options.defaultVariants?.[key];return Array.isArray(value)?value.includes(actual):value===actual;})?[row.class,row.className]:[]),
|
|
583
737
|
props?.class,
|
|
584
738
|
props?.className,
|
|
585
739
|
]
|
|
@@ -590,7 +744,7 @@ export async function analyze({
|
|
|
590
744
|
const target = ev(node.expression.expression),
|
|
591
745
|
args = node.arguments.map(ev),
|
|
592
746
|
method = node.expression.name.text;
|
|
593
|
-
if (isUnknown(target) || args.some(isUnknown)) return
|
|
747
|
+
if (isUnknown(target) || args.some(isUnknown)) return query(node,module,context);
|
|
594
748
|
if (
|
|
595
749
|
typeof target === "string" &&
|
|
596
750
|
[
|
|
@@ -606,7 +760,7 @@ export async function analyze({
|
|
|
606
760
|
return target.filter((v) => !isUnknown(v)).join(args[0]);
|
|
607
761
|
}
|
|
608
762
|
}
|
|
609
|
-
if (ts.isCallExpression(node)) return
|
|
763
|
+
if (ts.isCallExpression(node)) return query(node,module,context);
|
|
610
764
|
return UNKNOWN;
|
|
611
765
|
}
|
|
612
766
|
// Preserve only definitely present class tokens when a template contains an
|
|
@@ -662,15 +816,14 @@ export async function analyze({
|
|
|
662
816
|
const omitted = new Set(param.name.elements.filter(item => !item.dotDotDotToken).map(item => item.propertyName?.getText() || item.name.getText()));
|
|
663
817
|
const rest = Object.fromEntries(Object.entries(props).filter(([key]) => !omitted.has(key)));
|
|
664
818
|
const parent = forwarded.get(props);
|
|
665
|
-
if (parent) forwarded.set(rest, { bindings: parent.bindings.filter(b => !omitted.has(b.property)), handlers: parent.handlers.filter(h => !omitted.has(h.key)) });
|
|
819
|
+
if (parent) forwarded.set(rest, { bindings: parent.bindings.filter(b => !omitted.has(b.property)), handlers: parent.handlers.filter(h => !omitted.has(h.key)), attributes:(parent.attributes??[]).filter(a=>!omitted.has(a.key)) });
|
|
666
820
|
ctx[el.name.text] = rest; continue;
|
|
667
821
|
}
|
|
668
822
|
const key = el.propertyName?.getText() || el.name.getText();
|
|
669
823
|
ctx[el.name.getText()] =
|
|
670
|
-
props[key]
|
|
671
|
-
(el.initializer
|
|
672
|
-
?
|
|
673
|
-
: UNKNOWN);
|
|
824
|
+
Object.hasOwn(props, key) && props[key] !== undefined ? props[key] :
|
|
825
|
+
(el.initializer ? evalExpression(el.initializer, def.module, ctx) :
|
|
826
|
+
props.__iamOpaqueProps ? UNKNOWN : undefined);
|
|
674
827
|
const inherited = forwarded.get(props);
|
|
675
828
|
const binding = inherited?.bindings.find(b => b.property === key), handler = inherited?.handlers.find(h => h.key === key);
|
|
676
829
|
if (binding) ctx.__iamPropBindings.set(el.name.getText(), binding.value);
|
|
@@ -682,8 +835,22 @@ export async function analyze({
|
|
|
682
835
|
if (ts.isVariableStatement(s))
|
|
683
836
|
for (const d of s.declarationList.declarations) {
|
|
684
837
|
if (ts.isIdentifier(d.name)) {
|
|
685
|
-
|
|
838
|
+
const projected=environment(d.initializer,def.module,ctx);
|
|
839
|
+
if(projected?.kind==='viewport'){
|
|
840
|
+
const width=Number(projected.query.match(/\d+/)[0]);ctx[d.name.text]=projected.query.includes('max-width')?1280<=width:1280>=width;ctx.__iamStates.add(d.name.text);ctx.__iamStateNodes[d.name.text]=d;ctx.__iamViewportQueries??={};ctx.__iamViewportQueries[d.name.text]=projected.query;
|
|
841
|
+
gap('VIEWPORT_SOURCE_PROJECTION',def.module.file,'Source matchMedia query projected against the preview viewport; subscription effects are not executed.');
|
|
842
|
+
}else ctx[d.name.text] = sensitiveName(d.name.text) ? UNKNOWN : evalExpression(d.initializer, def.module, ctx);
|
|
686
843
|
ctx.__iamLocals.set(d.name.text, d.initializer);
|
|
844
|
+
} else if(ts.isObjectBindingPattern(d.name)){
|
|
845
|
+
const value=evalExpression(d.initializer,def.module,ctx);
|
|
846
|
+
for(const field of d.name.elements)if(ts.isIdentifier(field.name)&&!field.dotDotDotToken){
|
|
847
|
+
const key=field.propertyName?.text||field.name.text;
|
|
848
|
+
ctx[field.name.text]=!isUnknown(value)&&value&&typeof value==='object'&&Object.hasOwn(value,key)?value[key]:field.initializer?evalExpression(field.initializer,def.module,ctx):UNKNOWN;
|
|
849
|
+
const inherited=value&&typeof value==='object'&&objectBindings.get(value)?.get(key);
|
|
850
|
+
if(inherited)ctx.__iamPropBindings.set(field.name.text,inherited);
|
|
851
|
+
const setter=value?.[key]?.[UI_SETTER];
|
|
852
|
+
if(setter)ctx.__iamPropHandlers.set(field.name.text,setter);
|
|
853
|
+
}
|
|
687
854
|
} else if (ts.isArrayBindingPattern(d.name)) {
|
|
688
855
|
const call = unwrap(d.initializer), hook = call && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && def.module.imports.get(call.expression.text);
|
|
689
856
|
if (hook?.specifier === "react" && hook.name === "useActionState") {
|
|
@@ -707,11 +874,16 @@ export async function analyze({
|
|
|
707
874
|
Object.hasOwn(Object(value), "stateInitial") &&
|
|
708
875
|
d.name.elements[0]
|
|
709
876
|
) {
|
|
710
|
-
const
|
|
711
|
-
|
|
877
|
+
const stateBinding = d.name.elements[0];
|
|
878
|
+
if (!ts.isBindingElement(stateBinding) || stateBinding.dotDotDotToken || !ts.isIdentifier(stateBinding.name)) {
|
|
879
|
+
gap("STATE_BINDING_UNSUPPORTED", def.module.file, "Omitted, nested or rest state bindings have no named scalar value for UI simulation");
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
const key = stateBinding.name.text;
|
|
883
|
+
ctx[key] = sensitiveName(key) && typeof value.stateInitial !== 'boolean' ? UNKNOWN : value.stateInitial;
|
|
712
884
|
ctx.__iamStates.add(key);
|
|
713
885
|
ctx.__iamStateNodes[key] = d;
|
|
714
|
-
if (d.name.elements[1]?.name)
|
|
886
|
+
if (d.name.elements[1]?.name && !d.name.elements[1].dotDotDotToken && ts.isIdentifier(d.name.elements[1].name))
|
|
715
887
|
ctx.__iamSetters[d.name.elements[1].name.text] = key;
|
|
716
888
|
}
|
|
717
889
|
}
|
|
@@ -768,7 +940,10 @@ export async function analyze({
|
|
|
768
940
|
!/^\/(?:[^/?#[\]\\\s]+\/)*[^/?#[\]\\\s]+$/.test(routing.basePath)
|
|
769
941
|
)
|
|
770
942
|
throw new Error("Invalid source basePath");
|
|
771
|
-
const
|
|
943
|
+
const selection = await sourceEntry({ root, modules, resolveModule, ts, entry });
|
|
944
|
+
if (selection.htmlSource) inventory.push(selection.htmlSource);
|
|
945
|
+
inventory.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
|
|
946
|
+
const compositions = frameworkRoutes({ modules, ...selection, resolveModule, ts, sourceLocation, navigationPaths, gap, resolution, inlineComponent });
|
|
772
947
|
const pageFiles = compositions.pageFiles;
|
|
773
948
|
routing.framework = compositions.framework;
|
|
774
949
|
if (compositions.routeSources) routing.routeSources = compositions.routeSources;
|
|
@@ -823,19 +998,20 @@ export async function analyze({
|
|
|
823
998
|
};
|
|
824
999
|
const record = new Map(),
|
|
825
1000
|
used = new Set(),
|
|
826
|
-
branches = [];
|
|
1001
|
+
branches = [],attributeRequests=[];
|
|
827
1002
|
let renderedCount = 0;
|
|
828
1003
|
function renderComponent(def, props, instance, depth) {
|
|
829
1004
|
if (depth === 0 && composition.entryMode === "soft") instance = composition.compositionKey + ":" + instance;
|
|
830
1005
|
// Component recursion and JSX nesting are separate budgets. Counting DOM
|
|
831
1006
|
// wrappers as component calls silently dropped ordinary nested navigation.
|
|
832
|
-
if (used.size >=
|
|
1007
|
+
if (used.size >= 24 || used.has(def.key + ":" + instance)) {
|
|
833
1008
|
gap("COMPONENT_DEPTH_LIMIT", def.module.file, def.symbol + ": active component call budget exceeded");
|
|
834
1009
|
return "";
|
|
835
1010
|
}
|
|
836
1011
|
used.add(def.key + ":" + instance);
|
|
837
1012
|
if (!page.componentKeys.includes(def.key))
|
|
838
1013
|
page.componentKeys.push(def.key);
|
|
1014
|
+
props.__iamRoutePath=/[\[\]]/.test(route)?UNKNOWN:route;
|
|
839
1015
|
const ctx = componentContext(def, props);
|
|
840
1016
|
ctx.__iamOutlet = props.__iamOutlet;
|
|
841
1017
|
ctx.__iamScope = identity("state-scope", {
|
|
@@ -860,11 +1036,17 @@ export async function analyze({
|
|
|
860
1036
|
}
|
|
861
1037
|
if (!uiVariableKeys.has(variableKey)) {
|
|
862
1038
|
uiVariableKeys.add(variableKey);
|
|
863
|
-
ui.variables.push({ variableKey, routeKey: pageKey, scopeKey: ctx.__iamScope, label, valueType: typeof initialValue, initialValue, source: sourceLocation(def.module, ctx.__iamStateNodes[label], def.symbol) });
|
|
1039
|
+
ui.variables.push({ variableKey, routeKey: pageKey, scopeKey: ctx.__iamScope, label:ctx.__iamViewportQueries?.[label]?'@media '+ctx.__iamViewportQueries[label]:label, valueType: typeof initialValue, initialValue, source: sourceLocation(def.module, ctx.__iamStateNodes[label], def.symbol) });
|
|
864
1040
|
}
|
|
865
1041
|
}
|
|
1042
|
+
// Rebind pure local records after scalar state identities exist. This preserves
|
|
1043
|
+
// useMemo/context field relationships without invoking a hook or callback.
|
|
1044
|
+
for(const [name,expression]of ctx.__iamLocals){
|
|
1045
|
+
if(ctx.__iamStates.has(name)||sensitiveName(name))continue;
|
|
1046
|
+
const value=evalExpression(expression,def.module,ctx);if(!isUnknown(value))ctx[name]=value;
|
|
1047
|
+
}
|
|
866
1048
|
const result = renderExpr(
|
|
867
|
-
|
|
1049
|
+
uiReturnExpression(def.fn),
|
|
868
1050
|
def,
|
|
869
1051
|
ctx,
|
|
870
1052
|
instance,
|
|
@@ -899,6 +1081,7 @@ export async function analyze({
|
|
|
899
1081
|
}
|
|
900
1082
|
if (ts.isIdentifier(node)) {
|
|
901
1083
|
if (Object.hasOwn(aliases, node.text)) return aliases[node.text];
|
|
1084
|
+
if(eventParam&&node.text===eventParam)return {op:'event',key:'value'};
|
|
902
1085
|
if (ctx.__iamVariableKeys[node.text]) return { op: "var", key: ctx.__iamVariableKeys[node.text] };
|
|
903
1086
|
if (ctx.__iamPropBindings.has(node.text)) return ctx.__iamPropBindings.get(node.text);
|
|
904
1087
|
if (ctx.__iamLocals.has(node.text)) return compile(ctx.__iamLocals.get(node.text));
|
|
@@ -927,43 +1110,385 @@ export async function analyze({
|
|
|
927
1110
|
const value = evalExpression(node, owner.module, ctx);
|
|
928
1111
|
return ["string", "boolean", "number"].includes(typeof value) ? { op: "const", value } : null;
|
|
929
1112
|
}
|
|
930
|
-
function
|
|
1113
|
+
function uiExpressionType(expression, payload = {}) {
|
|
1114
|
+
if (!expression) return undefined;
|
|
1115
|
+
if (expression.op === "const") return typeof expression.value;
|
|
1116
|
+
if (expression.op === "var") return ui.variables.find(variable => variable.variableKey === expression.key)?.valueType;
|
|
1117
|
+
if (expression.op === "event") return payload[expression.key];
|
|
1118
|
+
const type = child => uiExpressionType(child, payload);
|
|
1119
|
+
if (expression.op === "not") return type(expression.arg) ? "boolean" : undefined;
|
|
1120
|
+
if (expression.op === "classes") return expression.parts.every(part => type(part.when) === "boolean") ? "string" : undefined;
|
|
1121
|
+
if (["all", "any"].includes(expression.op)) return expression.args.every(child => type(child) === "boolean") ? "boolean" : undefined;
|
|
1122
|
+
const left = type(expression.left), right = type(expression.right);
|
|
1123
|
+
if (["eq", "ne"].includes(expression.op)) return left && left === right ? "boolean" : undefined;
|
|
1124
|
+
if (["add", "subtract"].includes(expression.op)) return left === "number" && right === "number" ? "number" : undefined;
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
function uiHandler(initializer, owner, ctx, property = '') {
|
|
931
1128
|
let fn = initializer && ts.isJsxExpression(initializer) ? unwrap(initializer.expression) : null;
|
|
932
|
-
|
|
933
|
-
if
|
|
934
|
-
if
|
|
935
|
-
const
|
|
936
|
-
|
|
1129
|
+
const eventValue = (variableKey) => ({ variableKey, value: {op:'event',key:'value'}, payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType} });
|
|
1130
|
+
if(fn&&ts.isIdentifier(fn)&&ctx.__iamPropHandlers.has(fn.text))return ctx.__iamPropHandlers.get(fn.text);
|
|
1131
|
+
if(fn&&ts.isIdentifier(fn)&&ctx.__iamSetters[fn.text]&&['onValueChange','onOpenChange'].includes(property)) {
|
|
1132
|
+
const key=ctx.__iamVariableKeys[ctx.__iamSetters[fn.text]];return key?eventValue(key):null;
|
|
1133
|
+
}
|
|
1134
|
+
if(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text))fn=unwrap(ctx.__iamLocals.get(fn.text));
|
|
1135
|
+
if(fn&&ts.isPropertyAccessExpression(fn)&&ts.isIdentifier(fn.expression)) {
|
|
1136
|
+
const props=ctx[fn.expression.text],handler=props&&typeof props==='object'&&forwarded.get(props)?.handlers.find(h=>h.key===fn.name.text);
|
|
1137
|
+
if(handler)return handler;
|
|
1138
|
+
}
|
|
1139
|
+
if(!fn||!ts.isArrowFunction(fn)||fn.modifiers?.some(m=>m.kind===ts.SyntaxKind.AsyncKeyword))return null;
|
|
1140
|
+
let statements=ts.isBlock(fn.body)?[...fn.body.statements]:[fn.body];
|
|
1141
|
+
if(statements.length>16)return null;
|
|
1142
|
+
const writes=[],payload={};
|
|
1143
|
+
const inputs=e=>{if(e.op==='event')payload[e.key]=e.key==='checked'?'boolean':'string';for(const c of [e.arg,e.left,e.right,...(e.args||[]),...(e.parts||[]).map(p=>p.when)])if(c)inputs(c);};
|
|
1144
|
+
const replaceEvent=(value,arg)=>value.op==='event'&&value.key==='value'?arg:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,Array.isArray(v)?v.map(item=>item&&typeof item==='object'?replaceEvent(item,arg):item):v&&typeof v==='object'?replaceEvent(v,arg):v]));
|
|
1145
|
+
for(let body of statements) {
|
|
1146
|
+
if(ts.isExpressionStatement(body)||ts.isReturnStatement(body))body=body.expression;
|
|
1147
|
+
body=unwrap(body);
|
|
1148
|
+
if(body&&ts.isCallExpression(body)&&!body.arguments.length&&ts.isPropertyAccessExpression(body.expression)&&ts.isIdentifier(body.expression.expression)&&body.expression.expression.text===fn.parameters[0]?.name?.text&&['preventDefault','stopPropagation'].includes(body.expression.name.text))continue;
|
|
1149
|
+
if(body&&ts.isBinaryExpression(body)&&body.operatorToken.kind===ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1150
|
+
const guard=unwrap(body.left);
|
|
1151
|
+
if(!ts.isBinaryExpression(guard)||guard.operatorToken.kind!==ts.SyntaxKind.EqualsEqualsEqualsToken||!ts.isTypeOfExpression(guard.left)||!ts.isIdentifier(guard.left.expression)||guard.left.expression.text!==fn.parameters[0]?.name?.text||!ts.isStringLiteral(guard.right)||!['string','number','boolean'].includes(guard.right.text))return null;
|
|
1152
|
+
body=unwrap(body.right);
|
|
1153
|
+
}
|
|
1154
|
+
if(!body||!ts.isCallExpression(body)||!ts.isIdentifier(body.expression)||body.arguments.length!==1)return null;
|
|
1155
|
+
const inherited=ctx.__iamPropHandlers.get(body.expression.text);
|
|
1156
|
+
if(inherited) {
|
|
1157
|
+
const arg=uiExpression(body.arguments[0],owner,ctx,fn.parameters[0]?.name?.text||null);
|
|
1158
|
+
if(!arg)return null;
|
|
1159
|
+
const forwardedWrites=(inherited.writes||[{variableKey:inherited.variableKey,value:inherited.value}]).map(write=>({...write,value:replaceEvent(write.value,arg)}));
|
|
1160
|
+
for(const write of forwardedWrites)inputs(write.value);
|
|
1161
|
+
if(['onValueChange','onOpenChange'].includes(property))for(const [key,type]of Object.entries(inherited.payload))if(Object.hasOwn(payload,key))payload[key]=type;
|
|
1162
|
+
if(forwardedWrites.some(write=>uiExpressionType(write.value,payload)!==ui.variables.find(v=>v.variableKey===write.variableKey)?.valueType))return null;
|
|
1163
|
+
writes.push(...forwardedWrites);continue;
|
|
1164
|
+
}
|
|
1165
|
+
const label=ctx.__iamSetters[body.expression.text],variableKey=ctx.__iamVariableKeys[label];
|
|
1166
|
+
if(!variableKey||writes.some(w=>w.variableKey===variableKey))return null;
|
|
1167
|
+
const input=unwrap(body.arguments[0]),functional=input&&ts.isArrowFunction(input)&&!ts.isBlock(input.body)&&input.parameters.length===1&&ts.isIdentifier(input.parameters[0].name)?input:null;
|
|
1168
|
+
const aliases=functional?{[functional.parameters[0].name.text]:{op:'var',key:variableKey}}:{};
|
|
1169
|
+
const value=uiExpression(functional?functional.body:input,owner,ctx,fn.parameters[0]?.name?.text||null,aliases);
|
|
1170
|
+
if(!value)return null;
|
|
1171
|
+
inputs(value);const expected=ui.variables.find(v=>v.variableKey===variableKey)?.valueType;
|
|
1172
|
+
if(value.op==='event'&&value.key==='value'&&['onValueChange','onOpenChange'].includes(property))payload.value=expected;
|
|
1173
|
+
if(uiExpressionType(value,payload)!==expected)return null;
|
|
1174
|
+
writes.push({variableKey,value});
|
|
1175
|
+
}
|
|
1176
|
+
if(!writes.length||new Set(writes.map(w=>w.variableKey)).size!==writes.length)return null;
|
|
1177
|
+
return {...writes[0],writes,payload};
|
|
1178
|
+
}
|
|
1179
|
+
function queryBacked(node, owner, ctx) {
|
|
1180
|
+
return sourceQueryEvidence({
|
|
1181
|
+
ts,
|
|
1182
|
+
node,
|
|
1183
|
+
module: owner.module,
|
|
1184
|
+
locals: ctx.__iamLocals,
|
|
1185
|
+
binding,
|
|
1186
|
+
functionFrom,
|
|
1187
|
+
imported: externalImport,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function remoteResultCondition(node, owner, ctx) {
|
|
1191
|
+
let shape = false;
|
|
1192
|
+
const root =
|
|
1193
|
+
ts.isIdentifier(node) && ctx.__iamLocals.has(node.text)
|
|
1194
|
+
? ctx.__iamLocals.get(node.text)
|
|
1195
|
+
: node;
|
|
1196
|
+
const inspect = (n) => {
|
|
1197
|
+
if (!n || shape) return;
|
|
1198
|
+
if (
|
|
1199
|
+
ts.isPropertyAccessExpression(n) &&
|
|
1200
|
+
[
|
|
1201
|
+
"isLoading",
|
|
1202
|
+
"isPending",
|
|
1203
|
+
"isError",
|
|
1204
|
+
"error",
|
|
1205
|
+
"data",
|
|
1206
|
+
"length",
|
|
1207
|
+
"size",
|
|
1208
|
+
].includes(n.name.text)
|
|
1209
|
+
)
|
|
1210
|
+
shape = true;
|
|
1211
|
+
ts.forEachChild(n, inspect);
|
|
1212
|
+
};
|
|
1213
|
+
inspect(root);
|
|
1214
|
+
return shape
|
|
1215
|
+
? queryBacked(node, owner, ctx)
|
|
1216
|
+
? "remote"
|
|
1217
|
+
: "unobserved-data"
|
|
1218
|
+
: null;
|
|
1219
|
+
}
|
|
1220
|
+
function updatedCollectionContext(ctx, label, value, owner) {
|
|
1221
|
+
const next = { ...ctx, [label]: value, __iamCollectionTemplate: true };
|
|
1222
|
+
for (const [key, init] of ctx.__iamLocals)
|
|
1223
|
+
if (!ctx.__iamStates.has(key))
|
|
1224
|
+
next[key] = evalExpression(init, owner.module, next);
|
|
1225
|
+
return next;
|
|
1226
|
+
}
|
|
1227
|
+
function finiteCollection(node, owner, ctx, instance, depth) {
|
|
1228
|
+
if (ctx.__iamCollectionTemplate || !ts.isConditionalExpression(node))
|
|
1229
|
+
return null;
|
|
1230
|
+
const maps = [];
|
|
1231
|
+
const scan = (n) => {
|
|
1232
|
+
if (
|
|
1233
|
+
ts.isCallExpression(n) &&
|
|
1234
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
1235
|
+
n.expression.name.text === "map"
|
|
1236
|
+
)
|
|
1237
|
+
maps.push(n);
|
|
1238
|
+
if (!ts.isFunctionLike(n)) ts.forEachChild(n, scan);
|
|
1239
|
+
};
|
|
1240
|
+
scan(node);
|
|
1241
|
+
if (maps.length !== 1) return null;
|
|
1242
|
+
const receiver = maps[0].expression.expression,
|
|
1243
|
+
reads = collectionStateReads(ts, receiver, ctx),
|
|
1244
|
+
tabs = presentation.tabs.filter(
|
|
1245
|
+
(t) =>
|
|
1246
|
+
reads.includes(t.variableKey) &&
|
|
1247
|
+
Object.values(ctx.__iamVariableKeys).includes(t.variableKey),
|
|
1248
|
+
);
|
|
1249
|
+
if (tabs.length !== 1 || tabs[0].items.length < 2 || tabs[0].items.length > 8)
|
|
1250
|
+
return null;
|
|
1251
|
+
const tab = tabs[0],
|
|
1252
|
+
label = Object.keys(ctx.__iamVariableKeys).find(
|
|
1253
|
+
(k) => ctx.__iamVariableKeys[k] === tab.variableKey,
|
|
1254
|
+
);
|
|
1255
|
+
const candidates = tab.items.map((item) => {
|
|
1256
|
+
const next = updatedCollectionContext(ctx, label, item.value, owner);
|
|
1257
|
+
return {
|
|
1258
|
+
value: item.value,
|
|
1259
|
+
ctx: next,
|
|
1260
|
+
items: evalExpression(receiver, owner.module, next),
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
if (
|
|
1264
|
+
candidates.some((c) => !Array.isArray(c.items) || c.items.length > 128) ||
|
|
1265
|
+
new Set(candidates.map((c) => JSON.stringify(c.items))).size < 2
|
|
1266
|
+
)
|
|
1267
|
+
return null;
|
|
1268
|
+
const source = sourceLocation(owner.module, node, owner.symbol),
|
|
1269
|
+
captured = {};
|
|
1270
|
+
const capture = (n) => {
|
|
1271
|
+
if (
|
|
1272
|
+
ts.isIdentifier(n) &&
|
|
1273
|
+
Object.hasOwn(ctx, n.text) &&
|
|
1274
|
+
!n.text.startsWith("__iam")
|
|
1275
|
+
) {
|
|
1276
|
+
const value = ctx[n.text];
|
|
1277
|
+
captured[n.text] = isUnknown(value)
|
|
1278
|
+
? { kind: "unknown" }
|
|
1279
|
+
: value === undefined
|
|
1280
|
+
? { kind: "undefined" }
|
|
1281
|
+
: value?.[COMPONENT]
|
|
1282
|
+
? { kind: "component", key: value[COMPONENT].key }
|
|
1283
|
+
: value?.[LUCIDE_SOURCE]
|
|
1284
|
+
? { kind: "lucide", name: value[LUCIDE_SOURCE] }
|
|
1285
|
+
: { kind: "literal", value };
|
|
1286
|
+
}
|
|
1287
|
+
ts.forEachChild(n, capture);
|
|
1288
|
+
};
|
|
1289
|
+
capture(node);
|
|
1290
|
+
let cacheKey;
|
|
1291
|
+
try {
|
|
1292
|
+
cacheKey = identity("collection-template", {
|
|
1293
|
+
source,
|
|
1294
|
+
captured,
|
|
1295
|
+
cases: candidates.map((c) => ({ value: c.value, items: c.items })),
|
|
1296
|
+
});
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
let cases = collectionTemplates.get(cacheKey);
|
|
1301
|
+
if (!cases) {
|
|
1302
|
+
cases = [];
|
|
1303
|
+
for (const candidate of candidates) {
|
|
1304
|
+
const sizes = {
|
|
1305
|
+
bindings: ui.bindings.length,
|
|
1306
|
+
transitions: ui.transitions.length,
|
|
1307
|
+
variables: ui.variables.length,
|
|
1308
|
+
conditions: presentation.conditions.length,
|
|
1309
|
+
tabs: presentation.tabs.length,
|
|
1310
|
+
regions: presentation.regions.length,
|
|
1311
|
+
};
|
|
1312
|
+
const html = renderExpr(
|
|
1313
|
+
node,
|
|
1314
|
+
owner,
|
|
1315
|
+
candidate.ctx,
|
|
1316
|
+
cacheKey + "/" + candidate.value,
|
|
1317
|
+
// A shared source fragment owns its bounded subtree budget. App wrapper
|
|
1318
|
+
// depth must not erase leaf text from an otherwise identical collection.
|
|
1319
|
+
0,
|
|
1320
|
+
),
|
|
1321
|
+
fragment = sourceCollectionFragment(
|
|
1322
|
+
html,
|
|
1323
|
+
source,
|
|
1324
|
+
candidate.items.length,
|
|
1325
|
+
record,
|
|
1326
|
+
);
|
|
1327
|
+
for (const variable of ui.variables.slice(sizes.variables))
|
|
1328
|
+
uiVariableKeys.delete(variable.variableKey);
|
|
1329
|
+
ui.bindings.length = sizes.bindings;
|
|
1330
|
+
ui.transitions.length = sizes.transitions;
|
|
1331
|
+
ui.variables.length = sizes.variables;
|
|
1332
|
+
for (const key of ["conditions", "tabs", "regions"])
|
|
1333
|
+
presentation[key].length = sizes[key];
|
|
1334
|
+
if (
|
|
1335
|
+
!presentation.fragments.some(
|
|
1336
|
+
(f) => f.fragmentKey === fragment.fragmentKey,
|
|
1337
|
+
)
|
|
1338
|
+
)
|
|
1339
|
+
presentation.fragments.push(fragment);
|
|
1340
|
+
cases.push({ value: candidate.value, fragmentKey: fragment.fragmentKey });
|
|
1341
|
+
}
|
|
1342
|
+
collectionTemplates.set(cacheKey, cases);
|
|
1343
|
+
}
|
|
1344
|
+
const collectionKey = identity("ui-collection", {
|
|
1345
|
+
page: pageKey,
|
|
1346
|
+
instance,
|
|
1347
|
+
source,
|
|
1348
|
+
}),
|
|
1349
|
+
nodeId = identity("node", { page: pageKey, collectionKey }),
|
|
1350
|
+
elementKey = identity("element", {
|
|
1351
|
+
component: owner.key,
|
|
1352
|
+
sourceCollection: node.pos,
|
|
1353
|
+
});
|
|
1354
|
+
elements.set(elementKey, {
|
|
1355
|
+
elementKey,
|
|
1356
|
+
componentKey: owner.key,
|
|
1357
|
+
tag: "div",
|
|
1358
|
+
source,
|
|
1359
|
+
identityStrategy: "provisional_ast_ordinal",
|
|
1360
|
+
editableProperties: [],
|
|
1361
|
+
binding: "read_only",
|
|
1362
|
+
});
|
|
1363
|
+
record.set(nodeId, { elementKey, source, synthetic: true });
|
|
1364
|
+
const requires = reads
|
|
1365
|
+
.filter((key) => key !== tab.variableKey)
|
|
1366
|
+
.map((variableKey) => ({
|
|
1367
|
+
variableKey,
|
|
1368
|
+
value: ui.variables.find((v) => v.variableKey === variableKey)
|
|
1369
|
+
?.initialValue,
|
|
1370
|
+
}));
|
|
1371
|
+
presentation.collections.push({
|
|
1372
|
+
collectionKey,
|
|
1373
|
+
routeKey: pageKey,
|
|
1374
|
+
renderKey: "pending",
|
|
1375
|
+
nodeId,
|
|
1376
|
+
variableKey: tab.variableKey,
|
|
1377
|
+
requires,
|
|
1378
|
+
cases,
|
|
1379
|
+
source,
|
|
1380
|
+
});
|
|
1381
|
+
return (
|
|
1382
|
+
'<div data-iam-node="' +
|
|
1383
|
+
nodeId +
|
|
1384
|
+
'" data-iam-collection="' +
|
|
1385
|
+
collectionKey +
|
|
1386
|
+
'" data-iam-unobserved="true" hidden=""></div>'
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const iconGlyphs=new Set();
|
|
1390
|
+
function renderLucide(icon, node, owner, props, instance, shared=true) {
|
|
1391
|
+
const geometry = lucideGeometry(icon),
|
|
1392
|
+
source = sourceLocation(owner.module, node, owner.symbol);
|
|
1393
|
+
if (!geometry) return "";
|
|
1394
|
+
const element = (tag, attributes, children, part) => {
|
|
1395
|
+
const elementKey = identity("element", {
|
|
1396
|
+
component: owner.key,
|
|
1397
|
+
lucide: node.pos,
|
|
1398
|
+
part,
|
|
1399
|
+
tag,
|
|
1400
|
+
}),
|
|
1401
|
+
nodeId = identity("node", {
|
|
1402
|
+
page: pageKey,
|
|
1403
|
+
elementKey,
|
|
1404
|
+
instance,
|
|
1405
|
+
icon: icon[LUCIDE_SOURCE],
|
|
1406
|
+
});
|
|
1407
|
+
elements.set(elementKey, {
|
|
1408
|
+
elementKey,
|
|
1409
|
+
componentKey: owner.key,
|
|
1410
|
+
tag,
|
|
1411
|
+
source,
|
|
1412
|
+
identityStrategy: "provisional_ast_ordinal",
|
|
1413
|
+
editableProperties: [],
|
|
1414
|
+
binding: "read_only",
|
|
1415
|
+
});
|
|
1416
|
+
record.set(nodeId, { elementKey, source, synthetic: true });
|
|
1417
|
+
return (
|
|
1418
|
+
"<" +
|
|
1419
|
+
tag +
|
|
1420
|
+
' data-iam-node="' +
|
|
1421
|
+
nodeId +
|
|
1422
|
+
'" ' +
|
|
1423
|
+
Object.entries(attributes)
|
|
1424
|
+
.map(([key, value]) => key + '="' + escape(value) + '"')
|
|
1425
|
+
.join(" ") +
|
|
1426
|
+
">" +
|
|
1427
|
+
children +
|
|
1428
|
+
"</" +
|
|
1429
|
+
tag +
|
|
1430
|
+
">"
|
|
1431
|
+
);
|
|
1432
|
+
};
|
|
1433
|
+
const className = typeof props.className === "string" ? props.className : "";
|
|
1434
|
+
className
|
|
1435
|
+
.split(/\s+/)
|
|
1436
|
+
.filter(Boolean)
|
|
1437
|
+
.forEach((c) => allClasses.add(c));
|
|
1438
|
+
const glyphId='iam-glyph-'+sha256(String(icon[LUCIDE_SOURCE])).slice(0,24);
|
|
1439
|
+
let glyph='';if(shared&&!iconGlyphs.has(glyphId)){
|
|
1440
|
+
iconGlyphs.add(glyphId);glyph=element('defs',{},element('g',{id:glyphId},geometry.map(([tag,attrs],index)=>element(tag,attrs,'','glyph-'+glyphId+'-'+index)).join(''),'glyph-'+glyphId),'defs-'+glyphId);
|
|
937
1441
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1442
|
+
return element(
|
|
1443
|
+
"svg",
|
|
1444
|
+
{
|
|
1445
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1446
|
+
width: 24,
|
|
1447
|
+
height: 24,
|
|
1448
|
+
viewBox: "0 0 24 24",
|
|
1449
|
+
fill: "none",
|
|
1450
|
+
stroke: "currentColor",
|
|
1451
|
+
"stroke-width":
|
|
1452
|
+
typeof props.strokeWidth === "number" ? props.strokeWidth : 2,
|
|
1453
|
+
"stroke-linecap": "round",
|
|
1454
|
+
"stroke-linejoin": "round",
|
|
1455
|
+
"aria-hidden": "true",
|
|
1456
|
+
class: className,
|
|
1457
|
+
},
|
|
1458
|
+
shared?glyph+element('use',{href:'#'+glyphId},'','use'):geometry.map(([tag,attrs],index)=>element(tag,attrs,'',index)).join(''),
|
|
1459
|
+
"root",
|
|
1460
|
+
);
|
|
952
1461
|
}
|
|
953
|
-
function unobservedValue(node, owner, instance) {
|
|
1462
|
+
function unobservedValue(node, owner, instance,remote=false) {
|
|
954
1463
|
const source = sourceLocation(owner.module, node, owner.symbol);
|
|
955
1464
|
const elementKey = identity("element", { component: owner.key, unobservedValue: node.getStart(owner.module.source) - owner.fn.getStart(owner.module.source) });
|
|
956
1465
|
const nodeId = identity("node", { page: pageKey, element: elementKey, instance });
|
|
957
1466
|
elements.set(elementKey, { elementKey, componentKey: owner.key, tag: "span", source, identityStrategy: "provisional_ast_ordinal", editableProperties: [], binding: "preview_only" });
|
|
958
1467
|
record.set(nodeId, { elementKey, source, synthetic: true });
|
|
959
1468
|
// Preserve an unknown child boundary. An empty projection must never prove that replacing text is safe.
|
|
960
|
-
return '<span data-iam-node="' + nodeId + '" hidden=""></span>';
|
|
1469
|
+
return '<span data-iam-node="' + nodeId + '" data-iam-observation="'+(remote?'remote':'unobserved')+'" hidden=""></span>';
|
|
1470
|
+
}
|
|
1471
|
+
function renderInvocation(invocation,instance,depth){
|
|
1472
|
+
const {callback,args,caller}=invocation,next={...callback.ctx,__iamContexts:caller.__iamContexts??callback.ctx.__iamContexts,__iamOverlays:caller.__iamOverlays??callback.ctx.__iamOverlays};
|
|
1473
|
+
for(const [index,parameter]of callback.fn.parameters.entries()){
|
|
1474
|
+
const value=args[index];
|
|
1475
|
+
if(ts.isIdentifier(parameter.name))next[parameter.name.text]=value;
|
|
1476
|
+
else if(ts.isObjectBindingPattern(parameter.name))for(const field of parameter.name.elements)if(ts.isIdentifier(field.name))next[field.name.text]=!isUnknown(value)&&value&&typeof value==='object'?value[field.propertyName?.text||field.name.text]??UNKNOWN:UNKNOWN;
|
|
1477
|
+
}
|
|
1478
|
+
gap('RENDER_PROP_SOURCE_ONLY',callback.owner.module.file,callback.owner.symbol+': invoked JSX render-prop reconstructed from source; effects and business callbacks are not executed');
|
|
1479
|
+
return renderExpr(uiReturnExpression(callback.fn),callback.owner,next,callback.instance+'/call-'+instance,depth+1);
|
|
961
1480
|
}
|
|
1481
|
+
function hasSourceJsx(node){let found=false;const visit=n=>{if(ts.isJsxElement(n)||ts.isJsxSelfClosingElement(n)||ts.isJsxFragment(n)){found=true;return;}if(!ts.isFunctionLike(n))ts.forEachChild(n,visit);};if(node)visit(node);return found;}
|
|
962
1482
|
function renderExpr(node, owner, ctx, instance, depth) {
|
|
963
1483
|
node = unwrap(node);
|
|
964
1484
|
if (!node) return "";
|
|
965
1485
|
if (depth > 45) { gap("RENDER_DEPTH_LIMIT", owner.module.file, owner.symbol + ": JSX traversal budget exceeded"); return ""; }
|
|
966
1486
|
const m = owner.module;
|
|
1487
|
+
const collection=finiteCollection(node,owner,ctx,instance,depth);if(collection!==null)return collection;
|
|
1488
|
+
if(ts.isIdentifier(node)&&ctx.__iamLocals.has(node.text)&&hasSourceJsx(ctx.__iamLocals.get(node.text))){
|
|
1489
|
+
if(ctx.__iamActiveJsx?.has(node.text))return '';
|
|
1490
|
+
return renderExpr(ctx.__iamLocals.get(node.text),owner,{...ctx,__iamActiveJsx:new Set([...(ctx.__iamActiveJsx??[]),node.text])},instance+'/local-'+node.text,depth+1);
|
|
1491
|
+
}
|
|
967
1492
|
if (ts.isJsxFragment(node))
|
|
968
1493
|
return node.children
|
|
969
1494
|
.map((n, i) =>
|
|
@@ -973,6 +1498,13 @@ export async function analyze({
|
|
|
973
1498
|
if (ts.isJsxText(node)) return escape(jsxText(node.text));
|
|
974
1499
|
if (ts.isJsxExpression(node))
|
|
975
1500
|
return renderExpr(node.expression, owner, ctx, instance, depth + 1);
|
|
1501
|
+
if (ts.isBinaryExpression(node) && [ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken].includes(node.operatorToken.kind)) {
|
|
1502
|
+
const left = evalExpression(node.left, m, ctx);
|
|
1503
|
+
if (!isUnknown(left)) {
|
|
1504
|
+
const fallback = node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ? left == null : !left;
|
|
1505
|
+
return renderExpr(fallback ? node.right : node.left, owner, ctx, instance + '/fallback', depth + 1);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
976
1508
|
if(ts.isArrowFunction(node)){
|
|
977
1509
|
const parent=node.parent?.parent,tag=parent&&ts.isJsxElement(parent)?parent.openingElement.tagName.getText(m.source):'',imported=m.imports.get(tag),body=unwrap(node.body);
|
|
978
1510
|
if(imported?.specifier==='react-basics'&&imported.name==='Modal'&&!node.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&node.parameters.length<=3&&node.parameters.every(p=>ts.isIdentifier(p.name))&&(ts.isJsxElement(body)||ts.isJsxFragment(body)||ts.isJsxSelfClosingElement(body))){
|
|
@@ -1063,12 +1595,14 @@ export async function analyze({
|
|
|
1063
1595
|
gap("UNOBSERVED_CONDITION", m.file, label);
|
|
1064
1596
|
// Preserve both source alternatives for exact runtime binding. Neither
|
|
1065
1597
|
// is asserted to be authorized/visible, nor executed in source preview.
|
|
1598
|
+
const dataBranches=[yes,no].map(branch=>branch?evalExpression(branch,m,ctx):null),observation=remoteResultCondition(condition,owner,ctx)??(dataBranches.every(v=>v===null||['string','number'].includes(typeof v))?'unobserved-data':null),conditionRow={conditionKey:identity('ui-condition',{file:m.file,offset:condition.getStart(m.source)}),expression:label,source:sourceLocation(m,condition,owner.symbol),...(observation?{observation}:{})};
|
|
1599
|
+
presentation.conditions.push(conditionRow);
|
|
1066
1600
|
return [yes, no].map((branch,index)=>{
|
|
1067
1601
|
if(!branch)return '';
|
|
1068
1602
|
const elementKey=identity('element',{component:owner.key,state:label,unobserved:index,sourceOffset:condition.getStart(m.source)-owner.fn.getStart(m.source)}),id=identity('node',{page:pageKey,element:elementKey,instance:instance+'/unobserved-'+index}),source=sourceLocation(m,condition,owner.symbol);
|
|
1069
1603
|
record.set(id,{elementKey,source,synthetic:true});
|
|
1070
1604
|
elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});
|
|
1071
|
-
return '<div data-iam-node="'+id+'" data-iam-unobserved="true" hidden="">'+renderExpr(branch,owner,ctx,instance+'/unobserved-'+index,depth+1)+'</div>';
|
|
1605
|
+
return '<div data-iam-node="'+id+'" data-iam-unobserved="true" hidden="" data-iam-condition="'+conditionRow.conditionKey+'" data-iam-alternative="'+(index===0)+'">'+renderExpr(branch,owner,ctx,instance+'/unobserved-'+index,depth+1)+'</div>';
|
|
1072
1606
|
}).join('');
|
|
1073
1607
|
}
|
|
1074
1608
|
if (
|
|
@@ -1083,22 +1617,28 @@ export async function analyze({
|
|
|
1083
1617
|
fn &&
|
|
1084
1618
|
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))
|
|
1085
1619
|
) {
|
|
1086
|
-
|
|
1620
|
+
const limit=ctx.__iamCollectionTemplate?128:50;
|
|
1621
|
+
if (values.length > limit)
|
|
1087
1622
|
gap(
|
|
1088
1623
|
"COLLECTION_TRUNCATED",
|
|
1089
1624
|
m.file,
|
|
1090
1625
|
"Static collection limited to 50 preview items",
|
|
1091
1626
|
);
|
|
1092
1627
|
return values
|
|
1093
|
-
.slice(0,
|
|
1628
|
+
.slice(0, limit)
|
|
1094
1629
|
.map((value, i) => {
|
|
1095
1630
|
const next = { ...ctx };
|
|
1096
1631
|
if (fn.parameters[0] && ts.isIdentifier(fn.parameters[0].name))
|
|
1097
1632
|
next[fn.parameters[0].name.text] = value;
|
|
1098
1633
|
if (fn.parameters[1] && ts.isIdentifier(fn.parameters[1].name))
|
|
1099
1634
|
next[fn.parameters[1].name.text] = i;
|
|
1635
|
+
if(fn.parameters[0]&&ts.isObjectBindingPattern(fn.parameters[0].name))for(const field of fn.parameters[0].name.elements){
|
|
1636
|
+
if(!ts.isIdentifier(field.name)||field.dotDotDotToken)continue;
|
|
1637
|
+
const key=field.propertyName?.text||field.name.text;next[field.name.text]=value&&typeof value==='object'&&!isUnknown(value)&&Object.hasOwn(value,key)?value[key]:field.initializer?evalExpression(field.initializer,m,next):undefined;
|
|
1638
|
+
}
|
|
1639
|
+
if(ts.isBlock(fn.body))for(const statement of fn.body.statements)if(ts.isVariableStatement(statement))for(const decl of statement.declarationList.declarations)if(ts.isIdentifier(decl.name))next[decl.name.text]=evalExpression(decl.initializer,m,next);
|
|
1100
1640
|
return renderExpr(
|
|
1101
|
-
|
|
1641
|
+
uiReturnExpression(fn),
|
|
1102
1642
|
owner,
|
|
1103
1643
|
next,
|
|
1104
1644
|
instance + "/item-" + i,
|
|
@@ -1108,7 +1648,7 @@ export async function analyze({
|
|
|
1108
1648
|
.join("");
|
|
1109
1649
|
}
|
|
1110
1650
|
gap("DYNAMIC_COLLECTION", m.file, node.expression.getText(m.source));
|
|
1111
|
-
return unobservedValue(node, owner, instance);
|
|
1651
|
+
return unobservedValue(node, owner, instance,queryBacked(node.expression.expression,owner,ctx));
|
|
1112
1652
|
}
|
|
1113
1653
|
if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1114
1654
|
if (
|
|
@@ -1118,6 +1658,9 @@ export async function analyze({
|
|
|
1118
1658
|
)
|
|
1119
1659
|
return ctx[node.text].iamChildren;
|
|
1120
1660
|
const value = evalExpression(node, m, ctx), expr = uiExpression(node, owner, ctx);
|
|
1661
|
+
if(value?.iamRenderInvocation)return renderInvocation(value.iamRenderInvocation,instance,depth);
|
|
1662
|
+
if(value?.iamRenderChildren)return value.iamRenderChildren(ctx);
|
|
1663
|
+
if(value?.iamChildren!==undefined)return value.iamChildren;
|
|
1121
1664
|
if (expr && readVariables(expr).length && ["string", "number"].includes(typeof value)) {
|
|
1122
1665
|
const source = sourceLocation(m, node, owner.symbol), elementKey = identity("element", { component: owner.key, stateTextOffset: node.getStart(m.source) - owner.fn.getStart(m.source) }), id = identity("node", { page: pageKey, element: elementKey, instance });
|
|
1123
1666
|
record.set(id, { elementKey, source, synthetic: true });
|
|
@@ -1134,7 +1677,7 @@ export async function analyze({
|
|
|
1134
1677
|
return escape(value);
|
|
1135
1678
|
if (isUnknown(value)) {
|
|
1136
1679
|
gap("DYNAMIC_VALUE", m.file, node.getText(m.source).slice(0, 100));
|
|
1137
|
-
return unobservedValue(node, owner, instance);
|
|
1680
|
+
return unobservedValue(node, owner, instance,queryBacked(node,owner,ctx));
|
|
1138
1681
|
}
|
|
1139
1682
|
return "";
|
|
1140
1683
|
}
|
|
@@ -1142,12 +1685,13 @@ export async function analyze({
|
|
|
1142
1685
|
throw new Error("Rendered node budget exceeded");
|
|
1143
1686
|
const opening = ts.isJsxElement(node) ? node.openingElement : node,
|
|
1144
1687
|
tag = opening.tagName.getText(m.source),
|
|
1145
|
-
props = m.anchors.get(opening) ? { "data-indiana-id": m.anchors.get(opening) } : {}, uiHandlers = [], uiBindings = [], forwardedHandlers=[];
|
|
1688
|
+
props = m.anchors.get(opening) ? { "data-indiana-id": m.anchors.get(opening) } : {}, uiHandlers = [], uiBindings = [], forwardedHandlers=[],uiAttributes=[];
|
|
1146
1689
|
for (const attr of opening.attributes.properties) {
|
|
1147
1690
|
if (ts.isJsxSpreadAttribute(attr)) {
|
|
1148
1691
|
const v = evalExpression(attr.expression, m, ctx);
|
|
1149
1692
|
if (isUnknown(v)) {
|
|
1150
1693
|
for (const attribute of anchorAttributes) delete props[attribute];
|
|
1694
|
+
props.__iamOpaqueProps = true;
|
|
1151
1695
|
gap("UNKNOWN_ANCHOR_SPREAD", m.file, "Opaque spread may override element identity");
|
|
1152
1696
|
continue;
|
|
1153
1697
|
}
|
|
@@ -1157,17 +1701,22 @@ export async function analyze({
|
|
|
1157
1701
|
for (const key of Object.keys(v)) {
|
|
1158
1702
|
for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
|
|
1159
1703
|
for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
|
|
1704
|
+
for (let i = uiAttributes.length - 1; i >= 0; i--) if (uiAttributes[i].key === key) uiAttributes.splice(i, 1);
|
|
1160
1705
|
}
|
|
1161
|
-
if (inherited) { uiBindings.push(...inherited.bindings); uiHandlers.push(...inherited.handlers); }
|
|
1706
|
+
if (inherited) { uiBindings.push(...inherited.bindings); uiHandlers.push(...inherited.handlers); uiAttributes.push(...(inherited.attributes??[])); }
|
|
1162
1707
|
}
|
|
1163
1708
|
continue;
|
|
1164
1709
|
}
|
|
1165
1710
|
const key = attr.name.getText(m.source);
|
|
1711
|
+
for(let i=uiAttributes.length-1;i>=0;i--)if(uiAttributes[i].key===key)uiAttributes.splice(i,1);
|
|
1712
|
+
if(!ctx.__iamCollectionTemplate&&['className','placeholder','aria-label'].includes(key)&&attr.initializer&&ts.isJsxExpression(attr.initializer)){
|
|
1713
|
+
const reads=collectionStateReads(ts,attr.initializer.expression,ctx);if(reads.length)uiAttributes.push({key,owner,ctx,expression:attr.initializer.expression,property:key==='className'?'class':key,reads,source:sourceLocation(m,attr,owner.symbol)});
|
|
1714
|
+
}
|
|
1166
1715
|
for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
|
|
1167
1716
|
for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
|
|
1168
|
-
if (["onClick", "onChange", "onInput"].includes(key)) {
|
|
1717
|
+
if (["onClick", "onChange", "onInput", "onValueChange", "onOpenChange"].includes(key)) {
|
|
1169
1718
|
props[key] = undefined;
|
|
1170
|
-
const supported = uiHandler(attr.initializer, owner, ctx);
|
|
1719
|
+
const supported = ctx.__iamCollectionTemplate?null:uiHandler(attr.initializer, owner, ctx, key);
|
|
1171
1720
|
if (supported) { props[key] = undefined; uiHandlers.push({ ...supported, key, source: sourceLocation(m, attr, owner.symbol) }); }
|
|
1172
1721
|
const fn =
|
|
1173
1722
|
attr.initializer && ts.isJsxExpression(attr.initializer)
|
|
@@ -1199,7 +1748,7 @@ export async function analyze({
|
|
|
1199
1748
|
key +
|
|
1200
1749
|
" at line " +
|
|
1201
1750
|
sourceLocation(m, attr, owner.symbol).line +
|
|
1202
|
-
":
|
|
1751
|
+
": handler is not simulated; no business execution",
|
|
1203
1752
|
);
|
|
1204
1753
|
continue;
|
|
1205
1754
|
}
|
|
@@ -1225,6 +1774,16 @@ export async function analyze({
|
|
|
1225
1774
|
const expr = uiExpression(attr.initializer.expression, owner, ctx);
|
|
1226
1775
|
if (expr && readVariables(expr).length) uiBindings.push({ property: key, value: expr, source: sourceLocation(m, attr, owner.symbol) });
|
|
1227
1776
|
}
|
|
1777
|
+
if(attr.initializer&&ts.isJsxExpression(attr.initializer)){
|
|
1778
|
+
let fn=unwrap(attr.initializer.expression);const visited=new Set();
|
|
1779
|
+
while(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text)&&!visited.has(fn.text)){visited.add(fn.text);fn=unwrap(ctx.__iamLocals.get(fn.text));}
|
|
1780
|
+
if(fn&&(ts.isArrowFunction(fn)||ts.isFunctionExpression(fn))){
|
|
1781
|
+
let jsx=false;const inspect=n=>{if(ts.isJsxElement(n)||ts.isJsxSelfClosingElement(n)||ts.isJsxFragment(n))jsx=true;ts.forEachChild(n,inspect);};inspect(fn.body);
|
|
1782
|
+
const safe=!fn.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&(!ts.isBlock(fn.body)||fn.body.statements.every(s=>ts.isVariableStatement(s)||ts.isIfStatement(s)||ts.isReturnStatement(s)));
|
|
1783
|
+
if(jsx&&safe){props[key]={iamRenderProp:{fn,owner,ctx,instance:instance+'/prop-'+key}};continue;}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if(attr.initializer&&ts.isJsxExpression(attr.initializer)&&hasSourceJsx(attr.initializer.expression)){const value=attr.initializer.expression;props[key]={iamRenderChildren:inherited=>renderExpr(value,owner,{...ctx,__iamContexts:inherited.__iamContexts??ctx.__iamContexts,__iamTabs:inherited.__iamTabs??ctx.__iamTabs,__iamOverlays:inherited.__iamOverlays??ctx.__iamOverlays},instance+'/prop-'+key,depth+1)};continue;}
|
|
1228
1787
|
props[key] = !attr.initializer
|
|
1229
1788
|
? true
|
|
1230
1789
|
: ts.isStringLiteral(attr.initializer)
|
|
@@ -1235,24 +1794,96 @@ export async function analyze({
|
|
|
1235
1794
|
gap('DYNAMIC_CLASS_PARTIAL',m.file,'className at line '+sourceLocation(m,attr,owner.symbol).line+': only definitely present class tokens retained; unresolved styling needs runtime observation');
|
|
1236
1795
|
}
|
|
1237
1796
|
}
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1797
|
+
const chart=sourceChart({ts,node:opening,tag,owner,context:ctx,evaluate:evalExpression,imported:externalImport});
|
|
1798
|
+
if(chart){
|
|
1799
|
+
gap(chart.unobserved?'CHART_DATA_UNOBSERVED':'CHART_SOURCE_PROJECTION',m.file,chart.unobserved?'Chart data is dynamic, random or outside bounds; no values are invented.':'Bounded source data rendered as inert SVG; library effects, animation and tooltips are not executed.');
|
|
1800
|
+
const source=sourceLocation(m,node,owner.symbol);
|
|
1801
|
+
const draw=(item,part)=>{
|
|
1802
|
+
if(typeof item==='string')return escape(item);
|
|
1803
|
+
const elementKey=identity('element',{component:owner.key,chart:node.pos,part}),nodeId=identity('node',{page:pageKey,elementKey,instance});
|
|
1804
|
+
elements.set(elementKey,{elementKey,componentKey:owner.key,tag:item.tag,source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(nodeId,{elementKey,source,synthetic:true});
|
|
1805
|
+
return '<'+item.tag+' data-iam-node="'+nodeId+'" '+Object.entries(item.attrs).map(([k,v])=>k+'="'+escape(v)+'"').join(' ')+'>'+item.children.map((child,i)=>draw(child,part+'/'+i)).join('')+'</'+item.tag+'>';
|
|
1806
|
+
};
|
|
1807
|
+
return draw({tag:'svg',attrs:{xmlns:'http://www.w3.org/2000/svg',viewBox:'0 0 '+chart.width+' '+chart.height,width:chart.width,height:chart.height,role:'img','aria-label':chart.unobserved?'Chart: data unobserved':'Source chart projection','data-iam-chart':chart.unobserved?'unobserved':'source',style:'display:block;width:100%;height:auto;max-height:'+chart.height+'px'},children:chart.children},'root');
|
|
1808
|
+
}
|
|
1809
|
+
const sourceIcon=evalExpression(opening.tagName,m,ctx);
|
|
1810
|
+
if(sourceIcon?.[LUCIDE_SOURCE])return renderLucide(sourceIcon,node,owner,props,instance,!ctx.__iamCollectionTemplate);
|
|
1811
|
+
const tabPart=tabsPrimitive(m,tag),basePart=basePrimitive(m,tag),overlay=basePart&&basePart.family!=='Tabs'?basePart:null;
|
|
1812
|
+
let overlayGroup=overlay?ctx.__iamOverlays?.get(overlay.family):null;
|
|
1813
|
+
let childCtx=ctx,tabGroup=ctx.__iamTabs;
|
|
1814
|
+
const provider=tag.endsWith('.Provider')?contextKey(m,tag.slice(0,-9)):contextKey(m,tag);
|
|
1815
|
+
if(provider){const contexts=new Map(ctx.__iamContexts);contexts.set(provider,props.value);childCtx={...childCtx,__iamContexts:contexts};}
|
|
1816
|
+
if(overlay&&['Root','SubmenuRoot'].includes(overlay.part)){
|
|
1817
|
+
const openProperty=overlay.family==='Transition'?'in':'open',open=props[openProperty];
|
|
1818
|
+
overlayGroup={open:open===true||open===undefined&&props.defaultOpen===true,variable:uiBindings.find(b=>b.property===openProperty)?.value,controller:uiHandlers.find(h=>h.key==='onOpenChange')};
|
|
1819
|
+
if(overlay.family!=='Transition'&&open===undefined&&!props.__iamOpaqueProps&&(props.defaultOpen===undefined||typeof props.defaultOpen==='boolean')){
|
|
1820
|
+
const scopeKey=identity('primitive-state',{page:pageKey,instance,family:overlay.family}),variableKey=identity('ui-variable',{scope:scopeKey,label:'open'});
|
|
1821
|
+
ui.variables.push({variableKey,routeKey:pageKey,scopeKey,label:overlay.family+'.open',valueType:'boolean',initialValue:overlayGroup.open,source:sourceLocation(m,node,owner.symbol)});
|
|
1822
|
+
overlayGroup.variable={op:'var',key:variableKey};
|
|
1823
|
+
overlayGroup.controller={variableKey,value:{op:'event',key:'value'},payload:{value:'boolean'}};
|
|
1824
|
+
}
|
|
1825
|
+
if(overlay.family==='Transition'){
|
|
1826
|
+
const attr=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text==='onEnter');
|
|
1827
|
+
const callback=attr?.initializer&&ts.isJsxExpression(attr.initializer)?unwrap(attr.initializer.expression):null;
|
|
1828
|
+
let statement=callback&&ts.isArrowFunction(callback)&&!callback.parameters.length&&ts.isBlock(callback.body)?callback.body.statements[0]:null;
|
|
1829
|
+
if(statement&&ts.isIfStatement(statement))statement=evalExpression(statement.expression,m,ctx)===true?statement.thenStatement:null;
|
|
1830
|
+
if(statement&&ts.isBlock(statement))statement=statement.statements[0];
|
|
1831
|
+
const assignment=statement&&ts.isExpressionStatement(statement)?unwrap(statement.expression):null;
|
|
1832
|
+
const left=assignment&&ts.isBinaryExpression(assignment)&&assignment.operatorToken.kind===ts.SyntaxKind.EqualsToken?assignment.left:null;
|
|
1833
|
+
if(left&&ts.isPropertyAccessExpression(left)&&left.name.text==='display'&&ts.isPropertyAccessExpression(left.expression)&&left.expression.name.text==='style'&&ts.isPropertyAccessExpression(left.expression.expression)&&left.expression.expression.name.text==='current'&&ts.isIdentifier(left.expression.expression.expression)&&(assignment.right.kind===ts.SyntaxKind.NullKeyword||ts.isStringLiteral(assignment.right)&&assignment.right.text==='')){
|
|
1834
|
+
overlayGroup.reveal={ref:left.expression.expression.expression.text,file:m.file,symbol:owner.symbol};
|
|
1835
|
+
gap('TRANSITION_VISIBILITY_SOURCE_ONLY',m.file,'Source onEnter clears the referenced child display style; only that UI assignment is projected, without running callbacks or effects.');
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if(isUnknown(open)){
|
|
1839
|
+
const attribute=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text===openProperty),expression=attribute?.initializer&&ts.isJsxExpression(attribute.initializer)?attribute.initializer.expression:opening;
|
|
1840
|
+
overlayGroup.condition={conditionKey:identity('ui-condition',{file:m.file,offset:expression.getStart(m.source)}),expression:attribute?redactLiteral(expression.getText(m.source)).slice(0,160):'open (forwarded)',source:sourceLocation(m,expression,owner.symbol)};
|
|
1841
|
+
presentation.conditions.push(overlayGroup.condition);
|
|
1842
|
+
}
|
|
1843
|
+
const overlays=new Map(ctx.__iamOverlays);overlays.set(overlay.family,overlayGroup);childCtx={...childCtx,__iamOverlays:overlays};
|
|
1844
|
+
}
|
|
1845
|
+
if(tabPart==='Root') {
|
|
1846
|
+
const valueBinding=uiBindings.find(b=>b.property==='value')?.value;
|
|
1847
|
+
const controller=uiHandlers.find(h=>h.key==='onValueChange');
|
|
1848
|
+
tabGroup={tabKey:identity('ui-tab',{page:pageKey,scope:ctx.__iamScope,instance}),routeKey:pageKey,renderKey:'pending',nodeId:'pending',variableKey:valueBinding?.op==='var'?valueBinding.key:null,initialValue:typeof props.value==='string'?props.value:typeof props.defaultValue==='string'?props.defaultValue:null,items:[],source:sourceLocation(m,node,owner.symbol),scopeSource:ui.variables.find(v=>v.variableKey===valueBinding?.key)?.source??sourceLocation(m,node,owner.symbol),controller};
|
|
1849
|
+
childCtx={...childCtx,__iamTabs:tabGroup};
|
|
1850
|
+
}
|
|
1851
|
+
const localRef=componentBinding(m,tag),localTarget=ctx[tag]?.[COMPONENT]??(localRef&&componentDefs.get(localRef.module.file+'#'+localRef.name));
|
|
1852
|
+
const dynamicNode=ctx.__iamLocals.get(tag);
|
|
1853
|
+
const registry=!localTarget&&dynamicNode?componentRegistry({ts,node:dynamicNode,module:m,locals:ctx.__iamLocals,binding,functionFrom,value:(n,module)=>evalExpression(n,module,ctx),component:(n,module)=>sourceComponent(n,module)?.[COMPONENT],variable:name=>ctx.__iamVariableKeys[name]??(ctx.__iamPropBindings.get(name)?.op==='var'?ctx.__iamPropBindings.get(name).key:null),location:sourceLocation,unknown:UNKNOWN,imported:externalImport}):null;
|
|
1854
|
+
const renderChildren=(inherited=childCtx)=>ts.isJsxElement(node)?node.children.map((child,i)=>renderExpr(child,owner,{...childCtx,__iamContexts:inherited.__iamContexts??childCtx.__iamContexts,__iamTabs:inherited.__iamTabs??childCtx.__iamTabs,__iamOverlays:inherited.__iamOverlays??childCtx.__iamOverlays},instance+'/child-'+i,depth+1)).join(''):props.children?.iamRenderChildren?props.children.iamRenderChildren(inherited):props.children?.iamChildren||'';
|
|
1855
|
+
const children = localTarget||registry ? '' : renderChildren();
|
|
1856
|
+
|
|
1857
|
+
// Project the documented Controller render callback from source, with no
|
|
1858
|
+
// form initialization, live field values or business callback execution.
|
|
1859
|
+
const formPrimitive = externalImport(m, tag);
|
|
1860
|
+
if (formPrimitive?.specifier === 'react-hook-form') {
|
|
1861
|
+
if (formPrimitive.name === 'FormProvider') return children;
|
|
1862
|
+
if (formPrimitive.name === 'Controller' && props.render?.iamRenderProp)
|
|
1863
|
+
return renderInvocation({ callback: props.render.iamRenderProp,
|
|
1864
|
+
args: [{ field: { name: typeof props.name === 'string' ? props.name : UNKNOWN }, fieldState: UNKNOWN, formState: UNKNOWN }],
|
|
1865
|
+
caller: childCtx }, instance + '/controller', depth + 1);
|
|
1866
|
+
}
|
|
1867
|
+
const slotPrimitive = ctx[tag]?.[UI_PRIMITIVE]??externalImport(m, tag);
|
|
1868
|
+
if (slotPrimitive?.specifier === '@radix-ui/react-slot' && ['Slot', 'Root'].includes(slotPrimitive.name)) {
|
|
1869
|
+
const fragment=JSDOM.fragment(children),only=[...fragment.childNodes].filter(n=>n.nodeType!==3||n.textContent.trim());
|
|
1870
|
+
if(only.length!==1||only[0].nodeType!==1){gap('SLOT_CHILD_UNSUPPORTED',m.file,'Slot requires one concrete element; source-only children retained without prop merging');return children;}
|
|
1871
|
+
const child=only[0];
|
|
1872
|
+
if(typeof props.className==='string'){
|
|
1873
|
+
const names=[props.className,child.getAttribute('class')].filter(Boolean).join(' ');child.setAttribute('class',names);names.split(/\s+/).filter(Boolean).forEach(c=>allClasses.add(c));
|
|
1874
|
+
}
|
|
1875
|
+
for(const [key,value]of Object.entries(props))if(/^(?:title|role|data-(?:slot|state|side|variant|collapsible|active)|aria-[a-z-]+)$/.test(key)&&['string','number','boolean'].includes(typeof value)&&!child.hasAttribute(key))child.setAttribute(key,String(value));
|
|
1876
|
+
if(props.style&&typeof props.style==='object'){const css=staticStyle(props.style,()=>gap('INLINE_STYLE_UNSUPPORTED',m.file,'Unresolved Slot style'));if(css)child.setAttribute('style',css+';'+(child.getAttribute('style')||''));}
|
|
1877
|
+
return [...fragment.childNodes].map(n=>n.nodeType===1?n.outerHTML:n.textContent).join('');
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1251
1880
|
if (ctx[tag]?.iamChildren !== undefined) return ctx[tag].iamChildren;
|
|
1881
|
+
if (ctx[tag]?.iamRenderChildren) return ctx[tag].iamRenderChildren(ctx);
|
|
1252
1882
|
const routerPrimitive = externalImport(m, tag);
|
|
1253
|
-
if ([
|
|
1883
|
+
if (['react-router','react-router-dom'].includes(routerPrimitive?.specifier)&&routerPrimitive.name==='Navigate')return '';
|
|
1884
|
+
if (["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamRenderChildren ? ctx.__iamOutlet.iamRenderChildren(ctx) : ctx.__iamOutlet?.iamChildren ?? "";
|
|
1254
1885
|
if (
|
|
1255
|
-
/^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
|
|
1886
|
+
!localTarget && /^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
|
|
1256
1887
|
tag,
|
|
1257
1888
|
)
|
|
1258
1889
|
)
|
|
@@ -1260,12 +1891,26 @@ export async function analyze({
|
|
|
1260
1891
|
const intrinsicAlias = typeof ctx[tag] === "string" && allowedTags.has(ctx[tag]);
|
|
1261
1892
|
const importedPrimitive=m.imports.get(tag),knownExternalPrimitive=importedPrimitive?.specifier==='react-basics'&&['Text','Button','SubmitButton'].includes(importedPrimitive.name);
|
|
1262
1893
|
let htmlTag = intrinsicAlias ? ctx[tag] : tag;
|
|
1894
|
+
if(provider)return children;
|
|
1263
1895
|
if (tag === "html" || tag === "body") {
|
|
1264
1896
|
htmlTag = "div";
|
|
1265
1897
|
props["data-iam-adapter"] = tag;
|
|
1266
1898
|
}
|
|
1267
1899
|
if (/^[A-Z]/.test(tag) && !intrinsicAlias) {
|
|
1268
|
-
if
|
|
1900
|
+
if(tabPart){
|
|
1901
|
+
htmlTag=tabPart==='Tab'?'button':'div';
|
|
1902
|
+
if(tabPart==='List')props.role='tablist';
|
|
1903
|
+
if(tabPart==='Tab'){props.role='tab';props.type='button';props['aria-selected']=tabGroup?.initialValue===props.value;}
|
|
1904
|
+
if(tabPart==='Panel'){props.role='tabpanel';props.hidden=tabGroup?.initialValue!==props.value;}
|
|
1905
|
+
}
|
|
1906
|
+
else if(overlay){
|
|
1907
|
+
htmlTag=['Trigger','Close','Item','CheckboxItem','RadioItem','SubmenuTrigger'].includes(overlay.part)&&props.asChild!==true?'button':overlay.part==='Title'?'h2':overlay.part==='Description'?'p':'div';
|
|
1908
|
+
if(htmlTag==='button')props.type='button';
|
|
1909
|
+
if(['Portal','Popup','Backdrop'].includes(overlay.part)||overlay.family==='Transition'){props.hidden=!overlayGroup?.open;if(overlayGroup?.condition){props['data-iam-condition']=overlayGroup.condition.conditionKey;props['data-iam-alternative']='true';props['data-iam-condition-default']='false';props['data-iam-unobserved']='true';}if(overlayGroup?.variable)uiBindings.push({property:'visible',value:overlayGroup.variable,source:sourceLocation(m,node,owner.symbol)});}
|
|
1910
|
+
if(['Root','SubmenuRoot','Portal','Provider'].includes(overlay.part)||props.asChild===true)props.style={display:'contents'};
|
|
1911
|
+
if(overlay.part==='Popup')props.role=overlay.family==='Menu'?'menu':overlay.family==='Tooltip'?'tooltip':'dialog';
|
|
1912
|
+
}
|
|
1913
|
+
else if (tag === "Link" || ["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
|
|
1269
1914
|
htmlTag = "a";
|
|
1270
1915
|
if (typeof props.to === "string") props.href = props.to;
|
|
1271
1916
|
}
|
|
@@ -1284,14 +1929,18 @@ export async function analyze({
|
|
|
1284
1929
|
.filter((v) => typeof v === "string")
|
|
1285
1930
|
.join(" ");
|
|
1286
1931
|
} else {
|
|
1287
|
-
const ref =
|
|
1288
|
-
target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
|
|
1932
|
+
const ref = componentBinding(m, tag),
|
|
1933
|
+
target = ctx[tag]?.[COMPONENT]??(ref && componentDefs.get(ref.module.file + "#" + ref.name));
|
|
1289
1934
|
if (target) {
|
|
1290
1935
|
const own = components.get(owner.key);
|
|
1291
1936
|
if (!own.dependencies.includes(target.key))
|
|
1292
1937
|
own.dependencies.push(target.key);
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1938
|
+
const hasChildren=ts.isJsxElement(node)&&node.children.some(n=>!ts.isJsxText(n)||jsxText(n.text).trim());
|
|
1939
|
+
const meaningful=ts.isJsxElement(node)?node.children.filter(n=>!ts.isJsxText(n)||jsxText(n.text).trim()):[];
|
|
1940
|
+
const childCallback=meaningful.length===1&&ts.isJsxExpression(meaningful[0])?unwrap(meaningful[0].expression):null;
|
|
1941
|
+
const callbackBody=childCallback&&(ts.isArrowFunction(childCallback)||ts.isFunctionExpression(childCallback))&&!childCallback.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&(!ts.isBlock(childCallback.body)||childCallback.body.statements.every(s=>ts.isVariableStatement(s)||ts.isIfStatement(s)||ts.isReturnStatement(s)))&&hasSourceJsx(uiReturnExpression(childCallback));
|
|
1942
|
+
const childProps = { ...props, ...(hasChildren?{children:callbackBody?{iamRenderProp:{fn:childCallback,owner,ctx,instance:instance+'/render-child'}}:{iamRenderChildren:renderChildren}}:{}), __iamCollectionTemplate:ctx.__iamCollectionTemplate, __iamOutlet: ctx.__iamOutlet, __iamContexts:ctx.__iamContexts, __iamTabs:ctx.__iamTabs, __iamOverlays:ctx.__iamOverlays };
|
|
1943
|
+
forwarded.set(childProps, { bindings: uiBindings, handlers: [...uiHandlers,...forwardedHandlers], attributes:uiAttributes });
|
|
1295
1944
|
return renderComponent(
|
|
1296
1945
|
target,
|
|
1297
1946
|
childProps,
|
|
@@ -1299,6 +1948,23 @@ export async function analyze({
|
|
|
1299
1948
|
depth + 1,
|
|
1300
1949
|
);
|
|
1301
1950
|
}
|
|
1951
|
+
if(registry){
|
|
1952
|
+
const regionKey=identity('ui-region',{page:pageKey,instance,tag}),regionNode=identity('node',{page:pageKey,instance,regionKey});
|
|
1953
|
+
const source=sourceLocation(m,opening,owner.symbol),elementKey=identity('element',{component:owner.key,regionOffset:opening.getStart(m.source)-owner.fn.getStart(m.source)});
|
|
1954
|
+
elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(regionNode,{elementKey,source,synthetic:true});
|
|
1955
|
+
const row={regionKey,routeKey:pageKey,renderKey:'pending',nodeId:regionNode,expression:redactLiteral(dynamicNode.getText(m.source)).slice(0,160),variableKey:registry.variableKey,initialValue:ui.variables.find(v=>v.variableKey===registry.variableKey)?.initialValue??null,options:[],source};
|
|
1956
|
+
if(typeof row.initialValue!=='string'||!registry.options.some(o=>o.value===row.initialValue))row.initialValue=null;
|
|
1957
|
+
let html='<div data-iam-node="'+regionNode+'" data-iam-region="'+regionKey+'">';
|
|
1958
|
+
for(const option of registry.options){
|
|
1959
|
+
const id=identity('node',{page:pageKey,regionKey,value:option.value}),optionElement=identity('element',{component:owner.key,regionKey,value:option.value});
|
|
1960
|
+
elements.set(optionElement,{elementKey:optionElement,componentKey:owner.key,tag:'div',source:option.source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(id,{elementKey:optionElement,source:option.source,synthetic:true});
|
|
1961
|
+
const catalog=ctx.t?.[TRANSLATION]??(translationResources.size===1?[...translationResources][0]:null),title=catalog?option.label.split('.').reduce((v,key)=>v?.[key],catalog):undefined;
|
|
1962
|
+
row.options.push({value:option.value,label:typeof title==='string'?title:option.label,nodeId:id,componentKey:option.component.key,source:option.source,scope:option.scope});
|
|
1963
|
+
const own=components.get(owner.key);if(!own.dependencies.includes(option.component.key))own.dependencies.push(option.component.key);
|
|
1964
|
+
html+='<div data-iam-node="'+id+'" data-iam-region-value="'+escape(option.value)+'" data-iam-unobserved="true" hidden="">'+renderComponent(option.component,{...props,children:{iamRenderChildren:renderChildren},__iamOutlet:ctx.__iamOutlet,__iamContexts:ctx.__iamContexts,__iamOverlays:ctx.__iamOverlays},instance+'/registry-'+option.value,depth+1)+'</div>';
|
|
1965
|
+
}
|
|
1966
|
+
html+='</div>';presentation.regions.push(row);gap('DYNAMIC_COMPONENT_CANDIDATES',m.file,tag+': finite registry alternatives retained as source-only UI previews; selection and scope are unobserved');return html;
|
|
1967
|
+
}
|
|
1302
1968
|
htmlTag = /Button|Trigger/.test(tag)
|
|
1303
1969
|
? "button"
|
|
1304
1970
|
: /Input/.test(tag)
|
|
@@ -1311,6 +1977,10 @@ export async function analyze({
|
|
|
1311
1977
|
}
|
|
1312
1978
|
}
|
|
1313
1979
|
if (!allowedTags.has(htmlTag)) return children;
|
|
1980
|
+
const reveal=ctx.__iamOverlays?.get('Transition')?.reveal,ref=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text==='ref');
|
|
1981
|
+
if(reveal?.file===m.file&&reveal.symbol===owner.symbol&&ref?.initializer&&ts.isJsxExpression(ref.initializer)&&ts.isIdentifier(ref.initializer.expression)&&ref.initializer.expression.text===reveal.ref&&props.style?.display==='none'){
|
|
1982
|
+
const {display,...style}=props.style;props.style=style;
|
|
1983
|
+
}
|
|
1314
1984
|
const anchor = anchorAttributes
|
|
1315
1985
|
.map((key) =>
|
|
1316
1986
|
typeof props[key] === "string" ? key + "=" + props[key] : null,
|
|
@@ -1335,6 +2005,7 @@ export async function analyze({
|
|
|
1335
2005
|
element: elementKey,
|
|
1336
2006
|
instance,
|
|
1337
2007
|
});
|
|
2008
|
+
if(!ctx.__iamCollectionTemplate)for(const request of uiAttributes)attributeRequests.push({nodeId,...request});
|
|
1338
2009
|
elements.set(elementKey, {
|
|
1339
2010
|
elementKey,
|
|
1340
2011
|
componentKey: owner.key,
|
|
@@ -1351,12 +2022,38 @@ export async function analyze({
|
|
|
1351
2022
|
source,
|
|
1352
2023
|
synthetic: htmlTag !== tag && tag !== "Link" && tag !== "Image"&&!intrinsicAlias&&!knownExternalPrimitive,
|
|
1353
2024
|
});
|
|
1354
|
-
|
|
2025
|
+
if(tabPart==='Root'){
|
|
2026
|
+
// Tab initialization below shares the same concrete source container.
|
|
2027
|
+
tabGroup.nodeId=nodeId;
|
|
2028
|
+
const {controller,...row}=tabGroup;
|
|
2029
|
+
if(row.items.length&&row.initialValue!==null&&!row.items.some(item=>item.value===row.initialValue))row.initialValue=null;
|
|
2030
|
+
presentation.tabs.push(row);
|
|
2031
|
+
}
|
|
2032
|
+
if(overlayGroup?.controller&&overlayGroup.variable&&['Dialog','AlertDialog','Menu','Popover','Collapsible'].includes(overlay?.family)&&['Trigger','Close'].includes(overlay.part)&&!uiHandlers.some(h=>h.key==='onClick')){
|
|
2033
|
+
const next=overlay.part==='Close'?{op:'const',value:false}:{op:'not',arg:overlayGroup.variable};
|
|
2034
|
+
const replace=expression=>expression.op==='event'&&expression.key==='value'?next:Object.fromEntries(Object.entries(expression).map(([key,value])=>[key,Array.isArray(value)?value.map(v=>v&&typeof v==='object'?replace(v):v):value&&typeof value==='object'?replace(value):value]));
|
|
2035
|
+
const controller=overlayGroup.controller,writes=(controller.writes||[{variableKey:controller.variableKey,value:controller.value}]).map(w=>({...w,value:replace(w.value)}));
|
|
2036
|
+
uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});
|
|
2037
|
+
}
|
|
2038
|
+
if(tabPart==='Tab'&&tabGroup&&typeof props.value==='string'&&props.value.length<=2048){
|
|
2039
|
+
if(!tabGroup.items.some(item=>item.value===props.value))tabGroup.items.push({value:props.value,nodeId});
|
|
2040
|
+
props['data-iam-tab-value']=props.value;props['data-iam-tab-key']=tabGroup.tabKey;
|
|
2041
|
+
const replace=value=>value.op==='event'&&value.key==='value'?{op:'const',value:props.value}:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,Array.isArray(v)?v.map(item=>item&&typeof item==='object'?replace(item):item):v&&typeof v==='object'?replace(v):v]));
|
|
2042
|
+
if(tabGroup.controller){const writes=(tabGroup.controller.writes||[{variableKey:tabGroup.controller.variableKey,value:tabGroup.controller.value}]).map(w=>({...w,value:replace(w.value)}));uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});}
|
|
2043
|
+
}
|
|
2044
|
+
if(tabPart==='Panel'&&tabGroup?.variableKey&&typeof props.value==='string')uiBindings.push({property:'visible',value:{op:'eq',left:{op:'var',key:tabGroup.variableKey},right:{op:'const',value:props.value}},source});
|
|
2045
|
+
for (const handler of uiHandlers.filter(h=>!['onValueChange','onOpenChange'].includes(h.key))) {
|
|
1355
2046
|
const event = handler.key === "onClick" ? "click" : htmlTag === "select" || handler.payload.checked ? "change" : "input";
|
|
1356
|
-
|
|
2047
|
+
const writes=handler.writes||[{variableKey:handler.variableKey,value:handler.value}];
|
|
2048
|
+
ui.transitions.push({ transitionKey: identity("ui-transition", { page: pageKey, node: nodeId, event }), routeKey: pageKey, fromStateKey: "pending", toStateKey: "pending", trigger: { nodeId, event, payload: handler.payload }, guard: { op: "const", value: true }, stateReads: [...new Set(writes.flatMap(w=>readVariables(w.value)))].sort(), stateWrites: writes, simulation: "implemented", evidence: "source_inferred", source: handler.source });
|
|
1357
2049
|
}
|
|
1358
|
-
for (const binding of uiBindings.filter(b => ["value", "checked", "disabled", "className"].includes(b.property))) {
|
|
2050
|
+
for (const binding of uiBindings.filter(b => !ctx.__iamCollectionTemplate&&["value", "checked", "disabled", "className", "visible"].includes(b.property)&&!(tabPart==='Root'&&b.property==='value'))) {
|
|
1359
2051
|
if(['value','checked'].includes(binding.property)&&!['input','select','textarea'].includes(htmlTag)){gap('UI_BINDING_TARGET_UNSUPPORTED',m.file,tag+': input property cannot be simulated on a non-input adapter');continue;}
|
|
2052
|
+
const expectedType = ["checked", "disabled", "visible"].includes(binding.property) ? "boolean" : "string";
|
|
2053
|
+
if (uiExpressionType(binding.value) !== expectedType) {
|
|
2054
|
+
gap("UI_BINDING_TYPE_UNSUPPORTED", m.file, tag + "." + binding.property + ": source value cannot be represented by the typed UI simulation");
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
1360
2057
|
ui.bindings.push({ bindingKey: identity("ui-binding", { node: nodeId, property: binding.property }), routeKey: pageKey, nodeId, elementKey, ...binding });
|
|
1361
2058
|
}
|
|
1362
2059
|
const sensitiveInput = htmlTag === "input" && (["password", "hidden"].includes(String(props.type).toLowerCase()) || sensitiveName(String(props.name || props.id || "")));
|
|
@@ -1366,7 +2063,7 @@ export async function analyze({
|
|
|
1366
2063
|
ui.transitions = ui.transitions.filter(t => t.trigger.nodeId !== nodeId);
|
|
1367
2064
|
gap("SENSITIVE_INPUT_REDACTED", m.file, "Input value and state bridge omitted");
|
|
1368
2065
|
}
|
|
1369
|
-
const attrs = { "data-iam-node": nodeId };
|
|
2066
|
+
const attrs = { "data-iam-node": nodeId,...(['input','textarea','select'].includes(htmlTag)&&Object.hasOwn(props,'value')&&isUnknown(props.value)?{'data-iam-input-unobserved':'true'}:{}) };
|
|
1370
2067
|
for (const [key, value] of Object.entries(props)) {
|
|
1371
2068
|
if (key === "style") {
|
|
1372
2069
|
const css = staticStyle(value, () => gap("INLINE_STYLE_UNSUPPORTED", m.file,
|
|
@@ -1394,7 +2091,7 @@ export async function analyze({
|
|
|
1394
2091
|
clipRule: "clip-rule",
|
|
1395
2092
|
}[key] || key;
|
|
1396
2093
|
if (
|
|
1397
|
-
!/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|width|height|viewBox|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule|cx|cy|r|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-indiana-id|data-testid|data-test|data-qa|data-iam-(?:adapter|toggle))$/.test(
|
|
2094
|
+
!/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|width|height|viewBox|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule|cx|cy|r|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-indiana-id|data-indiana-map-(?:area|flow|kind|auth|edge)|data-(?:slot|state|side|variant|collapsible|orientation|active)|data-testid|data-test|data-qa|data-iam-(?:adapter|toggle|tab-value|tab-key|condition|alternative|condition-default|unobserved))$/.test(
|
|
1398
2095
|
attr,
|
|
1399
2096
|
)
|
|
1400
2097
|
)
|
|
@@ -1436,26 +2133,50 @@ export async function analyze({
|
|
|
1436
2133
|
);
|
|
1437
2134
|
}
|
|
1438
2135
|
function renderEntry(file, props, instance, symbol) {
|
|
1439
|
-
const entryModule = modules.get(file), entryRef = entryModule &&
|
|
2136
|
+
const entryModule = modules.get(file), entryRef = entryModule && componentBinding(entryModule, symbol ?? entryModule.defaultName);
|
|
1440
2137
|
const entryDef = entryRef && componentDefs.get(entryRef.module.file + "#" + entryRef.name);
|
|
1441
|
-
if (!entryDef) {
|
|
2138
|
+
if (!entryDef) {
|
|
2139
|
+
if (entryRef) {
|
|
2140
|
+
const key = identity("component", { file: entryRef.module.file, symbol: entryRef.name });
|
|
2141
|
+
if (!components.has(key)) components.set(key, { componentKey: key, file: entryRef.module.file, name: entryRef.name, source: sourceLocation(entryRef.module, entryRef.node, entryRef.name), dependencies: [] });
|
|
2142
|
+
}
|
|
2143
|
+
gap("PAGE_NO_JSX", file, "Composition entry has no inspectable JSX; null defaults are inert");
|
|
2144
|
+
return "";
|
|
2145
|
+
}
|
|
1442
2146
|
return renderComponent(entryDef, props, instance, 0);
|
|
1443
2147
|
}
|
|
1444
|
-
function renderBranch(branch) {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2148
|
+
function renderBranch(branch, inherited = {}) {
|
|
2149
|
+
const contentId=identity('node',{page:pageKey,lane:branch.lane,pageContent:true,...(composition.entryMode==='soft'?{composition:composition.compositionKey}:{})}),contentElement=identity('element',{page:pageKey,lane:branch.lane,pageContent:true});
|
|
2150
|
+
const contentSource=sourceLocation(modules.get(branch.file),modules.get(branch.file).source,branch.symbol??'$page-content');
|
|
2151
|
+
elements.set(contentElement,{elementKey:contentElement,componentKey:def.key,tag:'div',source:contentSource,identityStrategy:'page_root',editableProperties:[],binding:'preview_only'});record.set(contentId,{elementKey:contentElement,source:contentSource,synthetic:true});
|
|
2152
|
+
const contextProps=ctx=>({__iamContexts:ctx.__iamContexts,__iamTabs:ctx.__iamTabs,__iamOverlays:ctx.__iamOverlays});
|
|
2153
|
+
function renderLayer(index,ctx){
|
|
2154
|
+
if(index<0){
|
|
2155
|
+
const content=renderEntry(branch.file,contextProps(ctx),branch===composition.composition?'page':'slot-page:'+branch.file,branch.symbol);
|
|
2156
|
+
return '<div data-iam-node="'+contentId+'" data-iam-page-content="true">'+content+'</div>';
|
|
2157
|
+
}
|
|
2158
|
+
const layer=branch.layers[index],child={iamRenderChildren:next=>renderLayer(index-1,next)};
|
|
2159
|
+
const props = ["react-router", "tanstack-router"].includes(routing.framework)
|
|
2160
|
+
? { ...contextProps(ctx), __iamOutlet: child }
|
|
2161
|
+
: { ...contextProps(ctx), children: child };
|
|
2162
|
+
if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = child; props.pageProps = {}; }
|
|
1450
2163
|
for (const slot of layer.slots) {
|
|
1451
|
-
props[slot.name] = {
|
|
2164
|
+
props[slot.name] = { iamRenderChildren:next=>renderBranch(slot.branch,next) };
|
|
1452
2165
|
if (composition.entryMode === "hard") gap("SOFT_NAVIGATION_STATE_UNOBSERVED", layer.file, "Slot " + slot.name + ": hard-load composition alone does not establish retained soft-navigation state");
|
|
1453
2166
|
}
|
|
1454
|
-
|
|
2167
|
+
return renderEntry(layer.file, props, "layout:" + path.posix.dirname(layer.file), layer.symbol);
|
|
1455
2168
|
}
|
|
1456
|
-
return
|
|
2169
|
+
return renderLayer(branch.layers.length-1,inherited);
|
|
1457
2170
|
}
|
|
1458
2171
|
let html = composition.gaps.length ? "" : renderBranch(composition.composition);
|
|
2172
|
+
for(const request of attributeRequests){
|
|
2173
|
+
const tabs=presentation.tabs.filter(t=>request.reads.includes(t.variableKey));if(tabs.length!==1||tabs[0].items.length>8)continue;
|
|
2174
|
+
const tab=tabs[0],cases=[];
|
|
2175
|
+
for(const item of tab.items){const next={...request.ctx};for(const [label,key]of Object.entries(next.__iamVariableKeys))if(key===tab.variableKey)next[label]=item.value;for(const [label,expr]of next.__iamPropBindings)if(expr.op==='var'&&expr.key===tab.variableKey)next[label]=item.value;for(const [label,init]of next.__iamLocals)if(!next.__iamStates.has(label))next[label]=evalExpression(init,request.owner.module,next);const value=evalExpression(request.expression,request.owner.module,next);if(typeof value!=='string'||value.length>4000){cases.length=0;break;}cases.push({value:item.value,text:value});if(request.property==='class')value.split(/\s+/).filter(Boolean).forEach(c=>allClasses.add(c));}
|
|
2176
|
+
if(cases.length<2||new Set(cases.map(c=>c.text)).size<2)continue;
|
|
2177
|
+
const set={property:request.property,cases,source:request.source},setKey=identity('ui-attribute-set',set);if(!presentation.attributeSets.some(s=>s.setKey===setKey))presentation.attributeSets.push({setKey,...set});
|
|
2178
|
+
const row={routeKey:pageKey,renderKey:'pending',nodeId:request.nodeId,variableKey:tab.variableKey,setKey};presentation.attributes.push({attributeKey:identity('ui-attribute',row),...row});
|
|
2179
|
+
}
|
|
1459
2180
|
const rootId = identity("node", { page: pageKey, root: true, ...(composition.entryMode === "soft" ? { composition: composition.compositionKey } : {}) }),
|
|
1460
2181
|
rootElement = identity("element", { page: pageKey, root: true });
|
|
1461
2182
|
record.set(rootId, {
|
|
@@ -1481,6 +2202,27 @@ export async function analyze({
|
|
|
1481
2202
|
const dom = new JSDOM(
|
|
1482
2203
|
"<!doctype html><html><body>" + html + "</body></html>",
|
|
1483
2204
|
);
|
|
2205
|
+
// JSX children may be forwarded more than once, and HTML repair may clone
|
|
2206
|
+
// an invalid nested anchor. Each concrete occurrence needs a unique node;
|
|
2207
|
+
// ambiguous copies remain preview-only and have no simulated state bridge.
|
|
2208
|
+
const occurrences = new Map();
|
|
2209
|
+
for (const element of dom.window.document.querySelectorAll("[data-iam-node]")) {
|
|
2210
|
+
const id = element.getAttribute("data-iam-node");
|
|
2211
|
+
const matches = occurrences.get(id) ?? [];
|
|
2212
|
+
matches.push(element);
|
|
2213
|
+
occurrences.set(id, matches);
|
|
2214
|
+
}
|
|
2215
|
+
for (const [original, matches] of occurrences) {
|
|
2216
|
+
if (matches.length < 2) continue;
|
|
2217
|
+
const rec = record.get(original);
|
|
2218
|
+
if (!rec) throw Error("Unknown repeated source node");
|
|
2219
|
+
gap("REPEATED_SOURCE_INSTANCE", rec.source.file, "Repeated forwarded children or HTML repair: copies are preview-only without a state bridge");
|
|
2220
|
+
for (const [index, element] of matches.entries()) {
|
|
2221
|
+
const id = identity("node", { page: pageKey, original, occurrence: index });
|
|
2222
|
+
element.setAttribute("data-iam-node", id);
|
|
2223
|
+
record.set(id, { ...rec, synthetic: true });
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1484
2226
|
const nodes = [];
|
|
1485
2227
|
function collect(node, parentId, index) {
|
|
1486
2228
|
if (node.nodeType !== 1 && node.nodeType !== 3) return null;
|
|
@@ -1552,21 +2294,12 @@ export async function analyze({
|
|
|
1552
2294
|
source: page.source,
|
|
1553
2295
|
documentKey,
|
|
1554
2296
|
});
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (!states.some((s) => s.stateKey === key)) {
|
|
1562
|
-
states.push({
|
|
1563
|
-
stateKey: key,
|
|
1564
|
-
pageKey,
|
|
1565
|
-
label: branch.label,
|
|
1566
|
-
evidence: branch.evidence,
|
|
1567
|
-
source: branch.source,
|
|
1568
|
-
documentKey: null,
|
|
1569
|
-
});
|
|
2297
|
+
// Unknown source alternatives live in ui.conditions. Representable local
|
|
2298
|
+
// state candidates remain separate from concrete source documents.
|
|
2299
|
+
for (const branch of branches.filter(b=>b.evidence==='source_inferred').slice(0, 30)) {
|
|
2300
|
+
const key = identity("state", {page:pageKey,source:branch.source,label:branch.label});
|
|
2301
|
+
if(!states.some(state=>state.stateKey===key)){
|
|
2302
|
+
states.push({stateKey:key,pageKey,label:branch.label,evidence:branch.evidence,source:branch.source,documentKey:null});
|
|
1570
2303
|
page.stateKeys.push(key);
|
|
1571
2304
|
}
|
|
1572
2305
|
}
|
|
@@ -1595,7 +2328,7 @@ export async function analyze({
|
|
|
1595
2328
|
for (const node of doc.nodes) {
|
|
1596
2329
|
if (
|
|
1597
2330
|
node.kind !== "element" ||
|
|
1598
|
-
|
|
2331
|
+
node.tag !== "a"
|
|
1599
2332
|
)
|
|
1600
2333
|
continue;
|
|
1601
2334
|
const href = node.attributes.href || "",
|
|
@@ -1629,9 +2362,18 @@ export async function analyze({
|
|
|
1629
2362
|
});
|
|
1630
2363
|
}
|
|
1631
2364
|
const packagedAssets = [];
|
|
2365
|
+
inventory.push(...await referencedRasterSources(root,assets.keys()));
|
|
1632
2366
|
for (const [url] of assets) {
|
|
1633
2367
|
gap("PUBLIC_ASSET_NOT_PACKAGED", "public", url + ": requires an explicit public-asset build input");
|
|
1634
2368
|
}
|
|
2369
|
+
// Only statically imported styles belong to the source entry's default presentation.
|
|
2370
|
+
const styleFiles=new Map(cssSources.map(s=>[s.file,s])),activeStyles=new Map();
|
|
2371
|
+
const visitStyle=file=>{const style=styleFiles.get(file);if(!style||activeStyles.has(file))return;activeStyles.set(file,style);for(const match of style.text.matchAll(/@import\s+["']([^"']+)["']/g))if(match[1].startsWith('.'))visitStyle(path.posix.normalize(path.posix.join(path.posix.dirname(file),match[1])));};
|
|
2372
|
+
for(const module of selection.discoveryModules.values())for(const statement of module.source.statements)if(ts.isImportDeclaration(statement)&&statement.moduleSpecifier&&ts.isStringLiteral(statement.moduleSpecifier)){
|
|
2373
|
+
const spec=statement.moduleSpecifier.text;if(spec.endsWith('.css')&&spec.startsWith('.'))visitStyle(path.posix.normalize(path.posix.join(path.posix.dirname(module.file),spec)));
|
|
2374
|
+
}
|
|
2375
|
+
const sourceStyles=[...activeStyles.values()],sourceThemes=sourceStyles.flatMap(s=>[...s.text.matchAll(/@theme(?:\s+inline)?\s*\{([^{}]*)\}/g)].map(m=>m[0])).join('\n');
|
|
2376
|
+
const color=t=>sourceStyles.some(s=>new RegExp('--'+t+':\\s*(?:oklch|oklab|rgb|hsl|lab|lch|color|var)\\(|--'+t+':\\s*#').test(s.text))?'var(--'+t+')':'hsl(var(--'+t+'))';
|
|
1635
2377
|
let css = "";
|
|
1636
2378
|
try {
|
|
1637
2379
|
const tw = dependency("tailwindcss");
|
|
@@ -1662,16 +2404,16 @@ export async function analyze({
|
|
|
1662
2404
|
];
|
|
1663
2405
|
const custom =
|
|
1664
2406
|
"@theme inline {" +
|
|
1665
|
-
tokens.map((t) => "--color-" + t + ":
|
|
2407
|
+
tokens.map((t) => "--color-" + t + ":" + color(t) + ";").join("") +
|
|
1666
2408
|
"--font-heading: ui-sans-serif,system-ui;--radius-lg:var(--radius);--radius-md:calc(var(--radius) - 2px);}";
|
|
1667
2409
|
const compile = await tw.value.compile(
|
|
1668
|
-
theme + "\n" + custom + "\n@tailwind utilities;",
|
|
2410
|
+
theme + "\n" + custom + "\n" + sourceThemes + "\n@tailwind utilities;",
|
|
1669
2411
|
);
|
|
1670
2412
|
css = compile.build([...allClasses].sort());
|
|
1671
2413
|
} catch (error) {
|
|
1672
2414
|
gap("STYLE_COMPILER", "", error.message);
|
|
1673
2415
|
}
|
|
1674
|
-
const vars =
|
|
2416
|
+
const vars = sourceStyles
|
|
1675
2417
|
.flatMap((s) =>
|
|
1676
2418
|
[...s.text.matchAll(/(?::root|\.dark)\s*\{([^{}]*)\}/g)].map(
|
|
1677
2419
|
(m) => m[0],
|
|
@@ -1681,7 +2423,7 @@ export async function analyze({
|
|
|
1681
2423
|
css +=
|
|
1682
2424
|
"\n" +
|
|
1683
2425
|
vars +
|
|
1684
|
-
"\n*{box-sizing:border-box;border-color:
|
|
2426
|
+
"\n*{box-sizing:border-box;border-color:"+color("border")+";border-style:solid;border-width:0}[hidden]{display:none!important}body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;color:"+color("foreground")+";background:"+color("background")+"}h1,h2,h3,p,ul,ol{margin:0}a{color:inherit;text-decoration:none}button,input,textarea,select{font:inherit}button,a{cursor:pointer}button{background:transparent}svg{display:block}img{max-width:100%;height:auto}.container{width:100%;max-width:1400px;padding-inline:2rem;margin-inline:auto}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-page{min-height:100vh}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}";
|
|
1685
2427
|
const runtime = await fs.readFile(
|
|
1686
2428
|
new URL("../assets/document-runtime.js", import.meta.url),
|
|
1687
2429
|
"utf8",
|
|
@@ -1697,6 +2439,16 @@ export async function analyze({
|
|
|
1697
2439
|
doc.js = runtime;
|
|
1698
2440
|
sealDocument(doc);
|
|
1699
2441
|
}
|
|
2442
|
+
// Inventory every source file, but serialize UI definitions reachable from
|
|
2443
|
+
// projected routes (including retained alternatives and shared fragments).
|
|
2444
|
+
const liveElements=new Set([...documents.flatMap(d=>d.nodes.map(n=>n.elementKey)),...presentation.fragments.flatMap(f=>f.sourceElements)]);
|
|
2445
|
+
for(const key of elements.keys())if(!liveElements.has(key))elements.delete(key);
|
|
2446
|
+
const liveComponents=new Set([...pages.flatMap(p=>p.componentKeys),...[...elements.values()].map(e=>e.componentKey)]);
|
|
2447
|
+
const compositionFiles=new Set(renderDefinitions.flatMap(d=>flattenComposition(d.composition).flatMap(entry=>[entry.file,...entry.layers.map(layer=>layer.file)])));
|
|
2448
|
+
for(const component of components.values())if(compositionFiles.has(component.file))liveComponents.add(component.componentKey);
|
|
2449
|
+
const retainComponent=key=>{const c=components.get(key);if(!c)return;for(const dep of c.dependencies)if(!liveComponents.has(dep)){liveComponents.add(dep);retainComponent(dep);}};
|
|
2450
|
+
for(const key of [...liveComponents])retainComponent(key);
|
|
2451
|
+
for(const key of components.keys())if(!liveComponents.has(key))components.delete(key);
|
|
1700
2452
|
const sourceGraphDigest = hashObject("iam/source-graph/v1", {
|
|
1701
2453
|
files: inventory,
|
|
1702
2454
|
components: [...components.values()],
|
|
@@ -1706,8 +2458,8 @@ export async function analyze({
|
|
|
1706
2458
|
schemaVersion: 2,
|
|
1707
2459
|
producer: {
|
|
1708
2460
|
name: "indiana-app-manifest-local",
|
|
1709
|
-
version: "0.
|
|
1710
|
-
method: "typescript-ast-and-bounded-static-projection",
|
|
2461
|
+
version: "0.24.0",
|
|
2462
|
+
method: "typescript-ast-and-bounded-static-projection;lucide-static-"+LUCIDE_SOURCE_VERSION,
|
|
1711
2463
|
},
|
|
1712
2464
|
app: { name, platforms: ["web"] },
|
|
1713
2465
|
routing,
|
|
@@ -1740,7 +2492,29 @@ export async function analyze({
|
|
|
1740
2492
|
ui.bindings = ui.bindings.filter(b => concrete.has(b.nodeId));
|
|
1741
2493
|
ui.transitions = ui.transitions.filter(t => concrete.has(t.trigger.nodeId));
|
|
1742
2494
|
for (const doc of documents) for (const node of doc.nodes) if (node.kind === "element" && ["button", "form", "input", "select", "textarea"].includes(node.tag) && !ui.transitions.some(t => t.trigger.nodeId === node.nodeId)) ui.uncovered.push({ routeKey: doc.pageKey, nodeId: node.nodeId, event: "source-event", reason: "No supported UI-only handler; business callbacks are not executed", source: node.source });
|
|
2495
|
+
// State with no representable UI read/write has no preview effect.
|
|
2496
|
+
const usedVariables=new Set([...ui.bindings.flatMap(b=>readVariables(b.value)),...ui.transitions.flatMap(t=>[...t.stateReads,...t.stateWrites.map(w=>w.variableKey)]),...[...presentation.tabs,...presentation.regions,...presentation.collections].map(t=>t.variableKey).filter(Boolean),...presentation.collections.flatMap(c=>c.requires.map(r=>r.variableKey))]);
|
|
2497
|
+
ui.variables=ui.variables.filter(v=>usedVariables.has(v.variableKey));
|
|
2498
|
+
// Repeated instances of the same unsupported source control share one source gap.
|
|
2499
|
+
const sourceGaps=new Map();
|
|
2500
|
+
for(const row of ui.uncovered){const key=JSON.stringify([row.routeKey,row.source,row.event,row.reason]);const prior=sourceGaps.get(key);if(prior)prior.nodeId=null;else sourceGaps.set(key,{...row});}
|
|
2501
|
+
ui.uncovered=[...sourceGaps.values()];
|
|
1743
2502
|
manifest.routeTree = assembleRouteTree(manifest, ui, renderDefinitions, compositions.navigation);
|
|
2503
|
+
presentation.conditions=[...new Map(presentation.conditions.map(c=>[c.conditionKey,c])).values()];
|
|
2504
|
+
presentation.regions=presentation.regions.filter(r=>concrete.has(r.nodeId)&&r.options.every(o=>concrete.has(o.nodeId)));
|
|
2505
|
+
presentation.tabs=presentation.tabs.filter(t=>concrete.has(t.nodeId)&&t.items.length&&t.items.every(i=>concrete.has(i.nodeId)));
|
|
2506
|
+
presentation.attributes=presentation.attributes.filter(a=>concrete.has(a.nodeId));presentation.attributeSets=presentation.attributeSets.filter(s=>presentation.attributes.some(a=>a.setKey===s.setKey));
|
|
2507
|
+
presentation.navigation=sourceRedirects({ts,modules,binding,functionFrom,externalImport,sourceLocation,constant:(node,module)=>{const result=evalExpression(node,module,{});return isUnknown(result)?undefined:result;},pages,components:[...components.values()],documents});
|
|
2508
|
+
manifest.routeTree=attachUiStructure(manifest.routeTree,presentation,documents);
|
|
2509
|
+
// Discarded adapter children have no concrete UI. Keep every document and
|
|
2510
|
+
// shared-fragment mapping, but do not serialize unreachable element records.
|
|
2511
|
+
const referencedElements=new Set([...documents.flatMap(d=>d.nodes.map(n=>n.elementKey)),...presentation.fragments.flatMap(f=>f.sourceElements)]);
|
|
2512
|
+
manifest.elements=manifest.elements.filter(element=>referencedElements.has(element.elementKey));
|
|
2513
|
+
// Each navigation row references its exact existing interaction. The original
|
|
2514
|
+
// interaction ID remains in the authoritative array, instead of being repeated.
|
|
2515
|
+
const interactionIndices=new Map(manifest.interactions.map((row,index)=>[row.interactionKey,index]));
|
|
2516
|
+
for(const row of manifest.routeTree.navigation)row.interactionKey=interactionIndices.get(row.interactionKey);
|
|
2517
|
+
internGraphSources(manifest.routeTree);
|
|
1744
2518
|
manifest.integrity.routeTreeDigest = routeIndexDigest(manifest.routeTree);
|
|
1745
2519
|
const concreteNodes = new Map(documents.flatMap(doc => doc.nodes.map(node => [node.nodeId, node])));
|
|
1746
2520
|
for (const target of manifest.routeTree.targets) {
|