@tinkerlandai/indiana-iam 0.5.0 → 0.7.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/src/analyze.mjs CHANGED
@@ -16,6 +16,8 @@ import { dependency } from "./dependencies.mjs";
16
16
  import { assembleRouteTree, readVariables } from "./route-tree.mjs";
17
17
  import { sourceEntry } from "./source-entry.mjs";
18
18
  import { frameworkRoutes, externalImport } from "./framework-routes.mjs";
19
+ import { reactNativeRoutes, nativeModuleCandidates } from "./react-native-routes.mjs";
20
+ import { nativeExpression, nativePrimitive } from "./react-native-projection.mjs";
19
21
  import { sourceResolution } from "./source-resolution.mjs";
20
22
  import { routeIndexDigest, refreshRenderHashes, flattenComposition } from "./render-index.mjs";
21
23
  import { storeDocumentNodes } from "./node-hashes.mjs";
@@ -200,7 +202,12 @@ export async function analyze({
200
202
  navigationPaths = [],
201
203
  entry,
202
204
  automaticAnchors = false,
205
+ platform = "web",
206
+ routerRoot,
203
207
  }) {
208
+ if (!["web", "ios", "android"].includes(platform)) throw Error("Unsupported IAM platform");
209
+ const nativePlatform = platform !== "web";
210
+ if (!nativePlatform && routerRoot !== undefined) throw Error("routerRoot is only supported for native Expo projects");
204
211
  root = await fs.realpath(root);
205
212
  const files = await sourceFiles(root, { maxFiles });
206
213
  const modules = new Map(),
@@ -266,7 +273,7 @@ export async function analyze({
266
273
  file,
267
274
  text,
268
275
  source,
269
- anchors: automaticAnchors && !mdx ? automaticSourceAnchors(file, source) : new Map(),
276
+ anchors: automaticAnchors && !nativePlatform && !mdx ? automaticSourceAnchors(file, source) : new Map(),
270
277
  mdx,
271
278
  originalSource: mdx ? ts.createSourceFile(file, originalText, ts.ScriptTarget.Latest, false) : source,
272
279
  bindings: new Map(),
@@ -338,6 +345,15 @@ export async function analyze({
338
345
  module.exports.set(e.name.text,local);if(e.name.text==='default')module.defaultName=local;
339
346
  }
340
347
  }
348
+ if (/^(?:src\/)?pages\//.test(file)) {
349
+ for (const statement of source.statements) {
350
+ const assignment = ts.isExpressionStatement(statement) && unwrap(statement.expression);
351
+ if (assignment && ts.isBinaryExpression(assignment) && assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(assignment.left) && assignment.left.name.text === 'getLayout' && ts.isIdentifier(assignment.left.expression) && assignment.left.expression.text === module.defaultName) {
352
+ const fn = functionFrom(assignment.right);
353
+ if (fn && fn.parameters.length === 1 && ts.isIdentifier(fn.parameters[0].name) && !fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) module.bindings.set('$pageLayout', fn);
354
+ }
355
+ }
356
+ }
341
357
  modules.set(file, module);
342
358
  }
343
359
  const alias = await sourceAliases(root,ts);
@@ -360,7 +376,7 @@ export async function analyze({
360
376
  }
361
377
  if (!relative || relative.startsWith("../")) return null;
362
378
  return (
363
- [
379
+ (nativePlatform ? nativeModuleCandidates(relative, platform) : [
364
380
  relative,
365
381
  ...[
366
382
  ".tsx",
@@ -374,7 +390,7 @@ export async function analyze({
374
390
  "/index.ts",
375
391
  "/index.jsx",
376
392
  ].map((ext) => relative + ext),
377
- ]
393
+ ])
378
394
  .map((p) => modules.get(p))
379
395
  .find(Boolean) || null
380
396
  );
@@ -391,7 +407,7 @@ export async function analyze({
391
407
  }
392
408
  function sourceContextCall(node,module,ctx){
393
409
  const imported=externalImport(module,node.expression.getText(module.source));
394
- if(imported?.specifier==='react-i18next'&&imported.name==='useTranslation')return {t:translationHook(node,module,ctx)};
410
+ if(['react-i18next','next-i18next'].includes(imported?.specifier)&&imported.name==='useTranslation')return {t:translationHook(node,module,ctx)};
395
411
  if(imported?.specifier==='react'&&imported.name==='useContext'&&ts.isIdentifier(node.arguments[0]))return ctx.__iamContexts?.get(contextKey(module,node.arguments[0].text))??UNKNOWN;
396
412
  const ref=ts.isIdentifier(node.expression)&&binding(module,node.expression.text),fn=ref&&functionFrom(ref.node);
397
413
  if(!fn?.body||!ts.isBlock(fn.body))return UNKNOWN;
@@ -411,6 +427,15 @@ export async function analyze({
411
427
  }
412
428
  let i18nextPreview;
413
429
  function translationHook(node,module,ctx){
430
+ if(externalImport(module,node.expression.getText(module.source))?.specifier==='next-i18next') {
431
+ const config=[...modules.values()].find(m=>/^next-i18next\.config\.[cm]?[jt]s$/.test(m.file));
432
+ const statement=config?.source.statements.find(s=>ts.isExpressionStatement(s)&&ts.isBinaryExpression(s.expression)&&s.expression.left.getText(config.source)==='module.exports');
433
+ const options=statement&&evalExpression(statement.expression.right,config,{}),language=options?.i18n?.defaultLocale;
434
+ const namespace=node.arguments[0]?evalExpression(node.arguments[0],module,ctx):options?.defaultNS??'common';
435
+ const catalog=typeof language==='string'&&typeof namespace==='string'?jsonResources.get('public/locales/'+language+'/'+namespace+'.json'):null;
436
+ if(catalog&&typeof catalog==='object') {translationResources.add(catalog);gap('UI_LOCALE_FALLBACK_PREVIEW',config.file,'Read-only preview uses source defaultLocale '+language+'; request locale and remote translations are unobserved.');return {[TRANSLATION]:catalog};}
437
+ return UNKNOWN;
438
+ }
414
439
  i18nextPreview??=sourceI18next({ts,modules,binding,imported:externalImport,jsonResource})??false;
415
440
  if(!i18nextPreview)return UNKNOWN;
416
441
  const namespace=node.arguments[0]?evalExpression(node.arguments[0],module,ctx):i18nextPreview.namespace;
@@ -426,6 +451,10 @@ export async function analyze({
426
451
  if(seen.has(module.file+'#'+tag)||seen.size>8)return null;seen.add(module.file+'#'+tag);
427
452
  const [name,part,extra]=tag.split('.'),ref=module.imports.get(name);
428
453
  const imported=externalImport(module,tag);
454
+ if (ref?.specifier === 'vaul' && ref.name === 'Drawer' && !extra) {
455
+ const mapped = {Root:'Root',Portal:'Portal',Overlay:'Backdrop',Content:'Popup',Title:'Title',Description:'Description',Trigger:'Trigger',Close:'Close'}[part];
456
+ if (mapped) return {family:'Dialog',part:mapped};
457
+ }
429
458
  if (ref?.specifier === '@mantine/core' && ref.name === 'Tabs' && !extra) return {family:'Tabs',part:part ?? 'Root',mantine:true};
430
459
  if(imported?.specifier==='@headlessui/react'){
431
460
  const part={Dialog:'Root',DialogPanel:'Popup',DialogBackdrop:'Backdrop',DialogTitle:'Title',Description:'Description'}[imported.name];
@@ -599,6 +628,13 @@ export async function analyze({
599
628
  node = unwrap(node);
600
629
  if (!node || depth > 30) return UNKNOWN;
601
630
  const ev = (n) => evalExpression(n, module, context, stack, depth + 1);
631
+ if (nativePlatform) {
632
+ const result = nativeExpression(node, module, { ts, platform, evaluate: ev, unknown: UNKNOWN,
633
+ shadowed: name => Object.hasOwn(context, name),
634
+ gap: detail => gap("RN_CAPABILITY_UNOBSERVED", module.file, detail),
635
+ });
636
+ if (result) return result.value;
637
+ }
602
638
  if (ts.isAwaitExpression(node)) return query(node, module, context, true);
603
639
  const component=!(ts.isIdentifier(node)&&Object.hasOwn(context,node.text))&&sourceComponent(node,module);if(component)return component;
604
640
  if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
@@ -791,6 +827,7 @@ export async function analyze({
791
827
  args = node.arguments.map(ev),
792
828
  method = node.expression.name.text;
793
829
  if (isUnknown(target) || args.some(isUnknown)) return query(node,module,context);
830
+ if (typeof target === 'string' && method === 'toString' && !args.length) return target;
794
831
  if (
795
832
  typeof target === "string" &&
796
833
  [
@@ -855,7 +892,7 @@ export async function analyze({
855
892
  const ctx = { ...props, __iamSetters: {}, __iamStates: new Set(), __iamStateNodes: {}, __iamLocals: new Map(), __iamQueryLocals: new Map(), __iamPropBindings: new Map(), __iamPropHandlers: new Map(), __iamDisclosures: [] };
856
893
  const param = def.fn?.parameters?.[0];
857
894
  if (param) {
858
- if (ts.isIdentifier(param.name)) ctx[param.name.text] = props;
895
+ if (ts.isIdentifier(param.name)) ctx[param.name.text] = def.symbol === '$pageLayout' ? props.children : props;
859
896
  else if (ts.isObjectBindingPattern(param.name))
860
897
  for (const el of param.name.elements) {
861
898
  if (el.dotDotDotToken && ts.isIdentifier(el.name)) {
@@ -962,7 +999,7 @@ export async function analyze({
962
999
  module.file,
963
1000
  "Middleware/proxy may change public routes; not executed by source analysis",
964
1001
  );
965
- if (!/^next\.config\.[cm]?[jt]s$/.test(module.file)) continue;
1002
+ if (nativePlatform || !/^next\.config\.[cm]?[jt]s$/.test(module.file)) continue;
966
1003
  const found = new Set();
967
1004
  function readConfig(node) {
968
1005
  const key = node.name?.text;
@@ -1000,11 +1037,18 @@ export async function analyze({
1000
1037
  !/^\/(?:[^/?#[\]\\\s]+\/)*[^/?#[\]\\\s]+$/.test(routing.basePath)
1001
1038
  )
1002
1039
  throw new Error("Invalid source basePath");
1003
- const selection = await sourceEntry({ root, modules, resolveModule, ts, entry });
1040
+ const selection = nativePlatform ? { discoveryModules: modules } : await sourceEntry({ root, modules, resolveModule, ts, entry });
1041
+ if (nativePlatform) {
1042
+ gap("RN_SOURCE_PREVIEW", "", "Inert HTML projection of React Native source. Native Yoga layout, device density, accessibility, gestures and navigation stacks are not observed; preview DOM locators never authorize native visual changes.");
1043
+ gap("RN_METRO_CONFIG_UNOBSERVED", "", "Uses default Metro source extensions; custom resolvers, Babel plugins and Metro configuration are not executed.");
1044
+ }
1004
1045
  const flowbiteTheme=rootFlowbiteTheme({ts,modules:selection.discoveryModules,imported:externalImport,evaluate:evalExpression});
1005
1046
  if (selection.htmlSource) inventory.push(selection.htmlSource);
1006
1047
  inventory.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
1007
- const compositions = frameworkRoutes({ modules, ...selection, resolveModule, ts, sourceLocation, navigationPaths, gap, resolution, inlineComponent });
1048
+ if (nativePlatform && navigationPaths.length) throw Error("Retained Web navigation paths do not describe a native back stack");
1049
+ const compositions = nativePlatform
1050
+ ? reactNativeRoutes({ modules, platform, entry, routerRoot, binding, sourceLocation, gap })
1051
+ : frameworkRoutes({ modules, ...selection, resolveModule, ts, sourceLocation, navigationPaths, gap, resolution, inlineComponent });
1008
1052
  const pageFiles = compositions.pageFiles;
1009
1053
  routing.framework = compositions.framework;
1010
1054
  if (compositions.routeSources) routing.routeSources = compositions.routeSources;
@@ -1193,7 +1237,7 @@ export async function analyze({
1193
1237
  if(fn&&ts.isPropertyAccessExpression(fn)&&ctx.__iamPropHandlers.has(fn.getText(owner.module.source)))return ctx.__iamPropHandlers.get(fn.getText(owner.module.source));
1194
1238
  const eventValue = (variableKey) => ({ variableKey, value: {op:'event',key:'value'}, payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType} });
1195
1239
  if(fn&&ts.isIdentifier(fn)&&ctx.__iamPropHandlers.has(fn.text))return ctx.__iamPropHandlers.get(fn.text);
1196
- if(fn&&ts.isIdentifier(fn)&&ctx.__iamSetters[fn.text]&&['onValueChange','onOpenChange'].includes(property)) {
1240
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamSetters[fn.text]&&['onValueChange','onOpenChange',...(nativePlatform?['onChangeText']:[])].includes(property)) {
1197
1241
  const key=ctx.__iamVariableKeys[ctx.__iamSetters[fn.text]];return key?eventValue(key):null;
1198
1242
  }
1199
1243
  if(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text))fn=unwrap(ctx.__iamLocals.get(fn.text));
@@ -1223,7 +1267,7 @@ export async function analyze({
1223
1267
  if(!arg)return null;
1224
1268
  const forwardedWrites=(inherited.writes||[{variableKey:inherited.variableKey,value:inherited.value}]).map(write=>({...write,value:replaceEvent(write.value,arg)}));
1225
1269
  for(const write of forwardedWrites)inputs(write.value);
1226
- if(['onValueChange','onOpenChange'].includes(property))for(const [key,type]of Object.entries(inherited.payload))if(Object.hasOwn(payload,key))payload[key]=type;
1270
+ if(['onValueChange','onOpenChange',...(nativePlatform?['onChangeText']:[])].includes(property))for(const [key,type]of Object.entries(inherited.payload))if(Object.hasOwn(payload,key))payload[key]=type;
1227
1271
  if(forwardedWrites.some(write=>uiExpressionType(write.value,payload)!==ui.variables.find(v=>v.variableKey===write.variableKey)?.valueType))return null;
1228
1272
  writes.push(...forwardedWrites);continue;
1229
1273
  }
@@ -1234,7 +1278,7 @@ export async function analyze({
1234
1278
  const value=uiExpression(functional?functional.body:input,owner,ctx,fn.parameters[0]?.name?.text||null,aliases);
1235
1279
  if(!value)return null;
1236
1280
  inputs(value);const expected=ui.variables.find(v=>v.variableKey===variableKey)?.valueType;
1237
- if(value.op==='event'&&value.key==='value'&&['onValueChange','onOpenChange'].includes(property))payload.value=expected;
1281
+ if(value.op==='event'&&value.key==='value'&&['onValueChange','onOpenChange',...(nativePlatform?['onChangeText']:[])].includes(property))payload.value=expected;
1238
1282
  if(uiExpressionType(value,payload)!==expected)return null;
1239
1283
  writes.push({variableKey,value});
1240
1284
  }
@@ -1271,7 +1315,7 @@ export async function analyze({
1271
1315
  "data",
1272
1316
  "length",
1273
1317
  "size",
1274
- ].includes(n.name.text)
1318
+ ].includes(n.name.text) || ts.isPropertyAccessExpression(n) && isUnknown(evalExpression(n.expression,owner.module,ctx))
1275
1319
  )
1276
1320
  shape = true;
1277
1321
  ts.forEachChild(n, inspect);
@@ -1548,11 +1592,11 @@ export async function analyze({
1548
1592
  function renderExpr(node, owner, ctx, instance, depth) {
1549
1593
  node = unwrap(node);
1550
1594
  if (!node) return "";
1551
- if (depth > 45) { gap("RENDER_DEPTH_LIMIT", owner.module.file, owner.symbol + ": JSX traversal budget exceeded"); return ""; }
1595
+ if (depth > 128) { gap("RENDER_DEPTH_LIMIT", owner.module.file, owner.symbol + ": JSX nesting budget exceeded"); return ""; }
1552
1596
  const m = owner.module;
1553
1597
  if(ts.isCallExpression(node)&&ts.isIdentifier(node.expression)&&ctx.__iamLocals.has(node.expression.text)){
1554
1598
  const fn=unwrap(ctx.__iamLocals.get(node.expression.text));
1555
- if(fn&&ts.isArrowFunction(fn)&&!fn.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&!ts.isBlock(fn.body)&&hasSourceJsx(fn.body)&&fn.parameters.length<=8&&fn.parameters.every(p=>ts.isIdentifier(p.name)&&!p.dotDotDotToken)&&!node.arguments.some(ts.isSpreadElement)){
1599
+ if(fn&&(ts.isArrowFunction(fn)||ts.isFunctionDeclaration(fn)||ts.isFunctionExpression(fn))&&!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)))&&hasSourceJsx(uiReturnExpression(fn))&&fn.parameters.length<=8&&fn.parameters.every(p=>ts.isIdentifier(p.name)&&!p.dotDotDotToken)&&!node.arguments.some(ts.isSpreadElement)){
1556
1600
  return renderInvocation({callback:{fn,owner,ctx,instance:instance+'/source-helper'},args:node.arguments.map(arg=>evalExpression(arg,m,ctx)),caller:ctx},instance,depth+1);
1557
1601
  }
1558
1602
  }
@@ -1712,6 +1756,18 @@ export async function analyze({
1712
1756
  .join("");
1713
1757
  }
1714
1758
  gap("DYNAMIC_COLLECTION", m.file, node.expression.getText(m.source));
1759
+ // Inspect one source template, never a fabricated data item. It stays
1760
+ // hidden in the page preview; a nested UI scene can inspect its structure.
1761
+ if (fn && (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && !fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) && hasSourceJsx(uiReturnExpression(fn)) && !ctx.__iamUnknownTemplate) {
1762
+ const next = {...ctx,__iamUnknownTemplate:true};
1763
+ for (const parameter of fn.parameters) {
1764
+ if (ts.isIdentifier(parameter.name)) next[parameter.name.text] = UNKNOWN;
1765
+ else if (ts.isObjectBindingPattern(parameter.name)) for (const field of parameter.name.elements) if (ts.isIdentifier(field.name)) next[field.name.text] = UNKNOWN;
1766
+ }
1767
+ const source = sourceLocation(m,node,owner.symbol),elementKey=identity('element',{component:owner.key,template:node.getStart(m.source)}),id=identity('node',{page:pageKey,element:elementKey,instance});
1768
+ elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(id,{elementKey,source,synthetic:true});
1769
+ return '<div data-iam-node="'+id+'" data-iam-source-template="unobserved" hidden="">'+renderExpr(uiReturnExpression(fn),owner,next,instance+'/source-template',depth+1)+'</div>';
1770
+ }
1715
1771
  return unobservedValue(node, owner, instance,queryBacked(node.expression.expression,owner,ctx));
1716
1772
  }
1717
1773
  if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
@@ -1776,7 +1832,7 @@ export async function analyze({
1776
1832
  }
1777
1833
  for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
1778
1834
  for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
1779
- if (["onClick", "onChange", "onInput", "onValueChange", "onOpenChange", "onClose"].includes(key)) {
1835
+ if (["onClick", "onChange", "onInput", "onValueChange", "onOpenChange", "onClose", ...(nativePlatform ? ["onPress", "onChangeText"] : [])].includes(key)) {
1780
1836
  props[key] = undefined;
1781
1837
  const supported = ctx.__iamCollectionTemplate?null:uiHandler(attr.initializer, owner, ctx, key);
1782
1838
  if (supported) { props[key] = undefined; uiHandlers.push({ ...supported, key, source: sourceLocation(m, attr, owner.symbol) }); }
@@ -1906,8 +1962,21 @@ export async function analyze({
1906
1962
  }
1907
1963
  if(tabPart==='Root') {
1908
1964
  const valueBinding=uiBindings.find(b=>b.property==='value')?.value;
1909
- const controller=uiHandlers.find(h=>h.key==='onValueChange'||basePart?.mantine&&h.key==='onChange');
1910
- 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};
1965
+ let controller=uiHandlers.find(h=>h.key==='onValueChange'||basePart?.mantine&&h.key==='onChange');
1966
+ const attribute = name => {const a=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text===name);return a?.initializer&&ts.isJsxExpression(a.initializer)?unwrap(a.initializer.expression):null;};
1967
+ const input=attribute('value'),handler=attribute('onValueChange');
1968
+ if (!controller && handler && (ts.isArrowFunction(handler)||ts.isFunctionExpression(handler)) && !handler.modifiers?.length && handler.parameters.length===1 && ts.isIdentifier(handler.parameters[0].name)) {
1969
+ let call=ts.isBlock(handler.body)&&handler.body.statements.length===1?handler.body.statements[0]:handler.body;
1970
+ if(ts.isExpressionStatement(call))call=call.expression;call=unwrap(call);
1971
+ const label=call&&ts.isCallExpression(call)&&ts.isIdentifier(call.expression)&&call.arguments.length===1?ctx.__iamSetters[call.expression.text]:null,key=label&&ctx.__iamVariableKeys[label];
1972
+ if(key&&input&&collectionStateReads(ts,input,ctx).includes(key)) controller={variableKey:key,forValue:value=>{
1973
+ const written=evalExpression(call.arguments[0],m,{...ctx,[handler.parameters[0].name.text]:value});
1974
+ if(typeof written!=='string'||evalExpression(input,m,{...ctx,[label]:written})!==value)return null;
1975
+ return {variableKey:key,value:{op:'const',value:written}};
1976
+ }};
1977
+ }
1978
+ const variableKey=valueBinding?.op==='var'?valueBinding.key:controller?.forValue?controller.variableKey:null;
1979
+ tabGroup={tabKey:identity('ui-tab',{page:pageKey,scope:ctx.__iamScope,instance}),routeKey:pageKey,renderKey:'pending',nodeId:'pending',variableKey,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===variableKey)?.source??sourceLocation(m,node,owner.symbol),controller};
1911
1980
  childCtx={...childCtx,__iamTabs:tabGroup};
1912
1981
  }
1913
1982
  const mantineRef=m.imports.get(tag.split('.')[0]),mantineName=mantineRef?.specifier==='@mantine/core'?(mantineRef.name==='*'?tag.split('.').slice(1).join('.'):mantineRef.name+tag.split('.').slice(1).map(s=>'.'+s).join('')):null;
@@ -1921,7 +1990,8 @@ export async function analyze({
1921
1990
  const localRef=componentBinding(m,tag),localTarget=ctx[tag]?.[COMPONENT]??(localRef&&componentDefs.get(localRef.module.file+'#'+localRef.name));
1922
1991
  const dynamicNode=ctx.__iamLocals.get(tag);
1923
1992
  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;
1924
- 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||'';
1993
+ let childrenRenders=0;
1994
+ const renderChildren=(inherited=childCtx)=>{if(childrenRenders)gap('REPEATED_SOURCE_INSTANCE',m.file,'Forwarded children have distinct preview instances; repeated runtime selectors remain ambiguous');const childInstance=instance+(childrenRenders++?'/forward-'+(childrenRenders-1):'');return 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},childInstance+'/child-'+i,depth+1)).join(''):props.children?.iamRenderChildren?props.children.iamRenderChildren(inherited):props.children?.iamChildren||'';};
1925
1995
  let children = localTarget||registry ? '' : renderChildren();
1926
1996
  const mantine = mantinePrimitive(m.imports.get(tag.split('.')[0]), props, tag.split('.').slice(1).join('.'));
1927
1997
  if (mantine) {
@@ -1976,6 +2046,14 @@ export async function analyze({
1976
2046
 
1977
2047
  if (ctx[tag]?.iamChildren !== undefined) return ctx[tag].iamChildren;
1978
2048
  if (ctx[tag]?.iamRenderChildren) return ctx[tag].iamRenderChildren(ctx);
2049
+ const native = nativePlatform ? nativePrimitive(m, tag, props, { platform, gap: detail => gap("RN_CAPABILITY_UNOBSERVED", m.file, detail) }) : null;
2050
+ if (native?.ignore) return "";
2051
+ if (native?.outlet) {
2052
+ gap("RN_NAVIGATOR_SOURCE_ONLY", m.file, "Projects the current route through the source layout; native headers, tabs, focus and stack history are unobserved");
2053
+ return ctx.__iamOutlet?.iamRenderChildren?.(ctx) ?? "";
2054
+ }
2055
+ if (native?.props) Object.assign(props, native.props);
2056
+ if (native?.text !== undefined) children = escape(native.text);
1979
2057
  const routerPrimitive = externalImport(m, tag);
1980
2058
  if (['react-router','react-router-dom'].includes(routerPrimitive?.specifier)&&routerPrimitive.name==='Navigate')return '';
1981
2059
  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 ?? "";
@@ -1994,7 +2072,8 @@ export async function analyze({
1994
2072
  props["data-iam-adapter"] = tag;
1995
2073
  }
1996
2074
  if (/^[A-Z]/.test(tag) && !intrinsicAlias) {
1997
- if (mantine) htmlTag = mantine.tag;
2075
+ if (native?.tag) htmlTag = native.tag;
2076
+ else if (mantine) htmlTag = mantine.tag;
1998
2077
  else if(tabPart){
1999
2078
  htmlTag=tabPart==='Tab'?'button':'div';
2000
2079
  if(tabPart==='List')props.role='tablist';
@@ -2128,7 +2207,7 @@ export async function analyze({
2128
2207
  record.set(nodeId, {
2129
2208
  elementKey,
2130
2209
  source,
2131
- synthetic: Boolean(m.mdx) || htmlTag !== tag && tag !== "Link" && tag !== "Image"&&!intrinsicAlias&&!knownExternalPrimitive,
2210
+ synthetic: nativePlatform || Boolean(m.mdx) || htmlTag !== tag && tag !== "Link" && tag !== "Image"&&!intrinsicAlias&&!knownExternalPrimitive,
2132
2211
  });
2133
2212
  if(tabPart==='Root'){
2134
2213
  // Tab initialization below shares the same concrete source container.
@@ -2147,11 +2226,11 @@ export async function analyze({
2147
2226
  if(!tabGroup.items.some(item=>item.value===props.value))tabGroup.items.push({value:props.value,nodeId});
2148
2227
  props['data-iam-tab-value']=props.value;props['data-iam-tab-key']=tabGroup.tabKey;
2149
2228
  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]));
2150
- 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});}
2229
+ if(tabGroup.controller){const specialized=tabGroup.controller.forValue?.(props.value),writes=(tabGroup.controller.forValue?(specialized?[specialized]:[]):tabGroup.controller.writes||[{variableKey:tabGroup.controller.variableKey,value:tabGroup.controller.value}]).map(w=>({...w,value:replace(w.value)}));if(writes.length)uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});}
2151
2230
  }
2152
2231
  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});
2153
2232
  for (const handler of uiHandlers.filter(h=>!['onValueChange','onOpenChange','onClose'].includes(h.key)&&!(tabPart==='Root'&&h.key==='onChange'))) {
2154
- const event = handler.key === "onClick" ? "click" : htmlTag === "select" || handler.payload.checked ? "change" : "input";
2233
+ const event = ["onClick", "onPress"].includes(handler.key) ? "click" : htmlTag === "select" || handler.payload.checked ? "change" : "input";
2155
2234
  const writes=handler.writes||[{variableKey:handler.variableKey,value:handler.value}];
2156
2235
  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 });
2157
2236
  }
@@ -2164,7 +2243,7 @@ export async function analyze({
2164
2243
  }
2165
2244
  ui.bindings.push({ bindingKey: identity("ui-binding", { node: nodeId, property: binding.property }), routeKey: pageKey, nodeId, elementKey, ...binding });
2166
2245
  }
2167
- const sensitiveInput = ['input', 'textarea', 'select'].includes(htmlTag) && (htmlTag === 'input' && ["password", "hidden"].includes(String(props.type).toLowerCase()) || sensitiveName(String(props.name || props.id || "")));
2246
+ const sensitiveInput = native?.sensitive || ['input', 'textarea', 'select'].includes(htmlTag) && (htmlTag === 'input' && ["password", "hidden"].includes(String(props.type).toLowerCase()) || sensitiveName(String(props.name || props.id || "")));
2168
2247
  if (sensitiveInput) {
2169
2248
  delete props.value; delete props.defaultValue;
2170
2249
  if (htmlTag === 'textarea') children = '';
@@ -2212,7 +2291,7 @@ export async function analyze({
2212
2291
  clipRule: "clip-rule",
2213
2292
  }[key] || key;
2214
2293
  if (
2215
- !/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|required|readonly|multiple|selected|open|start|min|max|step|minlength|maxlength|pattern|autocomplete|inputmode|rows|cols|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(
2294
+ !/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|required|readonly|multiple|selected|open|start|min|max|step|minlength|maxlength|pattern|autocomplete|inputmode|rows|cols|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-(?:native-component|native-platform|native-unobserved|adapter|toggle|tab-value|tab-key|condition|alternative|condition-default|unobserved))$/.test(
2216
2295
  attr,
2217
2296
  )
2218
2297
  )
@@ -2284,7 +2363,7 @@ export async function analyze({
2284
2363
  return '<div data-iam-node="'+contentId+'" data-iam-page-content="true">'+content+'</div>';
2285
2364
  }
2286
2365
  const layer=branch.layers[index],child={iamRenderChildren:next=>renderLayer(index-1,next)};
2287
- const props = ["react-router", "tanstack-router"].includes(routing.framework)
2366
+ const props = ["react-router", "tanstack-router", "expo-router"].includes(routing.framework)
2288
2367
  ? { ...contextProps(ctx), __iamOutlet: child }
2289
2368
  : { ...contextProps(ctx), children: child };
2290
2369
  if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = child; props.pageProps = {}; }
@@ -2437,7 +2516,7 @@ export async function analyze({
2437
2516
  stateKey,
2438
2517
  kind: "source_projection",
2439
2518
  evidence: unprojected?'uncovered':"source_inferred",
2440
- viewport: { width: 1280, height: 800 },
2519
+ viewport: nativePlatform ? { width: 390, height: 844 } : { width: 1280, height: 800 },
2441
2520
  html: concreteRoot.outerHTML,
2442
2521
  css: "",
2443
2522
  js: "",
@@ -2590,9 +2669,9 @@ export async function analyze({
2590
2669
  producer: {
2591
2670
  name: "indiana-app-manifest-local",
2592
2671
  version: "0.27.0",
2593
- method: "typescript-ast-and-bounded-static-projection;lucide-static-"+LUCIDE_SOURCE_VERSION,
2672
+ method: "typescript-ast-and-bounded-static-projection;lucide-static-"+LUCIDE_SOURCE_VERSION+(nativePlatform ? ";react-native-source-preview-v1" : ""),
2594
2673
  },
2595
- app: { name, platforms: ["web"] },
2674
+ app: { name, platforms: [platform] },
2596
2675
  routing,
2597
2676
  release: { repository, commit, sourceGraphDigest },
2598
2677
  pages,
@@ -2607,7 +2686,7 @@ export async function analyze({
2607
2686
  runtimeObserved: false,
2608
2687
  generatedPageCount: pages.length,
2609
2688
  sourcePageFileCount: pageFiles.length,
2610
- unsupportedFrameworks: ["vue", "svelte", "android", "ios"],
2689
+ unsupportedFrameworks: nativePlatform ? ["vue", "svelte", "swiftui", "compose", "flutter", "react-navigation"] : ["vue", "svelte", "android", "ios"],
2611
2690
  gaps,
2612
2691
  },
2613
2692
  integrity: {
package/src/build.mjs CHANGED
@@ -27,10 +27,10 @@ async function guarded({ strict, logger }, action, what) {
27
27
  * Before compilation: scans source once per build and returns its release ID.
28
28
  * The same source and producer always get the same ID, so the page can embed its descriptor URL before the IAM exists.
29
29
  */
30
- export async function prepareBuildRelease({ root, name, entry, automaticAnchors = true, strict = false, logger = console }) {
30
+ export async function prepareBuildRelease({ root, name, entry, platform = "web", routerRoot, automaticAnchors = platform === "web", strict = false, logger = console }) {
31
31
  return guarded({ strict, logger }, async () => {
32
32
  root = await fs.realpath(root);
33
- const analysis = await analyzeSource({ root, name, entry, automaticAnchors });
33
+ const analysis = await analyzeSource({ root, name, entry, platform, routerRoot, automaticAnchors });
34
34
  const releaseId = deriveReleaseId(analysis);
35
35
  process.env[STATE] = JSON.stringify({ root, releaseId, automaticAnchors });
36
36
  return { releaseId };
@@ -60,7 +60,9 @@ export async function finishBuildRelease({
60
60
  releaseId,
61
61
  name,
62
62
  entry,
63
- automaticAnchors = true,
63
+ platform = "web",
64
+ routerRoot,
65
+ automaticAnchors = platform === "web",
64
66
  cssDirectory,
65
67
  mounts,
66
68
  publicDirectory,
@@ -72,7 +74,7 @@ export async function finishBuildRelease({
72
74
  }) {
73
75
  root = await fs.realpath(root);
74
76
  return guarded({ strict, logger }, async () => {
75
- const analysis = await analyzeSource({ root, name, entry, automaticAnchors });
77
+ const analysis = await analyzeSource({ root, name, entry, platform, routerRoot, automaticAnchors });
76
78
  if (deriveReleaseId(analysis) !== releaseId) throw new Error("Source changed during the build; rebuild before publishing IAM");
77
79
  const css = cssDirectory ? await compiledCss(root, cssDirectory) : [];
78
80
  const plain = () => generateAppManifest({ root, analysis, releaseId, referencedImages:true, mounts });
@@ -109,6 +111,9 @@ export async function finishBuildRelease({
109
111
  apiKey: settings.apiKey,
110
112
  baseUrl: settings.baseUrl,
111
113
  fetch: upload.fetch,
114
+ onProgress: ({ phase, uploadedBytes, totalBytes }) => {
115
+ if (phase === "upload") report(logger, "log", `IAM upload ${Math.floor(uploadedBytes / totalBytes * 100)}% (${uploadedBytes}/${totalBytes} bytes)`);
116
+ },
112
117
  });
113
118
  report(
114
119
  logger,
package/src/cli.mjs CHANGED
@@ -10,7 +10,7 @@ import { planAnchors, applyAnchors } from "./instrument.mjs";
10
10
  import { buildLabel, uploadAppManifest, uploadSettings } from "./upload.mjs";
11
11
 
12
12
  const usage =
13
- "Usage: indiana-iam generate --root <app> --out <new directory> [--entry <source module> --name <name> --repository <url> --commit <sha> --release-id <uuid> --write-public-release --build-css <file>...]" +
13
+ "Usage: indiana-iam generate --root <app> --out <new directory> [--platform web|ios|android --router-root <expo routes> --entry <source module> --name <name> --repository <url> --commit <sha> --release-id <uuid> --write-public-release --build-css <file>...]" +
14
14
  " | indiana-iam validate --file <manifest>" +
15
15
  " | INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest> [--client <key> --label <label> --base-url <url>]" +
16
16
  " | indiana-iam prepare --root <app>" +
@@ -23,6 +23,8 @@ const { values, positionals } = parseArgs({
23
23
  out: { type: "string" },
24
24
  name: { type: "string" },
25
25
  entry: { type: "string" },
26
+ platform: { type: "string" },
27
+ "router-root": { type: "string" },
26
28
  repository: { type: "string" },
27
29
  commit: { type: "string" },
28
30
  file: { type: "string" },
@@ -49,6 +51,8 @@ if (command === "generate") {
49
51
  if (!values.root || !values.out) throw new Error("generate requires --root and --out");
50
52
  if (values["write-public-release"] && !values["release-id"])
51
53
  throw new Error("--write-public-release requires the prebuild --release-id");
54
+ if (values["write-public-release"] && values.platform && values.platform !== "web")
55
+ throw new Error("Use the React Native build entry to stage a native release descriptor");
52
56
  if (values["root-font"] && !values["build-css"]?.length)
53
57
  throw new Error("--root-font requires --build-css");
54
58
  const root = await fs.realpath(values.root);
@@ -56,12 +60,15 @@ if (command === "generate") {
56
60
  root,
57
61
  name: values.name,
58
62
  entry: values.entry,
63
+ platform: values.platform,
64
+ routerRoot: values["router-root"],
59
65
  repository: values.repository || "",
60
66
  commit: values.commit || "",
61
67
  navigationPaths: (values["navigation-path"] || []).map((value) => JSON.parse(value)),
62
68
  releaseId: values["release-id"],
63
69
  buildCss: values["build-css"] || [],
64
70
  publicAssets: values["public-asset"] || [],
71
+ referencedImages: ["ios", "android"].includes(values.platform),
65
72
  optimizePublicImages: values["optimize-public-images"],
66
73
  rootFont: values["root-font"] || "",
67
74
  omitSvgResources: values["omit-svg-resources"],
@@ -105,6 +112,9 @@ if (command === "generate") {
105
112
  client: settings.client,
106
113
  apiKey: settings.apiKey,
107
114
  baseUrl: settings.baseUrl,
115
+ onProgress: ({ phase, uploadedBytes, totalBytes }) => {
116
+ console.error(`[indiana-iam] ${phase}${phase === "upload" ? ` ${Math.floor(uploadedBytes / totalBytes * 100)}% (${uploadedBytes}/${totalBytes} bytes)` : ""}`);
117
+ },
108
118
  });
109
119
  console.log(
110
120
  JSON.stringify({ uploaded, version: { id: version.id, client: version.clientKey, state: version.state, isCurrent: version.isCurrent } }, null, 2),
@@ -70,12 +70,11 @@ export function frameworkRoutes({
70
70
  const layers = [];
71
71
  if (customApp) {
72
72
  // A dynamic per-page layout is not the shared App wrapper contract.
73
- if (/\.getLayout\b/.test(modules.get(customApp).text))
74
- gap(
75
- "PAGES_LAYOUT_UNSUPPORTED",
76
- customApp,
77
- "Per-page getLayout requires an explicit adapter or a runtime observation; the page alone is projected",
78
- );
73
+ if (/\.getLayout\b/.test(modules.get(customApp).text)) {
74
+ if (modules.get(file).bindings.has('$pageLayout')) layers.push({file,symbol:'$pageLayout',slots:[]});
75
+ else if (/\.getLayout\s*=/.test(modules.get(file).text)) gap('PAGES_LAYOUT_UNSUPPORTED',file,'Dynamic per-page layout remains unobserved');
76
+ gap('PAGES_APP_WRAPPER_UNOBSERVED',customApp,'Per-page source layout is projected separately; shared provider effects are not executed');
77
+ }
79
78
  else layers.push({ file: customApp, slots: [] });
80
79
  }
81
80
  const composition = { lane, file, layers };
package/src/generate.mjs CHANGED
@@ -24,6 +24,8 @@ export async function analyzeSource({
24
24
  navigationPaths = [],
25
25
  entry,
26
26
  automaticAnchors = false,
27
+ platform = "web",
28
+ routerRoot,
27
29
  }) {
28
30
  root = await fs.realpath(root);
29
31
  return analyze({
@@ -34,6 +36,8 @@ export async function analyzeSource({
34
36
  navigationPaths,
35
37
  entry,
36
38
  automaticAnchors,
39
+ platform,
40
+ routerRoot,
37
41
  });
38
42
  }
39
43
 
@@ -50,6 +54,8 @@ export async function generateAppManifest({
50
54
  navigationPaths,
51
55
  entry,
52
56
  automaticAnchors = false,
57
+ platform,
58
+ routerRoot,
53
59
  releaseId,
54
60
  buildCss = [],
55
61
  publicAssets = [],
@@ -70,7 +76,11 @@ export async function generateAppManifest({
70
76
  navigationPaths,
71
77
  entry,
72
78
  automaticAnchors,
79
+ platform,
80
+ routerRoot,
73
81
  });
82
+ if (analysis && platform !== undefined && (analysis.app.platforms.length !== 1 || analysis.app.platforms[0] !== platform))
83
+ throw Error("Analyzed IAM belongs to another platform");
74
84
  for (const document of manifest.documents)
75
85
  if (document.domPayload) {
76
86
  Object.assign(document, documentDom(document));
package/src/index.d.ts CHANGED
@@ -20,8 +20,12 @@ export type ValidationCounts = Record<string, unknown>;
20
20
 
21
21
  export type GenerateOptions = {
22
22
  root: string;
23
- /** Browser entry relative to root. Defaults to the single module script in index.html. */
23
+ /** Web browser entry or single-root RN screen, relative to root. */
24
24
  entry?: string;
25
+ /** Defaults to web. RN/Expo artifacts are generated separately for ios and android. */
26
+ platform?: "web" | "ios" | "android";
27
+ /** Expo route root for a native platform; mutually exclusive with entry. */
28
+ routerRoot?: string;
25
29
  automaticAnchors?: boolean;
26
30
  /** A previous {@link analyzeSource} result; scanned again when omitted. */
27
31
  analysis?: AppManifest;
@@ -50,8 +54,12 @@ export type GeneratedAppManifest = {
50
54
 
51
55
  export declare function analyzeSource(options: {
52
56
  root: string;
53
- /** Browser entry relative to root. Defaults to the single module script in index.html. */
57
+ /** Web browser entry or single-root RN screen, relative to root. */
54
58
  entry?: string;
59
+ /** Defaults to web. RN/Expo artifacts are generated separately for ios and android. */
60
+ platform?: "web" | "ios" | "android";
61
+ /** Expo route root for a native platform; mutually exclusive with entry. */
62
+ routerRoot?: string;
55
63
  automaticAnchors?: boolean;
56
64
  name?: string;
57
65
  repository?: string;
@@ -87,8 +95,12 @@ export type BuildLogger = Pick<Console, "log" | "warn">;
87
95
  export type BuildReleaseOptions = {
88
96
  strict?: boolean;
89
97
  logger?: BuildLogger;
90
- /** Browser entry relative to root. Defaults to the single module script in index.html. */
98
+ /** Web browser entry or single-root RN screen, relative to root. */
91
99
  entry?: string;
100
+ /** Defaults to web. RN/Expo artifacts are generated separately for ios and android. */
101
+ platform?: "web" | "ios" | "android";
102
+ /** Expo route root for a native platform; mutually exclusive with entry. */
103
+ routerRoot?: string;
92
104
  automaticAnchors?: boolean;
93
105
  };
94
106
  export declare function prepareBuildRelease(
@@ -106,6 +118,7 @@ export declare function finishBuildRelease(
106
118
  omitSvgResources?: boolean;
107
119
  upload?: {
108
120
  apiKey?: string;
121
+ client?: string;
109
122
  baseUrl?: string;
110
123
  upload?: "production" | "always" | false;
111
124
  fetch?: typeof globalThis.fetch;
@@ -131,6 +144,13 @@ export declare function uploadAppManifest(options: {
131
144
  apiKey: string;
132
145
  baseUrl?: string;
133
146
  fetch?: typeof globalThis.fetch;
147
+ /** Private local recovery cache; defaults to INDIANA_IAM_CACHE_DIR or ~/.cache/indiana/iam-uploads. */
148
+ cacheDirectory?: string;
149
+ onProgress?: (progress: {
150
+ phase: "registration" | "upload" | "validation";
151
+ uploadedBytes: number;
152
+ totalBytes: number;
153
+ }) => void;
134
154
  }): Promise<{
135
155
  version: { id: string; state: string; isCurrent: boolean } & Record<
136
156
  string,
@@ -146,7 +166,13 @@ export declare function uploadSettings(
146
166
  client?: string;
147
167
  },
148
168
  env?: Record<string, string | undefined>,
149
- ): { enabled: boolean; apiKey: string; baseUrl: string; client: string; missingKey: boolean };
169
+ ): {
170
+ enabled: boolean;
171
+ apiKey: string;
172
+ baseUrl: string;
173
+ client: string;
174
+ missingKey: boolean;
175
+ };
150
176
  export declare function buildLabel(
151
177
  root: string,
152
178
  env?: Record<string, string | undefined>,
@@ -19,7 +19,7 @@ export async function validateIamBytes(bytes) {
19
19
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
20
20
  const manifest = parseManifest(text);
21
21
  const validation = await validate(manifest);
22
- if (manifest.coverage.runtimeObserved || manifest.app.platforms.length !== 1 || manifest.app.platforms[0] !== "web" || manifest.documents.some(d => d.kind !== "source_projection" || !['source_inferred','uncovered'].includes(d.evidence))) throw new Error("Only the Web source projection profile is admitted");
22
+ if (manifest.coverage.runtimeObserved || manifest.app.platforms.length !== 1 || !(manifest.app.platforms[0] === "web" || ["ios", "android"].includes(manifest.app.platforms[0]) && ["expo-router", "react-native"].includes(manifest.routing.framework)) || manifest.documents.some(d => d.kind !== "source_projection" || !['source_inferred','uncovered'].includes(d.evidence))) throw new Error("Only supported source projection profiles are admitted");
23
23
  return { manifest, validation, sha256: sha256(bytes), byteLength: bytes.byteLength };
24
24
  }
25
25
  /** Only a freshly authorized immutable ready fact from the trusted finalizer can use this path.
@@ -30,6 +30,7 @@ export async function readFinalizedIamBytes(bytes, fact) {
30
30
  if (stamp?.profile !== "bounded-web-preview" || stamp.runtimeVerified !== false || stamp.valid !== true || ![stamp.pages, stamp.components, stamp.elements, stamp.documents, stamp.concreteNodes].every(n => Number.isSafeInteger(n) && n >= 0) || typeof stamp.routeTree?.digest !== "string") return validateIamBytes(bytes);
31
31
  const manifest = parseManifest(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
32
32
  assertManifestShape(manifest);
33
+ if (["expo-router", "react-native"].includes(manifest.routing?.framework)) return validateIamBytes(bytes);
33
34
  if (manifest.format !== "indiana-app-manifest" || manifest.schemaVersion !== 2 || manifest.routeTree.schemaVersion !== 3 || manifest.coverage.runtimeObserved || manifest.app.platforms.length !== 1 || manifest.app.platforms[0] !== "web" || manifest.documents.some(d => d.kind !== "source_projection" || !["source_inferred", "uncovered"].includes(d.evidence)) || manifest.pages.length !== stamp.pages || manifest.documents.length !== stamp.documents || manifest.integrity.routeTreeDigest !== stamp.routeTree.digest) throw Error("Finalized IAM admission mismatch");
34
35
  const trusted = await loadTrustedRuntimes();
35
36
  for (const script of manifest.scripts ?? []) if (sha256(script.js) !== script.sha256 || script.scriptKey !== "script_" + script.sha256 || !trusted.includes(script.js)) throw Error("Finalized IAM runtime mismatch");