@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.
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { dependency } from "./dependencies.mjs";
4
+ import { sha256 } from "./integrity.mjs";
5
+
6
+ /** Select a browser import graph without evaluating the entry or its configuration. */
7
+ export async function sourceEntry({ root, modules, resolveModule, ts, entry }) {
8
+ let htmlSource;
9
+ if (entry === undefined) {
10
+ const file = path.join(root, "index.html");
11
+ let stat;
12
+ try { stat = await fs.lstat(file); }
13
+ catch (error) { if (error.code !== "ENOENT") throw error; }
14
+ if (stat) {
15
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256_000)
16
+ throw Error("Browser entry HTML must be a bounded local file");
17
+ const bytes = await fs.readFile(file), text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
18
+ const { JSDOM } = dependency("jsdom").value;
19
+ const dom = new JSDOM(text);
20
+ try {
21
+ const scripts = [...dom.window.document.querySelectorAll('script[type="module"]')];
22
+ if (scripts.length !== 1 || !scripts[0].hasAttribute("src") || scripts[0].textContent.trim() || dom.window.document.querySelector("base[href]"))
23
+ throw Error("Ambiguous browser entry: set entry to one local source module");
24
+ entry = scripts[0].getAttribute("src").replace(/^\/(?!\/)/, "");
25
+ htmlSource = { file: "index.html", sha256: sha256(bytes), bytes: bytes.length };
26
+ } finally { dom.window.close(); }
27
+ }
28
+ }
29
+ if (entry === undefined) return { discoveryModules: modules };
30
+ if (typeof entry !== "string" || !entry || entry.startsWith("/") || /[\\?#:\s]/.test(entry) || entry.split("/").some(part => !part || part === "." || part === "..") || !modules.has(entry))
31
+ throw Error("entry must name one source module relative to the scanned root");
32
+ const selected = new Map();
33
+ function visit(module) {
34
+ if (selected.has(module.file)) return;
35
+ selected.set(module.file, module);
36
+ const dependencies = new Set([...module.imports.values()].map(ref => ref.specifier));
37
+ function imports(node) {
38
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) dependencies.add(node.moduleSpecifier.text);
39
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0])) dependencies.add(node.arguments[0].text);
40
+ ts.forEachChild(node, imports);
41
+ }
42
+ imports(module.source);
43
+ for (const specifier of dependencies) {
44
+ const target = resolveModule(module.file, specifier);
45
+ if (target) visit(target);
46
+ }
47
+ }
48
+ visit(modules.get(entry));
49
+ return { discoveryModules: selected, htmlSource, entry };
50
+ }
@@ -0,0 +1,268 @@
1
+ /** Bounded AST references only. No merchant module, getter, factory or callback is invoked. */
2
+ export function sourceResolution({ ts, resolveModule, resolveBinding }) {
3
+ const unwrap = (node) => {
4
+ while (
5
+ node &&
6
+ (ts.isParenthesizedExpression(node) ||
7
+ ts.isAsExpression(node) ||
8
+ ts.isTypeAssertionExpression(node) ||
9
+ ts.isSatisfiesExpression(node) ||
10
+ ts.isNonNullExpression(node) ||
11
+ ts.isAwaitExpression(node))
12
+ )
13
+ node = node.expression;
14
+ return node;
15
+ };
16
+ function reference(node, module, seen = new Set()) {
17
+ node = unwrap(node);
18
+ if (!node || seen.size >= 32) return null;
19
+ const key = module.file + ":" + node.pos + ":" + node.end;
20
+ if (seen.has(key)) return null;
21
+ seen = new Set([...seen, key]);
22
+ if (ts.isIdentifier(node)) {
23
+ const ref = resolveBinding(module, node.text);
24
+ return ref && reference(ref.node, ref.module, seen);
25
+ }
26
+ if (
27
+ ts.isPropertyAccessExpression(node) ||
28
+ ts.isElementAccessExpression(node)
29
+ ) {
30
+ if (node.questionDotToken) return null;
31
+ const name = ts.isPropertyAccessExpression(node)
32
+ ? node.name.text
33
+ : string(node.argumentExpression, module, seen);
34
+ if (typeof name !== "string") return null;
35
+ if (ts.isIdentifier(node.expression)) {
36
+ const imported = module.imports.get(node.expression.text);
37
+ if (imported?.name === "*") {
38
+ const target = resolveModule(module.file, imported.specifier);
39
+ const ref =
40
+ target && resolveBinding(target, target.exports.get(name) ?? name);
41
+ return ref && reference(ref.node, ref.module, seen);
42
+ }
43
+ }
44
+ const record = reference(node.expression, module, seen);
45
+ if (!record) return null;
46
+ if (ts.isEnumDeclaration(record.node)) {
47
+ const member = record.node.members.find(
48
+ (member) => member.name.text === name,
49
+ );
50
+ return member && reference(member.initializer, record.module, seen);
51
+ }
52
+ if (
53
+ !ts.isObjectLiteralExpression(record.node) ||
54
+ record.node.properties.some(
55
+ (property) =>
56
+ !ts.isPropertyAssignment(property) ||
57
+ (property.name && ts.isComputedPropertyName(property.name)),
58
+ )
59
+ )
60
+ return null;
61
+ const fields = record.node.properties.filter(
62
+ (property) => property.name.text === name,
63
+ );
64
+ return fields.length === 1
65
+ ? reference(fields[0].initializer, record.module, seen)
66
+ : null;
67
+ }
68
+ return { node, module };
69
+ }
70
+ function string(node, module, seen = new Set()) {
71
+ const ref = reference(node, module, seen);
72
+ if (!ref) return undefined;
73
+ node = ref.node;
74
+ module = ref.module;
75
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
76
+ return node.text;
77
+ const key = module.file + ":string:" + node.pos;
78
+ if (seen.has(key) || seen.size >= 32) return undefined;
79
+ seen = new Set([...seen, key]);
80
+ if (ts.isTemplateExpression(node)) {
81
+ let result = node.head.text;
82
+ for (const span of node.templateSpans) {
83
+ const value = string(span.expression, module, seen);
84
+ if (value === undefined) return undefined;
85
+ result += value + span.literal.text;
86
+ if (result.length > 2048) return undefined;
87
+ }
88
+ return result;
89
+ }
90
+ if (
91
+ ts.isBinaryExpression(node) &&
92
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken
93
+ ) {
94
+ const left = string(node.left, module, seen),
95
+ right = string(node.right, module, seen);
96
+ return left !== undefined &&
97
+ right !== undefined &&
98
+ left.length + right.length <= 2048
99
+ ? left + right
100
+ : undefined;
101
+ }
102
+ return undefined;
103
+ }
104
+ function external(module, node) {
105
+ node = unwrap(node);
106
+ if (node && ts.isIdentifier(node)) return module.imports.get(node.text);
107
+ if (
108
+ node &&
109
+ ts.isPropertyAccessExpression(node) &&
110
+ ts.isIdentifier(node.expression)
111
+ ) {
112
+ const owner = module.imports.get(node.expression.text);
113
+ if (owner && ["*", "default"].includes(owner.name))
114
+ return { specifier: owner.specifier, name: node.name.text };
115
+ }
116
+ return null;
117
+ }
118
+ function functionNode(node) {
119
+ node = unwrap(node);
120
+ return node &&
121
+ (ts.isFunctionDeclaration(node) ||
122
+ ts.isFunctionExpression(node) ||
123
+ ts.isArrowFunction(node))
124
+ ? node
125
+ : null;
126
+ }
127
+ function returned(fn) {
128
+ if (!fn?.body) return null;
129
+ if (!ts.isBlock(fn.body)) return fn.body;
130
+ return fn.body.statements.length === 1 &&
131
+ ts.isReturnStatement(fn.body.statements[0])
132
+ ? fn.body.statements[0].expression
133
+ : null;
134
+ }
135
+ // Prove only the successful module return of a forwarding function. Catch
136
+ // side effects are opaque and must rethrow; finally/alternate module returns refuse.
137
+ function forwards(node, module, parameters, depth = 0) {
138
+ node = unwrap(node);
139
+ if (!node || depth > 8 || !ts.isCallExpression(node)) return false;
140
+ if (
141
+ ts.isIdentifier(node.expression) &&
142
+ parameters.has(node.expression.text)
143
+ )
144
+ return node.arguments.length === 0;
145
+ if (!ts.isIdentifier(node.expression)) return false;
146
+ const ref = resolveBinding(module, node.expression.text),
147
+ fn = ref && functionNode(ref.node);
148
+ if (
149
+ !fn ||
150
+ fn.asteriskToken ||
151
+ fn.parameters.length !== 1 ||
152
+ fn.parameters[0].dotDotDotToken ||
153
+ !ts.isIdentifier(fn.parameters[0].name) ||
154
+ node.arguments.length !== 1 ||
155
+ !ts.isIdentifier(node.arguments[0]) ||
156
+ !parameters.has(node.arguments[0].text)
157
+ )
158
+ return false;
159
+ const nested = new Set([fn.parameters[0].name.text]);
160
+ let result = returned(fn);
161
+ if (
162
+ !result &&
163
+ fn.body &&
164
+ ts.isBlock(fn.body) &&
165
+ fn.body.statements.length === 1
166
+ ) {
167
+ const statement = fn.body.statements[0];
168
+ if (
169
+ !ts.isTryStatement(statement) ||
170
+ statement.finallyBlock ||
171
+ !statement.catchClause ||
172
+ !ts.isThrowStatement(statement.catchClause.block.statements.at(-1))
173
+ )
174
+ return false;
175
+ let overrides = false;
176
+ const visit = (item) => {
177
+ if (ts.isReturnStatement(item)) overrides = true;
178
+ ts.forEachChild(item, visit);
179
+ };
180
+ visit(statement.catchClause);
181
+ if (
182
+ overrides ||
183
+ statement.tryBlock.statements.length !== 1 ||
184
+ !ts.isReturnStatement(statement.tryBlock.statements[0])
185
+ )
186
+ return false;
187
+ result = statement.tryBlock.statements[0].expression;
188
+ }
189
+ return forwards(result, ref.module, nested, depth + 1);
190
+ }
191
+ function lazyFactory(call, module) {
192
+ if (call.arguments.length !== 1) return null;
193
+ const imported = external(module, call.expression);
194
+ if (imported?.specifier === "react" && imported.name === "lazy")
195
+ return { node: call.arguments[0], module, wrapped: false };
196
+ if (!ts.isIdentifier(call.expression)) return null;
197
+ const ref = resolveBinding(module, call.expression.text),
198
+ fn = ref && functionNode(ref.node);
199
+ if (
200
+ !fn ||
201
+ fn.asteriskToken ||
202
+ fn.parameters.length !== 1 ||
203
+ fn.parameters[0].dotDotDotToken ||
204
+ !ts.isIdentifier(fn.parameters[0].name)
205
+ )
206
+ return null;
207
+ if (fn.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return null;
208
+ const value = unwrap(returned(fn));
209
+ if (!value || !ts.isCallExpression(value) || value.arguments.length !== 1)
210
+ return null;
211
+ const wrapper = external(ref.module, value.expression),
212
+ callback = functionNode(value.arguments[0]);
213
+ if (
214
+ wrapper?.specifier !== "react" ||
215
+ wrapper.name !== "lazy" ||
216
+ !callback ||
217
+ callback.asteriskToken ||
218
+ callback.parameters.length ||
219
+ !forwards(
220
+ returned(callback),
221
+ ref.module,
222
+ new Set([fn.parameters[0].name.text]),
223
+ )
224
+ )
225
+ return null;
226
+ return { node: call.arguments[0], module, wrapped: true };
227
+ }
228
+ function component(module, name) {
229
+ if (typeof name !== "string" || !name) return null;
230
+ const parts = name.split(".");
231
+ let ref;
232
+ if (parts.length === 2) {
233
+ const imported = module.imports.get(parts[0]),
234
+ target =
235
+ imported &&
236
+ ["*", "default"].includes(imported.name) &&
237
+ resolveModule(module.file, imported.specifier);
238
+ ref =
239
+ target &&
240
+ resolveBinding(target, target.exports.get(parts[1]) ?? parts[1]);
241
+ } else if (parts.length === 1) ref = resolveBinding(module, name);
242
+ if (!ref) return null;
243
+ const node = unwrap(ref.node);
244
+ if (node && ts.isCallExpression(node)) {
245
+ const factory = lazyFactory(node, ref.module),
246
+ callback = factory && functionNode(factory.node),
247
+ body = callback && unwrap(returned(callback));
248
+ if (
249
+ body &&
250
+ !callback.asteriskToken &&
251
+ !callback.parameters.length &&
252
+ ts.isCallExpression(body) &&
253
+ body.expression.kind === ts.SyntaxKind.ImportKeyword &&
254
+ body.arguments.length === 1
255
+ ) {
256
+ const specifier = string(body.arguments[0], factory.module),
257
+ target =
258
+ typeof specifier === "string" &&
259
+ resolveModule(factory.module.file, specifier);
260
+ const resolved = target && resolveBinding(target, target.defaultName);
261
+ if (resolved)
262
+ return { ...resolved, lazy: true, wrapped: factory.wrapped };
263
+ }
264
+ }
265
+ return ref;
266
+ }
267
+ return { reference, string, component };
268
+ }
@@ -0,0 +1,143 @@
1
+ /** Generated file-route graphs only: read the registered tree, never execute a router. */
2
+ export function tanstackRoutes({ discoveryModules, ts, resolution, gap, component, add, externalImport }) {
3
+ let routers = 0;
4
+ const unwrap = node => {
5
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
6
+ return node;
7
+ };
8
+ const fail = message => { throw Error("TanStack route adapter: " + message); };
9
+ function ref(node, module) {
10
+ const resolved = resolution.reference(node, module);
11
+ if (!resolved) fail("unresolved or cyclic source reference");
12
+ return resolved;
13
+ }
14
+ function fields(node, module) {
15
+ const resolved = ref(node, module);
16
+ node = resolved.node;
17
+ if (!ts.isObjectLiteralExpression(node) || node.properties.some(p => !(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p) || ts.isMethodDeclaration(p)) || ts.isComputedPropertyName(p.name))) fail("configuration must be a literal record without spreads or getters");
18
+ const values = new Map();
19
+ for (const property of node.properties) {
20
+ if (values.has(property.name.text)) fail("duplicate configuration field");
21
+ values.set(property.name.text, ts.isShorthandPropertyAssignment(property) ? property.name : ts.isMethodDeclaration(property) ? property : property.initializer);
22
+ }
23
+ return { module: resolved.module, node, values };
24
+ }
25
+ function returned(node) {
26
+ node = unwrap(node);
27
+ if (!node || !(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) || node.parameters.length) fail("parent must be a parameterless source callback");
28
+ if (!ts.isBlock(node.body)) return node.body;
29
+ if (node.body.statements.length === 1 && ts.isReturnStatement(node.body.statements[0])) return node.body.statements[0].expression;
30
+ fail("parent callback is not a direct return");
31
+ }
32
+ function identity(node, module, depth = 0) {
33
+ if (depth > 32) fail("route reference budget exceeded");
34
+ const resolved = ref(node, module);
35
+ node = resolved.node; module = resolved.module;
36
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["update", "_addFileChildren", "_addFileTypes"].includes(node.expression.name.text)) return identity(node.expression.expression, module, depth + 1);
37
+ return module.file + ":" + node.pos + ":" + node.end;
38
+ }
39
+ function graph(node, module, seen = new Set()) {
40
+ const resolved = ref(node, module);
41
+ node = resolved.node; module = resolved.module;
42
+ const key = module.file + ":" + node.pos + ":" + node.end;
43
+ if (seen.has(key) || seen.size > 32) fail("cyclic or excessive route graph");
44
+ seen = new Set([...seen, key]);
45
+ if (!ts.isCallExpression(node)) fail("route graph is not a supported source declaration");
46
+ if (ts.isPropertyAccessExpression(node.expression)) {
47
+ const method = node.expression.name.text;
48
+ if (method === "_addFileTypes" && !node.arguments.length) return graph(node.expression.expression, module, seen);
49
+ if (method === "_addFileChildren" && node.arguments.length === 1) {
50
+ const parent = graph(node.expression.expression, module, seen), children = fields(node.arguments[0], module);
51
+ if (parent.children.length) fail("route children registered more than once");
52
+ parent.children = [...children.values.values()].map(child => graph(child, children.module, seen));
53
+ return parent;
54
+ }
55
+ if (method === "update" && node.arguments.length === 1) {
56
+ const original = graph(node.expression.expression, module, seen), config = fields(node.arguments[0], module);
57
+ if (original.root || original.updated || original.children.length) fail("unexpected route update");
58
+ if ([...config.values.keys()].some(key => !["id", "path", "getParentRoute"].includes(key))) fail("unsupported generated update field");
59
+ const routeId = resolution.string(config.values.get("id"), config.module);
60
+ if (!routeId || !config.values.has("getParentRoute")) fail("route update needs a static id and parent");
61
+ original.parent = identity(returned(config.values.get("getParentRoute")), config.module);
62
+ original.path = config.values.has("path") ? resolution.string(config.values.get("path"), config.module) : undefined;
63
+ if (config.values.has("path") && original.path === undefined) fail("dynamic generated path");
64
+ original.updated = true;
65
+ original.id = routeId;
66
+ original.routeModule = module;
67
+ original.routeNode = node;
68
+ return original;
69
+ }
70
+ fail("unsupported graph method " + method);
71
+ }
72
+ let factory = node.expression, options = node.arguments[0], root = false, sourceId = "$root";
73
+ if (ts.isCallExpression(factory)) {
74
+ const creator = externalImport(module, factory.expression.getText(module.source));
75
+ if (creator?.specifier !== "@tanstack/react-router") fail("unrecognized route factory");
76
+ if (creator.name === "createFileRoute") {
77
+ if (factory.arguments.length !== 1 || resolution.string(factory.arguments[0], module) === undefined) fail("file route identity must be static");
78
+ sourceId = resolution.string(factory.arguments[0], module);
79
+ } else if (creator.name === "createRootRouteWithContext" && !factory.arguments.length) root = true;
80
+ else fail("unsupported route factory");
81
+ } else {
82
+ const creator = externalImport(module, factory.getText(module.source));
83
+ if (creator?.specifier !== "@tanstack/react-router" || creator.name !== "createRootRoute") fail("unrecognized root factory");
84
+ root = true;
85
+ }
86
+ if (node.arguments.length !== 1) fail("route factory needs one source configuration");
87
+ const config = fields(options, module);
88
+ if (["id", "path", "getParentRoute"].some(key => config.values.has(key))) fail("file-route URL configuration must come from the generated update");
89
+ if ([...config.values.keys()].some(key => ["loader", "beforeLoad", "validateSearch", "errorComponent", "notFoundComponent", "pendingComponent"].includes(key))) gap("ROUTE_DATA_UNOBSERVED", module.file, "TanStack loaders, guards, search and error states are not executed");
90
+ return { key: identity(node, module), root, sourceId, config, children: [], routeModule: module, routeNode: node };
91
+ }
92
+ function pathOf(parent, child) {
93
+ if (child === undefined) return parent;
94
+ if (/[?#\\\s]/.test(child) || child.split("/").some(part => part === "." || part === "..")) fail("invalid generated path");
95
+ const segments = child.split("/").filter(Boolean).map(part => {
96
+ if (part === "$") return "*";
97
+ if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(part)) return ":" + part.slice(1);
98
+ if (/[$\[\]{}()*:]/.test(part)) fail("unsupported parameter pattern");
99
+ return part;
100
+ });
101
+ return (parent.replace(/\/$/, "") + "/" + segments.join("/")).replace(/\/$/, "") || "/";
102
+ }
103
+ function project(root) {
104
+ const registered = new Set();
105
+ function walk(route, parent, path, ancestors, depth = 0) {
106
+ if (depth > 16 || registered.size >= 200 || registered.has(route.key)) fail("duplicate, cyclic or excessive registered route");
107
+ registered.add(route.key);
108
+ if (parent ? route.root || !route.updated || route.parent !== parent.key : !route.root) fail("registered parent does not match generated parent");
109
+ const publicPath = pathOf(path, route.path);
110
+ // Use source route identity, not the public URL, for inline layout symbols.
111
+ const target = component(route.config.values.get("component"), route.config.module, "$tanstack:" + route.config.module.file + ":" + route.sourceId);
112
+ let indexAtPath = false;
113
+ const childIds = new Set();
114
+ for (const child of route.children) {
115
+ if (childIds.has(child.id)) fail("duplicate generated child id");
116
+ childIds.add(child.id);
117
+ }
118
+ for (const child of route.children) indexAtPath = walk(child, route, publicPath, target ? [...ancestors, target] : ancestors, depth + 1) || indexAtPath;
119
+ const ownsPath = route.root || route.path !== undefined;
120
+ if (ownsPath && !indexAtPath && target) add(route.routeModule, route.routeNode, publicPath, target, ancestors);
121
+ if (ownsPath && !target && !route.children.length) gap("ROUTE_COMPONENT_UNSUPPORTED", route.config.module.file, "TanStack route without an inspectable component");
122
+ return ownsPath && publicPath === path || indexAtPath && publicPath === path;
123
+ }
124
+ walk(root, null, "/", []);
125
+ }
126
+ for (const module of discoveryModules.values()) {
127
+ function visit(node) {
128
+ if (ts.isCallExpression(node)) {
129
+ const imported = externalImport(module, node.expression.getText(module.source));
130
+ if (imported?.specifier === "@tanstack/react-router" && imported.name === "createRouter") {
131
+ if (++routers > 1) fail("multiple router authorities require one explicit entry");
132
+ const options = fields(node.arguments[0], module);
133
+ if (node.arguments.length !== 1 || !options.values.has("routeTree") || ["basepath", "history", "rewrite", "trailingSlash"].some(key => options.values.has(key))) fail("custom URL routing requires a separate adapter");
134
+ project(graph(options.values.get("routeTree"), options.module));
135
+ return;
136
+ }
137
+ }
138
+ ts.forEachChild(node, visit);
139
+ }
140
+ visit(module.source);
141
+ }
142
+ return routers;
143
+ }
package/src/transform.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import MagicString from "magic-string";
2
2
  import { dependency } from "./dependencies.mjs";
