ooxml.js 1.0.0 → 1.2.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 +14 -5
- package/dist/index.cjs +21 -15
- package/dist/index.js +21 -15
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ooxml.js
|
|
2
2
|
|
|
3
|
+
[](https://github.com/ExaDev/ooxml.js) [](https://www.npmjs.com/package/ooxml.js) [](https://github.com/ExaDev/ooxml.js/actions)
|
|
4
|
+
|
|
3
5
|
> Type-safe, lossless round-trip conversion between OOXML packages (`.docx`, `.pptx`, `.xlsx`) and a faithful JSON model, built on [Zod 4](https://zod.dev) codecs.
|
|
4
6
|
|
|
5
7
|
An OOXML file is a ZIP archive of parts (an OPC "package"): `[Content_Types].xml`, relationships (`*.rels`), XML content parts, and binary parts (images, embedded objects). `ooxml.js` decodes the **whole package** into a faithful JSON model and encodes it back — part for part — so `encode(decode(file))` reproduces the original content.
|
|
@@ -140,13 +142,14 @@ const out = encodeCompactPackage(compact); // CompactPackage -> OOXML bytes dire
|
|
|
140
142
|
|
|
141
143
|
```sh
|
|
142
144
|
pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts, via tsdown.config.ts)
|
|
145
|
+
pnpm lint # eslint . --max-warnings 0
|
|
143
146
|
pnpm typecheck # tsc --noEmit
|
|
144
147
|
pnpm test # vitest run
|
|
145
148
|
pnpm test:watch # vitest
|
|
146
149
|
pnpm test:smoke # builds dist/, then runs test/smoke.test.mjs to verify the built ESM and CJS artifacts both load and behave identically
|
|
147
150
|
```
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
`pnpm prepublishOnly` runs `lint`, `typecheck`, `tsdown`, `publint`, and `@arethetypeswrong/cli` (`attw --pack`) — the full publish-readiness check — before a release.
|
|
150
153
|
|
|
151
154
|
`test/smoke.test.mjs` loads the actual built `dist/index.js` (ESM) and `dist/index.cjs` (CJS) artifacts and checks they load and behave identically — a check none of `vitest`'s normal run, `tsc`, `publint`, or `attw` can do, since those either run against source or statically analyse package metadata without executing the compiled output. `vitest.config.ts` defines it as its own `smoke` project (vitest's `test.projects`), separate from the `unit` project (`src/**/*.test.ts`); `pnpm test`/`test:watch` pass `--project unit` and `pnpm test:smoke` passes `--project smoke` after `tsdown` rebuilds `dist/`, so neither run touches the other project's files.
|
|
152
155
|
|
|
@@ -170,15 +173,15 @@ The package is layered from a lossless core outward to lossy convenience views:
|
|
|
170
173
|
- **`XmlNode` uses a recursive structural guard, not `z.lazy`.** `z.lazy` collapses to `unknown` for the element-children case in the Zod version this project pins, so `XmlElementSchema` validates `children` via `z.custom<XmlNode>(isXmlNode)`, a hand-written recursive type guard in `model/node.ts`. Any change to `XmlNode`'s shape must update `isXmlNode` in step. `src/compact.ts`'s `CompactXmlNode` reuses the same pattern (`isCompactXmlNode` + `z.custom`) for the same reason.
|
|
171
174
|
- **Lossless core vs. lossy views is a hard boundary.** `decodePackage`/`encodePackage` (and the underlying codecs) must stay byte/part faithful — every part round-trips unchanged. `src/typed/*` readers are explicitly one-way and are allowed to drop information (documented per-reader, e.g. `readDocx`'s bold/italic toggle presence check ignores `w:val`, and `readXlsx` drops cell styles, formats and charts). Don't blur this line by adding write-back support to a typed reader; a full round-trip always goes through the generic `Package`.
|
|
172
175
|
- **XML entities stay raw in the lossless layer.** `parseXml` runs with `processEntities: false` so encoded entities (e.g. `&`) are preserved verbatim for round-trip fidelity; typed readers decode the five standard entities (`decodeEntities` in `typed/util.ts`) only in their own lossy projection, never in the core model.
|
|
176
|
+
- **No type assertions.** `eslint.config.ts` runs `@typescript-eslint/consistent-type-assertions` with `assertionStyle: "never"`, banning `as` and angle-bracket casts outright, with `linterOptions.noInlineConfig: true` so there is no `eslint-disable` escape hatch either — narrow with a guard or parse with Zod. An exception would have to be scoped structurally, as a `files`-matched override block in `eslint.config.ts`, not an inline comment.
|
|
173
177
|
|
|
174
178
|
## Gotchas and quirks
|
|
175
179
|
|
|
176
|
-
- **No git remote is configured yet.** `git remote -v` is empty; this repository has not been pushed anywhere. Confirm the intended origin before assuming a `git push` target.
|
|
177
180
|
- **`test:smoke` depends on a fresh build.** It runs `tsdown && vitest run --project smoke`, so it always rebuilds `dist/` first — don't run it expecting to test a stale build.
|
|
178
181
|
- **`--project` matters for `test/smoke.test.mjs`.** `vitest.config.ts` defines `unit` and `smoke` as separate projects; `pnpm test`/`test:watch`/`test:smoke` always pass the right `--project` flag. A bare `vitest`/`vitest run` with no `--project` filter runs both projects, and `smoke` fails loudly (`Cannot find module '../dist/index.js'`) if `dist/` hasn't been built yet — a clear failure pointing at the cause, not a silent false pass, but still worth knowing if you invoke `vitest` directly instead of through the npm scripts.
|
|
179
182
|
- **Binary-vs-XML part classification is a byte sniff, not an extension check.** `package-io/read.ts`'s `looksLikeXml` looks for a leading `<` after skipping a UTF-8 BOM and whitespace; this is deliberate (no standard OOXML binary part starts with `<`) but means any future binary format starting with `<` would misclassify.
|
|
180
|
-
- **`
|
|
181
|
-
- **
|
|
183
|
+
- **`Array.isArray` narrows `unknown` to `any[]`, not `unknown[]`.** `lib.es5.d.ts` types its parameter as `any`, so TypeScript can't do better even after the check succeeds — indexing straight into the result (e.g. `value[0]`) silently reintroduces `any` and trips `@typescript-eslint/no-unsafe-assignment`. `compact.ts` and `xml/parse.ts` each define a local `isUnknownArray` guard (`value is unknown[]`) for exactly this reason; reach for it instead of `Array.isArray` wherever the narrowed element is going to be read.
|
|
184
|
+
- **TypeScript is pinned to the latest 6.x, not 7.** TypeScript 7 restructured its JS-facing API surface heavily enough that both `typescript-eslint` (peer range `<6.1.0`) and `cosmiconfig`'s TypeScript loader (used by `semantic-release` to read `release.config.ts`, via `typescript.findConfigFile`, which TS 7 no longer exports) break under it. Upgrading past 6.x has to wait for that ecosystem tooling to add TS 7 support.
|
|
182
185
|
|
|
183
186
|
## Fidelity
|
|
184
187
|
|
|
@@ -186,9 +189,15 @@ Conversion is **part-content-faithful**: every XML part re-serialises to equival
|
|
|
186
189
|
|
|
187
190
|
It is **not** guaranteed to be byte-for-byte identical at the ZIP-container level — re-zipping changes archive entry layout (entry order, compression, metadata), and that is not achievable deterministically across the tools that produce OOXML files.
|
|
188
191
|
|
|
192
|
+
## Release and publishing
|
|
193
|
+
|
|
194
|
+
`.github/workflows/ci.yml` runs commitlint, lint, typecheck, the unit suite, and the smoke test on every push and pull request. On a push to `main` where those all pass, `release.config.ts` drives [semantic-release](https://semantic-release.gitbook.io/semantic-release): commit history since the last tag decides the version bump, `CHANGELOG.md` and `package.json` are committed back to `main`, a GitHub Release is cut, and the package publishes to [npmjs.org](https://www.npmjs.com/package/ooxml.js) — via npm's OIDC trusted publishing, so no `NPM_TOKEN` exists anywhere in the pipeline.
|
|
195
|
+
|
|
196
|
+
Whether that release actually published a new version is detected by diffing `package.json`'s version before and after the release step, not by trusting a third-party action's own detection. Two further jobs gate on that: one republishes the same build under the scoped `@exadev/ooxml.js` alias to GitHub Packages (which has no OIDC exchange of its own, so it authenticates with `GITHUB_TOKEN` instead), and one packs the release into its own directory, generates an SPDX SBOM (`pnpm sbom`), and signs both an SBOM and a build-provenance attestation against that exact tarball — verifiable independently of the registry, and still present if the package is later unpublished.
|
|
197
|
+
|
|
189
198
|
## Contributing
|
|
190
199
|
|
|
191
|
-
Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore
|
|
200
|
+
Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore:`, …), enforced by commitlint (`commitlint.config.ts`) via a husky `commit-msg` hook and a CI `commitlint` job — semantic-release's version bump depends on these being well-formed, not just style. A husky `pre-commit` hook runs `lint-staged` (`eslint --fix` on staged `*.ts` files) and `pre-push` runs the test suite. There is a single `main` branch and no open pull request workflow established so far.
|
|
192
201
|
|
|
193
202
|
## License
|
|
194
203
|
|
package/dist/index.cjs
CHANGED
|
@@ -32,15 +32,15 @@ function isRecord$1(value) {
|
|
|
32
32
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
33
|
}
|
|
34
34
|
function isAttribute(value) {
|
|
35
|
-
return isRecord$1(value) && typeof value
|
|
35
|
+
return isRecord$1(value) && typeof value.name === "string" && typeof value.value === "string";
|
|
36
36
|
}
|
|
37
37
|
function isXmlNode(value) {
|
|
38
38
|
if (!isRecord$1(value)) return false;
|
|
39
|
-
const t = value
|
|
40
|
-
if (t === "text" || t === "cdata" || t === "comment") return typeof value
|
|
41
|
-
if (t === "declaration") return Array.isArray(value
|
|
42
|
-
if (t === "pi") return typeof value
|
|
43
|
-
if (t === "element") return typeof value
|
|
39
|
+
const t = value.type;
|
|
40
|
+
if (t === "text" || t === "cdata" || t === "comment") return typeof value.value === "string";
|
|
41
|
+
if (t === "declaration") return Array.isArray(value.attributes) && value.attributes.every(isAttribute);
|
|
42
|
+
if (t === "pi") return typeof value.target === "string" && typeof value.content === "string";
|
|
43
|
+
if (t === "element") return typeof value.tag === "string" && Array.isArray(value.attributes) && value.attributes.every(isAttribute) && Array.isArray(value.children) && value.children.every(isXmlNode);
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
const XmlElementSchema = zod.z.object({
|
|
@@ -133,12 +133,15 @@ function parseXml(xml) {
|
|
|
133
133
|
function isRecord(value) {
|
|
134
134
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
135
135
|
}
|
|
136
|
+
function isUnknownArray$1(value) {
|
|
137
|
+
return Array.isArray(value);
|
|
138
|
+
}
|
|
136
139
|
function asString(value) {
|
|
137
140
|
if (typeof value !== "string") throw new Error(`expected string while parsing XML, got ${typeof value}`);
|
|
138
141
|
return value;
|
|
139
142
|
}
|
|
140
143
|
function parseNodes(raw) {
|
|
141
|
-
if (!
|
|
144
|
+
if (!isUnknownArray$1(raw)) throw new Error("fast-xml-parser output was not an ordered array");
|
|
142
145
|
return raw.map(parseNode);
|
|
143
146
|
}
|
|
144
147
|
function parseNode(raw) {
|
|
@@ -156,11 +159,11 @@ function parseNode(raw) {
|
|
|
156
159
|
};
|
|
157
160
|
if (tagKey === "__comment") return {
|
|
158
161
|
type: "comment",
|
|
159
|
-
value: scalarText(raw
|
|
162
|
+
value: scalarText(raw.__comment)
|
|
160
163
|
};
|
|
161
164
|
if (tagKey === "__cdata") return {
|
|
162
165
|
type: "cdata",
|
|
163
|
-
value: scalarText(raw
|
|
166
|
+
value: scalarText(raw.__cdata)
|
|
164
167
|
};
|
|
165
168
|
if (tagKey === "?xml") return {
|
|
166
169
|
type: "declaration",
|
|
@@ -192,7 +195,7 @@ function parseAttributes(raw) {
|
|
|
192
195
|
return attrs;
|
|
193
196
|
}
|
|
194
197
|
function scalarText(raw) {
|
|
195
|
-
if (!
|
|
198
|
+
if (!isUnknownArray$1(raw) || raw.length === 0) throw new Error("expected a scalar-text wrapper array");
|
|
196
199
|
const first = raw[0];
|
|
197
200
|
if (!isRecord(first)) throw new Error("scalar-text wrapper was not an object");
|
|
198
201
|
return asString(first["#text"]);
|
|
@@ -308,11 +311,14 @@ function encodePackage(pkg) {
|
|
|
308
311
|
}
|
|
309
312
|
//#endregion
|
|
310
313
|
//#region src/compact.ts
|
|
314
|
+
function isUnknownArray(value) {
|
|
315
|
+
return Array.isArray(value);
|
|
316
|
+
}
|
|
311
317
|
function isCompactAttrPairs(value) {
|
|
312
|
-
return
|
|
318
|
+
return isUnknownArray(value) && value.every((v) => typeof v === "number");
|
|
313
319
|
}
|
|
314
320
|
function isCompactXmlNode(value) {
|
|
315
|
-
if (!
|
|
321
|
+
if (!isUnknownArray(value)) return false;
|
|
316
322
|
const code = value[0];
|
|
317
323
|
if (code === 1 || code === 2 || code === 3) return value.length === 2 && typeof value[1] === "number";
|
|
318
324
|
if (code === 4) return value.length === 2 && isCompactAttrPairs(value[1]);
|
|
@@ -473,7 +479,7 @@ function attr(element, name) {
|
|
|
473
479
|
for (const a of element.attributes) if (a.name === name) return a.value;
|
|
474
480
|
}
|
|
475
481
|
function rootElement(part) {
|
|
476
|
-
if (part
|
|
482
|
+
if (part?.kind !== "xml") return;
|
|
477
483
|
for (const node of part.nodes) if (node.type === "element") return node;
|
|
478
484
|
}
|
|
479
485
|
function decodeEntities(value) {
|
|
@@ -647,7 +653,7 @@ function readHeaderFooterText(pkg, prefix) {
|
|
|
647
653
|
for (const path of Object.keys(pkg.parts)) {
|
|
648
654
|
if (!path.startsWith(prefix) || !path.endsWith(".xml")) continue;
|
|
649
655
|
const part = pkg.parts[path];
|
|
650
|
-
if (part
|
|
656
|
+
if (part?.kind !== "xml") continue;
|
|
651
657
|
out.push(elementsWithTag(part.nodes, "w:t").map(textContent).join(""));
|
|
652
658
|
}
|
|
653
659
|
return out;
|
|
@@ -708,7 +714,7 @@ function readNotes(pkg, slidePath) {
|
|
|
708
714
|
}
|
|
709
715
|
if (notesPath === void 0) return "";
|
|
710
716
|
const part = pkg.parts[notesPath];
|
|
711
|
-
if (part
|
|
717
|
+
if (part?.kind !== "xml") return "";
|
|
712
718
|
return elementsWithTag(part.nodes, "a:t").map(textContent).join("");
|
|
713
719
|
}
|
|
714
720
|
function readPptx(pkg) {
|
package/dist/index.js
CHANGED
|
@@ -31,15 +31,15 @@ function isRecord$1(value) {
|
|
|
31
31
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
32
|
}
|
|
33
33
|
function isAttribute(value) {
|
|
34
|
-
return isRecord$1(value) && typeof value
|
|
34
|
+
return isRecord$1(value) && typeof value.name === "string" && typeof value.value === "string";
|
|
35
35
|
}
|
|
36
36
|
function isXmlNode(value) {
|
|
37
37
|
if (!isRecord$1(value)) return false;
|
|
38
|
-
const t = value
|
|
39
|
-
if (t === "text" || t === "cdata" || t === "comment") return typeof value
|
|
40
|
-
if (t === "declaration") return Array.isArray(value
|
|
41
|
-
if (t === "pi") return typeof value
|
|
42
|
-
if (t === "element") return typeof value
|
|
38
|
+
const t = value.type;
|
|
39
|
+
if (t === "text" || t === "cdata" || t === "comment") return typeof value.value === "string";
|
|
40
|
+
if (t === "declaration") return Array.isArray(value.attributes) && value.attributes.every(isAttribute);
|
|
41
|
+
if (t === "pi") return typeof value.target === "string" && typeof value.content === "string";
|
|
42
|
+
if (t === "element") return typeof value.tag === "string" && Array.isArray(value.attributes) && value.attributes.every(isAttribute) && Array.isArray(value.children) && value.children.every(isXmlNode);
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
const XmlElementSchema = z.object({
|
|
@@ -132,12 +132,15 @@ function parseXml(xml) {
|
|
|
132
132
|
function isRecord(value) {
|
|
133
133
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
134
134
|
}
|
|
135
|
+
function isUnknownArray$1(value) {
|
|
136
|
+
return Array.isArray(value);
|
|
137
|
+
}
|
|
135
138
|
function asString(value) {
|
|
136
139
|
if (typeof value !== "string") throw new Error(`expected string while parsing XML, got ${typeof value}`);
|
|
137
140
|
return value;
|
|
138
141
|
}
|
|
139
142
|
function parseNodes(raw) {
|
|
140
|
-
if (!
|
|
143
|
+
if (!isUnknownArray$1(raw)) throw new Error("fast-xml-parser output was not an ordered array");
|
|
141
144
|
return raw.map(parseNode);
|
|
142
145
|
}
|
|
143
146
|
function parseNode(raw) {
|
|
@@ -155,11 +158,11 @@ function parseNode(raw) {
|
|
|
155
158
|
};
|
|
156
159
|
if (tagKey === "__comment") return {
|
|
157
160
|
type: "comment",
|
|
158
|
-
value: scalarText(raw
|
|
161
|
+
value: scalarText(raw.__comment)
|
|
159
162
|
};
|
|
160
163
|
if (tagKey === "__cdata") return {
|
|
161
164
|
type: "cdata",
|
|
162
|
-
value: scalarText(raw
|
|
165
|
+
value: scalarText(raw.__cdata)
|
|
163
166
|
};
|
|
164
167
|
if (tagKey === "?xml") return {
|
|
165
168
|
type: "declaration",
|
|
@@ -191,7 +194,7 @@ function parseAttributes(raw) {
|
|
|
191
194
|
return attrs;
|
|
192
195
|
}
|
|
193
196
|
function scalarText(raw) {
|
|
194
|
-
if (!
|
|
197
|
+
if (!isUnknownArray$1(raw) || raw.length === 0) throw new Error("expected a scalar-text wrapper array");
|
|
195
198
|
const first = raw[0];
|
|
196
199
|
if (!isRecord(first)) throw new Error("scalar-text wrapper was not an object");
|
|
197
200
|
return asString(first["#text"]);
|
|
@@ -307,11 +310,14 @@ function encodePackage(pkg) {
|
|
|
307
310
|
}
|
|
308
311
|
//#endregion
|
|
309
312
|
//#region src/compact.ts
|
|
313
|
+
function isUnknownArray(value) {
|
|
314
|
+
return Array.isArray(value);
|
|
315
|
+
}
|
|
310
316
|
function isCompactAttrPairs(value) {
|
|
311
|
-
return
|
|
317
|
+
return isUnknownArray(value) && value.every((v) => typeof v === "number");
|
|
312
318
|
}
|
|
313
319
|
function isCompactXmlNode(value) {
|
|
314
|
-
if (!
|
|
320
|
+
if (!isUnknownArray(value)) return false;
|
|
315
321
|
const code = value[0];
|
|
316
322
|
if (code === 1 || code === 2 || code === 3) return value.length === 2 && typeof value[1] === "number";
|
|
317
323
|
if (code === 4) return value.length === 2 && isCompactAttrPairs(value[1]);
|
|
@@ -472,7 +478,7 @@ function attr(element, name) {
|
|
|
472
478
|
for (const a of element.attributes) if (a.name === name) return a.value;
|
|
473
479
|
}
|
|
474
480
|
function rootElement(part) {
|
|
475
|
-
if (part
|
|
481
|
+
if (part?.kind !== "xml") return;
|
|
476
482
|
for (const node of part.nodes) if (node.type === "element") return node;
|
|
477
483
|
}
|
|
478
484
|
function decodeEntities(value) {
|
|
@@ -646,7 +652,7 @@ function readHeaderFooterText(pkg, prefix) {
|
|
|
646
652
|
for (const path of Object.keys(pkg.parts)) {
|
|
647
653
|
if (!path.startsWith(prefix) || !path.endsWith(".xml")) continue;
|
|
648
654
|
const part = pkg.parts[path];
|
|
649
|
-
if (part
|
|
655
|
+
if (part?.kind !== "xml") continue;
|
|
650
656
|
out.push(elementsWithTag(part.nodes, "w:t").map(textContent).join(""));
|
|
651
657
|
}
|
|
652
658
|
return out;
|
|
@@ -707,7 +713,7 @@ function readNotes(pkg, slidePath) {
|
|
|
707
713
|
}
|
|
708
714
|
if (notesPath === void 0) return "";
|
|
709
715
|
const part = pkg.parts[notesPath];
|
|
710
|
-
if (part
|
|
716
|
+
if (part?.kind !== "xml") return "";
|
|
711
717
|
return elementsWithTag(part.nodes, "a:t").map(textContent).join("");
|
|
712
718
|
}
|
|
713
719
|
function readPptx(pkg) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ooxml.js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Type-safe, lossless round-trip conversion between OOXML packages (docx, pptx, xlsx) and JSON, built on Zod 4 codecs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsdown",
|
|
41
|
-
"prepublishOnly": "pnpm run typecheck && tsdown && publint && attw --pack",
|
|
41
|
+
"prepublishOnly": "pnpm run lint && pnpm run typecheck && tsdown && publint && attw --pack",
|
|
42
|
+
"lint": "eslint . --max-warnings 0",
|
|
42
43
|
"typecheck": "tsc --noEmit",
|
|
43
44
|
"test": "vitest run --project unit",
|
|
44
45
|
"test:watch": "vitest --project unit",
|
|
@@ -67,14 +68,19 @@
|
|
|
67
68
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
68
69
|
"@commitlint/cli": "^21.2.1",
|
|
69
70
|
"@commitlint/config-conventional": "^21.2.0",
|
|
71
|
+
"@eslint/js": "^10.0.1",
|
|
70
72
|
"@semantic-release/changelog": "^7.0.0",
|
|
71
73
|
"@semantic-release/git": "^11.0.1",
|
|
72
74
|
"@types/node": "^26.1.1",
|
|
75
|
+
"eslint": "^10.8.0",
|
|
76
|
+
"globals": "^17.8.0",
|
|
73
77
|
"husky": "^9.1.7",
|
|
78
|
+
"lint-staged": "^17.2.0",
|
|
74
79
|
"publint": "^0.3.21",
|
|
75
80
|
"semantic-release": "^25.0.8",
|
|
76
81
|
"tsdown": "^0.22.13",
|
|
77
|
-
"typescript": "^
|
|
82
|
+
"typescript": "^6.0.3",
|
|
83
|
+
"typescript-eslint": "^8.65.0",
|
|
78
84
|
"vitest": "^4.1.10"
|
|
79
85
|
}
|
|
80
86
|
}
|