@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/README.md +94 -9
- package/assets/document-controller.js +51 -1
- package/assets/document-runtime.js +54 -1
- package/assets/viewer.css +9 -0
- package/assets/viewer.html +4 -0
- package/assets/viewer.js +63 -4
- package/package.json +6 -2
- package/src/analyze.mjs +108 -29
- package/src/build.mjs +9 -4
- package/src/cli.mjs +11 -1
- package/src/framework-routes.mjs +5 -6
- package/src/generate.mjs +10 -0
- package/src/index.d.ts +30 -4
- package/src/library-entry.mjs +2 -1
- package/src/react-native-projection.mjs +300 -0
- package/src/react-native-routes.mjs +182 -0
- package/src/react-native.d.ts +47 -0
- package/src/react-native.mjs +45 -0
- package/src/render-index.mjs +1 -1
- package/src/routing.mjs +1 -1
- package/src/schema-definition.mjs +1 -1
- package/src/source-collections.mjs +3 -0
- package/src/source-next-redirects.mjs +98 -0
- package/src/source-preview-css.mjs +117 -0
- package/src/source-redirects.mjs +2 -0
- package/src/ui-preview.mjs +36 -0
- package/src/ui-structure.mjs +2 -1
- package/src/upload-checkpoint.mjs +59 -0
- package/src/upload.mjs +168 -49
- package/src/validate.mjs +7 -1
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(
|
|
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
|
-
|
|
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 >
|
|
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)
|
|
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
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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: [
|
|
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 =
|
|
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
|
-
|
|
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),
|
package/src/framework-routes.mjs
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
): {
|
|
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>,
|
package/src/library-entry.mjs
CHANGED
|
@@ -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]
|
|
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");
|