@tinkerlandai/indiana-iam 0.2.0 → 0.3.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 +74 -5
- package/assets/viewer.js +55 -2
- package/package.json +2 -1
- package/src/analyze.mjs +106 -22
- package/src/anchors.mjs +5 -0
- package/src/build-options.d.ts +5 -0
- package/src/build.mjs +10 -4
- package/src/cli.mjs +10 -4
- package/src/dom-payload-browser.mjs +28 -0
- package/src/dom-payload-json.mjs +12 -0
- package/src/dom-payload.mjs +50 -0
- package/src/framework-routes.mjs +41 -88
- package/src/generate.mjs +17 -6
- package/src/index.d.ts +11 -1
- package/src/index.mjs +1 -1
- package/src/integrity.mjs +4 -2
- package/src/library-entry.mjs +1 -0
- package/src/next.mjs +4 -3
- package/src/node-hashes.mjs +5 -2
- package/src/render-index.mjs +6 -4
- package/src/routing.mjs +1 -1
- package/src/schema-definition.mjs +8 -1
- package/src/source-entry.mjs +50 -0
- package/src/source-resolution.mjs +268 -0
- package/src/tanstack-routes.mjs +143 -0
- package/src/transform.mjs +2 -2
- package/src/upload.mjs +28 -6
- package/src/validate.mjs +14 -4
- package/src/vite.d.ts +4 -1
- package/src/vite.mjs +25 -10
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ not import merchant modules to execute them. Source enumeration and optional Git
|
|
|
47
47
|
the package starts no external commands and works without a Git executable. Developers' explicitly configured
|
|
48
48
|
framework callbacks remain part of their normal build.
|
|
49
49
|
|
|
50
|
-
## Vite and
|
|
50
|
+
## Vite, React Router and TanStack Router
|
|
51
51
|
|
|
52
52
|
```js
|
|
53
53
|
// vite.config.mjs
|
|
@@ -56,12 +56,20 @@ import { indianaIam } from "@tinkerlandai/indiana-iam/vite";
|
|
|
56
56
|
export default { plugins: [indianaIam()] };
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Use an ESM Vite config (`vite.config.mjs` or `vite.config.mts`, or a project with `"type": "module"`).
|
|
60
|
+
CommonJS configs cannot `require` this ESM package.
|
|
61
|
+
|
|
59
62
|
Client builds expose `import.meta.env.INDIANA_IAM_RELEASE_URL` and write the descriptor into the build output folder.
|
|
60
63
|
Server builds are skipped.
|
|
61
64
|
|
|
65
|
+
Browser apps use the single local module script in `index.html` to select reachable router and React root declarations.
|
|
66
|
+
For multiple entry points, set `indianaIam({ entry: "src/main.tsx" })`, or pass `--entry src/main.tsx` to standalone generation.
|
|
67
|
+
The path must stay inside the scanned root. Next.js uses its App/Pages root instead.
|
|
68
|
+
|
|
62
69
|
## Options
|
|
63
70
|
|
|
64
|
-
Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload }`;
|
|
71
|
+
Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload, client }`;
|
|
72
|
+
browser applications additionally accept `entry`;
|
|
65
73
|
`withIndianaIam` also takes `root`. By default a source the scanner cannot read prints a warning and the build continues without an IAM;
|
|
66
74
|
`strict: true` fails the build instead. The same source always produces the same release ID and IAM bytes.
|
|
67
75
|
If source changes during compilation, generation fails instead of publishing a descriptor for different code.
|
|
@@ -99,7 +107,20 @@ optional command for explicit source annotations. Next and Vite do not need that
|
|
|
99
107
|
|
|
100
108
|
## Scope
|
|
101
109
|
|
|
102
|
-
Supported source: Next.js App Router and Pages Router,
|
|
110
|
+
Supported source: Next.js App Router and Pages Router, React Router / single-root React apps built with Vite,
|
|
111
|
+
and statically generated TanStack file-route trees registered with `createRouter({ routeTree })`.
|
|
112
|
+
React Router supports imported static path constants, inline `Component` functions and source-proven lazy import forwarding.
|
|
113
|
+
TanStack reads the registered generated tree, including pathless layouts, index routes and simple `$param` paths;
|
|
114
|
+
custom history/basepath and complex parameter patterns require separate adapters. Loaders, guards, retry/reload behavior
|
|
115
|
+
and runtime data remain unobserved. Unsupported optional React paths are isolated as gaps.
|
|
116
|
+
The complete validated IAM must fit within 8 MiB; oversized projections fail without emitting a truncated artifact.
|
|
117
|
+
Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
|
|
118
|
+
and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
|
|
119
|
+
HTML/DOM hashes and targets. The document hash pins its encoded storage body; decoding is local and bounded to
|
|
120
|
+
8 MiB per payload and 32 MiB in aggregate. The final IAM file limit stays 8 MiB. Exported page HTML is expanded.
|
|
121
|
+
Use the matching 0.3 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
|
|
122
|
+
before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
|
|
123
|
+
viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
|
|
103
124
|
The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
|
|
104
125
|
step calls Indiana, and only when a key is set.
|
|
105
126
|
Values that look like credentials, tokens, emails or private URLs are removed, but review what your pages contain.
|
|
@@ -108,13 +129,61 @@ Values that look like credentials, tokens, emails or private URLs are removed, b
|
|
|
108
129
|
|
|
109
130
|
Create a Key with only the `app-manifests:write` permission (Console → developer settings) and set it as
|
|
110
131
|
`INDIANA_IAM_KEY` in your build environment. Each production build then registers the IAM, stores it through a
|
|
111
|
-
server-signed URL and asks Indiana to validate it; the validated version becomes
|
|
132
|
+
server-signed URL and asks Indiana to validate it; the validated version becomes its client's current version.
|
|
112
133
|
Rebuilding unchanged source only makes the existing version current again.
|
|
113
134
|
|
|
135
|
+
An App environment can hold several clients, each a named build with its own current version: for example `web` and
|
|
136
|
+
`web-dashboard`, or later `ios` and `android` apps. Set the `client` option (or `INDIANA_IAM_CLIENT`) to 1-40 lowercase
|
|
137
|
+
letters, digits or hyphens starting with a letter; the default is `web`. A client keeps the platform of its first
|
|
138
|
+
upload, and the same IAM bytes cannot be uploaded for two clients.
|
|
139
|
+
|
|
114
140
|
- Uploads go to `https://tinker.cash`; set `INDIANA_IAM_BASE_URL` (or the `baseUrl` option) for another Indiana
|
|
115
141
|
environment.
|
|
116
142
|
- On Vercel only production deployments upload by default; `upload: "always"` uploads every build and
|
|
117
143
|
`upload: false` never uploads.
|
|
118
144
|
- A failed upload prints a warning and the build continues, unless `strict: true`.
|
|
119
145
|
- The App's Owner or Admin must turn on the App page map first.
|
|
120
|
-
- `INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest
|
|
146
|
+
- `INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest> [--client <key>]` uploads a file generated elsewhere.
|
|
147
|
+
|
|
148
|
+
## App map annotations
|
|
149
|
+
|
|
150
|
+
The Console App map groups pages into areas, shows recorded navigation, and opens a main-path workbench before
|
|
151
|
+
opening a page editor. Thumbnails are static source projections. They do not execute authentication, data loaders,
|
|
152
|
+
form submissions or application code.
|
|
153
|
+
|
|
154
|
+
Optional static attributes preserve product intent that cannot be established from a URL alone:
|
|
155
|
+
|
|
156
|
+
| Attribute | Location | Meaning |
|
|
157
|
+
| -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `data-indiana-map-area` | Page root | Functional area label; otherwise the first URL segment groups the directory. |
|
|
159
|
+
| `data-indiana-map-flow` | Page root | Display name for the process containing this page. |
|
|
160
|
+
| `data-indiana-map-kind="login"` | Page root | Declares an authentication page. |
|
|
161
|
+
| `data-indiana-map-auth="public\|required"` | Page root | Declares the page's access requirement. Omission remains unknown. |
|
|
162
|
+
| `data-indiana-map-edge="step\|tab\|login\|return"` | An existing link or supported UI control | Classifies an existing recorded action. It never invents a destination or makes an unsupported action executable. |
|
|
163
|
+
|
|
164
|
+
Use one consistent declaration on the outer page element. Native `role="tab"` also classifies a Tab control.
|
|
165
|
+
A link Tab retains its exact destination URL, including query and fragment; a supported state-writing Tab remains
|
|
166
|
+
an in-page action. Login and return links remain separate from ordinary steps. Unsupported actions appear as
|
|
167
|
+
unknown outcomes. Authentication requirements are display declarations, not proof of a session or runtime access.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<main
|
|
171
|
+
data-indiana-map-area="Workspace"
|
|
172
|
+
data-indiana-map-flow="Create project"
|
|
173
|
+
data-indiana-map-auth="required"
|
|
174
|
+
>
|
|
175
|
+
<a href="/login?next=%2Fprojects" data-indiana-map-edge="login">
|
|
176
|
+
Sign in
|
|
177
|
+
</a>
|
|
178
|
+
<a href="/projects/configure" data-indiana-map-edge="step">
|
|
179
|
+
Continue
|
|
180
|
+
</a>
|
|
181
|
+
<a href="/projects?tab=activity" role="tab">
|
|
182
|
+
Activity
|
|
183
|
+
</a>
|
|
184
|
+
</main>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
An unambiguous source path can continue automatically until a branch or cycle. Explicit step links take precedence
|
|
188
|
+
over ordinary navigation; login/return links and Tabs are inspected separately. A new IAM build is needed to add
|
|
189
|
+
these annotations to an already uploaded version. Existing immutable versions are never rewritten.
|
package/assets/viewer.js
CHANGED
|
@@ -489,6 +489,44 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
489
489
|
}
|
|
490
490
|
// Verify the compact source document first, then derive its exact v1 hashes.
|
|
491
491
|
// Work is page-local and cancellable; hashes never become another saved authority.
|
|
492
|
+
/* BEGIN COMPRESSED DOM */
|
|
493
|
+
function checkedDomPayload(document) {
|
|
494
|
+
const payload = document.domPayload;
|
|
495
|
+
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 || payload.bytes > 8_388_608 || typeof payload.data !== "string" || !payload.data.length || payload.data.length > 11_184_812 || payload.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(payload.data)) throw Error("Invalid compressed DOM payload");
|
|
497
|
+
return payload;
|
|
498
|
+
}
|
|
499
|
+
function parsedDomPayload(text) {
|
|
500
|
+
const value = JSON.parse(text);
|
|
501
|
+
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" || value.html.length > 2_000_000 || !value.html.isWellFormed() || !Array.isArray(value.nodes) || value.nodes.length > 12000 || JSON.stringify(value) !== text) throw Error("Invalid expanded DOM payload");
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
async function documentDomBrowser(document) {
|
|
505
|
+
const payload = checkedDomPayload(document);
|
|
506
|
+
if (!payload) return { html: document.html, nodes: document.nodes };
|
|
507
|
+
if (typeof DecompressionStream !== "function") throw Error("Compressed IAM requires DecompressionStream support");
|
|
508
|
+
const binary = atob(payload.data);
|
|
509
|
+
if (btoa(binary) !== payload.data) throw Error("Noncanonical compressed DOM base64");
|
|
510
|
+
const compressed = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
511
|
+
const stream = new ReadableStream({ start(controller) { controller.enqueue(compressed); controller.close(); } }).pipeThrough(new DecompressionStream("gzip"));
|
|
512
|
+
const reader = stream.getReader(), chunks = [];
|
|
513
|
+
let total = 0;
|
|
514
|
+
try {
|
|
515
|
+
for (;;) {
|
|
516
|
+
const next = await reader.read();
|
|
517
|
+
if (next.done) break;
|
|
518
|
+
total += next.value.byteLength;
|
|
519
|
+
if (total > payload.bytes) throw Error("Expanded DOM byte budget exceeded");
|
|
520
|
+
chunks.push(next.value);
|
|
521
|
+
}
|
|
522
|
+
} finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
523
|
+
if (total !== payload.bytes) throw Error("Expanded DOM byte length mismatch");
|
|
524
|
+
const decoded = new Uint8Array(total);
|
|
525
|
+
let offset = 0;
|
|
526
|
+
for (const chunk of chunks) { decoded.set(chunk, offset); offset += chunk.byteLength; }
|
|
527
|
+
return parsedDomPayload(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
|
|
528
|
+
}
|
|
529
|
+
/* END COMPRESSED DOM */
|
|
492
530
|
/* BEGIN HTML DOM DERIVATION */
|
|
493
531
|
function expandDomNodes(document, body) {
|
|
494
532
|
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash||document.nodes.length>12000)throw Error('Invalid HTML-derived DOM mode');
|
|
@@ -507,6 +545,14 @@ function expandDomNodes(document, body) {
|
|
|
507
545
|
}
|
|
508
546
|
/* END HTML DOM DERIVATION */
|
|
509
547
|
async function expandNodeHashes(document,request) {
|
|
548
|
+
if (document.domPayload) {
|
|
549
|
+
const {hashes, ...body} = document;
|
|
550
|
+
if (await hash("iam/document/v1\0" + canonical(body)) !== hashes.document) throw Error("Compressed document hash mismatch");
|
|
551
|
+
const decoded = await documentDomBrowser(document);
|
|
552
|
+
if (await hash(decoded.html) !== hashes.html || await hash("iam/dom-map/v1\0" + canonical({rootNodeId: document.rootNodeId, nodes: decoded.nodes, sourceMappingsHash: document.sourceMappingsHash})) !== hashes.dom) throw Error("Expanded DOM hash mismatch");
|
|
553
|
+
const {domPayload, ...plain} = document;
|
|
554
|
+
document = {...plain, ...decoded};
|
|
555
|
+
}
|
|
510
556
|
if(!Array.isArray(document.nodes)||document.nodes.length>12000)throw Error('Node hash budget');
|
|
511
557
|
const nodes=expandDomNodes(document,new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body),byId=new Map(nodes.map(node=>[node.nodeId,node]));
|
|
512
558
|
if(byId.size!==nodes.length)throw Error('Duplicate node');
|
|
@@ -601,7 +647,8 @@ function expandDomNodes(document, body) {
|
|
|
601
647
|
if (data.type === "open-page" && typeof data.routeKey === "string") {
|
|
602
648
|
const next = m.pages.find(p => p.pageKey === data.routeKey);
|
|
603
649
|
if (!next) return;
|
|
604
|
-
|
|
650
|
+
if(typeof data.renderKey==="string"&&!m.routeTree.renders.some(r=>r.routeKey===next.pageKey&&r.renderKey===data.renderKey))return;
|
|
651
|
+
const variableRender=typeof data.renderKey==="string"?data.renderKey:m.routeTree.variables.find(variable=>variable.variableKey===data.variableKey)?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===data.variableKey)?.renderKey;
|
|
605
652
|
const targetUrl=typeof data.url==="string"?data.url:(next.pageKey===page.pageKey?currentUrl:next.route);
|
|
606
653
|
if (!doc || next.pageKey !== page.pageKey || variableRender && variableRender!==render?.renderKey || targetUrl!==currentUrl) openPage(next,targetUrl,variableRender);
|
|
607
654
|
else consoleContext?.resume?.();
|
|
@@ -801,8 +848,14 @@ function expandDomNodes(document, body) {
|
|
|
801
848
|
if (m.documents.length) {
|
|
802
849
|
const initialRequest = {};
|
|
803
850
|
pendingDocument = initialRequest;
|
|
804
|
-
|
|
851
|
+
const expandedBytes = m.documents.reduce((total, document) => total + (document.domPayload?.bytes ?? new TextEncoder().encode(JSON.stringify({html:document.html,nodes:document.nodes})).byteLength), 0);
|
|
852
|
+
if (!Number.isSafeInteger(expandedBytes) || expandedBytes > 33_554_432) throw Error("Expanded DOM aggregate byte budget exceeded");
|
|
853
|
+
// Sequential decoding bounds peak memory for a multi-route artifact.
|
|
854
|
+
const expandedDocuments = [];
|
|
855
|
+
for (const document of m.documents) expandedDocuments.push(await expandNodeHashes(document, initialRequest));
|
|
856
|
+
m.documents = expandedDocuments;
|
|
805
857
|
pendingDocument = null;
|
|
858
|
+
renderNav();
|
|
806
859
|
}
|
|
807
860
|
openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
808
861
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinkerlandai/indiana-iam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Build-time Indiana App Manifest (IAM) generator for Next.js and Vite apps: scans source after the production build and writes the IAM and its release descriptor.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"css-tree": "3.2.1",
|
|
48
48
|
"jsdom": "26.1.0",
|
|
49
49
|
"magic-string": "0.30.21",
|
|
50
|
+
"pako": "3.0.2",
|
|
50
51
|
"tailwind-merge": "3.7.0",
|
|
51
52
|
"tailwindcss": "4.3.3",
|
|
52
53
|
"typescript": "5.9.3"
|
package/src/analyze.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { automaticSourceAnchors, anchorAttributes } from "./anchors.mjs";
|
|
3
|
+
import { automaticSourceAnchors, anchorAttributes, sourceScriptKind } from "./anchors.mjs";
|
|
4
4
|
import { sourceFiles } from "./source-files.mjs";
|
|
5
5
|
import { dependency } from "./dependencies.mjs";
|
|
6
6
|
import { assembleRouteTree, readVariables } from "./route-tree.mjs";
|
|
7
|
+
import { sourceEntry } from "./source-entry.mjs";
|
|
7
8
|
import { frameworkRoutes, externalImport } from "./framework-routes.mjs";
|
|
9
|
+
import { sourceResolution } from "./source-resolution.mjs";
|
|
8
10
|
import { routeIndexDigest, refreshRenderHashes } from "./render-index.mjs";
|
|
9
11
|
import { storeDocumentNodes } from "./node-hashes.mjs";
|
|
10
12
|
import { staticQuery } from "./static-query.mjs";
|
|
@@ -140,7 +142,8 @@ function functionFrom(node) {
|
|
|
140
142
|
if (
|
|
141
143
|
ts.isFunctionDeclaration(node) ||
|
|
142
144
|
ts.isArrowFunction(node) ||
|
|
143
|
-
ts.isFunctionExpression(node)
|
|
145
|
+
ts.isFunctionExpression(node) ||
|
|
146
|
+
ts.isMethodDeclaration(node)
|
|
144
147
|
)
|
|
145
148
|
return node;
|
|
146
149
|
if (ts.isCallExpression(node))
|
|
@@ -156,6 +159,7 @@ export async function analyze({
|
|
|
156
159
|
maxFiles = 5000,
|
|
157
160
|
maxBytes = 100_000_000,
|
|
158
161
|
navigationPaths = [],
|
|
162
|
+
entry,
|
|
159
163
|
automaticAnchors = false,
|
|
160
164
|
}) {
|
|
161
165
|
root = await fs.realpath(root);
|
|
@@ -213,7 +217,7 @@ export async function analyze({
|
|
|
213
217
|
text,
|
|
214
218
|
ts.ScriptTarget.Latest,
|
|
215
219
|
true,
|
|
216
|
-
|
|
220
|
+
sourceScriptKind(file, ts),
|
|
217
221
|
);
|
|
218
222
|
const module = {
|
|
219
223
|
file,
|
|
@@ -250,6 +254,7 @@ export async function analyze({
|
|
|
250
254
|
}
|
|
251
255
|
if (ts.isFunctionDeclaration(statement) && (statement.name||statement.modifiers?.some(m=>m.kind===ts.SyntaxKind.DefaultKeyword)))
|
|
252
256
|
module.bindings.set(statement.name?.text||'$default', statement);
|
|
257
|
+
if (ts.isEnumDeclaration(statement)) module.bindings.set(statement.name.text, statement);
|
|
253
258
|
if (ts.isVariableStatement(statement))
|
|
254
259
|
for (const d of statement.declarationList.declarations)
|
|
255
260
|
if (ts.isIdentifier(d.name) && d.initializer)
|
|
@@ -348,10 +353,9 @@ export async function analyze({
|
|
|
348
353
|
return binding(target,key,seen);
|
|
349
354
|
}
|
|
350
355
|
const componentDefs = new Map();
|
|
351
|
-
|
|
352
|
-
for (const [symbol, node] of module.bindings) {
|
|
356
|
+
function registerComponent(module, symbol, node) {
|
|
353
357
|
const fn = functionFrom(node);
|
|
354
|
-
if (!fn)
|
|
358
|
+
if (!fn) return null;
|
|
355
359
|
let hasJsx = false;
|
|
356
360
|
function visit(n) {
|
|
357
361
|
if (
|
|
@@ -363,7 +367,7 @@ export async function analyze({
|
|
|
363
367
|
ts.forEachChild(n, visit);
|
|
364
368
|
}
|
|
365
369
|
visit(fn);
|
|
366
|
-
if (!hasJsx)
|
|
370
|
+
if (!hasJsx) return null;
|
|
367
371
|
const key = identity("component", { file: module.file, symbol }),
|
|
368
372
|
ordinals = new Map();
|
|
369
373
|
let ordinal = 0;
|
|
@@ -403,12 +407,21 @@ export async function analyze({
|
|
|
403
407
|
source: loc,
|
|
404
408
|
dependencies: [],
|
|
405
409
|
});
|
|
406
|
-
|
|
407
|
-
|
|
410
|
+
return def;
|
|
411
|
+
}
|
|
412
|
+
for (const module of modules.values())
|
|
413
|
+
for (const [symbol, node] of module.bindings) registerComponent(module, symbol, node);
|
|
414
|
+
const resolution = sourceResolution({ ts, resolveModule, resolveBinding: binding });
|
|
415
|
+
const componentBinding = (module, name) => {
|
|
416
|
+
const ref = resolution.component(module, name);
|
|
417
|
+
if (ref?.wrapped) gap("LAZY_WRAPPER_UNOBSERVED", module.file, name + ": only the statically forwarded successful module is projected; retry/reload behavior is not executed");
|
|
418
|
+
return ref;
|
|
419
|
+
};
|
|
420
|
+
function collectComponentDependencies(def) {
|
|
408
421
|
function collectDependencies(node) {
|
|
409
422
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
410
423
|
const opening = ts.isJsxElement(node) ? node.openingElement : node,
|
|
411
|
-
ref =
|
|
424
|
+
ref = componentBinding(def.module, opening.tagName.getText(def.module.source)),
|
|
412
425
|
target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
|
|
413
426
|
if (
|
|
414
427
|
target &&
|
|
@@ -420,6 +433,20 @@ export async function analyze({
|
|
|
420
433
|
}
|
|
421
434
|
collectDependencies(def.fn);
|
|
422
435
|
}
|
|
436
|
+
for (const def of componentDefs.values()) collectComponentDependencies(def);
|
|
437
|
+
const inlineNodes = new WeakMap(), inlineCounts = new Map();
|
|
438
|
+
function inlineComponent(module, node, route) {
|
|
439
|
+
if (inlineNodes.has(node)) return inlineNodes.get(node);
|
|
440
|
+
const group = module.file + "#" + route, occurrence = inlineCounts.get(group) ?? 0;
|
|
441
|
+
inlineCounts.set(group, occurrence + 1);
|
|
442
|
+
const symbol = "$route_" + hashObject("iam/inline-route-component/v1", { route, occurrence }).slice(0, 24);
|
|
443
|
+
module.bindings.set(symbol, node);
|
|
444
|
+
const def = registerComponent(module, symbol, node);
|
|
445
|
+
if (def) collectComponentDependencies(def);
|
|
446
|
+
const target = { file: module.file, symbol };
|
|
447
|
+
inlineNodes.set(node, target);
|
|
448
|
+
return target;
|
|
449
|
+
}
|
|
423
450
|
function sourceLocation(module, node, symbol) {
|
|
424
451
|
const loc = module.source.getLineAndCharacterOfPosition(
|
|
425
452
|
node.getStart(module.source),
|
|
@@ -707,11 +734,16 @@ export async function analyze({
|
|
|
707
734
|
Object.hasOwn(Object(value), "stateInitial") &&
|
|
708
735
|
d.name.elements[0]
|
|
709
736
|
) {
|
|
710
|
-
const
|
|
737
|
+
const stateBinding = d.name.elements[0];
|
|
738
|
+
if (!ts.isBindingElement(stateBinding) || stateBinding.dotDotDotToken || !ts.isIdentifier(stateBinding.name)) {
|
|
739
|
+
gap("STATE_BINDING_UNSUPPORTED", def.module.file, "Omitted, nested or rest state bindings have no named scalar value for UI simulation");
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const key = stateBinding.name.text;
|
|
711
743
|
ctx[key] = sensitiveName(key) ? UNKNOWN : value.stateInitial;
|
|
712
744
|
ctx.__iamStates.add(key);
|
|
713
745
|
ctx.__iamStateNodes[key] = d;
|
|
714
|
-
if (d.name.elements[1]?.name)
|
|
746
|
+
if (d.name.elements[1]?.name && !d.name.elements[1].dotDotDotToken && ts.isIdentifier(d.name.elements[1].name))
|
|
715
747
|
ctx.__iamSetters[d.name.elements[1].name.text] = key;
|
|
716
748
|
}
|
|
717
749
|
}
|
|
@@ -768,7 +800,10 @@ export async function analyze({
|
|
|
768
800
|
!/^\/(?:[^/?#[\]\\\s]+\/)*[^/?#[\]\\\s]+$/.test(routing.basePath)
|
|
769
801
|
)
|
|
770
802
|
throw new Error("Invalid source basePath");
|
|
771
|
-
const
|
|
803
|
+
const selection = await sourceEntry({ root, modules, resolveModule, ts, entry });
|
|
804
|
+
if (selection.htmlSource) inventory.push(selection.htmlSource);
|
|
805
|
+
inventory.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
|
|
806
|
+
const compositions = frameworkRoutes({ modules, ...selection, resolveModule, ts, sourceLocation, navigationPaths, gap, resolution, inlineComponent });
|
|
772
807
|
const pageFiles = compositions.pageFiles;
|
|
773
808
|
routing.framework = compositions.framework;
|
|
774
809
|
if (compositions.routeSources) routing.routeSources = compositions.routeSources;
|
|
@@ -927,6 +962,20 @@ export async function analyze({
|
|
|
927
962
|
const value = evalExpression(node, owner.module, ctx);
|
|
928
963
|
return ["string", "boolean", "number"].includes(typeof value) ? { op: "const", value } : null;
|
|
929
964
|
}
|
|
965
|
+
function uiExpressionType(expression, payload = {}) {
|
|
966
|
+
if (!expression) return undefined;
|
|
967
|
+
if (expression.op === "const") return typeof expression.value;
|
|
968
|
+
if (expression.op === "var") return ui.variables.find(variable => variable.variableKey === expression.key)?.valueType;
|
|
969
|
+
if (expression.op === "event") return payload[expression.key];
|
|
970
|
+
const type = child => uiExpressionType(child, payload);
|
|
971
|
+
if (expression.op === "not") return type(expression.arg) ? "boolean" : undefined;
|
|
972
|
+
if (expression.op === "classes") return expression.parts.every(part => type(part.when) === "boolean") ? "string" : undefined;
|
|
973
|
+
if (["all", "any"].includes(expression.op)) return expression.args.every(child => type(child) === "boolean") ? "boolean" : undefined;
|
|
974
|
+
const left = type(expression.left), right = type(expression.right);
|
|
975
|
+
if (["eq", "ne"].includes(expression.op)) return left && left === right ? "boolean" : undefined;
|
|
976
|
+
if (["add", "subtract"].includes(expression.op)) return left === "number" && right === "number" ? "number" : undefined;
|
|
977
|
+
return undefined;
|
|
978
|
+
}
|
|
930
979
|
function uiHandler(initializer, owner, ctx) {
|
|
931
980
|
let fn = initializer && ts.isJsxExpression(initializer) ? unwrap(initializer.expression) : null;
|
|
932
981
|
if (fn && ts.isIdentifier(fn) && ctx.__iamPropHandlers.has(fn.text)) return ctx.__iamPropHandlers.get(fn.text);
|
|
@@ -948,6 +997,7 @@ export async function analyze({
|
|
|
948
997
|
const payload = {};
|
|
949
998
|
function eventInputs(e) { if (e.op === "event") payload[e.key] = e.key === "checked" ? "boolean" : "string"; for (const child of [e.arg, e.left, e.right, ...(e.args || []),...(e.parts||[]).map(part=>part.when)]) if (child) eventInputs(child); }
|
|
950
999
|
eventInputs(value);
|
|
1000
|
+
if (uiExpressionType(value, payload) !== ui.variables.find(variable => variable.variableKey === variableKey)?.valueType) return null;
|
|
951
1001
|
return { variableKey, value, payload };
|
|
952
1002
|
}
|
|
953
1003
|
function unobservedValue(node, owner, instance) {
|
|
@@ -1250,7 +1300,7 @@ export async function analyze({
|
|
|
1250
1300
|
: props.children?.iamChildren || "";
|
|
1251
1301
|
if (ctx[tag]?.iamChildren !== undefined) return ctx[tag].iamChildren;
|
|
1252
1302
|
const routerPrimitive = externalImport(m, tag);
|
|
1253
|
-
if (["react-router", "react-router-dom"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamChildren ?? "";
|
|
1303
|
+
if (["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamChildren ?? "";
|
|
1254
1304
|
if (
|
|
1255
1305
|
/^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
|
|
1256
1306
|
tag,
|
|
@@ -1265,7 +1315,7 @@ export async function analyze({
|
|
|
1265
1315
|
props["data-iam-adapter"] = tag;
|
|
1266
1316
|
}
|
|
1267
1317
|
if (/^[A-Z]/.test(tag) && !intrinsicAlias) {
|
|
1268
|
-
if (tag === "Link" || ["react-router", "react-router-dom"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
|
|
1318
|
+
if (tag === "Link" || ["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
|
|
1269
1319
|
htmlTag = "a";
|
|
1270
1320
|
if (typeof props.to === "string") props.href = props.to;
|
|
1271
1321
|
}
|
|
@@ -1284,7 +1334,7 @@ export async function analyze({
|
|
|
1284
1334
|
.filter((v) => typeof v === "string")
|
|
1285
1335
|
.join(" ");
|
|
1286
1336
|
} else {
|
|
1287
|
-
const ref =
|
|
1337
|
+
const ref = componentBinding(m, tag),
|
|
1288
1338
|
target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
|
|
1289
1339
|
if (target) {
|
|
1290
1340
|
const own = components.get(owner.key);
|
|
@@ -1357,6 +1407,11 @@ export async function analyze({
|
|
|
1357
1407
|
}
|
|
1358
1408
|
for (const binding of uiBindings.filter(b => ["value", "checked", "disabled", "className"].includes(b.property))) {
|
|
1359
1409
|
if(['value','checked'].includes(binding.property)&&!['input','select','textarea'].includes(htmlTag)){gap('UI_BINDING_TARGET_UNSUPPORTED',m.file,tag+': input property cannot be simulated on a non-input adapter');continue;}
|
|
1410
|
+
const expectedType = ["checked", "disabled"].includes(binding.property) ? "boolean" : "string";
|
|
1411
|
+
if (uiExpressionType(binding.value) !== expectedType) {
|
|
1412
|
+
gap("UI_BINDING_TYPE_UNSUPPORTED", m.file, tag + "." + binding.property + ": source value cannot be represented by the typed UI simulation");
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1360
1415
|
ui.bindings.push({ bindingKey: identity("ui-binding", { node: nodeId, property: binding.property }), routeKey: pageKey, nodeId, elementKey, ...binding });
|
|
1361
1416
|
}
|
|
1362
1417
|
const sensitiveInput = htmlTag === "input" && (["password", "hidden"].includes(String(props.type).toLowerCase()) || sensitiveName(String(props.name || props.id || "")));
|
|
@@ -1394,7 +1449,7 @@ export async function analyze({
|
|
|
1394
1449
|
clipRule: "clip-rule",
|
|
1395
1450
|
}[key] || key;
|
|
1396
1451
|
if (
|
|
1397
|
-
!/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|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-testid|data-test|data-qa|data-iam-(?:adapter|toggle))$/.test(
|
|
1452
|
+
!/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|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-testid|data-test|data-qa|data-iam-(?:adapter|toggle))$/.test(
|
|
1398
1453
|
attr,
|
|
1399
1454
|
)
|
|
1400
1455
|
)
|
|
@@ -1436,16 +1491,24 @@ export async function analyze({
|
|
|
1436
1491
|
);
|
|
1437
1492
|
}
|
|
1438
1493
|
function renderEntry(file, props, instance, symbol) {
|
|
1439
|
-
const entryModule = modules.get(file), entryRef = entryModule &&
|
|
1494
|
+
const entryModule = modules.get(file), entryRef = entryModule && componentBinding(entryModule, symbol ?? entryModule.defaultName);
|
|
1440
1495
|
const entryDef = entryRef && componentDefs.get(entryRef.module.file + "#" + entryRef.name);
|
|
1441
|
-
if (!entryDef) {
|
|
1496
|
+
if (!entryDef) {
|
|
1497
|
+
if (entryRef) {
|
|
1498
|
+
const key = identity("component", { file: entryRef.module.file, symbol: entryRef.name });
|
|
1499
|
+
if (!components.has(key)) components.set(key, { componentKey: key, file: entryRef.module.file, name: entryRef.name, source: sourceLocation(entryRef.module, entryRef.node, entryRef.name), dependencies: [] });
|
|
1500
|
+
}
|
|
1501
|
+
gap("PAGE_NO_JSX", file, "Composition entry has no inspectable JSX; null defaults are inert");
|
|
1502
|
+
return "";
|
|
1503
|
+
}
|
|
1442
1504
|
return renderComponent(entryDef, props, instance, 0);
|
|
1443
1505
|
}
|
|
1444
1506
|
function renderBranch(branch) {
|
|
1445
1507
|
let content = renderEntry(branch.file, {}, branch === composition.composition ? "page" : "slot-page:" + branch.file, branch.symbol);
|
|
1446
1508
|
for (const layer of branch.layers) {
|
|
1447
|
-
const props =
|
|
1448
|
-
|
|
1509
|
+
const props = ["react-router", "tanstack-router"].includes(routing.framework)
|
|
1510
|
+
? { __iamOutlet: { iamChildren: content } }
|
|
1511
|
+
: { children: { iamChildren: content } };
|
|
1449
1512
|
if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = { iamChildren: content }; props.pageProps = {}; }
|
|
1450
1513
|
for (const slot of layer.slots) {
|
|
1451
1514
|
props[slot.name] = { iamChildren: renderBranch(slot.branch) };
|
|
@@ -1481,6 +1544,27 @@ export async function analyze({
|
|
|
1481
1544
|
const dom = new JSDOM(
|
|
1482
1545
|
"<!doctype html><html><body>" + html + "</body></html>",
|
|
1483
1546
|
);
|
|
1547
|
+
// JSX children may be forwarded more than once, and HTML repair may clone
|
|
1548
|
+
// an invalid nested anchor. Each concrete occurrence needs a unique node;
|
|
1549
|
+
// ambiguous copies remain preview-only and have no simulated state bridge.
|
|
1550
|
+
const occurrences = new Map();
|
|
1551
|
+
for (const element of dom.window.document.querySelectorAll("[data-iam-node]")) {
|
|
1552
|
+
const id = element.getAttribute("data-iam-node");
|
|
1553
|
+
const matches = occurrences.get(id) ?? [];
|
|
1554
|
+
matches.push(element);
|
|
1555
|
+
occurrences.set(id, matches);
|
|
1556
|
+
}
|
|
1557
|
+
for (const [original, matches] of occurrences) {
|
|
1558
|
+
if (matches.length < 2) continue;
|
|
1559
|
+
const rec = record.get(original);
|
|
1560
|
+
if (!rec) throw Error("Unknown repeated source node");
|
|
1561
|
+
gap("REPEATED_SOURCE_INSTANCE", rec.source.file, "Repeated forwarded children or HTML repair: copies are preview-only without a state bridge");
|
|
1562
|
+
for (const [index, element] of matches.entries()) {
|
|
1563
|
+
const id = identity("node", { page: pageKey, original, occurrence: index });
|
|
1564
|
+
element.setAttribute("data-iam-node", id);
|
|
1565
|
+
record.set(id, { ...rec, synthetic: true });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1484
1568
|
const nodes = [];
|
|
1485
1569
|
function collect(node, parentId, index) {
|
|
1486
1570
|
if (node.nodeType !== 1 && node.nodeType !== 3) return null;
|
|
@@ -1706,7 +1790,7 @@ export async function analyze({
|
|
|
1706
1790
|
schemaVersion: 2,
|
|
1707
1791
|
producer: {
|
|
1708
1792
|
name: "indiana-app-manifest-local",
|
|
1709
|
-
version: "0.
|
|
1793
|
+
version: "0.19.0",
|
|
1710
1794
|
method: "typescript-ast-and-bounded-static-projection",
|
|
1711
1795
|
},
|
|
1712
1796
|
app: { name, platforms: ["web"] },
|
package/src/anchors.mjs
CHANGED
|
@@ -9,6 +9,11 @@ export const anchorAttributes = [
|
|
|
9
9
|
"data-qa",
|
|
10
10
|
"data-indiana-id",
|
|
11
11
|
];
|
|
12
|
+
export function sourceScriptKind(file, ts) {
|
|
13
|
+
return /\.[cm]?ts$/.test(file)
|
|
14
|
+
? ts.ScriptKind.TS
|
|
15
|
+
: /\.tsx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX;
|
|
16
|
+
}
|
|
12
17
|
export function instrumentableSource(file, text) {
|
|
13
18
|
return (
|
|
14
19
|
typeof text === "string" &&
|
package/src/build-options.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type IndianaIamBuildOptions = {
|
|
|
6
6
|
enabled?: boolean;
|
|
7
7
|
/** Reuse existing UI IDs and add build-only anchors for unambiguous native JSX (default: true). */
|
|
8
8
|
automaticAnchors?: boolean;
|
|
9
|
+
/** Browser entry relative to root for apps with multiple source roots. */
|
|
10
|
+
entry?: string;
|
|
9
11
|
/** App name recorded in the IAM; defaults to the root folder name. */
|
|
10
12
|
name?: string;
|
|
11
13
|
/** Fail the build when the IAM cannot be generated (default: warn and continue). */
|
|
@@ -17,6 +19,9 @@ export type IndianaIamBuildOptions = {
|
|
|
17
19
|
logger?: BuildLogger;
|
|
18
20
|
/** Key holding only app-manifests:write; defaults to `INDIANA_IAM_KEY`. Without one the IAM is not uploaded. */
|
|
19
21
|
apiKey?: string;
|
|
22
|
+
/** Client this build belongs to, such as `web` or `web-dashboard`; each client has its own current version.
|
|
23
|
+
* Defaults to `INDIANA_IAM_CLIENT` or `web`. */
|
|
24
|
+
client?: string;
|
|
20
25
|
/** Indiana origin; defaults to `INDIANA_IAM_BASE_URL` or https://tinker.cash. */
|
|
21
26
|
baseUrl?: string;
|
|
22
27
|
/** "production" (default) skips Vercel preview deployments; "always" uploads every build; false never uploads. */
|
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, automaticAnchors = true, strict = false, logger = console }) {
|
|
30
|
+
export async function prepareBuildRelease({ root, name, entry, automaticAnchors = true, 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, automaticAnchors });
|
|
33
|
+
const analysis = await analyzeSource({ root, name, entry, automaticAnchors });
|
|
34
34
|
const releaseId = deriveReleaseId(analysis);
|
|
35
35
|
process.env[STATE] = JSON.stringify({ root, releaseId, automaticAnchors });
|
|
36
36
|
return { releaseId };
|
|
@@ -59,6 +59,7 @@ export async function finishBuildRelease({
|
|
|
59
59
|
root,
|
|
60
60
|
releaseId,
|
|
61
61
|
name,
|
|
62
|
+
entry,
|
|
62
63
|
automaticAnchors = true,
|
|
63
64
|
cssDirectory,
|
|
64
65
|
mounts,
|
|
@@ -71,7 +72,7 @@ export async function finishBuildRelease({
|
|
|
71
72
|
}) {
|
|
72
73
|
root = await fs.realpath(root);
|
|
73
74
|
return guarded({ strict, logger }, async () => {
|
|
74
|
-
const analysis = await analyzeSource({ root, name, automaticAnchors });
|
|
75
|
+
const analysis = await analyzeSource({ root, name, entry, automaticAnchors });
|
|
75
76
|
if (deriveReleaseId(analysis) !== releaseId) throw new Error("Source changed during the build; rebuild before publishing IAM");
|
|
76
77
|
const css = cssDirectory ? await compiledCss(root, cssDirectory) : [];
|
|
77
78
|
const plain = () => generateAppManifest({ root, analysis, releaseId });
|
|
@@ -104,11 +105,16 @@ export async function finishBuildRelease({
|
|
|
104
105
|
serialized: result.serialized,
|
|
105
106
|
digest: result.digest,
|
|
106
107
|
label,
|
|
108
|
+
client: settings.client,
|
|
107
109
|
apiKey: settings.apiKey,
|
|
108
110
|
baseUrl: settings.baseUrl,
|
|
109
111
|
fetch: upload.fetch,
|
|
110
112
|
});
|
|
111
|
-
report(
|
|
113
|
+
report(
|
|
114
|
+
logger,
|
|
115
|
+
"log",
|
|
116
|
+
`IAM ${result.digest.slice(0, 12)} ${outcome.uploaded ? "uploaded" : "already uploaded"} as the current version of client ${settings.client} (${label})`,
|
|
117
|
+
);
|
|
112
118
|
return outcome.version;
|
|
113
119
|
}, "IAM upload");
|
|
114
120
|
else if (settings.missingKey && upload.upload !== false)
|
package/src/cli.mjs
CHANGED
|
@@ -10,9 +10,9 @@ 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> [--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> [--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
|
-
" | INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest> [--label <label> --base-url <url>]" +
|
|
15
|
+
" | INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest> [--client <key> --label <label> --base-url <url>]" +
|
|
16
16
|
" | indiana-iam prepare --root <app>" +
|
|
17
17
|
" | indiana-iam anchor --root <checkout> --out <new plan file> [--apply --primitive-map <file> --public-labels --forward-anchors]";
|
|
18
18
|
|
|
@@ -22,6 +22,7 @@ const { values, positionals } = parseArgs({
|
|
|
22
22
|
root: { type: "string" },
|
|
23
23
|
out: { type: "string" },
|
|
24
24
|
name: { type: "string" },
|
|
25
|
+
entry: { type: "string" },
|
|
25
26
|
repository: { type: "string" },
|
|
26
27
|
commit: { type: "string" },
|
|
27
28
|
file: { type: "string" },
|
|
@@ -39,6 +40,7 @@ const { values, positionals } = parseArgs({
|
|
|
39
40
|
"public-labels": { type: "boolean", default: false },
|
|
40
41
|
"forward-anchors": { type: "boolean", default: false },
|
|
41
42
|
label: { type: "string" },
|
|
43
|
+
client: { type: "string" },
|
|
42
44
|
"base-url": { type: "string" },
|
|
43
45
|
},
|
|
44
46
|
});
|
|
@@ -53,6 +55,7 @@ if (command === "generate") {
|
|
|
53
55
|
const result = await generateAppManifest({
|
|
54
56
|
root,
|
|
55
57
|
name: values.name,
|
|
58
|
+
entry: values.entry,
|
|
56
59
|
repository: values.repository || "",
|
|
57
60
|
commit: values.commit || "",
|
|
58
61
|
navigationPaths: (values["navigation-path"] || []).map((value) => JSON.parse(value)),
|
|
@@ -90,7 +93,7 @@ if (command === "generate") {
|
|
|
90
93
|
} else if (command === "upload") {
|
|
91
94
|
if (!values.file) throw new Error("upload requires --file");
|
|
92
95
|
// The key is read from INDIANA_IAM_KEY only, so it stays out of shell history.
|
|
93
|
-
const settings = uploadSettings({ baseUrl: values["base-url"], upload: "always" });
|
|
96
|
+
const settings = uploadSettings({ baseUrl: values["base-url"], upload: "always", client: values.client });
|
|
94
97
|
if (settings.missingKey) throw new Error("Set INDIANA_IAM_KEY to a key with only the app-manifests:write permission");
|
|
95
98
|
const bytes = await fs.readFile(values.file),
|
|
96
99
|
serialized = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
@@ -99,10 +102,13 @@ if (command === "generate") {
|
|
|
99
102
|
serialized,
|
|
100
103
|
digest: sha256(bytes),
|
|
101
104
|
label: values.label || buildLabel(path.dirname(path.resolve(values.file))),
|
|
105
|
+
client: settings.client,
|
|
102
106
|
apiKey: settings.apiKey,
|
|
103
107
|
baseUrl: settings.baseUrl,
|
|
104
108
|
});
|
|
105
|
-
console.log(
|
|
109
|
+
console.log(
|
|
110
|
+
JSON.stringify({ uploaded, version: { id: version.id, client: version.clientKey, state: version.state, isCurrent: version.isCurrent } }, null, 2),
|
|
111
|
+
);
|
|
106
112
|
} else if (command === "prepare") {
|
|
107
113
|
if (!values.root) throw new Error("prepare requires --root");
|
|
108
114
|
console.log(JSON.stringify(await prepareRelease(values.root)));
|