@tinkerlandai/indiana-iam 0.3.0 → 0.4.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.
@@ -14,7 +14,7 @@ export type IndianaIamBuildOptions = {
14
14
  strict?: boolean;
15
15
  /** Font family declared by the compiled CSS that renders the root element. */
16
16
  rootFont?: string;
17
- /** Drop inline SVG resources from compiled CSS instead of skipping the CSS. */
17
+ /** Drop inline SVG resources with a coverage gap instead of skipping the CSS (default: true). */
18
18
  omitSvgResources?: boolean;
19
19
  logger?: BuildLogger;
20
20
  /** Key holding only app-manifests:write; defaults to `INDIANA_IAM_KEY`. Without one the IAM is not uploaded. */
@@ -1,10 +1,13 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { expandDomNodes } from './node-hashes.mjs';
3
+ import {applySourceAttributes} from './source-attributes.mjs';
4
+ import {applySourceCollections,markUnobservedData,checkedCollectionFragment} from './source-collections.mjs';
3
5
  // Only these two reviewed local assets form the executable preview runtime.
4
6
  const machine = await fs.readFile(new URL("../assets/state-machine.js", import.meta.url), "utf8");
7
+ await fs.writeFile(new URL('./ui-state-machine.mjs', import.meta.url), '/* Generated from assets/state-machine.js by build-runtime.mjs. */\n'+machine);
5
8
  const controller = await fs.readFile(new URL("../assets/document-controller.js", import.meta.url), "utf8");
6
9
  if (!machine.includes("export function createStateMachine(")) throw new Error("Unexpected state runtime export");
7
- await fs.writeFile(new URL("../assets/document-runtime.js", import.meta.url), "/* Generated by scripts/build-runtime.mjs. */\n(() => {\n" + machine.replace("export function createStateMachine(", "function createStateMachine(") + "\n" + controller + "\n})();\n");
10
+ await fs.writeFile(new URL("../assets/document-runtime.js", import.meta.url), "/* Generated by scripts/build-runtime.mjs. */\n(() => {\n" + machine.replace("export function createStateMachine(", "function createStateMachine(") + "\n" + [checkedCollectionFragment,applySourceAttributes,applySourceCollections,markUnobservedData].map(fn=>fn.toString()).join('\n')+'\n'+ controller + "\n})();\n");
8
11
  const viewerUrl=new URL('../assets/viewer.js',import.meta.url),viewer=await fs.readFile(viewerUrl,'utf8');
9
12
  if(!viewer.includes('/* BEGIN HTML DOM DERIVATION */'))throw Error('Missing DOM derivation asset boundary');
10
13
  await fs.writeFile(viewerUrl,viewer.replace(/\/\* BEGIN HTML DOM DERIVATION \*\/[\s\S]*?\/\* END HTML DOM DERIVATION \*\//,'/* BEGIN HTML DOM DERIVATION */\n'+expandDomNodes.toString()+'\n /* END HTML DOM DERIVATION */'));
@@ -30,8 +30,8 @@ function checkedMounts(mounts) {
30
30
  }
31
31
 
32
32
  /** Explicit public build inputs only. Never runs builds, reads env, downloads URLs or imports App code. */
33
- export async function attachBuildStyles(manifest, { root, files = [], publicFiles = [], optimizePublicImages = false, rootFont = "", omitSvgResources = false, mounts = nextStaticMounts() }) {
34
- if (!Array.isArray(files) || files.length > 16 || !Array.isArray(publicFiles) || publicFiles.length > 128 || (!files.length && !publicFiles.length)) throw new Error("Choose up to 16 CSS files and 128 public assets");
33
+ export async function attachBuildStyles(manifest, { root, files = [], publicFiles = [], referencedImages = false, optimizePublicImages = false, rootFont = "", omitSvgResources = false, mounts = nextStaticMounts() }) {
34
+ if (!Array.isArray(files) || files.length > 16 || !Array.isArray(publicFiles) || publicFiles.length > 128 || (!files.length && !publicFiles.length && !referencedImages)) throw new Error("Choose up to 16 CSS files and 128 public assets");
35
35
  if (rootFont && !/^[A-Za-z][A-Za-z0-9 _-]{0,63}$/.test(rootFont)) throw new Error("Invalid root font family");
36
36
  root = await fs.realpath(root);
37
37
  const cssTree = dependency("css-tree").value;
@@ -45,9 +45,9 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
45
45
  const fileOf = pathname => { const mount = [...served].sort((a, b) => b.urlPath.length - a.urlPath.length).find(m => pathname.startsWith(m.urlPath)); if (!mount) throw new Error("CSS asset is outside the public build directories"); return mount.directory + "/" + pathname.slice(mount.urlPath.length); };
46
46
  const inputs = new Map(), assets = new Map(manifest.assets.map(a => [a.assetKey, a]));
47
47
  let total = 0, publicSourceBytes = 0;
48
- async function read(file, publicImage = false) {
48
+ async function read(file, publicImage = false, sourceImage = false) {
49
49
  const absolute = path.resolve(root, file), real = await fs.realpath(absolute);
50
- if ((await fs.lstat(absolute)).isSymbolicLink() || !allowed.some(base => real.startsWith(base + path.sep))) throw new Error("Asset must be inside a real public build directory");
50
+ if ((await fs.lstat(absolute)).isSymbolicLink() || !(sourceImage ? real.startsWith(root+path.sep) : allowed.some(base => real.startsWith(base + path.sep)))) throw new Error("Asset must be inside a real public build directory");
51
51
  const relative = path.relative(root, real).split(path.sep).join("/");
52
52
  if (inputs.has(relative)) return inputs.get(relative);
53
53
  const stat = await fs.stat(real);
@@ -113,14 +113,50 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
113
113
  assets.set(key, {assetKey:key, sourceUrl, mime, bytes:bytes.length, sha256:digest, dataUrl:'data:'+mime+';base64,'+bytes.toString('base64')});
114
114
  }
115
115
  }
116
+ if(referencedImages){
117
+ const urls=[...new Set(manifest.documents.flatMap(doc=>documentNodes(doc).map(n=>n.attributes?.src).filter(v=>typeof v==='string'&&/^\/(?!\/)[A-Za-z0-9_./-]+\.(?:png|jpe?g|webp|gif)$/i.test(v))))].sort();
118
+ if(urls.length>128)throw Error('Referenced image budget exceeded');
119
+ for(const url of urls){
120
+ if(assets.size>=128)throw Error('Referenced image budget exceeded');
121
+ if([...assets.values()].some(a=>a.sourceUrl===url))continue;
122
+ const source=url.startsWith('/__iam_source__/'),file=source?url.slice('/__iam_source__/'.length):fileOf(url);
123
+ if(file.split('/').some(p=>!p||p==='..'||source&&p.startsWith('.'))||/(?:^|\/)(?:api|server|secrets?|credentials)(?:\/|\.)/i.test(file))continue;
124
+ try{
125
+ const input=await read(file,false,source);let bytes=input.bytes,mime=types.get(path.extname(file).toLowerCase());
126
+ const valid=mime==='image/png'?bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])):mime==='image/jpeg'?bytes[0]===255&&bytes[1]===216&&bytes[2]===255:mime==='image/webp'?bytes.subarray(0,4).toString()==='RIFF'&&bytes.subarray(8,12).toString()==='WEBP':/^GIF8[79]a$/.test(bytes.subarray(0,6).toString());
127
+ if(!valid)throw Error('Referenced raster signature mismatch');
128
+ if(bytes.length>65536){
129
+ const sharp=dependency('sharp').value;if(sharp.versions.sharp!=='0.35.5')throw Error('Pinned image encoder required');
130
+ const image=sharp(bytes,{limitInputPixels:16777216,failOn:'warning'}),metadata=await image.metadata();if((metadata.pages||1)!==1)throw Error('Animated source images require an explicit adapter');
131
+ bytes=await image.rotate().resize({width:800,height:800,fit:'inside',withoutEnlargement:true}).webp({quality:90,effort:4}).toBuffer();mime='image/webp';
132
+ manifest.coverage.gaps.push({code:'REFERENCED_IMAGE_PREVIEW_OPTIMIZED',file,detail:'Build preview profile for rasters over 64 KiB: auto-orient, fit inside 800x800, WebP quality 90; source bytes remain hashed. Lossy preview, not original pixels.'});
133
+ }
134
+ const digest=sha256(bytes),assetKey=identity('asset',{sourceUrl:url,mime,sha256:digest});assets.set(assetKey,{assetKey,sourceUrl:url,mime,bytes:bytes.length,sha256:digest,dataUrl:'data:'+mime+';base64,'+bytes.toString('base64')});
135
+ }catch(error){
136
+ // A missing reference is a fidelity gap. Unsafe files/budgets fail the packaging step.
137
+ if(error.code!=='ENOENT')throw error;
138
+ manifest.coverage.gaps.push({code:'REFERENCED_IMAGE_MISSING',file,detail:'Referenced raster is absent from the local public build; no network fetch performed.'});
139
+ }
140
+ }
141
+ }
116
142
  if (optimizePublicImages && publicFiles.length) manifest.coverage.gaps.push({code:'PUBLIC_IMAGES_OPTIMIZED', file:publicFiles[0], detail:'Explicit sharp 0.35.5 profile: auto-orient, fit inside 800x800 without enlargement, WebP quality 90 effort 4; metadata stripped. Source bytes hashed independently; not pixel-identical originals.'});
117
143
  const sheets = [], families = new Set();
118
144
  for (const file of [...new Set(files)].sort()) {
119
145
  if (!file.endsWith(".css")) throw new Error("Compiled CSS file required");
120
146
  const input = await read(file), ast = cssTree.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes), { parseCustomProperty: true });
121
147
  const urls = [];
148
+ // A build may retain a remote font import. Keep its local stylesheet, but
149
+ // never fetch or copy the external dependency into an offline projection.
150
+ cssTree.walk(ast, { visit: "Atrule", enter(node, item, list) {
151
+ if (node.name.toLowerCase() !== "import") return;
152
+ const target = node.prelude?.children?.first;
153
+ if (!["String", "Url"].includes(target?.type) || !/^(?:https?:)?\/\//i.test(target.value))
154
+ throw new Error("Precompile CSS imports before packaging");
155
+ list.remove(item);
156
+ if (!manifest.coverage.gaps.some(g => g.code === "BUILD_EXTERNAL_STYLESHEET_OMITTED" && g.file === input.relative))
157
+ manifest.coverage.gaps.push({ code: "BUILD_EXTERNAL_STYLESHEET_OMITTED", file: input.relative, detail: "External CSS import omitted without network access; local compiled styles retained. Remote fonts or rules remain unavailable." });
158
+ }});
122
159
  cssTree.walk(ast, node => {
123
- if (node.type === "Atrule" && node.name.toLowerCase() === "import") throw new Error("Precompile CSS imports before packaging");
124
160
  if (node.type === "Atrule" && node.name.toLowerCase() === "font-face") node.block?.children.forEach(declaration => { if (declaration.type === "Declaration" && declaration.property === "font-family") families.add(cssTree.generate(declaration.value).replace(/^["']|["']$/g, "")); });
125
161
  if (node.type === "Url") urls.push(node);
126
162
  });
@@ -128,7 +164,7 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
128
164
  sheets.push(cssTree.generate(ast));
129
165
  }
130
166
  if (rootFont && !families.has(rootFont)) throw new Error("Root font must exist in compiled font faces");
131
- const css = sheets.join("\n") + "\n.iam-page{min-height:100vh}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}" + (rootFont ? "\nbody{font-family:" + JSON.stringify(rootFont) + "," + JSON.stringify(rootFont + " Fallback") + ",sans-serif}" : "");
167
+ const css = sheets.join("\n") + "\n[hidden]{display:none!important}.iam-state{display:contents}.iam-page{min-height:100vh}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}" + (rootFont ? "\nbody{font-family:" + JSON.stringify(rootFont) + "," + JSON.stringify(rootFont + " Fallback") + ",sans-serif}" : "");
132
168
  if (/<\/(?:style|script)/i.test(css)) throw new Error("HTML closing tag in compiled CSS");
133
169
  if (files.length) for (const doc of manifest.documents) { doc.css = css; sealDocument(doc); }
134
170
  // One content-addressed image in the envelope; DOM instances carry references.
@@ -142,6 +178,13 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
142
178
  doc.html = doc.html.replaceAll('src="' + asset.sourceUrl + '"', 'src="' + reference + '"');
143
179
  }
144
180
  storeDocumentNodes(doc, nodes, new Map(manifest.elements.map(element => [element.elementKey, element])));
181
+ // Resource replacement changes element and ancestor hashes. Keep target
182
+ // fingerprints tied to the final document, never the pre-packaging HTML.
183
+ const finalNodes=documentNodes(doc);
184
+ for(const target of manifest.routeTree?.targets??[]){
185
+ if(target.routeKey!==doc.pageKey)continue;const node=finalNodes.find(n=>n.nodeId===target.nodeId);if(!node)continue;
186
+ target.contentHash=node.contentHash;if(target.subtreeHash!==undefined)target.subtreeHash=node.subtreeHash;
187
+ }
145
188
  }
146
189
  manifest.coverage.gaps = manifest.coverage.gaps.filter(g => g.code !== 'PUBLIC_ASSET_NOT_PACKAGED' || ![...assets.values()].some(a => g.detail.startsWith(a.sourceUrl + ':')));
147
190
  refreshRenderHashes(manifest);
package/src/build.mjs CHANGED
@@ -65,7 +65,7 @@ export async function finishBuildRelease({
65
65
  mounts,
66
66
  publicDirectory,
67
67
  rootFont = "",
68
- omitSvgResources = false,
68
+ omitSvgResources = true,
69
69
  upload = {},
70
70
  strict = false,
71
71
  logger = console,
@@ -75,7 +75,7 @@ export async function finishBuildRelease({
75
75
  const analysis = await analyzeSource({ root, name, entry, automaticAnchors });
76
76
  if (deriveReleaseId(analysis) !== releaseId) throw new Error("Source changed during the build; rebuild before publishing IAM");
77
77
  const css = cssDirectory ? await compiledCss(root, cssDirectory) : [];
78
- const plain = () => generateAppManifest({ root, analysis, releaseId });
78
+ const plain = () => generateAppManifest({ root, analysis, releaseId, referencedImages:true, mounts });
79
79
  let result;
80
80
  if (!css.length) result = await plain();
81
81
  else if (css.length > MAX_BUILD_CSS) {
@@ -83,7 +83,7 @@ export async function finishBuildRelease({
83
83
  result = await plain();
84
84
  } else
85
85
  try {
86
- result = await generateAppManifest({ root, analysis, releaseId, buildCss: css, mounts, rootFont, omitSvgResources });
86
+ result = await generateAppManifest({ root, analysis, releaseId, buildCss: css, referencedImages:true, mounts, rootFont, omitSvgResources });
87
87
  } catch (error) {
88
88
  report(logger, "warn", "compiled CSS skipped: " + (error instanceof Error ? error.message : String(error)));
89
89
  result = await plain();
@@ -0,0 +1,229 @@
1
+ /** Follow finite source selectors to their literal component registry, retaining alternatives as assumptions. */
2
+ export function componentRegistry({
3
+ ts,
4
+ node,
5
+ module,
6
+ locals,
7
+ binding,
8
+ functionFrom,
9
+ value,
10
+ component,
11
+ variable,
12
+ location,
13
+ unknown,
14
+ imported,
15
+ }) {
16
+ let steps = 0;
17
+ const choices = [],
18
+ variables = new Set(),
19
+ seen = new Set();
20
+ const unwrap = (n) => {
21
+ while (
22
+ n &&
23
+ (ts.isParenthesizedExpression(n) ||
24
+ ts.isAsExpression(n) ||
25
+ ts.isNonNullExpression(n) ||
26
+ ts.isSatisfiesExpression(n))
27
+ )
28
+ n = n.expression;
29
+ return n;
30
+ };
31
+ const localEnvironment = (fn, m, parent) => {
32
+ const env = new Map(parent);
33
+ if (ts.isBlock(fn.body))
34
+ for (const s of fn.body.statements)
35
+ if (ts.isVariableStatement(s))
36
+ for (const d of s.declarationList.declarations)
37
+ if (ts.isIdentifier(d.name))
38
+ env.set(d.name.text, { node: d.initializer, module: m, env });
39
+ return env;
40
+ };
41
+ const returns = (fn) =>
42
+ ts.isBlock(fn.body)
43
+ ? fn.body.statements
44
+ .filter((s) => ts.isReturnStatement(s))
45
+ .map((s) => s.expression)
46
+ : [fn.body];
47
+ function walk(raw, m, env, path = [], record = null, depth = 0) {
48
+ const n = unwrap(raw);
49
+ if (!n || depth > 24 || ++steps > 3000 || choices.length > 32) return;
50
+ if (!path.length && !(ts.isIdentifier(n) && env.has(n.text))) {
51
+ const c = component(n, m);
52
+ if (c) {
53
+ const optionValue =
54
+ typeof record?.key === "string" ? record.key : c.symbol;
55
+ choices.push({
56
+ component: c,
57
+ value: optionValue,
58
+ label:
59
+ typeof record?.label === "string"
60
+ ? record.label
61
+ : typeof record?.labelKey === "string"
62
+ ? record.labelKey
63
+ : c.symbol,
64
+ source: location(m, n, c.symbol),
65
+ scope: typeof record?.scope === "string" ? record.scope : null,
66
+ });
67
+ return;
68
+ }
69
+ }
70
+ const identity = m.file + ":" + n.pos + ":" + n.end + ":" + path.join("/");
71
+ if (seen.has(identity)) return;
72
+ seen.add(identity);
73
+ if (ts.isIdentifier(n)) {
74
+ const local = env.get(n.text);
75
+ if (local) {
76
+ walk(local.node, local.module, local.env, path, record, depth + 1);
77
+ return;
78
+ }
79
+ const ref = binding(m, n.text);
80
+ if (ref) walk(ref.node, ref.module, new Map(), path, record, depth + 1);
81
+ return;
82
+ }
83
+ if (ts.isPropertyAccessExpression(n)) {
84
+ walk(n.expression, m, env, [n.name.text, ...path], record, depth + 1);
85
+ return;
86
+ }
87
+ if (ts.isElementAccessExpression(n)) {
88
+ const key = value(n.argumentExpression, m);
89
+ walk(
90
+ n.expression,
91
+ m,
92
+ env,
93
+ [
94
+ typeof key === "number" || typeof key === "string"
95
+ ? String(key)
96
+ : "[]",
97
+ ...path,
98
+ ],
99
+ record,
100
+ depth + 1,
101
+ );
102
+ return;
103
+ }
104
+ if (
105
+ ts.isBinaryExpression(n) &&
106
+ [ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken].includes(
107
+ n.operatorToken.kind,
108
+ )
109
+ ) {
110
+ walk(n.left, m, env, path, record, depth + 1);
111
+ walk(n.right, m, env, path, record, depth + 1);
112
+ return;
113
+ }
114
+ if (ts.isConditionalExpression(n)) {
115
+ walk(n.whenTrue, m, env, path, record, depth + 1);
116
+ walk(n.whenFalse, m, env, path, record, depth + 1);
117
+ return;
118
+ }
119
+ if (ts.isArrayLiteralExpression(n)) {
120
+ const [key, ...rest] = path;
121
+ if (key === "[]")
122
+ for (const item of n.elements)
123
+ walk(item, m, env, rest, record, depth + 1);
124
+ else if (/^\d+$/.test(key ?? "") && n.elements[Number(key)])
125
+ walk(n.elements[Number(key)], m, env, rest, record, depth + 1);
126
+ return;
127
+ }
128
+ if (ts.isObjectLiteralExpression(n) && path.length) {
129
+ const [key, ...rest] = path,
130
+ fields = n.properties.filter(
131
+ (p) =>
132
+ ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p),
133
+ );
134
+ const field = fields.find((p) => p.name.text === key);
135
+ if (!field) return;
136
+ const meta = Object.fromEntries(
137
+ fields.flatMap((p) => {
138
+ if (!["key", "label", "labelKey", "scope"].includes(p.name.text))
139
+ return [];
140
+ const v = value(
141
+ ts.isPropertyAssignment(p) ? p.initializer : p.name,
142
+ m,
143
+ );
144
+ return typeof v === "string" ? [[p.name.text, v]] : [];
145
+ }),
146
+ );
147
+ walk(
148
+ ts.isPropertyAssignment(field) ? field.initializer : field.name,
149
+ m,
150
+ env,
151
+ rest,
152
+ Object.keys(meta).length ? meta : record,
153
+ depth + 1,
154
+ );
155
+ return;
156
+ }
157
+ if (ts.isCallExpression(n)) {
158
+ if (
159
+ ts.isPropertyAccessExpression(n.expression) &&
160
+ ["find", "filter"].includes(n.expression.name.text)
161
+ ) {
162
+ const fn = unwrap(n.arguments[0]);
163
+ if (
164
+ n.expression.name.text === "find" &&
165
+ fn &&
166
+ ts.isArrowFunction(fn) &&
167
+ ts.isBinaryExpression(unwrap(fn.body))
168
+ ) {
169
+ const comparison = unwrap(fn.body);
170
+ if (
171
+ comparison.operatorToken.kind ===
172
+ ts.SyntaxKind.EqualsEqualsEqualsToken
173
+ )
174
+ for (const side of [comparison.left, comparison.right])
175
+ if (ts.isIdentifier(side)) {
176
+ const key = variable(side.text);
177
+ if (key) variables.add(key);
178
+ }
179
+ }
180
+ walk(
181
+ n.expression.expression,
182
+ m,
183
+ env,
184
+ n.expression.name.text === "find" ? ["[]", ...path] : path,
185
+ record,
186
+ depth + 1,
187
+ );
188
+ return;
189
+ }
190
+ const callback = unwrap(n.arguments[0]);
191
+ if (
192
+ callback &&
193
+ (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) &&
194
+ imported(m, n.expression.getText(m.source))?.specifier === "react" &&
195
+ imported(m, n.expression.getText(m.source))?.name === "useMemo"
196
+ ) {
197
+ const local = localEnvironment(callback, m, env);
198
+ for (const returned of returns(callback))
199
+ walk(returned, m, local, path, record, depth + 1);
200
+ return;
201
+ }
202
+ if (ts.isIdentifier(n.expression)) {
203
+ const ref = binding(m, n.expression.text),
204
+ fn = ref && functionFrom(ref.node);
205
+ if (
206
+ fn &&
207
+ !fn.modifiers?.some((a) => a.kind === ts.SyntaxKind.AsyncKeyword)
208
+ ) {
209
+ const local = localEnvironment(fn, ref.module, new Map());
210
+ for (const returned of returns(fn))
211
+ walk(returned, ref.module, local, path, record, depth + 1);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ const env = new Map(
217
+ [...locals].map(([key, init]) => [key, { node: init, module, env: null }]),
218
+ );
219
+ for (const entry of env.values()) entry.env = env;
220
+ walk(node, module, env);
221
+ const options = [
222
+ ...new Map(
223
+ choices.map((c) => [c.value + "\0" + c.component.key, c]),
224
+ ).values(),
225
+ ];
226
+ return options.length > 1 && options.length <= 32
227
+ ? { options, variableKey: variables.size === 1 ? [...variables][0] : null }
228
+ : null;
229
+ }
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
 
3
3
  const require = createRequire(import.meta.url);
4
4
 
5
- /** Loads one of this package's pinned dependencies (sharp is an optional peer). */
5
+ /** Loads one of this package's pinned dependencies including the image encoder. */
6
6
  export function dependency(name) {
7
7
  try {
8
8
  return { value: require(name), resolve: (path) => require.resolve(path) };
@@ -13,7 +13,7 @@ export function dependency(name) {
13
13
  )
14
14
  throw error;
15
15
  throw new Error(
16
- `Missing ${name}. Reinstall @tinkerlandai/indiana-iam with its dependencies${name === "sharp" ? ", and install sharp 0.35.5 to optimize public images" : ""}.`,
16
+ `Missing ${name}. Reinstall @tinkerlandai/indiana-iam with its dependencies.`,
17
17
  );
18
18
  }
19
19
  }
@@ -1,4 +1,5 @@
1
1
  import { tanstackRoutes } from "./tanstack-routes.mjs";
2
+ import { routeLazyComponent } from "./route-lazy.mjs";
2
3
  import { pagesRoute } from "./routing.mjs";
3
4
  import { routeCompositions } from "./route-composition.mjs";
4
5
  import { compositionIdentity, flattenComposition } from "./render-index.mjs";
@@ -294,7 +295,8 @@ export function frameworkRoutes({
294
295
  properties.get("element") ?? properties.get("Component"),
295
296
  module,
296
297
  route,
297
- );
298
+ ) ?? (!properties.has("element") && !properties.has("Component") && properties.has("lazy")
299
+ ? routeLazyComponent(properties.get("lazy"), module, { ts, resolution, resolveModule }) : null);
298
300
  const index = properties.get("index")?.kind === ts.SyntaxKind.TrueKeyword;
299
301
  if (
300
302
  properties.has("index") &&
package/src/generate.mjs CHANGED
@@ -33,6 +33,7 @@ export async function generateAppManifest({
33
33
  releaseId,
34
34
  buildCss = [],
35
35
  publicAssets = [],
36
+ referencedImages = false,
36
37
  optimizePublicImages = false,
37
38
  rootFont = "",
38
39
  omitSvgResources = false,
@@ -51,11 +52,12 @@ export async function generateAppManifest({
51
52
  releasePath(releaseId);
52
53
  manifest.release.runtimeBinding = { schemaVersion: 1, releaseId };
53
54
  }
54
- if (buildCss.length || publicAssets.length)
55
+ if (buildCss.length || publicAssets.length || referencedImages)
55
56
  await attachBuildStyles(manifest, {
56
57
  root,
57
58
  files: buildCss,
58
59
  publicFiles: publicAssets,
60
+ referencedImages,
59
61
  optimizePublicImages,
60
62
  rootFont,
61
63
  omitSvgResources,
@@ -0,0 +1,54 @@
1
+ import { canonical } from "./integrity.mjs";
2
+ /** Lossless references to repeated source locations; semantic identities and hashes stay unchanged. */
3
+ export function internGraphSources(graph) {
4
+ const rows = [
5
+ "variables",
6
+ "bindings",
7
+ "transitions",
8
+ "states",
9
+ "navigation",
10
+ "uncovered",
11
+ ].flatMap((key) => graph[key]),
12
+ counts = new Map();
13
+ for (const row of rows) {
14
+ const key = canonical(row.source);
15
+ counts.set(key, (counts.get(key) ?? 0) + 1);
16
+ }
17
+ const pool = [],
18
+ indices = new Map();
19
+ for (const row of rows) {
20
+ const key = canonical(row.source);
21
+ if (counts.get(key) < 2) continue;
22
+ let index = indices.get(key);
23
+ if (index === undefined) {
24
+ index = pool.length;
25
+ indices.set(key, index);
26
+ pool.push(row.source);
27
+ }
28
+ row.source = index;
29
+ }
30
+ if (pool.length) graph.sourcePool = pool;
31
+ const scopes = [],
32
+ scopeIndices = new Map(),
33
+ scopeCounts = new Map();
34
+ for (const v of graph.variables)
35
+ scopeCounts.set(v.scopeKey, (scopeCounts.get(v.scopeKey) ?? 0) + 1);
36
+ for (const v of graph.variables)
37
+ if (scopeCounts.get(v.scopeKey) > 1) {
38
+ let i = scopeIndices.get(v.scopeKey);
39
+ if (i === undefined) {
40
+ i = scopes.length;
41
+ scopeIndices.set(v.scopeKey, i);
42
+ scopes.push(v.scopeKey);
43
+ }
44
+ v.scopeKey = i;
45
+ }
46
+ if (scopes.length) graph.scopePool = scopes;
47
+ }
48
+ export function graphSource(graph, row) {
49
+ return typeof row?.source === "number"
50
+ ? Number.isInteger(row.source)
51
+ ? graph.sourcePool?.[row.source]
52
+ : undefined
53
+ : row?.source;
54
+ }
package/src/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export type GenerateOptions = {
32
32
  releaseId?: string;
33
33
  buildCss?: string[];
34
34
  publicAssets?: string[];
35
+ /** Package only projected local raster references; large images use the bounded build preview profile. */
36
+ referencedImages?: boolean;
35
37
  optimizePublicImages?: boolean;
36
38
  rootFont?: string;
37
39
  omitSvgResources?: boolean;
@@ -82,3 +82,7 @@ export async function renderIamViewer(manifest, context) {
82
82
  .replace('<link rel="stylesheet" href="viewer.css" />', base ? '<link rel="stylesheet" href="'+asset(assets.css,'.css')+'" integrity="'+integrity(assets.css)+'" crossorigin="anonymous">' : '<style>' + assets.css.replaceAll(/<\/style/gi, "<\\/style") + '</style>')
83
83
  .replace('<script src="viewer.js"></script>', '<script id="iam-context" type="application/json">' + json(context) + '</script>' + (base ? '<script src="'+asset(assets.js,'.js')+'" integrity="'+integrity(assets.js)+'" crossorigin="anonymous"></script>' : '<script>' + script + '</script>'));
84
84
  }
85
+
86
+ export { previewUiDocument as previewIamUiDocument } from './ui-preview.mjs';
87
+
88
+ export { tabUiScenes as iamTabUiScenes } from './tab-scenes.mjs';
@@ -0,0 +1,18 @@
1
+ import { createRequire } from "node:module";
2
+ // Pinned, inert geometry data. Merchant packages and React components are never imported.
3
+ const require = createRequire(import.meta.url),
4
+ catalog = require("lucide-static/icon-nodes.json");
5
+ export const LUCIDE_SOURCE_VERSION = "1.34.0";
6
+ export const LUCIDE_SOURCE = Symbol("source-lucide");
7
+ export function lucideSource(name) {
8
+ const key = name
9
+ .replace(/Icon$/, "")
10
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
11
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
12
+ .replace(/([a-zA-Z])(\d)/g, "$1-$2")
13
+ .toLowerCase();
14
+ return Object.hasOwn(catalog, key) ? { [LUCIDE_SOURCE]: key } : null;
15
+ }
16
+ export function lucideGeometry(value) {
17
+ return catalog[value?.[LUCIDE_SOURCE]] ?? null;
18
+ }
@@ -7,10 +7,16 @@ module.exports = function indianaIamLoader(source, inputMap) {
7
7
  const { root } = this.getOptions();
8
8
  Promise.all([
9
9
  import("./transform.mjs"),
10
- fs.realpath(this.resourcePath),
10
+ fs.realpath(this.resourcePath).catch((error) => {
11
+ // Turbopack can retype loader output (e.g. icon.svg -> icon.svg.js).
12
+ // Such virtual modules have no original source binding to instrument.
13
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
14
+ throw error;
15
+ }),
11
16
  fs.realpath(root),
12
17
  ])
13
18
  .then(([{ transformSourceForIam }, resource, directory]) => {
19
+ if (!resource) return done(null, source, inputMap);
14
20
  const file = path.relative(directory, resource).split(path.sep).join("/");
15
21
  if (file.startsWith("../") || path.isAbsolute(file))
16
22
  return done(null, source, inputMap);
package/src/next.cjs ADDED
@@ -0,0 +1,5 @@
1
+ /** Next transpiles next.config.ts to CommonJS before loading its plugins. */
2
+ exports.withIndianaIam = (nextConfig = {}, options = {}) => async (phase, context) => {
3
+ const { withIndianaIam } = await import("./next.mjs");
4
+ return withIndianaIam(nextConfig, options)(phase, context);
5
+ };
@@ -1,21 +1,21 @@
1
1
  import { documentDom } from "./dom-payload.mjs";
2
- import { canonical, sealNodes, sealDocument } from './integrity.mjs';
2
+ import { canonical, identity, sealNodes, sealDocument } from './integrity.mjs';
3
3
  import { dependency } from './dependencies.mjs';
4
4
  import { sourceMappingsHash } from './compact-dom.mjs';
5
5
 
6
6
  /** HTML already owns the DOM values and ordering. Store only the source map
7
7
  * in preorder, rather than repeating attributes, text and every tree edge. */
8
- export function expandDomNodes(document, body) {
8
+ export function expandDomNodes(document, body, deriveTextId) {
9
9
  if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash||document.nodes.length>12000)throw Error('Invalid HTML-derived DOM mode');
10
10
  let index=0;const result=[];
11
- function visit(node,parentId,depth){
11
+ function visit(node,parentId,depth,childIndex=0){
12
12
  if(depth>128||![1,3].includes(node.nodeType))throw Error('Invalid HTML-derived DOM structure');
13
13
  const ref=document.nodes[index++];
14
14
  if(!ref||Object.keys(ref).some(key=>!['nodeId','elementKey','source','synthetic'].includes(key)))throw Error('Mixed HTML-derived DOM values');
15
- const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId;
15
+ const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId??deriveTextId?.(document.pageKey,parentId,childIndex);
16
16
  if(!/^node_[a-f0-9]{64}$/.test(nodeId??'')||element&&Object.hasOwn(ref,'nodeId')||Object.hasOwn(ref,'synthetic')&&ref.synthetic!==true)throw Error('HTML-derived DOM identity differs');
17
17
  const full={...ref,nodeId,synthetic:ref.synthetic??false,parentId,kind:element?'element':'text',tag:element?node.tagName.toLowerCase():null,text:element?null:node.data,attributes:element?Object.fromEntries([...node.attributes].filter(a=>!a.name.startsWith('data-iam-')).map(a=>[a.name,a.value])):{},children:[]};
18
- result.push(full);for(const child of node.childNodes)full.children.push(visit(child,nodeId,depth+1));return nodeId;
18
+ result.push(full);for(const [i,child]of [...node.childNodes].entries())full.children.push(visit(child,nodeId,depth+1,i));return nodeId;
19
19
  }
20
20
  if(body.childNodes.length!==1||visit(body.firstChild,null,0)!==document.rootNodeId||index!==document.nodes.length)throw Error('HTML-derived DOM coverage differs');
21
21
  return result;
@@ -23,7 +23,7 @@ export function expandDomNodes(document, body) {
23
23
  function domNodes(document){
24
24
  const decoded = {...document, ...documentDom(document)};
25
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();}
26
+ try{return expandDomNodes(decoded,dom.window.document.body,(page,parentId,index)=>identity('node',{page,parentId,index,kind:3}));}finally{dom.window.close();}
27
27
  }
28
28
 
29
29
  /** Node hashes are pure derived values. IDs, source mappings and algorithms never change. */
@@ -38,8 +38,9 @@ export function storeDocumentNodes(document, nodes, elements) {
38
38
  document.sourceMappingsHash = sourceMappingsHash(nodes, elements);
39
39
  document.nodeEncoding = 'html-preorder-v1';
40
40
  document.nodeHashMode = 'derived-v1';
41
- document.nodes = nodes.map(({nodeId,kind,elementKey,synthetic,source}) => ({
42
- ...(kind === 'text' ? {nodeId} : {}),
41
+ const byId=new Map(nodes.map(node=>[node.nodeId,node]));
42
+ document.nodes = nodes.map(({nodeId,parentId,kind,elementKey,synthetic,source}) => ({
43
+ ...(kind === 'text' && nodeId!==identity('node',{page:document.pageKey,parentId,index:byId.get(parentId)?.children.indexOf(nodeId)??0,kind:3}) ? {nodeId} : {}),
43
44
  elementKey,
44
45
  ...(synthetic ? {synthetic:true} : {}),
45
46
  ...(source && canonical(source) !== canonical(elements.get(elementKey)?.source) ? {source} : {}),