@tinkerlandai/indiana-iam 0.5.0 → 0.6.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 +78 -7
- package/package.json +6 -2
- package/src/analyze.mjs +108 -29
- package/src/build.mjs +6 -4
- package/src/cli.mjs +8 -1
- package/src/framework-routes.mjs +5 -6
- package/src/generate.mjs +10 -0
- package/src/index.d.ts +16 -3
- 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 +14 -0
- package/src/ui-structure.mjs +2 -1
- package/src/validate.mjs +7 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @tinkerlandai/indiana-iam
|
|
2
2
|
|
|
3
|
-
Build-time generator for the Indiana App Manifest (IAM).
|
|
4
|
-
and
|
|
5
|
-
|
|
3
|
+
Build-time generator for the Indiana App Manifest (IAM). It scans your App's source, generates and validates the IAM,
|
|
4
|
+
and writes an immutable release descriptor. Web integrations bind published visual changes to that exact source.
|
|
5
|
+
React Native / Expo integrations produce source previews; native visual-change execution is a separate capability.
|
|
6
6
|
|
|
7
7
|
Install it as a development dependency (Node.js 22 or later):
|
|
8
8
|
|
|
@@ -73,6 +73,77 @@ Browser apps use the single local module script in `index.html` to select reacha
|
|
|
73
73
|
For multiple entry points, set `indianaIam({ entry: "src/main.tsx" })`, or pass `--entry src/main.tsx` to standalone generation.
|
|
74
74
|
The path must stay inside the scanned root. Next.js uses its App/Pages root instead.
|
|
75
75
|
|
|
76
|
+
## React Native and Expo Router
|
|
77
|
+
|
|
78
|
+
Use the build-only `@tinkerlandai/indiana-iam/react-native` entry from a Node.js build script. Generate a separate artifact
|
|
79
|
+
for each platform. It reuses the same IAM format, validator, asset packaging, release identity and upload client as Web.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { prepareReactNativeIam, finishReactNativeIam } from "@tinkerlandai/indiana-iam/react-native";
|
|
83
|
+
|
|
84
|
+
const options = { root: process.cwd(), platform: "ios", routerRoot: "app", strict: true };
|
|
85
|
+
const prepared = await prepareReactNativeIam(options);
|
|
86
|
+
// Run your existing Expo / Metro bundle step here, using the same unchanged source.
|
|
87
|
+
const result = await finishReactNativeIam({
|
|
88
|
+
...options,
|
|
89
|
+
releaseId: prepared.releaseId,
|
|
90
|
+
bundleDirectory: "dist/ios", // asset staging directory inside root
|
|
91
|
+
});
|
|
92
|
+
// Include dist/ios + result.descriptorPath with this release's native assets.
|
|
93
|
+
// result.file is the validated IAM outside the checkout.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For Android, use `platform: "android"` and a separate staging directory. With Expo Router, `routerRoot` defaults to
|
|
97
|
+
the sole `app/` or `src/app/` directory. Ordinary route groups, index/dynamic/catch-all paths and nested `_layout`
|
|
98
|
+
modules with `Slot`, `Stack` or `Tabs` retain route and source provenance. Group paths that collapse to the same URL
|
|
99
|
+
are rejected. Shared array groups and native navigation state remain explicit gaps. Expo platform routes and layouts
|
|
100
|
+
need a non-platform base module. For a single exported React Native screen, use `entry: "App.tsx"` instead of `routerRoot`.
|
|
101
|
+
That entry does not extract a React Navigation graph or invoke `AppRegistry`.
|
|
102
|
+
|
|
103
|
+
The adapter selects `.ios` / `.android`, then `.native`, then base modules using Metro's default JS/JSX/TS/TSX
|
|
104
|
+
extensions. Literal `Platform.OS`, `Platform.select`, `StyleSheet.create`, `flatten`, `compose` and static style arrays
|
|
105
|
+
are interpreted from source. Basic View/Text/Image, buttons, text inputs, scrolling and modal declarations project to
|
|
106
|
+
inert semantic HTML. Local raster imports and literal `require` references use the existing bounded asset packer.
|
|
107
|
+
Supported local state setters on `onPress` / `onChangeText` reuse the bounded preview interpreter. Business handlers,
|
|
108
|
+
effects, remote data, custom Metro/Babel transforms, Yoga/device layout, safe-area measurement, navigation stacks,
|
|
109
|
+
gestures and virtual lists are not executed; their missing behavior stays in coverage gaps.
|
|
110
|
+
|
|
111
|
+
Every native preview node stays synthetic and supplies no SDK target binding, even when it has a `testID` or `nativeID`.
|
|
112
|
+
This entry adds no native runtime dependency or automatic source-anchor transform. The descriptor records release identity;
|
|
113
|
+
it does not install a native visual executor or make a Web visual script applicable to native views. Package its file
|
|
114
|
+
with native assets in your existing release process; the helper does not configure Metro or Xcode/Gradle for you.
|
|
115
|
+
|
|
116
|
+
Uploads are disabled by default. Explicitly authorized builds can pass
|
|
117
|
+
`upload: { upload: "always", client: "rn-ios", apiKey, baseUrl }` to `finishReactNativeIam`;
|
|
118
|
+
the default client is `rn-ios` / `rn-android`, and each client's platform is immutable. Use the matching Indiana
|
|
119
|
+
ingestion validator version before uploading new source adapters. Local generation does not deploy it.
|
|
120
|
+
|
|
121
|
+
Standalone generation uses the same adapter:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
indiana-iam generate --root . --platform ios --router-root app --out <new folder outside checkout>
|
|
125
|
+
indiana-iam generate --root . --platform android --entry App.tsx --out <another new folder>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Memos integration example
|
|
129
|
+
|
|
130
|
+
[`examples/memos/indiana.vite.config.mjs`](examples/memos/indiana.vite.config.mjs) wraps the original Vite configuration of
|
|
131
|
+
[Memos v0.25.3](https://github.com/usememos/memos/tree/e17cd163c6c37726f8397c8f426d585b540c9562). It keeps Memos’s Go backend,
|
|
132
|
+
SQLite database and application source, adds build anchors and binds the visual SDK to the generated release descriptor.
|
|
133
|
+
|
|
134
|
+
After installing Memos’s own Web dependencies, set `INDIANA_MEMOS_WEB_ROOT` to its absolute `web` directory,
|
|
135
|
+
`INDIANA_ENVIRONMENT_ID` to the intended Sandbox environment UUID, `INDIANA_CONSOLE_ORIGIN` to `https://dev.tinker.cash`
|
|
136
|
+
and `INDIANA_API_ORIGIN` to `https://api.dev.tinker.cash`. From Memos’s `web` directory, run its installed `vite build`
|
|
137
|
+
with `--config <Indiana checkout>/packages/iam/examples/memos/indiana.vite.config.mjs --mode release --outDir dist --emptyOutDir`.
|
|
138
|
+
The example disables automatic uploads; upload the IAM file reported in the build log through the separately authorized IAM upload flow.
|
|
139
|
+
|
|
140
|
+
Copy `web/dist` into Memos’s `server/router/frontend/dist` before building its native binary. Run that binary on
|
|
141
|
+
`127.0.0.1:15230`, then run [`examples/memos/gateway.mjs`](examples/memos/gateway.mjs) with the same Web root. The gateway
|
|
142
|
+
listens on `127.0.0.1:15231`, serves the generated `.well-known/indiana/releases/` descriptor omitted by Go’s `dist/*` embed,
|
|
143
|
+
and forwards every other request unchanged to native Memos. Ports can be set with `INDIANA_MEMOS_PORT` and
|
|
144
|
+
`INDIANA_MEMOS_BACKEND_PORT`. Allow the exact gateway origin in the same Sandbox environment before publishing page changes.
|
|
145
|
+
Keep accounts, keys, SQLite files, generated manifests and binaries outside the committed example.
|
|
146
|
+
|
|
76
147
|
## Options
|
|
77
148
|
|
|
78
149
|
Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload, client }`;
|
|
@@ -83,8 +154,8 @@ If source changes during compilation, generation fails instead of publishing a d
|
|
|
83
154
|
|
|
84
155
|
## Target identity and release changes
|
|
85
156
|
|
|
86
|
-
|
|
87
|
-
the source loader adds a deterministic `data-indiana-id` to unambiguous
|
|
157
|
+
Next.js / Vite builds reuse `id`, `data-testid`, `data-test`, `data-qa`, then `data-indiana-id`. When none is declared,
|
|
158
|
+
the source loader adds a deterministic `data-indiana-id` to unambiguous HTML JSX in the build output and IAM.
|
|
88
159
|
Source files stay unchanged; formatting, comments and unrelated sibling insertions preserve a leaf's marker.
|
|
89
160
|
Set `automaticAnchors: false` to use only existing attributes.
|
|
90
161
|
|
|
@@ -157,7 +228,7 @@ Source-proven `useSyncExternalStore` matchMedia getters retain both typed viewpo
|
|
|
157
228
|
viewport. Subscriptions are not run. UI preference cookies with an explicit source SSR guard use an empty-cookie
|
|
158
229
|
preview; auth/session cookies and arbitrary stores stay unobserved. Pure lazy local-state initializers are interpreted
|
|
159
230
|
under the same bounded rules; merchant callbacks and browser storage are never executed or read.
|
|
160
|
-
IAM has no configured page, node or total artifact byte limit. The exporter retains every supported route.
|
|
231
|
+
Local IAM generation has no configured page, node or total artifact byte limit. The exporter retains every supported route. Online ingestion separately limits each upload to 8 MiB (8,388,608 bytes); a large local artifact is not automatically uploadable. Signed GCS upload URLs expire within ten minutes.
|
|
161
232
|
Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
|
|
162
233
|
and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
|
|
163
234
|
HTML/DOM hashes and targets. Route navigation stores the exact interaction-array index instead of repeating its ID; validation checks node, route, destination and parameters. Source files retain their full hashes; component/element records cover reachable route compositions and shared fragments. Text node IDs that follow the original parent/index identity formula are derived locally instead of repeated; explicit exceptions remain stored, and every resolved source-map hash is checked. The document hash pins its encoded storage body; decoding is local and checks the exact declared size and hashes. Exported page HTML is expanded.
|
|
@@ -166,7 +237,7 @@ the selected document on demand, cancels superseded reads and retains one expand
|
|
|
166
237
|
come from the complete validated IAM; the complete JSON remains the source of release identity.
|
|
167
238
|
Serve the output directory over loopback HTTP to open `index.html`; browser `file://` restrictions prevent
|
|
168
239
|
on-demand reads of its document files. Serving the exported files does not run the App or its backend.
|
|
169
|
-
Use the matching
|
|
240
|
+
Use the matching generator, validator and viewer from the same package version together. Indiana ingestion/viewer sources must be updated
|
|
170
241
|
before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
|
|
171
242
|
viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
|
|
172
243
|
The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinkerlandai/indiana-iam",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Build-time Indiana App Manifest (IAM) generator for Next.js
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Build-time Indiana App Manifest (IAM) generator for Next.js, Vite and React Native / Expo source.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"./vite": {
|
|
39
39
|
"types": "./src/vite.d.ts",
|
|
40
40
|
"import": "./src/vite.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./react-native": {
|
|
43
|
+
"types": "./src/react-native.d.ts",
|
|
44
|
+
"import": "./src/react-native.mjs"
|
|
41
45
|
}
|
|
42
46
|
},
|
|
43
47
|
"scripts": {
|
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 });
|
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"],
|
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 };
|