3
- import { automaticSourceAnchors, instrumentableSource } from "./anchors.mjs";
3
+ import { automaticSourceAnchors, instrumentableSource, sourceScriptKind } from "./anchors.mjs";
4
4
 
5
5
  /** Adds build-only anchors without editing merchant source or evaluating any expression. */
6
6
  export function transformSourceForIam(code, file) {
@@ -12,7 +12,7 @@ export function transformSourceForIam(code, file) {
12
12
  code,
13
13
  ts.ScriptTarget.Latest,
14
14
  true,
15
- /\.jsx?$/.test(file) ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
15
+ sourceScriptKind(file, ts),
16
16
  );
17
17
  if (source.parseDiagnostics.length)
18
18
  throw new Error("IAM cannot instrument invalid source: " + file);
package/src/upload.mjs CHANGED
@@ -2,10 +2,13 @@ import { repositoryCommit } from "./git-metadata.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
 
4
4
  export const DEFAULT_BASE_URL = "https://tinker.cash";
5
+ /** One App environment can hold several clients (named builds); each has its own current version. */
6
+ export const DEFAULT_CLIENT = "web";
7
+ const CLIENT_KEY = /^[a-z][a-z0-9-]{0,39}$/;
5
8
  const SIGNED_UPLOAD_ORIGIN = "https://storage.googleapis.com";
6
9
 
7
- /** The build's upload settings: the key and API origin come from options or the build environment. */
8
- export function uploadSettings({ apiKey, baseUrl, upload = "production" } = {}, env = process.env) {
10
+ /** The build's upload settings: the key, client and API origin come from options or the build environment. */
11
+ export function uploadSettings({ apiKey, baseUrl, upload = "production", client } = {}, env = process.env) {
9
12
  const key = apiKey ?? env.INDIANA_IAM_KEY ?? "";
10
13
  const mode = upload === true ? "always" : upload;
11
14
  const enabled =
@@ -13,7 +16,13 @@ export function uploadSettings({ apiKey, baseUrl, upload = "production" } = {},
13
16
  mode !== false &&
14
17
  // On Vercel only production deployments upload; preview builds would keep switching the current version.
15
18
  (mode === "always" || !env.VERCEL_ENV || env.VERCEL_ENV === "production");
16
- return { enabled, apiKey: key, baseUrl: baseUrl ?? env.INDIANA_IAM_BASE_URL ?? DEFAULT_BASE_URL, missingKey: !key };
19
+ return {
20
+ enabled,
21
+ apiKey: key,
22
+ baseUrl: baseUrl ?? env.INDIANA_IAM_BASE_URL ?? DEFAULT_BASE_URL,
23
+ client: client ?? env.INDIANA_IAM_CLIENT ?? DEFAULT_CLIENT,
24
+ missingKey: !key,
25
+ };
17
26
  }
18
27
 
19
28
  /** A short version label: the commit being built when the CI or git exposes it. */
@@ -35,19 +44,32 @@ async function failure(response, step) {
35
44
  /**
36
45
  * Uploads one generated IAM to Indiana: register, PUT the bytes to the server-signed URL,
37
46
  * then let the server verify them. The key needs only the app-manifests:write permission;
38
- * the uploaded (or redeployed) version becomes the App environment's current version.
47
+ * the uploaded (or redeployed) version becomes its client's current version.
39
48
  */
40
- export async function uploadAppManifest({ serialized, digest, label, apiKey, baseUrl = DEFAULT_BASE_URL, fetch = globalThis.fetch }) {
49
+ export async function uploadAppManifest({
50
+ serialized,
51
+ digest,
52
+ label,
53
+ client = DEFAULT_CLIENT,
54
+ apiKey,
55
+ baseUrl = DEFAULT_BASE_URL,
56
+ fetch = globalThis.fetch,
57
+ }) {
41
58
  if (!apiKey) throw new Error("IAM upload needs a key with the app-manifests:write permission");
42
59
  const base = new URL(baseUrl);
43
60
  if (base.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(base.hostname))
44
61
  throw new Error("IAM upload needs an https Indiana origin");
62
+ if (!CLIENT_KEY.test(client))
63
+ throw new Error("IAM client must be 1-40 lowercase letters, digits or hyphens, starting with a letter");
64
+ // The server checks the stored file declares this same platform before the version becomes current.
65
+ const platforms = JSON.parse(serialized)?.app?.platforms;
66
+ if (!Array.isArray(platforms) || platforms.length !== 1) throw new Error("IAM upload needs a manifest for exactly one platform");
45
67
  const bytes = Buffer.from(serialized);
46
68
  const authorization = { Authorization: "Bearer " + apiKey };
47
69
  const registered = await fetch(new URL("/api/iam/versions", base), {
48
70
  method: "POST",
49
71
  headers: { ...authorization, "Content-Type": "application/json" },
50
- body: JSON.stringify({ requestId: randomUUID(), label, sha256: digest, bytes: bytes.byteLength }),
72
+ body: JSON.stringify({ requestId: randomUUID(), client, platform: platforms[0], label, sha256: digest, bytes: bytes.byteLength }),
51
73
  signal: AbortSignal.timeout(60_000),
52
74
  });
53
75
  if (!registered.ok) throw await failure(registered, "registration");
package/src/validate.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { documentDom, assertDomBudget } from "./dom-payload.mjs";
1
2
  import { loadTrustedRuntimes } from "./runtime-asset.mjs";
2
3
  export { parseManifest } from "./json.mjs";
3
4
  import { dependency } from "./dependencies.mjs";
@@ -31,6 +32,14 @@ export async function validate(manifest) {
31
32
  const verifiedSelectors = new Map();
32
33
  canonical(manifest);
33
34
  assertManifestShape(manifest);
35
+ assertDomBudget(manifest.documents);
36
+ if (manifest.documents.some(doc => doc.domPayload)) assertManifestShape({
37
+ ...manifest,
38
+ documents: manifest.documents.map(doc => {
39
+ const { domPayload, ...body } = doc;
40
+ return { ...body, ...documentDom(doc) };
41
+ }),
42
+ });
34
43
  const map = (values, key) => {
35
44
  const result = new Map(values.map((v) => [v[key], v]));
36
45
  if (result.size !== values.length) throw new Error("Duplicate " + key);
@@ -57,7 +66,7 @@ export async function validate(manifest) {
57
66
  if (!components.has(element.componentKey))
58
67
  throw new Error("Missing element component");
59
68
  }
60
- if (["react-router", "react-spa"].includes(manifest.routing.framework)) {
69
+ if (["react-router", "react-spa", "tanstack-router"].includes(manifest.routing.framework)) {
61
70
  if (manifest.routing.basePath || manifest.routing.trailingSlash || manifest.routing.routeSources?.length !== pages.size)
62
71
  throw Error("React route provenance is incomplete");
63
72
  for (const row of manifest.routing.routeSources) {
@@ -121,10 +130,11 @@ export async function validate(manifest) {
121
130
  throw new Error("Document hash mismatch");
122
131
  const fullNodes=documentNodes(doc);
123
132
  if (doc.sourceMappingsHash !== sourceMappingsHash(fullNodes, elements)) throw new Error('Source mapping hash mismatch');
124
- expandedDocuments.push({...doc,nodes:fullNodes});
133
+ const decoded = documentDom(doc);
134
+ expandedDocuments.push({...doc,html:decoded.html,nodes:fullNodes});
125
135
  const nodes = map(fullNodes, "nodeId");
126
136
  const dom = new JSDOM(
127
- "<!doctype html><html><body>" + doc.html + "</body></html>",
137
+ "<!doctype html><html><body>" + decoded.html + "</body></html>",
128
138
  );
129
139
  try {
130
140
  const document = dom.window.document;
@@ -277,7 +287,7 @@ export async function validate(manifest) {
277
287
  components: components.size,
278
288
  elements: elements.size,
279
289
  documents: docs.size,
280
- concreteNodes: manifest.documents.reduce((n, d) => n + d.nodes.length, 0),
290
+ concreteNodes: expandedDocuments.reduce((n, d) => n + d.nodes.length, 0),
281
291
  runtimeObserved: manifest.coverage.runtimeObserved,
282
292
  routeTree,
283
293
  };
package/src/vite.d.ts CHANGED
@@ -6,7 +6,10 @@ export declare function indianaIam(options?: IndianaIamBuildOptions): {
6
6
  name: "indiana-iam";
7
7
  apply: "build";
8
8
  enforce: "pre";
9
- transform(code: string, id: string): IamSourceTransform | null;
9
+ transform: {
10
+ order: "pre";
11
+ handler(code: string, id: string): IamSourceTransform | null;
12
+ };
10
13
  config(config: {
11
14
  root?: string;
12
15
  build?: { ssr?: unknown };