@tinkerlandai/indiana-iam 0.3.0 → 0.5.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 +81 -5
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +78 -36
- package/package.json +12 -14
- package/src/analyze.mjs +968 -146
- package/src/anchors.mjs +2 -0
- package/src/build-options.d.ts +1 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +56 -8
- package/src/build.mjs +3 -3
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/document-material.mjs +180 -0
- package/src/dom-payload-json.mjs +2 -2
- package/src/dom-payload.mjs +3 -3
- package/src/framework-routes.mjs +4 -3
- package/src/generate.mjs +107 -25
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +2 -1
- package/src/index.mjs +1 -1
- package/src/json.mjs +2 -3
- package/src/library-entry.mjs +9 -2
- 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 +10 -9
- package/src/render-index.mjs +7 -7
- package/src/route-composition.mjs +5 -5
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +19 -7
- package/src/routing.mjs +7 -6
- package/src/schema-definition.mjs +13 -15
- package/src/selected-manifest.mjs +12 -0
- package/src/source-aliases.mjs +34 -0
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +53 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-css-modules.mjs +79 -0
- package/src/source-environment.mjs +26 -0
- package/src/source-files.mjs +8 -7
- package/src/source-flowbite.mjs +60 -0
- package/src/source-i18next.mjs +22 -0
- package/src/source-mantine.mjs +61 -0
- package/src/source-mdx.mjs +92 -0
- package/src/source-media.mjs +30 -0
- package/src/source-query-evidence.mjs +64 -0
- package/src/source-redirects.mjs +388 -0
- package/src/source-styles.mjs +9 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +102 -14
- package/src/svg-preview.mjs +23 -0
- package/src/tab-scenes.mjs +214 -0
- package/src/tanstack-routes.mjs +1 -1
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +634 -0
- package/src/validate.mjs +5 -0
- package/src/viewer-index.mjs +49 -0
package/src/anchors.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export function instrumentableSource(file, text) {
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
export const anchorSelectorPattern =
|
|
28
|
+
// eslint-disable-next-line no-control-regex -- Reject control bytes in source anchors and navigation targets.
|
|
28
29
|
/^[a-z][a-z0-9-]*\[(?:id|data-testid|data-test|data-qa|data-indiana-id)="(?:[^"\\\x00-\x1f\x7f]|\\["\\])*"\]$/;
|
|
29
30
|
|
|
30
31
|
export function anchorSelector(node) {
|
|
@@ -35,6 +36,7 @@ export function anchorSelector(node) {
|
|
|
35
36
|
typeof value !== "string" ||
|
|
36
37
|
!value ||
|
|
37
38
|
value.length > 256 ||
|
|
39
|
+
// eslint-disable-next-line no-control-regex -- Reject control bytes in source anchors and navigation targets.
|
|
38
40
|
/[\x00-\x1f\x7f]/.test(value) ||
|
|
39
41
|
redactLiteral(value) !== value
|
|
40
42
|
)
|
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
|
@@ -4,12 +4,14 @@ import { dependency } from "./dependencies.mjs";
|
|
|
4
4
|
import { hashObject, identity, sealDocument, sha256 } from "./integrity.mjs";
|
|
5
5
|
import { refreshRenderHashes } from "./render-index.mjs";
|
|
6
6
|
import { documentNodes, storeDocumentNodes } from "./node-hashes.mjs";
|
|
7
|
+
import {assertStaticSvg} from './svg-preview.mjs';
|
|
8
|
+
import {rasterMime} from './source-media.mjs';
|
|
7
9
|
|
|
8
10
|
/** Keep one CSS copy in the single IAM file; document hashes pin its content-addressed key. */
|
|
9
11
|
export function shareDocumentStyles(manifest){
|
|
10
12
|
const counts=new Map();for(const doc of manifest.documents)if(doc.css)counts.set(doc.css,(counts.get(doc.css)||0)+1);
|
|
11
13
|
const styles=new Map((manifest.styles||[]).map(style=>[style.styleKey,style]));
|
|
12
|
-
for(const doc of manifest.documents){if((counts.get(doc.css)||0)<2)continue;const hash=sha256(doc.css),styleKey='style_'+hash;styles.set(styleKey,{styleKey,sha256:hash,css:doc.css});doc.styleKeys=[styleKey];doc.css='';sealDocument(doc);}
|
|
14
|
+
for(const doc of manifest.documents){if(!doc.css||(counts.get(doc.css)||0)<2&&!doc.styleKeys?.length)continue;const hash=sha256(doc.css),styleKey='style_'+hash;styles.set(styleKey,{styleKey,sha256:hash,css:doc.css});doc.styleKeys=[...new Set([styleKey,...(doc.styleKeys??[])])];doc.css='';sealDocument(doc);}
|
|
13
15
|
if(styles.size)manifest.styles=[...styles.values()];
|
|
14
16
|
refreshRenderHashes(manifest);
|
|
15
17
|
}
|
|
@@ -30,8 +32,8 @@ function checkedMounts(mounts) {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** 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");
|
|
35
|
+
export async function attachBuildStyles(manifest, { root, files = [], publicFiles = [], referencedImages = false, optimizePublicImages = false, rootFont = "", omitSvgResources = false, mounts = nextStaticMounts() }) {
|
|
36
|
+
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
37
|
if (rootFont && !/^[A-Za-z][A-Za-z0-9 _-]{0,63}$/.test(rootFont)) throw new Error("Invalid root font family");
|
|
36
38
|
root = await fs.realpath(root);
|
|
37
39
|
const cssTree = dependency("css-tree").value;
|
|
@@ -45,13 +47,13 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
|
|
|
45
47
|
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
48
|
const inputs = new Map(), assets = new Map(manifest.assets.map(a => [a.assetKey, a]));
|
|
47
49
|
let total = 0, publicSourceBytes = 0;
|
|
48
|
-
async function read(file, publicImage = false) {
|
|
50
|
+
async function read(file, publicImage = false, sourceImage = false, previewImage = false) {
|
|
49
51
|
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");
|
|
52
|
+
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
53
|
const relative = path.relative(root, real).split(path.sep).join("/");
|
|
52
54
|
if (inputs.has(relative)) return inputs.get(relative);
|
|
53
55
|
const stat = await fs.stat(real);
|
|
54
|
-
const optimized = publicImage && optimizePublicImages;
|
|
56
|
+
const optimized = previewImage || publicImage && optimizePublicImages;
|
|
55
57
|
const maxInput = optimized ? 4_194_304 : 1_048_576;
|
|
56
58
|
if (!stat.isFile() || stat.size > maxInput || (optimized ? publicSourceBytes + stat.size > 33_554_432 : total + stat.size > 6_291_456) || inputs.size >= 128) throw new Error("Build style budget exceeded");
|
|
57
59
|
const bytes = await fs.readFile(real);
|
|
@@ -113,14 +115,53 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
|
|
|
113
115
|
assets.set(key, {assetKey:key, sourceUrl, mime, bytes:bytes.length, sha256:digest, dataUrl:'data:'+mime+';base64,'+bytes.toString('base64')});
|
|
114
116
|
}
|
|
115
117
|
}
|
|
118
|
+
if(referencedImages){
|
|
119
|
+
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|svg)$/i.test(v))))].sort();
|
|
120
|
+
if(urls.length>128)throw Error('Referenced image budget exceeded');
|
|
121
|
+
for(const url of urls){
|
|
122
|
+
if(assets.size>=128)throw Error('Referenced image budget exceeded');
|
|
123
|
+
if([...assets.values()].some(a=>a.sourceUrl===url))continue;
|
|
124
|
+
const source=url.startsWith('/__iam_source__/'),file=source?url.slice('/__iam_source__/'.length):fileOf(url);
|
|
125
|
+
if(file.split('/').some(p=>!p||p==='..'||source&&p.startsWith('.'))||/(?:^|\/)(?:api|server|secrets?|credentials)(?:\/|\.)/i.test(file))continue;
|
|
126
|
+
try{
|
|
127
|
+
const svg=file.toLowerCase().endsWith('.svg');
|
|
128
|
+
const input=await read(file,false,source,true);let bytes=input.bytes,mime=svg?'image/svg+xml':types.get(path.extname(file).toLowerCase());
|
|
129
|
+
if(svg)assertStaticSvg(bytes);
|
|
130
|
+
if(!svg){const actual=rasterMime(bytes);if(!actual)throw Error('Referenced raster signature mismatch');if(actual!==mime)manifest.coverage.gaps.push({code:'REFERENCED_IMAGE_TYPE_SNIFFED',file,detail:'Raster signature differs from its extension; preview uses the verified inert media type.'});mime=actual;}
|
|
131
|
+
if(svg||bytes.length>65536){
|
|
132
|
+
const sharp=dependency('sharp').value;if(sharp.versions.sharp!=='0.35.5')throw Error('Pinned image encoder required');
|
|
133
|
+
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');
|
|
134
|
+
if(!metadata.width||!metadata.height||metadata.width*metadata.height>16777216)throw Error('Referenced image pixel budget exceeded');
|
|
135
|
+
bytes=await image.rotate().resize({width:800,height:800,fit:'inside',withoutEnlargement:true}).webp({quality:90,effort:4}).toBuffer();mime='image/webp';
|
|
136
|
+
manifest.coverage.gaps.push({code:'REFERENCED_IMAGE_PREVIEW_OPTIMIZED',file,detail:'Build preview profile for rasters over 64 KiB and reviewed static SVG: auto-orient, fit inside 800x800, WebP quality 90; source bytes remain hashed. Lossy preview, not original pixels.'});
|
|
137
|
+
}
|
|
138
|
+
if(bytes.length>1_048_576||(total+=bytes.length)>6_291_456)throw Error('Referenced image output budget exceeded');
|
|
139
|
+
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')});
|
|
140
|
+
}catch(error){
|
|
141
|
+
// A missing reference is a fidelity gap. Unsafe files/budgets fail the packaging step.
|
|
142
|
+
if(error.code!=='ENOENT')throw error;
|
|
143
|
+
manifest.coverage.gaps.push({code:'REFERENCED_IMAGE_MISSING',file,detail:'Referenced raster is absent from the local public build; no network fetch performed.'});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
116
147
|
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
148
|
const sheets = [], families = new Set();
|
|
118
149
|
for (const file of [...new Set(files)].sort()) {
|
|
119
150
|
if (!file.endsWith(".css")) throw new Error("Compiled CSS file required");
|
|
120
151
|
const input = await read(file), ast = cssTree.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes), { parseCustomProperty: true });
|
|
121
152
|
const urls = [];
|
|
153
|
+
// A build may retain a remote font import. Keep its local stylesheet, but
|
|
154
|
+
// never fetch or copy the external dependency into an offline projection.
|
|
155
|
+
cssTree.walk(ast, { visit: "Atrule", enter(node, item, list) {
|
|
156
|
+
if (node.name.toLowerCase() !== "import") return;
|
|
157
|
+
const target = node.prelude?.children?.first;
|
|
158
|
+
if (!["String", "Url"].includes(target?.type) || !/^(?:https?:)?\/\//i.test(target.value))
|
|
159
|
+
throw new Error("Precompile CSS imports before packaging");
|
|
160
|
+
list.remove(item);
|
|
161
|
+
if (!manifest.coverage.gaps.some(g => g.code === "BUILD_EXTERNAL_STYLESHEET_OMITTED" && g.file === input.relative))
|
|
162
|
+
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." });
|
|
163
|
+
}});
|
|
122
164
|
cssTree.walk(ast, node => {
|
|
123
|
-
if (node.type === "Atrule" && node.name.toLowerCase() === "import") throw new Error("Precompile CSS imports before packaging");
|
|
124
165
|
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
166
|
if (node.type === "Url") urls.push(node);
|
|
126
167
|
});
|
|
@@ -128,7 +169,7 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
|
|
|
128
169
|
sheets.push(cssTree.generate(ast));
|
|
129
170
|
}
|
|
130
171
|
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}" : "");
|
|
172
|
+
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
173
|
if (/<\/(?:style|script)/i.test(css)) throw new Error("HTML closing tag in compiled CSS");
|
|
133
174
|
if (files.length) for (const doc of manifest.documents) { doc.css = css; sealDocument(doc); }
|
|
134
175
|
// One content-addressed image in the envelope; DOM instances carry references.
|
|
@@ -142,6 +183,13 @@ export async function attachBuildStyles(manifest, { root, files = [], publicFile
|
|
|
142
183
|
doc.html = doc.html.replaceAll('src="' + asset.sourceUrl + '"', 'src="' + reference + '"');
|
|
143
184
|
}
|
|
144
185
|
storeDocumentNodes(doc, nodes, new Map(manifest.elements.map(element => [element.elementKey, element])));
|
|
186
|
+
// Resource replacement changes element and ancestor hashes. Keep target
|
|
187
|
+
// fingerprints tied to the final document, never the pre-packaging HTML.
|
|
188
|
+
const finalNodes=documentNodes(doc);
|
|
189
|
+
for(const target of manifest.routeTree?.targets??[]){
|
|
190
|
+
if(target.routeKey!==doc.pageKey)continue;const node=finalNodes.find(n=>n.nodeId===target.nodeId);if(!node)continue;
|
|
191
|
+
target.contentHash=node.contentHash;if(target.subtreeHash!==undefined)target.subtreeHash=node.subtreeHash;
|
|
192
|
+
}
|
|
145
193
|
}
|
|
146
194
|
manifest.coverage.gaps = manifest.coverage.gaps.filter(g => g.code !== 'PUBLIC_ASSET_NOT_PACKAGED' || ![...assets.values()].some(a => g.detail.startsWith(a.sourceUrl + ':')));
|
|
147
195
|
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: _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
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { documentNodes } from "./node-hashes.mjs";
|
|
2
|
+
import { documentDom } from "./dom-payload.mjs";
|
|
3
|
+
import { sha256, canonical } from "./integrity.mjs";
|
|
4
|
+
const resourceHashes = new WeakMap();
|
|
5
|
+
export function previewResourceHash(kind, value) {
|
|
6
|
+
let hashes = resourceHashes.get(value);
|
|
7
|
+
if (!hashes) {
|
|
8
|
+
hashes = new Map();
|
|
9
|
+
resourceHashes.set(value, hashes);
|
|
10
|
+
}
|
|
11
|
+
if (!hashes.has(kind))
|
|
12
|
+
hashes.set(
|
|
13
|
+
kind,
|
|
14
|
+
sha256("iam/preview-resource/v1\0" + canonical({ kind, value })),
|
|
15
|
+
);
|
|
16
|
+
return hashes.get(kind);
|
|
17
|
+
}
|
|
18
|
+
/** Pure selected-render material. Caller owns immutable-source verification and authorization. */
|
|
19
|
+
export function documentMaterial(
|
|
20
|
+
manifest,
|
|
21
|
+
pageKey,
|
|
22
|
+
renderKey,
|
|
23
|
+
knownResources = [],
|
|
24
|
+
) {
|
|
25
|
+
const route = manifest.routeTree.routes.find(
|
|
26
|
+
(route) => route.routeKey === pageKey,
|
|
27
|
+
);
|
|
28
|
+
if (!route) throw Error("Unknown IAM preview route");
|
|
29
|
+
const render = manifest.routeTree.renders.find(
|
|
30
|
+
(render) =>
|
|
31
|
+
render.routeKey === pageKey &&
|
|
32
|
+
render.renderKey === (renderKey ?? route.initialRenderKey),
|
|
33
|
+
);
|
|
34
|
+
if (!render) throw Error("Unknown IAM preview render");
|
|
35
|
+
const document = manifest.documents.find(
|
|
36
|
+
(document) => document.documentKey === render.documentKey,
|
|
37
|
+
);
|
|
38
|
+
if (!document) throw Error("Missing IAM preview document");
|
|
39
|
+
const concreteNodes = documentNodes(document);
|
|
40
|
+
const elementKeys = new Set(concreteNodes.map((node) => node.elementKey));
|
|
41
|
+
const ui = manifest.routeTree.ui;
|
|
42
|
+
const collections =
|
|
43
|
+
ui?.collections?.filter((row) => row.renderKey === render.renderKey) ?? [];
|
|
44
|
+
const fragmentKeys = new Set(
|
|
45
|
+
collections.flatMap((row) => row.cases.map((item) => item.fragmentKey)),
|
|
46
|
+
);
|
|
47
|
+
const fragments =
|
|
48
|
+
ui?.fragments?.filter((row) => fragmentKeys.has(row.fragmentKey)) ?? [];
|
|
49
|
+
for (const fragment of fragments)
|
|
50
|
+
for (const key of fragment.sourceElements) elementKeys.add(key);
|
|
51
|
+
const nodeIds = new Set(concreteNodes.map((node) => node.nodeId));
|
|
52
|
+
const elements = manifest.elements.filter((element) =>
|
|
53
|
+
elementKeys.has(element.elementKey),
|
|
54
|
+
);
|
|
55
|
+
const componentKeys = new Set(
|
|
56
|
+
elements.map((element) => element.componentKey),
|
|
57
|
+
);
|
|
58
|
+
const styles =
|
|
59
|
+
manifest.styles?.filter((style) =>
|
|
60
|
+
document.styleKeys?.includes(style.styleKey),
|
|
61
|
+
) ?? [];
|
|
62
|
+
const resourceText = [
|
|
63
|
+
documentDom(document).html,
|
|
64
|
+
document.css,
|
|
65
|
+
...styles.map((style) => style.css),
|
|
66
|
+
].join("\n");
|
|
67
|
+
const assetKeys = new Set(
|
|
68
|
+
[...resourceText.matchAll(/iam-asset:([a-z0-9_]+)/g)].map(
|
|
69
|
+
(match) => match[1],
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
const scopeRows = (rows) =>
|
|
73
|
+
rows
|
|
74
|
+
.filter(
|
|
75
|
+
(row) =>
|
|
76
|
+
row &&
|
|
77
|
+
typeof row === "object" &&
|
|
78
|
+
("renderKey" in row
|
|
79
|
+
? row.renderKey === render.renderKey
|
|
80
|
+
: "routeKey" in row && row.routeKey === pageKey),
|
|
81
|
+
)
|
|
82
|
+
.map((row) => {
|
|
83
|
+
if (!row || typeof row !== "object") return row;
|
|
84
|
+
const source =
|
|
85
|
+
"source" in row && typeof row.source === "number"
|
|
86
|
+
? manifest.routeTree.sourcePool?.[row.source]
|
|
87
|
+
: undefined;
|
|
88
|
+
const scopeKey =
|
|
89
|
+
"scopeKey" in row && typeof row.scopeKey === "number"
|
|
90
|
+
? manifest.routeTree.scopePool?.[row.scopeKey]
|
|
91
|
+
: undefined;
|
|
92
|
+
return {
|
|
93
|
+
...row,
|
|
94
|
+
...(source ? { source } : {}),
|
|
95
|
+
...(scopeKey ? { scopeKey } : {}),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
const attributes =
|
|
99
|
+
ui?.attributes?.filter((row) => row.renderKey === render.renderKey) ?? [];
|
|
100
|
+
const attributeSetKeys = new Set(attributes.map((row) => row.setKey));
|
|
101
|
+
const conditionKeys = new Set(
|
|
102
|
+
[
|
|
103
|
+
...documentDom(document).html.matchAll(/data-iam-condition="([^"]+)"/g),
|
|
104
|
+
].map((match) => match[1]),
|
|
105
|
+
);
|
|
106
|
+
const routeTree = {
|
|
107
|
+
format: manifest.routeTree.format,
|
|
108
|
+
schemaVersion: manifest.routeTree.schemaVersion,
|
|
109
|
+
profile: manifest.routeTree.profile,
|
|
110
|
+
roots: [pageKey],
|
|
111
|
+
routes: [route],
|
|
112
|
+
renders: [render],
|
|
113
|
+
targets: manifest.routeTree.targets.filter(
|
|
114
|
+
(target) => target.renderKey === render.renderKey,
|
|
115
|
+
),
|
|
116
|
+
variables: scopeRows(manifest.routeTree.variables),
|
|
117
|
+
bindings: scopeRows(manifest.routeTree.bindings),
|
|
118
|
+
transitions: scopeRows(manifest.routeTree.transitions),
|
|
119
|
+
states: scopeRows(manifest.routeTree.states),
|
|
120
|
+
navigation: scopeRows(manifest.routeTree.navigation),
|
|
121
|
+
uncovered: scopeRows(manifest.routeTree.uncovered),
|
|
122
|
+
renderNavigation: manifest.routeTree.renderNavigation.filter(
|
|
123
|
+
(edge) => edge.fromRenderKey === render.renderKey,
|
|
124
|
+
),
|
|
125
|
+
...(ui
|
|
126
|
+
? {
|
|
127
|
+
ui: {
|
|
128
|
+
schemaVersion: ui.schemaVersion,
|
|
129
|
+
conditions: ui.conditions.filter((row) =>
|
|
130
|
+
conditionKeys.has(row.conditionKey),
|
|
131
|
+
),
|
|
132
|
+
tabs: ui.tabs.filter((row) => row.renderKey === render.renderKey),
|
|
133
|
+
regions:
|
|
134
|
+
ui.regions?.filter((row) => row.renderKey === render.renderKey) ??
|
|
135
|
+
[],
|
|
136
|
+
navigation: ui.navigation.filter((row) => row.routeKey === pageKey),
|
|
137
|
+
collections,
|
|
138
|
+
fragments,
|
|
139
|
+
attributes,
|
|
140
|
+
attributeSets:
|
|
141
|
+
ui.attributeSets?.filter((row) =>
|
|
142
|
+
attributeSetKeys.has(row.setKey),
|
|
143
|
+
) ?? [],
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
: {}),
|
|
147
|
+
};
|
|
148
|
+
const known = new Set(knownResources);
|
|
149
|
+
const resourceRefs = [];
|
|
150
|
+
const missing = (kind, values) =>
|
|
151
|
+
values.filter((value) => {
|
|
152
|
+
const hash = previewResourceHash(kind, value);
|
|
153
|
+
resourceRefs.push({ kind, hash });
|
|
154
|
+
return !known.has(hash);
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
document,
|
|
158
|
+
elements,
|
|
159
|
+
styles: missing("styles", styles),
|
|
160
|
+
assets: missing(
|
|
161
|
+
"assets",
|
|
162
|
+
manifest.assets.filter((asset) => assetKeys.has(asset.assetKey)),
|
|
163
|
+
),
|
|
164
|
+
scripts: missing(
|
|
165
|
+
"scripts",
|
|
166
|
+
manifest.scripts?.filter(
|
|
167
|
+
(script) => script.scriptKey === document.scriptKey,
|
|
168
|
+
) ?? [],
|
|
169
|
+
),
|
|
170
|
+
resourceRefs,
|
|
171
|
+
components: manifest.components.filter((component) =>
|
|
172
|
+
componentKeys.has(component.componentKey),
|
|
173
|
+
),
|
|
174
|
+
interactions: manifest.interactions.filter(
|
|
175
|
+
(interaction) =>
|
|
176
|
+
interaction.pageKey === pageKey && nodeIds.has(interaction.nodeId),
|
|
177
|
+
),
|
|
178
|
+
routeTree,
|
|
179
|
+
};
|
|
180
|
+
}
|
package/src/dom-payload-json.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
export function checkedDomPayload(document) {
|
|
3
3
|
const payload = document.domPayload;
|
|
4
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 ||
|
|
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 || typeof payload.data !== "string" || !payload.data.length || payload.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(payload.data)) throw Error("Invalid compressed DOM payload");
|
|
6
6
|
return payload;
|
|
7
7
|
}
|
|
8
8
|
export function parsedDomPayload(text) {
|
|
9
9
|
const value = JSON.parse(text);
|
|
10
|
-
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" ||
|
|
10
|
+
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" || !value.html.isWellFormed() || !Array.isArray(value.nodes) || JSON.stringify(value) !== text) throw Error("Invalid expanded DOM payload");
|
|
11
11
|
return value;
|
|
12
12
|
}
|
package/src/dom-payload.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { gzip, Inflate } from "pako";
|
|
2
2
|
import { checkedDomPayload, parsedDomPayload } from "./dom-payload-json.mjs";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
/** Lossless, local decoding with a declared and enforced output bound. */
|
|
7
7
|
export function documentDom(document) {
|
|
@@ -31,7 +31,7 @@ export function assertDomBudget(documents) {
|
|
|
31
31
|
for (const document of documents) {
|
|
32
32
|
const payload = checkedDomPayload(document);
|
|
33
33
|
total += payload ? payload.bytes : Buffer.byteLength(JSON.stringify({ html: document.html, nodes: document.nodes }));
|
|
34
|
-
if (total
|
|
34
|
+
if (!Number.isSafeInteger(total)) throw Error("Invalid expanded DOM size");
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -39,7 +39,7 @@ export function assertDomBudget(documents) {
|
|
|
39
39
|
export function packDocumentDom(document) {
|
|
40
40
|
if (document.domPayload) return false;
|
|
41
41
|
const raw = Buffer.from(JSON.stringify({ html: document.html, nodes: document.nodes }));
|
|
42
|
-
if (raw.length < 16_384
|
|
42
|
+
if (raw.length < 16_384) return false;
|
|
43
43
|
const zipped = gzip(raw, { level: 9, header: { time: 0, os: 255 } });
|
|
44
44
|
const payload = { encoding: "gzip-json-v1", bytes: raw.length, data: Buffer.from(zipped).toString("base64") };
|
|
45
45
|
if (Buffer.byteLength(JSON.stringify(payload)) + 1024 >= raw.length) return false;
|