From c05efb77da8f06cfb4844606cb519108abed747f Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:51:55 +0100 Subject: [PATCH 1/4] build: commit dist/ so a git-dependency install needs no build step pnpm/npm only run a git dependency's own build (the "prepare" script) from inside a node_modules/ checkout -- exactly the one path Node's native TypeScript-stripping refuses to run against a .ts file. Committing dist/ removes the need for that build step entirely: a git-dependency install gets already-built output straight from the checkout, so the restriction never comes up, and tsdown.config.ts can stay a real TypeScript file for everyone working on this repo directly instead of being downgraded to plain JS to dodge it. --- dist/emitter.cjs | 278 ++++++ dist/emitter.d.cts | 11 + dist/emitter.d.mts | 11 + dist/emitter.mjs | 274 ++++++ dist/parse.cjs | 1257 ++++++++++++++++++++++++++++ dist/parse.d.cts | 3 + dist/parse.d.mts | 3 + dist/parse.mjs | 1253 +++++++++++++++++++++++++++ dist/rolldown-runtime-VH7oDXx4.cjs | 28 + dist/runtime.cjs | 25 + dist/runtime.d.cts | 4 + dist/runtime.d.mts | 4 + dist/runtime.mjs | 24 + tsdown.config.ts | 11 + 14 files changed, 3186 insertions(+) create mode 100644 dist/emitter.cjs create mode 100644 dist/emitter.d.cts create mode 100644 dist/emitter.d.mts create mode 100644 dist/emitter.mjs create mode 100644 dist/parse.cjs create mode 100644 dist/parse.d.cts create mode 100644 dist/parse.d.mts create mode 100644 dist/parse.mjs create mode 100644 dist/rolldown-runtime-VH7oDXx4.cjs create mode 100644 dist/runtime.cjs create mode 100644 dist/runtime.d.cts create mode 100644 dist/runtime.d.mts create mode 100644 dist/runtime.mjs create mode 100644 tsdown.config.ts diff --git a/dist/emitter.cjs b/dist/emitter.cjs new file mode 100644 index 0000000..fec1587 --- /dev/null +++ b/dist/emitter.cjs @@ -0,0 +1,278 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); +const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs"); +let camelcase = require("camelcase"); +camelcase = require_rolldown_runtime.__toESM(camelcase, 1); +//#region src/emitter.ts +function isPlainObject(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function isUnknownArray(value) { + return Array.isArray(value); +} +function isRawRule(value) { + return isPlainObject(value) && typeof value.Type === "string" && typeof value.Name === "string"; +} +function isOccurrence(value) { + return isPlainObject(value) && typeof value.n === "number" && typeof value.m === "number"; +} +function isLiteralValueNode(value) { + return isPlainObject(value) && typeof value.Type === "string" && "Value" in value; +} +function mergeRules(parsed) { + if (!isUnknownArray(parsed)) throw new Error("expected parse() to return an array of top-level rules"); + const byName = /* @__PURE__ */ new Map(); + for (const entry of parsed) { + if (!isRawRule(entry)) throw new Error(`unrecognised top-level parse() entry: ${JSON.stringify(entry)}`); + if (entry.Type !== "group" && entry.Type !== "array" && entry.Type !== "variable") throw new Error(`unsupported top-level rule kind "${entry.Type}" for rule "${entry.Name}"`); + const existing = byName.get(entry.Name); + if (existing) { + if (!existing.isSocket) throw new Error(`rule "${entry.Name}" is defined more than once, outside cddl.js's supported subset (only a socket name, "$name /= X", may have multiple definitions)`); + existing.raw.push(entry); + continue; + } + byName.set(entry.Name, { + name: entry.Name, + isSocket: entry.Name.startsWith("$"), + raw: [entry] + }); + } + return [...byName.values()]; +} +function schemaVarName(ruleName) { + const clean = ruleName.startsWith("$") ? ruleName.slice(1) : ruleName; + return `${(0, camelcase.default)(clean)}Schema`; +} +function typeName(ruleName) { + const clean = ruleName.startsWith("$") ? ruleName.slice(1) : ruleName; + return (0, camelcase.default)(clean, { pascalCase: true }); +} +function requireStringField(obj, key, ruleName) { + const value = obj[key]; + if (typeof value !== "string") throw new Error(`rule "${ruleName}" is missing the expected "${key}" string field`); + return value; +} +function requireObjectField(obj, key, ruleName) { + const value = obj[key]; + if (!isPlainObject(value)) throw new Error(`rule "${ruleName}" is missing the expected "${key}" field`); + return value; +} +function emitOccurrence(occurrence, inner, ruleName) { + if (!isOccurrence(occurrence)) throw new Error(`rule "${ruleName}" has a member with an unrecognised Occurrence`); + const { n, m } = occurrence; + if (n === 1 && m === 1) return { + expr: inner, + optional: false + }; + if (n === 0 && m === Infinity) return { + expr: inner, + optional: true + }; + if (n === 0 && m === 1) return { + expr: `${inner}.optional()`, + optional: false + }; + throw new Error(`rule "${ruleName}" uses an occurrence indicator (n=${String(n)}, m=${String(m)}) outside cddl.js's supported subset (?, required, or unbounded *)`); +} +function emitOperatorValue(operator, ruleName) { + const opValue = operator.Value; + if (isLiteralValueNode(opValue) && opValue.Type === "literal") return JSON.stringify(opValue.Value); + throw new Error(`rule "${ruleName}" uses an unsupported operator value shape`); +} +function emitWithOperator(node, ruleName, refs) { + const baseType = node.Type; + const operator = requireObjectField(node, "Operator", ruleName); + const operatorType = operator.Type; + let baseExpr; + if (typeof baseType === "string") baseExpr = emitNativeType(baseType, ruleName); + else if (isPlainObject(baseType) && baseType.Type === "group" && typeof baseType.Value === "string") { + refs.add(baseType.Value); + baseExpr = `z.lazy(() => ${schemaVarName(baseType.Value)})`; + } else throw new Error(`rule "${ruleName}" has an operator attached to an unsupported base type`); + if (operatorType === "size") { + const opValue = operator.Value; + if (!isLiteralValueNode(opValue) || opValue.Type !== "literal" || typeof opValue.Value !== "number") throw new Error(`rule "${ruleName}" uses .size with a non-literal-integer value, outside cddl.js's supported subset`); + const size = opValue.Value; + return `${baseExpr}.refine((v) => v.length === ${String(size)}, { message: "expected exactly ${String(size)} bytes" })`; + } + if (operatorType === "regexp") { + const pattern = emitOperatorValue(operator, ruleName); + return `${baseExpr}.regex(new RegExp(${pattern}))`; + } + if (operatorType === "cbor" || operatorType === "cborseq") { + const opValue = operator.Value; + if (!isPlainObject(opValue) || opValue.Type !== "group" || typeof opValue.Value !== "string") throw new Error(`rule "${ruleName}" uses .cbor/.cborseq with a value that isn't a plain rule reference, outside cddl.js's supported subset`); + refs.add(opValue.Value); + return `cborDecodesAs(z.lazy(() => ${schemaVarName(opValue.Value)}))`; + } + throw new Error(`rule "${ruleName}" uses unsupported operator ".${String(operatorType)}"`); +} +function emitNativeType(name, ruleName) { + switch (name) { + case "bstr": return "z.instanceof(Uint8Array)"; + case "tstr": return "z.string()"; + case "int": return "z.number().int()"; + case "uint": return "z.number().int().nonnegative()"; + case "nint": return "z.number().int().negative()"; + case "bool": return "z.boolean()"; + case "any": return "z.unknown()"; + case "nil": + case "null": return "z.null()"; + default: throw new Error(`rule "${ruleName}" uses native type "${name}", outside cddl.js's supported subset`); + } +} +function buildLiteralKeyMap(rules) { + const map = /* @__PURE__ */ new Map(); + for (const rule of rules) { + if (rule.raw.length !== 1) continue; + const entry = rule.raw[0]; + if (entry.Type !== "variable") continue; + const propertyType = entry.PropertyType; + if (!isUnknownArray(propertyType) || propertyType.length !== 1) continue; + const only = propertyType[0]; + if (isLiteralValueNode(only) && only.Type === "literal" && (typeof only.Value === "string" || typeof only.Value === "number")) map.set(rule.name, only.Value); + } + return map; +} +function emitSingleType(node, ruleName, literalKeys, refs) { + if (typeof node === "string") return emitNativeType(node, ruleName); + if (!isPlainObject(node)) throw new Error(`rule "${ruleName}" has a property type that isn't a recognised shape: ${JSON.stringify(node)}`); + if ("Operator" in node) return emitWithOperator(node, ruleName, refs); + if (node.Type === "literal") return `z.literal(${JSON.stringify(node.Value)})`; + if (node.Type === "group" && typeof node.Value === "string") { + refs.add(node.Value); + return `z.lazy(() => ${schemaVarName(node.Value)})`; + } + if (node.Type === "group" && "Properties" in node) return emitGroupExpr(node, ruleName, literalKeys, refs); + if (node.Type === "array") return emitArrayExpr(node, ruleName, literalKeys, refs); + throw new Error(`rule "${ruleName}" has a property type shape outside cddl.js's supported subset: ${JSON.stringify(node)}`); +} +/** A property type is normally an array of branch nodes (one per `/`-separated alternative), but the vendored parser emits a bare node directly -- not array-wrapped -- for an array's own anonymous, single-member value type (confirmed against wire-mesh's real spec: `entries: [* bstr]` produces a bare `"bstr"` where `peers: [* peer-advert]` produces a wrapped `[{Type:"group",...}]`). Both shapes are handled uniformly here. */ +function emitPropertyType(types, ruleName, literalKeys, refs) { + if (!isUnknownArray(types)) return emitSingleType(types, ruleName, literalKeys, refs); + if (types.length === 0) throw new Error(`rule "${ruleName}" has an empty property type`); + if (types.length === 1) return emitSingleType(types[0], ruleName, literalKeys, refs); + return `z.union([${types.map((t) => emitSingleType(t, ruleName, literalKeys, refs)).join(", ")}])`; +} +function emitGroupProperties(properties, ruleName, literalKeys, refs) { + if (!isUnknownArray(properties)) throw new Error(`rule "${ruleName}" has malformed Properties`); + const fields = []; + let catchall; + for (const rawProp of properties) { + if (!isPlainObject(rawProp)) throw new Error(`rule "${ruleName}" has a malformed property entry`); + const occurrence = rawProp.Occurrence; + const keyName = requireStringField(rawProp, "Name", ruleName); + if (rawProp.HasCut === false) { + const literalKey = literalKeys.get(keyName); + if (literalKey !== void 0) { + const { expr, optional } = emitOccurrence(occurrence, emitPropertyType(rawProp.Type, ruleName, literalKeys, refs), ruleName); + fields.push(` ${JSON.stringify(String(literalKey))}: ${optional ? `${expr}.optional()` : expr},`); + continue; + } + if (isOccurrence(occurrence) && occurrence.n === 0 && occurrence.m === Infinity) { + if (catchall !== void 0) throw new Error(`rule "${ruleName}" has more than one open-map-tail (* key => value) entry, outside cddl.js's supported subset (one per map)`); + catchall = emitPropertyType(rawProp.Type, ruleName, literalKeys, refs); + continue; + } + throw new Error(`rule "${ruleName}" has an arrow-syntax map entry ("${keyName} => ...") that is neither a known literal-valued key nor the open-tail pattern, outside cddl.js's supported subset`); + } + const { expr, optional } = emitOccurrence(occurrence, emitPropertyType(rawProp.Type, ruleName, literalKeys, refs), ruleName); + fields.push(` ${JSON.stringify(keyName)}: ${optional ? `${expr}.optional()` : expr},`); + } + return { + fields, + catchall + }; +} +function emitGroupExpr(entry, ruleName, literalKeys, refs) { + const { fields, catchall } = emitGroupProperties(entry.Properties, ruleName, literalKeys, refs); + const object = `z.object({\n${fields.join("\n")}\n})`; + return catchall !== void 0 ? `${object}.catchall(${catchall})` : object; +} +function emitArrayExpr(entry, ruleName, literalKeys, refs) { + const values = entry.Values; + if (!isUnknownArray(values) || values.length === 0) throw new Error(`rule "${ruleName}" has malformed or empty array Values`); + if (values.length === 1) { + const only = values[0]; + if (isPlainObject(only)) { + const occurrence = only.Occurrence; + if (only.Name === "" && isOccurrence(occurrence) && occurrence.n === 0 && occurrence.m === Infinity) return `z.array(${emitPropertyType(only.Type, ruleName, literalKeys, refs)})`; + } + } + return `z.tuple([${values.map((value) => { + if (!isPlainObject(value)) throw new Error(`rule "${ruleName}" has a malformed array member`); + const occurrence = value.Occurrence; + if (!isOccurrence(occurrence) || occurrence.n !== 1 || occurrence.m !== 1) throw new Error(`rule "${ruleName}" mixes occurrence indicators within a fixed array, outside cddl.js's supported subset`); + return emitPropertyType(value.Type, ruleName, literalKeys, refs); + }).join(", ")}])`; +} +/** Emits one raw AST entry's own bare Zod expression (no `z.lazy`/`export const` wrapper), dispatching on that entry's own `Type` -- not the rule's, since a socket's entries can mix kinds (see MergedRule). */ +function emitEntryExpr(entry, ruleName, literalKeys, refs) { + const kind = entry.Type; + if (kind === "group") return emitGroupExpr(entry, ruleName, literalKeys, refs); + if (kind === "array") return emitArrayExpr(entry, ruleName, literalKeys, refs); + if (kind === "variable") return emitPropertyType(entry.PropertyType, ruleName, literalKeys, refs); + throw new Error(`rule "${ruleName}" has an unsupported top-level rule kind "${String(kind)}"`); +} +/** A rule with exactly one entry emits that entry's own expression directly; a socket with several entries (its base definition plus every `/=` choice-addition) unions every entry's own expression together. */ +function computeRuleExpr(rule, literalKeys) { + const refs = /* @__PURE__ */ new Set(); + const exprs = rule.raw.map((entry) => emitEntryExpr(entry, rule.name, literalKeys, refs)); + return { + expr: rule.raw.length === 1 ? exprs.join("") : `z.union([${exprs.join(", ")}])`, + refs + }; +} +/** A rule is "cyclic" if, following z.lazy() references transitively, it can reach itself -- e.g. `frame`'s own union includes `federation-envelope-frame`, whose `inner` field is `.cbor frame`, a reference straight back to `frame`. Only these rules need an explicit `z.ZodType` type annotation to break TypeScript's circular-inference restriction; every other rule can have its schema's concrete shape inferred naturally, which is the entire point of generating Zod schemas with attached inferred types rather than hand-written ones. */ +function computeCyclicRules(perRule) { + const cyclic = /* @__PURE__ */ new Set(); + for (const [name, ruleExpr] of perRule) { + const visited = /* @__PURE__ */ new Set(); + const stack = [...ruleExpr.refs]; + while (stack.length > 0) { + const next = stack.pop(); + if (next === void 0 || visited.has(next)) continue; + visited.add(next); + if (next === name) { + cyclic.add(name); + break; + } + const nextRefs = perRule.get(next)?.refs; + if (nextRefs) stack.push(...nextRefs); + } + } + return cyclic; +} +/** Compiles a parsed CDDL AST into a single TypeScript module source: one Zod schema plus one inferred type export per named rule. */ +function emitModule(parsed) { + const rules = mergeRules(parsed); + const literalKeys = buildLiteralKeyMap(rules); + const perRule = /* @__PURE__ */ new Map(); + for (const rule of rules) perRule.set(rule.name, computeRuleExpr(rule, literalKeys)); + const cyclicRules = computeCyclicRules(perRule); + const schemaLines = []; + const typeLines = []; + const needsCbor = JSON.stringify(parsed).includes("\"cbor\""); + for (const rule of rules) { + const ruleExpr = perRule.get(rule.name); + if (!ruleExpr) throw new Error(`internal error: no expression computed for rule "${rule.name}"`); + const annotation = cyclicRules.has(rule.name) ? ": z.ZodType" : ""; + schemaLines.push(`export const ${schemaVarName(rule.name)}${annotation} = z.lazy(() => ${ruleExpr.expr});`); + typeLines.push(`export type ${typeName(rule.name)} = z.infer;`); + } + return [ + ...[ + "// Generated by cddl.js. Do not edit by hand -- regenerate from the source .cddl instead.", + "", + "import { z } from \"zod\";", + needsCbor ? "import { cborDecodesAs } from \"./runtime.js\";" : void 0, + "" + ].filter((line) => line !== void 0), + ...schemaLines, + "", + ...typeLines, + "" + ].join("\n"); +} +//#endregion +exports.emitModule = emitModule; +exports.mergeRules = mergeRules; diff --git a/dist/emitter.d.cts b/dist/emitter.d.cts new file mode 100644 index 0000000..1b0b0b7 --- /dev/null +++ b/dist/emitter.d.cts @@ -0,0 +1,11 @@ +//#region src/emitter.d.ts +/** A named rule, after socket (`$name /= X`) choice-additions for the same name have been merged into one combined union. A socket's own entries can mix AST kinds -- e.g. `$manage-command-params = {* tstr => any}` (a group) extended by `$manage-command-params /= pty-spawn / ...` (a variable union of group references) -- so `raw` carries each entry's own kind rather than one shared kind for the whole rule; emitEntryExpr dispatches per entry. `raw` is a non-empty tuple type -- mergeRules never produces a MergedRule without at least one entry -- so callers can index `raw[0]` without an undefined check. */ +interface MergedRule { + name: string; + isSocket: boolean; + raw: [Record, ...Record[]]; +} +export declare function mergeRules(parsed: unknown): MergedRule[]; +/** Compiles a parsed CDDL AST into a single TypeScript module source: one Zod schema plus one inferred type export per named rule. */ +export declare function emitModule(parsed: unknown): string; +//#endregion \ No newline at end of file diff --git a/dist/emitter.d.mts b/dist/emitter.d.mts new file mode 100644 index 0000000..1b0b0b7 --- /dev/null +++ b/dist/emitter.d.mts @@ -0,0 +1,11 @@ +//#region src/emitter.d.ts +/** A named rule, after socket (`$name /= X`) choice-additions for the same name have been merged into one combined union. A socket's own entries can mix AST kinds -- e.g. `$manage-command-params = {* tstr => any}` (a group) extended by `$manage-command-params /= pty-spawn / ...` (a variable union of group references) -- so `raw` carries each entry's own kind rather than one shared kind for the whole rule; emitEntryExpr dispatches per entry. `raw` is a non-empty tuple type -- mergeRules never produces a MergedRule without at least one entry -- so callers can index `raw[0]` without an undefined check. */ +interface MergedRule { + name: string; + isSocket: boolean; + raw: [Record, ...Record[]]; +} +export declare function mergeRules(parsed: unknown): MergedRule[]; +/** Compiles a parsed CDDL AST into a single TypeScript module source: one Zod schema plus one inferred type export per named rule. */ +export declare function emitModule(parsed: unknown): string; +//#endregion \ No newline at end of file diff --git a/dist/emitter.mjs b/dist/emitter.mjs new file mode 100644 index 0000000..0d9a41b --- /dev/null +++ b/dist/emitter.mjs @@ -0,0 +1,274 @@ +import camelCase from "camelcase"; +//#region src/emitter.ts +function isPlainObject(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function isUnknownArray(value) { + return Array.isArray(value); +} +function isRawRule(value) { + return isPlainObject(value) && typeof value.Type === "string" && typeof value.Name === "string"; +} +function isOccurrence(value) { + return isPlainObject(value) && typeof value.n === "number" && typeof value.m === "number"; +} +function isLiteralValueNode(value) { + return isPlainObject(value) && typeof value.Type === "string" && "Value" in value; +} +function mergeRules(parsed) { + if (!isUnknownArray(parsed)) throw new Error("expected parse() to return an array of top-level rules"); + const byName = /* @__PURE__ */ new Map(); + for (const entry of parsed) { + if (!isRawRule(entry)) throw new Error(`unrecognised top-level parse() entry: ${JSON.stringify(entry)}`); + if (entry.Type !== "group" && entry.Type !== "array" && entry.Type !== "variable") throw new Error(`unsupported top-level rule kind "${entry.Type}" for rule "${entry.Name}"`); + const existing = byName.get(entry.Name); + if (existing) { + if (!existing.isSocket) throw new Error(`rule "${entry.Name}" is defined more than once, outside cddl.js's supported subset (only a socket name, "$name /= X", may have multiple definitions)`); + existing.raw.push(entry); + continue; + } + byName.set(entry.Name, { + name: entry.Name, + isSocket: entry.Name.startsWith("$"), + raw: [entry] + }); + } + return [...byName.values()]; +} +function schemaVarName(ruleName) { + const clean = ruleName.startsWith("$") ? ruleName.slice(1) : ruleName; + return `${camelCase(clean)}Schema`; +} +function typeName(ruleName) { + const clean = ruleName.startsWith("$") ? ruleName.slice(1) : ruleName; + return camelCase(clean, { pascalCase: true }); +} +function requireStringField(obj, key, ruleName) { + const value = obj[key]; + if (typeof value !== "string") throw new Error(`rule "${ruleName}" is missing the expected "${key}" string field`); + return value; +} +function requireObjectField(obj, key, ruleName) { + const value = obj[key]; + if (!isPlainObject(value)) throw new Error(`rule "${ruleName}" is missing the expected "${key}" field`); + return value; +} +function emitOccurrence(occurrence, inner, ruleName) { + if (!isOccurrence(occurrence)) throw new Error(`rule "${ruleName}" has a member with an unrecognised Occurrence`); + const { n, m } = occurrence; + if (n === 1 && m === 1) return { + expr: inner, + optional: false + }; + if (n === 0 && m === Infinity) return { + expr: inner, + optional: true + }; + if (n === 0 && m === 1) return { + expr: `${inner}.optional()`, + optional: false + }; + throw new Error(`rule "${ruleName}" uses an occurrence indicator (n=${String(n)}, m=${String(m)}) outside cddl.js's supported subset (?, required, or unbounded *)`); +} +function emitOperatorValue(operator, ruleName) { + const opValue = operator.Value; + if (isLiteralValueNode(opValue) && opValue.Type === "literal") return JSON.stringify(opValue.Value); + throw new Error(`rule "${ruleName}" uses an unsupported operator value shape`); +} +function emitWithOperator(node, ruleName, refs) { + const baseType = node.Type; + const operator = requireObjectField(node, "Operator", ruleName); + const operatorType = operator.Type; + let baseExpr; + if (typeof baseType === "string") baseExpr = emitNativeType(baseType, ruleName); + else if (isPlainObject(baseType) && baseType.Type === "group" && typeof baseType.Value === "string") { + refs.add(baseType.Value); + baseExpr = `z.lazy(() => ${schemaVarName(baseType.Value)})`; + } else throw new Error(`rule "${ruleName}" has an operator attached to an unsupported base type`); + if (operatorType === "size") { + const opValue = operator.Value; + if (!isLiteralValueNode(opValue) || opValue.Type !== "literal" || typeof opValue.Value !== "number") throw new Error(`rule "${ruleName}" uses .size with a non-literal-integer value, outside cddl.js's supported subset`); + const size = opValue.Value; + return `${baseExpr}.refine((v) => v.length === ${String(size)}, { message: "expected exactly ${String(size)} bytes" })`; + } + if (operatorType === "regexp") { + const pattern = emitOperatorValue(operator, ruleName); + return `${baseExpr}.regex(new RegExp(${pattern}))`; + } + if (operatorType === "cbor" || operatorType === "cborseq") { + const opValue = operator.Value; + if (!isPlainObject(opValue) || opValue.Type !== "group" || typeof opValue.Value !== "string") throw new Error(`rule "${ruleName}" uses .cbor/.cborseq with a value that isn't a plain rule reference, outside cddl.js's supported subset`); + refs.add(opValue.Value); + return `cborDecodesAs(z.lazy(() => ${schemaVarName(opValue.Value)}))`; + } + throw new Error(`rule "${ruleName}" uses unsupported operator ".${String(operatorType)}"`); +} +function emitNativeType(name, ruleName) { + switch (name) { + case "bstr": return "z.instanceof(Uint8Array)"; + case "tstr": return "z.string()"; + case "int": return "z.number().int()"; + case "uint": return "z.number().int().nonnegative()"; + case "nint": return "z.number().int().negative()"; + case "bool": return "z.boolean()"; + case "any": return "z.unknown()"; + case "nil": + case "null": return "z.null()"; + default: throw new Error(`rule "${ruleName}" uses native type "${name}", outside cddl.js's supported subset`); + } +} +function buildLiteralKeyMap(rules) { + const map = /* @__PURE__ */ new Map(); + for (const rule of rules) { + if (rule.raw.length !== 1) continue; + const entry = rule.raw[0]; + if (entry.Type !== "variable") continue; + const propertyType = entry.PropertyType; + if (!isUnknownArray(propertyType) || propertyType.length !== 1) continue; + const only = propertyType[0]; + if (isLiteralValueNode(only) && only.Type === "literal" && (typeof only.Value === "string" || typeof only.Value === "number")) map.set(rule.name, only.Value); + } + return map; +} +function emitSingleType(node, ruleName, literalKeys, refs) { + if (typeof node === "string") return emitNativeType(node, ruleName); + if (!isPlainObject(node)) throw new Error(`rule "${ruleName}" has a property type that isn't a recognised shape: ${JSON.stringify(node)}`); + if ("Operator" in node) return emitWithOperator(node, ruleName, refs); + if (node.Type === "literal") return `z.literal(${JSON.stringify(node.Value)})`; + if (node.Type === "group" && typeof node.Value === "string") { + refs.add(node.Value); + return `z.lazy(() => ${schemaVarName(node.Value)})`; + } + if (node.Type === "group" && "Properties" in node) return emitGroupExpr(node, ruleName, literalKeys, refs); + if (node.Type === "array") return emitArrayExpr(node, ruleName, literalKeys, refs); + throw new Error(`rule "${ruleName}" has a property type shape outside cddl.js's supported subset: ${JSON.stringify(node)}`); +} +/** A property type is normally an array of branch nodes (one per `/`-separated alternative), but the vendored parser emits a bare node directly -- not array-wrapped -- for an array's own anonymous, single-member value type (confirmed against wire-mesh's real spec: `entries: [* bstr]` produces a bare `"bstr"` where `peers: [* peer-advert]` produces a wrapped `[{Type:"group",...}]`). Both shapes are handled uniformly here. */ +function emitPropertyType(types, ruleName, literalKeys, refs) { + if (!isUnknownArray(types)) return emitSingleType(types, ruleName, literalKeys, refs); + if (types.length === 0) throw new Error(`rule "${ruleName}" has an empty property type`); + if (types.length === 1) return emitSingleType(types[0], ruleName, literalKeys, refs); + return `z.union([${types.map((t) => emitSingleType(t, ruleName, literalKeys, refs)).join(", ")}])`; +} +function emitGroupProperties(properties, ruleName, literalKeys, refs) { + if (!isUnknownArray(properties)) throw new Error(`rule "${ruleName}" has malformed Properties`); + const fields = []; + let catchall; + for (const rawProp of properties) { + if (!isPlainObject(rawProp)) throw new Error(`rule "${ruleName}" has a malformed property entry`); + const occurrence = rawProp.Occurrence; + const keyName = requireStringField(rawProp, "Name", ruleName); + if (rawProp.HasCut === false) { + const literalKey = literalKeys.get(keyName); + if (literalKey !== void 0) { + const { expr, optional } = emitOccurrence(occurrence, emitPropertyType(rawProp.Type, ruleName, literalKeys, refs), ruleName); + fields.push(` ${JSON.stringify(String(literalKey))}: ${optional ? `${expr}.optional()` : expr},`); + continue; + } + if (isOccurrence(occurrence) && occurrence.n === 0 && occurrence.m === Infinity) { + if (catchall !== void 0) throw new Error(`rule "${ruleName}" has more than one open-map-tail (* key => value) entry, outside cddl.js's supported subset (one per map)`); + catchall = emitPropertyType(rawProp.Type, ruleName, literalKeys, refs); + continue; + } + throw new Error(`rule "${ruleName}" has an arrow-syntax map entry ("${keyName} => ...") that is neither a known literal-valued key nor the open-tail pattern, outside cddl.js's supported subset`); + } + const { expr, optional } = emitOccurrence(occurrence, emitPropertyType(rawProp.Type, ruleName, literalKeys, refs), ruleName); + fields.push(` ${JSON.stringify(keyName)}: ${optional ? `${expr}.optional()` : expr},`); + } + return { + fields, + catchall + }; +} +function emitGroupExpr(entry, ruleName, literalKeys, refs) { + const { fields, catchall } = emitGroupProperties(entry.Properties, ruleName, literalKeys, refs); + const object = `z.object({\n${fields.join("\n")}\n})`; + return catchall !== void 0 ? `${object}.catchall(${catchall})` : object; +} +function emitArrayExpr(entry, ruleName, literalKeys, refs) { + const values = entry.Values; + if (!isUnknownArray(values) || values.length === 0) throw new Error(`rule "${ruleName}" has malformed or empty array Values`); + if (values.length === 1) { + const only = values[0]; + if (isPlainObject(only)) { + const occurrence = only.Occurrence; + if (only.Name === "" && isOccurrence(occurrence) && occurrence.n === 0 && occurrence.m === Infinity) return `z.array(${emitPropertyType(only.Type, ruleName, literalKeys, refs)})`; + } + } + return `z.tuple([${values.map((value) => { + if (!isPlainObject(value)) throw new Error(`rule "${ruleName}" has a malformed array member`); + const occurrence = value.Occurrence; + if (!isOccurrence(occurrence) || occurrence.n !== 1 || occurrence.m !== 1) throw new Error(`rule "${ruleName}" mixes occurrence indicators within a fixed array, outside cddl.js's supported subset`); + return emitPropertyType(value.Type, ruleName, literalKeys, refs); + }).join(", ")}])`; +} +/** Emits one raw AST entry's own bare Zod expression (no `z.lazy`/`export const` wrapper), dispatching on that entry's own `Type` -- not the rule's, since a socket's entries can mix kinds (see MergedRule). */ +function emitEntryExpr(entry, ruleName, literalKeys, refs) { + const kind = entry.Type; + if (kind === "group") return emitGroupExpr(entry, ruleName, literalKeys, refs); + if (kind === "array") return emitArrayExpr(entry, ruleName, literalKeys, refs); + if (kind === "variable") return emitPropertyType(entry.PropertyType, ruleName, literalKeys, refs); + throw new Error(`rule "${ruleName}" has an unsupported top-level rule kind "${String(kind)}"`); +} +/** A rule with exactly one entry emits that entry's own expression directly; a socket with several entries (its base definition plus every `/=` choice-addition) unions every entry's own expression together. */ +function computeRuleExpr(rule, literalKeys) { + const refs = /* @__PURE__ */ new Set(); + const exprs = rule.raw.map((entry) => emitEntryExpr(entry, rule.name, literalKeys, refs)); + return { + expr: rule.raw.length === 1 ? exprs.join("") : `z.union([${exprs.join(", ")}])`, + refs + }; +} +/** A rule is "cyclic" if, following z.lazy() references transitively, it can reach itself -- e.g. `frame`'s own union includes `federation-envelope-frame`, whose `inner` field is `.cbor frame`, a reference straight back to `frame`. Only these rules need an explicit `z.ZodType` type annotation to break TypeScript's circular-inference restriction; every other rule can have its schema's concrete shape inferred naturally, which is the entire point of generating Zod schemas with attached inferred types rather than hand-written ones. */ +function computeCyclicRules(perRule) { + const cyclic = /* @__PURE__ */ new Set(); + for (const [name, ruleExpr] of perRule) { + const visited = /* @__PURE__ */ new Set(); + const stack = [...ruleExpr.refs]; + while (stack.length > 0) { + const next = stack.pop(); + if (next === void 0 || visited.has(next)) continue; + visited.add(next); + if (next === name) { + cyclic.add(name); + break; + } + const nextRefs = perRule.get(next)?.refs; + if (nextRefs) stack.push(...nextRefs); + } + } + return cyclic; +} +/** Compiles a parsed CDDL AST into a single TypeScript module source: one Zod schema plus one inferred type export per named rule. */ +function emitModule(parsed) { + const rules = mergeRules(parsed); + const literalKeys = buildLiteralKeyMap(rules); + const perRule = /* @__PURE__ */ new Map(); + for (const rule of rules) perRule.set(rule.name, computeRuleExpr(rule, literalKeys)); + const cyclicRules = computeCyclicRules(perRule); + const schemaLines = []; + const typeLines = []; + const needsCbor = JSON.stringify(parsed).includes("\"cbor\""); + for (const rule of rules) { + const ruleExpr = perRule.get(rule.name); + if (!ruleExpr) throw new Error(`internal error: no expression computed for rule "${rule.name}"`); + const annotation = cyclicRules.has(rule.name) ? ": z.ZodType" : ""; + schemaLines.push(`export const ${schemaVarName(rule.name)}${annotation} = z.lazy(() => ${ruleExpr.expr});`); + typeLines.push(`export type ${typeName(rule.name)} = z.infer;`); + } + return [ + ...[ + "// Generated by cddl.js. Do not edit by hand -- regenerate from the source .cddl instead.", + "", + "import { z } from \"zod\";", + needsCbor ? "import { cborDecodesAs } from \"./runtime.js\";" : void 0, + "" + ].filter((line) => line !== void 0), + ...schemaLines, + "", + ...typeLines, + "" + ].join("\n"); +} +//#endregion +export { emitModule, mergeRules }; diff --git a/dist/parse.cjs b/dist/parse.cjs new file mode 100644 index 0000000..6768ef0 --- /dev/null +++ b/dist/parse.cjs @@ -0,0 +1,1257 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); +const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs"); +let camelcase = require("camelcase"); +camelcase = require_rolldown_runtime.__toESM(camelcase, 1); +let node_fs = require("node:fs"); +node_fs = require_rolldown_runtime.__toESM(node_fs, 1); +//#region vendor/cddl/dist/tokens.js +var Tokens; +(function(Tokens) { + Tokens["ILLEGAL"] = "ILLEGAL"; + Tokens["EOF"] = "EOF"; + Tokens["NL"] = "\n"; + Tokens["SPACE"] = " "; + Tokens["UNDERSCORE"] = "_"; + Tokens["DOLLAR"] = "$"; + Tokens["ATSIGN"] = "@"; + Tokens["CARET"] = "^"; + Tokens["HASH"] = "#"; + Tokens["TILDE"] = "~"; + Tokens["IDENT"] = "IDENT"; + Tokens["INT"] = "INT"; + Tokens["COMMENT"] = "COMMENT"; + Tokens["STRING"] = "STRING"; + Tokens["NUMBER"] = "NUMBER"; + Tokens["FLOAT"] = "FLOAT"; + Tokens["ASSIGN"] = "="; + Tokens["PLUS"] = "+"; + Tokens["MINUS"] = "-"; + Tokens["SLASH"] = "/"; + Tokens["QUEST"] = "?"; + Tokens["ASTERISK"] = "*"; + Tokens["COMMA"] = ","; + Tokens["DOT"] = "."; + Tokens["COLON"] = ":"; + Tokens["SEMICOLON"] = ";"; + Tokens["LPAREN"] = "("; + Tokens["RPAREN"] = ")"; + Tokens["LBRACE"] = "{"; + Tokens["RBRACE"] = "}"; + Tokens["LBRACK"] = "["; + Tokens["RBRACK"] = "]"; + Tokens["LT"] = "<"; + Tokens["GT"] = ">"; + Tokens["QUOT"] = "\""; +})(Tokens || (Tokens = {})); +//#endregion +//#region vendor/cddl/dist/utils.js +function isLetter(ch) { + return "a" <= ch && ch <= "z" || "A" <= ch && ch <= "Z"; +} +function isAlphabeticCharacter(ch) { + return isLetter(ch) || ch === Tokens.ATSIGN || ch === Tokens.UNDERSCORE || ch === Tokens.DOLLAR; +} +function isDigit(ch) { + return !isNaN(ch) && ch !== Tokens.NL && ch !== Tokens.SPACE; +} +function hasSpecialNumberCharacter(ch) { + return ch === Tokens.MINUS.charCodeAt(0) || ch === Tokens.DOT.charCodeAt(0) || ch === "x".charCodeAt(0) || ch === "b".charCodeAt(0); +} +function parseNumberValue(token) { + if (token.Type === Tokens.FLOAT) return parseFloat(token.Literal); + if (token.Literal.includes("x") || token.Literal.includes("b")) return token.Literal; + return parseInt(token.Literal, 10); +} +//#endregion +//#region vendor/cddl/dist/constants.js +const WHITESPACE_CHARACTERS = [ + " ", + " ", + "\n", + "\r" +]; +const BOOLEAN_LITERALS = ["true", "false"]; +/** +* as defined in Appendix D +* https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#appendix-D +*/ +const PREDEFINED_IDENTIFIER = [ + "any", + "uint", + "nint", + "int", + "bstr", + "bytes", + "tstr", + "text", + "tdate", + "time", + "number", + "biguint", + "bignint", + "bigint", + "integer", + "unsigned", + "decfrac", + "bigfloat", + "eb64url", + "eb64legacy", + "eb16", + "encoded-cbor", + "uri", + "b64url", + "b64legacy", + "regexp", + "mime-message", + "cbor-any", + "float16", + "float32", + "float64", + "float16-32", + "float32-64", + "float", + "false", + "true", + "bool", + "nil", + "null", + "undefined" +]; +//#endregion +//#region vendor/cddl/dist/lexer.js +var Lexer = class { + input; + position = 0; + readPosition = 0; + ch = 0; + constructor(source) { + this.input = source; + this.readChar(); + } + readChar() { + if (this.readPosition >= this.input.length) this.ch = 0; + else this.ch = this.input[this.readPosition].charCodeAt(0); + this.position = this.readPosition; + this.readPosition++; + } + getLocation() { + const position = this.position - 2; + const sourceLineLength = this.input.split("\n").map((l) => l.length); + let i = 0; + for (const [line, lineLength] of Object.entries(sourceLineLength)) { + i += lineLength + 1; + if (i > position) { + const lineBegin = i - lineLength; + return { + line: parseInt(line, 10), + position: position - lineBegin + 1 + }; + } + } + return { + line: 0, + position: 0 + }; + } + getLine(lineNumber) { + return this.input.split("\n")[lineNumber]; + } + getLocationInfo() { + const loc = this.getLocation(); + let locationInfo = (loc ? this.getLine(loc.line) : "") + "\n"; + locationInfo += " ".repeat(loc?.position || 0) + "^\n"; + locationInfo += " ".repeat(loc?.position || 0) + "|\n"; + return locationInfo; + } + nextToken() { + let token; + this.skipWhitespace(); + const Literal = String.fromCharCode(this.ch); + switch (this.ch) { + case "=".charCodeAt(0): + token = { + Type: Tokens.ASSIGN, + Literal + }; + break; + case "(".charCodeAt(0): + token = { + Type: Tokens.LPAREN, + Literal + }; + break; + case ")".charCodeAt(0): + token = { + Type: Tokens.RPAREN, + Literal + }; + break; + case "{".charCodeAt(0): + token = { + Type: Tokens.LBRACE, + Literal + }; + break; + case "}".charCodeAt(0): + token = { + Type: Tokens.RBRACE, + Literal + }; + break; + case "[".charCodeAt(0): + token = { + Type: Tokens.LBRACK, + Literal + }; + break; + case "]".charCodeAt(0): + token = { + Type: Tokens.RBRACK, + Literal + }; + break; + case "<".charCodeAt(0): + token = { + Type: Tokens.LT, + Literal + }; + break; + case ">".charCodeAt(0): + token = { + Type: Tokens.GT, + Literal + }; + break; + case "+".charCodeAt(0): + token = { + Type: Tokens.PLUS, + Literal + }; + break; + case ",".charCodeAt(0): + token = { + Type: Tokens.COMMA, + Literal + }; + break; + case ".".charCodeAt(0): + token = { + Type: Tokens.DOT, + Literal + }; + break; + case ":".charCodeAt(0): + token = { + Type: Tokens.COLON, + Literal + }; + break; + case "?".charCodeAt(0): + token = { + Type: Tokens.QUEST, + Literal + }; + break; + case "/".charCodeAt(0): + token = { + Type: Tokens.SLASH, + Literal + }; + break; + case "*".charCodeAt(0): + token = { + Type: Tokens.ASTERISK, + Literal + }; + break; + case "^".charCodeAt(0): + token = { + Type: Tokens.CARET, + Literal + }; + break; + case "#".charCodeAt(0): + token = { + Type: Tokens.HASH, + Literal + }; + break; + case "~".charCodeAt(0): + token = { + Type: Tokens.TILDE, + Literal + }; + break; + case "\"".charCodeAt(0): + token = { + Type: Tokens.STRING, + Literal: this.readString() + }; + break; + case ";".charCodeAt(0): + token = { + Type: Tokens.COMMENT, + Literal: this.readComment() + }; + break; + case 0: + token = { + Type: Tokens.EOF, + Literal: "" + }; + break; + default: + if (isAlphabeticCharacter(Literal)) return { + Type: Tokens.IDENT, + Literal: this.readIdentifier() + }; + else if (isDigit(Literal) || this.ch === Tokens.MINUS.charCodeAt(0) && isDigit(this.input[this.readPosition])) { + const numberOrFloat = this.readNumberOrFloat(); + return { + Type: numberOrFloat.includes(Tokens.DOT) ? Tokens.FLOAT : Tokens.NUMBER, + Literal: numberOrFloat + }; + } + token = { + Type: Tokens.ILLEGAL, + Literal: "" + }; + } + this.readChar(); + return token; + } + readIdentifier() { + const position = this.position; + /** + * an identifier can contain + * see https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#section-3.1 + */ + while (isLetter(String.fromCharCode(this.ch)) || isDigit(String.fromCharCode(this.ch)) || [ + Tokens.MINUS.charCodeAt(0), + Tokens.UNDERSCORE.charCodeAt(0), + Tokens.ATSIGN.charCodeAt(0), + Tokens.DOT.charCodeAt(0), + Tokens.DOLLAR.charCodeAt(0) + ].includes(this.ch)) this.readChar(); + return this.input.slice(position, this.position); + } + readComment() { + const position = this.position; + while (this.ch && String.fromCharCode(this.ch) !== "\n") this.readChar(); + return this.input.slice(position, this.position).trim(); + } + readString() { + const position = this.position; + this.readChar(); + while (this.ch && String.fromCharCode(this.ch) !== Tokens.QUOT) this.readChar(); + return this.input.slice(position + 1, this.position).trim(); + } + readNumberOrFloat() { + const position = this.position; + let foundSpecialCharacter = false; + /** + * a number of float can contain + */ + while (isDigit(String.fromCharCode(this.ch)) || hasSpecialNumberCharacter(this.ch)) { + /** + * ensure we respect ranges, e.g. 0..10 + * so break after the second dot and adjust read position + */ + if (hasSpecialNumberCharacter(this.ch) && foundSpecialCharacter) { + this.position--; + this.readPosition--; + break; + } + foundSpecialCharacter = hasSpecialNumberCharacter(this.ch); + this.readChar(); + } + return this.input.slice(position, this.position).trim(); + } + skipWhitespace() { + while (WHITESPACE_CHARACTERS.includes(String.fromCharCode(this.ch))) this.readChar(); + } +}; +//#endregion +//#region vendor/cddl/dist/ast.js +var Type; +(function(Type) { + /** + * any types + */ + Type["ANY"] = "any"; + /** + * boolean types + */ + Type["BOOL"] = "bool"; + /** + * numeric types + */ + Type["INT"] = "int"; + Type["UINT"] = "uint"; + Type["NINT"] = "nint"; + Type["FLOAT"] = "float"; + Type["FLOAT16"] = "float16"; + Type["FLOAT32"] = "float32"; + Type["FLOAT64"] = "float64"; + /** + * string types + */ + Type["BSTR"] = "bstr"; + Type["BYTES"] = "bytes"; + Type["TSTR"] = "tstr"; + Type["TEXT"] = "text"; + /** + * null types + */ + Type["NIL"] = "nil"; + Type["NULL"] = "null"; +})(Type || (Type = {})); +//#endregion +//#region vendor/cddl/dist/parser.js +const NIL_TOKEN = { + Type: Tokens.ILLEGAL, + Literal: "" +}; +const DEFAULT_OCCURRENCE = { + n: 1, + m: 1 +}; +const OPERATORS = [ + "default", + "size", + "regexp", + "bits", + "and", + "within", + "eq", + "ne", + "lt", + "le", + "gt", + "ge", + "cbor", + "cborseq" +]; +const OPERATORS_EXPECTING_VALUES = { + default: void 0, + size: ["literal", "range"], + regexp: ["literal"], + bits: ["group"], + and: ["group"], + within: ["group"], + eq: ["group"], + ne: ["group"], + lt: ["group"], + le: ["group"], + gt: ["group"], + ge: ["group"], + cbor: ["group"], + cborseq: ["group"] +}; +var Parser = class { + #filePath; + l; + curToken = NIL_TOKEN; + peekToken = NIL_TOKEN; + peekBelowToken = NIL_TOKEN; + constructor(filePath) { + this.#filePath = filePath; + this.l = new Lexer(node_fs.default.readFileSync(filePath, "utf-8")); + this.nextToken(); + this.nextToken(); + this.nextToken(); + } + nextToken() { + this.curToken = this.peekToken; + this.peekToken = this.peekBelowToken; + this.peekBelowToken = this.l.nextToken(); + return true; + } + parseAssignments() { + const comments = []; + while (this.curToken.Type === Tokens.COMMENT) { + const comment = this.parseComment(); + if (comment) comments.push(comment); + } + /** + * expect group identifier, e.g. + * groupName = + * groupName /= + * groupName //= + */ + if (this.curToken.Type !== Tokens.IDENT || !(this.peekToken.Type === Tokens.ASSIGN || this.peekToken.Type === Tokens.SLASH)) throw this.parserError(`group identifier expected, received "${JSON.stringify(this.curToken)}"`); + let isChoiceAddition = false; + const groupName = this.curToken.Literal; + this.nextToken(); + if (this.curToken.Type === Tokens.SLASH) { + isChoiceAddition = true; + this.nextToken(); + } + if (this.curToken.Type === Tokens.SLASH) this.nextToken(); + this.nextToken(); + const assignmentValue = this.parseAssignmentValue(groupName, isChoiceAddition); + while (this.curToken.Type === Tokens.COMMENT) { + const comment = this.parseComment(); + comment && comments.push(comment); + } + assignmentValue.Comments = comments; + return assignmentValue; + } + parseAssignmentValue(groupName, isChoiceAddition = false) { + let isChoice = false; + const valuesOrProperties = []; + const closingTokens = this.openSegment(); + /** + * if no group segment was opened we have a variable assignment + * and can return immediatelly, e.g. + * + * attire = "bow tie" / "necktie" / "Internet attire" + * + */ + if (closingTokens.length === 0) { + if (groupName) return { + Type: "variable", + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: this.parsePropertyTypes(true), + Comments: [] + }; + return this.parsePropertyTypes(); + } + /** + * type or group choices can be wrapped within `(` and `)`, e.g. + * + * attireBlock = ( + * "bow tie" / + * "necktie" / + * "Internet attire" + * ) + * attireGroup = ( + * attire // + * attireBlock + * ) + */ + if (closingTokens.includes(Tokens.RPAREN) && this.peekToken.Type === Tokens.SLASH && this.peekBelowToken.Type !== Tokens.SLASH && !(this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH)) { + const propertyType = []; + while (!closingTokens.includes(this.curToken.Type)) { + propertyType.push(...this.parsePropertyTypes(true)); + if (closingTokens.includes(this.curToken.Type)) { + this.nextToken(); + break; + } + this.nextToken(); + if (this.curToken.Type === Tokens.SLASH) this.nextToken(); + } + if (this.curToken.Type === Tokens.RPAREN) this.nextToken(); + if (groupName) { + const variable = { + Type: "variable", + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: propertyType, + Comments: [] + }; + if (this.isOperator()) variable.Operator = this.parseOperator(); + return variable; + } + return propertyType; + } + /** + * parse operator assignments, e.g. `ip4 = (float .ge 0.0) .default 1.0` + */ + if (closingTokens.length === 1 && this.peekToken.Type === Tokens.DOT) { + const propertyType = this.parsePropertyType(); + const operator = this.isOperator() ? this.parseOperator() : void 0; + const prop = { + Type: propertyType, + ...operator ? { Operator: operator } : {} + }; + /** + * this branch exists for the single-element case, e.g. `ip4 = (float .ge 0.0)`, where the operator's value is immediately followed by the closing token. If a comma follows instead, e.g. `[ bstr .size 3, bstr ]`, there are more elements still to come -- seed the general array/group loop below with this first element rather than assuming we are already done. + */ + if (this.curToken.Type === Tokens.COMMA) { + valuesOrProperties.push({ + HasCut: false, + Occurrence: DEFAULT_OCCURRENCE, + Name: "", + Type: [prop], + Comments: [] + }); + this.nextToken(); + } else { + this.nextToken(); + if (groupName) { + const trailingOperator = this.isOperator() ? this.parseOperator() : void 0; + return { + Type: "variable", + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: prop, + ...trailingOperator ? { Operator: trailingOperator } : {}, + Comments: [] + }; + } + return [prop]; + } + } + while (!closingTokens.includes(this.curToken.Type)) { + const comments = []; + let leadingComment = this.parseComment(true); + while (leadingComment) { + comments.push(leadingComment); + leadingComment = this.parseComment(true); + } + /** + * check if we have a group choice instead of an assignment + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + if (valuesOrProperties.length === 0) throw this.parserError("Unexpected group choice operator \"//\" at start of group"); + if (!isChoice) { + const last = valuesOrProperties.pop(); + valuesOrProperties.push([last]); + isChoice = true; + } + this.nextToken(); + this.nextToken(); + continue; + } + const propertyType = []; + let isUnwrapped = false; + let hasCut = false; + let propertyName = ""; + const occurrence = this.parseOccurrences(); + /** + * check if variable name is unwrapped + */ + if (this.curToken.Literal === Tokens.TILDE) { + isUnwrapped = true; + this.nextToken(); + } + /** + * parse assignment within array, e.g. + * ``` + * ActionsPerformActionsParameters = [1* { + * type: "key", + * id: text, + * actions: ActionItems, + * *text => any + * }] + * ``` + * or + * ``` + * script.MappingRemoteValue = [*[(script.RemoteValue / text), script.RemoteValue]]; + * ``` + */ + if (this.curToken.Literal === Tokens.LBRACE || this.curToken.Literal === Tokens.LBRACK || this.curToken.Literal === Tokens.LPAREN) { + const prop = { + HasCut: false, + Occurrence: occurrence, + Name: "", + Type: this.parseAssignmentValue(), + Comments: [] + }; + if (isChoice) valuesOrProperties[valuesOrProperties.length - 1].push(prop); + else valuesOrProperties.push(prop); + if (this.curToken.Type === Tokens.COMMA) { + this.nextToken(); + isChoice = false; + } + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) { + if (!isChoice) { + const last = valuesOrProperties.pop(); + valuesOrProperties.push([last]); + isChoice = true; + } + this.nextToken(); + } + continue; + } + /** + * check if we are in an array and a new item is indicated + */ + if (this.curToken.Literal === Tokens.COMMA && closingTokens[0] === Tokens.RBRACK) { + this.nextToken(); + continue; + } + propertyName = this.parsePropertyName(); + /** + * an unnamed member decorated with an operator, e.g. `[ bstr .size 3, bstr ]` -- there is no colon here, so this must be handled before the colon-expecting path below ever sees it + */ + if (this.isOperator()) { + const operator = this.parseOperator(); + const baseType = PREDEFINED_IDENTIFIER.includes(propertyName) ? { Type: propertyName } : { + Type: "group", + Value: propertyName, + Unwrapped: isUnwrapped + }; + valuesOrProperties.push({ + HasCut: hasCut, + Occurrence: occurrence, + Name: "", + Type: [{ + ...baseType, + Operator: operator + }], + Comments: [] + }); + if (this.curToken.Type === Tokens.COMMA) this.nextToken(); + continue; + } + /** + * if `,` is found we have a group reference and jump to the next line + */ + if (this.curToken.Type === Tokens.COMMA || closingTokens.includes(this.curToken.Type)) { + const tokenType = this.curToken.Type; + let parsedComments = false; + let comment; + /** + * check if line has a comment + */ + if (this.curToken.Type === Tokens.COMMA && this.peekToken.Type === Tokens.COMMENT) { + this.nextToken(); + comment = this.parseComment(); + parsedComments = true; + } + valuesOrProperties.push({ + HasCut: hasCut, + Occurrence: occurrence, + Name: "", + Type: PREDEFINED_IDENTIFIER.includes(propertyName) ? propertyName : [{ + Type: "group", + Value: propertyName, + Unwrapped: isUnwrapped + }], + Comments: comment ? [comment] : [] + }); + if (this.curToken.Literal === Tokens.COMMA || this.curToken.Literal === closingTokens[0]) { + if (this.curToken.Literal === Tokens.COMMA) this.nextToken(); + continue; + } + if (!parsedComments) this.nextToken(); + /** + * only continue if next token contains a comma + */ + if (tokenType === Tokens.COMMA) continue; + /** + * otherwise break + */ + break; + } + /** + * check if property has cut, which happens if a property is described as + * - `? "optional-key" ^ => int,` + * - `? optional-key: int,` - since the colon shortcut includes cuts + */ + if (this.curToken.Type === Tokens.CARET || this.curToken.Type === Tokens.COLON) { + hasCut = true; + if (this.curToken.Type === Tokens.CARET) this.nextToken(); + } + /** + * check if we have a group choice instead of an assignment + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + const prop = { + HasCut: hasCut, + Occurrence: occurrence, + Name: "", + Type: { + Type: "group", + Value: propertyName, + Unwrapped: isUnwrapped + }, + Comments: comments + }; + if (isChoice) + /** + * if we already in a choice just push into it + */ + valuesOrProperties[valuesOrProperties.length - 1].push(prop); + else { + /** + * otherwise create a new one + */ + isChoice = true; + valuesOrProperties.push([prop]); + } + this.nextToken(); + this.nextToken(); + continue; + } + /** + * else if no colon was found, throw + */ + if (!this.isPropertyValueSeparator()) throw this.parserError("Expected \":\" or \"=>\""); + this.nextToken(); + /** + * parse property value + */ + const props = this.parseAssignmentValue(); + let operator = this.isOperator() ? this.parseOperator() : void 0; + if (!isChoice && this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) { + this.nextToken(); + const nextType = this.parsePropertyType(); + if (Array.isArray(props)) { + /** + * property has not yet been flagged as a choice, but is part + * of one, e.g. `(float .ge 1.0) / null` + */ + props.push(nextType); + if (!this.isOperator()) this.nextToken(); + } + } + if (this.isOperator()) operator = this.parseOperator(); + if (Array.isArray(props)) + /** + * property has multiple types (e.g. `float / tstr / int`) + */ + propertyType.push(...props); + else propertyType.push(props); + /** + * advance comma + */ + let flipIsChoice = false; + if (this.curToken.Type === Tokens.COMMA) { + /** + * if we are in a choice, we leave it here + */ + flipIsChoice = true; + this.nextToken(); + } + const trailingComment = this.parseComment(); + trailingComment && comments.push(trailingComment); + const prop = { + HasCut: hasCut, + Occurrence: occurrence, + Name: propertyName, + Type: propertyType, + Comments: comments, + ...operator ? { Operator: operator } : {} + }; + if (isChoice) valuesOrProperties[valuesOrProperties.length - 1].push(prop); + else valuesOrProperties.push(prop); + if (flipIsChoice) isChoice = false; + /** + * if `}` is found we are at the end of the group + */ + if (closingTokens.includes(this.curToken.Type)) { + /** + * Handle the case where a group is followed by an inclusion operator, e.g. + * + * group1 = { + * name: tstr, + * age: number, + * } + * + * group2 = { + * handle: tstr + * } .and group1 + * + */ + while (this.peekToken.Type === Tokens.DOT) { + this.nextToken(); + if (this.isOperator()) valuesOrProperties.push({ + Name: "", + Type: "group", + Occurrence: DEFAULT_OCCURRENCE, + Operator: this.parseOperator(), + Comments: [], + HasCut: false + }); + } + break; + } + /** + * eat // if we are in a choice + */ + if (isChoice) { + this.nextToken(); + this.nextToken(); + continue; + } + } + /** + * close segment + */ + if (this.curToken.Type === [...closingTokens].shift()) this.nextToken(); + /** + * if last closing token is "]" we have an array + */ + if (closingTokens[closingTokens.length - 1] === Tokens.RBRACK) return { + Type: "array", + Name: groupName || "", + Values: valuesOrProperties, + Comments: [] + }; + /** + * simplify wrapped types, e.g. from + * { + * "Type": "group", + * "Name": "", + * "Properties": [ + * { + * "HasCut": false, + * "Occurrence": { + * "n": 1, + * "m": 1 + * }, + * "Name": "", + * "Type": "bool", + * "Comment": "" + * } + * ], + * "IsChoiceAddition": false + * } + * back to: + * bool + */ + if (!groupName && valuesOrProperties.length === 1 && PREDEFINED_IDENTIFIER.includes(valuesOrProperties[0].Type)) return valuesOrProperties[0].Type; + /** + * otherwise a group + */ + return { + Type: "group", + Name: groupName || "", + Properties: valuesOrProperties, + IsChoiceAddition: isChoiceAddition, + Comments: [] + }; + } + isPropertyValueSeparator() { + if (this.curToken.Type === Tokens.COLON) return true; + if (this.curToken.Type === Tokens.ASSIGN && this.peekToken.Type === Tokens.GT) { + this.nextToken(); + return true; + } + return false; + } + /** + * checks if group segment is opened and forwards to beginning of + * first property declaration + * @returns {String[]} closing tokens for group (either `}`, `)` or both) + */ + openSegment() { + if (this.curToken.Type === Tokens.LBRACE) { + this.nextToken(); + return [Tokens.RBRACE]; + } else if (this.curToken.Type === Tokens.LPAREN) { + this.nextToken(); + return [Tokens.RPAREN]; + } else if (this.curToken.Type === Tokens.LBRACK) { + this.nextToken(); + return [Tokens.RBRACK]; + } + return []; + } + parsePropertyName() { + /** + * property name without quotes + */ + if (this.curToken.Type === Tokens.IDENT || this.curToken.Type === Tokens.STRING) { + const name = this.curToken.Literal; + this.nextToken(); + return name; + } + throw this.parserError(`Expected property name, received ${this.curToken.Type}(${this.curToken.Literal}), ${this.peekToken.Type}(${this.peekToken.Literal})`); + } + parsePropertyType() { + let type = void 0; + let isUnwrapped = false; + let isGroupedRange = false; + /** + * check if variable name is unwrapped + */ + if (this.curToken.Literal === Tokens.TILDE) { + isUnwrapped = true; + this.nextToken(); + } + /** + * a quoted string is always a literal, even if its text matches a + * reserved keyword like "null" or "bool" + */ + switch (this.curToken.Type === Tokens.STRING ? Tokens.STRING : this.curToken.Literal) { + case Type.ANY: + case Type.BOOL: + case Type.INT: + case Type.UINT: + case Type.NINT: + case Type.FLOAT: + case Type.FLOAT16: + case Type.FLOAT32: + case Type.FLOAT64: + case Type.BSTR: + case Type.BYTES: + case Type.TSTR: + case Type.TEXT: + case Type.NIL: + case Type.NULL: + type = this.curToken.Literal; + break; + default: if (this.curToken.Type === Tokens.STRING) type = { + Type: "literal", + Value: this.curToken.Literal, + Unwrapped: isUnwrapped + }; + else if (BOOLEAN_LITERALS.includes(this.curToken.Literal)) type = { + Type: "literal", + Value: this.curToken.Literal === "true", + Unwrapped: isUnwrapped + }; + else if (this.curToken.Literal === Tokens.LBRACE || this.curToken.Literal === Tokens.LBRACK) { + const val = this.parseAssignmentValue(); + if (Array.isArray(val)) throw new Error("Unexpected array in property type parsing"); + type = val; + } else if (this.curToken.Type === Tokens.IDENT) type = { + Type: "group", + Value: this.curToken.Literal, + Unwrapped: isUnwrapped + }; + else if (this.curToken.Type === Tokens.NUMBER || this.curToken.Type === Tokens.FLOAT) type = { + Type: "literal", + Value: parseNumberValue(this.curToken), + Unwrapped: isUnwrapped, + ...this.curToken.Type === Tokens.FLOAT ? { IsFloat: true } : {} + }; + else if (this.curToken.Type === Tokens.HASH) { + this.nextToken(); + const n = parseNumberValue(this.curToken); + this.nextToken(); + this.nextToken(); + const t = this.parsePropertyType(); + this.nextToken(); + type = { + Type: "tag", + Value: { + NumericPart: n, + TypePart: t + }, + Unwrapped: isUnwrapped + }; + } else if (this.curToken.Literal === Tokens.LPAREN && this.peekToken.Type === Tokens.NUMBER) { + this.nextToken(); + type = { + Type: "literal", + Value: parseNumberValue(this.curToken), + Unwrapped: isUnwrapped + }; + isGroupedRange = true; + } else throw this.parserError(`Invalid property type "${this.curToken.Literal}"`); + } + /** + * check if type continue as a range + */ + if (this.peekToken.Type === Tokens.DOT && this.nextToken() && this.peekToken.Type === Tokens.DOT) { + this.nextToken(); + let Inclusive = true; + /** + * check if range excludes upper bound + */ + if (this.peekToken.Type === Tokens.DOT) { + Inclusive = false; + this.nextToken(); + } + this.nextToken(); + if (!type || typeof type === "object" && !("Value" in type)) throw new Error("Invalid type for range definition"); + const Min = typeof type === "string" || typeof type.Value === "number" ? type : type.Value; + type = { + Type: "range", + Value: { + Inclusive, + Min, + Max: this.parsePropertyType() + }, + Unwrapped: isUnwrapped + }; + if (!isGroupedRange && this.peekToken.Literal === Tokens.RPAREN) { + /** + * If we are at the end of a grouped range, and this was called + * on the first item of the range as opposed to the opening + * parenthesis, isGroupedRange will not be set to true at this + * point. We need to advance to the closing parenthesis, and if + * the next token is an operator, we need to advance to the dot + * so that parseOperator will work properly. + * e.g. + * + * ``` + * (1.0..2.0) .default 1.5 + * ``` + * + * This will be called on the `1.0` and then the `2.0` will be parsed + * as a grouped range. + */ + this.nextToken(); + if (this.isOperator()) isGroupedRange = true; + } + if (isGroupedRange) this.nextToken(); + } + if (!type) { + const { line, position: column } = this.l.getLocation(); + throw new Error(`Unexpected type: ${this.curToken.Type} at line ${line} column ${column}`); + } + return type; + } + parseOperator() { + const type = this.peekToken.Literal; + if (this.curToken.Literal !== Tokens.DOT || !OPERATORS.includes(this.peekToken.Literal)) throw new Error(`Operator ".${type}", expects a ${OPERATORS_EXPECTING_VALUES[type].join(" or ")} property, but found ${this.peekToken.Literal}!`); + this.nextToken(); + this.nextToken(); + const value = this.parsePropertyType(); + this.nextToken(); + return { + Type: type, + Value: value + }; + } + isOperator() { + return this.curToken.Literal === Tokens.DOT && OPERATORS.includes(this.peekToken.Literal); + } + parsePropertyTypes(attachChoiceOperators = false) { + const propertyTypes = []; + let prop = this.parsePropertyType(); + if (this.isOperator()) prop = { + Type: prop, + Operator: this.parseOperator() + }; + else if (this.curToken.Type !== Tokens.SLASH) this.nextToken(); + propertyTypes.push(prop); + /** + * ignore comments between type choice members, e.g. + * ``` + * Foo = int ; comment + * / text + * ``` + * or + * ``` + * Foo = int / ; comment + * text + * ``` + */ + while (this.curToken.Type === Tokens.COMMENT && this.peekToken.Type === Tokens.SLASH) this.parseComment(); + /** + * ensure we don't go into the next choice, e.g.: + * ``` + * delivery = ( + * city // lala: tstr / bool // per-pickup: true, + * ) + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) return propertyTypes; + /** + * capture more if available (e.g. `tstr / float / boolean`) + */ + while (this.curToken.Type === Tokens.SLASH) { + this.nextToken(); + while ([Tokens.COMMENT].includes(this.curToken.Type)) this.parseComment(); + let nextProp = this.parsePropertyType(); + if (this.isOperator()) { + if (attachChoiceOperators) nextProp = { + Type: nextProp, + Operator: this.parseOperator() + }; + } else if (this.curToken.Type !== Tokens.SLASH) + /** + * If we are not parsing an operator, we need to eat the next token; + * otherwise, the operator will be parsed by the caller + */ + this.nextToken(); + propertyTypes.push(nextProp); + while ([Tokens.COMMENT].includes(this.curToken.Type) && this.peekToken.Type === Tokens.SLASH) this.parseComment(); + /** + * ensure we don't go into the next choice, e.g.: + * ``` + * delivery = ( + * city // lala: tstr / bool // per-pickup: true, + * ) + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) break; + } + return propertyTypes; + } + parseOccurrences() { + let occurrence = DEFAULT_OCCURRENCE; + /** + * check for non-numbered occurrence indicator, e.g. zero or more: + * ``` + * * bedroom: size, + * ``` + * zero or one: + * ``` + * ? bedroom: size, + * ``` + * or one or more: + * ``` + * + bedroom: size, + * ``` + */ + if (this.curToken.Type === Tokens.QUEST || this.curToken.Type === Tokens.ASTERISK || this.curToken.Type === Tokens.PLUS) { + const n = this.curToken.Type === Tokens.PLUS ? 1 : 0; + let m = this.curToken.Type === Tokens.QUEST ? 1 : Infinity; + /** + * check if there is a max definition + */ + if (this.peekToken.Type === Tokens.NUMBER) { + m = parseInt(this.peekToken.Literal, 10); + this.nextToken(); + } + occurrence = { + n, + m + }; + this.nextToken(); + } else if (this.curToken.Type === Tokens.NUMBER && this.peekToken.Type === Tokens.ASTERISK) { + const n = parseInt(this.curToken.Literal, 10); + let m = Infinity; + this.nextToken(); + this.nextToken(); + /** + * check if there is a max definition + */ + if (this.curToken.Type === Tokens.NUMBER) { + m = parseInt(this.curToken.Literal, 10); + this.nextToken(); + } + occurrence = { + n, + m + }; + } + return occurrence; + } + /** + * check if line has a comment + */ + parseComment(isLeading) { + if (this.curToken.Type !== Tokens.COMMENT) return; + const comment = this.curToken.Literal.replace(/^;(\s*)/, ""); + this.nextToken(); + if (comment.trim().length === 0) return; + return { + Type: "comment", + Content: comment, + Leading: Boolean(isLeading) + }; + } + parse() { + const definition = []; + while (this.curToken.Type !== Tokens.EOF) { + const group = this.parseAssignments(); + if (group) definition.push(group); + } + return definition; + } + parserError(message) { + const location = this.l.getLocation(); + const locInfo = this.l.getLocationInfo(); + return /* @__PURE__ */ new Error(`${this.#filePath.replace(process.cwd(), "")}:${location.line + 1}:${location.position} - error: ${message}\n\n${locInfo}`); + } +}; +//#endregion +//#region vendor/cddl/dist/index.js +function parse$1(filePath) { + return new Parser(filePath).parse(); +} +//#endregion +//#region src/parse.ts +function parse(filePath) { + return parse$1(filePath); +} +//#endregion +exports.parse = parse; diff --git a/dist/parse.d.cts b/dist/parse.d.cts new file mode 100644 index 0000000..b3a4d36 --- /dev/null +++ b/dist/parse.d.cts @@ -0,0 +1,3 @@ +//#region src/parse.d.ts +export declare function parse(filePath: string): unknown; +//#endregion \ No newline at end of file diff --git a/dist/parse.d.mts b/dist/parse.d.mts new file mode 100644 index 0000000..b3a4d36 --- /dev/null +++ b/dist/parse.d.mts @@ -0,0 +1,3 @@ +//#region src/parse.d.ts +export declare function parse(filePath: string): unknown; +//#endregion \ No newline at end of file diff --git a/dist/parse.mjs b/dist/parse.mjs new file mode 100644 index 0000000..443f09f --- /dev/null +++ b/dist/parse.mjs @@ -0,0 +1,1253 @@ +import "camelcase"; +import fs from "node:fs"; +//#region vendor/cddl/dist/tokens.js +var Tokens; +(function(Tokens) { + Tokens["ILLEGAL"] = "ILLEGAL"; + Tokens["EOF"] = "EOF"; + Tokens["NL"] = "\n"; + Tokens["SPACE"] = " "; + Tokens["UNDERSCORE"] = "_"; + Tokens["DOLLAR"] = "$"; + Tokens["ATSIGN"] = "@"; + Tokens["CARET"] = "^"; + Tokens["HASH"] = "#"; + Tokens["TILDE"] = "~"; + Tokens["IDENT"] = "IDENT"; + Tokens["INT"] = "INT"; + Tokens["COMMENT"] = "COMMENT"; + Tokens["STRING"] = "STRING"; + Tokens["NUMBER"] = "NUMBER"; + Tokens["FLOAT"] = "FLOAT"; + Tokens["ASSIGN"] = "="; + Tokens["PLUS"] = "+"; + Tokens["MINUS"] = "-"; + Tokens["SLASH"] = "/"; + Tokens["QUEST"] = "?"; + Tokens["ASTERISK"] = "*"; + Tokens["COMMA"] = ","; + Tokens["DOT"] = "."; + Tokens["COLON"] = ":"; + Tokens["SEMICOLON"] = ";"; + Tokens["LPAREN"] = "("; + Tokens["RPAREN"] = ")"; + Tokens["LBRACE"] = "{"; + Tokens["RBRACE"] = "}"; + Tokens["LBRACK"] = "["; + Tokens["RBRACK"] = "]"; + Tokens["LT"] = "<"; + Tokens["GT"] = ">"; + Tokens["QUOT"] = "\""; +})(Tokens || (Tokens = {})); +//#endregion +//#region vendor/cddl/dist/utils.js +function isLetter(ch) { + return "a" <= ch && ch <= "z" || "A" <= ch && ch <= "Z"; +} +function isAlphabeticCharacter(ch) { + return isLetter(ch) || ch === Tokens.ATSIGN || ch === Tokens.UNDERSCORE || ch === Tokens.DOLLAR; +} +function isDigit(ch) { + return !isNaN(ch) && ch !== Tokens.NL && ch !== Tokens.SPACE; +} +function hasSpecialNumberCharacter(ch) { + return ch === Tokens.MINUS.charCodeAt(0) || ch === Tokens.DOT.charCodeAt(0) || ch === "x".charCodeAt(0) || ch === "b".charCodeAt(0); +} +function parseNumberValue(token) { + if (token.Type === Tokens.FLOAT) return parseFloat(token.Literal); + if (token.Literal.includes("x") || token.Literal.includes("b")) return token.Literal; + return parseInt(token.Literal, 10); +} +//#endregion +//#region vendor/cddl/dist/constants.js +const WHITESPACE_CHARACTERS = [ + " ", + " ", + "\n", + "\r" +]; +const BOOLEAN_LITERALS = ["true", "false"]; +/** +* as defined in Appendix D +* https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#appendix-D +*/ +const PREDEFINED_IDENTIFIER = [ + "any", + "uint", + "nint", + "int", + "bstr", + "bytes", + "tstr", + "text", + "tdate", + "time", + "number", + "biguint", + "bignint", + "bigint", + "integer", + "unsigned", + "decfrac", + "bigfloat", + "eb64url", + "eb64legacy", + "eb16", + "encoded-cbor", + "uri", + "b64url", + "b64legacy", + "regexp", + "mime-message", + "cbor-any", + "float16", + "float32", + "float64", + "float16-32", + "float32-64", + "float", + "false", + "true", + "bool", + "nil", + "null", + "undefined" +]; +//#endregion +//#region vendor/cddl/dist/lexer.js +var Lexer = class { + input; + position = 0; + readPosition = 0; + ch = 0; + constructor(source) { + this.input = source; + this.readChar(); + } + readChar() { + if (this.readPosition >= this.input.length) this.ch = 0; + else this.ch = this.input[this.readPosition].charCodeAt(0); + this.position = this.readPosition; + this.readPosition++; + } + getLocation() { + const position = this.position - 2; + const sourceLineLength = this.input.split("\n").map((l) => l.length); + let i = 0; + for (const [line, lineLength] of Object.entries(sourceLineLength)) { + i += lineLength + 1; + if (i > position) { + const lineBegin = i - lineLength; + return { + line: parseInt(line, 10), + position: position - lineBegin + 1 + }; + } + } + return { + line: 0, + position: 0 + }; + } + getLine(lineNumber) { + return this.input.split("\n")[lineNumber]; + } + getLocationInfo() { + const loc = this.getLocation(); + let locationInfo = (loc ? this.getLine(loc.line) : "") + "\n"; + locationInfo += " ".repeat(loc?.position || 0) + "^\n"; + locationInfo += " ".repeat(loc?.position || 0) + "|\n"; + return locationInfo; + } + nextToken() { + let token; + this.skipWhitespace(); + const Literal = String.fromCharCode(this.ch); + switch (this.ch) { + case "=".charCodeAt(0): + token = { + Type: Tokens.ASSIGN, + Literal + }; + break; + case "(".charCodeAt(0): + token = { + Type: Tokens.LPAREN, + Literal + }; + break; + case ")".charCodeAt(0): + token = { + Type: Tokens.RPAREN, + Literal + }; + break; + case "{".charCodeAt(0): + token = { + Type: Tokens.LBRACE, + Literal + }; + break; + case "}".charCodeAt(0): + token = { + Type: Tokens.RBRACE, + Literal + }; + break; + case "[".charCodeAt(0): + token = { + Type: Tokens.LBRACK, + Literal + }; + break; + case "]".charCodeAt(0): + token = { + Type: Tokens.RBRACK, + Literal + }; + break; + case "<".charCodeAt(0): + token = { + Type: Tokens.LT, + Literal + }; + break; + case ">".charCodeAt(0): + token = { + Type: Tokens.GT, + Literal + }; + break; + case "+".charCodeAt(0): + token = { + Type: Tokens.PLUS, + Literal + }; + break; + case ",".charCodeAt(0): + token = { + Type: Tokens.COMMA, + Literal + }; + break; + case ".".charCodeAt(0): + token = { + Type: Tokens.DOT, + Literal + }; + break; + case ":".charCodeAt(0): + token = { + Type: Tokens.COLON, + Literal + }; + break; + case "?".charCodeAt(0): + token = { + Type: Tokens.QUEST, + Literal + }; + break; + case "/".charCodeAt(0): + token = { + Type: Tokens.SLASH, + Literal + }; + break; + case "*".charCodeAt(0): + token = { + Type: Tokens.ASTERISK, + Literal + }; + break; + case "^".charCodeAt(0): + token = { + Type: Tokens.CARET, + Literal + }; + break; + case "#".charCodeAt(0): + token = { + Type: Tokens.HASH, + Literal + }; + break; + case "~".charCodeAt(0): + token = { + Type: Tokens.TILDE, + Literal + }; + break; + case "\"".charCodeAt(0): + token = { + Type: Tokens.STRING, + Literal: this.readString() + }; + break; + case ";".charCodeAt(0): + token = { + Type: Tokens.COMMENT, + Literal: this.readComment() + }; + break; + case 0: + token = { + Type: Tokens.EOF, + Literal: "" + }; + break; + default: + if (isAlphabeticCharacter(Literal)) return { + Type: Tokens.IDENT, + Literal: this.readIdentifier() + }; + else if (isDigit(Literal) || this.ch === Tokens.MINUS.charCodeAt(0) && isDigit(this.input[this.readPosition])) { + const numberOrFloat = this.readNumberOrFloat(); + return { + Type: numberOrFloat.includes(Tokens.DOT) ? Tokens.FLOAT : Tokens.NUMBER, + Literal: numberOrFloat + }; + } + token = { + Type: Tokens.ILLEGAL, + Literal: "" + }; + } + this.readChar(); + return token; + } + readIdentifier() { + const position = this.position; + /** + * an identifier can contain + * see https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#section-3.1 + */ + while (isLetter(String.fromCharCode(this.ch)) || isDigit(String.fromCharCode(this.ch)) || [ + Tokens.MINUS.charCodeAt(0), + Tokens.UNDERSCORE.charCodeAt(0), + Tokens.ATSIGN.charCodeAt(0), + Tokens.DOT.charCodeAt(0), + Tokens.DOLLAR.charCodeAt(0) + ].includes(this.ch)) this.readChar(); + return this.input.slice(position, this.position); + } + readComment() { + const position = this.position; + while (this.ch && String.fromCharCode(this.ch) !== "\n") this.readChar(); + return this.input.slice(position, this.position).trim(); + } + readString() { + const position = this.position; + this.readChar(); + while (this.ch && String.fromCharCode(this.ch) !== Tokens.QUOT) this.readChar(); + return this.input.slice(position + 1, this.position).trim(); + } + readNumberOrFloat() { + const position = this.position; + let foundSpecialCharacter = false; + /** + * a number of float can contain + */ + while (isDigit(String.fromCharCode(this.ch)) || hasSpecialNumberCharacter(this.ch)) { + /** + * ensure we respect ranges, e.g. 0..10 + * so break after the second dot and adjust read position + */ + if (hasSpecialNumberCharacter(this.ch) && foundSpecialCharacter) { + this.position--; + this.readPosition--; + break; + } + foundSpecialCharacter = hasSpecialNumberCharacter(this.ch); + this.readChar(); + } + return this.input.slice(position, this.position).trim(); + } + skipWhitespace() { + while (WHITESPACE_CHARACTERS.includes(String.fromCharCode(this.ch))) this.readChar(); + } +}; +//#endregion +//#region vendor/cddl/dist/ast.js +var Type; +(function(Type) { + /** + * any types + */ + Type["ANY"] = "any"; + /** + * boolean types + */ + Type["BOOL"] = "bool"; + /** + * numeric types + */ + Type["INT"] = "int"; + Type["UINT"] = "uint"; + Type["NINT"] = "nint"; + Type["FLOAT"] = "float"; + Type["FLOAT16"] = "float16"; + Type["FLOAT32"] = "float32"; + Type["FLOAT64"] = "float64"; + /** + * string types + */ + Type["BSTR"] = "bstr"; + Type["BYTES"] = "bytes"; + Type["TSTR"] = "tstr"; + Type["TEXT"] = "text"; + /** + * null types + */ + Type["NIL"] = "nil"; + Type["NULL"] = "null"; +})(Type || (Type = {})); +//#endregion +//#region vendor/cddl/dist/parser.js +const NIL_TOKEN = { + Type: Tokens.ILLEGAL, + Literal: "" +}; +const DEFAULT_OCCURRENCE = { + n: 1, + m: 1 +}; +const OPERATORS = [ + "default", + "size", + "regexp", + "bits", + "and", + "within", + "eq", + "ne", + "lt", + "le", + "gt", + "ge", + "cbor", + "cborseq" +]; +const OPERATORS_EXPECTING_VALUES = { + default: void 0, + size: ["literal", "range"], + regexp: ["literal"], + bits: ["group"], + and: ["group"], + within: ["group"], + eq: ["group"], + ne: ["group"], + lt: ["group"], + le: ["group"], + gt: ["group"], + ge: ["group"], + cbor: ["group"], + cborseq: ["group"] +}; +var Parser = class { + #filePath; + l; + curToken = NIL_TOKEN; + peekToken = NIL_TOKEN; + peekBelowToken = NIL_TOKEN; + constructor(filePath) { + this.#filePath = filePath; + this.l = new Lexer(fs.readFileSync(filePath, "utf-8")); + this.nextToken(); + this.nextToken(); + this.nextToken(); + } + nextToken() { + this.curToken = this.peekToken; + this.peekToken = this.peekBelowToken; + this.peekBelowToken = this.l.nextToken(); + return true; + } + parseAssignments() { + const comments = []; + while (this.curToken.Type === Tokens.COMMENT) { + const comment = this.parseComment(); + if (comment) comments.push(comment); + } + /** + * expect group identifier, e.g. + * groupName = + * groupName /= + * groupName //= + */ + if (this.curToken.Type !== Tokens.IDENT || !(this.peekToken.Type === Tokens.ASSIGN || this.peekToken.Type === Tokens.SLASH)) throw this.parserError(`group identifier expected, received "${JSON.stringify(this.curToken)}"`); + let isChoiceAddition = false; + const groupName = this.curToken.Literal; + this.nextToken(); + if (this.curToken.Type === Tokens.SLASH) { + isChoiceAddition = true; + this.nextToken(); + } + if (this.curToken.Type === Tokens.SLASH) this.nextToken(); + this.nextToken(); + const assignmentValue = this.parseAssignmentValue(groupName, isChoiceAddition); + while (this.curToken.Type === Tokens.COMMENT) { + const comment = this.parseComment(); + comment && comments.push(comment); + } + assignmentValue.Comments = comments; + return assignmentValue; + } + parseAssignmentValue(groupName, isChoiceAddition = false) { + let isChoice = false; + const valuesOrProperties = []; + const closingTokens = this.openSegment(); + /** + * if no group segment was opened we have a variable assignment + * and can return immediatelly, e.g. + * + * attire = "bow tie" / "necktie" / "Internet attire" + * + */ + if (closingTokens.length === 0) { + if (groupName) return { + Type: "variable", + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: this.parsePropertyTypes(true), + Comments: [] + }; + return this.parsePropertyTypes(); + } + /** + * type or group choices can be wrapped within `(` and `)`, e.g. + * + * attireBlock = ( + * "bow tie" / + * "necktie" / + * "Internet attire" + * ) + * attireGroup = ( + * attire // + * attireBlock + * ) + */ + if (closingTokens.includes(Tokens.RPAREN) && this.peekToken.Type === Tokens.SLASH && this.peekBelowToken.Type !== Tokens.SLASH && !(this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH)) { + const propertyType = []; + while (!closingTokens.includes(this.curToken.Type)) { + propertyType.push(...this.parsePropertyTypes(true)); + if (closingTokens.includes(this.curToken.Type)) { + this.nextToken(); + break; + } + this.nextToken(); + if (this.curToken.Type === Tokens.SLASH) this.nextToken(); + } + if (this.curToken.Type === Tokens.RPAREN) this.nextToken(); + if (groupName) { + const variable = { + Type: "variable", + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: propertyType, + Comments: [] + }; + if (this.isOperator()) variable.Operator = this.parseOperator(); + return variable; + } + return propertyType; + } + /** + * parse operator assignments, e.g. `ip4 = (float .ge 0.0) .default 1.0` + */ + if (closingTokens.length === 1 && this.peekToken.Type === Tokens.DOT) { + const propertyType = this.parsePropertyType(); + const operator = this.isOperator() ? this.parseOperator() : void 0; + const prop = { + Type: propertyType, + ...operator ? { Operator: operator } : {} + }; + /** + * this branch exists for the single-element case, e.g. `ip4 = (float .ge 0.0)`, where the operator's value is immediately followed by the closing token. If a comma follows instead, e.g. `[ bstr .size 3, bstr ]`, there are more elements still to come -- seed the general array/group loop below with this first element rather than assuming we are already done. + */ + if (this.curToken.Type === Tokens.COMMA) { + valuesOrProperties.push({ + HasCut: false, + Occurrence: DEFAULT_OCCURRENCE, + Name: "", + Type: [prop], + Comments: [] + }); + this.nextToken(); + } else { + this.nextToken(); + if (groupName) { + const trailingOperator = this.isOperator() ? this.parseOperator() : void 0; + return { + Type: "variable", + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: prop, + ...trailingOperator ? { Operator: trailingOperator } : {}, + Comments: [] + }; + } + return [prop]; + } + } + while (!closingTokens.includes(this.curToken.Type)) { + const comments = []; + let leadingComment = this.parseComment(true); + while (leadingComment) { + comments.push(leadingComment); + leadingComment = this.parseComment(true); + } + /** + * check if we have a group choice instead of an assignment + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + if (valuesOrProperties.length === 0) throw this.parserError("Unexpected group choice operator \"//\" at start of group"); + if (!isChoice) { + const last = valuesOrProperties.pop(); + valuesOrProperties.push([last]); + isChoice = true; + } + this.nextToken(); + this.nextToken(); + continue; + } + const propertyType = []; + let isUnwrapped = false; + let hasCut = false; + let propertyName = ""; + const occurrence = this.parseOccurrences(); + /** + * check if variable name is unwrapped + */ + if (this.curToken.Literal === Tokens.TILDE) { + isUnwrapped = true; + this.nextToken(); + } + /** + * parse assignment within array, e.g. + * ``` + * ActionsPerformActionsParameters = [1* { + * type: "key", + * id: text, + * actions: ActionItems, + * *text => any + * }] + * ``` + * or + * ``` + * script.MappingRemoteValue = [*[(script.RemoteValue / text), script.RemoteValue]]; + * ``` + */ + if (this.curToken.Literal === Tokens.LBRACE || this.curToken.Literal === Tokens.LBRACK || this.curToken.Literal === Tokens.LPAREN) { + const prop = { + HasCut: false, + Occurrence: occurrence, + Name: "", + Type: this.parseAssignmentValue(), + Comments: [] + }; + if (isChoice) valuesOrProperties[valuesOrProperties.length - 1].push(prop); + else valuesOrProperties.push(prop); + if (this.curToken.Type === Tokens.COMMA) { + this.nextToken(); + isChoice = false; + } + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) { + if (!isChoice) { + const last = valuesOrProperties.pop(); + valuesOrProperties.push([last]); + isChoice = true; + } + this.nextToken(); + } + continue; + } + /** + * check if we are in an array and a new item is indicated + */ + if (this.curToken.Literal === Tokens.COMMA && closingTokens[0] === Tokens.RBRACK) { + this.nextToken(); + continue; + } + propertyName = this.parsePropertyName(); + /** + * an unnamed member decorated with an operator, e.g. `[ bstr .size 3, bstr ]` -- there is no colon here, so this must be handled before the colon-expecting path below ever sees it + */ + if (this.isOperator()) { + const operator = this.parseOperator(); + const baseType = PREDEFINED_IDENTIFIER.includes(propertyName) ? { Type: propertyName } : { + Type: "group", + Value: propertyName, + Unwrapped: isUnwrapped + }; + valuesOrProperties.push({ + HasCut: hasCut, + Occurrence: occurrence, + Name: "", + Type: [{ + ...baseType, + Operator: operator + }], + Comments: [] + }); + if (this.curToken.Type === Tokens.COMMA) this.nextToken(); + continue; + } + /** + * if `,` is found we have a group reference and jump to the next line + */ + if (this.curToken.Type === Tokens.COMMA || closingTokens.includes(this.curToken.Type)) { + const tokenType = this.curToken.Type; + let parsedComments = false; + let comment; + /** + * check if line has a comment + */ + if (this.curToken.Type === Tokens.COMMA && this.peekToken.Type === Tokens.COMMENT) { + this.nextToken(); + comment = this.parseComment(); + parsedComments = true; + } + valuesOrProperties.push({ + HasCut: hasCut, + Occurrence: occurrence, + Name: "", + Type: PREDEFINED_IDENTIFIER.includes(propertyName) ? propertyName : [{ + Type: "group", + Value: propertyName, + Unwrapped: isUnwrapped + }], + Comments: comment ? [comment] : [] + }); + if (this.curToken.Literal === Tokens.COMMA || this.curToken.Literal === closingTokens[0]) { + if (this.curToken.Literal === Tokens.COMMA) this.nextToken(); + continue; + } + if (!parsedComments) this.nextToken(); + /** + * only continue if next token contains a comma + */ + if (tokenType === Tokens.COMMA) continue; + /** + * otherwise break + */ + break; + } + /** + * check if property has cut, which happens if a property is described as + * - `? "optional-key" ^ => int,` + * - `? optional-key: int,` - since the colon shortcut includes cuts + */ + if (this.curToken.Type === Tokens.CARET || this.curToken.Type === Tokens.COLON) { + hasCut = true; + if (this.curToken.Type === Tokens.CARET) this.nextToken(); + } + /** + * check if we have a group choice instead of an assignment + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + const prop = { + HasCut: hasCut, + Occurrence: occurrence, + Name: "", + Type: { + Type: "group", + Value: propertyName, + Unwrapped: isUnwrapped + }, + Comments: comments + }; + if (isChoice) + /** + * if we already in a choice just push into it + */ + valuesOrProperties[valuesOrProperties.length - 1].push(prop); + else { + /** + * otherwise create a new one + */ + isChoice = true; + valuesOrProperties.push([prop]); + } + this.nextToken(); + this.nextToken(); + continue; + } + /** + * else if no colon was found, throw + */ + if (!this.isPropertyValueSeparator()) throw this.parserError("Expected \":\" or \"=>\""); + this.nextToken(); + /** + * parse property value + */ + const props = this.parseAssignmentValue(); + let operator = this.isOperator() ? this.parseOperator() : void 0; + if (!isChoice && this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) { + this.nextToken(); + const nextType = this.parsePropertyType(); + if (Array.isArray(props)) { + /** + * property has not yet been flagged as a choice, but is part + * of one, e.g. `(float .ge 1.0) / null` + */ + props.push(nextType); + if (!this.isOperator()) this.nextToken(); + } + } + if (this.isOperator()) operator = this.parseOperator(); + if (Array.isArray(props)) + /** + * property has multiple types (e.g. `float / tstr / int`) + */ + propertyType.push(...props); + else propertyType.push(props); + /** + * advance comma + */ + let flipIsChoice = false; + if (this.curToken.Type === Tokens.COMMA) { + /** + * if we are in a choice, we leave it here + */ + flipIsChoice = true; + this.nextToken(); + } + const trailingComment = this.parseComment(); + trailingComment && comments.push(trailingComment); + const prop = { + HasCut: hasCut, + Occurrence: occurrence, + Name: propertyName, + Type: propertyType, + Comments: comments, + ...operator ? { Operator: operator } : {} + }; + if (isChoice) valuesOrProperties[valuesOrProperties.length - 1].push(prop); + else valuesOrProperties.push(prop); + if (flipIsChoice) isChoice = false; + /** + * if `}` is found we are at the end of the group + */ + if (closingTokens.includes(this.curToken.Type)) { + /** + * Handle the case where a group is followed by an inclusion operator, e.g. + * + * group1 = { + * name: tstr, + * age: number, + * } + * + * group2 = { + * handle: tstr + * } .and group1 + * + */ + while (this.peekToken.Type === Tokens.DOT) { + this.nextToken(); + if (this.isOperator()) valuesOrProperties.push({ + Name: "", + Type: "group", + Occurrence: DEFAULT_OCCURRENCE, + Operator: this.parseOperator(), + Comments: [], + HasCut: false + }); + } + break; + } + /** + * eat // if we are in a choice + */ + if (isChoice) { + this.nextToken(); + this.nextToken(); + continue; + } + } + /** + * close segment + */ + if (this.curToken.Type === [...closingTokens].shift()) this.nextToken(); + /** + * if last closing token is "]" we have an array + */ + if (closingTokens[closingTokens.length - 1] === Tokens.RBRACK) return { + Type: "array", + Name: groupName || "", + Values: valuesOrProperties, + Comments: [] + }; + /** + * simplify wrapped types, e.g. from + * { + * "Type": "group", + * "Name": "", + * "Properties": [ + * { + * "HasCut": false, + * "Occurrence": { + * "n": 1, + * "m": 1 + * }, + * "Name": "", + * "Type": "bool", + * "Comment": "" + * } + * ], + * "IsChoiceAddition": false + * } + * back to: + * bool + */ + if (!groupName && valuesOrProperties.length === 1 && PREDEFINED_IDENTIFIER.includes(valuesOrProperties[0].Type)) return valuesOrProperties[0].Type; + /** + * otherwise a group + */ + return { + Type: "group", + Name: groupName || "", + Properties: valuesOrProperties, + IsChoiceAddition: isChoiceAddition, + Comments: [] + }; + } + isPropertyValueSeparator() { + if (this.curToken.Type === Tokens.COLON) return true; + if (this.curToken.Type === Tokens.ASSIGN && this.peekToken.Type === Tokens.GT) { + this.nextToken(); + return true; + } + return false; + } + /** + * checks if group segment is opened and forwards to beginning of + * first property declaration + * @returns {String[]} closing tokens for group (either `}`, `)` or both) + */ + openSegment() { + if (this.curToken.Type === Tokens.LBRACE) { + this.nextToken(); + return [Tokens.RBRACE]; + } else if (this.curToken.Type === Tokens.LPAREN) { + this.nextToken(); + return [Tokens.RPAREN]; + } else if (this.curToken.Type === Tokens.LBRACK) { + this.nextToken(); + return [Tokens.RBRACK]; + } + return []; + } + parsePropertyName() { + /** + * property name without quotes + */ + if (this.curToken.Type === Tokens.IDENT || this.curToken.Type === Tokens.STRING) { + const name = this.curToken.Literal; + this.nextToken(); + return name; + } + throw this.parserError(`Expected property name, received ${this.curToken.Type}(${this.curToken.Literal}), ${this.peekToken.Type}(${this.peekToken.Literal})`); + } + parsePropertyType() { + let type = void 0; + let isUnwrapped = false; + let isGroupedRange = false; + /** + * check if variable name is unwrapped + */ + if (this.curToken.Literal === Tokens.TILDE) { + isUnwrapped = true; + this.nextToken(); + } + /** + * a quoted string is always a literal, even if its text matches a + * reserved keyword like "null" or "bool" + */ + switch (this.curToken.Type === Tokens.STRING ? Tokens.STRING : this.curToken.Literal) { + case Type.ANY: + case Type.BOOL: + case Type.INT: + case Type.UINT: + case Type.NINT: + case Type.FLOAT: + case Type.FLOAT16: + case Type.FLOAT32: + case Type.FLOAT64: + case Type.BSTR: + case Type.BYTES: + case Type.TSTR: + case Type.TEXT: + case Type.NIL: + case Type.NULL: + type = this.curToken.Literal; + break; + default: if (this.curToken.Type === Tokens.STRING) type = { + Type: "literal", + Value: this.curToken.Literal, + Unwrapped: isUnwrapped + }; + else if (BOOLEAN_LITERALS.includes(this.curToken.Literal)) type = { + Type: "literal", + Value: this.curToken.Literal === "true", + Unwrapped: isUnwrapped + }; + else if (this.curToken.Literal === Tokens.LBRACE || this.curToken.Literal === Tokens.LBRACK) { + const val = this.parseAssignmentValue(); + if (Array.isArray(val)) throw new Error("Unexpected array in property type parsing"); + type = val; + } else if (this.curToken.Type === Tokens.IDENT) type = { + Type: "group", + Value: this.curToken.Literal, + Unwrapped: isUnwrapped + }; + else if (this.curToken.Type === Tokens.NUMBER || this.curToken.Type === Tokens.FLOAT) type = { + Type: "literal", + Value: parseNumberValue(this.curToken), + Unwrapped: isUnwrapped, + ...this.curToken.Type === Tokens.FLOAT ? { IsFloat: true } : {} + }; + else if (this.curToken.Type === Tokens.HASH) { + this.nextToken(); + const n = parseNumberValue(this.curToken); + this.nextToken(); + this.nextToken(); + const t = this.parsePropertyType(); + this.nextToken(); + type = { + Type: "tag", + Value: { + NumericPart: n, + TypePart: t + }, + Unwrapped: isUnwrapped + }; + } else if (this.curToken.Literal === Tokens.LPAREN && this.peekToken.Type === Tokens.NUMBER) { + this.nextToken(); + type = { + Type: "literal", + Value: parseNumberValue(this.curToken), + Unwrapped: isUnwrapped + }; + isGroupedRange = true; + } else throw this.parserError(`Invalid property type "${this.curToken.Literal}"`); + } + /** + * check if type continue as a range + */ + if (this.peekToken.Type === Tokens.DOT && this.nextToken() && this.peekToken.Type === Tokens.DOT) { + this.nextToken(); + let Inclusive = true; + /** + * check if range excludes upper bound + */ + if (this.peekToken.Type === Tokens.DOT) { + Inclusive = false; + this.nextToken(); + } + this.nextToken(); + if (!type || typeof type === "object" && !("Value" in type)) throw new Error("Invalid type for range definition"); + const Min = typeof type === "string" || typeof type.Value === "number" ? type : type.Value; + type = { + Type: "range", + Value: { + Inclusive, + Min, + Max: this.parsePropertyType() + }, + Unwrapped: isUnwrapped + }; + if (!isGroupedRange && this.peekToken.Literal === Tokens.RPAREN) { + /** + * If we are at the end of a grouped range, and this was called + * on the first item of the range as opposed to the opening + * parenthesis, isGroupedRange will not be set to true at this + * point. We need to advance to the closing parenthesis, and if + * the next token is an operator, we need to advance to the dot + * so that parseOperator will work properly. + * e.g. + * + * ``` + * (1.0..2.0) .default 1.5 + * ``` + * + * This will be called on the `1.0` and then the `2.0` will be parsed + * as a grouped range. + */ + this.nextToken(); + if (this.isOperator()) isGroupedRange = true; + } + if (isGroupedRange) this.nextToken(); + } + if (!type) { + const { line, position: column } = this.l.getLocation(); + throw new Error(`Unexpected type: ${this.curToken.Type} at line ${line} column ${column}`); + } + return type; + } + parseOperator() { + const type = this.peekToken.Literal; + if (this.curToken.Literal !== Tokens.DOT || !OPERATORS.includes(this.peekToken.Literal)) throw new Error(`Operator ".${type}", expects a ${OPERATORS_EXPECTING_VALUES[type].join(" or ")} property, but found ${this.peekToken.Literal}!`); + this.nextToken(); + this.nextToken(); + const value = this.parsePropertyType(); + this.nextToken(); + return { + Type: type, + Value: value + }; + } + isOperator() { + return this.curToken.Literal === Tokens.DOT && OPERATORS.includes(this.peekToken.Literal); + } + parsePropertyTypes(attachChoiceOperators = false) { + const propertyTypes = []; + let prop = this.parsePropertyType(); + if (this.isOperator()) prop = { + Type: prop, + Operator: this.parseOperator() + }; + else if (this.curToken.Type !== Tokens.SLASH) this.nextToken(); + propertyTypes.push(prop); + /** + * ignore comments between type choice members, e.g. + * ``` + * Foo = int ; comment + * / text + * ``` + * or + * ``` + * Foo = int / ; comment + * text + * ``` + */ + while (this.curToken.Type === Tokens.COMMENT && this.peekToken.Type === Tokens.SLASH) this.parseComment(); + /** + * ensure we don't go into the next choice, e.g.: + * ``` + * delivery = ( + * city // lala: tstr / bool // per-pickup: true, + * ) + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) return propertyTypes; + /** + * capture more if available (e.g. `tstr / float / boolean`) + */ + while (this.curToken.Type === Tokens.SLASH) { + this.nextToken(); + while ([Tokens.COMMENT].includes(this.curToken.Type)) this.parseComment(); + let nextProp = this.parsePropertyType(); + if (this.isOperator()) { + if (attachChoiceOperators) nextProp = { + Type: nextProp, + Operator: this.parseOperator() + }; + } else if (this.curToken.Type !== Tokens.SLASH) + /** + * If we are not parsing an operator, we need to eat the next token; + * otherwise, the operator will be parsed by the caller + */ + this.nextToken(); + propertyTypes.push(nextProp); + while ([Tokens.COMMENT].includes(this.curToken.Type) && this.peekToken.Type === Tokens.SLASH) this.parseComment(); + /** + * ensure we don't go into the next choice, e.g.: + * ``` + * delivery = ( + * city // lala: tstr / bool // per-pickup: true, + * ) + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) break; + } + return propertyTypes; + } + parseOccurrences() { + let occurrence = DEFAULT_OCCURRENCE; + /** + * check for non-numbered occurrence indicator, e.g. zero or more: + * ``` + * * bedroom: size, + * ``` + * zero or one: + * ``` + * ? bedroom: size, + * ``` + * or one or more: + * ``` + * + bedroom: size, + * ``` + */ + if (this.curToken.Type === Tokens.QUEST || this.curToken.Type === Tokens.ASTERISK || this.curToken.Type === Tokens.PLUS) { + const n = this.curToken.Type === Tokens.PLUS ? 1 : 0; + let m = this.curToken.Type === Tokens.QUEST ? 1 : Infinity; + /** + * check if there is a max definition + */ + if (this.peekToken.Type === Tokens.NUMBER) { + m = parseInt(this.peekToken.Literal, 10); + this.nextToken(); + } + occurrence = { + n, + m + }; + this.nextToken(); + } else if (this.curToken.Type === Tokens.NUMBER && this.peekToken.Type === Tokens.ASTERISK) { + const n = parseInt(this.curToken.Literal, 10); + let m = Infinity; + this.nextToken(); + this.nextToken(); + /** + * check if there is a max definition + */ + if (this.curToken.Type === Tokens.NUMBER) { + m = parseInt(this.curToken.Literal, 10); + this.nextToken(); + } + occurrence = { + n, + m + }; + } + return occurrence; + } + /** + * check if line has a comment + */ + parseComment(isLeading) { + if (this.curToken.Type !== Tokens.COMMENT) return; + const comment = this.curToken.Literal.replace(/^;(\s*)/, ""); + this.nextToken(); + if (comment.trim().length === 0) return; + return { + Type: "comment", + Content: comment, + Leading: Boolean(isLeading) + }; + } + parse() { + const definition = []; + while (this.curToken.Type !== Tokens.EOF) { + const group = this.parseAssignments(); + if (group) definition.push(group); + } + return definition; + } + parserError(message) { + const location = this.l.getLocation(); + const locInfo = this.l.getLocationInfo(); + return /* @__PURE__ */ new Error(`${this.#filePath.replace(process.cwd(), "")}:${location.line + 1}:${location.position} - error: ${message}\n\n${locInfo}`); + } +}; +//#endregion +//#region vendor/cddl/dist/index.js +function parse$1(filePath) { + return new Parser(filePath).parse(); +} +//#endregion +//#region src/parse.ts +function parse(filePath) { + return parse$1(filePath); +} +//#endregion +export { parse }; diff --git a/dist/rolldown-runtime-VH7oDXx4.cjs b/dist/rolldown-runtime-VH7oDXx4.cjs new file mode 100644 index 0000000..891d439 --- /dev/null +++ b/dist/rolldown-runtime-VH7oDXx4.cjs @@ -0,0 +1,28 @@ +//#region \0rolldown/runtime.js +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 __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { + key = keys[i]; + if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { + get: ((k) => from[k]).bind(null, key), + enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable + }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { + value: mod, + enumerable: true +}) : target, mod)); +//#endregion +Object.defineProperty(exports, "__toESM", { + enumerable: true, + get: function() { + return __toESM; + } +}); diff --git a/dist/runtime.cjs b/dist/runtime.cjs new file mode 100644 index 0000000..c2468c7 --- /dev/null +++ b/dist/runtime.cjs @@ -0,0 +1,25 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); +let zod = require("zod"); +let cbor2 = require("cbor2"); +//#region src/runtime.ts +function cborDecodesAs(schema) { + return zod.z.instanceof(Uint8Array).superRefine((bytes, ctx) => { + let decoded; + try { + decoded = (0, cbor2.decode)(bytes, cbor2.cdeDecodeOptions); + } catch (error) { + ctx.addIssue({ + code: "custom", + message: `bytes do not decode as CBOR: ${String(error)}` + }); + return; + } + const result = schema.safeParse(decoded); + if (!result.success) ctx.addIssue({ + code: "custom", + message: `decoded CBOR does not match the expected type: ${result.error.message}` + }); + }); +} +//#endregion +exports.cborDecodesAs = cborDecodesAs; diff --git a/dist/runtime.d.cts b/dist/runtime.d.cts new file mode 100644 index 0000000..424c0f6 --- /dev/null +++ b/dist/runtime.d.cts @@ -0,0 +1,4 @@ +import { z } from "zod"; +//#region src/runtime.d.ts +export declare function cborDecodesAs(schema: z.ZodType): z.ZodType; +//#endregion \ No newline at end of file diff --git a/dist/runtime.d.mts b/dist/runtime.d.mts new file mode 100644 index 0000000..424c0f6 --- /dev/null +++ b/dist/runtime.d.mts @@ -0,0 +1,4 @@ +import { z } from "zod"; +//#region src/runtime.d.ts +export declare function cborDecodesAs(schema: z.ZodType): z.ZodType; +//#endregion \ No newline at end of file diff --git a/dist/runtime.mjs b/dist/runtime.mjs new file mode 100644 index 0000000..90fc479 --- /dev/null +++ b/dist/runtime.mjs @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { cdeDecodeOptions, decode } from "cbor2"; +//#region src/runtime.ts +function cborDecodesAs(schema) { + return z.instanceof(Uint8Array).superRefine((bytes, ctx) => { + let decoded; + try { + decoded = decode(bytes, cdeDecodeOptions); + } catch (error) { + ctx.addIssue({ + code: "custom", + message: `bytes do not decode as CBOR: ${String(error)}` + }); + return; + } + const result = schema.safeParse(decoded); + if (!result.success) ctx.addIssue({ + code: "custom", + message: `decoded CBOR does not match the expected type: ${result.error.message}` + }); + }); +} +//#endregion +export { cborDecodesAs }; diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 0000000..56e3540 --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsdown"; + +// src/emitter.ts (emitModule/mergeRules), src/runtime.ts (cborDecodesAs), and src/parse.ts (parse, re-exported from the vendored parser) are the library's public surface, built directly as separate entries rather than through a re-exporting index.ts -- the project's barrel-policy lint rule requires importing straight from the module that owns each export. Each is built dual ESM/CJS with declarations so it resolves correctly under every module system; attw verifies that claim directly rather than trusting it. src/cli.ts (a script, not library surface) is deliberately not part of this build; vendor/cddl is compiled separately via its own tsc step (see package.json's _vendor:build), which src/parse.ts's own bundling depends on. +export default defineConfig({ + entry: ["src/emitter.ts", "src/runtime.ts", "src/parse.ts"], + format: ["esm", "cjs"], + dts: true, + exports: true, + attw: { profile: "node16" }, + clean: true, +}); From 02e15e573fd9435b030bd7e455295d65fd902f3f Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:52:17 +0100 Subject: [PATCH 2/4] build: stop building on install, restore tsdown.config.ts With dist/ now committed, the "prepare" script that ran a full build on every git-dependency install has nothing left to do -- remove it. tsdown.config.js reverts to tsdown.config.ts (nothing in the install path loads it anymore, so Node's node_modules TS-stripping restriction never applies to it), and every reference to the old plain-JS filename (turbo.json's _build inputs, eslint's ignore list, tsconfig.node.json's include) reverts to match. vendor/cddl/dist/ moves to .gitignore: dist/ already bundles its compiled output inline, so it's a build intermediate again, not something that needs tracking. --- .gitignore | 3 ++- eslint.config.ts | 2 -- package.json | 1 - tsconfig.node.json | 2 +- tsdown.config.js | 13 ------------- turbo.json | 2 +- 6 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 tsdown.config.js diff --git a/.gitignore b/.gitignore index da894c0..b289591 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ -dist/ .turbo/ .eslintcache coverage/ test/fixtures/generated/protocol.ts +# Not dist/ -- it's committed deliberately (see README.md's "Why dist/ is committed" section), so an npm/pnpm git-dependency install gets working code with no build step, which would otherwise need to run tsdown from inside node_modules, where Node's native TS-stripping refuses to load a .ts config at all. +vendor/cddl/dist/ diff --git a/eslint.config.ts b/eslint.config.ts index 928814f..db9aa98 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -12,8 +12,6 @@ export default exadevConfig( ".turbo", "vendor", "test/fixtures/generated", - // Plain JS (not TS), specifically so it never needs Node's native type-stripping to load -- see tsdown.config.js's own comment for why. Nothing here needs type-aware linting. - "tsdown.config.js", ], }, { diff --git a/package.json b/package.json index 4109645..a120ccd 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "dist" ], "scripts": { - "prepare": "pnpm run build", "build": "turbo run _build", "_build": "tsdown", "vendor:build": "turbo run _vendor:build", diff --git a/tsconfig.node.json b/tsconfig.node.json index 8914a61..aff3744 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,6 +3,6 @@ "compilerOptions": { "allowImportingTsExtensions": true }, - "include": ["src/cli.ts", "eslint.config.ts", "test/**/*.ts"], + "include": ["src/cli.ts", "tsdown.config.ts", "eslint.config.ts", "test/**/*.ts"], "exclude": ["test/fixtures/generated/protocol.ts"] } diff --git a/tsdown.config.js b/tsdown.config.js deleted file mode 100644 index be697b4..0000000 --- a/tsdown.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "tsdown"; - -// Plain JS, not TS: this file is loaded via Node's native module loader when cddl.js is installed as a git dependency (its own "prepare" script runs `pnpm run build`, which needs this config before any of it can typecheck or strip anything). Node's native TS-stripping refuses to process a .ts file located under node_modules, and a git-dependency install always resolves into exactly such a path during `prepare` -- a real CI failure this project already hit, not a hypothetical one. A tiny defineConfig() call gains nothing from TS here, so it stays plain JS to route around the restriction entirely rather than fight it with a config-loader flag. -// -// src/emitter.ts (emitModule/mergeRules), src/runtime.ts (cborDecodesAs), and src/parse.ts (parse, re-exported from the vendored parser) are the library's public surface, built directly as separate entries rather than through a re-exporting index.ts -- the project's barrel-policy lint rule requires importing straight from the module that owns each export. Each is built dual ESM/CJS with declarations so it resolves correctly under every module system; attw verifies that claim directly rather than trusting it. src/cli.ts (a script, not library surface) is deliberately not part of this build; vendor/cddl is compiled separately via its own tsc step (see package.json's _vendor:build), which src/parse.ts's own bundling depends on. -export default defineConfig({ - entry: ["src/emitter.ts", "src/runtime.ts", "src/parse.ts"], - format: ["esm", "cjs"], - dts: true, - exports: true, - attw: { profile: "node16" }, - clean: true, -}); diff --git a/turbo.json b/turbo.json index af08fe7..e8efd0c 100644 --- a/turbo.json +++ b/turbo.json @@ -9,7 +9,7 @@ }, "_build": { "dependsOn": ["_vendor:build"], - "inputs": ["src/emitter.ts", "src/runtime.ts", "src/parse.ts", "tsdown.config.js", "tsconfig.json"], + "inputs": ["src/emitter.ts", "src/runtime.ts", "src/parse.ts", "tsdown.config.ts", "tsconfig.json"], "outputs": ["dist/**"] }, "_generate": { From 610b4486c329fcbfb90909a81799a2d3f77ada05 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:52:21 +0100 Subject: [PATCH 3/4] ci: verify the committed dist/ matches a fresh build Rebuilds from source on every push and fails the check if dist/ doesn't match what's committed, so a change to src/ that isn't paired with a regenerated dist/ can't merge silently out of date. --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 067b644..612c3bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,13 @@ jobs: - name: Build the library (dual ESM/CJS, verified with attw) run: pnpm run build + - name: Confirm the committed dist/ matches a fresh build + run: | + if ! git diff --exit-code -- dist; then + echo "::error::dist/ is out of date. Run 'pnpm run build' and commit the result -- dist/ is committed deliberately (see README.md) and must never be edited by hand or left stale." + exit 1 + fi + - name: Typecheck run: pnpm run typecheck From a83c71e5d7e7c54b0d4d87c776b983f760a78b02 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:52:21 +0100 Subject: [PATCH 4/4] docs: explain why dist/ is committed --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 96c7e1f..10f3a00 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Recorded in full, with the evaluation evidence behind it, in [docs/0001-foundati - The Rust [`cddl`](https://github.com/anweiss/cddl) crate compiled to WASM — the original plan for this repository, retired in favour of the pure-JS foundation; the reasoning is in the decision record. - [BARE](https://datatracker.ietf.org/doc/draft-devault-bare/) already has a working TypeScript code generator, [`bare-ts/bare`](https://github.com/bare-ts/bare), closing the exact gap this project targets, but for a different, and currently still-draft (not yet a finalised RFC), wire format. If wire-mesh had chosen BARE over CBOR/CDDL, this project would likely not need to exist. +## Why dist/ is committed + +Consumers install this package as a git dependency (it isn't published to npm), and pnpm/npm run a git dependency's own build only from inside a `node_modules/` checkout -- exactly the one path Node's native TypeScript-stripping refuses to run, since it deliberately won't process a `.ts` file whose resolved path is under `node_modules`. Committing `dist/` means a git-dependency install never needs to build anything at all: it gets already-built output straight from the checkout, so that restriction never comes up, and the build config can stay a real `.ts` file for everyone actually working on this repo. CI rebuilds from source on every push and fails if the result doesn't match what's committed, so `dist/` can't silently drift from `src/`. + ## Contributing Too early for code contributions to be useful yet. If you have built CDDL tooling for JavaScript or TypeScript that we have missed, please open an issue. That is the most valuable thing anyone could tell us right now.