#!/usr/bin/env node
* ArkTS Protobuf Generator
*
* - Loads .proto files, resolves types/services and emits ArkTS (.ets) code
* - Options control int64 mode, JSON API generation, doc comments, packing and runtime bundling
* - Produces per-package directories with messages/enums/services and index exports
*/
import fs from 'fs'
import path from 'path'
import protobuf from 'protobufjs'
* Parse CLI args and normalize options with safe defaults
*/
function parseArgs() {
const args = process.argv.slice(2)
const opts = { in: './protos', out: './packages', int64: 'bigint', packed: 'on', delimited: 'off', docs: 'on', omitDefaults: 'off', json: 'off', jsonEnum: 'names', jsonStrict: 'off', concurrent: 'off', namespaceAsFile: 'off', bundleRuntime: 'on', runtimeDir: 'protobuf-core' }
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (a === '--in') opts.in = args[++i]
else if (a === '--out') opts.out = args[++i]
else if (a === '--int64') opts.int64 = args[++i]
else if (a === '--packed') opts.packed = args[++i]
else if (a === '--delimited') opts.delimited = args[++i]
else if (a === '--docs') opts.docs = args[++i]
else if (a === '--omit-defaults') opts.omitDefaults = args[++i]
else if (a === '--json') opts.json = args[++i]
else if (a === '--json-strict') opts.jsonStrict = args[++i]
else if (a === '--json-enum') opts.jsonEnum = args[++i]
else if (a === '--concurrent') opts.concurrent = args[++i]
else if (a === '--namespace-as-file') opts.namespaceAsFile = args[++i]
else if (a === '--bundle-runtime') opts.bundleRuntime = args[++i]
else if (a === '--runtime-dir') opts.runtimeDir = args[++i]
}
if (opts.int64 === 'long') opts.int64 = 'bigint'
if (!['bigint','number'].includes(opts.int64)) opts.int64 = 'bigint'
if (!['on','off'].includes(opts.packed)) opts.packed = 'on'
if (!['on','off'].includes(opts.docs)) opts.docs = 'on'
if (!['on','off'].includes(opts.omitDefaults)) opts.omitDefaults = 'off'
if (!['on','off'].includes(opts.json)) opts.json = 'off'
if (!['on','off'].includes(opts.jsonStrict)) opts.jsonStrict = 'off'
if (!['names','numbers','accept-both'].includes(opts.jsonEnum)) opts.jsonEnum = 'names'
if (!['off','sendable'].includes(opts.concurrent)) opts.concurrent = 'off'
if (!['on','off'].includes(opts.namespaceAsFile)) opts.namespaceAsFile = 'off'
if (!['on','off'].includes(opts.bundleRuntime)) opts.bundleRuntime = 'on'
if (!opts.runtimeDir || typeof opts.runtimeDir !== 'string') opts.runtimeDir = 'protobuf-core'
return opts
}
* Ensure directory exists (mkdir -p)
*/
function ensureDir(p) {
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true })
}
* Derive namespace segments from a fullName (without the trailing type name)
*/
function packageSegmentsFromObj(obj) {
const fn = (obj.fullName || '').replace(/^\./, '')
const parts = fn.split('.')
return parts.length > 1 ? parts.slice(0, parts.length - 1) : ['default']
}
* Resolve output base directory for a namespace depending on layout mode
*/
function outBaseDirForSegments(opts, segs) {
return opts.namespaceAsFile === 'on' ? path.join(opts.out, ...segs) : path.join(opts.out, segs[0] || 'default')
}
* Compute output file path (messages/enums/services) for a fully-qualified name
*/
function outPathForFull(opts, fullName, kind) {
const parts = fullName.replace(/^\./,'').split('.')
const segs = parts.length > 1 ? parts.slice(0, parts.length - 1) : ['default']
const name = parts[parts.length - 1]
const base = outBaseDirForSegments(opts, segs)
return path.join(base, kind, name + '.ets')
}
* Convert raw comment text into ArkTS JSDoc block lines
*/
function formatDoc(comment) {
if (!comment) return []
const safe = String(comment).replace(/\*\//g, '*\\/')
const lines = safe.split(/\r?\n/)
const out = ['/**']
for (const l of lines) out.push(` * ${l}`)
out.push(' */')
return out
}
* Map protobuf field type to ArkTS type name
*/
function typeFor(field, int64Mode) {
switch (field.type) {
case 'double':
case 'float':
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32':
return 'number'
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64':
return int64Mode === 'number' ? 'number' : 'bigint'
case 'bool':
return 'boolean'
case 'string':
return 'string'
case 'bytes':
return 'Uint8Array'
default:
if (field.resolvedType) {
return field.resolvedType.name
}
return 'number'
}
}
* Sanitize identifier to avoid ArkTS reserved keywords
*/
function safeIdent(name) {
const reserved = new Set(['export','default','class','enum','switch','case','function','let','const','var','implements','interface','package','private','protected','public','static','yield','await','delete','in','of','return'])
return reserved.has(name) ? name + '_' : name
}
* Compute protobuf wire type for the given field
*/
function wireTypeFor(field) {
if (field.resolvedType) {
if (field.resolvedType instanceof protobuf.Enum) return 0
return 2
}
switch (field.type) {
case 'string':
case 'bytes':
return 2
case 'float':
case 'fixed32':
case 'sfixed32':
return 5
case 'double':
case 'fixed64':
case 'sfixed64':
return 1
default:
return 0
}
}
* Whether a field is implicitly packable when repeated
*/
function isPackable(f) {
if (f.resolvedType) return f.resolvedType instanceof protobuf.Enum
switch (f.type) {
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32':
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64':
case 'float':
case 'double':
case 'bool':
return true
default:
return false
}
}
* Select Reader method name for the field (honors int64 mode)
*/
function readerMethodName(f, int64Mode) {
if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) return 'int32'
switch (f.type) {
case 'string': return 'string'
case 'bytes': return 'bytes'
case 'bool': return 'bool'
case 'int32':
case 'uint32': return 'int32'
case 'sint32': return 'sint32'
case 'fixed32': return 'fixed32'
case 'sfixed32': return 'sfixed32'
case 'int64':
return int64Mode === 'number' ? 'int64Number' : 'int64'
case 'uint64':
return int64Mode === 'number' ? 'uint64Number' : 'uint64'
case 'sint64': return 'sint64'
case 'fixed64': return 'fixed64'
case 'sfixed64': return 'sfixed64'
case 'float': return 'float'
case 'double': return 'double'
default: return 'int32'
}
}
* Get JSON type for a protobuf field (for JSON interface generation)
* Returns the TypeScript type used in JSON interfaces
*/
function getJsonType(field, int64Mode, aliasMap) {
if (field.resolvedType) {
if (field.resolvedType instanceof protobuf.Enum) {
return 'string | number'
} else {
const full = (field.resolvedType.fullName || '').replace(/^\./, '')
const alias = (aliasMap.get(full) && aliasMap.get(full).local) || field.resolvedType.name
return `${alias}JSON`
}
}
switch (field.type) {
case 'string': return 'string'
case 'bool': return 'boolean'
case 'bytes': return 'string'
case 'double':
case 'float':
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32':
return 'number'
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64':
return int64Mode === 'number' ? 'number' : 'string'
default:
return 'Object'
}
}
* Get map key type for JSON (always string)
*/
function getJsonMapKeyType(field) {
return 'string'
}
* Generate JSON interface for a message
* This interface defines the shape of JSON objects for toJson/fromJson
*
* IMPORTANT: For ArkTS compatibility, Map fields are represented as arrays of {key, value} pairs
* instead of objects, since ArkTS doesn't allow index access like obj[key]
*/
function generateJsonInterface(className, fields, oneofs, int64Mode, aliasMap) {
const lines = []
lines.push(`export interface ${className}JSON {`)
for (const f of fields) {
const fname = f.name
let jsonType
if (f.map) {
const keyType = getJsonMapKeyType(f)
const valueType = getJsonType(f, int64Mode, aliasMap)
jsonType = `Array<{key: ${keyType}, value: ${valueType}}>`
} else if (f.repeated) {
const elemType = getJsonType(f, int64Mode, aliasMap)
jsonType = `Array<${elemType}>`
} else {
jsonType = getJsonType(f, int64Mode, aliasMap)
}
lines.push(` ${fname}?: ${jsonType}`)
}
for (const g of oneofs) {
}
lines.push(`}`)
return lines
}
* Generate CreateInput interface for create() method
* ArkTS doesn't allow object literal types, so we must define an interface
*/
function generateCreateInterface(className, fields, oneofs, int64Mode, aliasMap) {
const lines = []
lines.push(`export interface ${className}CreateInput {`)
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
const ftype = f.resolvedType
? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name)
: typeFor(f, int64Mode)
const finalType = f.repeated ? `${ftype}[]` : ftype
lines.push(` ${fnameSafe}?: ${finalType}`)
}
for (const g of oneofs) {
lines.push(` ${g.name}?: { case: string; value?: Object }`)
}
lines.push(`}`)
return lines
}
* Generate toJson method body for ArkTS compatibility
* Uses dot notation instead of index access
*/
function generateToJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg) {
const lines = []
lines.push(` static toJson(m: ${className}): ${className}JSON {`)
lines.push(` const o: ${className}JSON = {}`)
for (const f of fields) {
const fname = f.name
const fnameSafe = safeIdent(fname)
const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
if (f.map) {
lines.push(` if (m.${fnameSafe}.size > 0) {`)
lines.push(` const arr: Array<{key: string, value: any}> = []`)
lines.push(` for (const [k, v] of m.${fnameSafe}.entries()) {`)
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` arr.push({key: String(k), value: ${alias}.toJson(v)})`)
} else {
const valueExpr = getJsonValueExpression('v', f, int64Mode, alias, jsonEnumMode)
lines.push(` arr.push({key: String(k), value: ${valueExpr}})`)
}
lines.push(` }`)
lines.push(` o.${fname} = arr`)
lines.push(` }`)
} else if (f.repeated) {
lines.push(` if (m.${fnameSafe}.length > 0) {`)
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` o.${fname} = m.${fnameSafe}.map(v => ${alias}.toJson(v))`)
} else {
const mapExpr = getJsonValueExpression('v', f, int64Mode, alias, jsonEnumMode)
lines.push(` o.${fname} = m.${fnameSafe}.map(v => ${mapExpr})`)
}
lines.push(` }`)
} else {
const defaultCheck = getFieldDefaultCheck(`m.${fnameSafe}`, f, int64Mode)
lines.push(` if (${defaultCheck}) {`)
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` o.${fname} = ${alias}.toJson(m.${fnameSafe})`)
} else {
const valueExpr = getJsonValueExpression(`m.${fnameSafe}`, f, int64Mode, alias, jsonEnumMode)
lines.push(` o.${fname} = ${valueExpr}`)
}
lines.push(` }`)
}
}
for (const g of oneofs) {
lines.push(` if (m.${g.name}) {`)
lines.push(` switch (m.${g.name}.kind) {`)
for (const fname of g.names) {
const f = msg.fields[fname]
const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
const valueExpr = getJsonValueExpression(`m.${g.name}.${fname}`, f, int64Mode, alias, jsonEnumMode)
lines.push(` case '${fname}': o.${fname} = ${valueExpr}; break`)
}
lines.push(` }`)
lines.push(` }`)
}
lines.push(` return o`)
lines.push(` }`)
return lines
}
* Get expression to convert a value to JSON format
*/
function getJsonValueExpression(valueRef, field, int64Mode, alias, jsonEnumMode) {
if (field.resolvedType && (field.resolvedType instanceof protobuf.Enum)) {
if (jsonEnumMode === 'names') {
return `${alias}_JSON_N2S.get(${valueRef} as number) ?? String(${valueRef} as number)`
} else {
return `${valueRef} as number`
}
}
switch (field.type) {
case 'bytes':
return `encodeBase64(${valueRef} as Uint8Array)`
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64':
return int64Mode === 'number' ? `${valueRef} as number` : `(${valueRef} as bigint).toString()`
default:
return valueRef
}
}
* Get check expression for non-default value
*/
function getFieldDefaultCheck(valueRef, field, int64Mode) {
if (field.resolvedType) {
return `${valueRef} !== null`
}
const t = typeFor(field, int64Mode)
switch (t) {
case 'string': return `${valueRef} !== ''`
case 'boolean': return `${valueRef} !== false`
case 'number': return `${valueRef} !== 0`
case 'bigint': return `${valueRef} !== 0n`
case 'Uint8Array': return `${valueRef}.length > 0`
default: return `${valueRef} !== null`
}
}
* Generate fromJson method body for ArkTS compatibility
*/
function generateFromJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg) {
const lines = []
lines.push(` static fromJson(j: ${className}JSON): ${className} {`)
lines.push(` const m = new ${className}()`)
for (const f of fields) {
const fname = f.name
const fnameSafe = safeIdent(fname)
const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
lines.push(` if (j.${fname} !== undefined) {`)
if (f.map) {
lines.push(` for (const entry of j.${fname}) {`)
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` m.${fnameSafe}.set(entry.key, ${alias}.fromJson(entry.value))`)
} else {
const valueExpr = getFromJsonValueExpression('entry.value', f, int64Mode, alias, jsonEnumMode)
lines.push(` m.${fnameSafe}.set(entry.key, ${valueExpr})`)
}
lines.push(` }`)
} else if (f.repeated) {
lines.push(` for (const item of j.${fname}) {`)
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` m.${fnameSafe}.push(${alias}.fromJson(item))`)
} else {
const valueExpr = getFromJsonValueExpression('item', f, int64Mode, alias, jsonEnumMode)
lines.push(` m.${fnameSafe}.push(${valueExpr})`)
}
lines.push(` }`)
} else {
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` m.${fnameSafe} = ${alias}.fromJson(j.${fname})`)
} else {
const valueExpr = getFromJsonValueExpression(`j.${fname}`, f, int64Mode, alias, jsonEnumMode)
lines.push(` m.${fnameSafe} = ${valueExpr}`)
}
}
lines.push(` }`)
}
lines.push(` return m`)
lines.push(` }`)
return lines
}
* Get expression to convert from JSON value
*/
function getFromJsonValueExpression(valueRef, field, int64Mode, alias, jsonEnumMode) {
if (field.resolvedType && (field.resolvedType instanceof protobuf.Enum)) {
if (jsonEnumMode === 'names') {
return `${alias}_JSON_S2N.get(String(${valueRef})) as ${alias}`
} else if (jsonEnumMode === 'accept-both') {
return `typeof ${valueRef} === 'number' ? ${valueRef} as ${alias} : ${alias}_JSON_S2N.get(String(${valueRef})) as ${alias}`
} else {
return `Number(${valueRef}) as ${alias}`
}
}
switch (field.type) {
case 'bytes':
return `decodeBase64(String(${valueRef}))`
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64':
return int64Mode === 'number' ? `Number(${valueRef})` : `BigInt(String(${valueRef}))`
case 'string':
return `String(${valueRef})`
case 'bool':
return `Boolean(${valueRef})`
case 'double':
case 'float':
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32':
return `Number(${valueRef})`
default:
return valueRef
}
}
* Emit ArkTS message class with encode/decode/verify and optional JSON methods
*/
function renderMessage(pkgDir, pkgName, msg, int64Mode, packedOn, docsOn, omitDefaultsOn, jsonOn, commentOf, jsonEnumMode, jsonStrict, opts) {
const className = msg.name
const fields = msg.fieldsArray
const oneofs = msg.oneofs ? Object.entries(msg.oneofs).map(([name, oo]) => ({ name, names: oo.oneof })) : []
const oneofsWithComments = (msg.oneofsArray || []).map(o => ({ name: o.name, comment: o.comment }))
const currMsgDir = path.join(pkgDir, 'messages')
const rtRelRaw = path.relative(currMsgDir, path.join(opts.out, opts.runtimeDir)).replace(/\\/g,'/')
const rtRel = rtRelRaw.startsWith('.') ? rtRelRaw : './' + rtRelRaw
const isTimestamp = msg.fullName === '.google.protobuf.Timestamp'
const isDuration = msg.fullName === '.google.protobuf.Duration'
const isAny = msg.fullName === '.google.protobuf.Any'
const utilImports = new Set()
for (const f of fields) {
if (f.type === 'bytes') {
utilImports.add('encodeBase64')
utilImports.add('decodeBase64')
}
}
if (jsonOn) {
if (isTimestamp) { utilImports.add('timestampToJson'); utilImports.add('timestampFromJson') }
if (isDuration) { utilImports.add('durationToJson'); utilImports.add('durationFromJson') }
if (isAny) { utilImports.add('encodeBase64'); utilImports.add('decodeBase64') }
if (msg.fullName === '.google.protobuf.BytesValue') { utilImports.add('encodeBase64'); utilImports.add('decodeBase64') }
}
const baseImports = ["Message", "Visitor", "Reader", "Writer", "BinaryEncodingVisitor"]
if (jsonOn) baseImports.push("JsonEncodingVisitor")
const imports = []
if (utilImports.size > 0) {
imports.push(`import { ${baseImports.join(', ')}, ${Array.from(utilImports).join(', ')} } from '${rtRel}'`)
} else {
imports.push(`import { ${baseImports.join(', ')} } from '${rtRel}'`)
}
const aliasMap = new Map()
const usedNames = new Map()
const depList = []
for (const f of fields) {
if (!f.resolvedType) continue
const full = (f.resolvedType.fullName || '').replace(/^\./, '')
const selfFull = (msg.fullName || '').replace(/^\./, '')
if (full === selfFull) continue
if (aliasMap.has(full)) continue
const isEnum = f.resolvedType instanceof protobuf.Enum
const depPkg = full.split('.')[0] || 'default'
const relRaw = path.relative(currMsgDir, outPathForFull(opts, full, isEnum ? 'enums' : 'messages')).replace(/\\/g,'/')
const relNoExt = relRaw.replace(/\.ets$/,'')
const rel = relNoExt.startsWith('.') ? relNoExt : './' + relNoExt
let local = f.resolvedType.name
if (usedNames.has(local) && usedNames.get(local) !== full) local = `${local}_${depPkg}`
usedNames.set(local, full)
aliasMap.set(full, { local, base: f.resolvedType.name, rel, isEnum })
depList.push({ local, base: f.resolvedType.name, rel, isEnum })
}
const lines = []
lines.push(...imports)
if (depList.length) {
depList.sort((a,b)=> a.local.localeCompare(b.local) || a.rel.localeCompare(b.rel))
for (const d of depList) {
if (jsonOn) {
if (d.local === d.base) {
if (d.isEnum) {
lines.push(`import { ${d.base} } from '${d.rel}'`)
} else {
lines.push(`import { ${d.base} } from '${d.rel}'`)
lines.push(`import { ${d.base}JSON } from '${d.rel}'`)
}
} else {
if (d.isEnum) {
lines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
} else {
lines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
lines.push(`import { ${d.base}JSON as ${d.local}JSON } from '${d.rel}'`)
}
}
} else {
if (d.local === d.base) lines.push(`import { ${d.base} } from '${d.rel}'`)
else lines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
}
}
}
if (jsonOn && jsonEnumMode === 'names') {
const emitted = new Set()
for (const f of fields) {
if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) {
const full = (f.resolvedType.fullName || '').replace(/^\./,'')
const alias = (aliasMap.get(full) && aliasMap.get(full).local) || f.resolvedType.name
if (emitted.has(alias)) continue
emitted.add(alias)
const values = Object.entries(f.resolvedType.values)
const n2sPairs = values.map(([k,v])=> ` [${v}, ${JSON.stringify(k)}]`).join(',\n')
const s2nPairs = values.map(([k,v])=> ` [${JSON.stringify(k)}, ${v}]`).join(',\n')
lines.push(`const ${alias}_JSON_N2S = new Map<number, string>([\n${n2sPairs}\n])`)
lines.push(`const ${alias}_JSON_S2N = new Map<string, number>([\n${s2nPairs}\n])`)
}
}
}
const fullName = (msg.fullName || '').replace(/^\./,'')
if (docsOn) {
const mc = commentOf ? commentOf(fullName) : (msg.comment || (msg.commentRange ? msg.commentRange.leadingComments : ''))
if (mc) lines.push(...formatDoc(mc))
}
if (opts.concurrent === 'sendable') lines.push('@Sendable')
if (jsonOn) {
const jsonInterfaceLines = generateJsonInterface(className, fields, oneofs, int64Mode, aliasMap)
lines.push(...jsonInterfaceLines)
lines.push('')
}
const createInterfaceLines = generateCreateInterface(className, fields, oneofs, int64Mode, aliasMap)
lines.push(...createInterfaceLines)
lines.push('')
lines.push(`export class ${className} extends Message {`)
lines.push(` readonly $typeName: string = '${fullName}'`)
lines.push(` static readonly typeName: string = '${fullName}'`)
lines.push('')
const localType = (ff) => ff.resolvedType ? (aliasMap.get((ff.resolvedType.fullName||'').replace(/^\./,''))?.local || ff.resolvedType.name) : typeFor(ff, int64Mode)
const getFieldDefault = (f) => {
if (f.map) return `new Map()`
if (f.repeated) return `[]`
const t = localType(f)
if (f.resolvedType) return 'null'
switch (t) {
case 'string': return `''`
case 'boolean': return 'false'
case 'number': return '0'
case 'bigint': return '0n'
case 'Uint8Array': return 'new Uint8Array(0)'
default: return 'null'
}
}
for (const f of fields) {
const t = localType(f)
const base = f.map ? `Map<${mapKeyType(f, int64Mode)}, ${localType(f)}>` : (f.repeated ? `Array<${t}>` : (f.resolvedType ? `${t} | null` : t))
const fnameSafe = safeIdent(f.name)
if (docsOn) {
const fc = commentOf ? commentOf(`${fullName}#${f.name}`) : (f.comment || (msg.comments && msg.comments[f.name]))
if (fc) lines.push(...formatDoc(fc).map(s => ' ' + s))
}
lines.push(` ${fnameSafe}: ${base} = ${getFieldDefault(f)}`)
}
for (const g of oneofs) {
const union = g.names.map(n => { const f = msg.fields[n]; return `{ kind: '${n}', ${n}: ${localType(f)} }` }).join(' | ')
if (docsOn) {
const oc = commentOf ? commentOf(`${fullName}#oneof:${g.name}`) : (oneofsWithComments.find(x => x.name === g.name)?.comment)
if (oc) lines.push(...formatDoc(oc).map(s => ' ' + s))
}
lines.push(` ${g.name}: (${union}) | undefined = undefined`)
}
lines.push('')
lines.push(` constructor() {`)
lines.push(` super()`)
lines.push(' }')
lines.push('')
lines.push(` clone(): ${className} {`)
lines.push(` const clone = new ${className}()`)
lines.push(` clone.mergeFrom(this)`)
lines.push(` return clone`)
lines.push(` }`)
lines.push('')
const createParamType = (fields.length > 0 || oneofs.length > 0) ? `${className}CreateInput` : 'Record<string, never>'
lines.push(` static create(init?: ${createParamType}): ${className} {`)
lines.push(` const msg = new ${className}()`)
lines.push(` if (init) {`)
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
if (f.map || f.repeated) {
lines.push(` if (init.${fnameSafe} !== undefined) msg.${fnameSafe} = init.${fnameSafe}`)
} else if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
lines.push(` if (init.${fnameSafe} !== undefined) {`)
lines.push(` msg.${fnameSafe} = init.${fnameSafe} instanceof ${alias}`)
lines.push(` ? init.${fnameSafe}`)
lines.push(` : ${alias}.create(init.${fnameSafe})`)
lines.push(` }`)
} else {
lines.push(` if (init.${fnameSafe} !== undefined) msg.${fnameSafe} = init.${fnameSafe}`)
}
}
for (const g of oneofs) {
lines.push(` if (init.${g.name} !== undefined) msg.${g.name} = init.${g.name}`)
}
lines.push(` }`)
lines.push(` return msg`)
lines.push(` }`)
lines.push('')
const getVisitorMethod = (f) => {
const prefix = f.map ? 'visitMap' : (f.repeated ? 'visitRepeated' : 'visit')
if (f.map) {
const kt = mapKeyType(f, int64Mode)
const keyPart = kt === 'string' ? 'String' : (kt === 'number' ? 'Int32' : 'Int64')
if (f.resolvedType) {
if (f.resolvedType instanceof protobuf.Enum) {
return `${prefix}${keyPart}Int32`
} else {
return `${prefix}${keyPart}Message`
}
}
const vt = typeFor(f, int64Mode)
const valPart = vt === 'string' ? 'String' : (vt === 'number' ? 'Int32' : (vt === 'bigint' ? 'Int64' : (vt === 'boolean' ? 'Bool' : (vt === 'Uint8Array' ? 'Bytes' : 'Int32'))))
return `${prefix}${keyPart}${valPart}`
}
if (f.resolvedType) {
if (f.resolvedType instanceof protobuf.Enum) {
return `${prefix}Int32`
} else {
return `${prefix}Message`
}
}
const t = typeFor(f, int64Mode)
switch (t) {
case 'string': return `${prefix}String`
case 'number': {
if (['int32','uint32','sint32','fixed32','sfixed32'].includes(f.type)) return `${prefix}Int32`
if (['float'].includes(f.type)) return `${prefix}Float`
if (['double'].includes(f.type)) return `${prefix}Double`
return `${prefix}Int32`
}
case 'bigint': return `${prefix}Int64`
case 'boolean': return `${prefix}Bool`
case 'Uint8Array': return `${prefix}Bytes`
default: return `${prefix}Int32`
}
}
lines.push(` traverse(v: Visitor): void {`)
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
const fieldNum = f.id
const visitorMethod = getVisitorMethod(f)
if (f.map) {
lines.push(` if (this.${fnameSafe}.size > 0) {`)
lines.push(` v.${visitorMethod}(this.${fnameSafe}, ${fieldNum})`)
lines.push(` }`)
} else if (f.repeated) {
lines.push(` if (this.${fnameSafe}.length > 0) {`)
lines.push(` v.${visitorMethod}(this.${fnameSafe}, ${fieldNum})`)
lines.push(` }`)
} else {
const defVal = getFieldDefault(f)
let condition
if (f.resolvedType) {
condition = `this.${fnameSafe} !== null`
} else {
const t = typeFor(f, int64Mode)
switch (t) {
case 'string': condition = `this.${fnameSafe} !== ''`; break
case 'number': condition = `this.${fnameSafe} !== 0`; break
case 'bigint': condition = `this.${fnameSafe} !== 0n`; break
case 'boolean': condition = `this.${fnameSafe} !== false`; break
case 'Uint8Array': condition = `this.${fnameSafe}.length > 0`; break
default: condition = `this.${fnameSafe} !== null`
}
}
lines.push(` if (${condition}) {`)
lines.push(` v.${visitorMethod}(this.${fnameSafe}, ${fieldNum})`)
lines.push(` }`)
}
}
for (const g of oneofs) {
lines.push(` if (this.${g.name} !== undefined) {`)
lines.push(` switch (this.${g.name}.kind) {`)
for (const n of g.names) {
const f = msg.fields[n]
const visitorMethod = getVisitorMethod(f)
lines.push(` case '${n}':`)
lines.push(` v.${visitorMethod}(this.${g.name}.${n}, ${f.id})`)
lines.push(` break`)
}
lines.push(` }`)
lines.push(` }`)
}
lines.push(` }`)
lines.push('')
lines.push(` decodeFrom(r: Reader, len?: number): void {`)
lines.push(` const endPos = len === undefined ? r.len : r.pos + len`)
lines.push('')
lines.push(` while (r.pos < endPos) {`)
lines.push(` const tag = r.uint32()`)
lines.push(` const fieldNum = tag >>> 3`)
lines.push('')
lines.push(` switch (fieldNum) {`)
for (const f of fields) {
const no = f.id
const fnameSafe = safeIdent(f.name)
lines.push(` case ${no}: {`)
if (f.map) {
const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
const kt = mapKeyType(f, int64Mode)
const vt = localType(f)
lines.push(` {`)
lines.push(` const l = r.uint32()`)
lines.push(` const end2 = r.pos + l`)
lines.push(` let k: ${kt}`)
lines.push(` let v: ${vt}`)
lines.push(` while (r.pos < end2) {`)
lines.push(` const tt = r.uint32()`)
lines.push(` switch (tt >>> 3) {`)
lines.push(` case 1: {`)
switch (f.keyType) {
case 'string': lines.push(` k = r.string()`); break
case 'bool': lines.push(` k = r.bool()`); break
case 'int32':
case 'uint32': lines.push(` k = r.int32()`); break
case 'sint32': lines.push(` k = r.sint32()`); break
case 'fixed32': lines.push(` k = r.fixed32()`); break
case 'sfixed32': lines.push(` k = r.sfixed32()`); break
case 'int64':
case 'uint64': lines.push(int64Mode === 'number' ? ` k = r.int64Number()` : ` k = r.int64()`); break
case 'sint64': lines.push(int64Mode === 'number' ? ` k = Number(r.sint64())` : ` k = r.sint64()`); break
case 'fixed64': lines.push(int64Mode === 'number' ? ` k = Number(r.fixed64())` : ` k = r.fixed64()`); break
case 'sfixed64': lines.push(int64Mode === 'number' ? ` k = Number(r.sfixed64())` : ` k = r.sfixed64()`); break
default: lines.push(` k = r.int32()`); break
}
lines.push(` break`)
lines.push(` }`)
lines.push(` case 2: {`)
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(` const msgLen = r.uint32()`)
lines.push(` v = new ${alias}()`)
lines.push(` v.decodeFrom(r, msgLen)`)
} else {
switch (f.type) {
case 'string': lines.push(` v = r.string()`); break
case 'bytes': lines.push(` v = r.bytes()`); break
case 'bool': lines.push(` v = r.bool()`); break
case 'int32':
case 'uint32': lines.push(` v = r.int32()`); break
case 'sint32': lines.push(` v = r.sint32()`); break
case 'fixed32': lines.push(` v = r.fixed32()`); break
case 'sfixed32': lines.push(` v = r.sfixed32()`); break
case 'int64': lines.push(int64Mode === 'number' ? ` v = r.int64Number()` : ` v = r.int64()`); break
case 'uint64': lines.push(int64Mode === 'number' ? ` v = r.uint64Number()` : ` v = r.uint64()`); break
case 'sint64': lines.push(int64Mode === 'number' ? ` v = Number(r.sint64())` : ` v = r.sint64()`); break
case 'fixed64': lines.push(int64Mode === 'number' ? ` v = Number(r.fixed64())` : ` v = r.fixed64()`); break
case 'sfixed64': lines.push(int64Mode === 'number' ? ` v = Number(r.sfixed64())` : ` v = r.sfixed64()`); break
case 'float': lines.push(` v = r.float()`); break
case 'double': lines.push(` v = r.double()`); break
default: lines.push(` v = r.int32()`); break
}
}
lines.push(` break`)
lines.push(` }`)
lines.push(` default: r.skipType(tt & 7)`)
lines.push(` }`)
lines.push(` }`)
lines.push(` this.${fnameSafe}.set(k as ${kt}, v as ${vt})`)
lines.push(` }`)
} else if (f.repeated) {
if (isPackable(f)) {
const rm = readerMethodName(f, int64Mode)
lines.push(` if ((tag & 7) === 2) {`)
lines.push(` const l = r.uint32()`)
lines.push(` const end2 = r.pos + l`)
lines.push(` while (r.pos < end2) {`)
if (['sint64','fixed64','sfixed64'].includes(f.type)) {
lines.push(int64Mode === 'number'
? ` this.${fnameSafe}.push(Number(r.${f.type}()))`
: ` this.${fnameSafe}.push(r.${f.type}())`)
} else {
lines.push(` this.${fnameSafe}.push(r.${rm}())`)
}
lines.push(` }`)
lines.push(` } else {`)
}
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
lines.push(isPackable(f) ? ` const msgLen = r.uint32()` : ` const msgLen = r.uint32()`)
lines.push(isPackable(f) ? ` const msg = new ${alias}()` : ` const msg = new ${alias}()`)
lines.push(isPackable(f) ? ` msg.decodeFrom(r, msgLen)` : ` msg.decodeFrom(r, msgLen)`)
lines.push(isPackable(f) ? ` this.${fnameSafe}.push(msg)` : ` this.${fnameSafe}.push(msg)`)
} else {
const rm = readerMethodName(f, int64Mode)
if (['sint64','fixed64','sfixed64'].includes(f.type)) {
lines.push(int64Mode === 'number'
? (isPackable(f) ? ` this.${fnameSafe}.push(Number(r.${f.type}()))` : ` this.${fnameSafe}.push(Number(r.${f.type}()))`)
: (isPackable(f) ? ` this.${fnameSafe}.push(r.${f.type}())` : ` this.${fnameSafe}.push(r.${f.type}())`))
} else {
lines.push(isPackable(f) ? ` this.${fnameSafe}.push(r.${rm}())` : ` this.${fnameSafe}.push(r.${rm}())`)
}
}
if (isPackable(f)) lines.push(` }`)
} else {
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
lines.push(` {`)
lines.push(` const msgLen = r.uint32()`)
lines.push(` this.${fnameSafe} = new ${alias}()`)
lines.push(` this.${fnameSafe}.decodeFrom(r, msgLen)`)
lines.push(` }`)
} else {
const rm = readerMethodName(f, int64Mode)
if (['sint64','fixed64','sfixed64'].includes(f.type)) {
lines.push(int64Mode === 'number'
? ` this.${fnameSafe} = Number(r.${f.type}())`
: ` this.${fnameSafe} = r.${f.type}()`)
} else {
lines.push(` this.${fnameSafe} = r.${rm}()`)
}
}
}
if (f.partOf) {
lines.push(` this.${f.partOf.name} = { kind: '${f.name}', ${f.name}: this.${fnameSafe} }`)
}
lines.push(` break`)
lines.push(` }`)
}
lines.push(` default: {`)
lines.push(` const wireType = tag & 7`)
lines.push(` const start = r.pos`)
lines.push(` r.skipType(wireType)`)
lines.push(` this.unknownFields.append(r.view(start, r.pos))`)
lines.push(` }`)
lines.push(` }`)
lines.push(` }`)
lines.push(` }`)
lines.push('')
lines.push(` static encode(m: ${className}, w: Writer): void {`)
lines.push(` const v = new BinaryEncodingVisitor(w)`)
lines.push(` m.traverse(v)`)
lines.push(` }`)
lines.push('')
lines.push(` static decode(r: Reader): ${className} {`)
lines.push(` const m = new ${className}()`)
lines.push(` m.decodeFrom(r)`)
lines.push(` return m`)
lines.push(` }`)
lines.push('')
lines.push(` static fromBinary(data: Uint8Array): ${className} {`)
lines.push(` try {`)
lines.push(` return new ${className}().fromBinary(data) as ${className}`)
lines.push(` } catch (e) {`)
lines.push(` throw new Error(\`Failed to parse ${className} from binary: \${e}\`)`)
lines.push(` }`)
lines.push(` }`)
lines.push('')
lines.push(` validate(): string | null {`)
for (const f of fields) {
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
const alias = aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name
const fnameSafe = safeIdent(f.name)
if (f.repeated) {
lines.push(` for (const item of this.${fnameSafe}) {`)
lines.push(` const err = item.validate()`)
lines.push(` if (err !== null) return '${f.name}: ' + err`)
lines.push(` }`)
} else if (f.map) {
lines.push(` for (const [k, v] of this.${fnameSafe}.entries()) {`)
lines.push(` const err = v.validate()`)
lines.push(` if (err !== null) return '${f.name}[' + String(k) + ']: ' + err`)
lines.push(` }`)
} else {
lines.push(` if (this.${fnameSafe} !== null) {`)
lines.push(` const err = this.${fnameSafe}.validate()`)
lines.push(` if (err !== null) return '${f.name}: ' + err`)
lines.push(` }`)
}
}
}
lines.push(` return null`)
lines.push(` }`)
lines.push('')
lines.push(` clear(): ${className} {`)
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
lines.push(` this.${fnameSafe} = ${getFieldDefault(f)}`)
}
for (const g of oneofs) {
lines.push(` this.${g.name} = undefined`)
}
lines.push(` this.unknownFields.clear()`)
lines.push(` return this`)
lines.push(` }`)
lines.push('')
lines.push(` isEmpty(): boolean {`)
const checks = []
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
if (f.map) {
checks.push(`this.${fnameSafe}.size === 0`)
} else if (f.repeated) {
checks.push(`this.${fnameSafe}.length === 0`)
} else if (f.resolvedType) {
checks.push(`this.${fnameSafe} === null`)
} else {
const t = typeFor(f, int64Mode)
switch (t) {
case 'string': checks.push(`this.${fnameSafe} === ''`); break
case 'number': checks.push(`this.${fnameSafe} === 0`); break
case 'bigint': checks.push(`this.${fnameSafe} === 0n`); break
case 'boolean': checks.push(`this.${fnameSafe} === false`); break
case 'Uint8Array': checks.push(`this.${fnameSafe}.length === 0`); break
default: checks.push(`this.${fnameSafe} === null`)
}
}
}
for (const g of oneofs) {
checks.push(`this.${g.name} === undefined`)
}
checks.push(`this.unknownFields.isEmpty()`)
if (checks.length <= 3) {
lines.push(` return ${checks.join(' && ')}`)
} else {
lines.push(` return ${checks[0]}`)
for (let i = 1; i < checks.length; i++) {
lines.push(` && ${checks[i]}`)
}
}
lines.push(` }`)
if (jsonOn) {
const fullNoDot = (msg.fullName||'').replace(/^\./,'')
const isTimestamp = (fullNoDot === 'google.protobuf.Timestamp')
const isDuration = (fullNoDot === 'google.protobuf.Duration')
const isAny = (fullNoDot === 'google.protobuf.Any')
const isStruct = (fullNoDot === 'google.protobuf.Struct')
const isValue = (fullNoDot === 'google.protobuf.Value')
const isListValue = (fullNoDot === 'google.protobuf.ListValue')
const isFieldMask = (fullNoDot === 'google.protobuf.FieldMask')
const isStringValue = (fullNoDot === 'google.protobuf.StringValue')
const isBoolValue = (fullNoDot === 'google.protobuf.BoolValue')
const isBytesValue = (fullNoDot === 'google.protobuf.BytesValue')
const isDoubleValue = (fullNoDot === 'google.protobuf.DoubleValue')
const isFloatValue = (fullNoDot === 'google.protobuf.FloatValue')
const isInt32Value = (fullNoDot === 'google.protobuf.Int32Value')
const isUInt32Value = (fullNoDot === 'google.protobuf.UInt32Value')
const isInt64Value = (fullNoDot === 'google.protobuf.Int64Value')
const isUInt64Value = (fullNoDot === 'google.protobuf.UInt64Value')
const structAlias = (aliasMap.get('google.protobuf.Struct') && aliasMap.get('google.protobuf.Struct').local) || 'Struct'
const listAlias = (aliasMap.get('google.protobuf.ListValue') && aliasMap.get('google.protobuf.ListValue').local) || 'ListValue'
const valueAlias = (aliasMap.get('google.protobuf.Value') && aliasMap.get('google.protobuf.Value').local) || 'Value'
if (isTimestamp) {
lines.push(' static toJson(m: ' + className + '): string {')
lines.push(' return timestampToJson(m.seconds as bigint, (m.nanos as number) ?? 0)')
lines.push(' }')
lines.push(' static fromJson(j: any): ' + className + ' {')
lines.push(' const t = timestampFromJson(String(j))')
lines.push(' return new ' + className + '({ seconds: t.seconds, nanos: t.nanos })')
lines.push(' }')
} else if (isDuration) {
lines.push(' static toJson(m: ' + className + '): string {')
lines.push(' return durationToJson(m.seconds as bigint, (m.nanos as number) ?? 0)')
lines.push(' }')
lines.push(' static fromJson(j: any): ' + className + ' {')
lines.push(' const t = durationFromJson(String(j))')
lines.push(' return new ' + className + '({ seconds: t.seconds, nanos: t.nanos })')
lines.push(' }')
} else if (isAny) {
lines.push(' static toJson(m: ' + className + '): {typeUrl: string, value: string} {')
lines.push(' const av = m as { type_url: string, value: Uint8Array }')
lines.push(' return { typeUrl: av.type_url, value: encodeBase64(av.value) }')
lines.push(' }')
lines.push(' static fromJson(j: {typeUrl: string, value: string}): ' + className + ' {')
lines.push(' return new ' + className + '({ type_url: j.typeUrl, value: decodeBase64(j.value) })')
lines.push(' }')
} else if (isStruct) {
lines.push(' static toJson(m: ' + className + '): Record<string, Object> {')
lines.push(' const o: Record<string, Object> = {}')
lines.push(' const mp = (m as { fields?: Map<string, ' + valueAlias + '> }).fields')
lines.push(' if (mp) { for (const [k,v] of mp.entries()) { o[k] = ' + valueAlias + '.toJson(v) } }')
lines.push(' return o')
lines.push(' }')
lines.push(' static fromJson(j: Record<string, Object>): ' + className + ' {')
lines.push(' const fields: Map<string, ' + valueAlias + '> = new Map()')
lines.push(' if (j) for (const k of Object.keys(j)) fields.set(k, ' + valueAlias + '.fromJson(j[k]))')
lines.push(' return new ' + className + '({ fields: fields })')
lines.push(' }')
} else if (isValue) {
lines.push(' static toJson(m: ' + className + '): any {')
lines.push(' const vv = m as Object')
lines.push(' if (vv.null_value !== undefined) return null')
lines.push(' if (vv.number_value !== undefined) return vv.number_value')
lines.push(' if (vv.string_value !== undefined) return vv.string_value')
lines.push(' if (vv.bool_value !== undefined) return vv.bool_value')
lines.push(' if (vv.struct_value !== undefined) return ' + structAlias + '.toJson(vv.struct_value)')
lines.push(' if (vv.list_value !== undefined) return ' + listAlias + '.toJson(vv.list_value)')
lines.push(' return null')
lines.push(' }')
lines.push(' static fromJson(j: any): ' + className + ' {')
lines.push(' if (j === null) return new ' + className + '({ null_value: 0 })')
lines.push(' if (typeof j === "number") return new ' + className + '({ number_value: j })')
lines.push(' if (typeof j === "string") return new ' + className + '({ string_value: j })')
lines.push(' if (typeof j === "boolean") return new ' + className + '({ bool_value: j })')
lines.push(' if (Array.isArray(j)) return new ' + className + '({ list_value: ' + listAlias + '.fromJson(j) })')
lines.push(' return new ' + className + '({ struct_value: ' + structAlias + '.fromJson(j) })')
lines.push(' }')
} else if (isListValue) {
lines.push(' static toJson(m: ' + className + '): Array<Object> {')
lines.push(' const arr: Array<Object> = []')
lines.push(' const lv = (m as { values?: Array<' + valueAlias + '> }).values')
lines.push(' if (lv) for (const v of lv) arr.push(' + valueAlias + '.toJson(v))')
lines.push(' return arr')
lines.push(' }')
lines.push(' static fromJson(j: Array<Object>): ' + className + ' {')
lines.push(' const values: Array<' + valueAlias + '> = []')
lines.push(' if (Array.isArray(j)) for (const v of j) values.push(' + valueAlias + '.fromJson(v))')
lines.push(' return new ' + className + '({ values: values })')
lines.push(' }')
} else if (isFieldMask) {
lines.push(' static toJson(m: ' + className + '): string {')
lines.push(' const fm = (m as { paths?: Array<string> }).paths')
lines.push(' const paths = fm ?? []')
lines.push(' return paths.join(",")')
lines.push(' }')
lines.push(' static fromJson(j: string): ' + className + ' {')
lines.push(' const s = String(j)')
lines.push(' const arr = s.length ? s.split(",").map(x=> x.trim()).filter(x=> x.length>0) : []')
lines.push(' return new ' + className + '({ paths: arr })')
lines.push(' }')
} else if (isStringValue) {
lines.push(' static toJson(m: ' + className + '): string {')
lines.push(' return (m as { value?: string }).value ?? ""')
lines.push(' }')
lines.push(' static fromJson(j: string): ' + className + ' {')
lines.push(' return new ' + className + '({ value: String(j) })')
lines.push(' }')
} else if (isBoolValue) {
lines.push(' static toJson(m: ' + className + '): boolean {')
lines.push(' return (m as { value?: boolean }).value ?? false')
lines.push(' }')
lines.push(' static fromJson(j: boolean): ' + className + ' {')
lines.push(' return new ' + className + '({ value: Boolean(j) })')
lines.push(' }')
} else if (isBytesValue) {
lines.push(' static toJson(m: ' + className + '): string {')
lines.push(' return encodeBase64((m as { value?: Uint8Array }).value ?? new Uint8Array(0))')
lines.push(' }')
lines.push(' static fromJson(j: string): ' + className + ' {')
lines.push(' return new ' + className + '({ value: decodeBase64(String(j)) })')
lines.push(' }')
} else if (isDoubleValue || isFloatValue) {
lines.push(' static toJson(m: ' + className + '): number {')
lines.push(' return Number((m as { value?: number }).value ?? 0)')
lines.push(' }')
lines.push(' static fromJson(j: number): ' + className + ' {')
lines.push(' return new ' + className + '({ value: Number(j) })')
lines.push(' }')
} else if (isInt32Value || isUInt32Value) {
lines.push(' static toJson(m: ' + className + '): number {')
lines.push(' return Number((m as { value?: number }).value ?? 0)')
lines.push(' }')
lines.push(' static fromJson(j: number): ' + className + ' {')
lines.push(' return new ' + className + '({ value: Number(j) })')
lines.push(' }')
} else if (isInt64Value || isUInt64Value) {
lines.push(' static toJson(m: ' + className + '): ' + (int64Mode === 'number' ? 'number' : 'string') + ' {')
lines.push(' return ' + (int64Mode === 'number' ? 'Number((m as { value?: number }).value ?? 0)' : '(m as { value?: bigint }).value?.toString() ?? "0"') )
lines.push(' }')
lines.push(' static fromJson(j: ' + (int64Mode === 'number' ? 'number' : 'string') + '): ' + className + ' {')
lines.push(' return new ' + className + '({ value: ' + (int64Mode === 'number' ? 'Number(j)' : 'BigInt(String(j))') + ' })')
lines.push(' }')
} else {
const toJsonLines = generateToJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg)
lines.push(...toJsonLines)
lines.push('')
const fromJsonLines = generateFromJsonMethod(className, fields, oneofs, int64Mode, aliasMap, jsonEnumMode, msg)
lines.push(...fromJsonLines)
}
}
lines.push('}\n')
return lines.join('\n')
}
* Build an expression to detect default scalar/enum values for omit-defaults
*/
function defaultCheckExpr(f, int64Mode, expr) {
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) return null
switch (f.type) {
case 'string': return `${expr} === ''`
case 'bytes': return `${expr}.length === 0`
case 'bool': return `${expr} === false`
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32':
case 'float':
case 'double': return `${expr} === 0`
case 'int64':
case 'sint64':
case 'fixed64':
case 'sfixed64': return int64Mode === 'number' ? `${expr} === 0` : `${expr} === 0n`
case 'uint64': return int64Mode === 'number' ? `${expr} === 0` : `${expr} === 0n`
default:
if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) return `${expr} === 0`
return null
}
}
* Generate Writer calls to serialize a single field value
*/
function renderWriteField(f, int64Mode, expr, wvar) {
const out = []
switch (f.type) {
case 'string':
out.push(`${wvar}.string(${expr})`)
break
case 'bytes':
out.push(`${wvar}.bytes(${expr})`)
break
case 'bool':
out.push(`${wvar}.bool(${expr})`)
break
case 'int32':
case 'uint32':
out.push(`${wvar}.int32(${expr} as number)`)
break
case 'sint32':
out.push(`${wvar}.sint32(${expr} as number)`)
break
case 'fixed32':
out.push(`${wvar}.fixed32(${expr} as number)`)
break
case 'sfixed32':
out.push(`${wvar}.sfixed32(${expr} as number)`)
break
case 'int64':
case 'uint64':
if (int64Mode === 'number') out.push(`${wvar}.int64Number(${expr} as number)`)
else out.push(`${wvar}.int64(${expr} as bigint)`)
break
case 'sint64':
out.push(`${wvar}.sint64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`)
break
case 'fixed64':
out.push(`${wvar}.fixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`)
break
case 'sfixed64':
out.push(`${wvar}.sfixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`)
break
case 'float':
out.push(`${wvar}.float(${expr} as number)`)
break
case 'double':
out.push(`${wvar}.double(${expr} as number)`)
break
default:
if (f.resolvedType) {
if (f.resolvedType instanceof protobuf.Enum) out.push(`${wvar}.int32(${expr} as number)`)
else out.push(`{ const cw = ${wvar}.fork(); ${f.resolvedType.name}.encode(${expr}, cw); ${wvar}.ldelim(cw) }`)
} else out.push(`${wvar}.int32(${expr} as number)`)
}
return out
}
* Generate Reader calls to deserialize into target variable
*/
function renderReadField(f, int64Mode, targetExpr, aliasName) {
const out = []
const target = targetExpr
if (f.repeated) out.push(`${target} = ${target} ?? []`)
switch (f.type) {
case 'string':
out.push(f.repeated ? `${target}.push(r.string())` : `${target} = r.string()`)
break
case 'bytes':
out.push(f.repeated ? `${target}.push(r.bytes())` : `${target} = r.bytes()`)
break
case 'bool':
out.push(f.repeated ? `${target}.push(r.bool())` : `${target} = r.bool()`)
break
case 'int32':
case 'uint32':
out.push(f.repeated ? `${target}.push(r.int32())` : `${target} = r.int32()`)
break
case 'sint32':
out.push(f.repeated ? `${target}.push(r.sint32())` : `${target} = r.sint32()`)
break
case 'fixed32':
out.push(f.repeated ? `${target}.push(r.fixed32())` : `${target} = r.fixed32()`)
break
case 'sfixed32':
out.push(f.repeated ? `${target}.push(r.sfixed32())` : `${target} = r.sfixed32()`)
break
case 'int64':
if (int64Mode === 'number') out.push(f.repeated ? `${target}.push(r.int64Number())` : `${target} = r.int64Number()`)
else out.push(f.repeated ? `${target}.push(r.int64())` : `${target} = r.int64()`)
break
case 'uint64':
if (int64Mode === 'number') out.push(f.repeated ? `${target}.push(r.uint64Number())` : `${target} = r.uint64Number()`)
else out.push(f.repeated ? `${target}.push(r.uint64())` : `${target} = r.uint64()`)
break
case 'sint64':
out.push(int64Mode === 'number'
? (f.repeated ? `${target}.push(Number(r.sint64()))` : `${target} = Number(r.sint64())`)
: (f.repeated ? `${target}.push(r.sint64())` : `${target} = r.sint64()`))
break
case 'fixed64':
out.push(int64Mode === 'number'
? (f.repeated ? `${target}.push(Number(r.fixed64()))` : `${target} = Number(r.fixed64())`)
: (f.repeated ? `${target}.push(r.fixed64())` : `${target} = r.fixed64()`))
break
case 'sfixed64':
out.push(int64Mode === 'number'
? (f.repeated ? `${target}.push(Number(r.sfixed64()))` : `${target} = Number(r.sfixed64())`)
: (f.repeated ? `${target}.push(r.sfixed64())` : `${target} = r.sfixed64()`))
break
case 'float':
out.push(f.repeated ? `${target}.push(r.float())` : `${target} = r.float()`)
break
case 'double':
out.push(f.repeated ? `${target}.push(r.double())` : `${target} = r.double()`)
break
default:
if (f.resolvedType) {
if (f.resolvedType instanceof protobuf.Enum) out.push(f.repeated ? `${target}.push(r.int32())` : `${target} = r.int32()`)
else {
const nm = aliasName || f.resolvedType.name
out.push(f.repeated ? `${target}.push(${nm}.decode(r, r.uint32()))` : `${target} = ${nm}.decode(r, r.uint32())`)
}
} else out.push(f.repeated ? `${target}.push(r.int32())` : `${target} = r.int32()`)
}
return out
}
* ArkTS type for map key (depends on int64 mode)
*/
function mapKeyType(f, int64Mode) {
const kt = f.keyType
switch (kt) {
case 'string': return 'string'
case 'bool': return 'boolean'
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32': return 'number'
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64': return int64Mode === 'number' ? 'number' : 'bigint'
default: return 'string'
}
}
* Generate map-entry key write sequence
*/
function renderWriteMapKey(f, int64Mode, expr) {
const out = []
const wt = (function(){
switch (f.keyType) {
case 'string':
return 2
case 'float':
case 'fixed32':
case 'sfixed32':
return 5
case 'double':
case 'fixed64':
case 'sfixed64':
return 1
default:
return 0
}
})()
out.push(`cw.uint32((1 << 3) | ${wt})`)
switch (f.keyType) {
case 'string': out.push(`cw.string(${expr})`); break
case 'bool': out.push(`cw.bool(${expr})`); break
case 'int32':
case 'uint32': out.push(`cw.int32(${expr} as number)`); break
case 'sint32': out.push(`cw.sint32(${expr} as number)`); break
case 'fixed32': out.push(`cw.fixed32(${expr} as number)`); break
case 'sfixed32': out.push(`cw.sfixed32(${expr} as number)`); break
case 'int64':
case 'uint64': out.push(int64Mode === 'number' ? `cw.int64Number(${expr} as number)` : `cw.int64(${expr} as bigint)`); break
case 'sint64': out.push(`cw.sint64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`); break
case 'fixed64': out.push(`cw.fixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`); break
case 'sfixed64': out.push(`cw.sfixed64(${int64Mode === 'number' ? `BigInt(${expr} as number)` : `${expr} as bigint`})`); break
default: out.push(`cw.int32(${expr} as number)`)
}
return out
}
* Generate map-entry value write sequence
*/
function renderWriteMapValue(f, int64Mode, expr) {
const out = []
const vt = f.type
if (f.resolvedType && (f.resolvedType instanceof protobuf.Enum)) {
out.push('cw.uint32((2 << 3) | 0)')
out.push(`cw.int32(${expr} as number)`)
} else {
out.push('cw.uint32((2 << 3) | ' + (f.resolvedType ? 2 : wireTypeFor(f)) + ')')
out.push(...renderWriteField(f, int64Mode, expr, 'cw'))
}
return out
}
* Generate map-entry read loop for a map field
*/
function renderReadMapField(f, int64Mode, varName, aliasName) {
const out = []
out.push(`${varName} = ${varName} ?? new Map()`)
out.push('const l = r.uint32()')
out.push('const end2 = r.pos + l')
const vType = (function(){
if (f.resolvedType) {
if (f.resolvedType instanceof protobuf.Enum) return 'number'
return aliasName || f.resolvedType.name
}
return typeFor(f, int64Mode)
})()
out.push(`let k: ${mapKeyType(f, int64Mode)}; let v: ${vType}`)
out.push('while (r.pos < end2) {')
out.push(' const tt = r.uint32()')
out.push(' switch (tt >>> 3) {')
out.push(' case 1: {')
switch (f.keyType) {
case 'string': out.push(' k = r.string(); break'); break
case 'bool': out.push(' k = r.bool(); break'); break
case 'int32':
case 'uint32': out.push(' k = r.int32(); break'); break
case 'sint32': out.push(' k = r.sint32(); break'); break
case 'fixed32': out.push(' k = r.fixed32(); break'); break
case 'sfixed32': out.push(' k = r.sfixed32(); break'); break
case 'int64':
case 'uint64': out.push(int64Mode === 'number' ? ' k = r.int64Number(); break' : ' k = r.int64(); break'); break
case 'sint64': out.push(int64Mode === 'number' ? ' k = Number(r.sint64()); break' : ' k = r.sint64(); break'); break
case 'fixed64': out.push(int64Mode === 'number' ? ' k = Number(r.fixed64()); break' : ' k = r.fixed64(); break'); break
case 'sfixed64': out.push(int64Mode === 'number' ? ' k = Number(r.sfixed64()); break' : ' k = r.sfixed64(); break'); break
default: out.push(' k = r.int32(); break'); break
}
out.push(' break')
out.push(' }')
out.push(' case 2: {')
switch (f.type) {
case 'string': out.push(' v = r.string(); break'); break
case 'bytes': out.push(' v = r.bytes(); break'); break
case 'bool': out.push(' v = r.bool(); break'); break
case 'int32':
case 'uint32': out.push(' v = r.int32(); break'); break
case 'sint32': out.push(' v = r.sint32(); break'); break
case 'fixed32': out.push(' v = r.fixed32(); break'); break
case 'sfixed32': out.push(' v = r.sfixed32(); break'); break
case 'int64': out.push(int64Mode === 'number' ? ' v = r.int64Number(); break' : ' v = r.int64(); break'); break
case 'uint64': out.push(int64Mode === 'number' ? ' v = r.uint64Number(); break' : ' v = r.uint64(); break'); break
case 'sint64': out.push(int64Mode === 'number' ? ' v = Number(r.sint64()); break' : ' v = r.sint64(); break'); break
case 'fixed64': out.push(int64Mode === 'number' ? ' v = Number(r.fixed64()); break' : ' v = r.fixed64(); break'); break
case 'sfixed64': out.push(int64Mode === 'number' ? ' v = Number(r.sfixed64()); break' : ' v = r.sfixed64(); break'); break
case 'float': out.push(' v = r.float(); break'); break
case 'double': out.push(' v = r.double(); break'); break
default: if (f.resolvedType) { out.push(` v = ${(aliasName || f.resolvedType.name)}.decode(r, r.uint32()); break`) } else { out.push(' v = r.int32(); break') } break
}
out.push(' break')
out.push(' }')
out.push(' default: r.skipType(tt & 7)')
out.push(' }')
out.push('}')
out.push(`${varName}.set(k as ${mapKeyType(f, int64Mode)}, v as ${vType})`)
return out
}
* Determine top-level package name from a reflection object
*/
function pkgFromObj(obj) {
const fn = (obj.fullName || '').replace(/^\./, '')
const seg = fn.split('.')[0]
return seg || 'default'
}
* DFS walk protobuf reflection tree and invoke callback for types/enums/services
*/
function walk(node, cb) {
if (!node) return
if (node instanceof protobuf.Type || node instanceof protobuf.Enum || node instanceof protobuf.Service) cb(node)
if (node.nestedArray) for (const n of node.nestedArray) walk(n, cb)
}
* Main entry: preprocess protos, load, emit code, build indexes
*/
async function main() {
const opts = parseArgs()
ensureDir(opts.out)
function collectProtoFiles(dir) {
const out = []
for (const entry of fs.readdirSync(dir)) {
const p = path.join(dir, entry)
const st = fs.statSync(p)
if (st.isDirectory()) out.push(...collectProtoFiles(p))
else if (entry.endsWith('.proto')) out.push(p)
}
return out
}
const files = collectProtoFiles(opts.in)
const tmpDir = path.join(process.cwd(), '.proto-preprocessed')
if (fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true })
}
fs.mkdirSync(tmpDir, { recursive: true })
function preprocessProto(src) {
return src.replace(/=\s*(-?)\s*(0[xX][0-9a-fA-F]+)/g, (_, sign, hex) => {
const v = parseInt(hex, 16)
return `= ${sign === '-' ? -v : v}`
})
}
function mirrorWrite(srcPath) {
const rel = path.relative(opts.in, srcPath)
const outPath = path.join(tmpDir, rel)
fs.mkdirSync(path.dirname(outPath), { recursive: true })
const src = fs.readFileSync(srcPath, 'utf-8')
fs.writeFileSync(outPath, preprocessProto(src))
return outPath
}
const preFiles = files.map(mirrorWrite)
if (opts.bundleRuntime === 'on') {
const rtSrc = path.join(process.cwd(), 'runtime', 'arkpb')
const rtDst = path.join(opts.out, opts.runtimeDir)
ensureDir(rtDst)
const runtimeFiles = [
'index.ets',
'Message.ets',
'Visitor.ets',
'UnknownFields.ets',
'BinaryEncodingVisitor.ets',
'JsonEncodingVisitor.ets',
'MessageRegistry.ets',
'MessageUtils.ets',
'Reader.ets',
'Writer.ets',
'RpcTransport.ets',
'util.ets'
]
for (const f of runtimeFiles) {
const s = path.join(rtSrc, f)
if (fs.existsSync(s)) fs.copyFileSync(s, path.join(rtDst, f))
}
}
function extractComments(src) {
const lines = src.split(/\r?\n/)
const pkgMatch = src.match(/\bpackage\s+([A-Za-z0-9_.]+)\s*;/)
const pkg = pkgMatch ? pkgMatch[1] : 'default'
const map = new Map()
let pending = []
function flushPending() {
const txt = pending.join('\n').trim()
pending = []
return txt.length ? txt : undefined
}
let stack = []
function full(n) { return pkg + '.' + n }
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()
if (trimmed.startsWith('/*')) {
let block = trimmed
while (!block.includes('*/') && i + 1 < lines.length) { i++; block += '\n' + lines[i] }
const body = block.replace(/^\/*\*?/,'').replace(/\*\/\s*$/,'')
pending.push(body)
continue
}
if (trimmed.startsWith('//')) { pending.push(trimmed.replace(/^\/\//,'')); continue }
const msgMatch = trimmed.match(/^message\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
const enumMatch = trimmed.match(/^enum\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
const svcMatch = trimmed.match(/^service\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
const oneofMatch = trimmed.match(/^oneof\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/)
if (msgMatch) { const name = msgMatch[1]; const c = flushPending(); if (c) map.set(full(name), c); stack.push({ kind: 'message', name }) ; continue }
if (enumMatch) { const name = enumMatch[1]; const c = flushPending(); if (c) map.set(full(name), c); stack.push({ kind: 'enum', name }) ; continue }
if (svcMatch) { const name = svcMatch[1]; const c = flushPending(); if (c) map.set(full(name), c); stack.push({ kind: 'service', name }) ; continue }
if (oneofMatch) { const name = oneofMatch[1]; const c = flushPending(); if (c && stack[stack.length-1]?.kind==='message') map.set(full(stack[stack.length-1].name)+`#oneof:${name}`, c); stack.push({ kind: 'oneof', name }); continue }
if (trimmed.startsWith('}')) { stack.pop(); continue }
if (stack.length) {
const top = stack[stack.length-1]
if (top.kind === 'message') {
const fieldInline = trimmed.match(/^(?:repeated\s+|optional\s+)?(?:map<[^>]+>|[A-Za-z_][A-Za-z0-9_.]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\d+.*?;/)
if (fieldInline) {
const name = fieldInline[1]
let trailing = undefined
const idx = line.indexOf('//')
if (idx >= 0) trailing = line.slice(idx + 2).trim()
const bidx = line.indexOf('/*')
if (bidx >= 0) {
let block = line.slice(bidx + 2)
if (block.includes('*/')) {
block = block.split('*/')[0]
} else {
let j = i + 1
while (j < lines.length) {
const ln = lines[j]
const pos = ln.indexOf('*/')
if (pos >= 0) { block += '\n' + ln.slice(0, pos); i = j; break }
block += '\n' + ln
j++
}
}
trailing = (trailing ? (trailing + '\n' + block.trim()) : block.trim())
}
const c = trailing || flushPending()
if (c) map.set(full(top.name)+`#${name}`, c)
}
} else if (top.kind === 'enum') {
const valInline = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[-]?(?:0[xX][0-9a-fA-F]+|\d+)\s*;/)
if (valInline) {
const name = valInline[1]
let trailing = undefined
const idx = line.indexOf('//')
if (idx >= 0) trailing = line.slice(idx + 2).trim()
const bidx = line.indexOf('/*')
if (bidx >= 0) {
let block = line.slice(bidx + 2)
if (block.includes('*/')) {
block = block.split('*/')[0]
} else {
let j = i + 1
while (j < lines.length) {
const ln = lines[j]
const pos = ln.indexOf('*/')
if (pos >= 0) { block += '\n' + ln.slice(0, pos); i = j; break }
block += '\n' + ln
j++
}
}
trailing = (trailing ? (trailing + '\n' + block.trim()) : block.trim())
}
const c = trailing || flushPending()
if (c) map.set(full(top.name)+`#${name}`, c)
}
} else if (top.kind === 'service') {
const rpcInline = trimmed.match(/^rpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/)
if (rpcInline) {
const name = rpcInline[1]
let trailing = undefined
const idx = line.indexOf('//')
if (idx >= 0) trailing = line.slice(idx + 2).trim()
const bidx = line.indexOf('/*')
if (bidx >= 0) {
let block = line.slice(bidx + 2)
if (block.includes('*/')) {
block = block.split('*/')[0]
} else {
let j = i + 1
while (j < lines.length) {
const ln = lines[j]
const pos = ln.indexOf('*/')
if (pos >= 0) { block += '\n' + ln.slice(0, pos); i = j; break }
block += '\n' + ln
j++
}
}
trailing = (trailing ? (trailing + '\n' + block.trim()) : block.trim())
}
const c = trailing || flushPending()
if (c) map.set(full(top.name)+`#method:${name}`, c)
}
}
}
}
return map
}
const globalComments = new Map()
for (const f of preFiles) {
const srcText0 = fs.readFileSync(f, 'utf-8')
const cm0 = extractComments(srcText0)
for (const [k,v] of cm0.entries()) if (!globalComments.has(k)) globalComments.set(k, v)
}
const commentOfGlobal = (key) => globalComments.get(key)
const pkgDirs = new Set()
const pkgIndex = new Map()
for (const f of preFiles) {
const root = await protobuf.load(f)
root.resolveAll()
walk(root, (obj) => {
const pkgName = pkgFromObj(obj)
const segs = packageSegmentsFromObj(obj)
const fullPkgName = segs.join('.')
const pkgDir = outBaseDirForSegments(opts, segs)
ensureDir(pkgDir)
ensureDir(path.join(pkgDir, 'messages'))
ensureDir(path.join(pkgDir, 'enums'))
ensureDir(path.join(pkgDir, 'services'))
if (!pkgDirs.has(fullPkgName)) {
pkgDirs.add(fullPkgName)
pkgIndex.set(fullPkgName, { messages: new Set(), enums: new Set(), services: new Set() })
} else {
if (!pkgIndex.has(fullPkgName)) {
pkgIndex.set(fullPkgName, { messages: new Set(), enums: new Set(), services: new Set() })
}
}
if (obj instanceof protobuf.Type) {
const code = renderMessage(pkgDir, pkgName, obj, opts.int64, opts.packed === 'on', opts.docs === 'on', opts.omitDefaults === 'on', opts.json === 'on', commentOfGlobal, opts.jsonEnum, opts.jsonStrict, opts)
fs.writeFileSync(path.join(pkgDir, 'messages', obj.name + '.ets'), code)
pkgIndex.get(fullPkgName).messages.add(obj.name)
} else if (obj instanceof protobuf.Enum) {
const enumLines = []
if (opts.docs === 'on') enumLines.push(...formatDoc(commentOfGlobal((obj.fullName||'').replace(/^\./,'')) || obj.comment))
enumLines.push(`export enum ${obj.name} {`)
const commentsMap = (obj.comments || {})
const entries = Object.entries(obj.values)
for (let i = 0; i < entries.length; i++) {
const [k,v] = entries[i]
const vc = commentOfGlobal(`${(obj.fullName||'').replace(/^\./,'')}#${k}`) || commentsMap[k]
if (opts.docs === 'on' && vc) enumLines.push(...formatDoc(vc).map(s => ' ' + s))
const comma = i < entries.length - 1 ? ',' : ''
enumLines.push(` ${k} = ${v}${comma}`)
}
enumLines.push('}')
enumLines.push('')
fs.writeFileSync(path.join(pkgDir, 'enums', obj.name + '.ets'), enumLines.join('\n'))
pkgIndex.get(fullPkgName).enums.add(obj.name)
} else if (obj instanceof protobuf.Service) {
const svcName = obj.name
const methods = Object.entries(obj.methods).map(([n,m]) => ({ n, m }))
const used = new Map()
const imports = new Map()
const currSvcDir = path.join(pkgDir, 'services')
for (const { n, m } of methods) {
const reqT = m.resolvedRequestType
const resT = m.resolvedResponseType
for (const T of [reqT, resT]) {
const full = (T.fullName || '').replace(/^\./,'')
const depPkg = full.split('.')[0] || 'default'
const relRaw = path.relative(currSvcDir, outPathForFull(opts, full, 'messages')).replace(/\\/g,'/')
const relNoExt = relRaw.replace(/\.ets$/,'')
const rel = relNoExt.startsWith('.') ? relNoExt : './' + relNoExt
let local = T.name
if (used.has(local) && used.get(local) !== full) local = `${local}_${depPkg}`
used.set(local, full)
imports.set(full, { base: T.name, local, rel })
}
}
const rtRelRawSvc = path.relative(currSvcDir, path.join(opts.out, opts.runtimeDir)).replace(/\\/g,'/')
const rtRelSvc = rtRelRawSvc.startsWith('.') ? rtRelRawSvc : './' + rtRelRawSvc
const importLines = ["import { RpcTransport, Writer, Reader } from '" + rtRelSvc + "'"]
const sorted = Array.from(imports.values()).sort((a,b)=> a.local.localeCompare(b.local) || a.rel.localeCompare(b.rel))
for (const d of sorted) {
if (d.local === d.base) importLines.push(`import { ${d.base} } from '${d.rel}'`)
else importLines.push(`import { ${d.base} as ${d.local} } from '${d.rel}'`)
}
const codeLines = []
codeLines.push(...importLines)
if (opts.docs === 'on') codeLines.push(...formatDoc(commentOfGlobal((obj.fullName||'').replace(/^\./,'')) || obj.comment))
if (opts.concurrent === 'sendable') codeLines.push('@Sendable')
codeLines.push(`export class ${svcName}Client {`)
codeLines.push(' private t: RpcTransport')
codeLines.push(' constructor(t: RpcTransport) { this.t = t }')
for (const { n, m } of methods) {
const reqT = m.resolvedRequestType
const resT = m.resolvedResponseType
const reqFull = (reqT.fullName || '').replace(/^\./,'')
const resFull = (resT.fullName || '').replace(/^\./,'')
const reqLocal = imports.get(reqFull).local
const resLocal = imports.get(resFull).local
if (opts.docs === 'on') {
const mc = commentOfGlobal(`${(obj.fullName||'').replace(/^\./,'')}#method:${n}`) || m.comment
const doc = mc ? `${mc}\n@rpc /${pkgName}.${svcName}/${n}\n@param req ${reqLocal}\n@returns ${resLocal}` : `@rpc /${pkgName}.${svcName}/${n}\n@param req ${reqLocal}\n@returns ${resLocal}`
codeLines.push(...formatDoc(doc).map(s => ' ' + s))
}
codeLines.push(` async ${n}(req: ${reqLocal}): Promise<${resLocal}> {`)
codeLines.push(' const w = new Writer()')
codeLines.push(` ${reqLocal}.encode(req, w)`)
codeLines.push(` const res = await this.t.unary('/${pkgName}.${svcName}/${n}', w.finish())`)
codeLines.push(' return ' + `${resLocal}.decode(new Reader(res))`)
codeLines.push(' }')
}
codeLines.push('}')
fs.writeFileSync(path.join(pkgDir, 'services', svcName + '.ets'), codeLines.join('\n'))
pkgIndex.get(fullPkgName).services.add(svcName)
}
})
}
function mergeIndex(idxPath, lines) {
const content = lines.join('\n') + '\n'
fs.writeFileSync(idxPath, content)
}
for (const fullPkgName of pkgDirs) {
const segs = fullPkgName.split('.')
const pkgDir = outBaseDirForSegments(opts, segs)
const index = pkgIndex.get(fullPkgName)
const enumsLines = []
const messagesLines = []
const servicesLines = []
if (index.enums.size) {
for (const e of Array.from(index.enums).sort()) enumsLines.push(`export { ${e} } from './${e}'`)
ensureDir(path.join(pkgDir, 'enums'))
mergeIndex(path.join(pkgDir, 'enums', 'index.ets'), enumsLines)
}
if (index.messages.size) {
for (const m of Array.from(index.messages).sort()) {
messagesLines.push(`export { ${m} } from './${m}'`)
messagesLines.push(`export { ${m}CreateInput } from './${m}'`)
if (opts.json === 'on') {
messagesLines.push(`export { ${m}JSON } from './${m}'`)
}
}
ensureDir(path.join(pkgDir, 'messages'))
mergeIndex(path.join(pkgDir, 'messages', 'index.ets'), messagesLines)
}
if (index.services.size) {
for (const s of Array.from(index.services).sort()) servicesLines.push(`export { ${s}Client } from './${s}'`)
ensureDir(path.join(pkgDir, 'services'))
mergeIndex(path.join(pkgDir, 'services', 'index.ets'), servicesLines)
}
const lines = []
if (index.enums.size) lines.push("export * from './enums'")
if (index.messages.size) lines.push("export * from './messages'")
if (index.services.size) lines.push("export * from './services'")
mergeIndex(path.join(pkgDir, 'index.ets'), lines)
}
if (opts.namespaceAsFile === 'on') {
function buildIndexesRecursive(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const subdirs = entries.filter(e => e.isDirectory()).map(e => e.name)
let lines = []
for (const sd of subdirs) {
const childIdx = path.join(dir, sd, 'index.ets')
if (fs.existsSync(childIdx)) lines.push(`export * from './${sd}'`)
buildIndexesRecursive(path.join(dir, sd))
}
if (lines.length) {
const idxPath = path.join(dir, 'index.ets')
mergeIndex(idxPath, lines)
}
}
buildIndexesRecursive(opts.out)
}
}
main().catch(e => { console.error(e); process.exit(1) })