@tinkerlandai/indiana-iam 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -3
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +19 -5
- package/package.json +5 -13
- package/src/analyze.mjs +799 -109
- package/src/build-options.d.ts +1 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +49 -6
- package/src/build.mjs +3 -3
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/framework-routes.mjs +3 -1
- package/src/generate.mjs +3 -1
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +2 -0
- package/src/library-entry.mjs +4 -0
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/node-hashes.mjs +9 -8
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +15 -3
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +38 -0
- package/src/source-collections.mjs +241 -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/static-primitives.mjs +48 -0
- package/src/static-query.mjs +39 -11
- package/src/tab-scenes.mjs +214 -0
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +645 -0
- package/src/validate.mjs +5 -0
package/src/analyze.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
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
6
|
import { automaticSourceAnchors, anchorAttributes, sourceScriptKind } from "./anchors.mjs";
|
|
4
7
|
import { sourceFiles } from "./source-files.mjs";
|
|
@@ -7,9 +10,16 @@ import { assembleRouteTree, readVariables } from "./route-tree.mjs";
|
|
|
7
10
|
import { sourceEntry } from "./source-entry.mjs";
|
|
8
11
|
import { frameworkRoutes, externalImport } from "./framework-routes.mjs";
|
|
9
12
|
import { sourceResolution } from "./source-resolution.mjs";
|
|
10
|
-
import { routeIndexDigest, refreshRenderHashes } from "./render-index.mjs";
|
|
13
|
+
import { routeIndexDigest, refreshRenderHashes, flattenComposition } from "./render-index.mjs";
|
|
11
14
|
import { storeDocumentNodes } from "./node-hashes.mjs";
|
|
12
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';
|
|
13
23
|
import {
|
|
14
24
|
publicRoute,
|
|
15
25
|
assertRoutes,
|
|
@@ -29,6 +39,27 @@ const { JSDOM } = dependency("jsdom").value;
|
|
|
29
39
|
const UNKNOWN = Symbol("unknown");
|
|
30
40
|
import { sensitiveName, redactLiteral, privateUrl } from "./privacy.mjs";
|
|
31
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
|
+
}
|
|
32
63
|
// React numeric styles use px except for unitless properties and CSS variables.
|
|
33
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(" "));
|
|
34
65
|
function staticStyle(value, unsupported) {
|
|
@@ -171,6 +202,8 @@ export async function analyze({
|
|
|
171
202
|
components = new Map(),
|
|
172
203
|
assets = new Map();
|
|
173
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();
|
|
174
207
|
const ui = { variables: [], bindings: [], transitions: [], uncovered: [] };
|
|
175
208
|
const uiVariableKeys = new Set();
|
|
176
209
|
let byteCount = 0;
|
|
@@ -200,6 +233,7 @@ export async function analyze({
|
|
|
200
233
|
sha256: sha256(bytes),
|
|
201
234
|
bytes: bytes.length,
|
|
202
235
|
});
|
|
236
|
+
if(file.endsWith('.json')){try{jsonResources.set(file,JSON.parse(text));}catch{/* Non-resource JSON stays opaque. */}continue;}
|
|
203
237
|
if (file.endsWith(".css")) {
|
|
204
238
|
gap(
|
|
205
239
|
file.endsWith(".module.css")
|
|
@@ -211,7 +245,11 @@ export async function analyze({
|
|
|
211
245
|
if (!file.endsWith(".module.css")) cssSources.push({ file, text });
|
|
212
246
|
continue;
|
|
213
247
|
}
|
|
214
|
-
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
|
+
}
|
|
215
253
|
const source = ts.createSourceFile(
|
|
216
254
|
file,
|
|
217
255
|
text,
|
|
@@ -338,6 +376,52 @@ export async function analyze({
|
|
|
338
376
|
.find(Boolean) || null
|
|
339
377
|
);
|
|
340
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;}
|
|
341
425
|
function binding(module, name, seen=new Set()) {
|
|
342
426
|
if(!name||seen.size>=16||seen.has(module.file+'#'+name))return null;seen.add(module.file+'#'+name);
|
|
343
427
|
if (module.bindings.has(name))
|
|
@@ -352,7 +436,7 @@ export async function analyze({
|
|
|
352
436
|
: target.exports.get(imported.name) || imported.name;
|
|
353
437
|
return binding(target,key,seen);
|
|
354
438
|
}
|
|
355
|
-
const componentDefs = new Map();
|
|
439
|
+
const componentDefs = new Map(),componentsByFunction=new WeakMap();
|
|
356
440
|
function registerComponent(module, symbol, node) {
|
|
357
441
|
const fn = functionFrom(node);
|
|
358
442
|
if (!fn) return null;
|
|
@@ -377,7 +461,7 @@ export async function analyze({
|
|
|
377
461
|
ts.forEachChild(n, number);
|
|
378
462
|
}
|
|
379
463
|
number(fn);
|
|
380
|
-
const def = { module, symbol, fn, key, ordinals };
|
|
464
|
+
const def = { module, symbol, fn, key, ordinals };componentsByFunction.set(fn,def);
|
|
381
465
|
let returns = 0;
|
|
382
466
|
function countReturns(item) {
|
|
383
467
|
if (
|
|
@@ -459,6 +543,17 @@ export async function analyze({
|
|
|
459
543
|
fileHash: module.hash,
|
|
460
544
|
};
|
|
461
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;}});}
|
|
462
557
|
function evalExpression(
|
|
463
558
|
node,
|
|
464
559
|
module,
|
|
@@ -469,6 +564,7 @@ export async function analyze({
|
|
|
469
564
|
node = unwrap(node);
|
|
470
565
|
if (!node || depth > 30) return UNKNOWN;
|
|
471
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;
|
|
472
568
|
if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
|
|
473
569
|
return ts.isNumericLiteral(node) ? Number(node.text) : redactLiteral(node.text);
|
|
474
570
|
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
@@ -476,7 +572,14 @@ export async function analyze({
|
|
|
476
572
|
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
477
573
|
if (ts.isIdentifier(node)) {
|
|
478
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}}};
|
|
479
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
|
+
}
|
|
480
583
|
const token = module.file + "#" + node.text;
|
|
481
584
|
if (stack.has(token)) return UNKNOWN;
|
|
482
585
|
const ref = binding(module, node.text);
|
|
@@ -487,12 +590,16 @@ export async function analyze({
|
|
|
487
590
|
if (ts.isArrayLiteralExpression(node)) return node.elements.map(ev);
|
|
488
591
|
if (ts.isObjectLiteralExpression(node)) {
|
|
489
592
|
const obj = {};
|
|
593
|
+
const fields=new Map();
|
|
490
594
|
for (const prop of node.properties) {
|
|
491
595
|
if (ts.isPropertyAssignment(prop))
|
|
492
596
|
obj[prop.name.getText(module.source).replace(/^['"]|['"]$/g, "")] =
|
|
493
597
|
ev(prop.initializer);
|
|
494
|
-
else if (ts.isShorthandPropertyAssignment(prop))
|
|
598
|
+
else if (ts.isShorthandPropertyAssignment(prop)){
|
|
495
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
|
+
}
|
|
496
603
|
else if (ts.isSpreadAssignment(prop)) {
|
|
497
604
|
const v = ev(prop.expression);
|
|
498
605
|
if (isUnknown(v)) return UNKNOWN;
|
|
@@ -500,6 +607,7 @@ export async function analyze({
|
|
|
500
607
|
Object.assign(obj, v);
|
|
501
608
|
}
|
|
502
609
|
}
|
|
610
|
+
if(fields.size)objectBindings.set(obj,fields);
|
|
503
611
|
return obj;
|
|
504
612
|
}
|
|
505
613
|
if (
|
|
@@ -553,6 +661,12 @@ export async function analyze({
|
|
|
553
661
|
return isUnknown(a) ? UNKNOWN : (a ?? b);
|
|
554
662
|
if (isUnknown(a) || isUnknown(b)) return UNKNOWN;
|
|
555
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);
|
|
556
670
|
if (
|
|
557
671
|
[
|
|
558
672
|
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
@@ -570,9 +684,21 @@ export async function analyze({
|
|
|
570
684
|
}
|
|
571
685
|
if (ts.isCallExpression(node)) {
|
|
572
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;
|
|
573
699
|
if (/(?:^|\.)useState$/.test(callee))
|
|
574
700
|
return {
|
|
575
|
-
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,
|
|
576
702
|
};
|
|
577
703
|
if (callee === "cn" || callee === "clsx") {
|
|
578
704
|
let valid=true;
|
|
@@ -607,6 +733,7 @@ export async function analyze({
|
|
|
607
733
|
([key, values]) =>
|
|
608
734
|
values[isUnknown((props || {})[key]) ? options.defaultVariants?.[key] : (props || {})[key] ?? options.defaultVariants?.[key]],
|
|
609
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]:[]),
|
|
610
737
|
props?.class,
|
|
611
738
|
props?.className,
|
|
612
739
|
]
|
|
@@ -617,7 +744,7 @@ export async function analyze({
|
|
|
617
744
|
const target = ev(node.expression.expression),
|
|
618
745
|
args = node.arguments.map(ev),
|
|
619
746
|
method = node.expression.name.text;
|
|
620
|
-
if (isUnknown(target) || args.some(isUnknown)) return
|
|
747
|
+
if (isUnknown(target) || args.some(isUnknown)) return query(node,module,context);
|
|
621
748
|
if (
|
|
622
749
|
typeof target === "string" &&
|
|
623
750
|
[
|
|
@@ -633,7 +760,7 @@ export async function analyze({
|
|
|
633
760
|
return target.filter((v) => !isUnknown(v)).join(args[0]);
|
|
634
761
|
}
|
|
635
762
|
}
|
|
636
|
-
if (ts.isCallExpression(node)) return
|
|
763
|
+
if (ts.isCallExpression(node)) return query(node,module,context);
|
|
637
764
|
return UNKNOWN;
|
|
638
765
|
}
|
|
639
766
|
// Preserve only definitely present class tokens when a template contains an
|
|
@@ -689,15 +816,14 @@ export async function analyze({
|
|
|
689
816
|
const omitted = new Set(param.name.elements.filter(item => !item.dotDotDotToken).map(item => item.propertyName?.getText() || item.name.getText()));
|
|
690
817
|
const rest = Object.fromEntries(Object.entries(props).filter(([key]) => !omitted.has(key)));
|
|
691
818
|
const parent = forwarded.get(props);
|
|
692
|
-
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)) });
|
|
693
820
|
ctx[el.name.text] = rest; continue;
|
|
694
821
|
}
|
|
695
822
|
const key = el.propertyName?.getText() || el.name.getText();
|
|
696
823
|
ctx[el.name.getText()] =
|
|
697
|
-
props[key]
|
|
698
|
-
(el.initializer
|
|
699
|
-
?
|
|
700
|
-
: 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);
|
|
701
827
|
const inherited = forwarded.get(props);
|
|
702
828
|
const binding = inherited?.bindings.find(b => b.property === key), handler = inherited?.handlers.find(h => h.key === key);
|
|
703
829
|
if (binding) ctx.__iamPropBindings.set(el.name.getText(), binding.value);
|
|
@@ -709,8 +835,22 @@ export async function analyze({
|
|
|
709
835
|
if (ts.isVariableStatement(s))
|
|
710
836
|
for (const d of s.declarationList.declarations) {
|
|
711
837
|
if (ts.isIdentifier(d.name)) {
|
|
712
|
-
|
|
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);
|
|
713
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
|
+
}
|
|
714
854
|
} else if (ts.isArrayBindingPattern(d.name)) {
|
|
715
855
|
const call = unwrap(d.initializer), hook = call && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && def.module.imports.get(call.expression.text);
|
|
716
856
|
if (hook?.specifier === "react" && hook.name === "useActionState") {
|
|
@@ -740,7 +880,7 @@ export async function analyze({
|
|
|
740
880
|
continue;
|
|
741
881
|
}
|
|
742
882
|
const key = stateBinding.name.text;
|
|
743
|
-
ctx[key] = sensitiveName(key) ? UNKNOWN : value.stateInitial;
|
|
883
|
+
ctx[key] = sensitiveName(key) && typeof value.stateInitial !== 'boolean' ? UNKNOWN : value.stateInitial;
|
|
744
884
|
ctx.__iamStates.add(key);
|
|
745
885
|
ctx.__iamStateNodes[key] = d;
|
|
746
886
|
if (d.name.elements[1]?.name && !d.name.elements[1].dotDotDotToken && ts.isIdentifier(d.name.elements[1].name))
|
|
@@ -858,19 +998,20 @@ export async function analyze({
|
|
|
858
998
|
};
|
|
859
999
|
const record = new Map(),
|
|
860
1000
|
used = new Set(),
|
|
861
|
-
branches = [];
|
|
1001
|
+
branches = [],attributeRequests=[];
|
|
862
1002
|
let renderedCount = 0;
|
|
863
1003
|
function renderComponent(def, props, instance, depth) {
|
|
864
1004
|
if (depth === 0 && composition.entryMode === "soft") instance = composition.compositionKey + ":" + instance;
|
|
865
1005
|
// Component recursion and JSX nesting are separate budgets. Counting DOM
|
|
866
1006
|
// wrappers as component calls silently dropped ordinary nested navigation.
|
|
867
|
-
if (used.size >=
|
|
1007
|
+
if (used.size >= 24 || used.has(def.key + ":" + instance)) {
|
|
868
1008
|
gap("COMPONENT_DEPTH_LIMIT", def.module.file, def.symbol + ": active component call budget exceeded");
|
|
869
1009
|
return "";
|
|
870
1010
|
}
|
|
871
1011
|
used.add(def.key + ":" + instance);
|
|
872
1012
|
if (!page.componentKeys.includes(def.key))
|
|
873
1013
|
page.componentKeys.push(def.key);
|
|
1014
|
+
props.__iamRoutePath=/[\[\]]/.test(route)?UNKNOWN:route;
|
|
874
1015
|
const ctx = componentContext(def, props);
|
|
875
1016
|
ctx.__iamOutlet = props.__iamOutlet;
|
|
876
1017
|
ctx.__iamScope = identity("state-scope", {
|
|
@@ -895,11 +1036,17 @@ export async function analyze({
|
|
|
895
1036
|
}
|
|
896
1037
|
if (!uiVariableKeys.has(variableKey)) {
|
|
897
1038
|
uiVariableKeys.add(variableKey);
|
|
898
|
-
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) });
|
|
899
1040
|
}
|
|
900
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
|
+
}
|
|
901
1048
|
const result = renderExpr(
|
|
902
|
-
|
|
1049
|
+
uiReturnExpression(def.fn),
|
|
903
1050
|
def,
|
|
904
1051
|
ctx,
|
|
905
1052
|
instance,
|
|
@@ -934,6 +1081,7 @@ export async function analyze({
|
|
|
934
1081
|
}
|
|
935
1082
|
if (ts.isIdentifier(node)) {
|
|
936
1083
|
if (Object.hasOwn(aliases, node.text)) return aliases[node.text];
|
|
1084
|
+
if(eventParam&&node.text===eventParam)return {op:'event',key:'value'};
|
|
937
1085
|
if (ctx.__iamVariableKeys[node.text]) return { op: "var", key: ctx.__iamVariableKeys[node.text] };
|
|
938
1086
|
if (ctx.__iamPropBindings.has(node.text)) return ctx.__iamPropBindings.get(node.text);
|
|
939
1087
|
if (ctx.__iamLocals.has(node.text)) return compile(ctx.__iamLocals.get(node.text));
|
|
@@ -976,44 +1124,371 @@ export async function analyze({
|
|
|
976
1124
|
if (["add", "subtract"].includes(expression.op)) return left === "number" && right === "number" ? "number" : undefined;
|
|
977
1125
|
return undefined;
|
|
978
1126
|
}
|
|
979
|
-
function uiHandler(initializer, owner, ctx) {
|
|
1127
|
+
function uiHandler(initializer, owner, ctx, property = '') {
|
|
980
1128
|
let fn = initializer && ts.isJsxExpression(initializer) ? unwrap(initializer.expression) : null;
|
|
981
|
-
|
|
982
|
-
if
|
|
983
|
-
if
|
|
984
|
-
const
|
|
985
|
-
|
|
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);
|
|
986
1441
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
+
);
|
|
1002
1461
|
}
|
|
1003
|
-
function unobservedValue(node, owner, instance) {
|
|
1462
|
+
function unobservedValue(node, owner, instance,remote=false) {
|
|
1004
1463
|
const source = sourceLocation(owner.module, node, owner.symbol);
|
|
1005
1464
|
const elementKey = identity("element", { component: owner.key, unobservedValue: node.getStart(owner.module.source) - owner.fn.getStart(owner.module.source) });
|
|
1006
1465
|
const nodeId = identity("node", { page: pageKey, element: elementKey, instance });
|
|
1007
1466
|
elements.set(elementKey, { elementKey, componentKey: owner.key, tag: "span", source, identityStrategy: "provisional_ast_ordinal", editableProperties: [], binding: "preview_only" });
|
|
1008
1467
|
record.set(nodeId, { elementKey, source, synthetic: true });
|
|
1009
1468
|
// Preserve an unknown child boundary. An empty projection must never prove that replacing text is safe.
|
|
1010
|
-
return '<span data-iam-node="' + nodeId + '" hidden=""></span>';
|
|
1469
|
+
return '<span data-iam-node="' + nodeId + '" data-iam-observation="'+(remote?'remote':'unobserved')+'" hidden=""></span>';
|
|
1011
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);
|
|
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;}
|
|
1012
1482
|
function renderExpr(node, owner, ctx, instance, depth) {
|
|
1013
1483
|
node = unwrap(node);
|
|
1014
1484
|
if (!node) return "";
|
|
1015
1485
|
if (depth > 45) { gap("RENDER_DEPTH_LIMIT", owner.module.file, owner.symbol + ": JSX traversal budget exceeded"); return ""; }
|
|
1016
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
|
+
}
|
|
1017
1492
|
if (ts.isJsxFragment(node))
|
|
1018
1493
|
return node.children
|
|
1019
1494
|
.map((n, i) =>
|
|
@@ -1023,6 +1498,13 @@ export async function analyze({
|
|
|
1023
1498
|
if (ts.isJsxText(node)) return escape(jsxText(node.text));
|
|
1024
1499
|
if (ts.isJsxExpression(node))
|
|
1025
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
|
+
}
|
|
1026
1508
|
if(ts.isArrowFunction(node)){
|
|
1027
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);
|
|
1028
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))){
|
|
@@ -1113,12 +1595,14 @@ export async function analyze({
|
|
|
1113
1595
|
gap("UNOBSERVED_CONDITION", m.file, label);
|
|
1114
1596
|
// Preserve both source alternatives for exact runtime binding. Neither
|
|
1115
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);
|
|
1116
1600
|
return [yes, no].map((branch,index)=>{
|
|
1117
1601
|
if(!branch)return '';
|
|
1118
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);
|
|
1119
1603
|
record.set(id,{elementKey,source,synthetic:true});
|
|
1120
1604
|
elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});
|
|
1121
|
-
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>';
|
|
1122
1606
|
}).join('');
|
|
1123
1607
|
}
|
|
1124
1608
|
if (
|
|
@@ -1133,22 +1617,28 @@ export async function analyze({
|
|
|
1133
1617
|
fn &&
|
|
1134
1618
|
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))
|
|
1135
1619
|
) {
|
|
1136
|
-
|
|
1620
|
+
const limit=ctx.__iamCollectionTemplate?128:50;
|
|
1621
|
+
if (values.length > limit)
|
|
1137
1622
|
gap(
|
|
1138
1623
|
"COLLECTION_TRUNCATED",
|
|
1139
1624
|
m.file,
|
|
1140
1625
|
"Static collection limited to 50 preview items",
|
|
1141
1626
|
);
|
|
1142
1627
|
return values
|
|
1143
|
-
.slice(0,
|
|
1628
|
+
.slice(0, limit)
|
|
1144
1629
|
.map((value, i) => {
|
|
1145
1630
|
const next = { ...ctx };
|
|
1146
1631
|
if (fn.parameters[0] && ts.isIdentifier(fn.parameters[0].name))
|
|
1147
1632
|
next[fn.parameters[0].name.text] = value;
|
|
1148
1633
|
if (fn.parameters[1] && ts.isIdentifier(fn.parameters[1].name))
|
|
1149
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);
|
|
1150
1640
|
return renderExpr(
|
|
1151
|
-
|
|
1641
|
+
uiReturnExpression(fn),
|
|
1152
1642
|
owner,
|
|
1153
1643
|
next,
|
|
1154
1644
|
instance + "/item-" + i,
|
|
@@ -1158,7 +1648,7 @@ export async function analyze({
|
|
|
1158
1648
|
.join("");
|
|
1159
1649
|
}
|
|
1160
1650
|
gap("DYNAMIC_COLLECTION", m.file, node.expression.getText(m.source));
|
|
1161
|
-
return unobservedValue(node, owner, instance);
|
|
1651
|
+
return unobservedValue(node, owner, instance,queryBacked(node.expression.expression,owner,ctx));
|
|
1162
1652
|
}
|
|
1163
1653
|
if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1164
1654
|
if (
|
|
@@ -1168,6 +1658,9 @@ export async function analyze({
|
|
|
1168
1658
|
)
|
|
1169
1659
|
return ctx[node.text].iamChildren;
|
|
1170
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;
|
|
1171
1664
|
if (expr && readVariables(expr).length && ["string", "number"].includes(typeof value)) {
|
|
1172
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 });
|
|
1173
1666
|
record.set(id, { elementKey, source, synthetic: true });
|
|
@@ -1184,7 +1677,7 @@ export async function analyze({
|
|
|
1184
1677
|
return escape(value);
|
|
1185
1678
|
if (isUnknown(value)) {
|
|
1186
1679
|
gap("DYNAMIC_VALUE", m.file, node.getText(m.source).slice(0, 100));
|
|
1187
|
-
return unobservedValue(node, owner, instance);
|
|
1680
|
+
return unobservedValue(node, owner, instance,queryBacked(node,owner,ctx));
|
|
1188
1681
|
}
|
|
1189
1682
|
return "";
|
|
1190
1683
|
}
|
|
@@ -1192,12 +1685,13 @@ export async function analyze({
|
|
|
1192
1685
|
throw new Error("Rendered node budget exceeded");
|
|
1193
1686
|
const opening = ts.isJsxElement(node) ? node.openingElement : node,
|
|
1194
1687
|
tag = opening.tagName.getText(m.source),
|
|
1195
|
-
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=[];
|
|
1196
1689
|
for (const attr of opening.attributes.properties) {
|
|
1197
1690
|
if (ts.isJsxSpreadAttribute(attr)) {
|
|
1198
1691
|
const v = evalExpression(attr.expression, m, ctx);
|
|
1199
1692
|
if (isUnknown(v)) {
|
|
1200
1693
|
for (const attribute of anchorAttributes) delete props[attribute];
|
|
1694
|
+
props.__iamOpaqueProps = true;
|
|
1201
1695
|
gap("UNKNOWN_ANCHOR_SPREAD", m.file, "Opaque spread may override element identity");
|
|
1202
1696
|
continue;
|
|
1203
1697
|
}
|
|
@@ -1207,17 +1701,22 @@ export async function analyze({
|
|
|
1207
1701
|
for (const key of Object.keys(v)) {
|
|
1208
1702
|
for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
|
|
1209
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);
|
|
1210
1705
|
}
|
|
1211
|
-
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??[])); }
|
|
1212
1707
|
}
|
|
1213
1708
|
continue;
|
|
1214
1709
|
}
|
|
1215
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
|
+
}
|
|
1216
1715
|
for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
|
|
1217
1716
|
for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
|
|
1218
|
-
if (["onClick", "onChange", "onInput"].includes(key)) {
|
|
1717
|
+
if (["onClick", "onChange", "onInput", "onValueChange", "onOpenChange"].includes(key)) {
|
|
1219
1718
|
props[key] = undefined;
|
|
1220
|
-
const supported = uiHandler(attr.initializer, owner, ctx);
|
|
1719
|
+
const supported = ctx.__iamCollectionTemplate?null:uiHandler(attr.initializer, owner, ctx, key);
|
|
1221
1720
|
if (supported) { props[key] = undefined; uiHandlers.push({ ...supported, key, source: sourceLocation(m, attr, owner.symbol) }); }
|
|
1222
1721
|
const fn =
|
|
1223
1722
|
attr.initializer && ts.isJsxExpression(attr.initializer)
|
|
@@ -1249,7 +1748,7 @@ export async function analyze({
|
|
|
1249
1748
|
key +
|
|
1250
1749
|
" at line " +
|
|
1251
1750
|
sourceLocation(m, attr, owner.symbol).line +
|
|
1252
|
-
":
|
|
1751
|
+
": handler is not simulated; no business execution",
|
|
1253
1752
|
);
|
|
1254
1753
|
continue;
|
|
1255
1754
|
}
|
|
@@ -1275,6 +1774,16 @@ export async function analyze({
|
|
|
1275
1774
|
const expr = uiExpression(attr.initializer.expression, owner, ctx);
|
|
1276
1775
|
if (expr && readVariables(expr).length) uiBindings.push({ property: key, value: expr, source: sourceLocation(m, attr, owner.symbol) });
|
|
1277
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;}
|
|
1278
1787
|
props[key] = !attr.initializer
|
|
1279
1788
|
? true
|
|
1280
1789
|
: ts.isStringLiteral(attr.initializer)
|
|
@@ -1285,24 +1794,96 @@ export async function analyze({
|
|
|
1285
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');
|
|
1286
1795
|
}
|
|
1287
1796
|
}
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
+
|
|
1301
1880
|
if (ctx[tag]?.iamChildren !== undefined) return ctx[tag].iamChildren;
|
|
1881
|
+
if (ctx[tag]?.iamRenderChildren) return ctx[tag].iamRenderChildren(ctx);
|
|
1302
1882
|
const routerPrimitive = externalImport(m, tag);
|
|
1303
|
-
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 ?? "";
|
|
1304
1885
|
if (
|
|
1305
|
-
/^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
|
|
1886
|
+
!localTarget && /^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
|
|
1306
1887
|
tag,
|
|
1307
1888
|
)
|
|
1308
1889
|
)
|
|
@@ -1310,12 +1891,26 @@ export async function analyze({
|
|
|
1310
1891
|
const intrinsicAlias = typeof ctx[tag] === "string" && allowedTags.has(ctx[tag]);
|
|
1311
1892
|
const importedPrimitive=m.imports.get(tag),knownExternalPrimitive=importedPrimitive?.specifier==='react-basics'&&['Text','Button','SubmitButton'].includes(importedPrimitive.name);
|
|
1312
1893
|
let htmlTag = intrinsicAlias ? ctx[tag] : tag;
|
|
1894
|
+
if(provider)return children;
|
|
1313
1895
|
if (tag === "html" || tag === "body") {
|
|
1314
1896
|
htmlTag = "div";
|
|
1315
1897
|
props["data-iam-adapter"] = tag;
|
|
1316
1898
|
}
|
|
1317
1899
|
if (/^[A-Z]/.test(tag) && !intrinsicAlias) {
|
|
1318
|
-
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)) {
|
|
1319
1914
|
htmlTag = "a";
|
|
1320
1915
|
if (typeof props.to === "string") props.href = props.to;
|
|
1321
1916
|
}
|
|
@@ -1335,13 +1930,17 @@ export async function analyze({
|
|
|
1335
1930
|
.join(" ");
|
|
1336
1931
|
} else {
|
|
1337
1932
|
const ref = componentBinding(m, tag),
|
|
1338
|
-
target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
|
|
1933
|
+
target = ctx[tag]?.[COMPONENT]??(ref && componentDefs.get(ref.module.file + "#" + ref.name));
|
|
1339
1934
|
if (target) {
|
|
1340
1935
|
const own = components.get(owner.key);
|
|
1341
1936
|
if (!own.dependencies.includes(target.key))
|
|
1342
1937
|
own.dependencies.push(target.key);
|
|
1343
|
-
const
|
|
1344
|
-
|
|
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 });
|
|
1345
1944
|
return renderComponent(
|
|
1346
1945
|
target,
|
|
1347
1946
|
childProps,
|
|
@@ -1349,6 +1948,23 @@ export async function analyze({
|
|
|
1349
1948
|
depth + 1,
|
|
1350
1949
|
);
|
|
1351
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
|
+
}
|
|
1352
1968
|
htmlTag = /Button|Trigger/.test(tag)
|
|
1353
1969
|
? "button"
|
|
1354
1970
|
: /Input/.test(tag)
|
|
@@ -1361,6 +1977,10 @@ export async function analyze({
|
|
|
1361
1977
|
}
|
|
1362
1978
|
}
|
|
1363
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
|
+
}
|
|
1364
1984
|
const anchor = anchorAttributes
|
|
1365
1985
|
.map((key) =>
|
|
1366
1986
|
typeof props[key] === "string" ? key + "=" + props[key] : null,
|
|
@@ -1385,6 +2005,7 @@ export async function analyze({
|
|
|
1385
2005
|
element: elementKey,
|
|
1386
2006
|
instance,
|
|
1387
2007
|
});
|
|
2008
|
+
if(!ctx.__iamCollectionTemplate)for(const request of uiAttributes)attributeRequests.push({nodeId,...request});
|
|
1388
2009
|
elements.set(elementKey, {
|
|
1389
2010
|
elementKey,
|
|
1390
2011
|
componentKey: owner.key,
|
|
@@ -1401,13 +2022,34 @@ export async function analyze({
|
|
|
1401
2022
|
source,
|
|
1402
2023
|
synthetic: htmlTag !== tag && tag !== "Link" && tag !== "Image"&&!intrinsicAlias&&!knownExternalPrimitive,
|
|
1403
2024
|
});
|
|
1404
|
-
|
|
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))) {
|
|
1405
2046
|
const event = handler.key === "onClick" ? "click" : htmlTag === "select" || handler.payload.checked ? "change" : "input";
|
|
1406
|
-
|
|
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 });
|
|
1407
2049
|
}
|
|
1408
|
-
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'))) {
|
|
1409
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;}
|
|
1410
|
-
const expectedType = ["checked", "disabled"].includes(binding.property) ? "boolean" : "string";
|
|
2052
|
+
const expectedType = ["checked", "disabled", "visible"].includes(binding.property) ? "boolean" : "string";
|
|
1411
2053
|
if (uiExpressionType(binding.value) !== expectedType) {
|
|
1412
2054
|
gap("UI_BINDING_TYPE_UNSUPPORTED", m.file, tag + "." + binding.property + ": source value cannot be represented by the typed UI simulation");
|
|
1413
2055
|
continue;
|
|
@@ -1421,7 +2063,7 @@ export async function analyze({
|
|
|
1421
2063
|
ui.transitions = ui.transitions.filter(t => t.trigger.nodeId !== nodeId);
|
|
1422
2064
|
gap("SENSITIVE_INPUT_REDACTED", m.file, "Input value and state bridge omitted");
|
|
1423
2065
|
}
|
|
1424
|
-
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'}:{}) };
|
|
1425
2067
|
for (const [key, value] of Object.entries(props)) {
|
|
1426
2068
|
if (key === "style") {
|
|
1427
2069
|
const css = staticStyle(value, () => gap("INLINE_STYLE_UNSUPPORTED", m.file,
|
|
@@ -1449,7 +2091,7 @@ export async function analyze({
|
|
|
1449
2091
|
clipRule: "clip-rule",
|
|
1450
2092
|
}[key] || key;
|
|
1451
2093
|
if (
|
|
1452
|
-
!/^(?: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-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(
|
|
1453
2095
|
attr,
|
|
1454
2096
|
)
|
|
1455
2097
|
)
|
|
@@ -1503,22 +2145,38 @@ export async function analyze({
|
|
|
1503
2145
|
}
|
|
1504
2146
|
return renderComponent(entryDef, props, instance, 0);
|
|
1505
2147
|
}
|
|
1506
|
-
function renderBranch(branch) {
|
|
1507
|
-
|
|
1508
|
-
|
|
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)};
|
|
1509
2159
|
const props = ["react-router", "tanstack-router"].includes(routing.framework)
|
|
1510
|
-
? { __iamOutlet:
|
|
1511
|
-
: { children:
|
|
1512
|
-
if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component =
|
|
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 = {}; }
|
|
1513
2163
|
for (const slot of layer.slots) {
|
|
1514
|
-
props[slot.name] = {
|
|
2164
|
+
props[slot.name] = { iamRenderChildren:next=>renderBranch(slot.branch,next) };
|
|
1515
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");
|
|
1516
2166
|
}
|
|
1517
|
-
|
|
2167
|
+
return renderEntry(layer.file, props, "layout:" + path.posix.dirname(layer.file), layer.symbol);
|
|
1518
2168
|
}
|
|
1519
|
-
return
|
|
2169
|
+
return renderLayer(branch.layers.length-1,inherited);
|
|
1520
2170
|
}
|
|
1521
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
|
+
}
|
|
1522
2180
|
const rootId = identity("node", { page: pageKey, root: true, ...(composition.entryMode === "soft" ? { composition: composition.compositionKey } : {}) }),
|
|
1523
2181
|
rootElement = identity("element", { page: pageKey, root: true });
|
|
1524
2182
|
record.set(rootId, {
|
|
@@ -1636,21 +2294,12 @@ export async function analyze({
|
|
|
1636
2294
|
source: page.source,
|
|
1637
2295
|
documentKey,
|
|
1638
2296
|
});
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (!states.some((s) => s.stateKey === key)) {
|
|
1646
|
-
states.push({
|
|
1647
|
-
stateKey: key,
|
|
1648
|
-
pageKey,
|
|
1649
|
-
label: branch.label,
|
|
1650
|
-
evidence: branch.evidence,
|
|
1651
|
-
source: branch.source,
|
|
1652
|
-
documentKey: null,
|
|
1653
|
-
});
|
|
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});
|
|
1654
2303
|
page.stateKeys.push(key);
|
|
1655
2304
|
}
|
|
1656
2305
|
}
|
|
@@ -1679,7 +2328,7 @@ export async function analyze({
|
|
|
1679
2328
|
for (const node of doc.nodes) {
|
|
1680
2329
|
if (
|
|
1681
2330
|
node.kind !== "element" ||
|
|
1682
|
-
|
|
2331
|
+
node.tag !== "a"
|
|
1683
2332
|
)
|
|
1684
2333
|
continue;
|
|
1685
2334
|
const href = node.attributes.href || "",
|
|
@@ -1713,9 +2362,18 @@ export async function analyze({
|
|
|
1713
2362
|
});
|
|
1714
2363
|
}
|
|
1715
2364
|
const packagedAssets = [];
|
|
2365
|
+
inventory.push(...await referencedRasterSources(root,assets.keys()));
|
|
1716
2366
|
for (const [url] of assets) {
|
|
1717
2367
|
gap("PUBLIC_ASSET_NOT_PACKAGED", "public", url + ": requires an explicit public-asset build input");
|
|
1718
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+'))';
|
|
1719
2377
|
let css = "";
|
|
1720
2378
|
try {
|
|
1721
2379
|
const tw = dependency("tailwindcss");
|
|
@@ -1746,16 +2404,16 @@ export async function analyze({
|
|
|
1746
2404
|
];
|
|
1747
2405
|
const custom =
|
|
1748
2406
|
"@theme inline {" +
|
|
1749
|
-
tokens.map((t) => "--color-" + t + ":
|
|
2407
|
+
tokens.map((t) => "--color-" + t + ":" + color(t) + ";").join("") +
|
|
1750
2408
|
"--font-heading: ui-sans-serif,system-ui;--radius-lg:var(--radius);--radius-md:calc(var(--radius) - 2px);}";
|
|
1751
2409
|
const compile = await tw.value.compile(
|
|
1752
|
-
theme + "\n" + custom + "\n@tailwind utilities;",
|
|
2410
|
+
theme + "\n" + custom + "\n" + sourceThemes + "\n@tailwind utilities;",
|
|
1753
2411
|
);
|
|
1754
2412
|
css = compile.build([...allClasses].sort());
|
|
1755
2413
|
} catch (error) {
|
|
1756
2414
|
gap("STYLE_COMPILER", "", error.message);
|
|
1757
2415
|
}
|
|
1758
|
-
const vars =
|
|
2416
|
+
const vars = sourceStyles
|
|
1759
2417
|
.flatMap((s) =>
|
|
1760
2418
|
[...s.text.matchAll(/(?::root|\.dark)\s*\{([^{}]*)\}/g)].map(
|
|
1761
2419
|
(m) => m[0],
|
|
@@ -1765,7 +2423,7 @@ export async function analyze({
|
|
|
1765
2423
|
css +=
|
|
1766
2424
|
"\n" +
|
|
1767
2425
|
vars +
|
|
1768
|
-
"\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}";
|
|
1769
2427
|
const runtime = await fs.readFile(
|
|
1770
2428
|
new URL("../assets/document-runtime.js", import.meta.url),
|
|
1771
2429
|
"utf8",
|
|
@@ -1781,6 +2439,16 @@ export async function analyze({
|
|
|
1781
2439
|
doc.js = runtime;
|
|
1782
2440
|
sealDocument(doc);
|
|
1783
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);
|
|
1784
2452
|
const sourceGraphDigest = hashObject("iam/source-graph/v1", {
|
|
1785
2453
|
files: inventory,
|
|
1786
2454
|
components: [...components.values()],
|
|
@@ -1790,8 +2458,8 @@ export async function analyze({
|
|
|
1790
2458
|
schemaVersion: 2,
|
|
1791
2459
|
producer: {
|
|
1792
2460
|
name: "indiana-app-manifest-local",
|
|
1793
|
-
version: "0.
|
|
1794
|
-
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,
|
|
1795
2463
|
},
|
|
1796
2464
|
app: { name, platforms: ["web"] },
|
|
1797
2465
|
routing,
|
|
@@ -1824,7 +2492,29 @@ export async function analyze({
|
|
|
1824
2492
|
ui.bindings = ui.bindings.filter(b => concrete.has(b.nodeId));
|
|
1825
2493
|
ui.transitions = ui.transitions.filter(t => concrete.has(t.trigger.nodeId));
|
|
1826
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()];
|
|
1827
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);
|
|
1828
2518
|
manifest.integrity.routeTreeDigest = routeIndexDigest(manifest.routeTree);
|
|
1829
2519
|
const concreteNodes = new Map(documents.flatMap(doc => doc.nodes.map(node => [node.nodeId, node])));
|
|
1830
2520
|
for (const target of manifest.routeTree.targets) {
|