@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/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install --save-dev @tinkerlandai/indiana-iam
|
|
10
|
+
npm install --save-dev --save-exact @tinkerlandai/indiana-iam@0.7.0
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Next.js (15.4.1 or later)
|
|
@@ -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 admission uses the environment's 512 MiB source storage budget, 256-version limit and build-key upload rate limit; it has no separate 8 MiB file cap.
|
|
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
|
|
@@ -175,11 +246,25 @@ Values that look like credentials, tokens, emails or private URLs are removed, b
|
|
|
175
246
|
|
|
176
247
|
## Upload
|
|
177
248
|
|
|
249
|
+
IAM 0.7.0 uses the matching Indiana resumable ingestion protocol. Upgrade the build package together with the receiver.
|
|
250
|
+
|
|
178
251
|
Create a Key with only the `app-manifests:write` permission (Console → developer settings) and set it as
|
|
179
252
|
`INDIANA_IAM_KEY` in your build environment. Each production build then registers the IAM, stores it through a
|
|
180
|
-
|
|
253
|
+
private GCS resumable session and asks Indiana to validate it; the validated version becomes its client's current version.
|
|
181
254
|
Rebuilding unchanged source only makes the existing version current again.
|
|
182
255
|
|
|
256
|
+
Interrupted uploads resume when the same command or build runs again on the same machine. The uploader keeps the
|
|
257
|
+
request ID, original label and private GCS session in `~/.cache/indiana/iam-uploads` (directory mode 0700, files 0600),
|
|
258
|
+
scoped by Indiana origin, key fingerprint, client, platform and exact content. Set `INDIANA_IAM_CACHE_DIR` to a private
|
|
259
|
+
directory to retain this state in a CI cache; CI workers with no shared cache start a new upload. Never publish this
|
|
260
|
+
directory or place it in source/public build output. It contains bearer upload capabilities, but no API keys.
|
|
261
|
+
On recovery, Indiana checks current key authority before the cached storage session is used. GCS's persisted offset
|
|
262
|
+
selects the next byte; chunks are at most 8 MiB. Transient failures retry up to three times, while completed transfers
|
|
263
|
+
retry validation without uploading again. Recovery retains the existing 15-minute SQL upload deadline; an expired
|
|
264
|
+
intent starts a new version. A successful validation removes the local checkpoint. Storage sessions can remain valid
|
|
265
|
+
longer than the SQL intent, but cannot make an expired or unauthorized version ready. The matching Indiana ingestion
|
|
266
|
+
and build package must be deployed/published together for this transfer protocol.
|
|
267
|
+
|
|
183
268
|
An App environment can hold several clients, each a named build with its own current version: for example `web` and
|
|
184
269
|
`web-dashboard`, or later `ios` and `android` apps. Set the `client` option (or `INDIANA_IAM_CLIENT`) to 1-40 lowercase
|
|
185
270
|
letters, digits or hyphens starting with a letter; the default is `web`. A client keeps the platform of its first
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
}
|
|
10
10
|
let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
|
|
11
11
|
const patches = new Map();
|
|
12
|
-
const
|
|
12
|
+
const previewNodes = new Map([...document.querySelectorAll("[data-iam-node]")].map(el => [el.dataset.iamNode, el]));
|
|
13
|
+
const node = id => {
|
|
14
|
+
const cached = previewNodes.get(id);
|
|
15
|
+
if (cached?.isConnected) return cached;
|
|
16
|
+
const current = [...document.querySelectorAll("[data-iam-node]")].find(el => el.dataset.iamNode === id);
|
|
17
|
+
if (current) previewNodes.set(id, current);
|
|
18
|
+
return current;
|
|
19
|
+
};
|
|
13
20
|
function viewportValues(graph,routeKey,renderKey){
|
|
14
21
|
const values={};for(const variable of graph.variables??[]){
|
|
15
22
|
if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
|
|
@@ -34,6 +41,49 @@
|
|
|
34
41
|
applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
|
|
35
42
|
markUnobservedData(document,sourceUi,true);
|
|
36
43
|
for (const patch of patches.values()) applyPatch(patch);
|
|
44
|
+
reportProjection();
|
|
45
|
+
}
|
|
46
|
+
function reportProjection() {
|
|
47
|
+
const visibility = new WeakMap();
|
|
48
|
+
const visible = el => {
|
|
49
|
+
if (!el) return false;
|
|
50
|
+
if (visibility.has(el)) return visibility.get(el);
|
|
51
|
+
const style = getComputedStyle(el);
|
|
52
|
+
const result = !el.hidden && style.display !== "none" && style.visibility !== "hidden" &&
|
|
53
|
+
(!el.parentElement || visible(el.parentElement));
|
|
54
|
+
visibility.set(el, result);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
const variables = new Set();
|
|
58
|
+
const reads = expression => {
|
|
59
|
+
if (!expression || typeof expression !== "object") return;
|
|
60
|
+
if (expression.op === "var") variables.add(expression.key);
|
|
61
|
+
for (const value of Object.values(expression)) {
|
|
62
|
+
if (Array.isArray(value)) value.forEach(reads);
|
|
63
|
+
else if (value && typeof value === "object") reads(value);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
for (const binding of viewportGraph.bindings) {
|
|
67
|
+
if (binding.routeKey !== viewportRoute || binding.renderKey !== sourceRenderKey) continue;
|
|
68
|
+
const el = node(binding.nodeId);
|
|
69
|
+
if (el && visible(binding.property === "visible" ? el.parentElement : el)) reads(binding.value);
|
|
70
|
+
}
|
|
71
|
+
for (const transition of activeTransitions) if (visible(node(transition.trigger.nodeId))) {
|
|
72
|
+
transition.stateReads.forEach(key => variables.add(key));
|
|
73
|
+
transition.stateWrites.forEach(write => variables.add(write.variableKey));
|
|
74
|
+
}
|
|
75
|
+
const unobserved = [...document.querySelectorAll("[data-iam-preview-note]")].some(visible);
|
|
76
|
+
let content = false;
|
|
77
|
+
if (unobserved) {
|
|
78
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
79
|
+
let text;
|
|
80
|
+
while ((text = walker.nextNode())) if (text.textContent.trim() &&
|
|
81
|
+
!text.parentElement.closest("script,style,[data-iam-preview-note],[data-iam-observation]") && visible(text.parentElement)) {
|
|
82
|
+
content = true; break;
|
|
83
|
+
}
|
|
84
|
+
if (!content) content = [...document.querySelectorAll("img,svg,canvas,input,textarea,select,button")].some(visible);
|
|
85
|
+
}
|
|
86
|
+
send("projection", { visibleVariableKeys: [...variables], dataUnavailable: unobserved && !content });
|
|
37
87
|
}
|
|
38
88
|
function applyPatch(data) {
|
|
39
89
|
const el = node(data.nodeId); if (!el) return;
|
|
@@ -245,6 +245,9 @@ function markUnobservedData(doc, ui, enabled, selections = new Map()) {
|
|
|
245
245
|
field.setAttribute("placeholder", "未观测");
|
|
246
246
|
field.setAttribute("title", "此字段的数据未观测");
|
|
247
247
|
}
|
|
248
|
+
for (const root of doc.querySelectorAll('[data-iam-source-template="unobserved"]')) {
|
|
249
|
+
const note=doc.createElement('div');note.setAttribute('data-iam-preview-note','unobserved');note.className='iam-observation-note';note.setAttribute('role','status');note.textContent='集合数据未观测 · 可检查源码组件结构';root.before(note);
|
|
250
|
+
}
|
|
248
251
|
return unobserved;
|
|
249
252
|
}
|
|
250
253
|
/* Indiana UI simulation runtime 0.2.0. No merchant code, requests, authentication or purchases. */
|
|
@@ -258,7 +261,14 @@ function markUnobservedData(doc, ui, enabled, selections = new Map()) {
|
|
|
258
261
|
}
|
|
259
262
|
let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
|
|
260
263
|
const patches = new Map();
|
|
261
|
-
const
|
|
264
|
+
const previewNodes = new Map([...document.querySelectorAll("[data-iam-node]")].map(el => [el.dataset.iamNode, el]));
|
|
265
|
+
const node = id => {
|
|
266
|
+
const cached = previewNodes.get(id);
|
|
267
|
+
if (cached?.isConnected) return cached;
|
|
268
|
+
const current = [...document.querySelectorAll("[data-iam-node]")].find(el => el.dataset.iamNode === id);
|
|
269
|
+
if (current) previewNodes.set(id, current);
|
|
270
|
+
return current;
|
|
271
|
+
};
|
|
262
272
|
function viewportValues(graph,routeKey,renderKey){
|
|
263
273
|
const values={};for(const variable of graph.variables??[]){
|
|
264
274
|
if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
|
|
@@ -283,6 +293,49 @@ function markUnobservedData(doc, ui, enabled, selections = new Map()) {
|
|
|
283
293
|
applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
|
|
284
294
|
markUnobservedData(document,sourceUi,true);
|
|
285
295
|
for (const patch of patches.values()) applyPatch(patch);
|
|
296
|
+
reportProjection();
|
|
297
|
+
}
|
|
298
|
+
function reportProjection() {
|
|
299
|
+
const visibility = new WeakMap();
|
|
300
|
+
const visible = el => {
|
|
301
|
+
if (!el) return false;
|
|
302
|
+
if (visibility.has(el)) return visibility.get(el);
|
|
303
|
+
const style = getComputedStyle(el);
|
|
304
|
+
const result = !el.hidden && style.display !== "none" && style.visibility !== "hidden" &&
|
|
305
|
+
(!el.parentElement || visible(el.parentElement));
|
|
306
|
+
visibility.set(el, result);
|
|
307
|
+
return result;
|
|
308
|
+
};
|
|
309
|
+
const variables = new Set();
|
|
310
|
+
const reads = expression => {
|
|
311
|
+
if (!expression || typeof expression !== "object") return;
|
|
312
|
+
if (expression.op === "var") variables.add(expression.key);
|
|
313
|
+
for (const value of Object.values(expression)) {
|
|
314
|
+
if (Array.isArray(value)) value.forEach(reads);
|
|
315
|
+
else if (value && typeof value === "object") reads(value);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
for (const binding of viewportGraph.bindings) {
|
|
319
|
+
if (binding.routeKey !== viewportRoute || binding.renderKey !== sourceRenderKey) continue;
|
|
320
|
+
const el = node(binding.nodeId);
|
|
321
|
+
if (el && visible(binding.property === "visible" ? el.parentElement : el)) reads(binding.value);
|
|
322
|
+
}
|
|
323
|
+
for (const transition of activeTransitions) if (visible(node(transition.trigger.nodeId))) {
|
|
324
|
+
transition.stateReads.forEach(key => variables.add(key));
|
|
325
|
+
transition.stateWrites.forEach(write => variables.add(write.variableKey));
|
|
326
|
+
}
|
|
327
|
+
const unobserved = [...document.querySelectorAll("[data-iam-preview-note]")].some(visible);
|
|
328
|
+
let content = false;
|
|
329
|
+
if (unobserved) {
|
|
330
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
331
|
+
let text;
|
|
332
|
+
while ((text = walker.nextNode())) if (text.textContent.trim() &&
|
|
333
|
+
!text.parentElement.closest("script,style,[data-iam-preview-note],[data-iam-observation]") && visible(text.parentElement)) {
|
|
334
|
+
content = true; break;
|
|
335
|
+
}
|
|
336
|
+
if (!content) content = [...document.querySelectorAll("img,svg,canvas,input,textarea,select,button")].some(visible);
|
|
337
|
+
}
|
|
338
|
+
send("projection", { visibleVariableKeys: [...variables], dataUnavailable: unobserved && !content });
|
|
286
339
|
}
|
|
287
340
|
function applyPatch(data) {
|
|
288
341
|
const el = node(data.nodeId); if (!el) return;
|
package/assets/viewer.css
CHANGED
|
@@ -706,6 +706,15 @@ button:disabled:hover { background: transparent; }
|
|
|
706
706
|
.field input:disabled, .field textarea:disabled { color: var(--iam-ink-3); background: var(--iam-surface-sunk); }
|
|
707
707
|
.state-control { display: grid; gap: 8px; margin-block: 12px; }
|
|
708
708
|
.state-control select { width: 100%; min-height: 40px; border: 1px solid var(--iam-field-border); border-radius: 2px; background: var(--iam-surface); }
|
|
709
|
+
.state-group { border: 0; padding: 0; margin: 16px 0; min-width: 0; }
|
|
710
|
+
.state-group legend { color: var(--iam-ink); font-weight: 500; overflow-wrap: anywhere; }
|
|
711
|
+
.state-group .field { margin: 8px 0; }
|
|
712
|
+
.state-group[hidden], .state-group .field[hidden] { display: none; }
|
|
713
|
+
.state-other { margin-block: 12px; }
|
|
714
|
+
.state-other summary { cursor: pointer; color: var(--iam-ink-2); }
|
|
715
|
+
.projection-note { padding: 12px 16px; border-bottom: 1px solid var(--iam-line); background: var(--iam-surface-sunk); color: var(--iam-ink-2); }
|
|
716
|
+
.projection-note p { margin: 0 0 8px; max-width: 80ch; }
|
|
717
|
+
.projection-note[hidden] { display: none; }
|
|
709
718
|
#preview { color-scheme: normal; background: #ffffff; }
|
|
710
719
|
.frame-wrap { background: #ffffff; }
|
|
711
720
|
body.embedded #export { display: none; }
|
package/assets/viewer.html
CHANGED
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
<div class="breadcrumb">
|
|
64
64
|
<span id="route"></span><span id="document-status"></span>
|
|
65
65
|
</div>
|
|
66
|
+
<div id="projection-note" class="projection-note" role="status" hidden>
|
|
67
|
+
<p>此页面依赖未观测的运行时数据,当前无法展示完整内容。可查看未覆盖项,或从页面地图查看源码分支。</p>
|
|
68
|
+
<button id="projection-gaps">查看未覆盖项</button>
|
|
69
|
+
</div>
|
|
66
70
|
<div class="canvas" id="canvas">
|
|
67
71
|
<div class="frame-wrap" id="frame-wrap">
|
|
68
72
|
<iframe
|
package/assets/viewer.js
CHANGED
|
@@ -67,7 +67,9 @@ const iamTranslations = {
|
|
|
67
67
|
"界面状态": "UI states",
|
|
68
68
|
"分析范围与未覆盖项": "Coverage and gaps",
|
|
69
69
|
"DOM 映射 / SHA-256 / 源码证据": "DOM mappings / SHA-256 / source evidence",
|
|
70
|
-
"正在打开页面资料…": "Opening page data…"
|
|
70
|
+
"正在打开页面资料…": "Opening page data…",
|
|
71
|
+
"此页面依赖未观测的运行时数据,当前无法展示完整内容。可查看未覆盖项,或从页面地图查看源码分支。": "This page depends on runtime data that has not been observed. Its full content is unavailable. Review coverage gaps or inspect source branches in the page map.",
|
|
72
|
+
"查看未覆盖项": "Review coverage gaps"
|
|
71
73
|
};
|
|
72
74
|
const tr = value => iamEnglish ? iamTranslations[value] ?? value : value;
|
|
73
75
|
// Theme changes are scoped to the Console tool shell, never its merchant iframe.
|
|
@@ -107,6 +109,7 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
107
109
|
nonce = "",
|
|
108
110
|
viewport = matchMedia("(max-width: 900px)").matches ? 390 : 1280,
|
|
109
111
|
pendingAnchor = null;
|
|
112
|
+
let projection = null, stateSearch = "";
|
|
110
113
|
function setMobilePanel(panel) {
|
|
111
114
|
document.body.dataset.panel = panel;
|
|
112
115
|
for (const button of document.querySelectorAll(".mobile-panels [data-panel]")) {
|
|
@@ -305,6 +308,8 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
305
308
|
);
|
|
306
309
|
}
|
|
307
310
|
async function openPage(next, targetUrl = next.route, renderKey, historyMove=null) {
|
|
311
|
+
projection = null; stateSearch = "";
|
|
312
|
+
$("projection-note").hidden = true;
|
|
308
313
|
if (consoleContext) targetUrl = viewUrl(targetUrl);
|
|
309
314
|
retryOpen={pageKey:next.pageKey,targetUrl,renderKey,historyMove};
|
|
310
315
|
if(consoleContext)consoleContext.documentReady=false;
|
|
@@ -479,6 +484,11 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
479
484
|
}
|
|
480
485
|
}
|
|
481
486
|
if (data.type === "selected") showSelection(data.element);
|
|
487
|
+
if (data.type === "projection" && Array.isArray(data.visibleVariableKeys)) {
|
|
488
|
+
projection = data;
|
|
489
|
+
$("projection-note").hidden = data.dataUnavailable !== true;
|
|
490
|
+
renderStates(uiValues.get(viewKey()) ?? {});
|
|
491
|
+
}
|
|
482
492
|
if (data.type === "stateChanged") {uiValues.set(viewKey(),data.values);renderStates(data.values);}
|
|
483
493
|
if (data.type === "navigate") {
|
|
484
494
|
const interaction = m.interactions.find(
|
|
@@ -507,23 +517,66 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
507
517
|
if (data.type === "simulated") toast(data.message);
|
|
508
518
|
});
|
|
509
519
|
function renderStates(values = {}) {
|
|
510
|
-
const target = $("state-list"), canPreviewValues = scriptFor(doc).js.includes('type === "previewValues"');
|
|
520
|
+
const target = $("state-list"), canPreviewValues = scriptFor(doc).js.includes('type === "previewValues"');
|
|
521
|
+
const focused = document.activeElement?.dataset.variableKey;
|
|
522
|
+
const otherOpen = target.querySelector(".state-other")?.open ?? false;
|
|
523
|
+
target.replaceChildren();
|
|
511
524
|
const renders=m.routeTree.renders?.filter(row=>row.routeKey===page.pageKey)??[];
|
|
512
525
|
if(renders.length>1){const label=text('label',iamEnglish?'Page state':'页面状态','state-control'),select=document.createElement('select');select.setAttribute('aria-label',iamEnglish?'Page state':'页面状态');for(const [index,row]of renders.entries()){const option=document.createElement('option');option.value=row.renderKey;option.textContent=row.entryMode==='hard'?(iamEnglish?'Direct load':'直接打开'):(iamEnglish?'Soft navigation ':'软导航 ')+index;option.selected=row.renderKey===render?.renderKey;select.append(option);}select.addEventListener('change',()=>openPage(page,currentUrl,select.value));label.append(select);target.append(label);}
|
|
513
526
|
const variables = m.routeTree.variables.filter(v => v.routeKey === page.pageKey && (!render || v.renderKey===render.renderKey));
|
|
514
527
|
const stateSection = target.closest("details"), hasControls = String(variables.length > 0 || renders.length > 1);
|
|
515
528
|
if (consoleContext && stateSection.dataset.hasControls !== hasControls) { stateSection.open = hasControls === "true"; stateSection.dataset.hasControls = hasControls; }
|
|
529
|
+
const active = new Set(projection?.visibleVariableKeys ?? []);
|
|
530
|
+
const other = document.createElement("details"); other.className = "state-other"; other.open = otherOpen;
|
|
531
|
+
other.append(text("summary", (iamEnglish ? "Other source states" : "其他源码状态") + ` (${variables.filter(v => !active.has(v.variableKey)).length})`));
|
|
532
|
+
const search = document.createElement("input"); search.type = "search"; search.value = stateSearch;
|
|
533
|
+
search.placeholder = iamEnglish ? "Find states or components" : "查找状态或组件";
|
|
534
|
+
search.setAttribute("aria-label", search.placeholder);
|
|
535
|
+
const searchLabel = text("label", iamEnglish ? "Filter states" : "筛选状态", "field"); searchLabel.append(search);
|
|
536
|
+
if (variables.length > 8) target.append(searchLabel);
|
|
537
|
+
const groups = new Map();
|
|
538
|
+
const filter = () => {
|
|
539
|
+
stateSearch = search.value;
|
|
540
|
+
const query = stateSearch.trim().toLocaleLowerCase();
|
|
541
|
+
for (const group of groups.values()) {
|
|
542
|
+
let shown = false;
|
|
543
|
+
for (const field of group.querySelectorAll(".field")) {
|
|
544
|
+
field.hidden = !field.dataset.search.includes(query); shown ||= !field.hidden;
|
|
545
|
+
}
|
|
546
|
+
group.hidden = !shown;
|
|
547
|
+
}
|
|
548
|
+
if (query) other.open = true;
|
|
549
|
+
};
|
|
550
|
+
search.addEventListener("input", filter);
|
|
516
551
|
for (const variable of variables) {
|
|
552
|
+
const source = typeof variable.source === "number" ? m.routeTree.sourcePool?.[variable.source] : variable.source;
|
|
553
|
+
const owner = source?.symbol || source?.file?.split("/").at(-1) || (iamEnglish ? "Page" : "页面");
|
|
554
|
+
const isActive = active.has(variable.variableKey), groupKey = `${isActive}:${source?.file}:${owner}`;
|
|
555
|
+
if (!groups.has(groupKey)) {
|
|
556
|
+
const group = document.createElement("fieldset"); group.className = "state-group";
|
|
557
|
+
group.append(text("legend", owner)); groups.set(groupKey, group);
|
|
558
|
+
(isActive ? target : other).append(group);
|
|
559
|
+
}
|
|
560
|
+
const group = groups.get(groupKey);
|
|
517
561
|
const value = Object.hasOwn(values, variable.variableKey) ? values[variable.variableKey] : variable.initialValue;
|
|
518
|
-
if (!canPreviewValues) {
|
|
562
|
+
if (!canPreviewValues) {
|
|
563
|
+
const field = text("p", variable.label + " = " + JSON.stringify(value), "field");
|
|
564
|
+
field.dataset.search = `${variable.label} ${owner} ${source?.file ?? ""}`.toLocaleLowerCase();
|
|
565
|
+
group.append(field); continue;
|
|
566
|
+
}
|
|
519
567
|
const label = text("label", variable.label, "field"), input = document.createElement("input");
|
|
568
|
+
label.dataset.search = `${variable.label} ${owner} ${source?.file ?? ""}`.toLocaleLowerCase();
|
|
569
|
+
if (source) label.title = `${source.file}:${source.line}`;
|
|
520
570
|
input.dataset.variableKey = variable.variableKey;
|
|
521
571
|
input.type = variable.valueType === "boolean" ? "checkbox" : variable.valueType === "number" ? "number" : "text";
|
|
522
572
|
input.setAttribute("aria-label", variable.label); input.maxLength = 4000;
|
|
523
573
|
if (variable.valueType === "boolean") input.checked = value; else input.value = String(value);
|
|
524
574
|
input.addEventListener("change", () => message("previewValues", { values: { [variable.variableKey]: variable.valueType === "boolean" ? input.checked : variable.valueType === "number" ? Number(input.value) : input.value } }));
|
|
525
|
-
label.append(input);
|
|
575
|
+
label.append(input); group.append(label);
|
|
526
576
|
}
|
|
577
|
+
if (variables.some(v => !active.has(v.variableKey))) target.append(other);
|
|
578
|
+
filter();
|
|
579
|
+
if (focused) [...target.querySelectorAll("input")].find(input => input.dataset.variableKey === focused)?.focus();
|
|
527
580
|
if (variables.length && canPreviewValues) target.append(text("p", tr("仅模拟界面值,不执行业务操作,也不保存为线上修改。")));
|
|
528
581
|
const transitions = m.routeTree.transitions.filter(t => t.routeKey === page.pageKey && (!render || t.renderKey===render.renderKey));
|
|
529
582
|
target.append(text("p", transitions.length + tr(" 个可模拟变化;其他业务行为列于未覆盖项。")));
|
|
@@ -761,11 +814,17 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
761
814
|
toast(tr("字号须为 8–120,已恢复当前值。"));
|
|
762
815
|
});
|
|
763
816
|
$("color").addEventListener("input", () => patch("color", $("color").value));
|
|
817
|
+
$("pick").setAttribute("aria-pressed", String(selecting));
|
|
764
818
|
$("pick").addEventListener("click", () => {
|
|
765
819
|
selecting = !selecting;
|
|
766
820
|
$("pick").classList.toggle("active", selecting);
|
|
821
|
+
$("pick").setAttribute("aria-pressed", String(selecting));
|
|
767
822
|
message("mode", { selecting });
|
|
768
823
|
});
|
|
824
|
+
$("projection-gaps").addEventListener("click", () => {
|
|
825
|
+
const section = $("gaps").closest("details"); section.open = true;
|
|
826
|
+
setMobilePanel("inspector"); section.querySelector("summary").focus();
|
|
827
|
+
});
|
|
769
828
|
let fitRequest = 0;
|
|
770
829
|
if (typeof ResizeObserver !== "undefined") new ResizeObserver(() => {
|
|
771
830
|
cancelAnimationFrame(fitRequest);
|
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.7.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": {
|