@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,28 @@
1
+ import { checkedDomPayload, parsedDomPayload } from "./dom-payload-json.mjs";
2
+
3
+ /** The viewer decodes locally; it never fetches content or executes merchant code. */
4
+ export async function documentDomBrowser(document) {
5
+ const payload = checkedDomPayload(document);
6
+ if (!payload) return { html: document.html, nodes: document.nodes };
7
+ if (typeof DecompressionStream !== "function") throw Error("Compressed IAM requires DecompressionStream support");
8
+ const binary = atob(payload.data);
9
+ if (btoa(binary) !== payload.data) throw Error("Noncanonical compressed DOM base64");
10
+ const compressed = Uint8Array.from(binary, character => character.charCodeAt(0));
11
+ const stream = new ReadableStream({ start(controller) { controller.enqueue(compressed); controller.close(); } }).pipeThrough(new DecompressionStream("gzip"));
12
+ const reader = stream.getReader(), chunks = [];
13
+ let total = 0;
14
+ try {
15
+ for (;;) {
16
+ const next = await reader.read();
17
+ if (next.done) break;
18
+ total += next.value.byteLength;
19
+ if (total > payload.bytes) throw Error("Expanded DOM byte budget exceeded");
20
+ chunks.push(next.value);
21
+ }
22
+ } finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
23
+ if (total !== payload.bytes) throw Error("Expanded DOM byte length mismatch");
24
+ const decoded = new Uint8Array(total);
25
+ let offset = 0;
26
+ for (const chunk of chunks) { decoded.set(chunk, offset); offset += chunk.byteLength; }
27
+ return parsedDomPayload(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
28
+ }
@@ -0,0 +1,12 @@
1
+ /** Shared with the offline viewer. Encodings describe storage, never executable code. */
2
+ export function checkedDomPayload(document) {
3
+ const payload = document.domPayload;
4
+ if (!payload) return null;
5
+ 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");
6
+ return payload;
7
+ }
8
+ export function parsedDomPayload(text) {
9
+ const value = JSON.parse(text);
10
+ 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");
11
+ return value;
12
+ }
@@ -0,0 +1,50 @@
1
+ import { gzip, Inflate } from "pako";
2
+ import { checkedDomPayload, parsedDomPayload } from "./dom-payload-json.mjs";
3
+
4
+ export const MAX_EXPANDED_DOM_BYTES = 33_554_432;
5
+
6
+ /** Lossless, local decoding with a declared and enforced output bound. */
7
+ export function documentDom(document) {
8
+ const payload = checkedDomPayload(document);
9
+ if (!payload) return { html: document.html, nodes: document.nodes };
10
+ const compressed = Buffer.from(payload.data, "base64");
11
+ if (compressed.toString("base64") !== payload.data) throw Error("Noncanonical compressed DOM base64");
12
+ const inflater = new Inflate({ windowBits: 31, chunkSize: 32_768 });
13
+ const chunks = [];
14
+ let total = 0;
15
+ inflater.onData = chunk => {
16
+ total += chunk.byteLength;
17
+ if (total > payload.bytes) throw Error("Expanded DOM byte budget exceeded");
18
+ chunks.push(chunk);
19
+ };
20
+ if (!inflater.push(compressed, true) || inflater.err || !inflater.ended) throw Error("Invalid compressed DOM stream: " + inflater.msg);
21
+ if (inflater.strm.avail_in !== 0) throw Error("Trailing compressed DOM data");
22
+ if (total !== payload.bytes) throw Error("Expanded DOM byte length mismatch");
23
+ const decoded = new Uint8Array(total);
24
+ let offset = 0;
25
+ for (const chunk of chunks) { decoded.set(chunk, offset); offset += chunk.byteLength; }
26
+ return parsedDomPayload(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
27
+ }
28
+
29
+ export function assertDomBudget(documents) {
30
+ let total = 0;
31
+ for (const document of documents) {
32
+ const payload = checkedDomPayload(document);
33
+ total += payload ? payload.bytes : Buffer.byteLength(JSON.stringify({ html: document.html, nodes: document.nodes }));
34
+ if (total > MAX_EXPANDED_DOM_BYTES) throw Error("Expanded DOM aggregate byte budget exceeded");
35
+ }
36
+ }
37
+
38
+ /** Only sizeable, beneficial payloads are compressed; the original IDs and values survive byte-for-byte. */
39
+ export function packDocumentDom(document) {
40
+ if (document.domPayload) return false;
41
+ const raw = Buffer.from(JSON.stringify({ html: document.html, nodes: document.nodes }));
42
+ if (raw.length < 16_384 || raw.length > 8_388_608) return false;
43
+ const zipped = gzip(raw, { level: 9, header: { time: 0, os: 255 } });
44
+ const payload = { encoding: "gzip-json-v1", bytes: raw.length, data: Buffer.from(zipped).toString("base64") };
45
+ if (Buffer.byteLength(JSON.stringify(payload)) + 1024 >= raw.length) return false;
46
+ document.html = "";
47
+ document.nodes = [];
48
+ document.domPayload = payload;
49
+ return true;
50
+ }
@@ -1,3 +1,4 @@
1
+ import { tanstackRoutes } from "./tanstack-routes.mjs";
1
2
  import { pagesRoute } from "./routing.mjs";
2
3
  import { routeCompositions } from "./route-composition.mjs";
3
4
  import { compositionIdentity, flattenComposition } from "./render-index.mjs";
@@ -16,11 +17,15 @@ export function externalImport(module, name) {
16
17
  /** Source-only adapters. Merchant modules, loaders and configuration are never executed. */
17
18
  export function frameworkRoutes({
18
19
  modules,
20
+ discoveryModules = modules,
21
+ entry,
19
22
  resolveModule,
20
23
  ts,
21
24
  sourceLocation,
22
25
  navigationPaths,
23
26
  gap,
27
+ resolution,
28
+ inlineComponent,
24
29
  }) {
25
30
  const files = [...modules.keys()];
26
31
  const app = routeCompositions(files, navigationPaths);
@@ -48,6 +53,7 @@ export function frameworkRoutes({
48
53
  app.entries.length ||
49
54
  (pages.length && (explicitNextPages || (!reactRouting && !reactBootstrap)))
50
55
  ) {
56
+ if (entry) throw Error("entry is for browser applications; scope Next.js by its app root");
51
57
  const entries = [...app.entries];
52
58
  const hardRoutes = new Set(
53
59
  entries.filter((e) => e.entryMode === "hard").map((e) => e.route),
@@ -118,87 +124,17 @@ export function frameworkRoutes({
118
124
  return node;
119
125
  };
120
126
  const external = externalImport;
121
- function literal(node, module, seen = new Set()) {
122
- node = unwrap(node);
123
- if (!node || seen.size > 16) return undefined;
124
- if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
125
- return node.text;
126
- if (ts.isIdentifier(node)) {
127
- if (seen.has(node.text)) return undefined;
128
- return literal(
129
- module.bindings.get(node.text),
130
- module,
131
- new Set([...seen, node.text]),
132
- );
133
- }
134
- if (
135
- ts.isPropertyAccessExpression(node) &&
136
- ts.isIdentifier(node.expression)
137
- ) {
138
- const declaration = module.source.statements.find(
139
- (s) => ts.isEnumDeclaration(s) && s.name.text === node.expression.text,
140
- );
141
- const member = declaration?.members.find(
142
- (m) =>
143
- m.name.getText(module.source).replace(/^['"]|['"]$/g, "") ===
144
- node.name.text,
145
- );
146
- if (member) return literal(member.initializer, module, seen);
147
- const record = unwrap(module.bindings.get(node.expression.text));
148
- if (record && ts.isObjectLiteralExpression(record))
149
- return literal(
150
- record.properties.find((p) => p.name?.text === node.name.text)
151
- ?.initializer,
152
- module,
153
- seen,
154
- );
155
- }
156
- return undefined;
157
- }
127
+ const literal = (node, module, seen = new Set()) => resolution.string(node, module, seen);
158
128
  function component(tag, module) {
159
- const imported = module.imports.get(tag);
160
- if (imported) {
161
- const target = resolveModule(module.file, imported.specifier);
162
- return target
163
- ? {
164
- file: target.file,
165
- symbol:
166
- imported.name === "default"
167
- ? target.defaultName
168
- : (target.exports.get(imported.name) ?? imported.name),
169
- }
170
- : null;
171
- }
172
- const local = unwrap(module.bindings.get(tag));
173
- // React.lazy(() => import("literal")) is inspected, never invoked.
174
- if (
175
- local &&
176
- ts.isCallExpression(local) &&
177
- external(module, local.expression.getText(module.source))?.specifier ===
178
- "react" &&
179
- external(module, local.expression.getText(module.source))?.name === "lazy"
180
- ) {
181
- const fn = local.arguments[0],
182
- body = fn && ts.isArrowFunction(fn) ? unwrap(fn.body) : null;
183
- if (
184
- body &&
185
- ts.isCallExpression(body) &&
186
- body.expression.kind === ts.SyntaxKind.ImportKeyword &&
187
- body.arguments.length === 1
188
- ) {
189
- const specifier = literal(body.arguments[0], module),
190
- target =
191
- typeof specifier === "string" &&
192
- resolveModule(module.file, specifier);
193
- if (target) return { file: target.file, symbol: target.defaultName };
194
- }
195
- }
196
- return local ? { file: module.file, symbol: tag } : null;
129
+ const ref = resolution.component(module, tag);
130
+ if (ref?.wrapped) gap("LAZY_WRAPPER_UNOBSERVED", module.file, tag + ": only a statically forwarded successful module is projected; retry/reload behavior is not executed");
131
+ return ref ? { file: ref.module.file, symbol: ref.name } : null;
197
132
  }
198
- function jsxComponent(node, module) {
133
+ function jsxComponent(node, module, route) {
199
134
  node = unwrap(node);
200
135
  if (!node) return null;
201
- if (ts.isJsxExpression(node)) return jsxComponent(node.expression, module);
136
+ if (ts.isJsxExpression(node)) return jsxComponent(node.expression, module, route);
137
+ if (inlineComponent && route !== undefined && (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node))) return inlineComponent(module, node, route);
202
138
  if (ts.isJsxElement(node)) {
203
139
  const name = node.openingElement.tagName.getText(module.source),
204
140
  imported = external(module, name);
@@ -209,13 +145,14 @@ export function frameworkRoutes({
209
145
  const children = node.children.filter(
210
146
  (n) => !ts.isJsxText(n) || n.text.trim(),
211
147
  );
212
- return children.length === 1 ? jsxComponent(children[0], module) : null;
148
+ return children.length === 1 ? jsxComponent(children[0], module, route) : null;
213
149
  }
214
150
  return component(name, module);
215
151
  }
216
152
  if (ts.isJsxSelfClosingElement(node))
217
153
  return component(node.tagName.getText(module.source), module);
218
154
  if (ts.isIdentifier(node)) return component(node.text, module);
155
+ if (ts.isPropertyAccessExpression(node)) return component(node.getText(module.source), module);
219
156
  return null;
220
157
  }
221
158
  function join(parent, child) {
@@ -321,7 +258,8 @@ export function frameworkRoutes({
321
258
  node = unwrap(node);
322
259
  if (
323
260
  !ts.isObjectLiteralExpression(node) ||
324
- node.properties.some((p) => !ts.isPropertyAssignment(p))
261
+ node.properties.some((p) => !(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p) || ts.isMethodDeclaration(p)) || ts.isComputedPropertyName(p.name)) ||
262
+ new Set(node.properties.map(p => p.name.text)).size !== node.properties.length
325
263
  ) {
326
264
  gap(
327
265
  "ROUTES_UNRESOLVED",
@@ -331,7 +269,7 @@ export function frameworkRoutes({
331
269
  continue;
332
270
  }
333
271
  const properties = new Map(
334
- node.properties.map((p) => [p.name.text, p.initializer]),
272
+ node.properties.map((p) => [p.name.text, ts.isMethodDeclaration(p) ? p : ts.isShorthandPropertyAssignment(p) ? p.name : p.initializer]),
335
273
  );
336
274
  const rawPath = properties.has("path")
337
275
  ? literal(properties.get("path"), module)
@@ -345,10 +283,17 @@ export function frameworkRoutes({
345
283
  continue;
346
284
  }
347
285
  const route = join(parent, rawPath);
348
- if (route === null) throw Error("Invalid nested React route");
286
+ if (route === null) {
287
+ if (typeof rawPath === "string" && /\?/.test(rawPath)) {
288
+ gap("ROUTE_PATTERN_UNSUPPORTED", module.file, rawPath + ": optional route parameters require an explicit expansion adapter");
289
+ continue;
290
+ }
291
+ throw Error("Invalid nested React route");
292
+ }
349
293
  const target = jsxComponent(
350
294
  properties.get("element") ?? properties.get("Component"),
351
295
  module,
296
+ route,
352
297
  );
353
298
  const index = properties.get("index")?.kind === ts.SyntaxKind.TrueKeyword;
354
299
  if (
@@ -455,8 +400,15 @@ export function frameworkRoutes({
455
400
  target = jsxComponent(
456
401
  props.get("element") ?? props.get("Component"),
457
402
  module,
403
+ route,
458
404
  );
459
- if (route === null) throw Error("Invalid nested React route");
405
+ if (route === null) {
406
+ if (typeof raw === "string" && /\?/.test(raw)) {
407
+ gap("ROUTE_PATTERN_UNSUPPORTED", module.file, raw + ": optional route parameters require an explicit expansion adapter");
408
+ continue;
409
+ }
410
+ throw Error("Invalid nested React route");
411
+ }
460
412
  const nested =
461
413
  ts.isJsxElement(child) &&
462
414
  child.children.some(
@@ -500,7 +452,8 @@ export function frameworkRoutes({
500
452
  add(module, child, route, target, ancestors);
501
453
  }
502
454
  }
503
- for (const module of modules.values()) {
455
+ const tanstack = tanstackRoutes({ discoveryModules, ts, resolution, gap, component: jsxComponent, add, externalImport });
456
+ for (const module of discoveryModules.values()) {
504
457
  if (
505
458
  /(^|\/)(__tests__|tests?|stories)(\/|$)|\.(test|spec|stories)\./.test(
506
459
  module.file,
@@ -517,7 +470,7 @@ export function frameworkRoutes({
517
470
  ["react-router", "react-router-dom"].includes(imported?.specifier) &&
518
471
  ["createBrowserRouter", "createHashRouter"].includes(imported.name)
519
472
  ) {
520
- if (++routers > 1)
473
+ if (++routers > 1 || tanstack)
521
474
  throw Error(
522
475
  "Multiple React route authorities require an explicit adapter",
523
476
  );
@@ -559,7 +512,7 @@ export function frameworkRoutes({
559
512
  ["react-router", "react-router-dom"].includes(imported?.specifier) &&
560
513
  imported.name === "Routes"
561
514
  ) {
562
- if (++routers > 1)
515
+ if (++routers > 1 || tanstack)
563
516
  throw Error(
564
517
  "Multiple React route authorities require an explicit adapter",
565
518
  );
@@ -571,9 +524,9 @@ export function frameworkRoutes({
571
524
  }
572
525
  visit(module.source);
573
526
  }
574
- if (!routers) {
527
+ if (!routers && !tanstack) {
575
528
  let roots = 0;
576
- for (const module of modules.values()) {
529
+ for (const module of discoveryModules.values()) {
577
530
  if (
578
531
  /(^|\/)(__tests__|tests?|stories)(\/|$)|\.(test|spec|stories)\./.test(
579
532
  module.file,
@@ -609,7 +562,7 @@ export function frameworkRoutes({
609
562
  }
610
563
  }
611
564
  return {
612
- framework: routers ? "react-router" : "react-spa",
565
+ framework: tanstack ? "tanstack-router" : routers ? "react-router" : "react-spa",
613
566
  entries,
614
567
  pageFiles: [...new Set(entries.map((e) => e.sourceFile))],
615
568
  unsupported: [],
package/src/generate.mjs CHANGED
@@ -2,7 +2,8 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { analyze } from "./analyze.mjs";
4
4
  import { validate } from "./validate.mjs";
5
- import { sha256 } from "./integrity.mjs";
5
+ import { documentDom, packDocumentDom, assertDomBudget } from "./dom-payload.mjs";
6
+ import { sha256, sealDocument } from "./integrity.mjs";
6
7
  import { attachBuildStyles, shareDocumentStyles } from "./build-styles.mjs";
7
8
  import { releasePath } from "./release-metadata.mjs";
8
9
  import { shareDocumentScripts, documentScript } from "./shared-scripts.mjs";
@@ -11,9 +12,9 @@ import { refreshRenderHashes } from "./render-index.mjs";
11
12
  export const IAM_MAX_BYTES = 8_388_608;
12
13
 
13
14
  /** Scans the source checkout. The result is the input of {@link generateAppManifest}. */
14
- export async function analyzeSource({ root, name, repository = "", commit = "", navigationPaths = [], automaticAnchors = false }) {
15
+ export async function analyzeSource({ root, name, repository = "", commit = "", navigationPaths = [], entry, automaticAnchors = false }) {
15
16
  root = await fs.realpath(root);
16
- return analyze({ root, name: name || path.basename(root), repository, commit, navigationPaths, automaticAnchors });
17
+ return analyze({ root, name: name || path.basename(root), repository, commit, navigationPaths, entry, automaticAnchors });
17
18
  }
18
19
 
19
20
  /**
@@ -27,6 +28,7 @@ export async function generateAppManifest({
27
28
  repository,
28
29
  commit,
29
30
  navigationPaths,
31
+ entry,
30
32
  automaticAnchors = false,
31
33
  releaseId,
32
34
  buildCss = [],
@@ -39,7 +41,12 @@ export async function generateAppManifest({
39
41
  root = await fs.realpath(root);
40
42
  const manifest = analysis
41
43
  ? structuredClone(analysis)
42
- : await analyzeSource({ root, name, repository, commit, navigationPaths, automaticAnchors });
44
+ : await analyzeSource({ root, name, repository, commit, navigationPaths, entry, automaticAnchors });
45
+ for (const document of manifest.documents) if (document.domPayload) {
46
+ Object.assign(document, documentDom(document));
47
+ delete document.domPayload;
48
+ sealDocument(document);
49
+ }
43
50
  if (releaseId) {
44
51
  releasePath(releaseId);
45
52
  manifest.release.runtimeBinding = { schemaVersion: 1, releaseId };
@@ -57,6 +64,9 @@ export async function generateAppManifest({
57
64
  else if (rootFont) throw new Error("rootFont requires build CSS");
58
65
  shareDocumentStyles(manifest);
59
66
  shareDocumentScripts(manifest);
67
+ assertDomBudget(manifest.documents);
68
+ if (Buffer.byteLength(JSON.stringify(manifest)) > 4_194_304)
69
+ for (const document of manifest.documents) if (packDocumentDom(document)) sealDocument(document);
60
70
  refreshRenderHashes(manifest);
61
71
  const validation = await validate(manifest);
62
72
  const serialized = JSON.stringify(manifest) + "\n",
@@ -83,15 +93,16 @@ export async function writeAppManifestOutput({ root, out, result }) {
83
93
  await fs.writeFile(path.join(out, "styles", style.styleKey + ".css"), style.css);
84
94
  }
85
95
  for (const document of manifest.documents) {
96
+ const decoded = documentDom(document);
86
97
  const folder = path.join(out, "documents", document.documentKey);
87
98
  await fs.mkdir(folder, { recursive: true });
88
99
  await Promise.all([
89
- fs.writeFile(path.join(folder, "page.html"), document.html),
100
+ fs.writeFile(path.join(folder, "page.html"), decoded.html),
90
101
  fs.writeFile(path.join(folder, "page.css"), document.css),
91
102
  fs.writeFile(path.join(folder, "page.js"), documentScript(manifest, document)),
92
103
  fs.writeFile(
93
104
  path.join(folder, "dom-map.json"),
94
- JSON.stringify({ rootNodeId: document.rootNodeId, nodes: document.nodes, hashes: document.hashes }, null, 2) + "\n",
105
+ JSON.stringify({ rootNodeId: document.rootNodeId, nodes: decoded.nodes, hashes: document.hashes }, null, 2) + "\n",
95
106
  ),
96
107
  ]);
97
108
  }
package/src/index.d.ts CHANGED
@@ -20,6 +20,8 @@ export type ValidationCounts = Record<string, unknown>;
20
20
 
21
21
  export type GenerateOptions = {
22
22
  root: string;
23
+ /** Browser entry relative to root. Defaults to the single module script in index.html. */
24
+ entry?: string;
23
25
  automaticAnchors?: boolean;
24
26
  /** A previous {@link analyzeSource} result; scanned again when omitted. */
25
27
  analysis?: AppManifest;
@@ -47,6 +49,8 @@ export type GeneratedAppManifest = {
47
49
  export declare const IAM_MAX_BYTES: 8388608;
48
50
  export declare function analyzeSource(options: {
49
51
  root: string;
52
+ /** Browser entry relative to root. Defaults to the single module script in index.html. */
53
+ entry?: string;
50
54
  automaticAnchors?: boolean;
51
55
  name?: string;
52
56
  repository?: string;
@@ -82,6 +86,8 @@ export type BuildLogger = Pick<Console, "log" | "warn">;
82
86
  export type BuildReleaseOptions = {
83
87
  strict?: boolean;
84
88
  logger?: BuildLogger;
89
+ /** Browser entry relative to root. Defaults to the single module script in index.html. */
90
+ entry?: string;
85
91
  automaticAnchors?: boolean;
86
92
  };
87
93
  export declare function prepareBuildRelease(
@@ -114,10 +120,13 @@ export declare function finishBuildRelease(
114
120
  } | null>;
115
121
 
116
122
  export declare const DEFAULT_BASE_URL: "https://tinker.cash";
123
+ export declare const DEFAULT_CLIENT: "web";
117
124
  export declare function uploadAppManifest(options: {
118
125
  serialized: string;
119
126
  digest: string;
120
127
  label: string;
128
+ /** Defaults to `web`; each client of an App environment has its own current version. */
129
+ client?: string;
121
130
  apiKey: string;
122
131
  baseUrl?: string;
123
132
  fetch?: typeof globalThis.fetch;
@@ -133,9 +142,10 @@ export declare function uploadSettings(
133
142
  apiKey?: string;
134
143
  baseUrl?: string;
135
144
  upload?: "production" | "always" | false | true;
145
+ client?: string;
136
146
  },
137
147
  env?: Record<string, string | undefined>,
138
- ): { enabled: boolean; apiKey: string; baseUrl: string; missingKey: boolean };
148
+ ): { enabled: boolean; apiKey: string; baseUrl: string; client: string; missingKey: boolean };
139
149
  export declare function buildLabel(
140
150
  root: string,
141
151
  env?: Record<string, string | undefined>,
package/src/index.mjs CHANGED
@@ -6,4 +6,4 @@ export { nextStaticMounts } from "./build-styles.mjs";
6
6
  export { transformSourceForIam } from "./transform.mjs";
7
7
  export { rebindIamSource } from "./rebind.mjs";
8
8
  export { planAnchors, applyAnchors } from "./instrument.mjs";
9
- export { uploadAppManifest, uploadSettings, buildLabel, DEFAULT_BASE_URL } from "./upload.mjs";
9
+ export { uploadAppManifest, uploadSettings, buildLabel, DEFAULT_BASE_URL, DEFAULT_CLIENT } from "./upload.mjs";
package/src/integrity.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { documentDom } from "./dom-payload.mjs";
2
3
 
3
4
  export const sha256 = (bytes) =>
4
5
  createHash("sha256").update(bytes).digest("hex");
@@ -70,13 +71,14 @@ export function sealNodes(nodes, rootId) {
70
71
  }
71
72
  export function sealDocument(document) {
72
73
  const { hashes: _hashes, ...body } = document;
74
+ const dom = documentDom(document);
73
75
  document.hashes = {
74
- html: sha256(document.html),
76
+ html: sha256(dom.html),
75
77
  css: sha256(document.css),
76
78
  js: sha256(document.js),
77
79
  dom: hashObject("iam/dom-map/v1", {
78
80
  rootNodeId: document.rootNodeId,
79
- nodes: document.nodes,
81
+ nodes: dom.nodes,
80
82
  ...(document.sourceMappingsHash?{sourceMappingsHash:document.sourceMappingsHash}:{}),
81
83
  }),
82
84
  document: hashObject("iam/document/v1", body),
@@ -8,6 +8,7 @@ import{renderGuardFor}from'./render-index.mjs';
8
8
  import { documentScript } from "./shared-scripts.mjs";
9
9
  import { documentNodes } from './node-hashes.mjs';
10
10
  export { documentNodes as iamDocumentNodes } from './node-hashes.mjs';
11
+ export { documentDom as iamDocumentDom } from './dom-payload.mjs';
11
12
  export { rebindIamSource } from './rebind.mjs';
12
13
  export const parseIamJson = parseManifest;
13
14
  export async function isReviewedIamScript(value) {
package/src/next.mjs CHANGED
@@ -17,12 +17,12 @@ const sourceLoader = fileURLToPath(new URL("./next-loader.cjs", import.meta.url)
17
17
  * for production deployments). Development servers and other phases get the config unchanged.
18
18
  */
19
19
  export function withIndianaIam(nextConfig = {}, options = {}) {
20
- const { enabled = true, root, name, automaticAnchors = true, strict = false, rootFont, omitSvgResources, logger = console, apiKey, baseUrl, upload, fetch } = options;
20
+ const { enabled = true, root, name, entry, automaticAnchors = true, strict = false, rootFont, omitSvgResources, logger = console, apiKey, baseUrl, upload, client, fetch } = options;
21
21
  return async (phase, context) => {
22
22
  const config = typeof nextConfig === "function" ? await nextConfig(phase, context) : nextConfig;
23
23
  if (phase !== PRODUCTION_BUILD || !enabled) return config;
24
24
  const directory = path.resolve(root ?? process.cwd());
25
- const prepared = await prepareBuildRelease({ root: directory, name, automaticAnchors, strict, logger });
25
+ const prepared = await prepareBuildRelease({ root: directory, name, entry, automaticAnchors, strict, logger });
26
26
  if (!prepared) return config;
27
27
  const { releaseId } = prepared;
28
28
  const previous = config.compiler?.runAfterProductionCompile;
@@ -56,13 +56,14 @@ export function withIndianaIam(nextConfig = {}, options = {}) {
56
56
  root: build.projectDir,
57
57
  releaseId,
58
58
  name,
59
+ entry,
59
60
  automaticAnchors,
60
61
  cssDirectory: distDir + "/static",
61
62
  mounts: nextStaticMounts(distDir, config.basePath ?? ""),
62
63
  publicDirectory: "public",
63
64
  rootFont,
64
65
  omitSvgResources,
65
- upload: { apiKey, baseUrl, upload, fetch },
66
+ upload: { apiKey, baseUrl, upload, client, fetch },
66
67
  strict,
67
68
  logger,
68
69
  });
@@ -1,3 +1,4 @@
1
+ import { documentDom } from "./dom-payload.mjs";
1
2
  import { canonical, sealNodes, sealDocument } from './integrity.mjs';
2
3
  import { dependency } from './dependencies.mjs';
3
4
  import { sourceMappingsHash } from './compact-dom.mjs';
@@ -20,8 +21,9 @@ export function expandDomNodes(document, body) {
20
21
  return result;
21
22
  }
22
23
  function domNodes(document){
23
- const {JSDOM}=dependency('jsdom').value,dom=new JSDOM('<!doctype html><html><body>'+document.html+'</body></html>');
24
- try{return expandDomNodes(document,dom.window.document.body);}finally{dom.window.close();}
24
+ const decoded = {...document, ...documentDom(document)};
25
+ const {JSDOM}=dependency('jsdom').value,dom=new JSDOM('<!doctype html><html><body>'+decoded.html+'</body></html>');
26
+ try{return expandDomNodes(decoded,dom.window.document.body);}finally{dom.window.close();}
25
27
  }
26
28
 
27
29
  /** Node hashes are pure derived values. IDs, source mappings and algorithms never change. */
@@ -32,6 +34,7 @@ export function documentNodes(document) {
32
34
  }
33
35
  /** Serialize the producer's in-memory DOM into the single admitted wire representation. */
34
36
  export function storeDocumentNodes(document, nodes, elements) {
37
+ delete document.domPayload;
35
38
  document.sourceMappingsHash = sourceMappingsHash(nodes, elements);
36
39
  document.nodeEncoding = 'html-preorder-v1';
37
40
  document.nodeHashMode = 'derived-v1';
@@ -5,7 +5,7 @@ export function flattenComposition(root) {
5
5
  const entries = [];
6
6
  function visit(branch) {
7
7
  if (!branch) return;
8
- entries.push({ lane: branch.lane, file: branch.file, layers: branch.layers.map(layer => ({ file: layer.file, slots: layer.slots.map(slot => ({ name: slot.name, lane: slot.branch?.lane ?? null })) })) });
8
+ entries.push({ lane: branch.lane, file: branch.file, ...(branch.symbol ? { symbol: branch.symbol } : {}), layers: branch.layers.map(layer => ({ file: layer.file, ...(layer.symbol ? { symbol: layer.symbol } : {}), slots: layer.slots.map(slot => ({ name: slot.name, lane: slot.branch?.lane ?? null })) })) });
9
9
  for (const layer of branch.layers) for (const slot of layer.slots) visit(slot.branch);
10
10
  }
11
11
  visit(root);
@@ -36,6 +36,7 @@ export function renderTreeSchema(base) {
36
36
  p.renderNavigation=array(object({fromRenderKey:str,toRenderKey:str}),512);
37
37
  p.guards = array(object({ routeKey: str, selectorKeys: array({type:"integer",minimum:0,maximum:511}, 512) }), 200);
38
38
  p.compositions = array(object({ compositionKey: str, entries: array(object({ lane: str, file: str, layers: array(object({ file: str, slots: array(object({ name: str, lane: nullable(str) }), 32) }), 32) }), 128) }), 1000);
39
+ for (const shape of [p.compositions.items.properties.entries.items, p.compositions.items.properties.entries.items.properties.layers.items]) shape.properties.symbol = str;
39
40
  schema.required.push("renders", "guards", "compositions", "selectorPool", "renderNavigation");
40
41
  return schema;
41
42
  }
@@ -85,7 +86,7 @@ export function attachRenderIndex(graph, manifest, definitions, navigation=[]) {
85
86
  }
86
87
 
87
88
  export function validateRenderIndex(graph, manifest) {
88
- const contained = (file, lane) => lane === "." && ["react-router", "react-spa"].includes(manifest.routing.framework)
89
+ const contained = (file, lane) => lane === "." && ["react-router", "react-spa", "tanstack-router"].includes(manifest.routing.framework)
89
90
  ? !file.startsWith("/") && !file.split("/").includes("..")
90
91
  : file.startsWith(lane + "/");
91
92
  const unique = (rows, key) => {
@@ -95,14 +96,15 @@ export function validateRenderIndex(graph, manifest) {
95
96
  };
96
97
  const renders = unique(graph.renders, "renderKey"), compositions = unique(graph.compositions, "compositionKey"), guards = unique(graph.guards, "routeKey");
97
98
  const files = new Set(manifest.sourceFiles.map(file => file.file));
99
+ const knownSymbol = entry => !entry.symbol || manifest.components.some(component => component.file === entry.file && component.name === entry.symbol);
98
100
  for (const composition of compositions.values()) {
99
101
  if (composition.compositionKey !== compositionIdentity(composition.entries)) throw new Error("Composition identity mismatch");
100
102
  const lanes = new Set();
101
103
  for (const entry of composition.entries) {
102
- if (lanes.has(entry.lane) || !files.has(entry.file) || !contained(entry.file, entry.lane)) throw new Error("Invalid composition entry");
104
+ if (lanes.has(entry.lane) || !files.has(entry.file) || !contained(entry.file, entry.lane) || !knownSymbol(entry)) throw new Error("Invalid composition entry");
103
105
  lanes.add(entry.lane);
104
106
  for (const layer of entry.layers) {
105
- if (!files.has(layer.file) || !contained(layer.file, entry.lane)) throw new Error("Invalid composition layout");
107
+ if (!files.has(layer.file) || !contained(layer.file, entry.lane) || !knownSymbol(layer)) throw new Error("Invalid composition layout");
106
108
  const folder = layer.file.slice(0, layer.file.lastIndexOf("/"));
107
109
  for (const slot of layer.slots) if (slot.lane !== null && slot.lane !== folder + "/@" + slot.name) throw new Error("Invalid composition slot");
108
110
  }
package/src/routing.mjs CHANGED
@@ -26,7 +26,7 @@ export function pagesRoute(file) {
26
26
 
27
27
  /** Router configuration provenance is explicit for source-declared React routes. */
28
28
  export function sourcePageRoute(page, routing) {
29
- if (["react-router", "react-spa"].includes(routing.framework)) {
29
+ if (["react-router", "react-spa", "tanstack-router"].includes(routing.framework)) {
30
30
  const rows = routing.routeSources?.filter(
31
31
  (row) =>
32
32
  row.path === page.route &&
@@ -39,7 +39,7 @@ export const schema = {
39
39
  platforms: array({ enum: ["web", "ios", "android"] }, 3),
40
40
  }),
41
41
  routing: object({
42
- framework: { enum: ["next-app-router", "next-pages-router", "react-router", "react-spa"] },
42
+ framework: { enum: ["next-app-router", "next-pages-router", "react-router", "react-spa", "tanstack-router"] },
43
43
  basePath: str,
44
44
  trailingSlash: { type: "boolean" },
45
45
  routeSources: array(object({ path: str, source, componentFile: str, componentSymbol: str }), 200),
@@ -176,3 +176,10 @@ schema.properties.styles=array(object({styleKey:{type:'string',pattern:'^style_[
176
176
  schema.properties.documents.items.properties.styleKeys=array({type:'string',pattern:'^style_[a-f0-9]{64}$'},16);
177
177
  schema.properties.scripts=array(object({scriptKey:{type:'string',pattern:'^script_[a-f0-9]{64}$'},sha256:hash,js:{type:'string',maxLength:100000}}),8);
178
178
  schema.properties.documents.items.properties.scriptKey={type:'string',pattern:'^script_[a-f0-9]{64}$'};
179
+
180
+ // HTML and preorder mappings may use bounded lossless storage compression.
181
+ schema.properties.documents.items.properties.domPayload = object({
182
+ encoding: { const: "gzip-json-v1" },
183
+ bytes: { type: "integer", minimum: 1, maximum: 8_388_608 },
184
+ data: { type: "string", minLength: 1, maxLength: 11_184_812 },
185
+ });