@tinkerlandai/indiana-iam 0.4.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 +104 -10
- package/assets/viewer.js +60 -32
- package/package.json +13 -3
- package/src/analyze.mjs +298 -87
- package/src/anchors.mjs +2 -0
- package/src/build-styles.mjs +14 -9
- package/src/build.mjs +6 -4
- package/src/cli.mjs +8 -1
- package/src/component-registry.mjs +1 -1
- package/src/document-material.mjs +180 -0
- package/src/dom-payload-json.mjs +2 -2
- package/src/dom-payload.mjs +3 -3
- package/src/framework-routes.mjs +6 -8
- package/src/generate.mjs +114 -24
- package/src/index.d.ts +16 -4
- package/src/index.mjs +1 -1
- package/src/json.mjs +2 -3
- package/src/library-entry.mjs +7 -3
- package/src/node-hashes.mjs +1 -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 +8 -8
- package/src/route-composition.mjs +5 -5
- package/src/route-tree.mjs +9 -9
- package/src/routing.mjs +8 -7
- package/src/schema-definition.mjs +14 -16
- package/src/selected-manifest.mjs +12 -0
- package/src/source-aliases.mjs +34 -0
- package/src/source-charts.mjs +15 -0
- package/src/source-collections.mjs +3 -0
- package/src/source-css-modules.mjs +79 -0
- package/src/source-files.mjs +8 -7
- package/src/source-flowbite.mjs +60 -0
- package/src/source-i18next.mjs +22 -0
- package/src/source-mantine.mjs +61 -0
- package/src/source-mdx.mjs +92 -0
- package/src/source-media.mjs +12 -3
- 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/source-styles.mjs +9 -0
- package/src/static-query.mjs +67 -7
- package/src/svg-preview.mjs +23 -0
- package/src/tanstack-routes.mjs +1 -1
- package/src/ui-preview.mjs +14 -0
- package/src/ui-structure.mjs +9 -19
- package/src/validate.mjs +7 -1
- package/src/viewer-index.mjs +49 -0
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
|
|
|
@@ -119,7 +190,26 @@ and statically generated TanStack file-route trees registered with `createRouter
|
|
|
119
190
|
React Router supports imported static path constants, inline `Component` functions and source-proven lazy import forwarding.
|
|
120
191
|
Data-router `route.lazy` objects support a static module's named `Component`, or a source-proven mapper that captures
|
|
121
192
|
the component export. Mapper factories, imports and business loaders are never executed. Unknown overrides remain gaps.
|
|
122
|
-
MDX pages
|
|
193
|
+
Markdown/MDX pages, layouts and local imports retain original routes and source coordinates.
|
|
194
|
+
The pinned Markdown/MDX parsers project headings, lists, GFM tables/tasks, code, links, images and bounded source JSX.
|
|
195
|
+
Merchant MDX compilers/plugins, ESM side effects, arbitrary expressions and active HTML are never executed.
|
|
196
|
+
Generated Markdown markup stays preview-only; custom MDX component mappings and plugin transforms remain gaps.
|
|
197
|
+
CSS Modules retain source-literal default imports, deterministic local preview class names,
|
|
198
|
+
scoped selectors and static declarations, including standard media/supports/container rules.
|
|
199
|
+
Global selectors, resource loads, composition, nested preprocessing and animation definitions stay explicit gaps;
|
|
200
|
+
merchant PostCSS plugins and configuration are never executed. Compiled CSS remains a separate build input.
|
|
201
|
+
Mantine core forms, headings, containers, tables, images and controlled Modal/Drawer visibility have an inert
|
|
202
|
+
semantic HTML profile. Source labels, ordinary form constraints and safe default values are retained;
|
|
203
|
+
password values stay redacted. Theme resolution, responsive object props, portal positioning, focus handling
|
|
204
|
+
and library callbacks remain unobserved. Boolean `useDisclosure` without lifecycle callbacks, controlled Tabs,
|
|
205
|
+
Drawer/Modal close controls and native Accordion folding have bounded local previews. This profile does not
|
|
206
|
+
import Mantine or run its hooks.
|
|
207
|
+
Async source helpers and TanStack queries can expose source-declared literal fixtures, including a source-proven
|
|
208
|
+
timer-only delay. IAM interprets their AST without executing functions, awaiting timers or invoking query callbacks.
|
|
209
|
+
Safe-integer locale formatting uses deterministic English grouping in the source preview.
|
|
210
|
+
Declared `initialData` retains its source initial state; loading/error/network outcomes otherwise stay unknown.
|
|
211
|
+
Only the known frontend `services/api` source path is included; framework API routes, server trees and credentials
|
|
212
|
+
stay outside the scan. Remote data, mutations, impure helpers and ambiguous bindings remain unobserved.
|
|
123
213
|
TanStack reads the registered generated tree, including pathless layouts, index routes and simple `$param` paths;
|
|
124
214
|
custom history/basepath and complex parameter patterns require separate adapters. Loaders, guards, retry/reload behavior
|
|
125
215
|
and runtime data remain unobserved. Unsupported optional React paths are isolated as gaps.
|
|
@@ -138,12 +228,16 @@ Source-proven `useSyncExternalStore` matchMedia getters retain both typed viewpo
|
|
|
138
228
|
viewport. Subscriptions are not run. UI preference cookies with an explicit source SSR guard use an empty-cookie
|
|
139
229
|
preview; auth/session cookies and arbitrary stores stay unobserved. Pure lazy local-state initializers are interpreted
|
|
140
230
|
under the same bounded rules; merchant callbacks and browser storage are never executed or read.
|
|
141
|
-
The
|
|
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.
|
|
142
232
|
Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
|
|
143
233
|
and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
|
|
144
|
-
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
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
235
|
+
The standalone viewer embeds only its route directory and hash-pinned document references. It reads and expands
|
|
236
|
+
the selected document on demand, cancels superseded reads and retains one expanded page. Derived page files
|
|
237
|
+
come from the complete validated IAM; the complete JSON remains the source of release identity.
|
|
238
|
+
Serve the output directory over loopback HTTP to open `index.html`; browser `file://` restrictions prevent
|
|
239
|
+
on-demand reads of its document files. Serving the exported files does not run the App or its backend.
|
|
240
|
+
Use the matching generator, validator and viewer from the same package version together. Indiana ingestion/viewer sources must be updated
|
|
147
241
|
before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
|
|
148
242
|
viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
|
|
149
243
|
The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
|
package/assets/viewer.js
CHANGED
|
@@ -119,6 +119,11 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
119
119
|
}
|
|
120
120
|
document.body.dataset.panel = "canvas";
|
|
121
121
|
$("viewport").value = String(viewport);
|
|
122
|
+
const inlineDocuments=new Map(m.documents.map(document=>[document.documentKey,document]));
|
|
123
|
+
const localDocumentFiles=new Map((m.documentFiles??[]).map(file=>[file.documentKey,file]));
|
|
124
|
+
const directoryInteractions=m.interactions;
|
|
125
|
+
const expandedDocuments=new WeakSet();
|
|
126
|
+
let localRead=null;
|
|
122
127
|
const changes = new Map(), baseline = new Map(), originalValues = new Map(), localValues = new Map(), savedViews = new Map(), seededViews = new Set();
|
|
123
128
|
let baselineLoaded = false;
|
|
124
129
|
let pendingDocument=null,projectionReadOnly=false,retryOpen=null;
|
|
@@ -208,7 +213,7 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
208
213
|
$("commit").textContent = "";
|
|
209
214
|
$("page-count").textContent = m.pages.length;
|
|
210
215
|
$("summary").textContent =
|
|
211
|
-
`${m.pages.length} ${tr("页面")} · ${m.components.length} ${tr("组件")} · ${consoleContext?.catalog?.nodeCount??m.documents.reduce((n, d) => n + d.nodes.length, 0)} ${tr("DOM 节点")}`;
|
|
216
|
+
`${m.pages.length} ${tr("页面")} · ${m.summary?.components??m.components.length} ${tr("组件")} · ${consoleContext?.catalog?.nodeCount??m.summary?.nodes??m.documents.reduce((n, d) => n + d.nodes.length, 0)} ${tr("DOM 节点")}`;
|
|
212
217
|
$("gap-count").textContent = `${consoleContext?.catalog?.gapCount??m.coverage.gaps.length} ${iamEnglish ? "coverage gaps" : "项缺口已记录"}`;
|
|
213
218
|
$("coverage-description").textContent =
|
|
214
219
|
tr("这是从 App 源码生成的 HTML/CSS/JS 投影。未运行原应用;动态数据、鉴权状态、字体与第三方组件可能不完整。只有明确提取的界面状态可模拟。");
|
|
@@ -299,13 +304,49 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
299
304
|
"<\/script>" + (keyboard ? "<script>" + keyboard.js.replaceAll("</script", "<\\/script") + "<\/script>" : "") + "</body></html>"
|
|
300
305
|
);
|
|
301
306
|
}
|
|
302
|
-
function openPage(next, targetUrl = next.route, renderKey, historyMove=null) {
|
|
307
|
+
async function openPage(next, targetUrl = next.route, renderKey, historyMove=null) {
|
|
303
308
|
if (consoleContext) targetUrl = viewUrl(targetUrl);
|
|
304
309
|
retryOpen={pageKey:next.pageKey,targetUrl,renderKey,historyMove};
|
|
305
310
|
if(consoleContext)consoleContext.documentReady=false;
|
|
306
311
|
const nextRender=m.routeTree.renders.find(render=>render.routeKey===next.pageKey&&render.renderKey===(renderKey??m.routeTree.routes.find(route=>route.routeKey===next.pageKey).initialRenderKey));
|
|
307
312
|
if (!nextRender) throw Error("Unknown preview render");
|
|
308
|
-
|
|
313
|
+
let nextDocument=byId(m.documents,"documentKey",nextRender.documentKey);
|
|
314
|
+
if(!consoleContext || consoleContext.mode!=="documents"){
|
|
315
|
+
localRead?.abort();localRead=null;pendingDocument=null;
|
|
316
|
+
if(!nextDocument||!expandedDocuments.has(nextDocument)){
|
|
317
|
+
const request={pageKey:next.pageKey,renderKey:nextRender.renderKey};
|
|
318
|
+
const controller=new AbortController();localRead=controller;pendingDocument=request;
|
|
319
|
+
$("document-status").textContent=tr("正在打开页面资料…");$("export").disabled=true;
|
|
320
|
+
try{
|
|
321
|
+
const file=localDocumentFiles.get(nextRender.documentKey);
|
|
322
|
+
if(file){
|
|
323
|
+
if(!/^document_[a-f0-9]{64}$/.test(file.documentKey)||!/^[a-f0-9]{64}$/.test(file.sha256)||!Number.isSafeInteger(file.bytes)||file.bytes<1)throw Error("Invalid local document reference");
|
|
324
|
+
const response=await fetch(new URL('documents/'+file.documentKey+'/material.json',location.href),{signal:controller.signal});
|
|
325
|
+
if(!response.ok)throw Error("Local document unavailable");
|
|
326
|
+
const bytes=await response.arrayBuffer();
|
|
327
|
+
if(bytes.byteLength!==file.bytes)throw Error("Local document size mismatch");
|
|
328
|
+
const actual=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
329
|
+
if(actual!==file.sha256)throw Error("Local document digest mismatch");
|
|
330
|
+
const material=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes));
|
|
331
|
+
const stored=material.document;
|
|
332
|
+
if(stored.documentKey!==nextRender.documentKey||stored.pageKey!==next.pageKey||stored.hashes.document!==nextRender.documentHash)throw Error("Local document scope mismatch");
|
|
333
|
+
const expanded=await expandNodeHashes(stored,request);
|
|
334
|
+
if(pendingDocument!==request)return;
|
|
335
|
+
for(const key of ['elements','styles','assets','scripts','components','interactions'])m[key]=material[key];
|
|
336
|
+
activeGraph=material.routeTree;m.routeTree={...activeGraph,routes:m.routeTree.routes,renders:m.routeTree.renders};expandedDocuments.add(expanded);m.documents=[expanded];
|
|
337
|
+
}else{
|
|
338
|
+
const stored=inlineDocuments.get(nextRender.documentKey);if(!stored)throw Error("Missing local document");
|
|
339
|
+
const expanded=await expandNodeHashes(stored,request);
|
|
340
|
+
if(pendingDocument!==request)return;
|
|
341
|
+
expandedDocuments.add(expanded);m.documents=[expanded];
|
|
342
|
+
}
|
|
343
|
+
pendingDocument=null;localRead=null;nextDocument=m.documents[0];
|
|
344
|
+
}catch(error){
|
|
345
|
+
if(pendingDocument===request){pendingDocument=null;localRead=null;$("document-status").textContent=tr("页面资料校验失败,请重新打开此版本。");toast(tr("页面资料校验失败,请重新打开此版本。"));}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
309
350
|
if(!nextDocument&&consoleContext?.mode==="documents"){
|
|
310
351
|
pendingDocument={requestId:crypto.randomUUID(),pageKey:next.pageKey,renderKey:nextRender.renderKey,targetUrl,historyMove,documentKey:nextRender.documentKey,documentHash:nextRender.documentHash};
|
|
311
352
|
$("document-status").textContent=tr("正在打开页面资料…");$("export").disabled=true;
|
|
@@ -493,12 +534,12 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
493
534
|
function checkedDomPayload(document) {
|
|
494
535
|
const payload = document.domPayload;
|
|
495
536
|
if (!payload) return null;
|
|
496
|
-
if (document.html !== "" || !Array.isArray(document.nodes) || document.nodes.length || Object.keys(payload).sort().join(",") !== "bytes,data,encoding" || payload.encoding !== "gzip-json-v1" || !Number.isSafeInteger(payload.bytes) || payload.bytes < 1 ||
|
|
537
|
+
if (document.html !== "" || !Array.isArray(document.nodes) || document.nodes.length || Object.keys(payload).sort().join(",") !== "bytes,data,encoding" || payload.encoding !== "gzip-json-v1" || !Number.isSafeInteger(payload.bytes) || payload.bytes < 1 || typeof payload.data !== "string" || !payload.data.length || payload.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(payload.data)) throw Error("Invalid compressed DOM payload");
|
|
497
538
|
return payload;
|
|
498
539
|
}
|
|
499
540
|
function parsedDomPayload(text) {
|
|
500
541
|
const value = JSON.parse(text);
|
|
501
|
-
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" ||
|
|
542
|
+
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" || !value.html.isWellFormed() || !Array.isArray(value.nodes) || JSON.stringify(value) !== text) throw Error("Invalid expanded DOM payload");
|
|
502
543
|
return value;
|
|
503
544
|
}
|
|
504
545
|
async function documentDomBrowser(document) {
|
|
@@ -529,7 +570,7 @@ async function documentDomBrowser(document) {
|
|
|
529
570
|
/* END COMPRESSED DOM */
|
|
530
571
|
/* BEGIN HTML DOM DERIVATION */
|
|
531
572
|
function expandDomNodes(document, body, deriveTextId) {
|
|
532
|
-
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash
|
|
573
|
+
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash)throw Error('Invalid HTML-derived DOM mode');
|
|
533
574
|
let index=0;const result=[];
|
|
534
575
|
function visit(node,parentId,depth,childIndex=0){
|
|
535
576
|
if(depth>128||![1,3].includes(node.nodeType))throw Error('Invalid HTML-derived DOM structure');
|
|
@@ -553,11 +594,11 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
553
594
|
const {domPayload, ...plain} = document;
|
|
554
595
|
document = {...plain, ...decoded};
|
|
555
596
|
}
|
|
556
|
-
if(!Array.isArray(document.nodes)
|
|
597
|
+
if(!Array.isArray(document.nodes))throw Error('Node hash budget');
|
|
557
598
|
const body=new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body,textIds=new Map(),pendingText=[];
|
|
558
599
|
let preorder=0;
|
|
559
600
|
const collectText=(node,parentId,childIndex=0,depth=0)=>{
|
|
560
|
-
if(!node||depth>128
|
|
601
|
+
if(!node||depth>128)throw Error('DOM derivation budget');
|
|
561
602
|
const ref=document.nodes[preorder++];if(!ref)throw Error('Missing source node');
|
|
562
603
|
const id=node.nodeType===1?node.getAttribute('data-iam-node'):ref.nodeId;
|
|
563
604
|
if(node.nodeType===3&&!id)pendingText.push({parentId,index:childIndex,key:parentId+':'+childIndex});
|
|
@@ -595,12 +636,12 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
595
636
|
const request=pendingDocument;
|
|
596
637
|
try{
|
|
597
638
|
const projection=data.projection;
|
|
598
|
-
if(projection?.format!=="indiana-document-projection"||projection.schemaVersion!==2||projection.versionId!==consoleContext.versionId||projection.manifestDigest!==consoleContext.manifestDigest||projection.pageKey!==request.pageKey||projection.renderKey!==(request.renderKey??null)||projection.documentKey!==request.documentKey||projection.documentHash!==request.documentHash||typeof projection.payload!=="string"
|
|
639
|
+
if(projection?.format!=="indiana-document-projection"||projection.schemaVersion!==2||projection.versionId!==consoleContext.versionId||projection.manifestDigest!==consoleContext.manifestDigest||projection.pageKey!==request.pageKey||projection.renderKey!==(request.renderKey??null)||projection.documentKey!==request.documentKey||projection.documentHash!==request.documentHash||typeof projection.payload!=="string")throw Error("Projection scope mismatch");
|
|
599
640
|
const material=JSON.parse(projection.payload),document=material.document;
|
|
600
641
|
const {hashes,...body}=document;
|
|
601
642
|
if(document.documentKey!==request.documentKey||document.pageKey!==request.pageKey||hashes.document!==request.documentHash||await hash("iam/document/v1\0"+canonical(body))!==request.documentHash)throw Error("Projection document hash mismatch");
|
|
602
643
|
const provided = new Map();
|
|
603
|
-
if (!Array.isArray(material.resourceRefs)
|
|
644
|
+
if (!Array.isArray(material.resourceRefs)) throw Error("Invalid preview resources");
|
|
604
645
|
for (const kind of ["styles","assets","scripts"]) {
|
|
605
646
|
if (!Array.isArray(material[kind])) throw Error("Invalid preview resource kind");
|
|
606
647
|
for (const value of material[kind]) {
|
|
@@ -616,7 +657,6 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
616
657
|
if(!entry||entry.kind!==ref.kind)throw Error("Missing preview resource");
|
|
617
658
|
resolved.set(ref.hash,entry);
|
|
618
659
|
}
|
|
619
|
-
if([...resolved.values()].reduce((bytes,entry)=>bytes+entry.bytes,0)>8_388_608)throw Error("Preview resource byte budget");
|
|
620
660
|
for (const kind of ["styles","assets","scripts"]) material[kind]=[...resolved.values()].filter(entry=>entry.kind===kind).map(entry=>entry.value);
|
|
621
661
|
const script=document.scriptKey?material.scripts.find(script=>script.scriptKey===document.scriptKey):{js:document.js,sha256:hashes.js};
|
|
622
662
|
if(!script||!consoleContext.reviewedRuntimeHashes.includes(script.sha256)||await hash(script.js)!==script.sha256||document.scriptKey&&document.scriptKey!=="script_"+script.sha256)throw Error("Unreviewed preview runtime");
|
|
@@ -629,7 +669,7 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
629
669
|
if(consoleContext.catalog)m.routeTree={...material.routeTree,routes:consoleContext.catalog.routes,renders:consoleContext.catalog.renders};
|
|
630
670
|
const expanded=await expandNodeHashes(document,request);
|
|
631
671
|
if(pendingDocument!==request)return;
|
|
632
|
-
m.documents=[expanded];projectionReadOnly=projection.readOnly;pendingDocument=null;
|
|
672
|
+
expandedDocuments.add(expanded);m.documents=[expanded];projectionReadOnly=projection.readOnly;pendingDocument=null;
|
|
633
673
|
openPage(m.pages.find(page=>page.pageKey===request.pageKey),request.targetUrl,request.renderKey,request.historyMove);
|
|
634
674
|
}catch{if(pendingDocument===request){pendingDocument=null;projectionReadOnly=true;renderSaveCount();parent.postMessage({channel:"indiana-iam-console-v1",nonce:consoleContext.nonce,versionId:consoleContext.versionId,type:"error"},"*");}}
|
|
635
675
|
return;
|
|
@@ -804,9 +844,9 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
804
844
|
text("strong", p.route),
|
|
805
845
|
text(
|
|
806
846
|
"small",
|
|
807
|
-
p.componentKeys.length +
|
|
847
|
+
(p.componentCount??p.componentKeys.length) +
|
|
808
848
|
tr(" 组件 · ") +
|
|
809
|
-
p.stateKeys.length +
|
|
849
|
+
(p.stateCount??p.stateKeys.length) +
|
|
810
850
|
tr(" 状态候选"),
|
|
811
851
|
),
|
|
812
852
|
);
|
|
@@ -814,7 +854,7 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
814
854
|
const links = text("div", "", "map-targets"),
|
|
815
855
|
ids = [
|
|
816
856
|
...new Set(
|
|
817
|
-
|
|
857
|
+
directoryInteractions
|
|
818
858
|
.filter((i) => i.pageKey === p.pageKey && i.targetPageKey)
|
|
819
859
|
.map((i) => i.targetPageKey),
|
|
820
860
|
),
|
|
@@ -839,9 +879,9 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
839
879
|
format: "indiana-local-preview-changes",
|
|
840
880
|
schemaVersion: 1,
|
|
841
881
|
sourceGraphDigest: m.release.sourceGraphDigest,
|
|
842
|
-
manifestDocumentHashes: m.
|
|
843
|
-
documentKey:
|
|
844
|
-
hash:
|
|
882
|
+
manifestDocumentHashes: m.routeTree.renders.map((row) => ({
|
|
883
|
+
documentKey: row.documentKey,
|
|
884
|
+
hash: row.documentHash,
|
|
845
885
|
})),
|
|
846
886
|
changes: [...changes.values()],
|
|
847
887
|
scope: "local_preview_only",
|
|
@@ -859,19 +899,7 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
859
899
|
toast(tr("导出本地 JSON;未保存到服务器或应用到商户。"));
|
|
860
900
|
});
|
|
861
901
|
renderNav();
|
|
862
|
-
|
|
863
|
-
const initialRequest = {};
|
|
864
|
-
pendingDocument = initialRequest;
|
|
865
|
-
const expandedBytes = m.documents.reduce((total, document) => total + (document.domPayload?.bytes ?? new TextEncoder().encode(JSON.stringify({html:document.html,nodes:document.nodes})).byteLength), 0);
|
|
866
|
-
if (!Number.isSafeInteger(expandedBytes) || expandedBytes > 33_554_432) throw Error("Expanded DOM aggregate byte budget exceeded");
|
|
867
|
-
// Sequential decoding bounds peak memory for a multi-route artifact.
|
|
868
|
-
const expandedDocuments = [];
|
|
869
|
-
for (const document of m.documents) expandedDocuments.push(await expandNodeHashes(document, initialRequest));
|
|
870
|
-
m.documents = expandedDocuments;
|
|
871
|
-
pendingDocument = null;
|
|
872
|
-
renderNav();
|
|
873
|
-
}
|
|
874
|
-
openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
902
|
+
await openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
875
903
|
}
|
|
876
904
|
(async () => {
|
|
877
905
|
const data = JSON.parse(document.getElementById("iam-data").textContent);
|
|
@@ -908,7 +936,7 @@ function expandDomNodes(document, body, deriveTextId) {
|
|
|
908
936
|
const model={format:"indiana-preview-material",schemaVersion:1,app:{name:catalog.appName},release:{sourceGraphDigest:catalog.sourceGraphDigest},pages:catalog.routes.map(route=>({pageKey:route.routeKey,route:route.path,title:route.path,documentKeys:[route.documentKey],stateKeys:[],componentKeys:[]})),documents:[],elements:[],components:[],interactions:[],styles:[],scripts:[],assets:[],coverage:{gaps:[]},routeTree:{schemaVersion:catalog.renderIndexVersion,routes:catalog.routes,renders:catalog.renders,variables:catalog.graph.variables.map(variable=>({variableKey:variable.id,routeKey:variable.routeId,renderKey:variable.renderKey})),states:[],transitions:[],bindings:[],targets:[],navigation:[]}};
|
|
909
937
|
await mountIamViewer(model,context);loading.hidden=true;document.querySelector(".workspace").hidden=false;return;
|
|
910
938
|
}
|
|
911
|
-
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength !== context.bytes
|
|
939
|
+
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength !== context.bytes) throw new Error("Invalid IAM bytes");
|
|
912
940
|
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", message.bytes))].map(b => b.toString(16).padStart(2,"0")).join("");
|
|
913
941
|
if (digest !== context.manifestDigest) throw new Error("IAM digest mismatch");
|
|
914
942
|
const manifest = JSON.parse(new TextDecoder("utf-8",{fatal:true}).decode(message.bytes));
|
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": {
|
|
@@ -49,10 +53,16 @@
|
|
|
49
53
|
"jsdom": "26.1.0",
|
|
50
54
|
"lucide-static": "1.34.0",
|
|
51
55
|
"magic-string": "0.30.21",
|
|
56
|
+
"mdast-util-from-markdown": "2.1.0",
|
|
57
|
+
"mdast-util-gfm": "3.1.0",
|
|
58
|
+
"mdast-util-mdx": "3.0.0",
|
|
59
|
+
"micromark-extension-gfm": "3.0.0",
|
|
60
|
+
"micromark-extension-mdxjs": "3.0.0",
|
|
52
61
|
"pako": "3.0.2",
|
|
53
62
|
"sharp": "0.35.5",
|
|
54
63
|
"tailwind-merge": "3.7.0",
|
|
55
64
|
"tailwindcss": "4.3.3",
|
|
56
|
-
"typescript": "5.9.3"
|
|
65
|
+
"typescript": "5.9.3",
|
|
66
|
+
"yaml": "2.9.1"
|
|
57
67
|
}
|
|
58
68
|
}
|