@tinkerlandai/indiana-iam 0.2.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 +127 -5
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +74 -7
- package/package.json +6 -13
- package/src/analyze.mjs +897 -123
- package/src/anchors.mjs +5 -0
- package/src/build-options.d.ts +6 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +49 -6
- package/src/build.mjs +13 -7
- package/src/cli.mjs +10 -4
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/dom-payload-browser.mjs +28 -0
- package/src/dom-payload-json.mjs +12 -0
- package/src/dom-payload.mjs +50 -0
- package/src/framework-routes.mjs +44 -89
- package/src/generate.mjs +20 -7
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +13 -1
- package/src/index.mjs +1 -1
- package/src/integrity.mjs +4 -2
- package/src/library-entry.mjs +5 -0
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/next.mjs +4 -3
- package/src/node-hashes.mjs +13 -9
- package/src/render-index.mjs +6 -4
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +15 -3
- package/src/routing.mjs +1 -1
- package/src/schema-definition.mjs +8 -1
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +38 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-entry.mjs +50 -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/source-resolution.mjs +268 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +39 -11
- package/src/tab-scenes.mjs +214 -0
- package/src/tanstack-routes.mjs +143 -0
- package/src/transform.mjs +2 -2
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +645 -0
- package/src/upload.mjs +28 -6
- package/src/validate.mjs +19 -4
- package/src/vite.d.ts +4 -1
- package/src/vite.mjs +25 -10
package/README.md
CHANGED
|
@@ -47,7 +47,14 @@ not import merchant modules to execute them. Source enumeration and optional Git
|
|
|
47
47
|
the package starts no external commands and works without a Git executable. Developers' explicitly configured
|
|
48
48
|
framework callbacks remain part of their normal build.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Build integrations retain local compiled CSS when a stylesheet contains an external `@import`: the import is omitted
|
|
51
|
+
without making a network request and the manifest records the missing external styles. Inline SVG resources are
|
|
52
|
+
omitted by default during framework builds, with a coverage gap, so an unsupported form icon does not discard the
|
|
53
|
+
stylesheet. Set `omitSvgResources: false` to reject those resources and use the source-style fallback instead.
|
|
54
|
+
Standalone generation keeps its explicit `--omit-svg-resources` option. Compiled previews preserve hidden source
|
|
55
|
+
branches even when application CSS uses `display: flex` or `display: grid`.
|
|
56
|
+
|
|
57
|
+
## Vite, React Router and TanStack Router
|
|
51
58
|
|
|
52
59
|
```js
|
|
53
60
|
// vite.config.mjs
|
|
@@ -56,12 +63,20 @@ import { indianaIam } from "@tinkerlandai/indiana-iam/vite";
|
|
|
56
63
|
export default { plugins: [indianaIam()] };
|
|
57
64
|
```
|
|
58
65
|
|
|
66
|
+
Use an ESM Vite config (`vite.config.mjs` or `vite.config.mts`, or a project with `"type": "module"`).
|
|
67
|
+
CommonJS configs cannot `require` this ESM package.
|
|
68
|
+
|
|
59
69
|
Client builds expose `import.meta.env.INDIANA_IAM_RELEASE_URL` and write the descriptor into the build output folder.
|
|
60
70
|
Server builds are skipped.
|
|
61
71
|
|
|
72
|
+
Browser apps use the single local module script in `index.html` to select reachable router and React root declarations.
|
|
73
|
+
For multiple entry points, set `indianaIam({ entry: "src/main.tsx" })`, or pass `--entry src/main.tsx` to standalone generation.
|
|
74
|
+
The path must stay inside the scanned root. Next.js uses its App/Pages root instead.
|
|
75
|
+
|
|
62
76
|
## Options
|
|
63
77
|
|
|
64
|
-
Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload }`;
|
|
78
|
+
Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload, client }`;
|
|
79
|
+
browser applications additionally accept `entry`;
|
|
65
80
|
`withIndianaIam` also takes `root`. By default a source the scanner cannot read prints a warning and the build continues without an IAM;
|
|
66
81
|
`strict: true` fails the build instead. The same source always produces the same release ID and IAM bytes.
|
|
67
82
|
If source changes during compilation, generation fails instead of publishing a descriptor for different code.
|
|
@@ -99,7 +114,38 @@ optional command for explicit source annotations. Next and Vite do not need that
|
|
|
99
114
|
|
|
100
115
|
## Scope
|
|
101
116
|
|
|
102
|
-
Supported source: Next.js App Router and Pages Router,
|
|
117
|
+
Supported source: Next.js App Router and Pages Router, React Router / single-root React apps built with Vite,
|
|
118
|
+
and statically generated TanStack file-route trees registered with `createRouter({ routeTree })`.
|
|
119
|
+
React Router supports imported static path constants, inline `Component` functions and source-proven lazy import forwarding.
|
|
120
|
+
Data-router `route.lazy` objects support a static module's named `Component`, or a source-proven mapper that captures
|
|
121
|
+
the component export. Mapper factories, imports and business loaders are never executed. Unknown overrides remain gaps.
|
|
122
|
+
MDX pages/layouts need a separate adapter and explicitly report incomplete route coverage.
|
|
123
|
+
TanStack reads the registered generated tree, including pathless layouts, index routes and simple `$param` paths;
|
|
124
|
+
custom history/basepath and complex parameter patterns require separate adapters. Loaders, guards, retry/reload behavior
|
|
125
|
+
and runtime data remain unobserved. Unsupported optional React paths are isolated as gaps.
|
|
126
|
+
React Hook Form Controller callbacks retain their source JSX, including first-party wrappers; field values, validation
|
|
127
|
+
and submissions are not executed. Source-proven Radix overlays and React Transition Group visibility retain their
|
|
128
|
+
initial state. Supported Radix triggers and close controls simulate local state only; unknown controlled states stay
|
|
129
|
+
closed and unobserved. These adapters do not implement portal positioning, focus management or business callbacks.
|
|
130
|
+
Chart.js canvas wrappers and Recharts line/bar/area/pie declarations project bounded literal series as inert SVG.
|
|
131
|
+
Chart modules, effects, formatters and callbacks are not executed. Random/remote/oversized series show an explicit
|
|
132
|
+
unobserved-data placeholder; animation, library layout and tooltips remain outside this source projection.
|
|
133
|
+
Build hooks package only raster images referenced by projected JSX: relative source imports and public build URLs.
|
|
134
|
+
They never crawl unrelated assets or fetch URLs. Files must remain inside the root without symlinks; referenced
|
|
135
|
+
source image bytes participate in release identity. The existing 1 MiB/file, 6 MiB input and 128-asset bounds apply. Referenced rasters over 64 KiB use the bundled sharp 0.35.5 preview profile: auto-orient, fit inside 800×800, WebP quality 90, metadata stripped. Source bytes stay independently hashed; this lossy preview is explicitly reported.
|
|
136
|
+
Standalone generation keeps images unpackaged unless explicit public inputs are supplied.
|
|
137
|
+
Source-proven `useSyncExternalStore` matchMedia getters retain both typed viewport branches and follow the preview
|
|
138
|
+
viewport. Subscriptions are not run. UI preference cookies with an explicit source SSR guard use an empty-cookie
|
|
139
|
+
preview; auth/session cookies and arbitrary stores stay unobserved. Pure lazy local-state initializers are interpreted
|
|
140
|
+
under the same bounded rules; merchant callbacks and browser storage are never executed or read.
|
|
141
|
+
The complete validated IAM must fit within 8 MiB; oversized projections fail without emitting a truncated artifact.
|
|
142
|
+
Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
|
|
143
|
+
and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
|
|
144
|
+
HTML/DOM hashes and targets. Route navigation stores the exact interaction-array index instead of repeating its ID; validation checks node, route, destination and parameters. Source files retain their full hashes; component/element records cover reachable route compositions and shared fragments. Text node IDs that follow the original parent/index identity formula are derived locally instead of repeated; explicit exceptions remain stored, and every resolved source-map hash is checked. The document hash pins its encoded storage body; decoding is local and bounded to
|
|
145
|
+
8 MiB per payload and 32 MiB in aggregate. The final IAM file limit stays 8 MiB. Exported page HTML is expanded.
|
|
146
|
+
Use the matching 0.4 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
|
|
147
|
+
before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
|
|
148
|
+
viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
|
|
103
149
|
The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
|
|
104
150
|
step calls Indiana, and only when a key is set.
|
|
105
151
|
Values that look like credentials, tokens, emails or private URLs are removed, but review what your pages contain.
|
|
@@ -108,13 +154,89 @@ Values that look like credentials, tokens, emails or private URLs are removed, b
|
|
|
108
154
|
|
|
109
155
|
Create a Key with only the `app-manifests:write` permission (Console → developer settings) and set it as
|
|
110
156
|
`INDIANA_IAM_KEY` in your build environment. Each production build then registers the IAM, stores it through a
|
|
111
|
-
server-signed URL and asks Indiana to validate it; the validated version becomes
|
|
157
|
+
server-signed URL and asks Indiana to validate it; the validated version becomes its client's current version.
|
|
112
158
|
Rebuilding unchanged source only makes the existing version current again.
|
|
113
159
|
|
|
160
|
+
An App environment can hold several clients, each a named build with its own current version: for example `web` and
|
|
161
|
+
`web-dashboard`, or later `ios` and `android` apps. Set the `client` option (or `INDIANA_IAM_CLIENT`) to 1-40 lowercase
|
|
162
|
+
letters, digits or hyphens starting with a letter; the default is `web`. A client keeps the platform of its first
|
|
163
|
+
upload, and the same IAM bytes cannot be uploaded for two clients.
|
|
164
|
+
|
|
114
165
|
- Uploads go to `https://tinker.cash`; set `INDIANA_IAM_BASE_URL` (or the `baseUrl` option) for another Indiana
|
|
115
166
|
environment.
|
|
116
167
|
- On Vercel only production deployments upload by default; `upload: "always"` uploads every build and
|
|
117
168
|
`upload: false` never uploads.
|
|
118
169
|
- A failed upload prints a warning and the build continues, unless `strict: true`.
|
|
119
170
|
- The App's Owner or Admin must turn on the App page map first.
|
|
120
|
-
- `INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest
|
|
171
|
+
- `INDIANA_IAM_KEY=<key> indiana-iam upload --file <manifest> [--client <key>]` uploads a file generated elsewhere.
|
|
172
|
+
|
|
173
|
+
## App map annotations
|
|
174
|
+
|
|
175
|
+
The Console App map groups pages into areas, shows recorded navigation, and opens a main-path workbench before
|
|
176
|
+
opening a page editor. Thumbnails are static source projections. They do not execute authentication, data loaders,
|
|
177
|
+
form submissions or application code.
|
|
178
|
+
|
|
179
|
+
The optional `routeTree.ui` index records source UI alternatives, controlled Base UI Tabs (including first-party
|
|
180
|
+
wrappers and context), and conditional redirect/return recipes. A recipe follows the actual React Router location
|
|
181
|
+
through a URL query writer to a matching query-consuming `Navigate`; a password input at the destination identifies
|
|
182
|
+
the login UI. The index never infers authentication from a route name or certifies a successful login. Dynamic
|
|
183
|
+
callbacks, unresolved registries and external data remain coverage gaps. Finite first-party component registries
|
|
184
|
+
retain their source labels and scope qualifiers in `ui.regions`; selecting an alternative is a preview assumption.
|
|
185
|
+
Local JSX aliases and invoked JSX render props can preserve page content without executing their modules or callbacks.
|
|
186
|
+
|
|
187
|
+
Unknown branches stay hidden in the admitted document. The shared `previewIamUiDocument` helper can select inert
|
|
188
|
+
source alternatives and typed local state for thumbnail inspection. It returns the selected conditions and source
|
|
189
|
+
locations. Controlled overlays with unknown `open` values stay closed until explicitly selected for inspection.
|
|
190
|
+
These preview choices never change render discriminators, editable targets, the manifest or runtime permissions.
|
|
191
|
+
`iamTabUiScenes` dispatches the recorded scalar Tab transitions to derive before/after preview states and directed
|
|
192
|
+
edges. It includes content controlled outside the Tab root within the same source component or overlay. Every edge
|
|
193
|
+
retains the transition, state writes and source locations. Missing collections remain gaps rather than sample rows.
|
|
194
|
+
Finite Tab-dependent literal collections use shared `ui.fragments` and concrete `ui.collections` mounts. Their
|
|
195
|
+
source element references, item counts, Tab cases and required local values remain pinned; derived rows are inert
|
|
196
|
+
and never become SDK edit targets. The Lucide adapter reads geometry from pinned `lucide-static` 1.34.0 JSON,
|
|
197
|
+
without loading merchant packages. Unknown filter combinations remain unobserved rather than reusing stale rows.
|
|
198
|
+
Finite source-derived Tab classes, search placeholders and accessible labels share `ui.attributeSets`; concrete
|
|
199
|
+
mounts in `ui.attributes` apply only the recorded presentation properties. Fragment class and element dictionaries
|
|
200
|
+
preserve their exact authored values. Repeated graph source locations and instance scope names use lossless
|
|
201
|
+
`sourcePool` / `scopePool` references; source hashes, state identities and per-instance state remain unchanged.
|
|
202
|
+
Unexecuted query status/result branches and unresolved scalar alternatives display explicit unobserved annotations;
|
|
203
|
+
preview does not select a loading, failed or empty result automatically. Literal empty source catalogs remain valid.
|
|
204
|
+
Static thumbnail crops can focus on the page body or a source node using a bounded viewport; they retain authored
|
|
205
|
+
descendants and never introduce scripts, frames or remote resources.
|
|
206
|
+
|
|
207
|
+
Optional static attributes preserve product intent that cannot be established from source dataflow alone:
|
|
208
|
+
|
|
209
|
+
| Attribute | Location | Meaning |
|
|
210
|
+
| -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
211
|
+
| `data-indiana-map-area` | Page root | Functional area label; otherwise the first URL segment groups the directory. |
|
|
212
|
+
| `data-indiana-map-flow` | Page root | Display name for the process containing this page. |
|
|
213
|
+
| `data-indiana-map-kind="login"` | Page root | Declares an authentication page. |
|
|
214
|
+
| `data-indiana-map-auth="public\|required"` | Page root | Declares the page's access requirement. Omission remains unknown. |
|
|
215
|
+
| `data-indiana-map-edge="step\|tab\|login\|return"` | An existing link or supported UI control | Classifies an existing recorded action. It never invents a destination or makes an unsupported action executable. |
|
|
216
|
+
|
|
217
|
+
Use one consistent declaration on the outer page element. Native `role="tab"` also classifies a Tab control.
|
|
218
|
+
A link Tab retains its exact destination URL, including query and fragment; a supported state-writing Tab remains
|
|
219
|
+
an in-page action. Login and return links remain separate from ordinary steps. Unsupported actions appear as
|
|
220
|
+
unknown outcomes. Authentication requirements are display declarations, not proof of a session or runtime access.
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
<main
|
|
224
|
+
data-indiana-map-area="Workspace"
|
|
225
|
+
data-indiana-map-flow="Create project"
|
|
226
|
+
data-indiana-map-auth="required"
|
|
227
|
+
>
|
|
228
|
+
<a href="/login?next=%2Fprojects" data-indiana-map-edge="login">
|
|
229
|
+
Sign in
|
|
230
|
+
</a>
|
|
231
|
+
<a href="/projects/configure" data-indiana-map-edge="step">
|
|
232
|
+
Continue
|
|
233
|
+
</a>
|
|
234
|
+
<a href="/projects?tab=activity" role="tab">
|
|
235
|
+
Activity
|
|
236
|
+
</a>
|
|
237
|
+
</main>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
An unambiguous source path can continue automatically until a branch or cycle. Explicit step links take precedence
|
|
241
|
+
over ordinary navigation; login/return links and Tabs are inspected separately. A new IAM build is needed to add
|
|
242
|
+
these annotations to an already uploaded version. Existing immutable versions are never rewritten.
|
|
@@ -7,9 +7,18 @@
|
|
|
7
7
|
const model = document.querySelector('[data-iam-adapter="' + name + '"]');
|
|
8
8
|
if (model) root.className = model.getAttribute("class") || "";
|
|
9
9
|
}
|
|
10
|
-
let machine = null;
|
|
10
|
+
let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
|
|
11
11
|
const patches = new Map();
|
|
12
12
|
const node = id => [...document.querySelectorAll("[data-iam-node]")].find(e => e.dataset.iamNode === id);
|
|
13
|
+
function viewportValues(graph,routeKey,renderKey){
|
|
14
|
+
const values={};for(const variable of graph.variables??[]){
|
|
15
|
+
if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
|
|
16
|
+
const match=/^@media (\((?:max|min)-width: [1-9]\d{1,3}px\))$/.exec(variable.label);
|
|
17
|
+
if(match)values[variable.variableKey]=matchMedia(match[1]).matches;
|
|
18
|
+
}return values;
|
|
19
|
+
}
|
|
20
|
+
let viewportGraph=null,viewportRoute=null,viewportRender=null;
|
|
21
|
+
addEventListener('resize',()=>{if(machine&&viewportGraph){machine.preview(viewportValues(viewportGraph,viewportRoute,viewportRender));renderState();}});
|
|
13
22
|
function renderState() {
|
|
14
23
|
if (!machine) return;
|
|
15
24
|
for (const binding of machine.bindings()) {
|
|
@@ -19,6 +28,11 @@
|
|
|
19
28
|
else if (binding.property === "text") el.textContent = String(binding.value);
|
|
20
29
|
else el[binding.property] = binding.value;
|
|
21
30
|
}
|
|
31
|
+
const values=machine.snapshot();
|
|
32
|
+
for(const tab of activeTabs){const selected=tab.variableKey?values[tab.variableKey]:tab.initialValue;for(const item of tab.items)node(item.nodeId)?.setAttribute('aria-selected',String(item.value===selected));}
|
|
33
|
+
applySourceAttributes(document,sourceUi,sourceRenderKey,values);
|
|
34
|
+
applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
|
|
35
|
+
markUnobservedData(document,sourceUi,true);
|
|
22
36
|
for (const patch of patches.values()) applyPatch(patch);
|
|
23
37
|
}
|
|
24
38
|
function applyPatch(data) {
|
|
@@ -78,7 +92,10 @@
|
|
|
78
92
|
channel = data.nonce;
|
|
79
93
|
if (data.graph && typeof data.routeKey === "string") {
|
|
80
94
|
machine = createStateMachine(data.graph, data.routeKey, data.renderKey);
|
|
95
|
+
viewportGraph=data.graph;viewportRoute=data.routeKey;viewportRender=data.renderKey;machine.preview(viewportValues(data.graph,data.routeKey,data.renderKey));
|
|
81
96
|
const renderKey = data.renderKey ?? data.graph.routes.find(route => route.routeKey === data.routeKey)?.initialRenderKey;
|
|
97
|
+
sourceUi=data.graph.ui;sourceRenderKey=renderKey;
|
|
98
|
+
activeTabs = (data.graph.ui?.tabs??[]).filter(t=>t.routeKey===data.routeKey&&t.renderKey===renderKey);
|
|
82
99
|
activeTransitions = data.graph.transitions.filter(t => t.routeKey === data.routeKey && t.renderKey === renderKey);
|
|
83
100
|
renderState();
|
|
84
101
|
}
|
|
@@ -76,6 +76,177 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function checkedCollectionFragment(doc, fragment) {
|
|
80
|
+
if (
|
|
81
|
+
typeof fragment?.html !== "string" ||
|
|
82
|
+
fragment.html.length > 262144 ||
|
|
83
|
+
!Array.isArray(fragment.elementKeys) ||
|
|
84
|
+
!Array.isArray(fragment.sourceElements) ||
|
|
85
|
+
!Array.isArray(fragment.classNames) ||
|
|
86
|
+
fragment.sourceElements.length > 2000 ||
|
|
87
|
+
fragment.classNames.length > 128 ||
|
|
88
|
+
fragment.classNames.some((s) => typeof s !== "string" || s.length > 4000) ||
|
|
89
|
+
fragment.elementKeys.some(
|
|
90
|
+
(i) =>
|
|
91
|
+
!Number.isInteger(i) || i < 0 || i >= fragment.sourceElements.length,
|
|
92
|
+
) ||
|
|
93
|
+
fragment.elementKeys.length > 2000
|
|
94
|
+
)
|
|
95
|
+
throw Error("Invalid shared source fragment");
|
|
96
|
+
const template = doc.createElement("template");
|
|
97
|
+
template.innerHTML = fragment.html;
|
|
98
|
+
const nodes = [...template.content.querySelectorAll("*")],
|
|
99
|
+
allowed = new Set(
|
|
100
|
+
"div span p button input svg path circle rect line polyline polygon ellipse g title".split(
|
|
101
|
+
" ",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
if (nodes.length !== fragment.elementKeys.length)
|
|
105
|
+
throw Error("Shared source fragment mapping mismatch");
|
|
106
|
+
for (const node of nodes) {
|
|
107
|
+
if (node.hasAttribute("data-iam-fragment-class")) {
|
|
108
|
+
const raw = node.getAttribute("data-iam-fragment-class"),
|
|
109
|
+
index = Number(raw);
|
|
110
|
+
if (
|
|
111
|
+
String(index) !== raw ||
|
|
112
|
+
!Number.isInteger(index) ||
|
|
113
|
+
index < 0 ||
|
|
114
|
+
index >= fragment.classNames.length
|
|
115
|
+
)
|
|
116
|
+
throw Error("Invalid source fragment class");
|
|
117
|
+
node.setAttribute("class", fragment.classNames[index]);
|
|
118
|
+
node.removeAttribute("data-iam-fragment-class");
|
|
119
|
+
}
|
|
120
|
+
if (!allowed.has(node.tagName.toLowerCase()))
|
|
121
|
+
throw Error("Active shared source fragment element");
|
|
122
|
+
for (const attr of node.attributes)
|
|
123
|
+
if (
|
|
124
|
+
!/^(?:class|title|type|value|placeholder|disabled|hidden|checked|tabindex|width|height|viewBox|viewbox|xmlns|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|cx|cy|r|rx|ry|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-(?:slot|state|active|disabled|pressed))$/.test(
|
|
125
|
+
attr.name,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
throw Error("Active shared source fragment attribute: " + attr.name);
|
|
129
|
+
for (const key of ["fill", "stroke"])
|
|
130
|
+
if (
|
|
131
|
+
node.hasAttribute(key) &&
|
|
132
|
+
!/^(?:none|currentColor|transparent|#[a-f\d]{3,8})$/i.test(
|
|
133
|
+
node.getAttribute(key),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
throw Error("External source fragment paint");
|
|
137
|
+
if (
|
|
138
|
+
node.hasAttribute("xmlns") &&
|
|
139
|
+
node.getAttribute("xmlns") !== "http://www.w3.org/2000/svg"
|
|
140
|
+
)
|
|
141
|
+
throw Error("Invalid source fragment namespace");
|
|
142
|
+
}
|
|
143
|
+
return { content: template.content, nodes };
|
|
144
|
+
}
|
|
145
|
+
function applySourceAttributes(doc, ui, renderKey, values) {
|
|
146
|
+
const sets = new Map((ui?.attributeSets ?? []).map((s) => [s.setKey, s])),
|
|
147
|
+
nodes = new Map(
|
|
148
|
+
[...doc.querySelectorAll("[data-iam-node]")].map((n) => [
|
|
149
|
+
n.getAttribute("data-iam-node"),
|
|
150
|
+
n,
|
|
151
|
+
]),
|
|
152
|
+
);
|
|
153
|
+
for (const row of ui?.attributes ?? []) {
|
|
154
|
+
if (row.renderKey !== renderKey) continue;
|
|
155
|
+
const set = sets.get(row.setKey),
|
|
156
|
+
node = nodes.get(row.nodeId),
|
|
157
|
+
chosen = set?.cases.find((c) => c.value === values[row.variableKey]);
|
|
158
|
+
if (!node || !chosen) continue;
|
|
159
|
+
if (
|
|
160
|
+
!["class", "placeholder", "aria-label"].includes(set.property) ||
|
|
161
|
+
typeof chosen.text !== "string" ||
|
|
162
|
+
chosen.text.length > 4000
|
|
163
|
+
)
|
|
164
|
+
throw Error("Invalid source presentation attribute");
|
|
165
|
+
node.setAttribute(set.property, chosen.text);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function applySourceCollections(doc, ui, renderKey, values, enabled) {
|
|
169
|
+
const choices = [],
|
|
170
|
+
fragments = new Map((ui?.fragments ?? []).map((f) => [f.fragmentKey, f]));
|
|
171
|
+
for (const row of ui?.collections ?? []) {
|
|
172
|
+
if (row.renderKey !== renderKey) continue;
|
|
173
|
+
const root = [...doc.querySelectorAll("[data-iam-node]")].find(
|
|
174
|
+
(n) => n.getAttribute("data-iam-node") === row.nodeId,
|
|
175
|
+
);
|
|
176
|
+
if (!root) continue;
|
|
177
|
+
root.replaceChildren();
|
|
178
|
+
if (!enabled) continue;
|
|
179
|
+
if (root.parentElement?.closest("[hidden]")) continue;
|
|
180
|
+
const selected = row.cases.find((c) => c.value === values[row.variableKey]),
|
|
181
|
+
satisfied = row.requires.every((r) => values[r.variableKey] === r.value);
|
|
182
|
+
root.hidden = false;
|
|
183
|
+
root.setAttribute("inert", "");
|
|
184
|
+
if (!selected || !satisfied) {
|
|
185
|
+
root.textContent = "未观测 · 此筛选组合未重建";
|
|
186
|
+
root.setAttribute("data-iam-observation", "unobserved-collection");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const fragment = fragments.get(selected.fragmentKey);
|
|
190
|
+
if (!fragment) throw Error("Missing source collection fragment");
|
|
191
|
+
const parsed = checkedCollectionFragment(doc, fragment);
|
|
192
|
+
root.replaceChildren(parsed.content);
|
|
193
|
+
parsed.nodes.forEach((n, index) => {
|
|
194
|
+
n.setAttribute("data-iam-preview-only", "true");
|
|
195
|
+
n.setAttribute(
|
|
196
|
+
"data-iam-source-element",
|
|
197
|
+
fragment.sourceElements[fragment.elementKeys[index]],
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
choices.push({
|
|
201
|
+
collectionKey: row.collectionKey,
|
|
202
|
+
value: selected.value,
|
|
203
|
+
itemCount: fragment.itemCount,
|
|
204
|
+
source: row.source,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return choices;
|
|
208
|
+
}
|
|
209
|
+
function markUnobservedData(doc, ui, enabled, selections = new Map()) {
|
|
210
|
+
doc.querySelectorAll("[data-iam-preview-note]").forEach((n) => n.remove());
|
|
211
|
+
const unobserved = [];
|
|
212
|
+
if (!enabled) return unobserved;
|
|
213
|
+
const source = new Map(
|
|
214
|
+
(ui?.conditions ?? []).map((c) => [c.conditionKey, c]),
|
|
215
|
+
);
|
|
216
|
+
for (const root of doc.querySelectorAll(
|
|
217
|
+
'[data-iam-condition][data-iam-alternative="true"]',
|
|
218
|
+
)) {
|
|
219
|
+
const condition = source.get(root.getAttribute("data-iam-condition")),
|
|
220
|
+
id = root.getAttribute("data-iam-node");
|
|
221
|
+
if (!condition?.observation || selections.has(id)) continue;
|
|
222
|
+
const note = doc.createElement("div");
|
|
223
|
+
note.setAttribute("data-iam-preview-note", "unobserved");
|
|
224
|
+
note.className = "iam-observation-note";
|
|
225
|
+
note.setAttribute("role", "status");
|
|
226
|
+
note.textContent =
|
|
227
|
+
condition.observation === "remote" ? "远程数据未观测" : "数据未观测";
|
|
228
|
+
note.title = condition.expression;
|
|
229
|
+
root.before(note);
|
|
230
|
+
if (!note.closest("[hidden]"))
|
|
231
|
+
unobserved.push({
|
|
232
|
+
groupKey: id,
|
|
233
|
+
expression: condition.expression,
|
|
234
|
+
source: condition.source,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
for (const root of doc.querySelectorAll(
|
|
238
|
+
'[data-iam-observation="remote"],[data-iam-observation="unobserved"]',
|
|
239
|
+
)) {
|
|
240
|
+
root.hidden = false;
|
|
241
|
+
root.textContent = "未观测";
|
|
242
|
+
root.classList.add("iam-unobserved-value");
|
|
243
|
+
}
|
|
244
|
+
for (const field of doc.querySelectorAll("[data-iam-input-unobserved]")) {
|
|
245
|
+
field.setAttribute("placeholder", "未观测");
|
|
246
|
+
field.setAttribute("title", "此字段的数据未观测");
|
|
247
|
+
}
|
|
248
|
+
return unobserved;
|
|
249
|
+
}
|
|
79
250
|
/* Indiana UI simulation runtime 0.2.0. No merchant code, requests, authentication or purchases. */
|
|
80
251
|
(() => {
|
|
81
252
|
"use strict";
|
|
@@ -85,9 +256,18 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
85
256
|
const model = document.querySelector('[data-iam-adapter="' + name + '"]');
|
|
86
257
|
if (model) root.className = model.getAttribute("class") || "";
|
|
87
258
|
}
|
|
88
|
-
let machine = null;
|
|
259
|
+
let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
|
|
89
260
|
const patches = new Map();
|
|
90
261
|
const node = id => [...document.querySelectorAll("[data-iam-node]")].find(e => e.dataset.iamNode === id);
|
|
262
|
+
function viewportValues(graph,routeKey,renderKey){
|
|
263
|
+
const values={};for(const variable of graph.variables??[]){
|
|
264
|
+
if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
|
|
265
|
+
const match=/^@media (\((?:max|min)-width: [1-9]\d{1,3}px\))$/.exec(variable.label);
|
|
266
|
+
if(match)values[variable.variableKey]=matchMedia(match[1]).matches;
|
|
267
|
+
}return values;
|
|
268
|
+
}
|
|
269
|
+
let viewportGraph=null,viewportRoute=null,viewportRender=null;
|
|
270
|
+
addEventListener('resize',()=>{if(machine&&viewportGraph){machine.preview(viewportValues(viewportGraph,viewportRoute,viewportRender));renderState();}});
|
|
91
271
|
function renderState() {
|
|
92
272
|
if (!machine) return;
|
|
93
273
|
for (const binding of machine.bindings()) {
|
|
@@ -97,6 +277,11 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
97
277
|
else if (binding.property === "text") el.textContent = String(binding.value);
|
|
98
278
|
else el[binding.property] = binding.value;
|
|
99
279
|
}
|
|
280
|
+
const values=machine.snapshot();
|
|
281
|
+
for(const tab of activeTabs){const selected=tab.variableKey?values[tab.variableKey]:tab.initialValue;for(const item of tab.items)node(item.nodeId)?.setAttribute('aria-selected',String(item.value===selected));}
|
|
282
|
+
applySourceAttributes(document,sourceUi,sourceRenderKey,values);
|
|
283
|
+
applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
|
|
284
|
+
markUnobservedData(document,sourceUi,true);
|
|
100
285
|
for (const patch of patches.values()) applyPatch(patch);
|
|
101
286
|
}
|
|
102
287
|
function applyPatch(data) {
|
|
@@ -156,7 +341,10 @@ function createStateMachine(graph, routeKey, renderKey) {
|
|
|
156
341
|
channel = data.nonce;
|
|
157
342
|
if (data.graph && typeof data.routeKey === "string") {
|
|
158
343
|
machine = createStateMachine(data.graph, data.routeKey, data.renderKey);
|
|
344
|
+
viewportGraph=data.graph;viewportRoute=data.routeKey;viewportRender=data.renderKey;machine.preview(viewportValues(data.graph,data.routeKey,data.renderKey));
|
|
159
345
|
const renderKey = data.renderKey ?? data.graph.routes.find(route => route.routeKey === data.routeKey)?.initialRenderKey;
|
|
346
|
+
sourceUi=data.graph.ui;sourceRenderKey=renderKey;
|
|
347
|
+
activeTabs = (data.graph.ui?.tabs??[]).filter(t=>t.routeKey===data.routeKey&&t.renderKey===renderKey);
|
|
160
348
|
activeTransitions = data.graph.transitions.filter(t => t.routeKey === data.routeKey && t.renderKey === renderKey);
|
|
161
349
|
renderState();
|
|
162
350
|
}
|
package/assets/viewer.js
CHANGED
|
@@ -489,26 +489,86 @@ async function mountIamViewer(m, consoleContext) {
|
|
|
489
489
|
}
|
|
490
490
|
// Verify the compact source document first, then derive its exact v1 hashes.
|
|
491
491
|
// Work is page-local and cancellable; hashes never become another saved authority.
|
|
492
|
+
/* BEGIN COMPRESSED DOM */
|
|
493
|
+
function checkedDomPayload(document) {
|
|
494
|
+
const payload = document.domPayload;
|
|
495
|
+
if (!payload) return null;
|
|
496
|
+
if (document.html !== "" || !Array.isArray(document.nodes) || document.nodes.length || Object.keys(payload).sort().join(",") !== "bytes,data,encoding" || payload.encoding !== "gzip-json-v1" || !Number.isSafeInteger(payload.bytes) || payload.bytes < 1 || payload.bytes > 8_388_608 || typeof payload.data !== "string" || !payload.data.length || payload.data.length > 11_184_812 || payload.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(payload.data)) throw Error("Invalid compressed DOM payload");
|
|
497
|
+
return payload;
|
|
498
|
+
}
|
|
499
|
+
function parsedDomPayload(text) {
|
|
500
|
+
const value = JSON.parse(text);
|
|
501
|
+
if (!value || Object.keys(value).sort().join(",") !== "html,nodes" || typeof value.html !== "string" || value.html.length > 2_000_000 || !value.html.isWellFormed() || !Array.isArray(value.nodes) || value.nodes.length > 12000 || JSON.stringify(value) !== text) throw Error("Invalid expanded DOM payload");
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
async function documentDomBrowser(document) {
|
|
505
|
+
const payload = checkedDomPayload(document);
|
|
506
|
+
if (!payload) return { html: document.html, nodes: document.nodes };
|
|
507
|
+
if (typeof DecompressionStream !== "function") throw Error("Compressed IAM requires DecompressionStream support");
|
|
508
|
+
const binary = atob(payload.data);
|
|
509
|
+
if (btoa(binary) !== payload.data) throw Error("Noncanonical compressed DOM base64");
|
|
510
|
+
const compressed = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
511
|
+
const stream = new ReadableStream({ start(controller) { controller.enqueue(compressed); controller.close(); } }).pipeThrough(new DecompressionStream("gzip"));
|
|
512
|
+
const reader = stream.getReader(), chunks = [];
|
|
513
|
+
let total = 0;
|
|
514
|
+
try {
|
|
515
|
+
for (;;) {
|
|
516
|
+
const next = await reader.read();
|
|
517
|
+
if (next.done) break;
|
|
518
|
+
total += next.value.byteLength;
|
|
519
|
+
if (total > payload.bytes) throw Error("Expanded DOM byte budget exceeded");
|
|
520
|
+
chunks.push(next.value);
|
|
521
|
+
}
|
|
522
|
+
} finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
523
|
+
if (total !== payload.bytes) throw Error("Expanded DOM byte length mismatch");
|
|
524
|
+
const decoded = new Uint8Array(total);
|
|
525
|
+
let offset = 0;
|
|
526
|
+
for (const chunk of chunks) { decoded.set(chunk, offset); offset += chunk.byteLength; }
|
|
527
|
+
return parsedDomPayload(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
|
|
528
|
+
}
|
|
529
|
+
/* END COMPRESSED DOM */
|
|
492
530
|
/* BEGIN HTML DOM DERIVATION */
|
|
493
|
-
function expandDomNodes(document, body) {
|
|
531
|
+
function expandDomNodes(document, body, deriveTextId) {
|
|
494
532
|
if(document.nodeEncoding!=='html-preorder-v1'||document.nodeHashMode!=='derived-v1'||!document.sourceMappingsHash||document.nodes.length>12000)throw Error('Invalid HTML-derived DOM mode');
|
|
495
533
|
let index=0;const result=[];
|
|
496
|
-
function visit(node,parentId,depth){
|
|
534
|
+
function visit(node,parentId,depth,childIndex=0){
|
|
497
535
|
if(depth>128||![1,3].includes(node.nodeType))throw Error('Invalid HTML-derived DOM structure');
|
|
498
536
|
const ref=document.nodes[index++];
|
|
499
537
|
if(!ref||Object.keys(ref).some(key=>!['nodeId','elementKey','source','synthetic'].includes(key)))throw Error('Mixed HTML-derived DOM values');
|
|
500
|
-
const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId;
|
|
538
|
+
const element=node.nodeType===1,nodeId=element?node.getAttribute('data-iam-node'):ref.nodeId??deriveTextId?.(document.pageKey,parentId,childIndex);
|
|
501
539
|
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');
|
|
502
540
|
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:[]};
|
|
503
|
-
result.push(full);for(const child
|
|
541
|
+
result.push(full);for(const [i,child]of [...node.childNodes].entries())full.children.push(visit(child,nodeId,depth+1,i));return nodeId;
|
|
504
542
|
}
|
|
505
543
|
if(body.childNodes.length!==1||visit(body.firstChild,null,0)!==document.rootNodeId||index!==document.nodes.length)throw Error('HTML-derived DOM coverage differs');
|
|
506
544
|
return result;
|
|
507
545
|
}
|
|
508
546
|
/* END HTML DOM DERIVATION */
|
|
509
547
|
async function expandNodeHashes(document,request) {
|
|
548
|
+
if (document.domPayload) {
|
|
549
|
+
const {hashes, ...body} = document;
|
|
550
|
+
if (await hash("iam/document/v1\0" + canonical(body)) !== hashes.document) throw Error("Compressed document hash mismatch");
|
|
551
|
+
const decoded = await documentDomBrowser(document);
|
|
552
|
+
if (await hash(decoded.html) !== hashes.html || await hash("iam/dom-map/v1\0" + canonical({rootNodeId: document.rootNodeId, nodes: decoded.nodes, sourceMappingsHash: document.sourceMappingsHash})) !== hashes.dom) throw Error("Expanded DOM hash mismatch");
|
|
553
|
+
const {domPayload, ...plain} = document;
|
|
554
|
+
document = {...plain, ...decoded};
|
|
555
|
+
}
|
|
510
556
|
if(!Array.isArray(document.nodes)||document.nodes.length>12000)throw Error('Node hash budget');
|
|
511
|
-
const
|
|
557
|
+
const body=new DOMParser().parseFromString('<!doctype html><html><body>'+document.html+'</body></html>','text/html').body,textIds=new Map(),pendingText=[];
|
|
558
|
+
let preorder=0;
|
|
559
|
+
const collectText=(node,parentId,childIndex=0,depth=0)=>{
|
|
560
|
+
if(!node||depth>128||preorder>=12000)throw Error('DOM derivation budget');
|
|
561
|
+
const ref=document.nodes[preorder++];if(!ref)throw Error('Missing source node');
|
|
562
|
+
const id=node.nodeType===1?node.getAttribute('data-iam-node'):ref.nodeId;
|
|
563
|
+
if(node.nodeType===3&&!id)pendingText.push({parentId,index:childIndex,key:parentId+':'+childIndex});
|
|
564
|
+
for(const [i,child]of [...node.childNodes].entries())collectText(child,id,i,depth+1);
|
|
565
|
+
};
|
|
566
|
+
collectText(body.firstChild,null);if(preorder!==document.nodes.length)throw Error('Source node count differs');
|
|
567
|
+
for(let offset=0;offset<pendingText.length;offset+=64){
|
|
568
|
+
if(pendingDocument!==request)throw Error('Superseded document');
|
|
569
|
+
await Promise.all(pendingText.slice(offset,offset+64).map(async row=>textIds.set(row.key,'node_'+await hash('iam/identity/node/v1\0'+canonical({page:document.pageKey,parentId:row.parentId,index:row.index,kind:3})))));
|
|
570
|
+
}
|
|
571
|
+
const nodes=expandDomNodes(document,body,(_page,parentId,index)=>textIds.get(parentId+':'+index)),byId=new Map(nodes.map(node=>[node.nodeId,node]));
|
|
512
572
|
if(byId.size!==nodes.length)throw Error('Duplicate node');
|
|
513
573
|
for(let offset=0;offset<nodes.length;offset+=64){
|
|
514
574
|
if(pendingDocument!==request)throw Error('Superseded document');
|
|
@@ -601,7 +661,8 @@ function expandDomNodes(document, body) {
|
|
|
601
661
|
if (data.type === "open-page" && typeof data.routeKey === "string") {
|
|
602
662
|
const next = m.pages.find(p => p.pageKey === data.routeKey);
|
|
603
663
|
if (!next) return;
|
|
604
|
-
|
|
664
|
+
if(typeof data.renderKey==="string"&&!m.routeTree.renders.some(r=>r.routeKey===next.pageKey&&r.renderKey===data.renderKey))return;
|
|
665
|
+
const variableRender=typeof data.renderKey==="string"?data.renderKey:m.routeTree.variables.find(variable=>variable.variableKey===data.variableKey)?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===data.variableKey)?.renderKey;
|
|
605
666
|
const targetUrl=typeof data.url==="string"?data.url:(next.pageKey===page.pageKey?currentUrl:next.route);
|
|
606
667
|
if (!doc || next.pageKey !== page.pageKey || variableRender && variableRender!==render?.renderKey || targetUrl!==currentUrl) openPage(next,targetUrl,variableRender);
|
|
607
668
|
else consoleContext?.resume?.();
|
|
@@ -801,8 +862,14 @@ function expandDomNodes(document, body) {
|
|
|
801
862
|
if (m.documents.length) {
|
|
802
863
|
const initialRequest = {};
|
|
803
864
|
pendingDocument = initialRequest;
|
|
804
|
-
|
|
865
|
+
const expandedBytes = m.documents.reduce((total, document) => total + (document.domPayload?.bytes ?? new TextEncoder().encode(JSON.stringify({html:document.html,nodes:document.nodes})).byteLength), 0);
|
|
866
|
+
if (!Number.isSafeInteger(expandedBytes) || expandedBytes > 33_554_432) throw Error("Expanded DOM aggregate byte budget exceeded");
|
|
867
|
+
// Sequential decoding bounds peak memory for a multi-route artifact.
|
|
868
|
+
const expandedDocuments = [];
|
|
869
|
+
for (const document of m.documents) expandedDocuments.push(await expandNodeHashes(document, initialRequest));
|
|
870
|
+
m.documents = expandedDocuments;
|
|
805
871
|
pendingDocument = null;
|
|
872
|
+
renderNav();
|
|
806
873
|
}
|
|
807
874
|
openPage(page,consoleContext?.initialPage?.url??page.route,consoleContext?.initialPage?.renderKey??consoleContext?.catalog?.graph.variables.find(variable=>variable.id===consoleContext.initialPage?.variableKey)?.renderKey);
|
|
808
875
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinkerlandai/indiana-iam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Build-time Indiana App Manifest (IAM) generator for Next.js and Vite apps: scans source after the production build and writes the IAM and its release descriptor.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"./next": {
|
|
34
34
|
"types": "./src/next.d.ts",
|
|
35
|
-
"import": "./src/next.mjs"
|
|
35
|
+
"import": "./src/next.mjs",
|
|
36
|
+
"require": "./src/next.cjs"
|
|
36
37
|
},
|
|
37
38
|
"./vite": {
|
|
38
39
|
"types": "./src/vite.d.ts",
|
|
@@ -46,20 +47,12 @@
|
|
|
46
47
|
"ajv": "6.15.0",
|
|
47
48
|
"css-tree": "3.2.1",
|
|
48
49
|
"jsdom": "26.1.0",
|
|
50
|
+
"lucide-static": "1.34.0",
|
|
49
51
|
"magic-string": "0.30.21",
|
|
52
|
+
"pako": "3.0.2",
|
|
53
|
+
"sharp": "0.35.5",
|
|
50
54
|
"tailwind-merge": "3.7.0",
|
|
51
55
|
"tailwindcss": "4.3.3",
|
|
52
56
|
"typescript": "5.9.3"
|
|
53
|
-
},
|
|
54
|
-
"peerDependencies": {
|
|
55
|
-
"sharp": "0.35.5"
|
|
56
|
-
},
|
|
57
|
-
"peerDependenciesMeta": {
|
|
58
|
-
"sharp": {
|
|
59
|
-
"optional": true
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"sharp": "0.35.5"
|
|
64
57
|
}
|
|
65
58
|
}
|