@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.
- package/README.md +56 -3
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +19 -5
- package/package.json +5 -13
- package/src/analyze.mjs +799 -109
- package/src/build-options.d.ts +1 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +49 -6
- package/src/build.mjs +3 -3
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/framework-routes.mjs +3 -1
- package/src/generate.mjs +3 -1
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +2 -0
- package/src/library-entry.mjs +4 -0
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/node-hashes.mjs +9 -8
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +15 -3
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +38 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-environment.mjs +26 -0
- package/src/source-media.mjs +21 -0
- package/src/source-query-evidence.mjs +64 -0
- package/src/source-redirects.mjs +388 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +39 -11
- package/src/tab-scenes.mjs +214 -0
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +645 -0
- package/src/validate.mjs +5 -0
package/src/build-options.d.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/src/build-runtime.mjs
CHANGED
|
@@ -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 */'));
|
package/src/build-styles.mjs
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/src/dependencies.mjs
CHANGED
|
@@ -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
|
|
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
|
|
16
|
+
`Missing ${name}. Reinstall @tinkerlandai/indiana-iam with its dependencies.`,
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/framework-routes.mjs
CHANGED
|
@@ -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;
|
package/src/library-entry.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/next-loader.cjs
CHANGED
|
@@ -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
|
+
};
|
package/src/node-hashes.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
42
|
-
|
|
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} : {}),
|