From 0448d5bd2c082260d2d9beba83098d74877a27ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Thu, 3 Sep 2026 19:13:33 +0800 Subject: [PATCH] feat(nacos): add config syntax validation with error location --- .../components/nacos/NacosAdminConsole.vue | 137 ++++++++- apps/desktop/src/i18n/locales/en.ts | 7 + apps/desktop/src/i18n/locales/es.ts | 7 + apps/desktop/src/i18n/locales/it.ts | 7 + apps/desktop/src/i18n/locales/ja.ts | 7 + apps/desktop/src/i18n/locales/ko.ts | 7 + apps/desktop/src/i18n/locales/pt-BR.ts | 7 + apps/desktop/src/i18n/locales/zh-CN.ts | 7 + apps/desktop/src/i18n/locales/zh-TW.ts | 7 + .../nacos/nacosConfigValidation.spec.ts | 75 +++++ .../src/lib/nacos/nacosConfigValidation.ts | 269 ++++++++++++++++++ flake.nix | 2 +- package.json | 2 + pnpm-lock.yaml | 67 +++++ 14 files changed, 604 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/nacos/nacosConfigValidation.spec.ts create mode 100644 apps/desktop/src/lib/nacos/nacosConfigValidation.ts diff --git a/apps/desktop/src/components/nacos/NacosAdminConsole.vue b/apps/desktop/src/components/nacos/NacosAdminConsole.vue index dd6a92c91e..8384a285dd 100644 --- a/apps/desktop/src/components/nacos/NacosAdminConsole.vue +++ b/apps/desktop/src/components/nacos/NacosAdminConsole.vue @@ -1,8 +1,8 @@ ", "html")).toBeNull(); + expect(validateNacosConfig("key=\\u12xz", "properties")?.message).toContain("Unicode"); + expect(validateNacosConfig("server = [", "toml")?.message).toContain("TOML"); + expect(validateNacosConfig('title = "unterminated', "toml")?.message).toContain("TOML"); + expect(validateNacosConfig('title = "invalid\\q"', "toml")?.message).toContain("TOML"); + expect(validateNacosConfig("ports = [1 2]", "toml")?.message).toContain("TOML"); + expect(validateNacosConfig("enabled = truth", "toml")?.message).toContain("TOML"); + expect(validateNacosConfig("[server] trailing", "toml")?.message).toContain("TOML"); + expect(validateNacosConfig('name = "dbx"\nname = "duplicate"', "toml")?.message).toContain("TOML"); + }); +}); diff --git a/apps/desktop/src/lib/nacos/nacosConfigValidation.ts b/apps/desktop/src/lib/nacos/nacosConfigValidation.ts new file mode 100644 index 0000000000..7f09517002 --- /dev/null +++ b/apps/desktop/src/lib/nacos/nacosConfigValidation.ts @@ -0,0 +1,269 @@ +import { htmlLanguage } from "@codemirror/lang-html"; +import { jsonLanguage } from "@codemirror/lang-json"; +import { XMLValidator } from "fast-xml-parser"; +import { parse as parseToml, TomlError } from "smol-toml"; +import { parseDocument } from "yaml"; + +export type NacosConfigValidationFormat = "text" | "json" | "xml" | "yaml" | "html" | "properties" | "toml"; + +export interface NacosConfigDiagnostic { + message: string; + line: number; + column: number; + from: number; + to: number; +} + +function lineColumnAt(text: string, offset: number) { + const bounded = Math.max(0, Math.min(offset, text.length)); + const before = text.slice(0, bounded); + const lineStart = before.lastIndexOf("\n") + 1; + return { line: before.split("\n").length, column: bounded - lineStart + 1 }; +} + +function diagnosticAt(text: string, offset: number, message: string, length = 1): NacosConfigDiagnostic { + const position = lineColumnAt(text, offset); + return { message, ...position, from: offset, to: Math.min(text.length, Math.max(offset + length, offset + 1)) }; +} + +function offsetAtLineColumn(text: string, line: number, column: number): number { + let offset = 0; + for (let currentLine = 1; currentLine < line && offset < text.length; currentLine += 1) { + const newline = text.indexOf("\n", offset); + offset = newline < 0 ? text.length : newline + 1; + } + return Math.min(text.length, offset + Math.max(0, column - 1)); +} + +type SyntaxErrorRange = { from: number; to: number }; +type HtmlOpenTag = { name: string; offset: number }; + +const HTML_VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]); +const HTML_OPTIONAL_END_TAGS = new Set(["html", "head", "body", "colgroup", "li", "dt", "dd", "p", "rt", "rp", "optgroup", "option", "thead", "tbody", "tfoot", "tr", "td", "th"]); +const HTML_RAW_TEXT_TAGS = new Set(["script", "style", "textarea", "title"]); +const HTML_BLOCK_TAGS_THAT_CLOSE_P = new Set(["address", "article", "aside", "blockquote", "div", "dl", "fieldset", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "main", "menu", "nav", "ol", "p", "pre", "section", "table", "ul"]); + +function syntaxErrors(text: string, language: { parser: { parse: (source: string) => { iterate: (spec: { enter: (node: { type: { isError: boolean }; from: number; to: number }) => void }) => void } } }): SyntaxErrorRange[] { + const errors: SyntaxErrorRange[] = []; + language.parser.parse(text).iterate({ + enter(node) { + if (node.type.isError) errors.push({ from: node.from, to: node.to }); + }, + }); + return errors.filter((error, index) => errors.findIndex((candidate) => candidate.from === error.from && candidate.to === error.to) === index); +} + +function firstSyntaxError(text: string, language: { parser: { parse: (source: string) => { iterate: (spec: { enter: (node: { type: { isError: boolean }; from: number; to: number }) => void }) => void } } }, label: string) { + const error = syntaxErrors(text, language)[0]; + return error ? diagnosticAt(text, error.from, `Invalid ${label} syntax`, Math.max(1, error.to - error.from)) : null; +} + +function htmlTagEnd(text: string, start: number): number { + let quote = ""; + for (let index = start; index < text.length; index += 1) { + const character = text[index]; + if (quote) { + if (character === quote) quote = ""; + } else if (character === '"' || character === "'") { + quote = character; + } else if (character === ">") { + return index; + } + } + return -1; +} + +function closeImplicitHtmlTags(stack: HtmlOpenTag[], openingTag: string) { + const current = stack[stack.length - 1]?.name; + if (!current) return; + if (openingTag === "li" && current === "li") stack.pop(); + else if (["dt", "dd"].includes(openingTag) && ["dt", "dd"].includes(current)) stack.pop(); + else if (openingTag === "option" && current === "option") stack.pop(); + else if (openingTag === "optgroup" && ["option", "optgroup"].includes(current)) stack.pop(); + else if (["thead", "tbody", "tfoot"].includes(openingTag) && ["thead", "tbody", "tfoot"].includes(current)) stack.pop(); + else if (openingTag === "tr" && current === "tr") stack.pop(); + else if (["td", "th"].includes(openingTag) && ["td", "th"].includes(current)) stack.pop(); + else if (current === "p" && HTML_BLOCK_TAGS_THAT_CLOSE_P.has(openingTag)) stack.pop(); +} + +function validateHtml(text: string): NacosConfigDiagnostic | null { + const syntaxError = firstSyntaxError(text, htmlLanguage, "HTML"); + if (syntaxError) return syntaxError; + + const stack: HtmlOpenTag[] = []; + for (let offset = 0; offset < text.length; ) { + if (text[offset] !== "<") { + offset += 1; + continue; + } + if (text.startsWith("", offset + 4); + if (end < 0) return diagnosticAt(text, offset, "Invalid HTML syntax: unclosed comment"); + offset = end + 3; + continue; + } + if (text.startsWith("", offset + 9); + if (end < 0) return diagnosticAt(text, offset, "Invalid HTML syntax: unclosed CDATA section"); + offset = end + 3; + continue; + } + if (text[offset + 1] === "!" || text[offset + 1] === "?") { + const end = htmlTagEnd(text, offset + 2); + if (end < 0) return diagnosticAt(text, offset, "Invalid HTML syntax: unclosed declaration"); + offset = end + 1; + continue; + } + + const closing = text[offset + 1] === "/"; + const nameStart = offset + (closing ? 2 : 1); + const name = /^[A-Za-z][A-Za-z0-9:_-]*/.exec(text.slice(nameStart))?.[0]?.toLowerCase(); + if (!name) { + offset += 1; + continue; + } + const end = htmlTagEnd(text, nameStart + name.length); + if (end < 0) return diagnosticAt(text, offset, "Invalid HTML syntax: unclosed tag"); + const selfClosing = /\/\s*$/.test(text.slice(nameStart + name.length, end)); + offset = end + 1; + + if (closing) { + while (stack.length && stack[stack.length - 1]?.name !== name && HTML_OPTIONAL_END_TAGS.has(stack[stack.length - 1]?.name || "")) stack.pop(); + const openTag = stack[stack.length - 1]; + if (!openTag) return diagnosticAt(text, nameStart, `Invalid HTML syntax: closing tag '${name}' has not been opened`, name.length); + if (openTag.name !== name) return diagnosticAt(text, nameStart, `Invalid HTML syntax: expected closing tag '${openTag.name}'`, name.length); + stack.pop(); + continue; + } + + closeImplicitHtmlTags(stack, name); + if (!selfClosing && !HTML_VOID_TAGS.has(name)) stack.push({ name, offset }); + if (HTML_RAW_TEXT_TAGS.has(name)) { + const closingTag = new RegExp(``, "i"); + const match = closingTag.exec(text.slice(offset)); + if (!match || match.index === undefined) return diagnosticAt(text, offset - 1, `Invalid HTML syntax: unclosed tag '${name}'`, name.length); + offset += match.index; + } + } + + const unclosed = [...stack].reverse().find((tag) => !HTML_OPTIONAL_END_TAGS.has(tag.name)); + return unclosed ? diagnosticAt(text, unclosed.offset, `Invalid HTML syntax: unclosed tag '${unclosed.name}'`, unclosed.name.length) : null; +} + +function jsonErrorOffset(text: string, error: SyntaxErrorRange): number { + if (error.to > error.from) return error.from; + const previous = previousNonWhitespaceOffset(text, error.from); + const next = nextNonWhitespaceOffset(text, error.from); + if (previous >= 0 && text[previous] === "," && next < text.length && (text[next] === "}" || text[next] === "]")) return previous; + if (previous >= 0 && (next >= text.length || "{[}]".includes(text[next]))) return previous; + return next < text.length ? next : Math.max(0, previous); +} + +function trailingCommaOffset(text: string, offset: number): number | null { + const previous = previousNonWhitespaceOffset(text, offset); + const next = nextNonWhitespaceOffset(text, offset); + return previous >= 0 && text[previous] === "," && next < text.length && (text[next] === "}" || text[next] === "]") ? previous : null; +} + +function validateJson(text: string): NacosConfigDiagnostic[] { + try { + JSON.parse(text); + return []; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const errors = syntaxErrors(text, jsonLanguage); + if (errors.length) { + return errors.map((syntaxError, index) => { + const offset = jsonErrorOffset(text, syntaxError); + const trailingComma = trailingCommaOffset(text, syntaxError.from) ?? trailingCommaOffset(text, offset); + if (trailingComma !== null) return diagnosticAt(text, trailingComma, "Trailing comma is not allowed in JSON"); + return diagnosticAt(text, offset, index === 0 ? message : "Invalid JSON syntax", Math.max(1, syntaxError.to - syntaxError.from)); + }); + } + const match = /position\s+(\d+)/i.exec(message); + return [diagnosticAt(text, match ? Number(match[1]) : 0, message)]; + } +} + +function previousNonWhitespaceOffset(text: string, offset: number): number { + let index = Math.min(offset, text.length) - 1; + while (index >= 0 && /\s/.test(text[index])) index--; + return index; +} + +function nextNonWhitespaceOffset(text: string, offset: number): number { + let index = Math.max(0, offset); + while (index < text.length && /\s/.test(text[index])) index++; + return index; +} + +function validateYaml(text: string): NacosConfigDiagnostic | null { + const lines = text.split(/\r?\n/); + let offset = 0; + for (const line of lines) { + const indentation = /^[ \t]*/.exec(line)?.[0] ?? ""; + const tabIndex = indentation.indexOf("\t"); + if (tabIndex >= 0) { + return diagnosticAt(text, offset + tabIndex, "YAML indentation must use spaces, not tabs"); + } + offset += line.length + 1; + } + const document = parseDocument(text); + const error = document.errors[0]; + if (error) { + const offsetFromError = Array.isArray(error.pos) ? error.pos[0] : 0; + return diagnosticAt(text, offsetFromError, error.message); + } + return null; +} + +function validateXml(text: string): NacosConfigDiagnostic | null { + const result = XMLValidator.validate(text); + if (result === true) return null; + const { err } = result; + return diagnosticAt(text, offsetAtLineColumn(text, err.line, err.col), `Invalid XML syntax: ${err.msg}`); +} + +function validateProperties(text: string): NacosConfigDiagnostic | null { + for (let offset = 0; offset < text.length; offset += 1) { + if (text[offset] !== "\\") continue; + if (text[offset + 1] !== "u") { + offset += 1; + continue; + } + const unicode = text.slice(offset + 2, offset + 6); + if (!/^[0-9a-f]{4}$/i.test(unicode)) { + return diagnosticAt(text, offset, "Invalid Properties Unicode escape", Math.min(6, text.length - offset)); + } + offset += 5; + } + return null; +} + +function validateToml(text: string): NacosConfigDiagnostic | null { + try { + parseToml(text); + return null; + } catch (error) { + const message = error instanceof Error ? error.message.split("\n\n", 1)[0] : String(error); + const position = error instanceof TomlError ? offsetAtLineColumn(text, error.line, error.column) : 0; + return diagnosticAt(text, position, `Invalid TOML syntax: ${message}`); + } +} + +export function validateNacosConfigContent(text: string, format: string): NacosConfigDiagnostic[] { + const rawFormat = format.trim().toLowerCase(); + const normalized = (rawFormat === "yml" ? "yaml" : rawFormat === "props" ? "properties" : rawFormat) as NacosConfigValidationFormat; + let diagnostic: NacosConfigDiagnostic | null = null; + if (normalized === "json") return validateJson(text); + else if (normalized === "yaml") diagnostic = validateYaml(text); + else if (normalized === "xml") diagnostic = validateXml(text); + else if (normalized === "html") diagnostic = validateHtml(text); + else if (normalized === "properties") diagnostic = validateProperties(text); + else if (normalized === "toml") diagnostic = validateToml(text); + return diagnostic ? [diagnostic] : []; +} + +export function validateNacosConfig(text: string, format: string): NacosConfigDiagnostic | null { + return validateNacosConfigContent(text, format)[0] ?? null; +} diff --git a/flake.nix b/flake.nix index 7c4ac5c80f..8f6a1cfb0f 100644 --- a/flake.nix +++ b/flake.nix @@ -197,7 +197,7 @@ fetcherVersion = 4; # Update with the hash reported by a failed fixed-output build: # nix build .#dbx-pnpm-deps 2>&1 | grep 'got:' - hash = "sha256-VYG+rEeTTcf3B4avT6UIAdwIymrrioAUtUP2MBYi6s0="; + hash = "sha256-7p9ivX+tkfj46PDTADJjvXTqNXlm2srPp8ngfHi/kLU="; }; # ── Step 2: vendor Cargo dependencies ───────────────────────────── # diff --git a/package.json b/package.json index 3548c765df..66245a2e48 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "dom-to-image-more": "^3.7.2", "echarts": "^6.1.0", "elkjs": "^0.11.1", + "fast-xml-parser": "^5.11.1", "leaflet": "^1.9.4", "marked": "^18.0.4", "pako": "^3.0.1", @@ -102,6 +103,7 @@ "reka-ui": "^2.10.1", "shadcn-vue": "^2.7.4", "shiki": "^4.2.0", + "smol-toml": "^1.8.0", "splitpanes": "^4.1.2", "sql-formatter": "^15.8.0", "tailwind-merge": "^3.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb6886a178..c321c7cf04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: elkjs: specifier: ^0.11.1 version: 0.11.1 + fast-xml-parser: + specifier: ^5.11.1 + version: 5.11.1 leaflet: specifier: ^1.9.4 version: 1.9.4 @@ -179,6 +182,9 @@ importers: shiki: specifier: ^4.2.0 version: 4.2.0 + smol-toml: + specifier: ^1.8.0 + version: 1.8.0 splitpanes: specifier: ^4.1.2 version: 4.1.2(vue@3.5.35(typescript@6.0.3)) @@ -963,6 +969,9 @@ packages: resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==} engines: {node: '>= 20.19.0'} + '@nodable/entities@3.0.0': + resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1843,6 +1852,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2431,6 +2443,13 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-builder@1.3.1: + resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} + + fast-xml-parser@5.11.1: + resolution: {integrity: sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2711,6 +2730,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-unsafe@2.0.2: + resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -3262,6 +3284,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3569,6 +3595,10 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + smol-toml@1.8.0: + resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3650,6 +3680,9 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strnum@2.4.2: + resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} + style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -4025,6 +4058,10 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + xml-naming@0.3.0: + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} + engines: {node: '>=16.0.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4719,6 +4756,8 @@ snapshots: '@noble/ciphers@2.2.0': {} + '@nodable/entities@3.0.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5529,6 +5568,8 @@ snapshots: ansi-styles@6.2.3: {} + anynum@1.0.1: {} + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -6135,6 +6176,20 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-builder@1.3.1: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.3.0 + + fast-xml-parser@5.11.1: + dependencies: + '@nodable/entities': 3.0.0 + fast-xml-builder: 1.3.1 + is-unsafe: 2.0.2 + path-expression-matcher: 1.6.2 + strnum: 2.4.2 + xml-naming: 0.3.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -6393,6 +6448,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-unsafe@2.0.2: {} + is-what@5.5.0: {} is-wsl@2.2.0: @@ -6896,6 +6953,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.6.2: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -7279,6 +7338,8 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + smol-toml@1.8.0: {} + source-map-js@1.2.1: {} source-map-resolve@0.6.0: @@ -7352,6 +7413,10 @@ snapshots: strip-final-newline@2.0.0: {} + strnum@2.4.2: + dependencies: + anynum: 1.0.1 + style-mod@4.1.3: {} stylus@0.57.0: @@ -7721,6 +7786,8 @@ snapshots: dependencies: is-wsl: 3.1.1 + xml-naming@0.3.0: {} + yallist@3.1.1: {} yaml@2.8.4: {}