diff --git a/.changeset/green-scroll-editor.md b/.changeset/green-scroll-editor.md deleted file mode 100644 index f2ef13b..0000000 --- a/.changeset/green-scroll-editor.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@druid-ui/component": minor -"@druid-ui/config-editor": minor -"@druid-ui/host": minor -"@druid-ui/plattform": minor -"@druid-ui/vite": patch ---- - -Add a manifest-driven, lossless server configuration editor with raw and WASM parity, automatically discovered typed fields, Java properties, INI, Unreal tuple, key-value, JSON, and XML-property adapters, template/default fallbacks, secret-safe editing, conflict-aware saves, accessible host attributes, and reliable external-source hot reload. diff --git a/.changeset/retire-config-editor-v3.md b/.changeset/retire-config-editor-v3.md new file mode 100644 index 0000000..8eb71dd --- /dev/null +++ b/.changeset/retire-config-editor-v3.md @@ -0,0 +1,7 @@ +--- +"@druid-ui/plattform": major +--- + +Retire the standalone `@druid-ui/config-editor` package and remove the +`saveFileToDeploymentIfMatch` platform API. Move configuration-editor +integrations to the inline editor shipped with Druid Scrolls. diff --git a/README.md b/README.md index 6e18235..0c7ef11 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,9 @@ npx gen-types # generates types for your extension - [Simple Extended Example](./examples/simple-extended) - Component with custom extensions - [Simple No-Sandbox Example](./examples/simple-no-sandbox) - Development without WASM overhead - [Simple Extended No-Sandbox Example](./examples/simple-extended-no-sandbox) - Extended component in raw mode -- [Configuration Editor Platform Example](./examples/config-editor-plattform) - Manifest-driven server configuration UI in raw development and sandboxed WASM modes -## Server Configuration Editor +## Configuration Editor Migration -`@druid-ui/config-editor` turns a strict JSON manifest into an accessible admin UI for Java properties, INI/Unreal INI, key-value, JSON, and raw configuration files. It preserves comments, ordering, unknown keys, and line endings; masks sensitive values; validates typed fields; reports restart impact; and verifies conflict-safe saves through the Druid platform file extension. - -Use raw mode for the fast development loop, then build and test the same entry point as WASM before release. See `examples/config-editor-plattform` for the complete host and fixture setup. +`@druid-ui/config-editor` is retired in Druid UI v3, along with the +`saveFileToDeploymentIfMatch` platform API. Use the inline configuration editor +staged with Druid Scrolls for server configuration editing. diff --git a/examples/README.md b/examples/README.md index fb790dd..7caec15 100644 --- a/examples/README.md +++ b/examples/README.md @@ -53,15 +53,6 @@ This directory contains various examples demonstrating different features and us ### Platform Examples -#### [config-editor-plattform](./config-editor-plattform/) - -**Generic game-server configuration editor** - -- Uses one manifest-driven UI across server families and versions -- Demonstrates fast raw HMR and production WASM sandbox modes -- Preserves unknown options and masks sensitive values -- Best for: Scroll private admin UIs - #### [druid-plattform](./druid-plattform/) **Full platform integration** diff --git a/examples/config-editor-plattform/fixtures/config-editor.manifest.json b/examples/config-editor-plattform/fixtures/config-editor.manifest.json deleted file mode 100644 index c56207d..0000000 --- a/examples/config-editor-plattform/fixtures/config-editor.manifest.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "version": 1, - "server": { - "family": "minecraft", - "displayName": "Minecraft Server", - "appVersion": "1.21.7" - }, - "files": [ - { - "path": "server.properties", - "format": "java-properties", - "label": "server.properties", - "description": "Minecraft dedicated-server settings.", - "sections": [ - { - "id": "server", - "label": "Server identity", - "fields": [ - { - "key": "motd", - "label": "Message of the day", - "description": "Text shown in the multiplayer server list.", - "documentation": "https://minecraft.wiki/w/Server.properties", - "type": "string" - }, - { - "key": "max-players", - "label": "Max players", - "description": "Maximum number of simultaneous players.", - "documentation": "https://minecraft.wiki/w/Server.properties", - "type": "integer", - "min": 1, - "max": 1000, - "restartRequired": true - }, - { - "key": "online-mode", - "label": "Online mode", - "description": "Verify connecting players with Minecraft account services.", - "documentation": "https://minecraft.wiki/w/Server.properties", - "type": "boolean", - "restartRequired": true - }, - { - "key": "difficulty", - "label": "Difficulty", - "description": "Default world difficulty.", - "documentation": "https://minecraft.wiki/w/Server.properties", - "type": "enum", - "values": ["peaceful", "easy", "normal", "hard"], - "restartRequired": true - } - ] - }, - { - "id": "remote", - "label": "Remote administration", - "fields": [ - { - "key": "rcon.password", - "label": "RCON password", - "description": "Password used by remote administration clients.", - "documentation": "https://minecraft.wiki/w/Server.properties", - "type": "secret", - "sensitive": true, - "restartRequired": true - } - ] - } - ] - } - ] -} diff --git a/examples/config-editor-plattform/fixtures/server.properties b/examples/config-editor-plattform/fixtures/server.properties deleted file mode 100644 index 3008e5c..0000000 --- a/examples/config-editor-plattform/fixtures/server.properties +++ /dev/null @@ -1,7 +0,0 @@ -#Minecraft server properties -motd=Druid development server -max-players=20 -online-mode=true -difficulty=normal -rcon.password=fixture-secret -future-option=preserved diff --git a/examples/config-editor-plattform/index.html b/examples/config-editor-plattform/index.html deleted file mode 100644 index 628e0b2..0000000 --- a/examples/config-editor-plattform/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Druid configuration editor - - - -
- - - diff --git a/examples/config-editor-plattform/package.json b/examples/config-editor-plattform/package.json deleted file mode 100644 index fbdb5d0..0000000 --- a/examples/config-editor-plattform/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "config-editor-plattform", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "npm run build:raw:public && npm run build:wasm:public && vite build", - "build:raw": "druid-ui-build --wit=wit/druid-plattform.wit --world-name=druid-plattform src/app.tsx --raw dist", - "build:wasm": "druid-ui-build --wit=wit/druid-plattform.wit --world-name=druid-plattform src/app.tsx dist", - "build:raw:public": "druid-ui-build --wit=wit/druid-plattform.wit --world-name=druid-plattform src/app.tsx --raw public", - "build:wasm:public": "druid-ui-build --wit=wit/druid-plattform.wit --world-name=druid-plattform src/app.tsx public" - }, - "dependencies": { - "@druid-ui/component": "*", - "@druid-ui/config-editor": "*", - "@druid-ui/host": "*", - "@druid-ui/plattform": "*", - "@druid-ui/vite": "*" - }, - "devDependencies": { - "@druid-ui/build": "*", - "typescript": "~5.8.3", - "vite": "^7.1.7", - "vite-plugin-wasm": "^3.5.0" - } -} diff --git a/examples/config-editor-plattform/public/app.bundled-raw.js b/examples/config-editor-plattform/public/app.bundled-raw.js deleted file mode 100644 index 4916297..0000000 --- a/examples/config-editor-plattform/public/app.bundled-raw.js +++ /dev/null @@ -1,2505 +0,0 @@ -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); - -// druid-ext:druid:ui/ui -var require_ui = __commonJS({ - "druid-ext:druid:ui/ui"(exports, module) { - module.exports = window["druid-extension"]["druid:ui/ui"]; - } -}); - -// druid-ext:druid:ui/utils -var require_utils = __commonJS({ - "druid-ext:druid:ui/utils"(exports, module) { - module.exports = window["druid-extension"]["druid:ui/utils"]; - } -}); - -// druid-ext:druid:ui/plattform -var require_plattform = __commonJS({ - "druid-ext:druid:ui/plattform"(exports, module) { - module.exports = window["druid-extension"]["druid:ui/plattform"]; - } -}); - -// ../../packages/config-editor/src/manifest.ts -var CONFIG_FORMATS = /* @__PURE__ */ new Set([ - "java-properties", - "ini", - "unreal-ini", - "key-value", - "json", - "xml-properties", - "raw" -]); -var FIELD_TYPES = /* @__PURE__ */ new Set([ - "string", - "integer", - "number", - "boolean", - "enum", - "secret" -]); -var DOTTED_VERSION = /^\d+(?:\.\d+)*$/; -var URL_SCHEME = /^[a-z][a-z\d+.-]*:/i; -var WINDOWS_DRIVE = /^[a-z]:\//i; -var fail = (message) => { - throw new TypeError(`Invalid config editor manifest: ${message}`); -}; -var objectValue = (value, name) => { - if (typeof value !== "object" || value === null || Array.isArray(value)) { - return fail(`${name} must be an object.`); - } - return value; -}; -var stringValue = (value, name) => { - if (typeof value !== "string" || value.trim().length === 0) { - return fail(`${name} must be a non-empty string.`); - } - return value; -}; -var optionalString = (value, name) => { - if (value === void 0) return void 0; - return stringValue(value, name); -}; -var optionalBoolean = (value, name) => { - if (value === void 0) return void 0; - if (typeof value !== "boolean") return fail(`${name} must be a boolean.`); - return value; -}; -var optionalNumber = (value, name) => { - if (value === void 0) return void 0; - if (typeof value !== "number" || !Number.isFinite(value)) { - return fail(`${name} must be a finite number.`); - } - return value; -}; -var arrayValue = (value, name) => { - if (!Array.isArray(value)) return fail(`${name} must be an array.`); - return value; -}; -var assertVersion = (value, name) => { - if (!DOTTED_VERSION.test(value)) { - fail(`${name} must be a dotted numeric version.`); - } -}; -var assertSafePath = (path) => { - const segments = path.split("/"); - const unsafe = path.startsWith("/") || path.includes("\\") || path.includes("\0") || URL_SCHEME.test(path) || WINDOWS_DRIVE.test(path) || segments.some((segment) => segment === "" || segment === "." || segment === ".."); - if (unsafe) fail(`file path "${path}" must be a safe relative path.`); -}; -var validateServer = (value) => { - const server = objectValue(value, "server"); - return { - family: stringValue(server["family"], "server.family"), - displayName: stringValue(server["displayName"], "server.displayName"), - ...server["appVersion"] === void 0 ? {} : { appVersion: stringValue(server["appVersion"], "server.appVersion") } - }; -}; -var validateField = (value, location) => { - const field = objectValue(value, location); - const type = stringValue(field["type"], `${location}.type`); - if (!FIELD_TYPES.has(type)) { - fail(`${location}.type "${type}" is unsupported.`); - } - const min = optionalNumber(field["min"], `${location}.min`); - const max = optionalNumber(field["max"], `${location}.max`); - if (min !== void 0 && max !== void 0 && min > max) { - fail(`${location} minimum cannot exceed its maximum.`); - } - if ((min !== void 0 || max !== void 0) && type !== "integer" && type !== "number") { - fail(`${location} numeric ranges require an integer or number field.`); - } - let values; - if (field["values"] !== void 0) { - values = arrayValue(field["values"], `${location}.values`).map( - (entry, index) => stringValue(entry, `${location}.values[${index}]`) - ); - if (new Set(values).size !== values.length) { - fail(`${location}.values contains duplicate enum values.`); - } - } - if (type === "enum" && (!values || values.length === 0)) { - fail(`${location} enum values must contain at least one value.`); - } - if (type !== "enum" && values !== void 0) { - fail(`${location}.values is only valid for enum fields.`); - } - const pattern = optionalString(field["pattern"], `${location}.pattern`); - if (pattern !== void 0) { - try { - new RegExp(pattern); - } catch { - fail(`${location}.pattern must be a valid regular expression.`); - } - } - const since = optionalString(field["since"], `${location}.since`); - const until = optionalString(field["until"], `${location}.until`); - if (since !== void 0) assertVersion(since, `${location}.since`); - if (until !== void 0) assertVersion(until, `${location}.until`); - if (since !== void 0 && until !== void 0 && compareVersions(since, until) > 0) { - fail(`${location}.since cannot be later than .until.`); - } - const rawDefaultValue = field["defaultValue"]; - if (rawDefaultValue !== void 0 && rawDefaultValue !== null && typeof rawDefaultValue !== "string" && typeof rawDefaultValue !== "number" && typeof rawDefaultValue !== "boolean") { - fail(`${location}.defaultValue must be a scalar value.`); - } - const result = { - key: stringValue(field["key"], `${location}.key`), - label: stringValue(field["label"], `${location}.label`), - description: stringValue(field["description"], `${location}.description`), - documentation: stringValue( - field["documentation"], - `${location}.documentation` - ), - type - }; - if (values !== void 0) result.values = values; - if (min !== void 0) result.min = min; - if (max !== void 0) result.max = max; - if (pattern !== void 0) result.pattern = pattern; - if (rawDefaultValue !== void 0) { - result.defaultValue = rawDefaultValue; - } - if (field["sensitive"] !== void 0) { - result.sensitive = optionalBoolean(field["sensitive"], `${location}.sensitive`); - } - if (field["restartRequired"] !== void 0) { - result.restartRequired = optionalBoolean( - field["restartRequired"], - `${location}.restartRequired` - ); - } - if (since !== void 0) result.since = since; - if (until !== void 0) result.until = until; - return result; -}; -var validateSection = (value, location) => { - const section = objectValue(value, location); - const fields = arrayValue(section["fields"], `${location}.fields`).map( - (field, index) => validateField(field, `${location}.fields[${index}]`) - ); - if (fields.length === 0) fail(`${location}.fields must not be empty.`); - const result = { - id: stringValue(section["id"], `${location}.id`), - label: stringValue(section["label"], `${location}.label`), - fields - }; - if (section["description"] !== void 0) { - result.description = stringValue( - section["description"], - `${location}.description` - ); - } - return result; -}; -var validateFile = (value, location) => { - const file = objectValue(value, location); - const path = stringValue(file["path"], `${location}.path`); - assertSafePath(path); - const format = stringValue(file["format"], `${location}.format`); - if (!CONFIG_FORMATS.has(format)) { - fail(`${location}.format "${format}" is unsupported.`); - } - const sections = arrayValue(file["sections"], `${location}.sections`).map( - (section, index) => validateSection(section, `${location}.sections[${index}]`) - ); - const sectionIds = /* @__PURE__ */ new Set(); - const fieldKeys = /* @__PURE__ */ new Set(); - for (const section of sections) { - if (sectionIds.has(section.id)) { - fail(`${location} contains duplicate section id "${section.id}".`); - } - sectionIds.add(section.id); - for (const field of section.fields) { - if (fieldKeys.has(field.key)) { - fail(`${location} contains duplicate field key "${field.key}".`); - } - fieldKeys.add(field.key); - } - } - const result = { - path, - format, - label: stringValue(file["label"], `${location}.label`), - sections - }; - if (file["description"] !== void 0) { - result.description = stringValue( - file["description"], - `${location}.description` - ); - } - if (file["documentation"] !== void 0) { - result.documentation = stringValue( - file["documentation"], - `${location}.documentation` - ); - } - return result; -}; -var validateManifest = (value) => { - const manifest = objectValue(value, "manifest"); - if (manifest["version"] !== 1) fail("version must be 1."); - const files = arrayValue(manifest["files"], "files").map( - (file, index) => validateFile(file, `files[${index}]`) - ); - if (files.length === 0) fail("files must not be empty."); - const paths = /* @__PURE__ */ new Set(); - for (const file of files) { - if (paths.has(file.path)) fail(`files contains duplicate path "${file.path}".`); - paths.add(file.path); - } - return { version: 1, server: validateServer(manifest["server"]), files }; -}; -var compareVersions = (left, right) => { - const leftParts = left.split(".").map(Number); - const rightParts = right.split(".").map(Number); - const length = Math.max(leftParts.length, rightParts.length); - for (let index = 0; index < length; index += 1) { - const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); - if (difference !== 0) return difference < 0 ? -1 : 1; - } - return 0; -}; - -// ../../packages/config-editor/src/adapters/line-document.ts -var splitSourceLines = (source) => { - const lines = []; - let start = 0; - while (start < source.length) { - const newline = source.indexOf("\n", start); - if (newline === -1) { - lines.push({ raw: source.slice(start), ending: "", start, end: source.length }); - break; - } - const hasCarriageReturn = newline > start && source[newline - 1] === "\r"; - const rawEnd = hasCarriageReturn ? newline - 1 : newline; - lines.push({ - raw: source.slice(start, rawEnd), - ending: hasCarriageReturn ? "\r\n" : "\n", - start, - end: rawEnd - }); - start = newline + 1; - } - return lines; -}; -var preferredLineEnding = (lines) => lines.find((line) => line.ending !== "")?.ending || "\n"; -var hasFinalNewline = (source) => source.endsWith("\n"); - -// ../../packages/config-editor/src/adapters/java-properties.ts -var trailingBackslashes = (value) => { - let count = 0; - for (let index = value.length - 1; index >= 0 && value[index] === "\\"; index -= 1) { - count += 1; - } - return count; -}; -var logicalLineAt = (lines, firstLine) => { - let text = ""; - const offsets = []; - let lineIndex = firstLine; - let continued = false; - for (; lineIndex < lines.length; lineIndex += 1) { - const line = lines[lineIndex]; - const trim = continued ? line.raw.match(/^[ \t\f]*/)?.[0].length ?? 0 : 0; - const content = line.raw.slice(trim); - const continues = trailingBackslashes(content) % 2 === 1; - const logicalContent = continues ? content.slice(0, -1) : content; - for (let index = 0; index < logicalContent.length; index += 1) { - text += logicalContent[index]; - offsets.push(line.start + trim + index); - } - continued = continues; - if (!continues) break; - } - const lastLine = Math.min(lineIndex, lines.length - 1); - return { - text, - offsets, - end: lines[lastLine]?.end ?? 0, - firstLine, - lastLine - }; -}; -var isEscaped = (value, index) => { - let slashes = 0; - for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) { - slashes += 1; - } - return slashes % 2 === 1; -}; -var decodeEscapes = (value) => { - let decoded = ""; - for (let index = 0; index < value.length; index += 1) { - const character = value[index]; - if (character !== "\\" || index === value.length - 1) { - decoded += character; - continue; - } - const escaped = value[index += 1]; - if (escaped === "t") decoded += " "; - else if (escaped === "n") decoded += "\n"; - else if (escaped === "r") decoded += "\r"; - else if (escaped === "f") decoded += "\f"; - else if (escaped === "u") { - while (value[index + 1] === "u") index += 1; - const hexadecimal = value.slice(index + 1, index + 5); - if (/^[\da-f]{4}$/i.test(hexadecimal)) { - decoded += String.fromCharCode(Number.parseInt(hexadecimal, 16)); - index += 4; - } else { - decoded += "u"; - } - } else decoded += escaped; - } - return decoded; -}; -var parseEntry = (logical) => { - const { text } = logical; - let start = 0; - while (start < text.length && /[ \t\f]/.test(text[start])) start += 1; - if (start === text.length || text[start] === "#" || text[start] === "!") { - return void 0; - } - let keyEnd = start; - while (keyEnd < text.length) { - const character = text[keyEnd]; - if (!isEscaped(text, keyEnd) && (character === "=" || character === ":" || /[ \t\f]/.test(character))) { - break; - } - keyEnd += 1; - } - let valueStart = keyEnd; - while (valueStart < text.length && /[ \t\f]/.test(text[valueStart])) { - valueStart += 1; - } - if (valueStart < text.length && (text[valueStart] === "=" || text[valueStart] === ":")) { - valueStart += 1; - } - while (valueStart < text.length && /[ \t\f]/.test(text[valueStart])) { - valueStart += 1; - } - return { - key: decodeEscapes(text.slice(start, keyEnd)), - value: decodeEscapes(text.slice(valueStart)), - valueSpan: { - start: logical.offsets[valueStart] ?? logical.end, - end: logical.end - }, - firstLine: logical.firstLine, - lastLine: logical.lastLine - }; -}; -var classifyNodes = (lines, entries) => { - const firstLines = new Map(entries.map((entry) => [entry.firstLine, entry])); - const continuationLines = /* @__PURE__ */ new Set(); - for (const entry of entries) { - for (let index = entry.firstLine + 1; index <= entry.lastLine; index += 1) { - continuationLines.add(index); - } - } - return lines.map((line, index) => { - const entry = firstLines.get(index); - if (entry) { - return { - ...line, - kind: "entry", - key: entry.key, - value: entry.value, - valueSpan: entry.valueSpan - }; - } - if (continuationLines.has(index)) return { ...line, kind: "continuation" }; - const trimmed = line.raw.trimStart(); - if (trimmed === "") return { ...line, kind: "blank" }; - if (trimmed.startsWith("#") || trimmed.startsWith("!")) { - return { ...line, kind: "comment" }; - } - return { ...line, kind: "invalid" }; - }); -}; -var parseJavaProperties = (source) => { - const lines = splitSourceLines(source); - const entries = []; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { - const logical = logicalLineAt(lines, lineIndex); - const entry = parseEntry(logical); - if (entry) entries.push(entry); - lineIndex = logical.lastLine; - } - return { - format: "java-properties", - source, - nodes: classifyNodes(lines, entries), - entries, - issues: [], - lineEnding: preferredLineEnding(lines), - finalNewline: hasFinalNewline(source) - }; -}; -var escapeKey = (value) => value.replace(/[\\ \t\f=:#!]/g, (character) => { - if (character === " ") return "\\t"; - if (character === "\f") return "\\f"; - return `\\${character}`; -}); -var escapeValue = (value) => { - const text = value === null ? "" : String(value); - return text.replace(/[\\\t\n\r\f]/g, (character) => { - if (character === "\\") return "\\\\"; - if (character === " ") return "\\t"; - if (character === "\n") return "\\n"; - if (character === "\r") return "\\r"; - return "\\f"; - }).replace(/^ +/, (spaces) => "\\ ".repeat(spaces.length)); -}; -var effectiveEntry = (document, key) => { - for (let index = document.entries.length - 1; index >= 0; index -= 1) { - const entry = document.entries[index]; - if (entry.key === key) return entry; - } - return void 0; -}; -var appendEntry = (document, key, value) => { - const entry = `${escapeKey(key)}=${escapeValue(value)}`; - if (document.source === "") return entry; - if (document.finalNewline) return `${document.source}${entry}${document.lineEnding}`; - return `${document.source}${document.lineEnding}${entry}`; -}; -var javaPropertiesAdapter = { - parse: parseJavaProperties, - entries(document) { - const entries = /* @__PURE__ */ new Map(); - for (const entry of document.entries) entries.set(entry.key, entry.value); - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveEntry(document, key)?.value; - }, - getAll(document, key) { - return document.entries.filter((entry) => entry.key === key).map((entry) => entry.value); - }, - getAllRaw(document, key) { - return document.entries.filter((entry) => entry.key === key).map((entry) => document.source.slice(entry.valueSpan.start, entry.valueSpan.end)); - }, - set(document, key, value) { - const entry = effectiveEntry(document, key); - if (!entry) return parseJavaProperties(appendEntry(document, key, value)); - const source = document.source.slice(0, entry.valueSpan.start) + escapeValue(value) + document.source.slice(entry.valueSpan.end); - return parseJavaProperties(source); - }, - setAll(document, key, values) { - const entries = document.entries.filter((entry) => entry.key === key); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} values for Java property "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + escapeValue(values[index]) + source.slice(entry.valueSpan.end); - } - return parseJavaProperties(source); - }, - setAllRaw(document, key, values) { - const entries = document.entries.filter((entry) => entry.key === key); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} raw values for Java property "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + values[index] + source.slice(entry.valueSpan.end); - } - return parseJavaProperties(source); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/ini.ts -var inlineCommentAt = (value) => { - let quote; - for (let index = 0; index < value.length; index += 1) { - const character = value[index]; - if (quote) { - if (quote === "'" && value.startsWith("'\\''", index)) { - index += 3; - continue; - } - if (character === quote && value[index - 1] !== "\\") quote = void 0; - continue; - } - if (character === '"' || character === "'") { - quote = character; - continue; - } - const precededBySpace = index > 0 && /\s/.test(value[index - 1]); - if (!precededBySpace) continue; - if (character === ";" || character === "#") return index; - if (character === "/" && value[index + 1] === "/") return index; - } - return value.length; -}; -var decodeQuotedValue = (value, quote) => { - if (quote === '"') return value.replace(/\\(["\\])/g, "$1"); - if (quote === "'") return value.replace(/'\\''/g, "'"); - return value; -}; -var parseEntryLine = (raw, start, section, line, format) => { - let keyStart = 0; - while (keyStart < raw.length && /[ \t]/.test(raw[keyStart])) keyStart += 1; - if (keyStart === raw.length) return void 0; - let keyEnd = keyStart; - while (keyEnd < raw.length && raw[keyEnd] !== "=" && raw[keyEnd] !== ":" && !/[ \t]/.test(raw[keyEnd])) { - keyEnd += 1; - } - if (keyEnd === keyStart) return void 0; - let valueStart = keyEnd; - while (valueStart < raw.length && /[ \t]/.test(raw[valueStart])) valueStart += 1; - if (raw[valueStart] === "=" || raw[valueStart] === ":") valueStart += 1; - while (valueStart < raw.length && /[ \t]/.test(raw[valueStart])) valueStart += 1; - const comment = inlineCommentAt(raw.slice(valueStart)); - let valueEnd = valueStart + comment; - while (valueEnd > valueStart && /[ \t]/.test(raw[valueEnd - 1])) valueEnd -= 1; - if (format === "key-value" && raw[valueEnd - 1] === ";") { - valueEnd -= 1; - while (valueEnd > valueStart && /[ \t]/.test(raw[valueEnd - 1])) valueEnd -= 1; - } - const quote = raw[valueStart]; - let valueQuote; - if ((quote === '"' || quote === "'") && valueEnd > valueStart && raw[valueEnd - 1] === quote) { - valueQuote = quote; - valueStart += 1; - valueEnd -= 1; - } - const localKey = raw.slice(keyStart, keyEnd).trim(); - return { - key: section ? `${section}.${localKey}` : localKey, - value: decodeQuotedValue(raw.slice(valueStart, valueEnd), valueQuote), - valueSpan: { start: start + valueStart, end: start + valueEnd }, - line, - ...valueQuote ? { quote: valueQuote } : {} - }; -}; -var tupleEntries = (entry) => { - if (!entry.value.startsWith("(") || !entry.value.endsWith(")")) return []; - const inner = entry.value.slice(1, -1); - const segments = []; - let segmentStart = 0; - let depth = 0; - let quote; - for (let index = 0; index <= inner.length; index += 1) { - const character = inner[index]; - if (quote) { - if (character === quote && inner[index - 1] !== "\\") quote = void 0; - continue; - } - if (character === '"' || character === "'") { - quote = character; - continue; - } - if (character === "(" || character === "[" || character === "{") depth += 1; - else if (character === ")" || character === "]" || character === "}") depth -= 1; - if (character === "," && depth === 0 || index === inner.length) { - segments.push({ start: segmentStart, end: index }); - segmentStart = index + 1; - } - } - const result = []; - for (const segment of segments) { - const raw = inner.slice(segment.start, segment.end); - let equals = -1; - depth = 0; - quote = void 0; - for (let index = 0; index < raw.length; index += 1) { - const character = raw[index]; - if (quote) { - if (character === quote && raw[index - 1] !== "\\") quote = void 0; - continue; - } - if (character === '"' || character === "'") quote = character; - else if (character === "(" || character === "[" || character === "{") depth += 1; - else if (character === ")" || character === "]" || character === "}") depth -= 1; - else if (character === "=" && depth === 0) { - equals = index; - break; - } - } - if (equals < 1) continue; - const key = raw.slice(0, equals).trim(); - let localStart = equals + 1; - while (localStart < raw.length && /\s/.test(raw[localStart])) localStart += 1; - let localEnd = raw.length; - while (localEnd > localStart && /\s/.test(raw[localEnd - 1])) localEnd -= 1; - const valueQuote = raw[localStart]; - let preservedQuote; - if ((valueQuote === '"' || valueQuote === "'") && localEnd > localStart && raw[localEnd - 1] === valueQuote) { - preservedQuote = valueQuote; - localStart += 1; - localEnd -= 1; - } - const absoluteStart = entry.valueSpan.start + 1 + segment.start + localStart; - result.push({ - key: `${entry.key}.${key}`, - value: decodeQuotedValue(raw.slice(localStart, localEnd), preservedQuote), - valueSpan: { - start: absoluteStart, - end: absoluteStart + localEnd - localStart - }, - line: entry.line, - ...preservedQuote ? { quote: preservedQuote } : {} - }); - } - return result; -}; -var parseIni = (source, format) => { - const lines = splitSourceLines(source); - const entries = []; - const sections = []; - const nodes = []; - let section; - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - const trimmed = line.raw.trimStart(); - if (trimmed === "") { - nodes.push({ ...line, kind: "blank" }); - continue; - } - if (trimmed.startsWith(";") || trimmed.startsWith("#") || trimmed.startsWith("//")) { - nodes.push({ ...line, kind: "comment" }); - continue; - } - const sectionMatch = format === "key-value" ? null : trimmed.match(/^\[([^\]]+)](?:\s*(?:[;#].*)?)$/); - if (sectionMatch) { - section = sectionMatch[1].trim(); - sections.push({ name: section, line: index }); - nodes.push({ ...line, kind: "section" }); - continue; - } - const entry = parseEntryLine(line.raw, line.start, section, index, format); - if (entry) { - entries.push(entry); - if (format === "unreal-ini") { - const children = tupleEntries(entry); - if (children.length > 0) { - entry.container = true; - entries.push(...children); - } - } - nodes.push({ - ...line, - kind: "entry", - key: entry.key, - value: entry.value, - valueSpan: entry.valueSpan - }); - } else nodes.push({ ...line, kind: "invalid" }); - } - return { - format, - source, - nodes, - entries, - sections, - issues: [], - lineEnding: preferredLineEnding(lines), - finalNewline: hasFinalNewline(source) - }; -}; -var effectiveEntry2 = (document, key) => { - for (let index = document.entries.length - 1; index >= 0; index -= 1) { - const entry = document.entries[index]; - if (entry.key === key) return entry; - } - return void 0; -}; -var formatValue = (value, quote) => { - let formatted = (value === null ? "" : String(value)).replace( - /[\r\n]/g, - (character) => character === "\r" ? "\\r" : "\\n" - ); - if (quote === '"') formatted = formatted.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); - else if (quote === "'") formatted = formatted.replace(/'/g, "'\\''"); - return formatted; -}; -var appendIniEntry = (document, key, value) => { - const split = document.format === "key-value" ? -1 : key.lastIndexOf("."); - const section = split < 0 ? void 0 : key.slice(0, split); - const localKey = split < 0 ? key : key.slice(split + 1); - const assignment = `${localKey}=${formatValue(value)}`; - const prefix = document.source === "" || document.finalNewline ? "" : document.lineEnding; - const suffix = document.finalNewline ? document.lineEnding : ""; - if (!section || document.sections.some((candidate) => candidate.name === section)) { - return `${document.source}${prefix}${assignment}${suffix}`; - } - const sectionHeader = `[${section}]${document.lineEnding}`; - return `${document.source}${prefix}${sectionHeader}${assignment}${suffix}`; -}; -var createIniAdapter = (format) => ({ - parse(source) { - return parseIni(source, format); - }, - entries(document) { - const entries = /* @__PURE__ */ new Map(); - for (const entry of document.entries) { - if (!entry.container) entries.set(entry.key, entry.value); - } - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveEntry2(document, key)?.value; - }, - getAll(document, key) { - return document.entries.filter((entry) => !entry.container && entry.key === key).map((entry) => entry.value); - }, - getAllRaw(document, key) { - return document.entries.filter((entry) => !entry.container && entry.key === key).map((entry) => document.source.slice(entry.valueSpan.start, entry.valueSpan.end)); - }, - set(document, key, value) { - const entry = effectiveEntry2(document, key); - if (!entry) return parseIni(appendIniEntry(document, key, value), format); - return parseIni( - document.source.slice(0, entry.valueSpan.start) + formatValue(value, entry.quote) + document.source.slice(entry.valueSpan.end), - format - ); - }, - setAll(document, key, values) { - const entries = document.entries.filter( - (entry) => !entry.container && entry.key === key - ); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} values for setting "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + formatValue(values[index], entry.quote) + source.slice(entry.valueSpan.end); - } - return parseIni(source, format); - }, - setAllRaw(document, key, values) { - const entries = document.entries.filter( - (entry) => !entry.container && entry.key === key - ); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} raw values for setting "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + values[index] + source.slice(entry.valueSpan.end); - } - return parseIni(source, format); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}); -var iniAdapter = createIniAdapter("ini"); -var unrealIniAdapter = createIniAdapter("unreal-ini"); -var keyValueAdapter = createIniAdapter("key-value"); - -// ../../packages/config-editor/src/adapters/json.ts -var JsonParseError = class extends Error { - position; - constructor(position, message) { - super(message); - this.position = position; - } -}; -var pointerSegment = (value) => value.replace(/~/g, "~0").replace(/\//g, "~1"); -var locationAt = (source, position) => { - const before = source.slice(0, position); - const lines = before.split("\n"); - return { line: lines.length, column: lines.at(-1).length + 1 }; -}; -var jsonNodes = (source) => splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid" -})); -var parseJson = (source) => { - const scalars = /* @__PURE__ */ new Map(); - const scalarOccurrences = /* @__PURE__ */ new Map(); - const containers = /* @__PURE__ */ new Map(); - let cursor = 0; - const addScalar = (pointer, scalar) => { - scalars.set(pointer, scalar); - const occurrences = scalarOccurrences.get(pointer) ?? []; - occurrences.push(scalar); - scalarOccurrences.set(pointer, occurrences); - }; - const fail2 = (message) => { - throw new JsonParseError(cursor, message); - }; - const skipWhitespace = () => { - while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; - }; - const consume = (character) => { - if (source[cursor] !== character) fail2(`Expected "${character}".`); - cursor += 1; - }; - const parseString = () => { - const start = cursor; - consume('"'); - while (cursor < source.length) { - const character = source[cursor]; - if (character === '"') { - cursor += 1; - const token = source.slice(start, cursor); - try { - return { value: JSON.parse(token), span: { start, end: cursor } }; - } catch { - fail2("Invalid JSON string."); - } - } - if (character === "\\") { - cursor += 2; - continue; - } - if (character.charCodeAt(0) < 32) fail2("Unescaped control character."); - cursor += 1; - } - return fail2("Unterminated JSON string."); - }; - const parseValue = (pointer) => { - skipWhitespace(); - const start = cursor; - const character = source[cursor]; - if (character === '"') { - const string = parseString(); - addScalar(pointer, { value: string.value, span: string.span }); - return; - } - if (character === "{") { - const open = cursor; - cursor += 1; - skipWhitespace(); - if (source[cursor] === "}") { - containers.set(pointer, { kind: "object", open, close: cursor }); - cursor += 1; - return; - } - while (cursor < source.length) { - skipWhitespace(); - if (source[cursor] !== '"') fail2("Expected an object key."); - const key = parseString().value; - skipWhitespace(); - consume(":"); - parseValue(`${pointer}/${pointerSegment(key)}`); - skipWhitespace(); - if (source[cursor] === "}") { - containers.set(pointer, { kind: "object", open, close: cursor }); - cursor += 1; - return; - } - consume(","); - } - fail2("Unterminated JSON object."); - } - if (character === "[") { - const open = cursor; - cursor += 1; - skipWhitespace(); - if (source[cursor] === "]") { - containers.set(pointer, { kind: "array", open, close: cursor }); - cursor += 1; - return; - } - let index = 0; - while (cursor < source.length) { - parseValue(`${pointer}/${index}`); - index += 1; - skipWhitespace(); - if (source[cursor] === "]") { - containers.set(pointer, { kind: "array", open, close: cursor }); - cursor += 1; - return; - } - consume(","); - } - fail2("Unterminated JSON array."); - } - const remainder = source.slice(cursor); - const literal = remainder.match(/^(?:true|false|null)(?![\w])/); - if (literal) { - cursor += literal[0].length; - addScalar(pointer, { - value: JSON.parse(literal[0]), - span: { start, end: cursor } - }); - return; - } - const number = remainder.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/); - if (number) { - cursor += number[0].length; - const unsafeInteger = /^-?\d+$/.test(number[0]) && !Number.isSafeInteger(Number(number[0])); - addScalar(pointer, { - value: unsafeInteger ? number[0] : Number(number[0]), - span: { start, end: cursor }, - ...unsafeInteger ? { unsafeInteger: true } : {} - }); - return; - } - fail2("Expected a JSON scalar, object, or array."); - }; - try { - parseValue(""); - skipWhitespace(); - if (cursor !== source.length) fail2("Unexpected content after the JSON value."); - return { - format: "json", - source, - nodes: jsonNodes(source), - issues: [], - scalars, - scalarOccurrences, - containers - }; - } catch (error) { - if (!(error instanceof JsonParseError)) throw error; - const location = locationAt(source, error.position); - return { - format: "json", - source, - nodes: jsonNodes(source), - issues: [ - { - code: "invalid-json", - message: error.message, - severity: "error", - ...location - } - ], - scalars: /* @__PURE__ */ new Map(), - scalarOccurrences: /* @__PURE__ */ new Map(), - containers: /* @__PURE__ */ new Map() - }; - } -}; -var decodePointerSegment = (value) => value.replace(/~1/g, "/").replace(/~0/g, "~"); -var insertJsonProperty = (document, pointer, value) => { - const separator = pointer.lastIndexOf("/"); - if (separator < 0) throw new Error(`JSON pointer "${pointer}" does not exist.`); - const parentPointer = pointer.slice(0, separator); - const property = decodePointerSegment(pointer.slice(separator + 1)); - const parent = document.containers.get(parentPointer); - if (!parent || parent.kind !== "object") { - throw new Error(`JSON pointer "${pointer}" does not exist.`); - } - const content = document.source.slice(parent.open + 1, parent.close); - const contentEnd = parent.close - (content.match(/\s*$/)?.[0].length ?? 0); - const closingLine = document.source.slice(0, parent.close).split("\n").at(-1) ?? ""; - const closingIndent = closingLine.match(/^\s*/)?.[0] ?? ""; - const serialized = `${JSON.stringify(property)}: ${JSON.stringify(value)}`; - const newline = document.source.includes("\r\n") ? "\r\n" : "\n"; - let insertion; - if (content.trim() === "") { - insertion = `${newline}${closingIndent} ${serialized}${newline}${closingIndent}`; - return parseJson( - document.source.slice(0, parent.open + 1) + insertion + document.source.slice(parent.close) - ); - } - const multiline = content.includes("\n"); - if (!multiline) insertion = `, ${serialized}`; - else { - const existingLine = document.source.slice(0, contentEnd).split("\n").at(-1) ?? ""; - const childIndent = existingLine.match(/^\s*/)?.[0] ?? `${closingIndent} `; - insertion = `,${newline}${childIndent}${serialized}`; - } - return parseJson( - document.source.slice(0, contentEnd) + insertion + document.source.slice(contentEnd) - ); -}; -var jsonAdapter = { - parse: parseJson, - entries(document) { - return [...document.scalars].map(([key, scalar]) => ({ - key, - value: scalar.value - })); - }, - get(document, key) { - return document.scalars.get(key)?.value; - }, - getAll(document, key) { - return (document.scalarOccurrences.get(key) ?? []).map((scalar) => scalar.value); - }, - getAllRaw(document, key) { - return (document.scalarOccurrences.get(key) ?? []).map( - (scalar) => document.source.slice(scalar.span.start, scalar.span.end) - ); - }, - set(document, key, value) { - if (document.issues.length > 0) throw new Error("Invalid JSON cannot be edited in form mode."); - const scalar = document.scalars.get(key); - if (!scalar) { - if (document.containers.has(key)) throw new Error(`JSON pointer "${key}" is not a scalar.`); - return insertJsonProperty(document, key, value); - } - const serialized = scalar.unsafeInteger && typeof value === "string" && /^-?\d+$/.test(value) ? value : JSON.stringify(value); - return parseJson( - document.source.slice(0, scalar.span.start) + serialized + document.source.slice(scalar.span.end) - ); - }, - setAll(document, key, values) { - const occurrences = document.scalarOccurrences.get(key) ?? []; - const expected = occurrences.length; - if (values.length !== expected) { - throw new Error(`Expected ${expected} values for JSON pointer "${key}".`); - } - let source = document.source; - for (let index = occurrences.length - 1; index >= 0; index -= 1) { - const scalar = occurrences[index]; - const value = values[index]; - const serialized = scalar.unsafeInteger && typeof value === "string" && /^-?\d+$/.test(value) ? value : JSON.stringify(value); - source = source.slice(0, scalar.span.start) + serialized + source.slice(scalar.span.end); - } - return parseJson(source); - }, - setAllRaw(document, key, values) { - const occurrences = document.scalarOccurrences.get(key) ?? []; - const expected = occurrences.length; - if (values.length !== expected) { - throw new Error(`Expected ${expected} raw values for JSON pointer "${key}".`); - } - let source = document.source; - for (let index = occurrences.length - 1; index >= 0; index -= 1) { - const scalar = occurrences[index]; - source = source.slice(0, scalar.span.start) + values[index] + source.slice(scalar.span.end); - } - return parseJson(source); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/raw.ts -var parseRaw = (source) => { - const nodes = splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid" - })); - return { format: "raw", source, nodes, issues: [] }; -}; -var rawAdapter = { - parse: parseRaw, - entries() { - return []; - }, - get() { - return void 0; - }, - getAll() { - return []; - }, - getAllRaw() { - return []; - }, - set(_document, _key, _value) { - throw new Error("Raw mode does not support typed field writes."); - }, - setAll(document, _key, values) { - if (values.length > 0) throw new Error("Raw mode does not support typed field writes."); - return document; - }, - setAllRaw(document, _key, values) { - if (values.length > 0) throw new Error("Raw mode does not support typed field writes."); - return document; - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/xml-properties.ts -var decodeXml = (value) => value.replace(/&(?:#x[\da-f]+|#\d+|amp|quot|apos|lt|gt);/gi, (entity) => { - const token = entity.slice(1, -1); - if (token.toLowerCase().startsWith("#x")) { - return String.fromCodePoint(Number.parseInt(token.slice(2), 16)); - } - if (token.startsWith("#")) { - return String.fromCodePoint(Number.parseInt(token.slice(1), 10)); - } - return { amp: "&", quot: '"', apos: "'", lt: "<", gt: ">" }[token.toLowerCase()]; -}); -var encodeXml = (value, quote) => { - let encoded = (value === null ? "" : String(value)).replace(/&/g, "&").replace(//g, ">"); - encoded = quote === '"' ? encoded.replace(/"/g, """) : encoded.replace(/'/g, "'"); - return encoded; -}; -var parseXmlProperties = (source) => { - const properties = []; - const propertyPattern = /]*>/gi; - for (const match of source.matchAll(propertyPattern)) { - const tag = match[0]; - const tagStart = match.index; - const attributes = /* @__PURE__ */ new Map(); - const attributePattern = /\b(name|value)\s*=\s*(["'])(.*?)\2/gi; - for (const attribute of tag.matchAll(attributePattern)) { - const quote = attribute[2]; - const rawValue = attribute[3]; - const rawStart = tagStart + attribute.index + attribute[0].indexOf(quote) + 1; - attributes.set(attribute[1].toLowerCase(), { - value: decodeXml(rawValue), - span: { start: rawStart, end: rawStart + rawValue.length }, - quote - }); - } - const name = attributes.get("name"); - const value = attributes.get("value"); - if (name && value) { - properties.push({ - key: name.value, - value: value.value, - valueSpan: value.span, - quote: value.quote - }); - } - } - const nodes = splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid" - })); - return { format: "xml-properties", source, nodes, issues: [], properties }; -}; -var effectiveProperty = (document, key) => { - for (let index = document.properties.length - 1; index >= 0; index -= 1) { - const property = document.properties[index]; - if (property.key === key) return property; - } - return void 0; -}; -var xmlPropertiesAdapter = { - parse: parseXmlProperties, - entries(document) { - const entries = /* @__PURE__ */ new Map(); - for (const property of document.properties) entries.set(property.key, property.value); - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveProperty(document, key)?.value; - }, - getAll(document, key) { - return document.properties.filter((property) => property.key === key).map((property) => property.value); - }, - getAllRaw(document, key) { - return document.properties.filter((property) => property.key === key).map((property) => document.source.slice(property.valueSpan.start, property.valueSpan.end)); - }, - set(document, key, value) { - const property = effectiveProperty(document, key); - if (!property) throw new Error(`XML property "${key}" does not exist.`); - return parseXmlProperties( - document.source.slice(0, property.valueSpan.start) + encodeXml(value, property.quote) + document.source.slice(property.valueSpan.end) - ); - }, - setAll(document, key, values) { - const properties = document.properties.filter((property) => property.key === key); - if (properties.length !== values.length) { - throw new Error(`Expected ${properties.length} values for XML property "${key}".`); - } - let source = document.source; - for (let index = properties.length - 1; index >= 0; index -= 1) { - const property = properties[index]; - source = source.slice(0, property.valueSpan.start) + encodeXml(values[index], property.quote) + source.slice(property.valueSpan.end); - } - return parseXmlProperties(source); - }, - setAllRaw(document, key, values) { - const properties = document.properties.filter((property) => property.key === key); - if (properties.length !== values.length) { - throw new Error(`Expected ${properties.length} raw values for XML property "${key}".`); - } - let source = document.source; - for (let index = properties.length - 1; index >= 0; index -= 1) { - const property = properties[index]; - source = source.slice(0, property.valueSpan.start) + values[index] + source.slice(property.valueSpan.end); - } - return parseXmlProperties(source); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/registry.ts -var createAdapterRegistry = () => /* @__PURE__ */ new Map([ - ["java-properties", javaPropertiesAdapter], - ["ini", iniAdapter], - ["unreal-ini", unrealIniAdapter], - ["key-value", keyValueAdapter], - ["json", jsonAdapter], - ["xml-properties", xmlPropertiesAdapter], - ["raw", rawAdapter] -]); - -// ../../packages/config-editor/src/validation.ts -var issue = (field, code, message) => ({ - code, - message, - severity: "error", - fieldKey: field.key -}); -var scalarText = (input) => { - if (input === null) return ""; - if (typeof input === "string") return input; - if (typeof input === "number" || typeof input === "boolean") return String(input); - throw new TypeError("Value must be a scalar."); -}; -var coerceFieldValue = (field, input) => { - if (field.type === "string" || field.type === "secret" || field.type === "enum") { - return scalarText(input); - } - if (field.type === "boolean") { - if (input === true || input === false) return input; - if (input === "true") return true; - if (input === "false") return false; - throw new TypeError("Value must be true or false."); - } - if (typeof input === "number") { - if (!Number.isFinite(input)) throw new TypeError("Value must be a finite number."); - if (field.type === "integer" && !Number.isSafeInteger(input)) { - throw new TypeError("Value must be an integer."); - } - return input; - } - const text = scalarText(input).trim(); - if (field.type === "integer") { - if (!/^[+-]?\d+$/.test(text)) throw new TypeError("Value must be an integer."); - const value2 = Number(text); - if (!Number.isSafeInteger(value2)) throw new TypeError("Value must be an integer."); - return value2; - } - if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) { - throw new TypeError("Value must be a finite number."); - } - const value = Number(text); - if (!Number.isFinite(value)) throw new TypeError("Value must be a finite number."); - return value; -}; -var validateField2 = (field, value) => { - const issues = []; - if (field.type === "integer" && (typeof value !== "number" || !Number.isSafeInteger(value))) { - issues.push(issue(field, "type", "Value must be an integer.")); - return issues; - } - if (field.type === "number" && (typeof value !== "number" || !Number.isFinite(value))) { - issues.push(issue(field, "type", "Value must be a finite number.")); - return issues; - } - if (field.type === "boolean" && typeof value !== "boolean") { - issues.push(issue(field, "type", "Value must be true or false.")); - return issues; - } - if ((field.type === "string" || field.type === "secret" || field.type === "enum") && typeof value !== "string") { - issues.push(issue(field, "type", "Value must be text.")); - return issues; - } - if (typeof value === "number") { - const below = field.min !== void 0 && value < field.min; - const above = field.max !== void 0 && value > field.max; - if (below || above) { - let message; - if (field.min !== void 0 && field.max !== void 0) { - message = `Value must be between ${field.min} and ${field.max} (inclusive).`; - } else if (field.min !== void 0) message = `Value must be at least ${field.min}.`; - else message = `Value must be at most ${field.max}.`; - issues.push(issue(field, "range", message)); - } - } - if (field.type === "enum" && typeof value === "string" && !field.values?.includes(value)) { - issues.push( - issue(field, "enum", `Value must be one of: ${(field.values ?? []).join(", ")}.`) - ); - } - if (field.pattern !== void 0 && typeof value === "string") { - const pattern = new RegExp(`^(?:${field.pattern})$`); - if (!pattern.test(value)) { - issues.push(issue(field, "pattern", "Value does not match the required format.")); - } - } - return issues; -}; - -// ../../packages/config-editor/src/store.ts -var MASKED_SECRET = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"; -var allFields = (schema) => schema.sections.flatMap((section) => section.fields); -var sensitiveKey = (key) => { - const normalized = key.replace(/([a-z\d])([A-Z])/g, "$1.$2").toLowerCase(); - const segments = normalized.split(/[^a-z\d]+/).filter(Boolean); - const sensitiveSegments = /* @__PURE__ */ new Set([ - "password", - "passwd", - "pass", - "secret", - "token", - "apikey", - "privatekey", - "gslt", - "steampass" - ]); - return segments.some((segment) => sensitiveSegments.has(segment)) || /(?:password|passwd|steampass|api[-_.]?key|private[-_.]?key|gslt)/i.test(key); -}; -var inferredType = (value) => { - if (typeof value === "boolean") return "boolean"; - if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number"; - if (typeof value !== "string") return "string"; - if (/^(?:true|false)$/i.test(value)) return "boolean"; - return "string"; -}; -var humanizeKey = (key) => { - const leaf = key.split(/[./]/).at(-1) ?? key; - const label = leaf.replace(/[-_]+/g, " ").replace(/([a-z\d])([A-Z])/g, "$1 $2"); - return label.charAt(0).toUpperCase() + label.slice(1); -}; -var schemaWithDetectedFields = (declaredSchema, entries) => { - const declaredKeys = new Set(allFields(declaredSchema).map((field) => field.key)); - const detectedFields = []; - for (const entry of entries) { - if (entry.key === "" || declaredKeys.has(entry.key)) continue; - const sensitive = sensitiveKey(entry.key); - detectedFields.push({ - key: entry.key, - label: humanizeKey(entry.key), - description: `Setting discovered in ${declaredSchema.label}.`, - documentation: declaredSchema.documentation ?? `Configuration file ${declaredSchema.path}`, - type: sensitive ? "secret" : inferredType(entry.value), - sensitive, - restartRequired: true - }); - } - if (detectedFields.length === 0) return declaredSchema; - const detectedSection = { - id: "detected-settings", - label: "Detected settings", - description: "Settings discovered from the loaded configuration file.", - fields: detectedFields - }; - return { - ...declaredSchema, - sections: [...declaredSchema.sections, detectedSection] - }; -}; -var valuesEqual = (left, right) => Object.is(left, right); -var displayValue = (field, value) => { - if ((field.sensitive || field.type === "secret") && value !== void 0 && value !== "") { - return MASKED_SECRET; - } - return value === null || value === void 0 ? "" : String(value); -}; -var deepFreezeSnapshot = (snapshot) => { - for (const field of Object.values(snapshot.fields)) { - for (const fieldIssue of field.issues) Object.freeze(fieldIssue); - Object.freeze(field.issues); - Object.freeze(field); - } - for (const change of snapshot.changes) Object.freeze(change); - for (const editorIssue of snapshot.issues) Object.freeze(editorIssue); - Object.freeze(snapshot.fields); - Object.freeze(snapshot.changes); - Object.freeze(snapshot.issues); - return Object.freeze(snapshot); -}; -var ConfigEditorStore = class _ConfigEditorStore { - declaredSchema; - schemaValue; - adapter; - originalDocument; - workingDocument; - fields; - constructor(schema, document, adapter) { - this.declaredSchema = schema; - this.adapter = adapter; - this.schemaValue = schemaWithDetectedFields(schema, adapter.entries(document)); - this.originalDocument = document; - this.workingDocument = document; - this.fields = this.readFields(document, document); - } - get schema() { - return this.schemaValue; - } - static fromLoadedFile(schema, document) { - if (schema.format !== document.format) { - throw new TypeError( - `Schema format "${schema.format}" does not match document format "${document.format}".` - ); - } - const adapter = createAdapterRegistry().get(schema.format); - if (!adapter) throw new TypeError(`No adapter registered for "${schema.format}".`); - return new _ConfigEditorStore(schema, document, adapter); - } - readValue(document, field) { - const raw = this.adapter.get(document, field.key); - if (raw === void 0) return { value: void 0, issues: [] }; - try { - const value = coerceFieldValue(field, raw); - return { value, issues: validateField2(field, value) }; - } catch (error) { - return { - value: void 0, - issues: [ - { - code: "type", - message: error instanceof Error ? error.message : "Invalid value.", - severity: "error", - fieldKey: field.key, - filePath: this.schema.path - } - ] - }; - } - } - readFields(original, working) { - return new Map( - allFields(this.schema).map((field) => { - const originalResult = this.readValue(original, field); - const workingResult = this.readValue(working, field); - return [ - field.key, - { - schema: field, - original: originalResult.value, - current: workingResult.value, - displayValue: displayValue(field, workingResult.value), - issues: workingResult.issues - } - ]; - }) - ); - } - setDisplayValue(key, input) { - const state = this.fields.get(key); - if (!state) throw new TypeError(`Unknown configuration field "${key}".`); - const sensitive = state.schema.sensitive || state.schema.type === "secret"; - if (sensitive && input === MASKED_SECRET) { - state.displayValue = MASKED_SECRET; - state.issues = []; - return; - } - state.displayValue = input; - let value; - try { - value = coerceFieldValue(state.schema, input); - } catch (error) { - state.issues = [ - { - code: "type", - message: error instanceof Error ? error.message : "Invalid value.", - severity: "error", - fieldKey: key, - filePath: this.schema.path - } - ]; - return; - } - const issues = validateField2(state.schema, value).map((fieldIssue) => ({ - ...fieldIssue, - filePath: this.schema.path - })); - state.issues = issues; - if (issues.length > 0) return; - this.workingDocument = this.adapter.set(this.workingDocument, key, value); - state.current = value; - if (sensitive) state.displayValue = MASKED_SECRET; - } - serializeSelectedFile() { - return this.adapter.serialize(this.workingDocument); - } - serializeForDisplay() { - let displayDocument = this.workingDocument; - for (const field of allFields(this.schema)) { - if (!(field.sensitive || field.type === "secret")) continue; - const values = this.adapter.getAll(displayDocument, field.key); - if (values.length === 0) continue; - displayDocument = this.adapter.setAll( - displayDocument, - field.key, - values.map(() => MASKED_SECRET) - ); - } - return this.adapter.serialize(displayDocument); - } - validateSerializedSource(source) { - const document = this.adapter.parse(source); - const issues = this.adapter.validate(document, this.schema); - for (const field of allFields(this.schema)) { - issues.push(...this.readValue(document, field).issues); - } - return issues; - } - loadedSource() { - return this.adapter.serialize(this.originalDocument); - } - replaceWorkingSource(source) { - let document = this.adapter.parse(source); - for (const field of allFields(this.schema)) { - if (!(field.sensitive || field.type === "secret")) continue; - const displayedValues = this.adapter.getAll(document, field.key); - if (!displayedValues.includes(MASKED_SECRET)) continue; - const currentSecrets = this.adapter.getAllRaw(this.workingDocument, field.key); - if (displayedValues.length !== currentSecrets.length) { - throw new Error( - `Secret occurrences for "${field.key}" cannot be added or removed in raw mode.` - ); - } - document = this.adapter.setAllRaw( - document, - field.key, - displayedValues.map( - (value, index) => value === MASKED_SECRET ? currentSecrets[index] : this.adapter.getAllRaw(document, field.key)[index] - ) - ); - } - this.workingDocument = document; - this.schemaValue = schemaWithDetectedFields( - this.declaredSchema, - this.adapter.entries(document) - ); - this.fields = this.readFields(this.originalDocument, this.workingDocument); - } - acceptSavedSource(source) { - const document = this.adapter.parse(source); - this.originalDocument = document; - this.workingDocument = document; - this.schemaValue = schemaWithDetectedFields( - this.declaredSchema, - this.adapter.entries(document) - ); - this.fields = this.readFields(document, document); - } - snapshot() { - const fields = {}; - const changes = []; - const issues = [...this.workingDocument.issues]; - const documentDirty = this.adapter.serialize(this.workingDocument) !== this.adapter.serialize(this.originalDocument); - for (const [key, state] of this.fields) { - const sensitive = Boolean(state.schema.sensitive || state.schema.type === "secret"); - const dirty = !valuesEqual(state.current, state.original); - const fieldIssues = state.issues.map((fieldIssue) => ({ ...fieldIssue })); - const fieldSnapshot = { - key, - label: state.schema.label, - type: state.schema.type, - displayValue: state.displayValue, - sensitive, - dirty, - issues: fieldIssues, - ...!sensitive && state.current !== void 0 ? { value: state.current } : {} - }; - fields[key] = fieldSnapshot; - issues.push(...fieldIssues); - if (dirty) { - changes.push({ - key, - label: state.schema.label, - before: sensitive ? null : state.original, - after: sensitive ? null : state.current, - sensitive, - restartRequired: Boolean(state.schema.restartRequired) - }); - } - } - const unstructuredChanges = documentDirty && changes.length === 0; - return deepFreezeSnapshot({ - filePath: this.schema.path, - fields, - changes, - issues, - dirty: documentDirty, - unstructuredChanges, - restartRequired: unstructuredChanges || changes.some((change) => change.restartRequired) - }); - } -}; - -// ../../packages/config-editor/src/fingerprint.ts -var SHA256_CONSTANTS = [ - 1116352408, - 1899447441, - 3049323471, - 3921009573, - 961987163, - 1508970993, - 2453635748, - 2870763221, - 3624381080, - 310598401, - 607225278, - 1426881987, - 1925078388, - 2162078206, - 2614888103, - 3248222580, - 3835390401, - 4022224774, - 264347078, - 604807628, - 770255983, - 1249150122, - 1555081692, - 1996064986, - 2554220882, - 2821834349, - 2952996808, - 3210313671, - 3336571891, - 3584528711, - 113926993, - 338241895, - 666307205, - 773529912, - 1294757372, - 1396182291, - 1695183700, - 1986661051, - 2177026350, - 2456956037, - 2730485921, - 2820302411, - 3259730800, - 3345764771, - 3516065817, - 3600352804, - 4094571909, - 275423344, - 430227734, - 506948616, - 659060556, - 883997877, - 958139571, - 1322822218, - 1537002063, - 1747873779, - 1955562222, - 2024104815, - 2227730452, - 2361852424, - 2428436474, - 2756734187, - 3204031479, - 3329325298 -]; -var rotateRight = (value, bits) => value >>> bits | value << 32 - bits; -var utf8Bytes = (source) => { - const bytes = []; - for (const character of source) { - const point = character.codePointAt(0); - if (point <= 127) bytes.push(point); - else if (point <= 2047) { - bytes.push(192 | point >>> 6, 128 | point & 63); - } else if (point <= 65535) { - bytes.push( - 224 | point >>> 12, - 128 | point >>> 6 & 63, - 128 | point & 63 - ); - } else { - bytes.push( - 240 | point >>> 18, - 128 | point >>> 12 & 63, - 128 | point >>> 6 & 63, - 128 | point & 63 - ); - } - } - return bytes; -}; -var sha256 = (source) => { - const bytes = utf8Bytes(source); - const bitLength = bytes.length * 8; - bytes.push(128); - while (bytes.length % 64 !== 56) bytes.push(0); - const high = Math.floor(bitLength / 4294967296); - const low = bitLength >>> 0; - for (let shift = 24; shift >= 0; shift -= 8) bytes.push(high >>> shift & 255); - for (let shift = 24; shift >= 0; shift -= 8) bytes.push(low >>> shift & 255); - const hash = [ - 1779033703, - 3144134277, - 1013904242, - 2773480762, - 1359893119, - 2600822924, - 528734635, - 1541459225 - ]; - const words = new Array(64).fill(0); - for (let offset = 0; offset < bytes.length; offset += 64) { - for (let index = 0; index < 16; index += 1) { - const cursor = offset + index * 4; - words[index] = (bytes[cursor] << 24 | bytes[cursor + 1] << 16 | bytes[cursor + 2] << 8 | bytes[cursor + 3]) >>> 0; - } - for (let index = 16; index < 64; index += 1) { - const previous15 = words[index - 15]; - const previous2 = words[index - 2]; - const sigma0 = rotateRight(previous15, 7) ^ rotateRight(previous15, 18) ^ previous15 >>> 3; - const sigma1 = rotateRight(previous2, 17) ^ rotateRight(previous2, 19) ^ previous2 >>> 10; - words[index] = words[index - 16] + sigma0 + words[index - 7] + sigma1 >>> 0; - } - let [a, b, c, d, e, f, g, h] = hash; - for (let index = 0; index < 64; index += 1) { - const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25); - const choose = e & f ^ ~e & g; - const temporary1 = h + sum1 + choose + SHA256_CONSTANTS[index] + words[index] >>> 0; - const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22); - const majority = a & b ^ a & c ^ b & c; - const temporary2 = sum0 + majority >>> 0; - h = g; - g = f; - f = e; - e = d + temporary1 >>> 0; - d = c; - c = b; - b = a; - a = temporary1 + temporary2 >>> 0; - } - hash[0] = hash[0] + a >>> 0; - hash[1] = hash[1] + b >>> 0; - hash[2] = hash[2] + c >>> 0; - hash[3] = hash[3] + d >>> 0; - hash[4] = hash[4] + e >>> 0; - hash[5] = hash[5] + f >>> 0; - hash[6] = hash[6] + g >>> 0; - hash[7] = hash[7] + h >>> 0; - } - return hash.map((word) => word.toString(16).padStart(8, "0")).join(""); -}; -var fingerprint = async (source) => sha256(source); - -// ../../packages/config-editor/src/gateway.ts -var MISSING_FILE_FINGERPRINT = "missing"; -var isMissingFileError = (error) => { - const message = error instanceof Error ? error.message : String(error); - return /(?:\b404\b|not[ -]?found|missing)/i.test(message); -}; -var withMissingFileFallback = (gateway2, suffixes = [".scroll_template", ".default"]) => { - const resolvedPaths = /* @__PURE__ */ new Map(); - const fallbackSuffixes = typeof suffixes === "string" ? [suffixes] : suffixes; - return { - async load(path) { - try { - const content = await gateway2.load(path); - resolvedPaths.set(path, path); - return content; - } catch (error) { - if (!isMissingFileError(error)) throw error; - const resolved = resolvedPaths.get(path); - if (resolved && resolved !== path) { - try { - return await gateway2.load(resolved); - } catch (fallbackError) { - if (!isMissingFileError(fallbackError)) throw fallbackError; - } - } - for (const suffix of fallbackSuffixes) { - const fallback = `${path}${suffix}`; - if (fallback === resolved) continue; - try { - const content = await gateway2.load(fallback); - resolvedPaths.set(path, fallback); - return content; - } catch (fallbackError) { - if (!isMissingFileError(fallbackError)) throw fallbackError; - } - } - throw new Error(`Missing configuration file: ${path}`); - } - }, - async save(path, content, expectedFingerprint) { - const resolved = resolvedPaths.get(path) ?? path; - if (resolved !== path) { - const result = await gateway2.save( - path, - content, - MISSING_FILE_FINGERPRINT - ); - if (result.status === "saved") resolvedPaths.set(path, path); - return result; - } - return await gateway2.save(resolved, content, expectedFingerprint); - } - }; -}; -var schemaForPath = (manifest, path) => { - const schema = path ? manifest.files.find((candidate) => candidate.path === path) : manifest.files[0]; - if (!schema) throw new TypeError(path ? `Manifest has no file "${path}".` : "Manifest has no files."); - return schema; -}; -var loadEditor = async (gateway2, manifest, path) => { - const schema = schemaForPath(manifest, path); - const adapter = createAdapterRegistry().get(schema.format); - if (!adapter) throw new TypeError(`No adapter registered for "${schema.format}".`); - const source = await gateway2.load(schema.path); - return ConfigEditorStore.fromLoadedFile(schema, adapter.parse(source)); -}; -var saveSelectedFile = async (store, gateway2) => { - if (store.snapshot().issues.some((issue2) => issue2.severity === "error")) { - throw new Error("Configuration has validation errors and cannot be saved."); - } - const content = store.serializeSelectedFile(); - const serializedIssues = store.validateSerializedSource(content); - if (serializedIssues.some((issue2) => issue2.severity === "error")) { - throw new Error("Serialized configuration failed validation."); - } - const expectedFingerprint = await fingerprint(store.loadedSource()); - const result = await gateway2.save(store.schema.path, content, expectedFingerprint); - if (result.status === "conflict") return result; - const contentFingerprint = await fingerprint(content); - if (result.fingerprint !== contentFingerprint) { - throw new Error("Save verification failed: gateway fingerprint does not match content."); - } - const remote = await gateway2.load(store.schema.path); - const remoteFingerprint = await fingerprint(remote); - if (remoteFingerprint !== result.fingerprint || remote !== content) { - throw new Error("Save verification failed: reloaded content differs from the saved content."); - } - store.acceptSavedSource(remote); - return result; -}; - -// ../../packages/component/dist/index.js -var import_ui = __toESM(require_ui(), 1); -var import_ui2 = __toESM(require_ui(), 1); -var import_utils = __toESM(require_utils(), 1); -var import_ui3 = __toESM(require_ui(), 1); -var createAsyncBridge = (onSettled = () => void 0, trace = () => void 0) => { - const pending = /* @__PURE__ */ new Map(); - const early = /* @__PURE__ */ new Map(); - const settle = (operation, result) => { - if (result.tag === "ok") operation.resolve(result.val); - else operation.reject(new Error(String(result.val))); - onSettled(); - }; - const complete = (id, result) => { - trace(`Async callback received for id: ${id} with result: ${result.tag}`); - const operation = pending.get(id); - if (!operation) { - early.set(id, result); - return; - } - pending.delete(id); - settle(operation, result); - }; - const wrap = (fn) => (...args) => new Promise((resolve, reject) => { - const id = fn(...args); - const operation = { - resolve: (value) => resolve(value), - reject - }; - const earlyResult = early.get(id); - if (earlyResult) { - early.delete(id); - settle(operation, earlyResult); - return; - } - pending.set(id, operation); - }); - return { complete, wrap }; -}; -var lowerPropertyValue = (value) => value === void 0 || value === null ? void 0 : String(value); -var callbackMap = {}; -function emit(nodeid, event, e) { - (0, import_ui2.log)(`Emit called for nodeid: ${nodeid}, event: ${event}`); - const callbacks = callbackMap[nodeid]; - const result = callbacks?.[event]?.(e); - if (result instanceof Promise) { - result.then(() => (0, import_ui2.rerender)()); - } -} -var registerHooks = (id, fnresult) => { - switch (true) { - case !!fnresult.init: - (0, import_ui2.setHook)(id, "init"); - callbackMap[id] = { - ...callbackMap[id], - init: fnresult.init - }; - break; - } -}; -var createDFunc = (dfunc2) => { - return (tag, props, ...children) => { - children = children.flat(); - if (typeof tag !== "string") { - if (typeof tag === "function") { - const fnresult = tag(props); - if (fnresult?.view) { - const id3 = fnresult.view(props); - registerHooks(id3, fnresult); - return id3; - } else { - return tag(props); - } - } - const id2 = tag.view(props); - registerHooks(id2, tag); - return id2; - } - const ps = { prop: [], on: [] }; - const cbObj = {}; - if (props) { - for (const [key, value] of Object.entries(props)) { - if (value instanceof Function) { - const eventKey = key.startsWith("on") ? key.slice(2).toLowerCase() : key; - cbObj[eventKey] = value; - ps.on.push(eventKey); - } else { - const loweredValue = lowerPropertyValue(value); - if (loweredValue === void 0) continue; - if (typeof value === "boolean") { - ps.prop.push({ key, value: loweredValue }); - continue; - } - ps.prop.push({ key, value: loweredValue }); - } - } - } - const id = dfunc2( - tag, - ps, - children.filter((c) => typeof c !== "boolean").map((c) => String(c)) - ); - callbackMap[id] = { - ...callbackMap[id], - ...cbObj - }; - return id; - }; -}; -var asyncBridge = createAsyncBridge( - () => (0, import_ui2.rerender)(), - (message) => (0, import_ui2.log)(message) -); -var asyncCallback = asyncBridge.complete; -var rawAsyncToPromise = asyncBridge.wrap; -var createComponent = (j) => ({ - init: (ctx) => j(ctx), - emit, - asyncComplete: asyncCallback -}); -var d2 = createDFunc(import_ui.d); - -// ../../packages/config-editor/src/copy.ts -var copy = { - appTitle: "Server configuration", - loading: "Loading configuration\u2026", - formTab: "Form", - rawTab: "Raw", - filesHeading: "Configuration files", - inspectorHeading: "Changes & validation", - noChanges: "No pending changes", - rawChanged: "Raw configuration changed", - unknownKeys: "Unknown keys: Preserved", - restartRequired: "Restart required", - stopBeforeSave: "Stop the server before saving configuration files that it may rewrite.", - noRestartRequired: "No restart required", - save: "Save changes", - saving: "Saving\u2026", - saved: "Saved and verified", - conflict: "Remote file changed. Reload before saving.", - rawLabel: "Raw configuration", - validationHeading: "Validation", - valid: "Configuration is valid", - secretChanged: "Secret changed", - fileFormat: "Format" -}; - -// ../../packages/config-editor/src/styles.ts -var EDITOR_STYLES = ` -:host { - display: block; - width: 100%; - --druid-bg: #07100d; - --druid-panel: #101b17; - --druid-panel-raised: #15231d; - --druid-border: #294137; - --druid-border-strong: #3f6655; - --druid-text: #f4f7f5; - --druid-muted: #9eb0a8; - --druid-accent: #a8ef9c; - --druid-accent-ink: #10210f; - --druid-warning: #f0c96b; - --druid-error: #ff8b7f; - color: var(--druid-text); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - color-scheme: dark; -} -* { box-sizing: border-box; } -button, input, select, textarea { font: inherit; } -button, input, select, textarea, a { outline: none; } -button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, a:focus-visible { - box-shadow: 0 0 0 3px rgba(168, 239, 156, .28); - border-color: var(--druid-accent); -} -.config-editor { min-height: 640px; background: radial-gradient(circle at 75% 0%, #173326 0, transparent 32%), var(--druid-bg); color: var(--druid-text); } -.editor-header { min-height: 76px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px 22px; border-bottom: 1px solid var(--druid-border); } -.eyebrow { color: var(--druid-accent); font-size: 11px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; } -.editor-title { margin: 3px 0 0; font-size: clamp(20px, 2.4vw, 30px); line-height: 1.1; } -.server-version { color: var(--druid-muted); font-size: 13px; } -.editor-grid { display: grid; grid-template-columns: minmax(210px, 250px) minmax(420px, 1fr) minmax(260px, 320px); min-height: 564px; } -.file-rail, .inspector { background: rgba(11, 21, 17, .8); } -.file-rail { padding: 18px 14px; border-right: 1px solid var(--druid-border); } -.rail-heading, .inspector h2 { margin: 0 0 12px; font-size: 12px; color: var(--druid-muted); letter-spacing: .08em; text-transform: uppercase; } -.file-list { display: grid; gap: 8px; } -.file-button { width: 100%; min-height: 52px; display: grid; gap: 3px; padding: 10px 12px; text-align: left; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--druid-text); cursor: pointer; } -.file-button:hover { background: var(--druid-panel); } -.file-button[aria-current="true"] { border-color: var(--druid-border-strong); background: var(--druid-panel-raised); } -.file-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; } -.file-format { color: var(--druid-muted); font-size: 12px; text-transform: uppercase; } -.editor-main { min-width: 0; padding: 18px 22px 92px; } -.tabs { display: inline-flex; gap: 4px; padding: 4px; margin-bottom: 18px; border: 1px solid var(--druid-border); border-radius: 10px; background: #0b1511; } -.tab { min-height: 40px; min-width: 92px; border: 0; border-radius: 7px; background: transparent; color: var(--druid-muted); cursor: pointer; } -.tab[aria-selected="true"] { background: var(--druid-panel-raised); color: var(--druid-text); } -.form-editor { display: grid; gap: 18px; } -.field-section { min-width: 0; margin: 0; padding: 18px; border: 1px solid var(--druid-border); border-radius: 14px; background: rgba(16, 27, 23, .84); } -.field-section legend { padding: 0 8px; color: var(--druid-accent); font-weight: 800; } -.section-description { margin: 0 0 16px; color: var(--druid-muted); font-size: 13px; } -.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } -.field-control { min-width: 0; display: grid; align-content: start; gap: 7px; } -.field-control label { font-size: 13px; font-weight: 750; } -.field-description { min-height: 34px; margin: 0; color: var(--druid-muted); font-size: 12px; line-height: 1.45; } -.field-input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--druid-border); border-radius: 9px; background: #09130f; color: var(--druid-text); } -.boolean-control { min-height: 44px; display: flex; align-items: center; gap: 10px; padding: 8px 11px; border: 1px solid var(--druid-border); border-radius: 9px; background: #09130f; } -.boolean-control input { width: 20px; height: 20px; accent-color: var(--druid-accent); } -.field-meta { display: flex; flex-wrap: wrap; gap: 8px; color: var(--druid-muted); font-size: 11px; } -.restart-chip { color: var(--druid-warning); } -.field-error { margin: 0; color: var(--druid-error); font-size: 12px; } -.raw-editor { display: grid; gap: 10px; } -.raw-textarea { width: 100%; min-height: 440px; resize: vertical; padding: 16px; border: 1px solid var(--druid-border); border-radius: 12px; background: #050b08; color: #dce8e1; font: 13px/1.55 "Cascadia Code", "SFMono-Regular", Consolas, monospace; tab-size: 2; } -.inspector { display: flex; flex-direction: column; gap: 18px; padding: 18px; border-left: 1px solid var(--druid-border); } -.status-card { padding: 14px; border: 1px solid var(--druid-border); border-radius: 12px; background: var(--druid-panel); } -.status-card p { margin: 4px 0; color: var(--druid-muted); font-size: 13px; } -.change-list, .issue-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } -.change-item { padding: 10px; border: 1px solid var(--druid-border); border-radius: 9px; background: #0b1511; font-size: 13px; } -.change-value { display: block; margin-top: 4px; color: var(--druid-muted); word-break: break-word; } -.issue-item { color: var(--druid-error); font-size: 12px; } -.action-bar { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: auto 0 0; padding-top: 14px; background: linear-gradient(transparent, rgba(11, 21, 17, .98) 32%); } -.save-button { min-height: 46px; padding: 0 18px; border: 0; border-radius: 9px; background: var(--druid-accent); color: var(--druid-accent-ink); font-weight: 850; cursor: pointer; } -.save-button:disabled { opacity: .45; cursor: not-allowed; } -.action-status { color: var(--druid-muted); font-size: 12px; } -.error-shell { margin: 20px; padding: 18px; border: 1px solid var(--druid-error); border-radius: 12px; background: #28120f; color: #ffd8d2; } -@media (max-width: 900px) { - .editor-grid { grid-template-columns: 190px minmax(0, 1fr); } - .inspector { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--druid-border); } -} -@media (max-width: 560px) { - .config-editor { min-height: 100%; } - .editor-header { align-items: flex-start; padding: 14px 16px; } - .editor-grid { display: block; } - .file-rail { padding: 12px 16px; border-right: 0; border-bottom: 1px solid var(--druid-border); overflow-x: auto; } - .file-list { display: flex; width: max-content; } - .file-button { width: 190px; } - .editor-main { padding: 16px 16px 28px; } - .tabs { display: flex; } - .tab { flex: 1; } - .field-section { padding: 14px; } - .field-grid { grid-template-columns: 1fr; } - .raw-textarea { min-height: 360px; } - .inspector { padding: 16px; } - .action-bar { position: static; } - .save-button { width: 100%; } -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } -} -`; - -// ../../packages/component/dist/jsx-runtime.js -function jsx(type, props) { - const { children, ...rest } = props || {}; - if (children !== void 0) { - return d2(type, rest, children); - } - return d2(type, rest); -} -var jsxs = jsx; -var Fragment = Symbol.for("react.fragment"); - -// ../../packages/config-editor/src/components/ActionBar.tsx -var ActionBar = ({ dirty, invalid, saving, status, onSave }) => /* @__PURE__ */ jsxs("div", { class: "action-bar", children: [ - /* @__PURE__ */ jsx("span", { class: "action-status", "aria-live": "polite", children: status }), - /* @__PURE__ */ jsx( - "button", - { - type: "button", - class: "save-button", - disabled: !dirty || invalid || saving, - onClick: () => onSave(), - children: saving ? copy.saving : copy.save - } - ) -] }); - -// ../../packages/config-editor/src/components/FileRail.tsx -var FileRail = ({ manifest, selectedPath, onSelect }) => /* @__PURE__ */ jsxs("nav", { class: "file-rail", "aria-label": copy.filesHeading, children: [ - /* @__PURE__ */ jsx("h2", { class: "rail-heading", children: copy.filesHeading }), - /* @__PURE__ */ jsx("div", { class: "file-list", children: manifest.files.map((file) => /* @__PURE__ */ jsxs( - "button", - { - type: "button", - class: "file-button", - "aria-current": file.path === selectedPath ? "true" : "false", - onClick: () => onSelect(file.path), - children: [ - /* @__PURE__ */ jsx("span", { class: "file-path", children: file.label }), - /* @__PURE__ */ jsx("span", { class: "file-format", children: file.format }) - ] - } - )) }) -] }); - -// ../../packages/config-editor/src/components/FieldControl.tsx -var FieldControl = ({ field, state, onChange }) => { - const inputId = `config-field-${field.key.replace(/[^a-zA-Z0-9_-]/g, "-")}`; - const descriptionId = `${inputId}-description`; - const input = field.type === "enum" ? /* @__PURE__ */ jsx( - "select", - { - id: inputId, - class: "field-input", - "aria-label": field.label, - "aria-describedby": descriptionId, - "aria-invalid": state.issues.length > 0 ? "true" : "false", - value: state.displayValue, - onChange: (event) => onChange(field.key, event.value()), - children: (field.values ?? []).map((value) => /* @__PURE__ */ jsx("option", { value, selected: state.displayValue === value, children: value })) - } - ) : field.type === "boolean" ? /* @__PURE__ */ jsxs("span", { class: "boolean-control", children: [ - /* @__PURE__ */ jsx( - "input", - { - id: inputId, - type: "checkbox", - "aria-label": field.label, - "aria-describedby": descriptionId, - checked: state.displayValue === "true", - onChange: (event) => onChange(field.key, event.checked() ? "true" : "false") - } - ), - /* @__PURE__ */ jsx("span", { children: state.displayValue === "true" ? "Enabled" : "Disabled" }) - ] }) : /* @__PURE__ */ jsx( - "input", - { - id: inputId, - class: "field-input", - "aria-label": field.label, - "aria-describedby": descriptionId, - type: field.type === "secret" || field.sensitive ? "password" : field.type === "integer" || field.type === "number" ? "number" : "text", - step: field.type === "integer" ? "1" : field.type === "number" ? "any" : void 0, - min: field.min, - max: field.max, - value: state.displayValue, - "aria-invalid": state.issues.length > 0 ? "true" : "false", - autocomplete: field.type === "secret" || field.sensitive ? "new-password" : "off", - onInput: (event) => onChange(field.key, event.value()) - } - ); - return /* @__PURE__ */ jsxs("div", { class: "field-control", children: [ - /* @__PURE__ */ jsx("label", { for: inputId, children: field.label }), - /* @__PURE__ */ jsx("p", { id: descriptionId, class: "field-description", children: field.description }), - input, - /* @__PURE__ */ jsxs("div", { class: "field-meta", children: [ - field.restartRequired ? /* @__PURE__ */ jsx("span", { class: "restart-chip", children: copy.restartRequired }) : false, - /* @__PURE__ */ jsx("span", { children: field.key }) - ] }), - state.issues.map((issue2) => /* @__PURE__ */ jsx("p", { class: "field-error", role: "alert", children: issue2.message })) - ] }); -}; - -// ../../packages/config-editor/src/components/FormEditor.tsx -var FormEditor = ({ schema, snapshot, onChange }) => /* @__PURE__ */ jsx("div", { class: "form-editor", children: schema.sections.map((section) => /* @__PURE__ */ jsxs("fieldset", { class: "field-section", children: [ - /* @__PURE__ */ jsx("legend", { children: section.label }), - section.description ? /* @__PURE__ */ jsx("p", { class: "section-description", children: section.description }) : false, - /* @__PURE__ */ jsx("div", { class: "field-grid", children: section.fields.map((field) => { - const state = snapshot.fields[field.key]; - return state ? /* @__PURE__ */ jsx(FieldControl, { field, state, onChange }) : false; - }) }) -] })) }); - -// ../../packages/config-editor/src/components/Inspector.tsx -var printable = (value) => value === void 0 ? "Not set" : value === null ? "Protected" : String(value); -var Inspector = ({ snapshot }) => /* @__PURE__ */ jsxs("aside", { class: "inspector", "aria-label": copy.inspectorHeading, children: [ - /* @__PURE__ */ jsxs("section", { children: [ - /* @__PURE__ */ jsx("h2", { children: copy.inspectorHeading }), - snapshot.unstructuredChanges ? /* @__PURE__ */ jsx("div", { class: "status-card", children: /* @__PURE__ */ jsx("p", { children: copy.rawChanged }) }) : snapshot.changes.length === 0 ? /* @__PURE__ */ jsx("div", { class: "status-card", children: /* @__PURE__ */ jsx("p", { children: copy.noChanges }) }) : /* @__PURE__ */ jsx("ul", { class: "change-list", children: snapshot.changes.map((change) => /* @__PURE__ */ jsxs("li", { class: "change-item", children: [ - /* @__PURE__ */ jsx("strong", { children: change.label }), - /* @__PURE__ */ jsx("span", { class: "change-value", children: change.sensitive ? copy.secretChanged : `${printable(change.before)} \u2192 ${printable(change.after)}` }) - ] })) }) - ] }), - /* @__PURE__ */ jsxs("section", { class: "status-card", children: [ - /* @__PURE__ */ jsx("h2", { children: copy.validationHeading }), - snapshot.issues.length === 0 ? /* @__PURE__ */ jsx("p", { children: copy.valid }) : /* @__PURE__ */ jsx("ul", { class: "issue-list", "aria-live": "polite", children: snapshot.issues.map((issue2) => /* @__PURE__ */ jsx("li", { class: "issue-item", children: issue2.message })) }), - /* @__PURE__ */ jsx("p", { children: copy.unknownKeys }), - /* @__PURE__ */ jsx("p", { children: snapshot.restartRequired ? copy.restartRequired : copy.noRestartRequired }), - snapshot.restartRequired ? /* @__PURE__ */ jsx("p", { children: copy.stopBeforeSave }) : false - ] }) -] }); - -// ../../packages/config-editor/src/components/RawEditor.tsx -var RawEditor = ({ source, onChange }) => /* @__PURE__ */ jsxs("div", { class: "raw-editor", children: [ - /* @__PURE__ */ jsx("label", { for: "config-raw-source", children: copy.rawLabel }), - /* @__PURE__ */ jsx( - "textarea", - { - id: "config-raw-source", - class: "raw-textarea", - "aria-label": copy.rawLabel, - spellcheck: "false", - value: source, - onInput: (event) => onChange(event.value()) - } - ) -] }); - -// ../../packages/config-editor/src/components/EditorApp.tsx -var EditorApp = ({ - manifest, - store, - snapshot, - mode, - saving, - status, - onMode, - onSelect, - onField, - onRaw, - onSave -}) => /* @__PURE__ */ jsxs("div", { class: "config-editor", children: [ - /* @__PURE__ */ jsx("style", { children: EDITOR_STYLES }), - /* @__PURE__ */ jsxs("header", { class: "editor-header", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("div", { class: "eyebrow", children: "Druid Admin UI" }), - /* @__PURE__ */ jsx("h1", { class: "editor-title", children: manifest.server.displayName }) - ] }), - /* @__PURE__ */ jsx("div", { class: "server-version", children: manifest.server.appVersion ? `Version ${manifest.server.appVersion}` : copy.appTitle }) - ] }), - /* @__PURE__ */ jsxs("div", { class: "editor-grid", children: [ - /* @__PURE__ */ jsx( - FileRail, - { - manifest, - selectedPath: store.schema.path, - onSelect - } - ), - /* @__PURE__ */ jsxs("main", { class: "editor-main", children: [ - /* @__PURE__ */ jsxs("div", { class: "tabs", role: "tablist", "aria-label": "Editor mode", children: [ - /* @__PURE__ */ jsx( - "button", - { - type: "button", - class: "tab", - role: "tab", - disabled: store.schema.sections.length === 0, - "aria-disabled": store.schema.sections.length === 0 ? "true" : "false", - "aria-selected": mode === "form" ? "true" : "false", - onClick: () => onMode("form"), - children: copy.formTab - } - ), - /* @__PURE__ */ jsx( - "button", - { - type: "button", - class: "tab", - role: "tab", - "aria-selected": mode === "raw" ? "true" : "false", - onClick: () => onMode("raw"), - children: copy.rawTab - } - ) - ] }), - mode === "form" ? /* @__PURE__ */ jsx(FormEditor, { schema: store.schema, snapshot, onChange: onField }) : /* @__PURE__ */ jsx(RawEditor, { source: store.serializeForDisplay(), onChange: onRaw }), - /* @__PURE__ */ jsx( - ActionBar, - { - dirty: snapshot.dirty, - invalid: snapshot.issues.some((issue2) => issue2.severity === "error"), - saving, - status, - onSave - } - ) - ] }), - /* @__PURE__ */ jsx(Inspector, { snapshot }) - ] }) -] }); -var createConfigEditorComponent = ({ - manifestPath, - gateway: gateway2 -}) => { - let manifest; - const stores = /* @__PURE__ */ new Map(); - let selectedPath = ""; - let mode = "form"; - let loading = false; - let saving = false; - let status = ""; - let errorMessage = ""; - const selectFile = async (path) => { - if (!manifest) return; - const previousPath = selectedPath; - status = copy.loading; - (0, import_ui3.rerender)(); - try { - let store = stores.get(path); - if (!store) { - store = await loadEditor(gateway2, manifest, path); - stores.set(path, store); - } - selectedPath = path; - mode = store.schema.sections.length === 0 ? "raw" : "form"; - status = ""; - } catch (error) { - selectedPath = previousPath; - status = error instanceof Error ? error.message : String(error); - if (!stores.has(previousPath)) throw error; - } finally { - (0, import_ui3.rerender)(); - } - }; - const initialise = async () => { - loading = true; - try { - manifest = validateManifest(JSON.parse(await gateway2.load(manifestPath))); - selectedPath = manifest.files[0].path; - await selectFile(selectedPath); - } catch (error) { - errorMessage = error instanceof Error ? error.message : String(error); - } finally { - loading = false; - (0, import_ui3.rerender)(); - } - }; - const save = async () => { - const store = stores.get(selectedPath); - if (!store) return; - saving = true; - status = copy.saving; - try { - const result = await saveSelectedFile(store, gateway2); - status = result.status === "saved" ? copy.saved : copy.conflict; - } catch (error) { - status = error instanceof Error ? error.message : String(error); - } finally { - saving = false; - (0, import_ui3.rerender)(); - } - }; - return createComponent(() => { - if (!loading && !manifest && !errorMessage) void initialise(); - if (errorMessage) { - return /* @__PURE__ */ jsxs("div", { class: "config-editor", children: [ - /* @__PURE__ */ jsx("style", { children: EDITOR_STYLES }), - /* @__PURE__ */ jsx("div", { class: "error-shell", role: "alert", children: errorMessage }) - ] }); - } - const store = stores.get(selectedPath); - if (!manifest || !store) { - return /* @__PURE__ */ jsxs("div", { class: "config-editor", children: [ - /* @__PURE__ */ jsx("style", { children: EDITOR_STYLES }), - /* @__PURE__ */ jsx("div", { class: "status-card", "aria-live": "polite", children: copy.loading }) - ] }); - } - const snapshot = store.snapshot(); - return /* @__PURE__ */ jsx( - EditorApp, - { - manifest, - store, - snapshot, - mode, - saving, - status, - onMode: (nextMode) => { - mode = nextMode; - }, - onSelect: selectFile, - onField: (key, value) => { - store.setDisplayValue(key, value); - status = ""; - }, - onRaw: (source) => { - store.replaceWorkingSource(source); - status = ""; - }, - onSave: save - } - ); - }); -}; - -// ../../packages/plattform/dist/index.js -var import_plattform = __toESM(require_plattform(), 1); -var request = rawAsyncToPromise(import_plattform.request); -var loadFileFromDeployment = rawAsyncToPromise( - import_plattform.loadFileFromDeployment -); -var saveFileToDeployment = rawAsyncToPromise(import_plattform.saveFileToDeployment); -var saveFileToDeploymentIfMatch = rawAsyncToPromise( - import_plattform.saveFileToDeploymentIfMatch -); - -// src/app.tsx -var gateway = withMissingFileFallback({ - async load(path) { - return await loadFileFromDeployment(path); - }, - async save(path, content, expectedFingerprint) { - return JSON.parse( - await saveFileToDeploymentIfMatch(path, content, expectedFingerprint) - ); - } -}); -var component = createConfigEditorComponent({ - manifestPath: "private/config-editor.manifest.json", - gateway -}); -export { - component -}; diff --git a/examples/config-editor-plattform/public/app.bundled.js b/examples/config-editor-plattform/public/app.bundled.js deleted file mode 100644 index 6359b48..0000000 --- a/examples/config-editor-plattform/public/app.bundled.js +++ /dev/null @@ -1,2463 +0,0 @@ -// ../../packages/config-editor/src/manifest.ts -var CONFIG_FORMATS = /* @__PURE__ */ new Set([ - "java-properties", - "ini", - "unreal-ini", - "key-value", - "json", - "xml-properties", - "raw" -]); -var FIELD_TYPES = /* @__PURE__ */ new Set([ - "string", - "integer", - "number", - "boolean", - "enum", - "secret" -]); -var DOTTED_VERSION = /^\d+(?:\.\d+)*$/; -var URL_SCHEME = /^[a-z][a-z\d+.-]*:/i; -var WINDOWS_DRIVE = /^[a-z]:\//i; -var fail = (message) => { - throw new TypeError(`Invalid config editor manifest: ${message}`); -}; -var objectValue = (value, name) => { - if (typeof value !== "object" || value === null || Array.isArray(value)) { - return fail(`${name} must be an object.`); - } - return value; -}; -var stringValue = (value, name) => { - if (typeof value !== "string" || value.trim().length === 0) { - return fail(`${name} must be a non-empty string.`); - } - return value; -}; -var optionalString = (value, name) => { - if (value === void 0) return void 0; - return stringValue(value, name); -}; -var optionalBoolean = (value, name) => { - if (value === void 0) return void 0; - if (typeof value !== "boolean") return fail(`${name} must be a boolean.`); - return value; -}; -var optionalNumber = (value, name) => { - if (value === void 0) return void 0; - if (typeof value !== "number" || !Number.isFinite(value)) { - return fail(`${name} must be a finite number.`); - } - return value; -}; -var arrayValue = (value, name) => { - if (!Array.isArray(value)) return fail(`${name} must be an array.`); - return value; -}; -var assertVersion = (value, name) => { - if (!DOTTED_VERSION.test(value)) { - fail(`${name} must be a dotted numeric version.`); - } -}; -var assertSafePath = (path) => { - const segments = path.split("/"); - const unsafe = path.startsWith("/") || path.includes("\\") || path.includes("\0") || URL_SCHEME.test(path) || WINDOWS_DRIVE.test(path) || segments.some((segment) => segment === "" || segment === "." || segment === ".."); - if (unsafe) fail(`file path "${path}" must be a safe relative path.`); -}; -var validateServer = (value) => { - const server = objectValue(value, "server"); - return { - family: stringValue(server["family"], "server.family"), - displayName: stringValue(server["displayName"], "server.displayName"), - ...server["appVersion"] === void 0 ? {} : { appVersion: stringValue(server["appVersion"], "server.appVersion") } - }; -}; -var validateField = (value, location) => { - const field = objectValue(value, location); - const type = stringValue(field["type"], `${location}.type`); - if (!FIELD_TYPES.has(type)) { - fail(`${location}.type "${type}" is unsupported.`); - } - const min = optionalNumber(field["min"], `${location}.min`); - const max = optionalNumber(field["max"], `${location}.max`); - if (min !== void 0 && max !== void 0 && min > max) { - fail(`${location} minimum cannot exceed its maximum.`); - } - if ((min !== void 0 || max !== void 0) && type !== "integer" && type !== "number") { - fail(`${location} numeric ranges require an integer or number field.`); - } - let values; - if (field["values"] !== void 0) { - values = arrayValue(field["values"], `${location}.values`).map( - (entry, index) => stringValue(entry, `${location}.values[${index}]`) - ); - if (new Set(values).size !== values.length) { - fail(`${location}.values contains duplicate enum values.`); - } - } - if (type === "enum" && (!values || values.length === 0)) { - fail(`${location} enum values must contain at least one value.`); - } - if (type !== "enum" && values !== void 0) { - fail(`${location}.values is only valid for enum fields.`); - } - const pattern = optionalString(field["pattern"], `${location}.pattern`); - if (pattern !== void 0) { - try { - new RegExp(pattern); - } catch { - fail(`${location}.pattern must be a valid regular expression.`); - } - } - const since = optionalString(field["since"], `${location}.since`); - const until = optionalString(field["until"], `${location}.until`); - if (since !== void 0) assertVersion(since, `${location}.since`); - if (until !== void 0) assertVersion(until, `${location}.until`); - if (since !== void 0 && until !== void 0 && compareVersions(since, until) > 0) { - fail(`${location}.since cannot be later than .until.`); - } - const rawDefaultValue = field["defaultValue"]; - if (rawDefaultValue !== void 0 && rawDefaultValue !== null && typeof rawDefaultValue !== "string" && typeof rawDefaultValue !== "number" && typeof rawDefaultValue !== "boolean") { - fail(`${location}.defaultValue must be a scalar value.`); - } - const result = { - key: stringValue(field["key"], `${location}.key`), - label: stringValue(field["label"], `${location}.label`), - description: stringValue(field["description"], `${location}.description`), - documentation: stringValue( - field["documentation"], - `${location}.documentation` - ), - type - }; - if (values !== void 0) result.values = values; - if (min !== void 0) result.min = min; - if (max !== void 0) result.max = max; - if (pattern !== void 0) result.pattern = pattern; - if (rawDefaultValue !== void 0) { - result.defaultValue = rawDefaultValue; - } - if (field["sensitive"] !== void 0) { - result.sensitive = optionalBoolean(field["sensitive"], `${location}.sensitive`); - } - if (field["restartRequired"] !== void 0) { - result.restartRequired = optionalBoolean( - field["restartRequired"], - `${location}.restartRequired` - ); - } - if (since !== void 0) result.since = since; - if (until !== void 0) result.until = until; - return result; -}; -var validateSection = (value, location) => { - const section = objectValue(value, location); - const fields = arrayValue(section["fields"], `${location}.fields`).map( - (field, index) => validateField(field, `${location}.fields[${index}]`) - ); - if (fields.length === 0) fail(`${location}.fields must not be empty.`); - const result = { - id: stringValue(section["id"], `${location}.id`), - label: stringValue(section["label"], `${location}.label`), - fields - }; - if (section["description"] !== void 0) { - result.description = stringValue( - section["description"], - `${location}.description` - ); - } - return result; -}; -var validateFile = (value, location) => { - const file = objectValue(value, location); - const path = stringValue(file["path"], `${location}.path`); - assertSafePath(path); - const format = stringValue(file["format"], `${location}.format`); - if (!CONFIG_FORMATS.has(format)) { - fail(`${location}.format "${format}" is unsupported.`); - } - const sections = arrayValue(file["sections"], `${location}.sections`).map( - (section, index) => validateSection(section, `${location}.sections[${index}]`) - ); - const sectionIds = /* @__PURE__ */ new Set(); - const fieldKeys = /* @__PURE__ */ new Set(); - for (const section of sections) { - if (sectionIds.has(section.id)) { - fail(`${location} contains duplicate section id "${section.id}".`); - } - sectionIds.add(section.id); - for (const field of section.fields) { - if (fieldKeys.has(field.key)) { - fail(`${location} contains duplicate field key "${field.key}".`); - } - fieldKeys.add(field.key); - } - } - const result = { - path, - format, - label: stringValue(file["label"], `${location}.label`), - sections - }; - if (file["description"] !== void 0) { - result.description = stringValue( - file["description"], - `${location}.description` - ); - } - if (file["documentation"] !== void 0) { - result.documentation = stringValue( - file["documentation"], - `${location}.documentation` - ); - } - return result; -}; -var validateManifest = (value) => { - const manifest = objectValue(value, "manifest"); - if (manifest["version"] !== 1) fail("version must be 1."); - const files = arrayValue(manifest["files"], "files").map( - (file, index) => validateFile(file, `files[${index}]`) - ); - if (files.length === 0) fail("files must not be empty."); - const paths = /* @__PURE__ */ new Set(); - for (const file of files) { - if (paths.has(file.path)) fail(`files contains duplicate path "${file.path}".`); - paths.add(file.path); - } - return { version: 1, server: validateServer(manifest["server"]), files }; -}; -var compareVersions = (left, right) => { - const leftParts = left.split(".").map(Number); - const rightParts = right.split(".").map(Number); - const length = Math.max(leftParts.length, rightParts.length); - for (let index = 0; index < length; index += 1) { - const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); - if (difference !== 0) return difference < 0 ? -1 : 1; - } - return 0; -}; - -// ../../packages/config-editor/src/adapters/line-document.ts -var splitSourceLines = (source) => { - const lines = []; - let start = 0; - while (start < source.length) { - const newline = source.indexOf("\n", start); - if (newline === -1) { - lines.push({ raw: source.slice(start), ending: "", start, end: source.length }); - break; - } - const hasCarriageReturn = newline > start && source[newline - 1] === "\r"; - const rawEnd = hasCarriageReturn ? newline - 1 : newline; - lines.push({ - raw: source.slice(start, rawEnd), - ending: hasCarriageReturn ? "\r\n" : "\n", - start, - end: rawEnd - }); - start = newline + 1; - } - return lines; -}; -var preferredLineEnding = (lines) => lines.find((line) => line.ending !== "")?.ending || "\n"; -var hasFinalNewline = (source) => source.endsWith("\n"); - -// ../../packages/config-editor/src/adapters/java-properties.ts -var trailingBackslashes = (value) => { - let count = 0; - for (let index = value.length - 1; index >= 0 && value[index] === "\\"; index -= 1) { - count += 1; - } - return count; -}; -var logicalLineAt = (lines, firstLine) => { - let text = ""; - const offsets = []; - let lineIndex = firstLine; - let continued = false; - for (; lineIndex < lines.length; lineIndex += 1) { - const line = lines[lineIndex]; - const trim = continued ? line.raw.match(/^[ \t\f]*/)?.[0].length ?? 0 : 0; - const content = line.raw.slice(trim); - const continues = trailingBackslashes(content) % 2 === 1; - const logicalContent = continues ? content.slice(0, -1) : content; - for (let index = 0; index < logicalContent.length; index += 1) { - text += logicalContent[index]; - offsets.push(line.start + trim + index); - } - continued = continues; - if (!continues) break; - } - const lastLine = Math.min(lineIndex, lines.length - 1); - return { - text, - offsets, - end: lines[lastLine]?.end ?? 0, - firstLine, - lastLine - }; -}; -var isEscaped = (value, index) => { - let slashes = 0; - for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) { - slashes += 1; - } - return slashes % 2 === 1; -}; -var decodeEscapes = (value) => { - let decoded = ""; - for (let index = 0; index < value.length; index += 1) { - const character = value[index]; - if (character !== "\\" || index === value.length - 1) { - decoded += character; - continue; - } - const escaped = value[index += 1]; - if (escaped === "t") decoded += " "; - else if (escaped === "n") decoded += "\n"; - else if (escaped === "r") decoded += "\r"; - else if (escaped === "f") decoded += "\f"; - else if (escaped === "u") { - while (value[index + 1] === "u") index += 1; - const hexadecimal = value.slice(index + 1, index + 5); - if (/^[\da-f]{4}$/i.test(hexadecimal)) { - decoded += String.fromCharCode(Number.parseInt(hexadecimal, 16)); - index += 4; - } else { - decoded += "u"; - } - } else decoded += escaped; - } - return decoded; -}; -var parseEntry = (logical) => { - const { text } = logical; - let start = 0; - while (start < text.length && /[ \t\f]/.test(text[start])) start += 1; - if (start === text.length || text[start] === "#" || text[start] === "!") { - return void 0; - } - let keyEnd = start; - while (keyEnd < text.length) { - const character = text[keyEnd]; - if (!isEscaped(text, keyEnd) && (character === "=" || character === ":" || /[ \t\f]/.test(character))) { - break; - } - keyEnd += 1; - } - let valueStart = keyEnd; - while (valueStart < text.length && /[ \t\f]/.test(text[valueStart])) { - valueStart += 1; - } - if (valueStart < text.length && (text[valueStart] === "=" || text[valueStart] === ":")) { - valueStart += 1; - } - while (valueStart < text.length && /[ \t\f]/.test(text[valueStart])) { - valueStart += 1; - } - return { - key: decodeEscapes(text.slice(start, keyEnd)), - value: decodeEscapes(text.slice(valueStart)), - valueSpan: { - start: logical.offsets[valueStart] ?? logical.end, - end: logical.end - }, - firstLine: logical.firstLine, - lastLine: logical.lastLine - }; -}; -var classifyNodes = (lines, entries) => { - const firstLines = new Map(entries.map((entry) => [entry.firstLine, entry])); - const continuationLines = /* @__PURE__ */ new Set(); - for (const entry of entries) { - for (let index = entry.firstLine + 1; index <= entry.lastLine; index += 1) { - continuationLines.add(index); - } - } - return lines.map((line, index) => { - const entry = firstLines.get(index); - if (entry) { - return { - ...line, - kind: "entry", - key: entry.key, - value: entry.value, - valueSpan: entry.valueSpan - }; - } - if (continuationLines.has(index)) return { ...line, kind: "continuation" }; - const trimmed = line.raw.trimStart(); - if (trimmed === "") return { ...line, kind: "blank" }; - if (trimmed.startsWith("#") || trimmed.startsWith("!")) { - return { ...line, kind: "comment" }; - } - return { ...line, kind: "invalid" }; - }); -}; -var parseJavaProperties = (source) => { - const lines = splitSourceLines(source); - const entries = []; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { - const logical = logicalLineAt(lines, lineIndex); - const entry = parseEntry(logical); - if (entry) entries.push(entry); - lineIndex = logical.lastLine; - } - return { - format: "java-properties", - source, - nodes: classifyNodes(lines, entries), - entries, - issues: [], - lineEnding: preferredLineEnding(lines), - finalNewline: hasFinalNewline(source) - }; -}; -var escapeKey = (value) => value.replace(/[\\ \t\f=:#!]/g, (character) => { - if (character === " ") return "\\t"; - if (character === "\f") return "\\f"; - return `\\${character}`; -}); -var escapeValue = (value) => { - const text = value === null ? "" : String(value); - return text.replace(/[\\\t\n\r\f]/g, (character) => { - if (character === "\\") return "\\\\"; - if (character === " ") return "\\t"; - if (character === "\n") return "\\n"; - if (character === "\r") return "\\r"; - return "\\f"; - }).replace(/^ +/, (spaces) => "\\ ".repeat(spaces.length)); -}; -var effectiveEntry = (document, key) => { - for (let index = document.entries.length - 1; index >= 0; index -= 1) { - const entry = document.entries[index]; - if (entry.key === key) return entry; - } - return void 0; -}; -var appendEntry = (document, key, value) => { - const entry = `${escapeKey(key)}=${escapeValue(value)}`; - if (document.source === "") return entry; - if (document.finalNewline) return `${document.source}${entry}${document.lineEnding}`; - return `${document.source}${document.lineEnding}${entry}`; -}; -var javaPropertiesAdapter = { - parse: parseJavaProperties, - entries(document) { - const entries = /* @__PURE__ */ new Map(); - for (const entry of document.entries) entries.set(entry.key, entry.value); - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveEntry(document, key)?.value; - }, - getAll(document, key) { - return document.entries.filter((entry) => entry.key === key).map((entry) => entry.value); - }, - getAllRaw(document, key) { - return document.entries.filter((entry) => entry.key === key).map((entry) => document.source.slice(entry.valueSpan.start, entry.valueSpan.end)); - }, - set(document, key, value) { - const entry = effectiveEntry(document, key); - if (!entry) return parseJavaProperties(appendEntry(document, key, value)); - const source = document.source.slice(0, entry.valueSpan.start) + escapeValue(value) + document.source.slice(entry.valueSpan.end); - return parseJavaProperties(source); - }, - setAll(document, key, values) { - const entries = document.entries.filter((entry) => entry.key === key); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} values for Java property "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + escapeValue(values[index]) + source.slice(entry.valueSpan.end); - } - return parseJavaProperties(source); - }, - setAllRaw(document, key, values) { - const entries = document.entries.filter((entry) => entry.key === key); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} raw values for Java property "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + values[index] + source.slice(entry.valueSpan.end); - } - return parseJavaProperties(source); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/ini.ts -var inlineCommentAt = (value) => { - let quote; - for (let index = 0; index < value.length; index += 1) { - const character = value[index]; - if (quote) { - if (quote === "'" && value.startsWith("'\\''", index)) { - index += 3; - continue; - } - if (character === quote && value[index - 1] !== "\\") quote = void 0; - continue; - } - if (character === '"' || character === "'") { - quote = character; - continue; - } - const precededBySpace = index > 0 && /\s/.test(value[index - 1]); - if (!precededBySpace) continue; - if (character === ";" || character === "#") return index; - if (character === "/" && value[index + 1] === "/") return index; - } - return value.length; -}; -var decodeQuotedValue = (value, quote) => { - if (quote === '"') return value.replace(/\\(["\\])/g, "$1"); - if (quote === "'") return value.replace(/'\\''/g, "'"); - return value; -}; -var parseEntryLine = (raw, start, section, line, format) => { - let keyStart = 0; - while (keyStart < raw.length && /[ \t]/.test(raw[keyStart])) keyStart += 1; - if (keyStart === raw.length) return void 0; - let keyEnd = keyStart; - while (keyEnd < raw.length && raw[keyEnd] !== "=" && raw[keyEnd] !== ":" && !/[ \t]/.test(raw[keyEnd])) { - keyEnd += 1; - } - if (keyEnd === keyStart) return void 0; - let valueStart = keyEnd; - while (valueStart < raw.length && /[ \t]/.test(raw[valueStart])) valueStart += 1; - if (raw[valueStart] === "=" || raw[valueStart] === ":") valueStart += 1; - while (valueStart < raw.length && /[ \t]/.test(raw[valueStart])) valueStart += 1; - const comment = inlineCommentAt(raw.slice(valueStart)); - let valueEnd = valueStart + comment; - while (valueEnd > valueStart && /[ \t]/.test(raw[valueEnd - 1])) valueEnd -= 1; - if (format === "key-value" && raw[valueEnd - 1] === ";") { - valueEnd -= 1; - while (valueEnd > valueStart && /[ \t]/.test(raw[valueEnd - 1])) valueEnd -= 1; - } - const quote = raw[valueStart]; - let valueQuote; - if ((quote === '"' || quote === "'") && valueEnd > valueStart && raw[valueEnd - 1] === quote) { - valueQuote = quote; - valueStart += 1; - valueEnd -= 1; - } - const localKey = raw.slice(keyStart, keyEnd).trim(); - return { - key: section ? `${section}.${localKey}` : localKey, - value: decodeQuotedValue(raw.slice(valueStart, valueEnd), valueQuote), - valueSpan: { start: start + valueStart, end: start + valueEnd }, - line, - ...valueQuote ? { quote: valueQuote } : {} - }; -}; -var tupleEntries = (entry) => { - if (!entry.value.startsWith("(") || !entry.value.endsWith(")")) return []; - const inner = entry.value.slice(1, -1); - const segments = []; - let segmentStart = 0; - let depth = 0; - let quote; - for (let index = 0; index <= inner.length; index += 1) { - const character = inner[index]; - if (quote) { - if (character === quote && inner[index - 1] !== "\\") quote = void 0; - continue; - } - if (character === '"' || character === "'") { - quote = character; - continue; - } - if (character === "(" || character === "[" || character === "{") depth += 1; - else if (character === ")" || character === "]" || character === "}") depth -= 1; - if (character === "," && depth === 0 || index === inner.length) { - segments.push({ start: segmentStart, end: index }); - segmentStart = index + 1; - } - } - const result = []; - for (const segment of segments) { - const raw = inner.slice(segment.start, segment.end); - let equals = -1; - depth = 0; - quote = void 0; - for (let index = 0; index < raw.length; index += 1) { - const character = raw[index]; - if (quote) { - if (character === quote && raw[index - 1] !== "\\") quote = void 0; - continue; - } - if (character === '"' || character === "'") quote = character; - else if (character === "(" || character === "[" || character === "{") depth += 1; - else if (character === ")" || character === "]" || character === "}") depth -= 1; - else if (character === "=" && depth === 0) { - equals = index; - break; - } - } - if (equals < 1) continue; - const key = raw.slice(0, equals).trim(); - let localStart = equals + 1; - while (localStart < raw.length && /\s/.test(raw[localStart])) localStart += 1; - let localEnd = raw.length; - while (localEnd > localStart && /\s/.test(raw[localEnd - 1])) localEnd -= 1; - const valueQuote = raw[localStart]; - let preservedQuote; - if ((valueQuote === '"' || valueQuote === "'") && localEnd > localStart && raw[localEnd - 1] === valueQuote) { - preservedQuote = valueQuote; - localStart += 1; - localEnd -= 1; - } - const absoluteStart = entry.valueSpan.start + 1 + segment.start + localStart; - result.push({ - key: `${entry.key}.${key}`, - value: decodeQuotedValue(raw.slice(localStart, localEnd), preservedQuote), - valueSpan: { - start: absoluteStart, - end: absoluteStart + localEnd - localStart - }, - line: entry.line, - ...preservedQuote ? { quote: preservedQuote } : {} - }); - } - return result; -}; -var parseIni = (source, format) => { - const lines = splitSourceLines(source); - const entries = []; - const sections = []; - const nodes = []; - let section; - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - const trimmed = line.raw.trimStart(); - if (trimmed === "") { - nodes.push({ ...line, kind: "blank" }); - continue; - } - if (trimmed.startsWith(";") || trimmed.startsWith("#") || trimmed.startsWith("//")) { - nodes.push({ ...line, kind: "comment" }); - continue; - } - const sectionMatch = format === "key-value" ? null : trimmed.match(/^\[([^\]]+)](?:\s*(?:[;#].*)?)$/); - if (sectionMatch) { - section = sectionMatch[1].trim(); - sections.push({ name: section, line: index }); - nodes.push({ ...line, kind: "section" }); - continue; - } - const entry = parseEntryLine(line.raw, line.start, section, index, format); - if (entry) { - entries.push(entry); - if (format === "unreal-ini") { - const children = tupleEntries(entry); - if (children.length > 0) { - entry.container = true; - entries.push(...children); - } - } - nodes.push({ - ...line, - kind: "entry", - key: entry.key, - value: entry.value, - valueSpan: entry.valueSpan - }); - } else nodes.push({ ...line, kind: "invalid" }); - } - return { - format, - source, - nodes, - entries, - sections, - issues: [], - lineEnding: preferredLineEnding(lines), - finalNewline: hasFinalNewline(source) - }; -}; -var effectiveEntry2 = (document, key) => { - for (let index = document.entries.length - 1; index >= 0; index -= 1) { - const entry = document.entries[index]; - if (entry.key === key) return entry; - } - return void 0; -}; -var formatValue = (value, quote) => { - let formatted = (value === null ? "" : String(value)).replace( - /[\r\n]/g, - (character) => character === "\r" ? "\\r" : "\\n" - ); - if (quote === '"') formatted = formatted.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); - else if (quote === "'") formatted = formatted.replace(/'/g, "'\\''"); - return formatted; -}; -var appendIniEntry = (document, key, value) => { - const split = document.format === "key-value" ? -1 : key.lastIndexOf("."); - const section = split < 0 ? void 0 : key.slice(0, split); - const localKey = split < 0 ? key : key.slice(split + 1); - const assignment = `${localKey}=${formatValue(value)}`; - const prefix = document.source === "" || document.finalNewline ? "" : document.lineEnding; - const suffix = document.finalNewline ? document.lineEnding : ""; - if (!section || document.sections.some((candidate) => candidate.name === section)) { - return `${document.source}${prefix}${assignment}${suffix}`; - } - const sectionHeader = `[${section}]${document.lineEnding}`; - return `${document.source}${prefix}${sectionHeader}${assignment}${suffix}`; -}; -var createIniAdapter = (format) => ({ - parse(source) { - return parseIni(source, format); - }, - entries(document) { - const entries = /* @__PURE__ */ new Map(); - for (const entry of document.entries) { - if (!entry.container) entries.set(entry.key, entry.value); - } - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveEntry2(document, key)?.value; - }, - getAll(document, key) { - return document.entries.filter((entry) => !entry.container && entry.key === key).map((entry) => entry.value); - }, - getAllRaw(document, key) { - return document.entries.filter((entry) => !entry.container && entry.key === key).map((entry) => document.source.slice(entry.valueSpan.start, entry.valueSpan.end)); - }, - set(document, key, value) { - const entry = effectiveEntry2(document, key); - if (!entry) return parseIni(appendIniEntry(document, key, value), format); - return parseIni( - document.source.slice(0, entry.valueSpan.start) + formatValue(value, entry.quote) + document.source.slice(entry.valueSpan.end), - format - ); - }, - setAll(document, key, values) { - const entries = document.entries.filter( - (entry) => !entry.container && entry.key === key - ); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} values for setting "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + formatValue(values[index], entry.quote) + source.slice(entry.valueSpan.end); - } - return parseIni(source, format); - }, - setAllRaw(document, key, values) { - const entries = document.entries.filter( - (entry) => !entry.container && entry.key === key - ); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} raw values for setting "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; - source = source.slice(0, entry.valueSpan.start) + values[index] + source.slice(entry.valueSpan.end); - } - return parseIni(source, format); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}); -var iniAdapter = createIniAdapter("ini"); -var unrealIniAdapter = createIniAdapter("unreal-ini"); -var keyValueAdapter = createIniAdapter("key-value"); - -// ../../packages/config-editor/src/adapters/json.ts -var JsonParseError = class extends Error { - position; - constructor(position, message) { - super(message); - this.position = position; - } -}; -var pointerSegment = (value) => value.replace(/~/g, "~0").replace(/\//g, "~1"); -var locationAt = (source, position) => { - const before = source.slice(0, position); - const lines = before.split("\n"); - return { line: lines.length, column: lines.at(-1).length + 1 }; -}; -var jsonNodes = (source) => splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid" -})); -var parseJson = (source) => { - const scalars = /* @__PURE__ */ new Map(); - const scalarOccurrences = /* @__PURE__ */ new Map(); - const containers = /* @__PURE__ */ new Map(); - let cursor = 0; - const addScalar = (pointer, scalar) => { - scalars.set(pointer, scalar); - const occurrences = scalarOccurrences.get(pointer) ?? []; - occurrences.push(scalar); - scalarOccurrences.set(pointer, occurrences); - }; - const fail2 = (message) => { - throw new JsonParseError(cursor, message); - }; - const skipWhitespace = () => { - while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; - }; - const consume = (character) => { - if (source[cursor] !== character) fail2(`Expected "${character}".`); - cursor += 1; - }; - const parseString = () => { - const start = cursor; - consume('"'); - while (cursor < source.length) { - const character = source[cursor]; - if (character === '"') { - cursor += 1; - const token = source.slice(start, cursor); - try { - return { value: JSON.parse(token), span: { start, end: cursor } }; - } catch { - fail2("Invalid JSON string."); - } - } - if (character === "\\") { - cursor += 2; - continue; - } - if (character.charCodeAt(0) < 32) fail2("Unescaped control character."); - cursor += 1; - } - return fail2("Unterminated JSON string."); - }; - const parseValue = (pointer) => { - skipWhitespace(); - const start = cursor; - const character = source[cursor]; - if (character === '"') { - const string = parseString(); - addScalar(pointer, { value: string.value, span: string.span }); - return; - } - if (character === "{") { - const open = cursor; - cursor += 1; - skipWhitespace(); - if (source[cursor] === "}") { - containers.set(pointer, { kind: "object", open, close: cursor }); - cursor += 1; - return; - } - while (cursor < source.length) { - skipWhitespace(); - if (source[cursor] !== '"') fail2("Expected an object key."); - const key = parseString().value; - skipWhitespace(); - consume(":"); - parseValue(`${pointer}/${pointerSegment(key)}`); - skipWhitespace(); - if (source[cursor] === "}") { - containers.set(pointer, { kind: "object", open, close: cursor }); - cursor += 1; - return; - } - consume(","); - } - fail2("Unterminated JSON object."); - } - if (character === "[") { - const open = cursor; - cursor += 1; - skipWhitespace(); - if (source[cursor] === "]") { - containers.set(pointer, { kind: "array", open, close: cursor }); - cursor += 1; - return; - } - let index = 0; - while (cursor < source.length) { - parseValue(`${pointer}/${index}`); - index += 1; - skipWhitespace(); - if (source[cursor] === "]") { - containers.set(pointer, { kind: "array", open, close: cursor }); - cursor += 1; - return; - } - consume(","); - } - fail2("Unterminated JSON array."); - } - const remainder = source.slice(cursor); - const literal = remainder.match(/^(?:true|false|null)(?![\w])/); - if (literal) { - cursor += literal[0].length; - addScalar(pointer, { - value: JSON.parse(literal[0]), - span: { start, end: cursor } - }); - return; - } - const number = remainder.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/); - if (number) { - cursor += number[0].length; - const unsafeInteger = /^-?\d+$/.test(number[0]) && !Number.isSafeInteger(Number(number[0])); - addScalar(pointer, { - value: unsafeInteger ? number[0] : Number(number[0]), - span: { start, end: cursor }, - ...unsafeInteger ? { unsafeInteger: true } : {} - }); - return; - } - fail2("Expected a JSON scalar, object, or array."); - }; - try { - parseValue(""); - skipWhitespace(); - if (cursor !== source.length) fail2("Unexpected content after the JSON value."); - return { - format: "json", - source, - nodes: jsonNodes(source), - issues: [], - scalars, - scalarOccurrences, - containers - }; - } catch (error) { - if (!(error instanceof JsonParseError)) throw error; - const location = locationAt(source, error.position); - return { - format: "json", - source, - nodes: jsonNodes(source), - issues: [ - { - code: "invalid-json", - message: error.message, - severity: "error", - ...location - } - ], - scalars: /* @__PURE__ */ new Map(), - scalarOccurrences: /* @__PURE__ */ new Map(), - containers: /* @__PURE__ */ new Map() - }; - } -}; -var decodePointerSegment = (value) => value.replace(/~1/g, "/").replace(/~0/g, "~"); -var insertJsonProperty = (document, pointer, value) => { - const separator = pointer.lastIndexOf("/"); - if (separator < 0) throw new Error(`JSON pointer "${pointer}" does not exist.`); - const parentPointer = pointer.slice(0, separator); - const property = decodePointerSegment(pointer.slice(separator + 1)); - const parent = document.containers.get(parentPointer); - if (!parent || parent.kind !== "object") { - throw new Error(`JSON pointer "${pointer}" does not exist.`); - } - const content = document.source.slice(parent.open + 1, parent.close); - const contentEnd = parent.close - (content.match(/\s*$/)?.[0].length ?? 0); - const closingLine = document.source.slice(0, parent.close).split("\n").at(-1) ?? ""; - const closingIndent = closingLine.match(/^\s*/)?.[0] ?? ""; - const serialized = `${JSON.stringify(property)}: ${JSON.stringify(value)}`; - const newline = document.source.includes("\r\n") ? "\r\n" : "\n"; - let insertion; - if (content.trim() === "") { - insertion = `${newline}${closingIndent} ${serialized}${newline}${closingIndent}`; - return parseJson( - document.source.slice(0, parent.open + 1) + insertion + document.source.slice(parent.close) - ); - } - const multiline = content.includes("\n"); - if (!multiline) insertion = `, ${serialized}`; - else { - const existingLine = document.source.slice(0, contentEnd).split("\n").at(-1) ?? ""; - const childIndent = existingLine.match(/^\s*/)?.[0] ?? `${closingIndent} `; - insertion = `,${newline}${childIndent}${serialized}`; - } - return parseJson( - document.source.slice(0, contentEnd) + insertion + document.source.slice(contentEnd) - ); -}; -var jsonAdapter = { - parse: parseJson, - entries(document) { - return [...document.scalars].map(([key, scalar]) => ({ - key, - value: scalar.value - })); - }, - get(document, key) { - return document.scalars.get(key)?.value; - }, - getAll(document, key) { - return (document.scalarOccurrences.get(key) ?? []).map((scalar) => scalar.value); - }, - getAllRaw(document, key) { - return (document.scalarOccurrences.get(key) ?? []).map( - (scalar) => document.source.slice(scalar.span.start, scalar.span.end) - ); - }, - set(document, key, value) { - if (document.issues.length > 0) throw new Error("Invalid JSON cannot be edited in form mode."); - const scalar = document.scalars.get(key); - if (!scalar) { - if (document.containers.has(key)) throw new Error(`JSON pointer "${key}" is not a scalar.`); - return insertJsonProperty(document, key, value); - } - const serialized = scalar.unsafeInteger && typeof value === "string" && /^-?\d+$/.test(value) ? value : JSON.stringify(value); - return parseJson( - document.source.slice(0, scalar.span.start) + serialized + document.source.slice(scalar.span.end) - ); - }, - setAll(document, key, values) { - const occurrences = document.scalarOccurrences.get(key) ?? []; - const expected = occurrences.length; - if (values.length !== expected) { - throw new Error(`Expected ${expected} values for JSON pointer "${key}".`); - } - let source = document.source; - for (let index = occurrences.length - 1; index >= 0; index -= 1) { - const scalar = occurrences[index]; - const value = values[index]; - const serialized = scalar.unsafeInteger && typeof value === "string" && /^-?\d+$/.test(value) ? value : JSON.stringify(value); - source = source.slice(0, scalar.span.start) + serialized + source.slice(scalar.span.end); - } - return parseJson(source); - }, - setAllRaw(document, key, values) { - const occurrences = document.scalarOccurrences.get(key) ?? []; - const expected = occurrences.length; - if (values.length !== expected) { - throw new Error(`Expected ${expected} raw values for JSON pointer "${key}".`); - } - let source = document.source; - for (let index = occurrences.length - 1; index >= 0; index -= 1) { - const scalar = occurrences[index]; - source = source.slice(0, scalar.span.start) + values[index] + source.slice(scalar.span.end); - } - return parseJson(source); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/raw.ts -var parseRaw = (source) => { - const nodes = splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid" - })); - return { format: "raw", source, nodes, issues: [] }; -}; -var rawAdapter = { - parse: parseRaw, - entries() { - return []; - }, - get() { - return void 0; - }, - getAll() { - return []; - }, - getAllRaw() { - return []; - }, - set(_document, _key, _value) { - throw new Error("Raw mode does not support typed field writes."); - }, - setAll(document, _key, values) { - if (values.length > 0) throw new Error("Raw mode does not support typed field writes."); - return document; - }, - setAllRaw(document, _key, values) { - if (values.length > 0) throw new Error("Raw mode does not support typed field writes."); - return document; - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/xml-properties.ts -var decodeXml = (value) => value.replace(/&(?:#x[\da-f]+|#\d+|amp|quot|apos|lt|gt);/gi, (entity) => { - const token = entity.slice(1, -1); - if (token.toLowerCase().startsWith("#x")) { - return String.fromCodePoint(Number.parseInt(token.slice(2), 16)); - } - if (token.startsWith("#")) { - return String.fromCodePoint(Number.parseInt(token.slice(1), 10)); - } - return { amp: "&", quot: '"', apos: "'", lt: "<", gt: ">" }[token.toLowerCase()]; -}); -var encodeXml = (value, quote) => { - let encoded = (value === null ? "" : String(value)).replace(/&/g, "&").replace(//g, ">"); - encoded = quote === '"' ? encoded.replace(/"/g, """) : encoded.replace(/'/g, "'"); - return encoded; -}; -var parseXmlProperties = (source) => { - const properties = []; - const propertyPattern = /]*>/gi; - for (const match of source.matchAll(propertyPattern)) { - const tag = match[0]; - const tagStart = match.index; - const attributes = /* @__PURE__ */ new Map(); - const attributePattern = /\b(name|value)\s*=\s*(["'])(.*?)\2/gi; - for (const attribute of tag.matchAll(attributePattern)) { - const quote = attribute[2]; - const rawValue = attribute[3]; - const rawStart = tagStart + attribute.index + attribute[0].indexOf(quote) + 1; - attributes.set(attribute[1].toLowerCase(), { - value: decodeXml(rawValue), - span: { start: rawStart, end: rawStart + rawValue.length }, - quote - }); - } - const name = attributes.get("name"); - const value = attributes.get("value"); - if (name && value) { - properties.push({ - key: name.value, - value: value.value, - valueSpan: value.span, - quote: value.quote - }); - } - } - const nodes = splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid" - })); - return { format: "xml-properties", source, nodes, issues: [], properties }; -}; -var effectiveProperty = (document, key) => { - for (let index = document.properties.length - 1; index >= 0; index -= 1) { - const property = document.properties[index]; - if (property.key === key) return property; - } - return void 0; -}; -var xmlPropertiesAdapter = { - parse: parseXmlProperties, - entries(document) { - const entries = /* @__PURE__ */ new Map(); - for (const property of document.properties) entries.set(property.key, property.value); - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveProperty(document, key)?.value; - }, - getAll(document, key) { - return document.properties.filter((property) => property.key === key).map((property) => property.value); - }, - getAllRaw(document, key) { - return document.properties.filter((property) => property.key === key).map((property) => document.source.slice(property.valueSpan.start, property.valueSpan.end)); - }, - set(document, key, value) { - const property = effectiveProperty(document, key); - if (!property) throw new Error(`XML property "${key}" does not exist.`); - return parseXmlProperties( - document.source.slice(0, property.valueSpan.start) + encodeXml(value, property.quote) + document.source.slice(property.valueSpan.end) - ); - }, - setAll(document, key, values) { - const properties = document.properties.filter((property) => property.key === key); - if (properties.length !== values.length) { - throw new Error(`Expected ${properties.length} values for XML property "${key}".`); - } - let source = document.source; - for (let index = properties.length - 1; index >= 0; index -= 1) { - const property = properties[index]; - source = source.slice(0, property.valueSpan.start) + encodeXml(values[index], property.quote) + source.slice(property.valueSpan.end); - } - return parseXmlProperties(source); - }, - setAllRaw(document, key, values) { - const properties = document.properties.filter((property) => property.key === key); - if (properties.length !== values.length) { - throw new Error(`Expected ${properties.length} raw values for XML property "${key}".`); - } - let source = document.source; - for (let index = properties.length - 1; index >= 0; index -= 1) { - const property = properties[index]; - source = source.slice(0, property.valueSpan.start) + values[index] + source.slice(property.valueSpan.end); - } - return parseXmlProperties(source); - }, - validate(document, schema) { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - } -}; - -// ../../packages/config-editor/src/adapters/registry.ts -var createAdapterRegistry = () => /* @__PURE__ */ new Map([ - ["java-properties", javaPropertiesAdapter], - ["ini", iniAdapter], - ["unreal-ini", unrealIniAdapter], - ["key-value", keyValueAdapter], - ["json", jsonAdapter], - ["xml-properties", xmlPropertiesAdapter], - ["raw", rawAdapter] -]); - -// ../../packages/config-editor/src/validation.ts -var issue = (field, code, message) => ({ - code, - message, - severity: "error", - fieldKey: field.key -}); -var scalarText = (input) => { - if (input === null) return ""; - if (typeof input === "string") return input; - if (typeof input === "number" || typeof input === "boolean") return String(input); - throw new TypeError("Value must be a scalar."); -}; -var coerceFieldValue = (field, input) => { - if (field.type === "string" || field.type === "secret" || field.type === "enum") { - return scalarText(input); - } - if (field.type === "boolean") { - if (input === true || input === false) return input; - if (input === "true") return true; - if (input === "false") return false; - throw new TypeError("Value must be true or false."); - } - if (typeof input === "number") { - if (!Number.isFinite(input)) throw new TypeError("Value must be a finite number."); - if (field.type === "integer" && !Number.isSafeInteger(input)) { - throw new TypeError("Value must be an integer."); - } - return input; - } - const text = scalarText(input).trim(); - if (field.type === "integer") { - if (!/^[+-]?\d+$/.test(text)) throw new TypeError("Value must be an integer."); - const value2 = Number(text); - if (!Number.isSafeInteger(value2)) throw new TypeError("Value must be an integer."); - return value2; - } - if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) { - throw new TypeError("Value must be a finite number."); - } - const value = Number(text); - if (!Number.isFinite(value)) throw new TypeError("Value must be a finite number."); - return value; -}; -var validateField2 = (field, value) => { - const issues = []; - if (field.type === "integer" && (typeof value !== "number" || !Number.isSafeInteger(value))) { - issues.push(issue(field, "type", "Value must be an integer.")); - return issues; - } - if (field.type === "number" && (typeof value !== "number" || !Number.isFinite(value))) { - issues.push(issue(field, "type", "Value must be a finite number.")); - return issues; - } - if (field.type === "boolean" && typeof value !== "boolean") { - issues.push(issue(field, "type", "Value must be true or false.")); - return issues; - } - if ((field.type === "string" || field.type === "secret" || field.type === "enum") && typeof value !== "string") { - issues.push(issue(field, "type", "Value must be text.")); - return issues; - } - if (typeof value === "number") { - const below = field.min !== void 0 && value < field.min; - const above = field.max !== void 0 && value > field.max; - if (below || above) { - let message; - if (field.min !== void 0 && field.max !== void 0) { - message = `Value must be between ${field.min} and ${field.max} (inclusive).`; - } else if (field.min !== void 0) message = `Value must be at least ${field.min}.`; - else message = `Value must be at most ${field.max}.`; - issues.push(issue(field, "range", message)); - } - } - if (field.type === "enum" && typeof value === "string" && !field.values?.includes(value)) { - issues.push( - issue(field, "enum", `Value must be one of: ${(field.values ?? []).join(", ")}.`) - ); - } - if (field.pattern !== void 0 && typeof value === "string") { - const pattern = new RegExp(`^(?:${field.pattern})$`); - if (!pattern.test(value)) { - issues.push(issue(field, "pattern", "Value does not match the required format.")); - } - } - return issues; -}; - -// ../../packages/config-editor/src/store.ts -var MASKED_SECRET = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"; -var allFields = (schema) => schema.sections.flatMap((section) => section.fields); -var sensitiveKey = (key) => { - const normalized = key.replace(/([a-z\d])([A-Z])/g, "$1.$2").toLowerCase(); - const segments = normalized.split(/[^a-z\d]+/).filter(Boolean); - const sensitiveSegments = /* @__PURE__ */ new Set([ - "password", - "passwd", - "pass", - "secret", - "token", - "apikey", - "privatekey", - "gslt", - "steampass" - ]); - return segments.some((segment) => sensitiveSegments.has(segment)) || /(?:password|passwd|steampass|api[-_.]?key|private[-_.]?key|gslt)/i.test(key); -}; -var inferredType = (value) => { - if (typeof value === "boolean") return "boolean"; - if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number"; - if (typeof value !== "string") return "string"; - if (/^(?:true|false)$/i.test(value)) return "boolean"; - return "string"; -}; -var humanizeKey = (key) => { - const leaf = key.split(/[./]/).at(-1) ?? key; - const label = leaf.replace(/[-_]+/g, " ").replace(/([a-z\d])([A-Z])/g, "$1 $2"); - return label.charAt(0).toUpperCase() + label.slice(1); -}; -var schemaWithDetectedFields = (declaredSchema, entries) => { - const declaredKeys = new Set(allFields(declaredSchema).map((field) => field.key)); - const detectedFields = []; - for (const entry of entries) { - if (entry.key === "" || declaredKeys.has(entry.key)) continue; - const sensitive = sensitiveKey(entry.key); - detectedFields.push({ - key: entry.key, - label: humanizeKey(entry.key), - description: `Setting discovered in ${declaredSchema.label}.`, - documentation: declaredSchema.documentation ?? `Configuration file ${declaredSchema.path}`, - type: sensitive ? "secret" : inferredType(entry.value), - sensitive, - restartRequired: true - }); - } - if (detectedFields.length === 0) return declaredSchema; - const detectedSection = { - id: "detected-settings", - label: "Detected settings", - description: "Settings discovered from the loaded configuration file.", - fields: detectedFields - }; - return { - ...declaredSchema, - sections: [...declaredSchema.sections, detectedSection] - }; -}; -var valuesEqual = (left, right) => Object.is(left, right); -var displayValue = (field, value) => { - if ((field.sensitive || field.type === "secret") && value !== void 0 && value !== "") { - return MASKED_SECRET; - } - return value === null || value === void 0 ? "" : String(value); -}; -var deepFreezeSnapshot = (snapshot) => { - for (const field of Object.values(snapshot.fields)) { - for (const fieldIssue of field.issues) Object.freeze(fieldIssue); - Object.freeze(field.issues); - Object.freeze(field); - } - for (const change of snapshot.changes) Object.freeze(change); - for (const editorIssue of snapshot.issues) Object.freeze(editorIssue); - Object.freeze(snapshot.fields); - Object.freeze(snapshot.changes); - Object.freeze(snapshot.issues); - return Object.freeze(snapshot); -}; -var ConfigEditorStore = class _ConfigEditorStore { - declaredSchema; - schemaValue; - adapter; - originalDocument; - workingDocument; - fields; - constructor(schema, document, adapter) { - this.declaredSchema = schema; - this.adapter = adapter; - this.schemaValue = schemaWithDetectedFields(schema, adapter.entries(document)); - this.originalDocument = document; - this.workingDocument = document; - this.fields = this.readFields(document, document); - } - get schema() { - return this.schemaValue; - } - static fromLoadedFile(schema, document) { - if (schema.format !== document.format) { - throw new TypeError( - `Schema format "${schema.format}" does not match document format "${document.format}".` - ); - } - const adapter = createAdapterRegistry().get(schema.format); - if (!adapter) throw new TypeError(`No adapter registered for "${schema.format}".`); - return new _ConfigEditorStore(schema, document, adapter); - } - readValue(document, field) { - const raw = this.adapter.get(document, field.key); - if (raw === void 0) return { value: void 0, issues: [] }; - try { - const value = coerceFieldValue(field, raw); - return { value, issues: validateField2(field, value) }; - } catch (error) { - return { - value: void 0, - issues: [ - { - code: "type", - message: error instanceof Error ? error.message : "Invalid value.", - severity: "error", - fieldKey: field.key, - filePath: this.schema.path - } - ] - }; - } - } - readFields(original, working) { - return new Map( - allFields(this.schema).map((field) => { - const originalResult = this.readValue(original, field); - const workingResult = this.readValue(working, field); - return [ - field.key, - { - schema: field, - original: originalResult.value, - current: workingResult.value, - displayValue: displayValue(field, workingResult.value), - issues: workingResult.issues - } - ]; - }) - ); - } - setDisplayValue(key, input) { - const state = this.fields.get(key); - if (!state) throw new TypeError(`Unknown configuration field "${key}".`); - const sensitive = state.schema.sensitive || state.schema.type === "secret"; - if (sensitive && input === MASKED_SECRET) { - state.displayValue = MASKED_SECRET; - state.issues = []; - return; - } - state.displayValue = input; - let value; - try { - value = coerceFieldValue(state.schema, input); - } catch (error) { - state.issues = [ - { - code: "type", - message: error instanceof Error ? error.message : "Invalid value.", - severity: "error", - fieldKey: key, - filePath: this.schema.path - } - ]; - return; - } - const issues = validateField2(state.schema, value).map((fieldIssue) => ({ - ...fieldIssue, - filePath: this.schema.path - })); - state.issues = issues; - if (issues.length > 0) return; - this.workingDocument = this.adapter.set(this.workingDocument, key, value); - state.current = value; - if (sensitive) state.displayValue = MASKED_SECRET; - } - serializeSelectedFile() { - return this.adapter.serialize(this.workingDocument); - } - serializeForDisplay() { - let displayDocument = this.workingDocument; - for (const field of allFields(this.schema)) { - if (!(field.sensitive || field.type === "secret")) continue; - const values = this.adapter.getAll(displayDocument, field.key); - if (values.length === 0) continue; - displayDocument = this.adapter.setAll( - displayDocument, - field.key, - values.map(() => MASKED_SECRET) - ); - } - return this.adapter.serialize(displayDocument); - } - validateSerializedSource(source) { - const document = this.adapter.parse(source); - const issues = this.adapter.validate(document, this.schema); - for (const field of allFields(this.schema)) { - issues.push(...this.readValue(document, field).issues); - } - return issues; - } - loadedSource() { - return this.adapter.serialize(this.originalDocument); - } - replaceWorkingSource(source) { - let document = this.adapter.parse(source); - for (const field of allFields(this.schema)) { - if (!(field.sensitive || field.type === "secret")) continue; - const displayedValues = this.adapter.getAll(document, field.key); - if (!displayedValues.includes(MASKED_SECRET)) continue; - const currentSecrets = this.adapter.getAllRaw(this.workingDocument, field.key); - if (displayedValues.length !== currentSecrets.length) { - throw new Error( - `Secret occurrences for "${field.key}" cannot be added or removed in raw mode.` - ); - } - document = this.adapter.setAllRaw( - document, - field.key, - displayedValues.map( - (value, index) => value === MASKED_SECRET ? currentSecrets[index] : this.adapter.getAllRaw(document, field.key)[index] - ) - ); - } - this.workingDocument = document; - this.schemaValue = schemaWithDetectedFields( - this.declaredSchema, - this.adapter.entries(document) - ); - this.fields = this.readFields(this.originalDocument, this.workingDocument); - } - acceptSavedSource(source) { - const document = this.adapter.parse(source); - this.originalDocument = document; - this.workingDocument = document; - this.schemaValue = schemaWithDetectedFields( - this.declaredSchema, - this.adapter.entries(document) - ); - this.fields = this.readFields(document, document); - } - snapshot() { - const fields = {}; - const changes = []; - const issues = [...this.workingDocument.issues]; - const documentDirty = this.adapter.serialize(this.workingDocument) !== this.adapter.serialize(this.originalDocument); - for (const [key, state] of this.fields) { - const sensitive = Boolean(state.schema.sensitive || state.schema.type === "secret"); - const dirty = !valuesEqual(state.current, state.original); - const fieldIssues = state.issues.map((fieldIssue) => ({ ...fieldIssue })); - const fieldSnapshot = { - key, - label: state.schema.label, - type: state.schema.type, - displayValue: state.displayValue, - sensitive, - dirty, - issues: fieldIssues, - ...!sensitive && state.current !== void 0 ? { value: state.current } : {} - }; - fields[key] = fieldSnapshot; - issues.push(...fieldIssues); - if (dirty) { - changes.push({ - key, - label: state.schema.label, - before: sensitive ? null : state.original, - after: sensitive ? null : state.current, - sensitive, - restartRequired: Boolean(state.schema.restartRequired) - }); - } - } - const unstructuredChanges = documentDirty && changes.length === 0; - return deepFreezeSnapshot({ - filePath: this.schema.path, - fields, - changes, - issues, - dirty: documentDirty, - unstructuredChanges, - restartRequired: unstructuredChanges || changes.some((change) => change.restartRequired) - }); - } -}; - -// ../../packages/config-editor/src/fingerprint.ts -var SHA256_CONSTANTS = [ - 1116352408, - 1899447441, - 3049323471, - 3921009573, - 961987163, - 1508970993, - 2453635748, - 2870763221, - 3624381080, - 310598401, - 607225278, - 1426881987, - 1925078388, - 2162078206, - 2614888103, - 3248222580, - 3835390401, - 4022224774, - 264347078, - 604807628, - 770255983, - 1249150122, - 1555081692, - 1996064986, - 2554220882, - 2821834349, - 2952996808, - 3210313671, - 3336571891, - 3584528711, - 113926993, - 338241895, - 666307205, - 773529912, - 1294757372, - 1396182291, - 1695183700, - 1986661051, - 2177026350, - 2456956037, - 2730485921, - 2820302411, - 3259730800, - 3345764771, - 3516065817, - 3600352804, - 4094571909, - 275423344, - 430227734, - 506948616, - 659060556, - 883997877, - 958139571, - 1322822218, - 1537002063, - 1747873779, - 1955562222, - 2024104815, - 2227730452, - 2361852424, - 2428436474, - 2756734187, - 3204031479, - 3329325298 -]; -var rotateRight = (value, bits) => value >>> bits | value << 32 - bits; -var utf8Bytes = (source) => { - const bytes = []; - for (const character of source) { - const point = character.codePointAt(0); - if (point <= 127) bytes.push(point); - else if (point <= 2047) { - bytes.push(192 | point >>> 6, 128 | point & 63); - } else if (point <= 65535) { - bytes.push( - 224 | point >>> 12, - 128 | point >>> 6 & 63, - 128 | point & 63 - ); - } else { - bytes.push( - 240 | point >>> 18, - 128 | point >>> 12 & 63, - 128 | point >>> 6 & 63, - 128 | point & 63 - ); - } - } - return bytes; -}; -var sha256 = (source) => { - const bytes = utf8Bytes(source); - const bitLength = bytes.length * 8; - bytes.push(128); - while (bytes.length % 64 !== 56) bytes.push(0); - const high = Math.floor(bitLength / 4294967296); - const low = bitLength >>> 0; - for (let shift = 24; shift >= 0; shift -= 8) bytes.push(high >>> shift & 255); - for (let shift = 24; shift >= 0; shift -= 8) bytes.push(low >>> shift & 255); - const hash = [ - 1779033703, - 3144134277, - 1013904242, - 2773480762, - 1359893119, - 2600822924, - 528734635, - 1541459225 - ]; - const words = new Array(64).fill(0); - for (let offset = 0; offset < bytes.length; offset += 64) { - for (let index = 0; index < 16; index += 1) { - const cursor = offset + index * 4; - words[index] = (bytes[cursor] << 24 | bytes[cursor + 1] << 16 | bytes[cursor + 2] << 8 | bytes[cursor + 3]) >>> 0; - } - for (let index = 16; index < 64; index += 1) { - const previous15 = words[index - 15]; - const previous2 = words[index - 2]; - const sigma0 = rotateRight(previous15, 7) ^ rotateRight(previous15, 18) ^ previous15 >>> 3; - const sigma1 = rotateRight(previous2, 17) ^ rotateRight(previous2, 19) ^ previous2 >>> 10; - words[index] = words[index - 16] + sigma0 + words[index - 7] + sigma1 >>> 0; - } - let [a, b, c, d, e, f, g, h] = hash; - for (let index = 0; index < 64; index += 1) { - const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25); - const choose = e & f ^ ~e & g; - const temporary1 = h + sum1 + choose + SHA256_CONSTANTS[index] + words[index] >>> 0; - const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22); - const majority = a & b ^ a & c ^ b & c; - const temporary2 = sum0 + majority >>> 0; - h = g; - g = f; - f = e; - e = d + temporary1 >>> 0; - d = c; - c = b; - b = a; - a = temporary1 + temporary2 >>> 0; - } - hash[0] = hash[0] + a >>> 0; - hash[1] = hash[1] + b >>> 0; - hash[2] = hash[2] + c >>> 0; - hash[3] = hash[3] + d >>> 0; - hash[4] = hash[4] + e >>> 0; - hash[5] = hash[5] + f >>> 0; - hash[6] = hash[6] + g >>> 0; - hash[7] = hash[7] + h >>> 0; - } - return hash.map((word) => word.toString(16).padStart(8, "0")).join(""); -}; -var fingerprint = async (source) => sha256(source); - -// ../../packages/config-editor/src/gateway.ts -var MISSING_FILE_FINGERPRINT = "missing"; -var isMissingFileError = (error) => { - const message = error instanceof Error ? error.message : String(error); - return /(?:\b404\b|not[ -]?found|missing)/i.test(message); -}; -var withMissingFileFallback = (gateway2, suffixes = [".scroll_template", ".default"]) => { - const resolvedPaths = /* @__PURE__ */ new Map(); - const fallbackSuffixes = typeof suffixes === "string" ? [suffixes] : suffixes; - return { - async load(path) { - try { - const content = await gateway2.load(path); - resolvedPaths.set(path, path); - return content; - } catch (error) { - if (!isMissingFileError(error)) throw error; - const resolved = resolvedPaths.get(path); - if (resolved && resolved !== path) { - try { - return await gateway2.load(resolved); - } catch (fallbackError) { - if (!isMissingFileError(fallbackError)) throw fallbackError; - } - } - for (const suffix of fallbackSuffixes) { - const fallback = `${path}${suffix}`; - if (fallback === resolved) continue; - try { - const content = await gateway2.load(fallback); - resolvedPaths.set(path, fallback); - return content; - } catch (fallbackError) { - if (!isMissingFileError(fallbackError)) throw fallbackError; - } - } - throw new Error(`Missing configuration file: ${path}`); - } - }, - async save(path, content, expectedFingerprint) { - const resolved = resolvedPaths.get(path) ?? path; - if (resolved !== path) { - const result = await gateway2.save( - path, - content, - MISSING_FILE_FINGERPRINT - ); - if (result.status === "saved") resolvedPaths.set(path, path); - return result; - } - return await gateway2.save(resolved, content, expectedFingerprint); - } - }; -}; -var schemaForPath = (manifest, path) => { - const schema = path ? manifest.files.find((candidate) => candidate.path === path) : manifest.files[0]; - if (!schema) throw new TypeError(path ? `Manifest has no file "${path}".` : "Manifest has no files."); - return schema; -}; -var loadEditor = async (gateway2, manifest, path) => { - const schema = schemaForPath(manifest, path); - const adapter = createAdapterRegistry().get(schema.format); - if (!adapter) throw new TypeError(`No adapter registered for "${schema.format}".`); - const source = await gateway2.load(schema.path); - return ConfigEditorStore.fromLoadedFile(schema, adapter.parse(source)); -}; -var saveSelectedFile = async (store, gateway2) => { - if (store.snapshot().issues.some((issue2) => issue2.severity === "error")) { - throw new Error("Configuration has validation errors and cannot be saved."); - } - const content = store.serializeSelectedFile(); - const serializedIssues = store.validateSerializedSource(content); - if (serializedIssues.some((issue2) => issue2.severity === "error")) { - throw new Error("Serialized configuration failed validation."); - } - const expectedFingerprint = await fingerprint(store.loadedSource()); - const result = await gateway2.save(store.schema.path, content, expectedFingerprint); - if (result.status === "conflict") return result; - const contentFingerprint = await fingerprint(content); - if (result.fingerprint !== contentFingerprint) { - throw new Error("Save verification failed: gateway fingerprint does not match content."); - } - const remote = await gateway2.load(store.schema.path); - const remoteFingerprint = await fingerprint(remote); - if (remoteFingerprint !== result.fingerprint || remote !== content) { - throw new Error("Save verification failed: reloaded content differs from the saved content."); - } - store.acceptSavedSource(remote); - return result; -}; - -// ../../packages/component/dist/index.js -import { d as dfunc } from "druid:ui/ui"; -import { log, rerender, setHook } from "druid:ui/ui"; -import { Event } from "druid:ui/utils"; -import { log as log2, rerender as rerender2 } from "druid:ui/ui"; -var createAsyncBridge = (onSettled = () => void 0, trace = () => void 0) => { - const pending = /* @__PURE__ */ new Map(); - const early = /* @__PURE__ */ new Map(); - const settle = (operation, result) => { - if (result.tag === "ok") operation.resolve(result.val); - else operation.reject(new Error(String(result.val))); - onSettled(); - }; - const complete = (id, result) => { - trace(`Async callback received for id: ${id} with result: ${result.tag}`); - const operation = pending.get(id); - if (!operation) { - early.set(id, result); - return; - } - pending.delete(id); - settle(operation, result); - }; - const wrap = (fn) => (...args) => new Promise((resolve, reject) => { - const id = fn(...args); - const operation = { - resolve: (value) => resolve(value), - reject - }; - const earlyResult = early.get(id); - if (earlyResult) { - early.delete(id); - settle(operation, earlyResult); - return; - } - pending.set(id, operation); - }); - return { complete, wrap }; -}; -var lowerPropertyValue = (value) => value === void 0 || value === null ? void 0 : String(value); -var callbackMap = {}; -function emit(nodeid, event, e) { - log(`Emit called for nodeid: ${nodeid}, event: ${event}`); - const callbacks = callbackMap[nodeid]; - const result = callbacks?.[event]?.(e); - if (result instanceof Promise) { - result.then(() => rerender()); - } -} -var registerHooks = (id, fnresult) => { - switch (true) { - case !!fnresult.init: - setHook(id, "init"); - callbackMap[id] = { - ...callbackMap[id], - init: fnresult.init - }; - break; - } -}; -var createDFunc = (dfunc2) => { - return (tag, props, ...children) => { - children = children.flat(); - if (typeof tag !== "string") { - if (typeof tag === "function") { - const fnresult = tag(props); - if (fnresult?.view) { - const id3 = fnresult.view(props); - registerHooks(id3, fnresult); - return id3; - } else { - return tag(props); - } - } - const id2 = tag.view(props); - registerHooks(id2, tag); - return id2; - } - const ps = { prop: [], on: [] }; - const cbObj = {}; - if (props) { - for (const [key, value] of Object.entries(props)) { - if (value instanceof Function) { - const eventKey = key.startsWith("on") ? key.slice(2).toLowerCase() : key; - cbObj[eventKey] = value; - ps.on.push(eventKey); - } else { - const loweredValue = lowerPropertyValue(value); - if (loweredValue === void 0) continue; - if (typeof value === "boolean") { - ps.prop.push({ key, value: loweredValue }); - continue; - } - ps.prop.push({ key, value: loweredValue }); - } - } - } - const id = dfunc2( - tag, - ps, - children.filter((c) => typeof c !== "boolean").map((c) => String(c)) - ); - callbackMap[id] = { - ...callbackMap[id], - ...cbObj - }; - return id; - }; -}; -var asyncBridge = createAsyncBridge( - () => rerender(), - (message) => log(message) -); -var asyncCallback = asyncBridge.complete; -var rawAsyncToPromise = asyncBridge.wrap; -var createComponent = (j) => ({ - init: (ctx) => j(ctx), - emit, - asyncComplete: asyncCallback -}); -var d2 = createDFunc(dfunc); - -// ../../packages/config-editor/src/copy.ts -var copy = { - appTitle: "Server configuration", - loading: "Loading configuration\u2026", - formTab: "Form", - rawTab: "Raw", - filesHeading: "Configuration files", - inspectorHeading: "Changes & validation", - noChanges: "No pending changes", - rawChanged: "Raw configuration changed", - unknownKeys: "Unknown keys: Preserved", - restartRequired: "Restart required", - stopBeforeSave: "Stop the server before saving configuration files that it may rewrite.", - noRestartRequired: "No restart required", - save: "Save changes", - saving: "Saving\u2026", - saved: "Saved and verified", - conflict: "Remote file changed. Reload before saving.", - rawLabel: "Raw configuration", - validationHeading: "Validation", - valid: "Configuration is valid", - secretChanged: "Secret changed", - fileFormat: "Format" -}; - -// ../../packages/config-editor/src/styles.ts -var EDITOR_STYLES = ` -:host { - display: block; - width: 100%; - --druid-bg: #07100d; - --druid-panel: #101b17; - --druid-panel-raised: #15231d; - --druid-border: #294137; - --druid-border-strong: #3f6655; - --druid-text: #f4f7f5; - --druid-muted: #9eb0a8; - --druid-accent: #a8ef9c; - --druid-accent-ink: #10210f; - --druid-warning: #f0c96b; - --druid-error: #ff8b7f; - color: var(--druid-text); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - color-scheme: dark; -} -* { box-sizing: border-box; } -button, input, select, textarea { font: inherit; } -button, input, select, textarea, a { outline: none; } -button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, a:focus-visible { - box-shadow: 0 0 0 3px rgba(168, 239, 156, .28); - border-color: var(--druid-accent); -} -.config-editor { min-height: 640px; background: radial-gradient(circle at 75% 0%, #173326 0, transparent 32%), var(--druid-bg); color: var(--druid-text); } -.editor-header { min-height: 76px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px 22px; border-bottom: 1px solid var(--druid-border); } -.eyebrow { color: var(--druid-accent); font-size: 11px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; } -.editor-title { margin: 3px 0 0; font-size: clamp(20px, 2.4vw, 30px); line-height: 1.1; } -.server-version { color: var(--druid-muted); font-size: 13px; } -.editor-grid { display: grid; grid-template-columns: minmax(210px, 250px) minmax(420px, 1fr) minmax(260px, 320px); min-height: 564px; } -.file-rail, .inspector { background: rgba(11, 21, 17, .8); } -.file-rail { padding: 18px 14px; border-right: 1px solid var(--druid-border); } -.rail-heading, .inspector h2 { margin: 0 0 12px; font-size: 12px; color: var(--druid-muted); letter-spacing: .08em; text-transform: uppercase; } -.file-list { display: grid; gap: 8px; } -.file-button { width: 100%; min-height: 52px; display: grid; gap: 3px; padding: 10px 12px; text-align: left; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--druid-text); cursor: pointer; } -.file-button:hover { background: var(--druid-panel); } -.file-button[aria-current="true"] { border-color: var(--druid-border-strong); background: var(--druid-panel-raised); } -.file-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; } -.file-format { color: var(--druid-muted); font-size: 12px; text-transform: uppercase; } -.editor-main { min-width: 0; padding: 18px 22px 92px; } -.tabs { display: inline-flex; gap: 4px; padding: 4px; margin-bottom: 18px; border: 1px solid var(--druid-border); border-radius: 10px; background: #0b1511; } -.tab { min-height: 40px; min-width: 92px; border: 0; border-radius: 7px; background: transparent; color: var(--druid-muted); cursor: pointer; } -.tab[aria-selected="true"] { background: var(--druid-panel-raised); color: var(--druid-text); } -.form-editor { display: grid; gap: 18px; } -.field-section { min-width: 0; margin: 0; padding: 18px; border: 1px solid var(--druid-border); border-radius: 14px; background: rgba(16, 27, 23, .84); } -.field-section legend { padding: 0 8px; color: var(--druid-accent); font-weight: 800; } -.section-description { margin: 0 0 16px; color: var(--druid-muted); font-size: 13px; } -.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } -.field-control { min-width: 0; display: grid; align-content: start; gap: 7px; } -.field-control label { font-size: 13px; font-weight: 750; } -.field-description { min-height: 34px; margin: 0; color: var(--druid-muted); font-size: 12px; line-height: 1.45; } -.field-input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--druid-border); border-radius: 9px; background: #09130f; color: var(--druid-text); } -.boolean-control { min-height: 44px; display: flex; align-items: center; gap: 10px; padding: 8px 11px; border: 1px solid var(--druid-border); border-radius: 9px; background: #09130f; } -.boolean-control input { width: 20px; height: 20px; accent-color: var(--druid-accent); } -.field-meta { display: flex; flex-wrap: wrap; gap: 8px; color: var(--druid-muted); font-size: 11px; } -.restart-chip { color: var(--druid-warning); } -.field-error { margin: 0; color: var(--druid-error); font-size: 12px; } -.raw-editor { display: grid; gap: 10px; } -.raw-textarea { width: 100%; min-height: 440px; resize: vertical; padding: 16px; border: 1px solid var(--druid-border); border-radius: 12px; background: #050b08; color: #dce8e1; font: 13px/1.55 "Cascadia Code", "SFMono-Regular", Consolas, monospace; tab-size: 2; } -.inspector { display: flex; flex-direction: column; gap: 18px; padding: 18px; border-left: 1px solid var(--druid-border); } -.status-card { padding: 14px; border: 1px solid var(--druid-border); border-radius: 12px; background: var(--druid-panel); } -.status-card p { margin: 4px 0; color: var(--druid-muted); font-size: 13px; } -.change-list, .issue-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } -.change-item { padding: 10px; border: 1px solid var(--druid-border); border-radius: 9px; background: #0b1511; font-size: 13px; } -.change-value { display: block; margin-top: 4px; color: var(--druid-muted); word-break: break-word; } -.issue-item { color: var(--druid-error); font-size: 12px; } -.action-bar { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: auto 0 0; padding-top: 14px; background: linear-gradient(transparent, rgba(11, 21, 17, .98) 32%); } -.save-button { min-height: 46px; padding: 0 18px; border: 0; border-radius: 9px; background: var(--druid-accent); color: var(--druid-accent-ink); font-weight: 850; cursor: pointer; } -.save-button:disabled { opacity: .45; cursor: not-allowed; } -.action-status { color: var(--druid-muted); font-size: 12px; } -.error-shell { margin: 20px; padding: 18px; border: 1px solid var(--druid-error); border-radius: 12px; background: #28120f; color: #ffd8d2; } -@media (max-width: 900px) { - .editor-grid { grid-template-columns: 190px minmax(0, 1fr); } - .inspector { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--druid-border); } -} -@media (max-width: 560px) { - .config-editor { min-height: 100%; } - .editor-header { align-items: flex-start; padding: 14px 16px; } - .editor-grid { display: block; } - .file-rail { padding: 12px 16px; border-right: 0; border-bottom: 1px solid var(--druid-border); overflow-x: auto; } - .file-list { display: flex; width: max-content; } - .file-button { width: 190px; } - .editor-main { padding: 16px 16px 28px; } - .tabs { display: flex; } - .tab { flex: 1; } - .field-section { padding: 14px; } - .field-grid { grid-template-columns: 1fr; } - .raw-textarea { min-height: 360px; } - .inspector { padding: 16px; } - .action-bar { position: static; } - .save-button { width: 100%; } -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } -} -`; - -// ../../packages/component/dist/jsx-runtime.js -function jsx(type, props) { - const { children, ...rest } = props || {}; - if (children !== void 0) { - return d2(type, rest, children); - } - return d2(type, rest); -} -var jsxs = jsx; -var Fragment = Symbol.for("react.fragment"); - -// ../../packages/config-editor/src/components/ActionBar.tsx -var ActionBar = ({ dirty, invalid, saving, status, onSave }) => /* @__PURE__ */ jsxs("div", { class: "action-bar", children: [ - /* @__PURE__ */ jsx("span", { class: "action-status", "aria-live": "polite", children: status }), - /* @__PURE__ */ jsx( - "button", - { - type: "button", - class: "save-button", - disabled: !dirty || invalid || saving, - onClick: () => onSave(), - children: saving ? copy.saving : copy.save - } - ) -] }); - -// ../../packages/config-editor/src/components/FileRail.tsx -var FileRail = ({ manifest, selectedPath, onSelect }) => /* @__PURE__ */ jsxs("nav", { class: "file-rail", "aria-label": copy.filesHeading, children: [ - /* @__PURE__ */ jsx("h2", { class: "rail-heading", children: copy.filesHeading }), - /* @__PURE__ */ jsx("div", { class: "file-list", children: manifest.files.map((file) => /* @__PURE__ */ jsxs( - "button", - { - type: "button", - class: "file-button", - "aria-current": file.path === selectedPath ? "true" : "false", - onClick: () => onSelect(file.path), - children: [ - /* @__PURE__ */ jsx("span", { class: "file-path", children: file.label }), - /* @__PURE__ */ jsx("span", { class: "file-format", children: file.format }) - ] - } - )) }) -] }); - -// ../../packages/config-editor/src/components/FieldControl.tsx -var FieldControl = ({ field, state, onChange }) => { - const inputId = `config-field-${field.key.replace(/[^a-zA-Z0-9_-]/g, "-")}`; - const descriptionId = `${inputId}-description`; - const input = field.type === "enum" ? /* @__PURE__ */ jsx( - "select", - { - id: inputId, - class: "field-input", - "aria-label": field.label, - "aria-describedby": descriptionId, - "aria-invalid": state.issues.length > 0 ? "true" : "false", - value: state.displayValue, - onChange: (event) => onChange(field.key, event.value()), - children: (field.values ?? []).map((value) => /* @__PURE__ */ jsx("option", { value, selected: state.displayValue === value, children: value })) - } - ) : field.type === "boolean" ? /* @__PURE__ */ jsxs("span", { class: "boolean-control", children: [ - /* @__PURE__ */ jsx( - "input", - { - id: inputId, - type: "checkbox", - "aria-label": field.label, - "aria-describedby": descriptionId, - checked: state.displayValue === "true", - onChange: (event) => onChange(field.key, event.checked() ? "true" : "false") - } - ), - /* @__PURE__ */ jsx("span", { children: state.displayValue === "true" ? "Enabled" : "Disabled" }) - ] }) : /* @__PURE__ */ jsx( - "input", - { - id: inputId, - class: "field-input", - "aria-label": field.label, - "aria-describedby": descriptionId, - type: field.type === "secret" || field.sensitive ? "password" : field.type === "integer" || field.type === "number" ? "number" : "text", - step: field.type === "integer" ? "1" : field.type === "number" ? "any" : void 0, - min: field.min, - max: field.max, - value: state.displayValue, - "aria-invalid": state.issues.length > 0 ? "true" : "false", - autocomplete: field.type === "secret" || field.sensitive ? "new-password" : "off", - onInput: (event) => onChange(field.key, event.value()) - } - ); - return /* @__PURE__ */ jsxs("div", { class: "field-control", children: [ - /* @__PURE__ */ jsx("label", { for: inputId, children: field.label }), - /* @__PURE__ */ jsx("p", { id: descriptionId, class: "field-description", children: field.description }), - input, - /* @__PURE__ */ jsxs("div", { class: "field-meta", children: [ - field.restartRequired ? /* @__PURE__ */ jsx("span", { class: "restart-chip", children: copy.restartRequired }) : false, - /* @__PURE__ */ jsx("span", { children: field.key }) - ] }), - state.issues.map((issue2) => /* @__PURE__ */ jsx("p", { class: "field-error", role: "alert", children: issue2.message })) - ] }); -}; - -// ../../packages/config-editor/src/components/FormEditor.tsx -var FormEditor = ({ schema, snapshot, onChange }) => /* @__PURE__ */ jsx("div", { class: "form-editor", children: schema.sections.map((section) => /* @__PURE__ */ jsxs("fieldset", { class: "field-section", children: [ - /* @__PURE__ */ jsx("legend", { children: section.label }), - section.description ? /* @__PURE__ */ jsx("p", { class: "section-description", children: section.description }) : false, - /* @__PURE__ */ jsx("div", { class: "field-grid", children: section.fields.map((field) => { - const state = snapshot.fields[field.key]; - return state ? /* @__PURE__ */ jsx(FieldControl, { field, state, onChange }) : false; - }) }) -] })) }); - -// ../../packages/config-editor/src/components/Inspector.tsx -var printable = (value) => value === void 0 ? "Not set" : value === null ? "Protected" : String(value); -var Inspector = ({ snapshot }) => /* @__PURE__ */ jsxs("aside", { class: "inspector", "aria-label": copy.inspectorHeading, children: [ - /* @__PURE__ */ jsxs("section", { children: [ - /* @__PURE__ */ jsx("h2", { children: copy.inspectorHeading }), - snapshot.unstructuredChanges ? /* @__PURE__ */ jsx("div", { class: "status-card", children: /* @__PURE__ */ jsx("p", { children: copy.rawChanged }) }) : snapshot.changes.length === 0 ? /* @__PURE__ */ jsx("div", { class: "status-card", children: /* @__PURE__ */ jsx("p", { children: copy.noChanges }) }) : /* @__PURE__ */ jsx("ul", { class: "change-list", children: snapshot.changes.map((change) => /* @__PURE__ */ jsxs("li", { class: "change-item", children: [ - /* @__PURE__ */ jsx("strong", { children: change.label }), - /* @__PURE__ */ jsx("span", { class: "change-value", children: change.sensitive ? copy.secretChanged : `${printable(change.before)} \u2192 ${printable(change.after)}` }) - ] })) }) - ] }), - /* @__PURE__ */ jsxs("section", { class: "status-card", children: [ - /* @__PURE__ */ jsx("h2", { children: copy.validationHeading }), - snapshot.issues.length === 0 ? /* @__PURE__ */ jsx("p", { children: copy.valid }) : /* @__PURE__ */ jsx("ul", { class: "issue-list", "aria-live": "polite", children: snapshot.issues.map((issue2) => /* @__PURE__ */ jsx("li", { class: "issue-item", children: issue2.message })) }), - /* @__PURE__ */ jsx("p", { children: copy.unknownKeys }), - /* @__PURE__ */ jsx("p", { children: snapshot.restartRequired ? copy.restartRequired : copy.noRestartRequired }), - snapshot.restartRequired ? /* @__PURE__ */ jsx("p", { children: copy.stopBeforeSave }) : false - ] }) -] }); - -// ../../packages/config-editor/src/components/RawEditor.tsx -var RawEditor = ({ source, onChange }) => /* @__PURE__ */ jsxs("div", { class: "raw-editor", children: [ - /* @__PURE__ */ jsx("label", { for: "config-raw-source", children: copy.rawLabel }), - /* @__PURE__ */ jsx( - "textarea", - { - id: "config-raw-source", - class: "raw-textarea", - "aria-label": copy.rawLabel, - spellcheck: "false", - value: source, - onInput: (event) => onChange(event.value()) - } - ) -] }); - -// ../../packages/config-editor/src/components/EditorApp.tsx -var EditorApp = ({ - manifest, - store, - snapshot, - mode, - saving, - status, - onMode, - onSelect, - onField, - onRaw, - onSave -}) => /* @__PURE__ */ jsxs("div", { class: "config-editor", children: [ - /* @__PURE__ */ jsx("style", { children: EDITOR_STYLES }), - /* @__PURE__ */ jsxs("header", { class: "editor-header", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("div", { class: "eyebrow", children: "Druid Admin UI" }), - /* @__PURE__ */ jsx("h1", { class: "editor-title", children: manifest.server.displayName }) - ] }), - /* @__PURE__ */ jsx("div", { class: "server-version", children: manifest.server.appVersion ? `Version ${manifest.server.appVersion}` : copy.appTitle }) - ] }), - /* @__PURE__ */ jsxs("div", { class: "editor-grid", children: [ - /* @__PURE__ */ jsx( - FileRail, - { - manifest, - selectedPath: store.schema.path, - onSelect - } - ), - /* @__PURE__ */ jsxs("main", { class: "editor-main", children: [ - /* @__PURE__ */ jsxs("div", { class: "tabs", role: "tablist", "aria-label": "Editor mode", children: [ - /* @__PURE__ */ jsx( - "button", - { - type: "button", - class: "tab", - role: "tab", - disabled: store.schema.sections.length === 0, - "aria-disabled": store.schema.sections.length === 0 ? "true" : "false", - "aria-selected": mode === "form" ? "true" : "false", - onClick: () => onMode("form"), - children: copy.formTab - } - ), - /* @__PURE__ */ jsx( - "button", - { - type: "button", - class: "tab", - role: "tab", - "aria-selected": mode === "raw" ? "true" : "false", - onClick: () => onMode("raw"), - children: copy.rawTab - } - ) - ] }), - mode === "form" ? /* @__PURE__ */ jsx(FormEditor, { schema: store.schema, snapshot, onChange: onField }) : /* @__PURE__ */ jsx(RawEditor, { source: store.serializeForDisplay(), onChange: onRaw }), - /* @__PURE__ */ jsx( - ActionBar, - { - dirty: snapshot.dirty, - invalid: snapshot.issues.some((issue2) => issue2.severity === "error"), - saving, - status, - onSave - } - ) - ] }), - /* @__PURE__ */ jsx(Inspector, { snapshot }) - ] }) -] }); -var createConfigEditorComponent = ({ - manifestPath, - gateway: gateway2 -}) => { - let manifest; - const stores = /* @__PURE__ */ new Map(); - let selectedPath = ""; - let mode = "form"; - let loading = false; - let saving = false; - let status = ""; - let errorMessage = ""; - const selectFile = async (path) => { - if (!manifest) return; - const previousPath = selectedPath; - status = copy.loading; - rerender2(); - try { - let store = stores.get(path); - if (!store) { - store = await loadEditor(gateway2, manifest, path); - stores.set(path, store); - } - selectedPath = path; - mode = store.schema.sections.length === 0 ? "raw" : "form"; - status = ""; - } catch (error) { - selectedPath = previousPath; - status = error instanceof Error ? error.message : String(error); - if (!stores.has(previousPath)) throw error; - } finally { - rerender2(); - } - }; - const initialise = async () => { - loading = true; - try { - manifest = validateManifest(JSON.parse(await gateway2.load(manifestPath))); - selectedPath = manifest.files[0].path; - await selectFile(selectedPath); - } catch (error) { - errorMessage = error instanceof Error ? error.message : String(error); - } finally { - loading = false; - rerender2(); - } - }; - const save = async () => { - const store = stores.get(selectedPath); - if (!store) return; - saving = true; - status = copy.saving; - try { - const result = await saveSelectedFile(store, gateway2); - status = result.status === "saved" ? copy.saved : copy.conflict; - } catch (error) { - status = error instanceof Error ? error.message : String(error); - } finally { - saving = false; - rerender2(); - } - }; - return createComponent(() => { - if (!loading && !manifest && !errorMessage) void initialise(); - if (errorMessage) { - return /* @__PURE__ */ jsxs("div", { class: "config-editor", children: [ - /* @__PURE__ */ jsx("style", { children: EDITOR_STYLES }), - /* @__PURE__ */ jsx("div", { class: "error-shell", role: "alert", children: errorMessage }) - ] }); - } - const store = stores.get(selectedPath); - if (!manifest || !store) { - return /* @__PURE__ */ jsxs("div", { class: "config-editor", children: [ - /* @__PURE__ */ jsx("style", { children: EDITOR_STYLES }), - /* @__PURE__ */ jsx("div", { class: "status-card", "aria-live": "polite", children: copy.loading }) - ] }); - } - const snapshot = store.snapshot(); - return /* @__PURE__ */ jsx( - EditorApp, - { - manifest, - store, - snapshot, - mode, - saving, - status, - onMode: (nextMode) => { - mode = nextMode; - }, - onSelect: selectFile, - onField: (key, value) => { - store.setDisplayValue(key, value); - status = ""; - }, - onRaw: (source) => { - store.replaceWorkingSource(source); - status = ""; - }, - onSave: save - } - ); - }); -}; - -// ../../packages/plattform/dist/index.js -import { - request as requestRaw, - loadFileFromDeployment as loadFileFromDeploymentRaw, - saveFileToDeployment as saveFileToDeploymentRaw, - saveFileToDeploymentIfMatch as saveFileToDeploymentIfMatchRaw -} from "druid:ui/plattform"; -var request = rawAsyncToPromise(requestRaw); -var loadFileFromDeployment = rawAsyncToPromise( - loadFileFromDeploymentRaw -); -var saveFileToDeployment = rawAsyncToPromise(saveFileToDeploymentRaw); -var saveFileToDeploymentIfMatch = rawAsyncToPromise( - saveFileToDeploymentIfMatchRaw -); - -// src/app.tsx -var gateway = withMissingFileFallback({ - async load(path) { - return await loadFileFromDeployment(path); - }, - async save(path, content, expectedFingerprint) { - return JSON.parse( - await saveFileToDeploymentIfMatch(path, content, expectedFingerprint) - ); - } -}); -var component = createConfigEditorComponent({ - manifestPath: "private/config-editor.manifest.json", - gateway -}); -export { - component -}; diff --git a/examples/config-editor-plattform/public/wit/druid-component.wit b/examples/config-editor-plattform/public/wit/druid-component.wit deleted file mode 100644 index b3a7ba8..0000000 --- a/examples/config-editor-plattform/public/wit/druid-component.wit +++ /dev/null @@ -1,16 +0,0 @@ -package druid:ui; - - -interface component { - use utils.{event, context}; - - init: func(ctx: context) -> string; - emit: func(nodeid: string, event: string, e: event); - async-complete: func(id: string, value: result); -} - - -world druid-ui { - import ui; - export component; -} diff --git a/examples/config-editor-plattform/public/wit/druid-plattform.wit b/examples/config-editor-plattform/public/wit/druid-plattform.wit deleted file mode 100644 index 2ebe3e1..0000000 --- a/examples/config-editor-plattform/public/wit/druid-plattform.wit +++ /dev/null @@ -1,14 +0,0 @@ -package druid:ui; - -interface plattform { - request: func(url: string, method: string, body: string, header: tuple) -> string; - load-file-from-deployment: func(path: string) -> string; - save-file-to-deployment: func(path: string, content: string) -> string; - save-file-to-deployment-if-match: func(path: string, content: string, expected-fingerprint: string) -> string; -} - -world druid-plattform { - import ui; - import plattform; - export component; -} diff --git a/examples/config-editor-plattform/public/wit/druid-ui.wit b/examples/config-editor-plattform/public/wit/druid-ui.wit deleted file mode 100644 index 4e4b4f5..0000000 --- a/examples/config-editor-plattform/public/wit/druid-ui.wit +++ /dev/null @@ -1,35 +0,0 @@ -package druid:ui; - - -interface ui { - - record prop { key: string, value: string, } - - record props { - prop: list, - on: list, - } - - type async-id = string; - - type children = option>; - - d: func(element: string, props: props, children: children) -> string; - log: func(msg: string); - set-hook: func(id: async-id, hook: string); - rerender: func(); -} - -interface utils { - - resource event { - constructor(value: string, checked: bool); - prevent-default: func(); - stop-propagation: func(); - value: func() -> string; - checked: func() -> bool; - } - - type context = list>; - -} diff --git a/examples/config-editor-plattform/src/app.tsx b/examples/config-editor-plattform/src/app.tsx deleted file mode 100644 index 2debba7..0000000 --- a/examples/config-editor-plattform/src/app.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - createConfigEditorComponent, - withMissingFileFallback, - type FileGateway, -} from "../../../packages/config-editor/src/index.js"; -// The example consumes the workspace source so package edits participate in raw -// hot reload during local development. Released Scroll UIs consume the package. -import { - loadFileFromDeployment, - saveFileToDeploymentIfMatch, -} from "@druid-ui/plattform"; - -const gateway = withMissingFileFallback({ - async load(path) { - return await loadFileFromDeployment(path); - }, - async save(path, content, expectedFingerprint) { - return JSON.parse( - await saveFileToDeploymentIfMatch(path, content, expectedFingerprint), - ); - }, -} satisfies FileGateway); - -export const component = createConfigEditorComponent({ - manifestPath: "private/config-editor.manifest.json", - gateway, -}); diff --git a/examples/config-editor-plattform/src/main.ts b/examples/config-editor-plattform/src/main.ts deleted file mode 100644 index 05859ee..0000000 --- a/examples/config-editor-plattform/src/main.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { DruidUI, PromiseToResult } from "@druid-ui/host"; -import { ViteHMR } from "@druid-ui/vite/client"; -import manifest from "../fixtures/config-editor.manifest.json"; -import serverProperties from "../fixtures/server.properties?raw"; - -const query = new URLSearchParams(window.location.search); -const fixtureManifest = structuredClone(manifest); -if (query.get("rawOnly") === "1") fixtureManifest.files[0]!.sections = []; -const serverPropertiesPath = - query.get("templateOnly") === "1" - ? "server.properties.scroll_template" - : "server.properties"; - -const files = new Map([ - ["private/config-editor.manifest.json", JSON.stringify(fixtureManifest)], - [serverPropertiesPath, serverProperties], -]); - -const druidUiElement = new DruidUI(); -druidUiElement.extensionObject = { - "druid:ui/plattform": { - request: PromiseToResult(async () => ""), - loadFileFromDeployment: PromiseToResult(async (path: string) => { - const content = files.get(path); - if (content === undefined) throw new Error(`Missing fixture file: ${path}`); - return content; - }), - saveFileToDeployment: PromiseToResult(async (path: string, content: string) => { - files.set(path, content); - return "saved"; - }), - saveFileToDeploymentIfMatch: PromiseToResult( - async (path: string, content: string, expectedFingerprint: string) => { - const remote = files.get(path); - if (remote === undefined && expectedFingerprint === "missing") { - files.set(path, content); - const savedDigest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)); - const fingerprint = Array.from(new Uint8Array(savedDigest), (byte) => - byte.toString(16).padStart(2, "0") - ).join(""); - return JSON.stringify({ status: "saved", fingerprint }); - } - if (remote === undefined) throw new Error(`Missing fixture file: ${path}`); - const bytes = new TextEncoder().encode(remote); - const digest = await crypto.subtle.digest("SHA-256", bytes); - const remoteFingerprint = Array.from(new Uint8Array(digest), (byte) => - byte.toString(16).padStart(2, "0") - ).join(""); - if (remoteFingerprint !== expectedFingerprint) { - return JSON.stringify({ status: "conflict", remote, fingerprint: remoteFingerprint }); - } - files.set(path, content); - const savedDigest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)); - const fingerprint = Array.from(new Uint8Array(savedDigest), (byte) => - byte.toString(16).padStart(2, "0") - ).join(""); - return JSON.stringify({ status: "saved", fingerprint }); - }, - ), - }, -}; - -const mode = query.get("mode"); -const sandbox = mode === "wasm"; -druidUiElement.setAttribute( - "entrypoint", - sandbox ? "/app.wasm" : "/app.bundled-raw.js", -); -if (!sandbox) druidUiElement.setAttribute("no-sandbox", "true"); -druidUiElement.setAttribute("profile", "false"); - -document.getElementById("app")?.appendChild(druidUiElement); -ViteHMR(druidUiElement); - -Object.assign(window, { - __druidConfigEditorFixture: { - mode: sandbox ? "wasm" : "raw", - read(path: string) { - return files.get(path); - }, - replace(path: string, content: string) { - files.set(path, content); - }, - }, -}); diff --git a/examples/config-editor-plattform/tsconfig.json b/examples/config-editor-plattform/tsconfig.json deleted file mode 100644 index 19ee7df..0000000 --- a/examples/config-editor-plattform/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "jsx": "react-jsx", - "jsxImportSource": "@druid-ui/component", - "types": ["@druid-ui/component/jsx", "vite/client"], - "outDir": "./dist" - }, - "include": ["src/**/*", "public/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/examples/config-editor-plattform/vite.config.ts b/examples/config-editor-plattform/vite.config.ts deleted file mode 100644 index 91d8660..0000000 --- a/examples/config-editor-plattform/vite.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import wasm from "vite-plugin-wasm"; -import { defineConfig } from "vite"; -import { ViteHMRPlugin } from "@druid-ui/vite"; - -export default defineConfig({ - plugins: [ - wasm(), - ViteHMRPlugin("src/app.tsx", "raw", undefined, [ - "../../packages/config-editor/src/**/*.{ts,tsx}", - ]), - ], - build: { target: "esnext" }, - optimizeDeps: { - exclude: [ - "@bytecodealliance/jco", - "@bytecodealliance/componentize-js", - "@druid-ui/host", - ], - }, - server: { - fs: { allow: ["../.."] }, - watch: { usePolling: true, interval: 150 }, - }, -}); diff --git a/examples/config-editor-plattform/wit/druid-plattform.wit b/examples/config-editor-plattform/wit/druid-plattform.wit deleted file mode 100644 index 2ebe3e1..0000000 --- a/examples/config-editor-plattform/wit/druid-plattform.wit +++ /dev/null @@ -1,14 +0,0 @@ -package druid:ui; - -interface plattform { - request: func(url: string, method: string, body: string, header: tuple) -> string; - load-file-from-deployment: func(path: string) -> string; - save-file-to-deployment: func(path: string, content: string) -> string; - save-file-to-deployment-if-match: func(path: string, content: string, expected-fingerprint: string) -> string; -} - -world druid-plattform { - import ui; - import plattform; - export component; -} diff --git a/package-lock.json b/package-lock.json index 19d91fa..85c20a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,22 +23,6 @@ "typescript": "~5.8.3" } }, - "examples/config-editor-plattform": { - "version": "0.0.0", - "dependencies": { - "@druid-ui/component": "*", - "@druid-ui/config-editor": "*", - "@druid-ui/host": "*", - "@druid-ui/plattform": "*", - "@druid-ui/vite": "*" - }, - "devDependencies": { - "@druid-ui/build": "*", - "typescript": "~5.8.3", - "vite": "^7.1.7", - "vite-plugin-wasm": "^3.5.0" - } - }, "examples/druid-plattform": { "version": "0.0.0", "devDependencies": { @@ -1218,10 +1202,6 @@ "resolved": "packages/component", "link": true }, - "node_modules/@druid-ui/config-editor": { - "resolved": "packages/config-editor", - "link": true - }, "node_modules/@druid-ui/host": { "resolved": "packages/host", "link": true @@ -4899,10 +4879,6 @@ "node": ">=20" } }, - "node_modules/config-editor-plattform": { - "resolved": "examples/config-editor-plattform", - "link": true - }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -7731,6 +7707,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "chokidar": "^4.0.3", "typescript": "~5.8.3" } }, @@ -8704,13 +8681,6 @@ "@esbuild/win32-x64": "0.25.12" } }, - "packages/config-editor": { - "name": "@druid-ui/config-editor", - "version": "2.0.0", - "dependencies": { - "@druid-ui/component": "*" - } - }, "packages/create-druid-ui": { "version": "2.0.0", "dependencies": { @@ -10250,6 +10220,36 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } } } } diff --git a/packages/build/package.json b/packages/build/package.json index 6f7a12c..0e269eb 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -39,6 +39,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "chokidar": "^4.0.3", "typescript": "~5.8.3" } } diff --git a/packages/build/src/cmd/index.ts b/packages/build/src/cmd/index.ts index d38ca86..fd42740 100755 --- a/packages/build/src/cmd/index.ts +++ b/packages/build/src/cmd/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { buildWasm, buildRaw } from "../"; -import { watch } from "node:fs"; +import { watch } from "chokidar"; import { resolve } from "node:path"; console.log("Druid UI Build Tool"); @@ -67,22 +67,28 @@ if (watchMode) { console.log("Watching for changes..."); const srcDir = resolve(entryFile, ".."); let building = false; + let buildQueued = false; - watch(srcDir, { recursive: true }, async (_eventType, filename) => { - if ( - filename && - (filename.endsWith(".tsx") || - filename.endsWith(".ts") || - filename.endsWith(".jsx") || - filename.endsWith(".js")) - ) { - if (building) return; + const rebuild = async () => { + if (building) { + buildQueued = true; + return; + } + do { + buildQueued = false; building = true; - console.log(`\nFile changed: ${filename}`); await doBuild(); building = false; - } - }); + } while (buildQueued); + }; + + watch(srcDir, { ignoreInitial: true, usePolling: true, interval: 250 }) + .on("error", (error) => console.error("Watch failed:", error)) + .on("all", (_event, path) => { + if (!/\.[jt]sx?$/.test(path)) return; + console.log(`\nFile changed: ${path}`); + void rebuild(); + }); // Keep the process running await new Promise(() => {}); diff --git a/packages/config-editor/package.json b/packages/config-editor/package.json deleted file mode 100644 index c26142a..0000000 --- a/packages/config-editor/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@druid-ui/config-editor", - "version": "2.0.0", - "repository": { - "type": "git", - "url": "git+https://github.com/highcard-dev/druid-ui.git" - }, - "type": "module", - "main": "./dist/index.js", - "types": "./dist/types/index.d.ts", - "exports": { - ".": { - "types": "./dist/types/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "src" - ], - "scripts": { - "build": "npm run build:lib && npm run build:types", - "build:lib": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --platform=neutral --external:@druid-ui/component --external:druid:ui/*", - "build:types": "tsc --emitDeclarationOnly --outDir dist/types", - "test": "vitest run src" - }, - "dependencies": { - "@druid-ui/component": "*" - } -} diff --git a/packages/config-editor/src/adapters/ini.test.ts b/packages/config-editor/src/adapters/ini.test.ts deleted file mode 100644 index 102af2b..0000000 --- a/packages/config-editor/src/adapters/ini.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { iniAdapter, keyValueAdapter, unrealIniAdapter } from "./ini.js"; - -describe("iniAdapter", () => { - it("changes only a value in the selected section", () => { - const source = - "; generated\r\n[Server]\r\nName = Old ; keep this\r\nPlayers=20\r\n\r\n[Other]\r\nName=Untouched\r\n"; - const document = iniAdapter.parse(source); - expect(iniAdapter.get(document, "Server.Name")).toBe("Old"); - const changed = iniAdapter.set(document, "Server.Name", "Druid"); - expect(iniAdapter.serialize(changed)).toBe( - source.replace("Name = Old", "Name = Druid"), - ); - }); - - it("uses the last duplicate key and keeps all other lines", () => { - const source = "[Server]\nName=old\nName=new\n"; - const changed = iniAdapter.set(iniAdapter.parse(source), "Server.Name", "final"); - expect(iniAdapter.get(changed, "Server.Name")).toBe("final"); - expect(iniAdapter.serialize(changed)).toBe( - "[Server]\nName=old\nName=final\n", - ); - }); - - it("preserves an untouched document byte-for-byte", () => { - const source = "# comment\nroot=value\n[Section]\nempty=\n"; - expect(iniAdapter.serialize(iniAdapter.parse(source))).toBe(source); - }); -}); - -describe("unrealIniAdapter", () => { - it("changes only an Unreal INI value in the selected section", () => { - const source = - "[/Script/ShooterGame.ShooterGameUserSettings]\r\nServerPassword=\r\nDifficultyOffset=0.2\r\n"; - const changed = unrealIniAdapter.set( - unrealIniAdapter.parse(source), - "/Script/ShooterGame.ShooterGameUserSettings.DifficultyOffset", - 1, - ); - expect(unrealIniAdapter.serialize(changed)).toBe( - source.replace("0.2", "1"), - ); - }); - - it("retains Unreal array prefixes", () => { - const source = "[Rules]\n+AllowedClasses=One\n+AllowedClasses=Two\n"; - const changed = unrealIniAdapter.set( - unrealIniAdapter.parse(source), - "Rules.+AllowedClasses", - "Three", - ); - expect(unrealIniAdapter.serialize(changed)).toBe( - "[Rules]\n+AllowedClasses=One\n+AllowedClasses=Three\n", - ); - }); -}); - -describe("keyValueAdapter", () => { - it("preserves comments and inline spacing while changing one value", () => { - const source = "// server config\nhostname Druid\nmaxplayers = 20 // capacity\n"; - const document = keyValueAdapter.parse(source); - expect(keyValueAdapter.get(document, "hostname")).toBe("Druid"); - expect( - keyValueAdapter.serialize(keyValueAdapter.set(document, "maxplayers", 64)), - ).toBe("// server config\nhostname Druid\nmaxplayers = 64 // capacity\n"); - }); - - it("preserves shell-style quotes while editing their contents", () => { - const source = 'servername="Palworld Server"\n'; - const document = keyValueAdapter.parse(source); - - expect(keyValueAdapter.get(document, "servername")).toBe("Palworld Server"); - expect( - keyValueAdapter.serialize( - keyValueAdapter.set(document, "servername", "Druid Server"), - ), - ).toBe('servername="Druid Server"\n'); - }); - - it("escapes matching quote characters inside preserved double quotes", () => { - const document = keyValueAdapter.parse('servername="Palworld Server"\n'); - - const changed = keyValueAdapter.set(document, "servername", 'Druid "EU"'); - expect(keyValueAdapter.serialize(changed)).toBe('servername="Druid \\"EU\\""\n'); - expect(keyValueAdapter.get(changed, "servername")).toBe('Druid "EU"'); - }); - - it("uses POSIX-safe quoting inside preserved shell single quotes", () => { - const document = keyValueAdapter.parse("servername='Druid Server'\n"); - const changed = keyValueAdapter.set(document, "servername", "Druid's Server"); - expect(keyValueAdapter.serialize(changed)).toBe("servername='Druid'\\''s Server'\n"); - expect(keyValueAdapter.get(changed, "servername")).toBe("Druid's Server"); - }); - - it("roundtrips POSIX apostrophes without consuming inline comments", () => { - const document = keyValueAdapter.parse("servername='Old' # public name\n"); - const changed = keyValueAdapter.set(document, "servername", "Druid's"); - const serialized = keyValueAdapter.serialize(changed); - - expect(serialized).toBe("servername='Druid'\\''s' # public name\n"); - expect(keyValueAdapter.get(keyValueAdapter.parse(serialized), "servername")).toBe("Druid's"); - expect( - keyValueAdapter.serialize( - keyValueAdapter.set(keyValueAdapter.parse(serialized), "servername", "Druid's EU"), - ), - ).toBe("servername='Druid'\\''s EU' # public name\n"); - }); - - it("does not treat comment markers inside quotes as inline comments", () => { - const document = keyValueAdapter.parse('servername="Druid #1" # public name\n'); - - expect(keyValueAdapter.get(document, "servername")).toBe("Druid #1"); - expect( - keyValueAdapter.serialize(keyValueAdapter.set(document, "servername", "Druid #2")), - ).toBe('servername="Druid #2" # public name\n'); - }); - - it("preserves DayZ quotes and statement terminators", () => { - const document = keyValueAdapter.parse('hostname = "Druid Server";\nmaxPlayers = 60;\n'); - - expect(keyValueAdapter.get(document, "hostname")).toBe("Druid Server"); - expect(keyValueAdapter.get(document, "maxPlayers")).toBe("60"); - expect( - keyValueAdapter.serialize(keyValueAdapter.set(document, "hostname", "Druid EU")), - ).toBe('hostname = "Druid EU";\nmaxPlayers = 60;\n'); - }); -}); - -describe("unrealIniAdapter OptionSettings", () => { - it("discovers and edits Palworld tuple options independently", () => { - const source = - '[/Script/Pal.PalGameWorldSettings]\nOptionSettings=(Difficulty=None,ServerName="Palworld, EU",bIsPvP=false,DayTimeSpeedRate=1.000000)\n'; - const document = unrealIniAdapter.parse(source); - - expect(unrealIniAdapter.entries(document)).toEqual([ - { key: "/Script/Pal.PalGameWorldSettings.OptionSettings.Difficulty", value: "None" }, - { key: "/Script/Pal.PalGameWorldSettings.OptionSettings.ServerName", value: "Palworld, EU" }, - { key: "/Script/Pal.PalGameWorldSettings.OptionSettings.bIsPvP", value: "false" }, - { key: "/Script/Pal.PalGameWorldSettings.OptionSettings.DayTimeSpeedRate", value: "1.000000" }, - ]); - - const updated = unrealIniAdapter.set( - document, - "/Script/Pal.PalGameWorldSettings.OptionSettings.ServerName", - "Druid, EU", - ); - expect(unrealIniAdapter.serialize(updated)).toBe( - source.replace('ServerName="Palworld, EU"', 'ServerName="Druid, EU"'), - ); - }); -}); diff --git a/packages/config-editor/src/adapters/ini.ts b/packages/config-editor/src/adapters/ini.ts deleted file mode 100644 index f91ca3e..0000000 --- a/packages/config-editor/src/adapters/ini.ts +++ /dev/null @@ -1,378 +0,0 @@ -import type { ConfigFormat, ConfigValue, FileSchema, ValidationIssue } from "../model.js"; -import { hasFinalNewline, preferredLineEnding, splitSourceLines } from "./line-document.js"; -import type { - ConfigAdapter, - LineNode, - ParsedDocument, - SourceSpan, -} from "./types.js"; - -interface IniEntry { - key: string; - value: string; - valueSpan: SourceSpan; - line: number; - quote?: '"' | "'"; - container?: boolean; -} - -interface IniSection { - name: string; - line: number; -} - -export interface IniDocument extends ParsedDocument { - format: "ini" | "unreal-ini" | "key-value"; - entries: readonly IniEntry[]; - sections: readonly IniSection[]; - lineEnding: "\n" | "\r\n"; - finalNewline: boolean; -} - -const inlineCommentAt = (value: string): number => { - let quote: '"' | "'" | undefined; - for (let index = 0; index < value.length; index += 1) { - const character = value[index]!; - if (quote) { - if (quote === "'" && value.startsWith("'\\''", index)) { - index += 3; - continue; - } - if (character === quote && value[index - 1] !== "\\") quote = undefined; - continue; - } - if (character === '"' || character === "'") { - quote = character; - continue; - } - const precededBySpace = index > 0 && /\s/.test(value[index - 1]!); - if (!precededBySpace) continue; - if (character === ";" || character === "#") return index; - if (character === "/" && value[index + 1] === "/") return index; - } - return value.length; -}; - -const decodeQuotedValue = (value: string, quote?: '"' | "'"): string => { - if (quote === '"') return value.replace(/\\(["\\])/g, "$1"); - if (quote === "'") return value.replace(/'\\''/g, "'"); - return value; -}; - -const parseEntryLine = ( - raw: string, - start: number, - section: string | undefined, - line: number, - format: IniDocument["format"], -): IniEntry | undefined => { - let keyStart = 0; - while (keyStart < raw.length && /[ \t]/.test(raw[keyStart]!)) keyStart += 1; - if (keyStart === raw.length) return undefined; - - let keyEnd = keyStart; - while ( - keyEnd < raw.length && - raw[keyEnd] !== "=" && - raw[keyEnd] !== ":" && - !/[ \t]/.test(raw[keyEnd]!) - ) { - keyEnd += 1; - } - if (keyEnd === keyStart) return undefined; - - let valueStart = keyEnd; - while (valueStart < raw.length && /[ \t]/.test(raw[valueStart]!)) valueStart += 1; - if (raw[valueStart] === "=" || raw[valueStart] === ":") valueStart += 1; - while (valueStart < raw.length && /[ \t]/.test(raw[valueStart]!)) valueStart += 1; - - const comment = inlineCommentAt(raw.slice(valueStart)); - let valueEnd = valueStart + comment; - while (valueEnd > valueStart && /[ \t]/.test(raw[valueEnd - 1]!)) valueEnd -= 1; - if (format === "key-value" && raw[valueEnd - 1] === ";") { - valueEnd -= 1; - while (valueEnd > valueStart && /[ \t]/.test(raw[valueEnd - 1]!)) valueEnd -= 1; - } - const quote = raw[valueStart]; - let valueQuote: '"' | "'" | undefined; - if ( - (quote === '"' || quote === "'") && - valueEnd > valueStart && - raw[valueEnd - 1] === quote - ) { - valueQuote = quote; - valueStart += 1; - valueEnd -= 1; - } - const localKey = raw.slice(keyStart, keyEnd).trim(); - return { - key: section ? `${section}.${localKey}` : localKey, - value: decodeQuotedValue(raw.slice(valueStart, valueEnd), valueQuote), - valueSpan: { start: start + valueStart, end: start + valueEnd }, - line, - ...(valueQuote ? { quote: valueQuote } : {}), - }; -}; - -const tupleEntries = (entry: IniEntry): IniEntry[] => { - if (!entry.value.startsWith("(") || !entry.value.endsWith(")")) return []; - const inner = entry.value.slice(1, -1); - const segments: Array<{ start: number; end: number }> = []; - let segmentStart = 0; - let depth = 0; - let quote: '"' | "'" | undefined; - for (let index = 0; index <= inner.length; index += 1) { - const character = inner[index]; - if (quote) { - if (character === quote && inner[index - 1] !== "\\") quote = undefined; - continue; - } - if (character === '"' || character === "'") { - quote = character; - continue; - } - if (character === "(" || character === "[" || character === "{") depth += 1; - else if (character === ")" || character === "]" || character === "}") depth -= 1; - if ((character === "," && depth === 0) || index === inner.length) { - segments.push({ start: segmentStart, end: index }); - segmentStart = index + 1; - } - } - - const result: IniEntry[] = []; - for (const segment of segments) { - const raw = inner.slice(segment.start, segment.end); - let equals = -1; - depth = 0; - quote = undefined; - for (let index = 0; index < raw.length; index += 1) { - const character = raw[index]!; - if (quote) { - if (character === quote && raw[index - 1] !== "\\") quote = undefined; - continue; - } - if (character === '"' || character === "'") quote = character; - else if (character === "(" || character === "[" || character === "{") depth += 1; - else if (character === ")" || character === "]" || character === "}") depth -= 1; - else if (character === "=" && depth === 0) { - equals = index; - break; - } - } - if (equals < 1) continue; - const key = raw.slice(0, equals).trim(); - let localStart = equals + 1; - while (localStart < raw.length && /\s/.test(raw[localStart]!)) localStart += 1; - let localEnd = raw.length; - while (localEnd > localStart && /\s/.test(raw[localEnd - 1]!)) localEnd -= 1; - const valueQuote = raw[localStart]; - let preservedQuote: '"' | "'" | undefined; - if ( - (valueQuote === '"' || valueQuote === "'") && - localEnd > localStart && - raw[localEnd - 1] === valueQuote - ) { - preservedQuote = valueQuote; - localStart += 1; - localEnd -= 1; - } - const absoluteStart = entry.valueSpan.start + 1 + segment.start + localStart; - result.push({ - key: `${entry.key}.${key}`, - value: decodeQuotedValue(raw.slice(localStart, localEnd), preservedQuote), - valueSpan: { - start: absoluteStart, - end: absoluteStart + localEnd - localStart, - }, - line: entry.line, - ...(preservedQuote ? { quote: preservedQuote } : {}), - }); - } - return result; -}; - -const parseIni = ( - source: string, - format: IniDocument["format"], -): IniDocument => { - const lines = splitSourceLines(source); - const entries: IniEntry[] = []; - const sections: IniSection[] = []; - const nodes: LineNode[] = []; - let section: string | undefined; - - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]!; - const trimmed = line.raw.trimStart(); - if (trimmed === "") { - nodes.push({ ...line, kind: "blank" }); - continue; - } - if ( - trimmed.startsWith(";") || - trimmed.startsWith("#") || - trimmed.startsWith("//") - ) { - nodes.push({ ...line, kind: "comment" }); - continue; - } - - const sectionMatch = format === "key-value" ? null : trimmed.match(/^\[([^\]]+)](?:\s*(?:[;#].*)?)$/); - if (sectionMatch) { - section = sectionMatch[1]!.trim(); - sections.push({ name: section, line: index }); - nodes.push({ ...line, kind: "section" }); - continue; - } - - const entry = parseEntryLine(line.raw, line.start, section, index, format); - if (entry) { - entries.push(entry); - if (format === "unreal-ini") { - const children = tupleEntries(entry); - if (children.length > 0) { - entry.container = true; - entries.push(...children); - } - } - nodes.push({ - ...line, - kind: "entry", - key: entry.key, - value: entry.value, - valueSpan: entry.valueSpan, - }); - } else nodes.push({ ...line, kind: "invalid" }); - } - - return { - format, - source, - nodes, - entries, - sections, - issues: [], - lineEnding: preferredLineEnding(lines), - finalNewline: hasFinalNewline(source), - }; -}; - -const effectiveEntry = (document: IniDocument, key: string): IniEntry | undefined => { - for (let index = document.entries.length - 1; index >= 0; index -= 1) { - const entry = document.entries[index]!; - if (entry.key === key) return entry; - } - return undefined; -}; - -const formatValue = (value: ConfigValue, quote?: '"' | "'"): string => { - let formatted = (value === null ? "" : String(value)).replace( - /[\r\n]/g, - (character) => character === "\r" ? "\\r" : "\\n", - ); - if (quote === '"') formatted = formatted.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); - else if (quote === "'") formatted = formatted.replace(/'/g, "'\\''"); - return formatted; -}; - -const appendIniEntry = ( - document: IniDocument, - key: string, - value: ConfigValue, -): string => { - const split = document.format === "key-value" ? -1 : key.lastIndexOf("."); - const section = split < 0 ? undefined : key.slice(0, split); - const localKey = split < 0 ? key : key.slice(split + 1); - const assignment = `${localKey}=${formatValue(value)}`; - const prefix = document.source === "" || document.finalNewline ? "" : document.lineEnding; - const suffix = document.finalNewline ? document.lineEnding : ""; - - if (!section || document.sections.some((candidate) => candidate.name === section)) { - return `${document.source}${prefix}${assignment}${suffix}`; - } - const sectionHeader = `[${section}]${document.lineEnding}`; - return `${document.source}${prefix}${sectionHeader}${assignment}${suffix}`; -}; - -const createIniAdapter = ( - format: IniDocument["format"], -): ConfigAdapter => ({ - parse(source) { - return parseIni(source, format); - }, - entries(document) { - const entries = new Map(); - for (const entry of document.entries) { - if (!entry.container) entries.set(entry.key, entry.value); - } - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveEntry(document, key)?.value; - }, - getAll(document, key) { - return document.entries - .filter((entry) => !entry.container && entry.key === key) - .map((entry) => entry.value); - }, - getAllRaw(document, key) { - return document.entries - .filter((entry) => !entry.container && entry.key === key) - .map((entry) => document.source.slice(entry.valueSpan.start, entry.valueSpan.end)); - }, - set(document, key, value) { - const entry = effectiveEntry(document, key); - if (!entry) return parseIni(appendIniEntry(document, key, value), format); - return parseIni( - document.source.slice(0, entry.valueSpan.start) + - formatValue(value, entry.quote) + - document.source.slice(entry.valueSpan.end), - format, - ); - }, - setAll(document, key, values) { - const entries = document.entries.filter( - (entry) => !entry.container && entry.key === key, - ); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} values for setting "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]!; - source = - source.slice(0, entry.valueSpan.start) + - formatValue(values[index]!, entry.quote) + - source.slice(entry.valueSpan.end); - } - return parseIni(source, format); - }, - setAllRaw(document, key, values) { - const entries = document.entries.filter( - (entry) => !entry.container && entry.key === key, - ); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} raw values for setting "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]!; - source = source.slice(0, entry.valueSpan.start) + values[index]! + source.slice(entry.valueSpan.end); - } - return parseIni(source, format); - }, - validate(document, schema: FileSchema): ValidationIssue[] { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - }, -}); - -export const iniAdapter = createIniAdapter("ini"); -export const unrealIniAdapter = createIniAdapter("unreal-ini"); -export const keyValueAdapter = createIniAdapter("key-value"); - -export const isIniFormat = (format: ConfigFormat): boolean => - format === "ini" || format === "unreal-ini" || format === "key-value"; diff --git a/packages/config-editor/src/adapters/java-properties.test.ts b/packages/config-editor/src/adapters/java-properties.test.ts deleted file mode 100644 index 757c3a6..0000000 --- a/packages/config-editor/src/adapters/java-properties.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { javaPropertiesAdapter } from "./java-properties.js"; - -const source = - "#Minecraft server properties\r\nmax-players = 20\r\nunknown.future:key\\ value\r\n\r\n"; - -describe("javaPropertiesAdapter", () => { - it("serializes an untouched properties file byte-for-byte", () => { - const document = javaPropertiesAdapter.parse(source); - expect(javaPropertiesAdapter.serialize(document)).toBe(source); - }); - - it("changes only the selected value token", () => { - const document = javaPropertiesAdapter.parse(source); - const changed = javaPropertiesAdapter.set(document, "max-players", 50); - expect(javaPropertiesAdapter.serialize(changed)).toBe( - "#Minecraft server properties\r\nmax-players = 50\r\nunknown.future:key\\ value\r\n\r\n", - ); - }); - - it("uses the last duplicate key as the effective value and retains every line", () => { - const duplicateSource = "motd=old\nmotd=new\n"; - const document = javaPropertiesAdapter.parse(duplicateSource); - expect(javaPropertiesAdapter.get(document, "motd")).toBe("new"); - expect( - javaPropertiesAdapter.serialize( - javaPropertiesAdapter.set(document, "motd", "final"), - ), - ).toBe("motd=old\nmotd=final\n"); - }); - - it.each([ - ["equals", "name=value\n", "name", "value"], - ["colon", "name:value\n", "name", "value"], - ["whitespace", "name value\n", "name", "value"], - ["empty", "name=\n", "name", ""], - ["escaped separator", "part\\=name=value\n", "part=name", "value"], - ["escaped space", "server\\ name=Druid\n", "server name", "Druid"], - ["unicode", "motd=Grüße 世界\n", "motd", "Grüße 世界"], - ["unicode escape", "motd=Hello\\u0020World\n", "motd", "Hello World"], - ])("reads %s syntax", (_name, input, key, expected) => { - expect(javaPropertiesAdapter.get(javaPropertiesAdapter.parse(input), key)).toBe( - expected, - ); - }); - - it.each([ - "key=value", - "key=value\n", - "key=value\r\n", - "# comment\n! another\n\nkey=value\n", - "continued=first\\\n second\n", - ])("preserves untouched source %j", (input) => { - expect( - javaPropertiesAdapter.serialize(javaPropertiesAdapter.parse(input)), - ).toBe(input); - }); - - it("decodes a continued logical value without changing its source", () => { - const input = "motd=Hello \\\r\n Druid\\ World\r\n"; - const document = javaPropertiesAdapter.parse(input); - expect(javaPropertiesAdapter.get(document, "motd")).toBe( - "Hello Druid World", - ); - expect(javaPropertiesAdapter.serialize(document)).toBe(input); - }); - - it("appends a missing key using the existing line ending and final-newline policy", () => { - expect( - javaPropertiesAdapter.serialize( - javaPropertiesAdapter.set( - javaPropertiesAdapter.parse("motd=Druid\r\n"), - "max-players", - 20, - ), - ), - ).toBe("motd=Druid\r\nmax-players=20\r\n"); - - expect( - javaPropertiesAdapter.serialize( - javaPropertiesAdapter.set( - javaPropertiesAdapter.parse("motd=Druid"), - "max-players", - 20, - ), - ), - ).toBe("motd=Druid\nmax-players=20"); - }); - - it("escapes a newly appended key and string value safely", () => { - const changed = javaPropertiesAdapter.set( - javaPropertiesAdapter.parse(""), - "server name", - "Druid\\Realm\nLine", - ); - expect(javaPropertiesAdapter.serialize(changed)).toBe( - "server\\ name=Druid\\\\Realm\\nLine", - ); - expect(javaPropertiesAdapter.get(changed, "server name")).toBe( - "Druid\\Realm\nLine", - ); - }); -}); diff --git a/packages/config-editor/src/adapters/java-properties.ts b/packages/config-editor/src/adapters/java-properties.ts deleted file mode 100644 index ad4db67..0000000 --- a/packages/config-editor/src/adapters/java-properties.ts +++ /dev/null @@ -1,311 +0,0 @@ -import type { ConfigValue, FileSchema, ValidationIssue } from "../model.js"; -import { hasFinalNewline, preferredLineEnding, splitSourceLines } from "./line-document.js"; -import type { - ConfigAdapter, - LineNode, - ParsedDocument, - SourceLine, - SourceSpan, -} from "./types.js"; - -interface JavaPropertiesEntry { - key: string; - value: string; - valueSpan: SourceSpan; - firstLine: number; - lastLine: number; -} - -export interface JavaPropertiesDocument extends ParsedDocument { - format: "java-properties"; - entries: readonly JavaPropertiesEntry[]; - lineEnding: "\n" | "\r\n"; - finalNewline: boolean; -} - -interface LogicalLine { - text: string; - offsets: number[]; - end: number; - firstLine: number; - lastLine: number; -} - -const trailingBackslashes = (value: string): number => { - let count = 0; - for (let index = value.length - 1; index >= 0 && value[index] === "\\"; index -= 1) { - count += 1; - } - return count; -}; - -const logicalLineAt = ( - lines: readonly SourceLine[], - firstLine: number, -): LogicalLine => { - let text = ""; - const offsets: number[] = []; - let lineIndex = firstLine; - let continued = false; - - for (; lineIndex < lines.length; lineIndex += 1) { - const line = lines[lineIndex]!; - const trim = continued ? line.raw.match(/^[ \t\f]*/)?.[0].length ?? 0 : 0; - const content = line.raw.slice(trim); - const continues = trailingBackslashes(content) % 2 === 1; - const logicalContent = continues ? content.slice(0, -1) : content; - - for (let index = 0; index < logicalContent.length; index += 1) { - text += logicalContent[index]!; - offsets.push(line.start + trim + index); - } - - continued = continues; - if (!continues) break; - } - - const lastLine = Math.min(lineIndex, lines.length - 1); - return { - text, - offsets, - end: lines[lastLine]?.end ?? 0, - firstLine, - lastLine, - }; -}; - -const isEscaped = (value: string, index: number): boolean => { - let slashes = 0; - for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) { - slashes += 1; - } - return slashes % 2 === 1; -}; - -const decodeEscapes = (value: string): string => { - let decoded = ""; - for (let index = 0; index < value.length; index += 1) { - const character = value[index]!; - if (character !== "\\" || index === value.length - 1) { - decoded += character; - continue; - } - - const escaped = value[(index += 1)]!; - if (escaped === "t") decoded += "\t"; - else if (escaped === "n") decoded += "\n"; - else if (escaped === "r") decoded += "\r"; - else if (escaped === "f") decoded += "\f"; - else if (escaped === "u") { - while (value[index + 1] === "u") index += 1; - const hexadecimal = value.slice(index + 1, index + 5); - if (/^[\da-f]{4}$/i.test(hexadecimal)) { - decoded += String.fromCharCode(Number.parseInt(hexadecimal, 16)); - index += 4; - } else { - decoded += "u"; - } - } else decoded += escaped; - } - return decoded; -}; - -const parseEntry = (logical: LogicalLine): JavaPropertiesEntry | undefined => { - const { text } = logical; - let start = 0; - while (start < text.length && /[ \t\f]/.test(text[start]!)) start += 1; - if (start === text.length || text[start] === "#" || text[start] === "!") { - return undefined; - } - - let keyEnd = start; - while (keyEnd < text.length) { - const character = text[keyEnd]!; - if (!isEscaped(text, keyEnd) && (character === "=" || character === ":" || /[ \t\f]/.test(character))) { - break; - } - keyEnd += 1; - } - - let valueStart = keyEnd; - while (valueStart < text.length && /[ \t\f]/.test(text[valueStart]!)) { - valueStart += 1; - } - if (valueStart < text.length && (text[valueStart] === "=" || text[valueStart] === ":")) { - valueStart += 1; - } - while (valueStart < text.length && /[ \t\f]/.test(text[valueStart]!)) { - valueStart += 1; - } - - return { - key: decodeEscapes(text.slice(start, keyEnd)), - value: decodeEscapes(text.slice(valueStart)), - valueSpan: { - start: logical.offsets[valueStart] ?? logical.end, - end: logical.end, - }, - firstLine: logical.firstLine, - lastLine: logical.lastLine, - }; -}; - -const classifyNodes = ( - lines: readonly SourceLine[], - entries: readonly JavaPropertiesEntry[], -): LineNode[] => { - const firstLines = new Map(entries.map((entry) => [entry.firstLine, entry])); - const continuationLines = new Set(); - for (const entry of entries) { - for (let index = entry.firstLine + 1; index <= entry.lastLine; index += 1) { - continuationLines.add(index); - } - } - - return lines.map((line, index) => { - const entry = firstLines.get(index); - if (entry) { - return { - ...line, - kind: "entry", - key: entry.key, - value: entry.value, - valueSpan: entry.valueSpan, - }; - } - if (continuationLines.has(index)) return { ...line, kind: "continuation" }; - const trimmed = line.raw.trimStart(); - if (trimmed === "") return { ...line, kind: "blank" }; - if (trimmed.startsWith("#") || trimmed.startsWith("!")) { - return { ...line, kind: "comment" }; - } - return { ...line, kind: "invalid" }; - }); -}; - -const parseJavaProperties = (source: string): JavaPropertiesDocument => { - const lines = splitSourceLines(source); - const entries: JavaPropertiesEntry[] = []; - - for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { - const logical = logicalLineAt(lines, lineIndex); - const entry = parseEntry(logical); - if (entry) entries.push(entry); - lineIndex = logical.lastLine; - } - - return { - format: "java-properties", - source, - nodes: classifyNodes(lines, entries), - entries, - issues: [], - lineEnding: preferredLineEnding(lines), - finalNewline: hasFinalNewline(source), - }; -}; - -const escapeKey = (value: string): string => - value.replace(/[\\ \t\f=:#!]/g, (character) => { - if (character === "\t") return "\\t"; - if (character === "\f") return "\\f"; - return `\\${character}`; - }); - -const escapeValue = (value: ConfigValue): string => { - const text = value === null ? "" : String(value); - return text.replace(/[\\\t\n\r\f]/g, (character) => { - if (character === "\\") return "\\\\"; - if (character === "\t") return "\\t"; - if (character === "\n") return "\\n"; - if (character === "\r") return "\\r"; - return "\\f"; - }).replace(/^ +/, (spaces) => "\\ ".repeat(spaces.length)); -}; - -const effectiveEntry = ( - document: JavaPropertiesDocument, - key: string, -): JavaPropertiesEntry | undefined => { - for (let index = document.entries.length - 1; index >= 0; index -= 1) { - const entry = document.entries[index]!; - if (entry.key === key) return entry; - } - return undefined; -}; - -const appendEntry = ( - document: JavaPropertiesDocument, - key: string, - value: ConfigValue, -): string => { - const entry = `${escapeKey(key)}=${escapeValue(value)}`; - if (document.source === "") return entry; - if (document.finalNewline) return `${document.source}${entry}${document.lineEnding}`; - return `${document.source}${document.lineEnding}${entry}`; -}; - -export const javaPropertiesAdapter: ConfigAdapter = { - parse: parseJavaProperties, - entries(document) { - const entries = new Map(); - for (const entry of document.entries) entries.set(entry.key, entry.value); - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveEntry(document, key)?.value; - }, - getAll(document, key) { - return document.entries - .filter((entry) => entry.key === key) - .map((entry) => entry.value); - }, - getAllRaw(document, key) { - return document.entries - .filter((entry) => entry.key === key) - .map((entry) => document.source.slice(entry.valueSpan.start, entry.valueSpan.end)); - }, - set(document, key, value) { - const entry = effectiveEntry(document, key); - if (!entry) return parseJavaProperties(appendEntry(document, key, value)); - const source = - document.source.slice(0, entry.valueSpan.start) + - escapeValue(value) + - document.source.slice(entry.valueSpan.end); - return parseJavaProperties(source); - }, - setAll(document, key, values) { - const entries = document.entries.filter((entry) => entry.key === key); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} values for Java property "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]!; - source = - source.slice(0, entry.valueSpan.start) + - escapeValue(values[index]!) + - source.slice(entry.valueSpan.end); - } - return parseJavaProperties(source); - }, - setAllRaw(document, key, values) { - const entries = document.entries.filter((entry) => entry.key === key); - if (entries.length !== values.length) { - throw new Error(`Expected ${entries.length} raw values for Java property "${key}".`); - } - let source = document.source; - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]!; - source = source.slice(0, entry.valueSpan.start) + values[index]! + source.slice(entry.valueSpan.end); - } - return parseJavaProperties(source); - }, - validate(document, schema: FileSchema): ValidationIssue[] { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - }, -}; diff --git a/packages/config-editor/src/adapters/json.test.ts b/packages/config-editor/src/adapters/json.test.ts deleted file mode 100644 index c7dd34e..0000000 --- a/packages/config-editor/src/adapters/json.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { jsonAdapter } from "./json.js"; - -describe("jsonAdapter", () => { - it("updates a JSON scalar without reformatting siblings", () => { - const source = - '{\n "server": { "name": "Old", "players": 20 },\n "future": true\n}\n'; - const changed = jsonAdapter.set( - jsonAdapter.parse(source), - "/server/players", - 50, - ); - expect(jsonAdapter.serialize(changed)).toBe( - '{\n "server": { "name": "Old", "players": 50 },\n "future": true\n}\n', - ); - expect(jsonAdapter.get(changed, "/server/name")).toBe("Old"); - }); - - it("supports escaped JSON pointer segments and array indexes", () => { - const document = jsonAdapter.parse( - '{"a/b":{"~key":[false,null,"value"]}}', - ); - expect(jsonAdapter.get(document, "/a~1b/~0key/0")).toBe(false); - expect(jsonAdapter.get(document, "/a~1b/~0key/1")).toBe(null); - expect(jsonAdapter.get(document, "/a~1b/~0key/2")).toBe("value"); - }); - - it("reports invalid JSON with a line and column", () => { - const document = jsonAdapter.parse('{\n "broken":,\n}\n'); - expect(document.issues).toEqual([ - expect.objectContaining({ - code: "invalid-json", - severity: "error", - line: 2, - column: 12, - }), - ]); - }); - - it("rejects writes to objects and properties whose parent is missing", () => { - const document = jsonAdapter.parse('{"server":{"players":20}}'); - expect(() => jsonAdapter.set(document, "/server", "invalid")).toThrow( - /scalar/, - ); - expect(() => jsonAdapter.set(document, "/missing/nested", 1)).toThrow(/pointer/); - }); - - it("adds missing scalar properties without reformatting existing JSON", () => { - const source = '{\n "server": {\n "name": "Druid"\n }\n}\n'; - const changed = jsonAdapter.set(jsonAdapter.parse(source), "/server/port", 5520); - - expect(jsonAdapter.serialize(changed)).toBe( - '{\n "server": {\n "name": "Druid",\n "port": 5520\n }\n}\n', - ); - expect(jsonAdapter.get(changed, "/server/port")).toBe(5520); - }); - - it("preserves CRLF line endings while adding a missing property", () => { - const source = "{\r\n \"server\": {\r\n \"name\": \"Druid\"\r\n }\r\n}\r\n"; - const changed = jsonAdapter.set(jsonAdapter.parse(source), "/server/port", 5520); - - expect(jsonAdapter.serialize(changed)).toBe( - "{\r\n \"server\": {\r\n \"name\": \"Druid\",\r\n \"port\": 5520\r\n }\r\n}\r\n", - ); - }); - - it("keeps unsafe JSON integer identifiers exact and editable as strings", () => { - const source = '{"steamId":765611980123456789}\n'; - const document = jsonAdapter.parse(source); - expect(jsonAdapter.get(document, "/steamId")).toBe("765611980123456789"); - expect(jsonAdapter.serialize(document)).toBe(source); - - const changed = jsonAdapter.set(document, "/steamId", "765611980123456780"); - expect(jsonAdapter.serialize(changed)).toBe('{"steamId":765611980123456780}\n'); - }); -}); diff --git a/packages/config-editor/src/adapters/json.ts b/packages/config-editor/src/adapters/json.ts deleted file mode 100644 index db4f5ca..0000000 --- a/packages/config-editor/src/adapters/json.ts +++ /dev/null @@ -1,331 +0,0 @@ -import type { ConfigValue, FileSchema, ValidationIssue } from "../model.js"; -import { splitSourceLines } from "./line-document.js"; -import type { ConfigAdapter, LineNode, ParsedDocument, SourceSpan } from "./types.js"; - -interface JsonScalar { - value: ConfigValue; - span: SourceSpan; - unsafeInteger?: boolean; -} - -interface JsonContainer { - kind: "object" | "array"; - open: number; - close: number; -} - -export interface JsonDocument extends ParsedDocument { - format: "json"; - scalars: ReadonlyMap; - scalarOccurrences: ReadonlyMap; - containers: ReadonlyMap; -} - -class JsonParseError extends Error { - readonly position: number; - - constructor(position: number, message: string) { - super(message); - this.position = position; - } -} - -const pointerSegment = (value: string): string => - value.replace(/~/g, "~0").replace(/\//g, "~1"); - -const locationAt = (source: string, position: number): { line: number; column: number } => { - const before = source.slice(0, position); - const lines = before.split("\n"); - return { line: lines.length, column: lines.at(-1)!.length + 1 }; -}; - -const jsonNodes = (source: string): LineNode[] => - splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid", - })); - -const parseJson = (source: string): JsonDocument => { - const scalars = new Map(); - const scalarOccurrences = new Map(); - const containers = new Map(); - let cursor = 0; - const addScalar = (pointer: string, scalar: JsonScalar): void => { - scalars.set(pointer, scalar); - const occurrences = scalarOccurrences.get(pointer) ?? []; - occurrences.push(scalar); - scalarOccurrences.set(pointer, occurrences); - }; - - const fail = (message: string): never => { - throw new JsonParseError(cursor, message); - }; - const skipWhitespace = (): void => { - while (cursor < source.length && /\s/.test(source[cursor]!)) cursor += 1; - }; - const consume = (character: string): void => { - if (source[cursor] !== character) fail(`Expected "${character}".`); - cursor += 1; - }; - const parseString = (): { value: string; span: SourceSpan } => { - const start = cursor; - consume('"'); - while (cursor < source.length) { - const character = source[cursor]!; - if (character === '"') { - cursor += 1; - const token = source.slice(start, cursor); - try { - return { value: JSON.parse(token) as string, span: { start, end: cursor } }; - } catch { - fail("Invalid JSON string."); - } - } - if (character === "\\") { - cursor += 2; - continue; - } - if (character.charCodeAt(0) < 0x20) fail("Unescaped control character."); - cursor += 1; - } - return fail("Unterminated JSON string."); - }; - const parseValue = (pointer: string): void => { - skipWhitespace(); - const start = cursor; - const character = source[cursor]; - if (character === '"') { - const string = parseString(); - addScalar(pointer, { value: string.value, span: string.span }); - return; - } - if (character === "{") { - const open = cursor; - cursor += 1; - skipWhitespace(); - if (source[cursor] === "}") { - containers.set(pointer, { kind: "object", open, close: cursor }); - cursor += 1; - return; - } - while (cursor < source.length) { - skipWhitespace(); - if (source[cursor] !== '"') fail("Expected an object key."); - const key = parseString().value; - skipWhitespace(); - consume(":"); - parseValue(`${pointer}/${pointerSegment(key)}`); - skipWhitespace(); - if (source[cursor] === "}") { - containers.set(pointer, { kind: "object", open, close: cursor }); - cursor += 1; - return; - } - consume(","); - } - fail("Unterminated JSON object."); - } - if (character === "[") { - const open = cursor; - cursor += 1; - skipWhitespace(); - if (source[cursor] === "]") { - containers.set(pointer, { kind: "array", open, close: cursor }); - cursor += 1; - return; - } - let index = 0; - while (cursor < source.length) { - parseValue(`${pointer}/${index}`); - index += 1; - skipWhitespace(); - if (source[cursor] === "]") { - containers.set(pointer, { kind: "array", open, close: cursor }); - cursor += 1; - return; - } - consume(","); - } - fail("Unterminated JSON array."); - } - - const remainder = source.slice(cursor); - const literal = remainder.match(/^(?:true|false|null)(?![\w])/); - if (literal) { - cursor += literal[0].length; - addScalar(pointer, { - value: JSON.parse(literal[0]) as ConfigValue, - span: { start, end: cursor }, - }); - return; - } - const number = remainder.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/); - if (number) { - cursor += number[0].length; - const unsafeInteger = /^-?\d+$/.test(number[0]) && - !Number.isSafeInteger(Number(number[0])); - addScalar(pointer, { - value: unsafeInteger ? number[0] : Number(number[0]), - span: { start, end: cursor }, - ...(unsafeInteger ? { unsafeInteger: true } : {}), - }); - return; - } - fail("Expected a JSON scalar, object, or array."); - }; - - try { - parseValue(""); - skipWhitespace(); - if (cursor !== source.length) fail("Unexpected content after the JSON value."); - return { - format: "json", - source, - nodes: jsonNodes(source), - issues: [], - scalars, - scalarOccurrences, - containers, - }; - } catch (error) { - if (!(error instanceof JsonParseError)) throw error; - const location = locationAt(source, error.position); - return { - format: "json", - source, - nodes: jsonNodes(source), - issues: [ - { - code: "invalid-json", - message: error.message, - severity: "error", - ...location, - }, - ], - scalars: new Map(), - scalarOccurrences: new Map(), - containers: new Map(), - }; - } -}; - -const decodePointerSegment = (value: string): string => - value.replace(/~1/g, "/").replace(/~0/g, "~"); - -const insertJsonProperty = ( - document: JsonDocument, - pointer: string, - value: ConfigValue, -): JsonDocument => { - const separator = pointer.lastIndexOf("/"); - if (separator < 0) throw new Error(`JSON pointer "${pointer}" does not exist.`); - const parentPointer = pointer.slice(0, separator); - const property = decodePointerSegment(pointer.slice(separator + 1)); - const parent = document.containers.get(parentPointer); - if (!parent || parent.kind !== "object") { - throw new Error(`JSON pointer "${pointer}" does not exist.`); - } - - const content = document.source.slice(parent.open + 1, parent.close); - const contentEnd = parent.close - (content.match(/\s*$/)?.[0].length ?? 0); - const closingLine = document.source.slice(0, parent.close).split("\n").at(-1) ?? ""; - const closingIndent = closingLine.match(/^\s*/)?.[0] ?? ""; - const serialized = `${JSON.stringify(property)}: ${JSON.stringify(value)}`; - const newline = document.source.includes("\r\n") ? "\r\n" : "\n"; - let insertion: string; - if (content.trim() === "") { - insertion = `${newline}${closingIndent} ${serialized}${newline}${closingIndent}`; - return parseJson( - document.source.slice(0, parent.open + 1) + - insertion + - document.source.slice(parent.close), - ); - } - - const multiline = content.includes("\n"); - if (!multiline) insertion = `, ${serialized}`; - else { - const existingLine = document.source.slice(0, contentEnd).split("\n").at(-1) ?? ""; - const childIndent = existingLine.match(/^\s*/)?.[0] ?? `${closingIndent} `; - insertion = `,${newline}${childIndent}${serialized}`; - } - return parseJson( - document.source.slice(0, contentEnd) + - insertion + - document.source.slice(contentEnd), - ); -}; - -export const jsonAdapter: ConfigAdapter = { - parse: parseJson, - entries(document) { - return [...document.scalars].map(([key, scalar]) => ({ - key, - value: scalar.value, - })); - }, - get(document, key) { - return document.scalars.get(key)?.value; - }, - getAll(document, key) { - return (document.scalarOccurrences.get(key) ?? []).map((scalar) => scalar.value); - }, - getAllRaw(document, key) { - return (document.scalarOccurrences.get(key) ?? []).map((scalar) => - document.source.slice(scalar.span.start, scalar.span.end) - ); - }, - set(document, key, value) { - if (document.issues.length > 0) throw new Error("Invalid JSON cannot be edited in form mode."); - const scalar = document.scalars.get(key); - if (!scalar) { - if (document.containers.has(key)) throw new Error(`JSON pointer "${key}" is not a scalar.`); - return insertJsonProperty(document, key, value); - } - const serialized = scalar.unsafeInteger && typeof value === "string" && /^-?\d+$/.test(value) - ? value - : JSON.stringify(value); - return parseJson( - document.source.slice(0, scalar.span.start) + - serialized + - document.source.slice(scalar.span.end), - ); - }, - setAll(document, key, values) { - const occurrences = document.scalarOccurrences.get(key) ?? []; - const expected = occurrences.length; - if (values.length !== expected) { - throw new Error(`Expected ${expected} values for JSON pointer "${key}".`); - } - let source = document.source; - for (let index = occurrences.length - 1; index >= 0; index -= 1) { - const scalar = occurrences[index]!; - const value = values[index]!; - const serialized = scalar.unsafeInteger && typeof value === "string" && /^-?\d+$/.test(value) - ? value - : JSON.stringify(value); - source = source.slice(0, scalar.span.start) + serialized + source.slice(scalar.span.end); - } - return parseJson(source); - }, - setAllRaw(document, key, values) { - const occurrences = document.scalarOccurrences.get(key) ?? []; - const expected = occurrences.length; - if (values.length !== expected) { - throw new Error(`Expected ${expected} raw values for JSON pointer "${key}".`); - } - let source = document.source; - for (let index = occurrences.length - 1; index >= 0; index -= 1) { - const scalar = occurrences[index]!; - source = source.slice(0, scalar.span.start) + values[index]! + source.slice(scalar.span.end); - } - return parseJson(source); - }, - validate(document, schema: FileSchema): ValidationIssue[] { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - }, -}; diff --git a/packages/config-editor/src/adapters/line-document.ts b/packages/config-editor/src/adapters/line-document.ts deleted file mode 100644 index d688d7c..0000000 --- a/packages/config-editor/src/adapters/line-document.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { SourceLine } from "./types.js"; - -export const splitSourceLines = (source: string): SourceLine[] => { - const lines: SourceLine[] = []; - let start = 0; - - while (start < source.length) { - const newline = source.indexOf("\n", start); - if (newline === -1) { - lines.push({ raw: source.slice(start), ending: "", start, end: source.length }); - break; - } - - const hasCarriageReturn = newline > start && source[newline - 1] === "\r"; - const rawEnd = hasCarriageReturn ? newline - 1 : newline; - lines.push({ - raw: source.slice(start, rawEnd), - ending: hasCarriageReturn ? "\r\n" : "\n", - start, - end: rawEnd, - }); - start = newline + 1; - } - - return lines; -}; - -export const preferredLineEnding = ( - lines: readonly SourceLine[], -): "\n" | "\r\n" => - lines.find((line) => line.ending !== "")?.ending || "\n"; - -export const hasFinalNewline = (source: string): boolean => source.endsWith("\n"); diff --git a/packages/config-editor/src/adapters/raw.test.ts b/packages/config-editor/src/adapters/raw.test.ts deleted file mode 100644 index 2578d82..0000000 --- a/packages/config-editor/src/adapters/raw.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { rawAdapter } from "./raw.js"; - -describe("rawAdapter", () => { - it("round-trips arbitrary content byte-for-byte", () => { - const source = "arbitrary\r\n\0content\n"; - expect(rawAdapter.serialize(rawAdapter.parse(source))).toBe(source); - }); - - it("blocks typed writes through the raw fallback", () => { - const document = rawAdapter.parse("arbitrary content\n"); - expect(() => rawAdapter.set(document, "key", "value")).toThrow(/Raw mode/); - }); -}); diff --git a/packages/config-editor/src/adapters/raw.ts b/packages/config-editor/src/adapters/raw.ts deleted file mode 100644 index ba5824b..0000000 --- a/packages/config-editor/src/adapters/raw.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { ConfigValue, FileSchema, ValidationIssue } from "../model.js"; -import { splitSourceLines } from "./line-document.js"; -import type { ConfigAdapter, LineNode, ParsedDocument } from "./types.js"; - -export interface RawDocument extends ParsedDocument { - format: "raw"; -} - -const parseRaw = (source: string): RawDocument => { - const nodes: LineNode[] = splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid", - })); - return { format: "raw", source, nodes, issues: [] }; -}; - -export const rawAdapter: ConfigAdapter = { - parse: parseRaw, - entries() { - return []; - }, - get() { - return undefined; - }, - getAll() { - return []; - }, - getAllRaw() { - return []; - }, - set(_document, _key, _value: ConfigValue) { - throw new Error("Raw mode does not support typed field writes."); - }, - setAll(document, _key, values) { - if (values.length > 0) throw new Error("Raw mode does not support typed field writes."); - return document; - }, - setAllRaw(document, _key, values) { - if (values.length > 0) throw new Error("Raw mode does not support typed field writes."); - return document; - }, - validate(document, schema: FileSchema): ValidationIssue[] { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - }, -}; diff --git a/packages/config-editor/src/adapters/registry.test.ts b/packages/config-editor/src/adapters/registry.test.ts deleted file mode 100644 index 4718175..0000000 --- a/packages/config-editor/src/adapters/registry.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createAdapterRegistry } from "./registry.js"; - -describe("createAdapterRegistry", () => { - it("provides an adapter for every supported manifest format", () => { - const registry = createAdapterRegistry(); - expect([...registry.keys()]).toEqual([ - "java-properties", - "ini", - "unreal-ini", - "key-value", - "json", - "xml-properties", - "raw", - ]); - }); -}); diff --git a/packages/config-editor/src/adapters/registry.ts b/packages/config-editor/src/adapters/registry.ts deleted file mode 100644 index 6a798e7..0000000 --- a/packages/config-editor/src/adapters/registry.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ConfigFormat } from "../model.js"; -import { iniAdapter, keyValueAdapter, unrealIniAdapter } from "./ini.js"; -import { javaPropertiesAdapter } from "./java-properties.js"; -import { jsonAdapter } from "./json.js"; -import { rawAdapter } from "./raw.js"; -import { xmlPropertiesAdapter } from "./xml-properties.js"; -import type { ConfigAdapter } from "./types.js"; - -export type AdapterRegistry = ReadonlyMap; - -export const createAdapterRegistry = (): AdapterRegistry => - new Map([ - ["java-properties", javaPropertiesAdapter], - ["ini", iniAdapter], - ["unreal-ini", unrealIniAdapter], - ["key-value", keyValueAdapter], - ["json", jsonAdapter], - ["xml-properties", xmlPropertiesAdapter], - ["raw", rawAdapter], - ]); diff --git a/packages/config-editor/src/adapters/types.ts b/packages/config-editor/src/adapters/types.ts deleted file mode 100644 index 5801552..0000000 --- a/packages/config-editor/src/adapters/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { - ConfigFormat, - ConfigValue, - FileSchema, - ValidationIssue, -} from "../model.js"; - -export interface SourceSpan { - start: number; - end: number; -} - -export interface SourceLine { - raw: string; - ending: "\n" | "\r\n" | ""; - start: number; - end: number; -} - -export interface LineNode extends SourceLine { - kind: - | "blank" - | "comment" - | "section" - | "entry" - | "continuation" - | "invalid"; - key?: string; - value?: string; - valueSpan?: SourceSpan; -} - -export interface ParsedDocument { - format: ConfigFormat; - source: string; - nodes: readonly LineNode[]; - issues: readonly ValidationIssue[]; -} - -export interface ConfigEntry { - key: string; - value: ConfigValue; -} - -export interface ConfigAdapter { - parse(source: string): TDocument; - entries(document: TDocument): readonly ConfigEntry[]; - get(document: TDocument, key: string): ConfigValue | undefined; - getAll(document: TDocument, key: string): readonly ConfigValue[]; - getAllRaw(document: TDocument, key: string): readonly string[]; - set(document: TDocument, key: string, value: ConfigValue): TDocument; - setAll( - document: TDocument, - key: string, - values: readonly ConfigValue[], - ): TDocument; - setAllRaw(document: TDocument, key: string, values: readonly string[]): TDocument; - validate(document: TDocument, schema: FileSchema): ValidationIssue[]; - serialize(document: TDocument): string; -} diff --git a/packages/config-editor/src/adapters/xml-properties.test.ts b/packages/config-editor/src/adapters/xml-properties.test.ts deleted file mode 100644 index 823e603..0000000 --- a/packages/config-editor/src/adapters/xml-properties.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { xmlPropertiesAdapter } from "./xml-properties.js"; - -describe("xmlPropertiesAdapter", () => { - it("discovers 7 Days to Die property elements", () => { - const document = xmlPropertiesAdapter.parse( - '\n \n \n\n', - ); - - expect(xmlPropertiesAdapter.entries(document)).toEqual([ - { key: "ServerName", value: "Druid" }, - { key: "MaxPlayerCount", value: "8" }, - ]); - }); - - it("changes only the selected XML attribute value", () => { - const source = '\n'; - const document = xmlPropertiesAdapter.parse(source); - const updated = xmlPropertiesAdapter.set(document, "ServerName", 'Druid "EU" & Friends'); - - expect(xmlPropertiesAdapter.get(document, "ServerName")).toBe("Druid & Friends"); - expect(xmlPropertiesAdapter.serialize(updated)).toBe( - '\n', - ); - }); - - it("uses the last duplicate as the effective setting", () => { - const document = xmlPropertiesAdapter.parse( - '\n\n', - ); - - expect(xmlPropertiesAdapter.get(document, "ServerName")).toBe("new"); - expect(xmlPropertiesAdapter.entries(document)).toEqual([ - { key: "ServerName", value: "new" }, - ]); - }); -}); diff --git a/packages/config-editor/src/adapters/xml-properties.ts b/packages/config-editor/src/adapters/xml-properties.ts deleted file mode 100644 index eba49db..0000000 --- a/packages/config-editor/src/adapters/xml-properties.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { ConfigValue, FileSchema, ValidationIssue } from "../model.js"; -import { splitSourceLines } from "./line-document.js"; -import type { ConfigAdapter, LineNode, ParsedDocument, SourceSpan } from "./types.js"; - -interface XmlProperty { - key: string; - value: string; - valueSpan: SourceSpan; - quote: '"' | "'"; -} - -export interface XmlPropertiesDocument extends ParsedDocument { - format: "xml-properties"; - properties: readonly XmlProperty[]; -} - -const decodeXml = (value: string): string => - value.replace(/&(?:#x[\da-f]+|#\d+|amp|quot|apos|lt|gt);/gi, (entity) => { - const token = entity.slice(1, -1); - if (token.toLowerCase().startsWith("#x")) { - return String.fromCodePoint(Number.parseInt(token.slice(2), 16)); - } - if (token.startsWith("#")) { - return String.fromCodePoint(Number.parseInt(token.slice(1), 10)); - } - return { amp: "&", quot: '"', apos: "'", lt: "<", gt: ">" }[ - token.toLowerCase() - ]!; - }); - -const encodeXml = (value: ConfigValue, quote: '"' | "'"): string => { - let encoded = (value === null ? "" : String(value)) - .replace(/&/g, "&") - .replace(//g, ">"); - encoded = quote === '"' - ? encoded.replace(/"/g, """) - : encoded.replace(/'/g, "'"); - return encoded; -}; - -const parseXmlProperties = (source: string): XmlPropertiesDocument => { - const properties: XmlProperty[] = []; - const propertyPattern = /]*>/gi; - for (const match of source.matchAll(propertyPattern)) { - const tag = match[0]; - const tagStart = match.index; - const attributes = new Map(); - const attributePattern = /\b(name|value)\s*=\s*(["'])(.*?)\2/gi; - for (const attribute of tag.matchAll(attributePattern)) { - const quote = attribute[2] as '"' | "'"; - const rawValue = attribute[3]!; - const rawStart = tagStart + attribute.index + attribute[0].indexOf(quote) + 1; - attributes.set(attribute[1]!.toLowerCase(), { - value: decodeXml(rawValue), - span: { start: rawStart, end: rawStart + rawValue.length }, - quote, - }); - } - const name = attributes.get("name"); - const value = attributes.get("value"); - if (name && value) { - properties.push({ - key: name.value, - value: value.value, - valueSpan: value.span, - quote: value.quote, - }); - } - } - const nodes: LineNode[] = splitSourceLines(source).map((line) => ({ - ...line, - kind: line.raw.trim() === "" ? "blank" : "invalid", - })); - return { format: "xml-properties", source, nodes, issues: [], properties }; -}; - -const effectiveProperty = ( - document: XmlPropertiesDocument, - key: string, -): XmlProperty | undefined => { - for (let index = document.properties.length - 1; index >= 0; index -= 1) { - const property = document.properties[index]!; - if (property.key === key) return property; - } - return undefined; -}; - -export const xmlPropertiesAdapter: ConfigAdapter = { - parse: parseXmlProperties, - entries(document) { - const entries = new Map(); - for (const property of document.properties) entries.set(property.key, property.value); - return [...entries].map(([key, value]) => ({ key, value })); - }, - get(document, key) { - return effectiveProperty(document, key)?.value; - }, - getAll(document, key) { - return document.properties - .filter((property) => property.key === key) - .map((property) => property.value); - }, - getAllRaw(document, key) { - return document.properties - .filter((property) => property.key === key) - .map((property) => document.source.slice(property.valueSpan.start, property.valueSpan.end)); - }, - set(document, key, value) { - const property = effectiveProperty(document, key); - if (!property) throw new Error(`XML property "${key}" does not exist.`); - return parseXmlProperties( - document.source.slice(0, property.valueSpan.start) + - encodeXml(value, property.quote) + - document.source.slice(property.valueSpan.end), - ); - }, - setAll(document, key, values) { - const properties = document.properties.filter((property) => property.key === key); - if (properties.length !== values.length) { - throw new Error(`Expected ${properties.length} values for XML property "${key}".`); - } - let source = document.source; - for (let index = properties.length - 1; index >= 0; index -= 1) { - const property = properties[index]!; - source = - source.slice(0, property.valueSpan.start) + - encodeXml(values[index]!, property.quote) + - source.slice(property.valueSpan.end); - } - return parseXmlProperties(source); - }, - setAllRaw(document, key, values) { - const properties = document.properties.filter((property) => property.key === key); - if (properties.length !== values.length) { - throw new Error(`Expected ${properties.length} raw values for XML property "${key}".`); - } - let source = document.source; - for (let index = properties.length - 1; index >= 0; index -= 1) { - const property = properties[index]!; - source = source.slice(0, property.valueSpan.start) + values[index]! + source.slice(property.valueSpan.end); - } - return parseXmlProperties(source); - }, - validate(document, schema: FileSchema): ValidationIssue[] { - void schema; - return [...document.issues]; - }, - serialize(document) { - return document.source; - }, -}; diff --git a/packages/config-editor/src/components/ActionBar.tsx b/packages/config-editor/src/components/ActionBar.tsx deleted file mode 100644 index efce411..0000000 --- a/packages/config-editor/src/components/ActionBar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { copy } from "../copy.js"; - -export interface ActionBarProps { - dirty: boolean; - invalid: boolean; - saving: boolean; - status: string; - onSave(): Promise | void; -} - -export const ActionBar = ({ dirty, invalid, saving, status, onSave }: ActionBarProps) => ( -
- {status} - -
-); diff --git a/packages/config-editor/src/components/EditorApp.tsx b/packages/config-editor/src/components/EditorApp.tsx deleted file mode 100644 index 126e5ac..0000000 --- a/packages/config-editor/src/components/EditorApp.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { createComponent, rerender } from "@druid-ui/component"; -import type { Context, Event } from "@druid-ui/component"; -import { copy } from "../copy.js"; -import type { FileGateway } from "../gateway.js"; -import { loadEditor, saveSelectedFile } from "../gateway.js"; -import { validateManifest } from "../manifest.js"; -import type { ConfigEditorManifest } from "../model.js"; -import type { ConfigEditorStore, EditorSnapshot } from "../store.js"; -import { EDITOR_STYLES } from "../styles.js"; -import { ActionBar } from "./ActionBar.js"; -import { FileRail } from "./FileRail.js"; -import { FormEditor } from "./FormEditor.js"; -import { Inspector } from "./Inspector.js"; -import { RawEditor } from "./RawEditor.js"; - -export type EditorMode = "form" | "raw"; - -export interface EditorAppProps { - manifest: ConfigEditorManifest; - store: ConfigEditorStore; - snapshot: EditorSnapshot; - mode: EditorMode; - saving: boolean; - status: string; - onMode(mode: EditorMode): void; - onSelect(path: string): Promise | void; - onField(key: string, value: string): void; - onRaw(source: string): void; - onSave(): Promise | void; -} - -export const EditorApp = ({ - manifest, - store, - snapshot, - mode, - saving, - status, - onMode, - onSelect, - onField, - onRaw, - onSave, -}: EditorAppProps) => ( -
- -
-
-
Druid Admin UI
-

{manifest.server.displayName}

-
-
- {manifest.server.appVersion ? `Version ${manifest.server.appVersion}` : copy.appTitle} -
-
-
- -
-
- - -
- {mode === "form" ? ( - - ) : ( - - )} - issue.severity === "error")} - saving={saving} - status={status} - onSave={onSave} - /> -
- -
-
-); - -export interface ConfigEditorComponentOptions { - manifestPath: string; - gateway: FileGateway; -} - -export interface ConfigEditorComponent { - init(context: Context): unknown; - emit(nodeId: string, event: string, value: Event): void; - asyncComplete( - id: string, - result: { tag: "ok" | "err"; val: unknown }, - ): void; -} - -export const createConfigEditorComponent = ({ - manifestPath, - gateway, -}: ConfigEditorComponentOptions): ConfigEditorComponent => { - let manifest: ConfigEditorManifest | undefined; - const stores = new Map(); - let selectedPath = ""; - let mode: EditorMode = "form"; - let loading = false; - let saving = false; - let status = ""; - let errorMessage = ""; - - const selectFile = async (path: string): Promise => { - if (!manifest) return; - const previousPath = selectedPath; - status = copy.loading; - rerender(); - try { - let store = stores.get(path); - if (!store) { - store = await loadEditor(gateway, manifest, path); - stores.set(path, store); - } - selectedPath = path; - mode = store.schema.sections.length === 0 ? "raw" : "form"; - status = ""; - } catch (error) { - selectedPath = previousPath; - status = error instanceof Error ? error.message : String(error); - if (!stores.has(previousPath)) throw error; - } finally { - rerender(); - } - }; - - const initialise = async (): Promise => { - loading = true; - try { - manifest = validateManifest(JSON.parse(await gateway.load(manifestPath))); - selectedPath = manifest.files[0]!.path; - await selectFile(selectedPath); - } catch (error) { - errorMessage = error instanceof Error ? error.message : String(error); - } finally { - loading = false; - rerender(); - } - }; - - const save = async (): Promise => { - const store = stores.get(selectedPath); - if (!store) return; - saving = true; - status = copy.saving; - try { - const result = await saveSelectedFile(store, gateway); - status = result.status === "saved" ? copy.saved : copy.conflict; - } catch (error) { - status = error instanceof Error ? error.message : String(error); - } finally { - saving = false; - rerender(); - } - }; - - return createComponent(() => { - if (!loading && !manifest && !errorMessage) void initialise(); - if (errorMessage) { - return ( -
- - -
- ); - } - const store = stores.get(selectedPath); - if (!manifest || !store) { - return ( -
- -
{copy.loading}
-
- ); - } - const snapshot = store.snapshot(); - return ( - { - mode = nextMode; - }} - onSelect={selectFile} - onField={(key, value) => { - store.setDisplayValue(key, value); - status = ""; - }} - onRaw={(source) => { - store.replaceWorkingSource(source); - status = ""; - }} - onSave={save} - /> - ); - }); -}; diff --git a/packages/config-editor/src/components/FieldControl.tsx b/packages/config-editor/src/components/FieldControl.tsx deleted file mode 100644 index 627ccbe..0000000 --- a/packages/config-editor/src/components/FieldControl.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { Event } from "@druid-ui/component"; -import type { FieldSchema } from "../model.js"; -import type { FieldSnapshot } from "../store.js"; -import { copy } from "../copy.js"; - -export interface FieldControlProps { - field: FieldSchema; - state: FieldSnapshot; - onChange(key: string, value: string): void; -} - -export const FieldControl = ({ field, state, onChange }: FieldControlProps) => { - const inputId = `config-field-${field.key.replace(/[^a-zA-Z0-9_-]/g, "-")}`; - const descriptionId = `${inputId}-description`; - const input = field.type === "enum" ? ( - - ) : field.type === "boolean" ? ( - - - onChange(field.key, event.checked() ? "true" : "false") - } - /> - {state.displayValue === "true" ? "Enabled" : "Disabled"} - - ) : ( - 0 ? "true" : "false"} - autocomplete={field.type === "secret" || field.sensitive ? "new-password" : "off"} - onInput={(event: Event) => onChange(field.key, event.value())} - /> - ); - - return ( -
- -

{field.description}

- {input} -
- {field.restartRequired ? {copy.restartRequired} : false} - {field.key} -
- {state.issues.map((issue) => ( - - ))} -
- ); -}; diff --git a/packages/config-editor/src/components/FileRail.tsx b/packages/config-editor/src/components/FileRail.tsx deleted file mode 100644 index fa85828..0000000 --- a/packages/config-editor/src/components/FileRail.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { ConfigEditorManifest } from "../model.js"; -import { copy } from "../copy.js"; - -export interface FileRailProps { - manifest: ConfigEditorManifest; - selectedPath: string; - onSelect(path: string): Promise | void; -} - -export const FileRail = ({ manifest, selectedPath, onSelect }: FileRailProps) => ( - -); diff --git a/packages/config-editor/src/components/FormEditor.tsx b/packages/config-editor/src/components/FormEditor.tsx deleted file mode 100644 index eda4153..0000000 --- a/packages/config-editor/src/components/FormEditor.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { FileSchema } from "../model.js"; -import type { EditorSnapshot } from "../store.js"; -import { FieldControl } from "./FieldControl.js"; - -export interface FormEditorProps { - schema: FileSchema; - snapshot: EditorSnapshot; - onChange(key: string, value: string): void; -} - -export const FormEditor = ({ schema, snapshot, onChange }: FormEditorProps) => ( -
- {schema.sections.map((section) => ( -
- {section.label} - {section.description ?

{section.description}

: false} -
- {section.fields.map((field) => { - const state = snapshot.fields[field.key]; - return state ? ( - - ) : false; - })} -
-
- ))} -
-); diff --git a/packages/config-editor/src/components/Inspector.tsx b/packages/config-editor/src/components/Inspector.tsx deleted file mode 100644 index 93fb3ed..0000000 --- a/packages/config-editor/src/components/Inspector.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { copy } from "../copy.js"; -import type { EditorSnapshot } from "../store.js"; - -export interface InspectorProps { - snapshot: EditorSnapshot; -} - -const printable = (value: unknown): string => - value === undefined ? "Not set" : value === null ? "Protected" : String(value); - -export const Inspector = ({ snapshot }: InspectorProps) => ( - -); diff --git a/packages/config-editor/src/components/RawEditor.tsx b/packages/config-editor/src/components/RawEditor.tsx deleted file mode 100644 index 17f9d73..0000000 --- a/packages/config-editor/src/components/RawEditor.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Event } from "@druid-ui/component"; -import { copy } from "../copy.js"; - -export interface RawEditorProps { - source: string; - onChange(source: string): void; -} - -export const RawEditor = ({ source, onChange }: RawEditorProps) => ( -
- -