pg-proto-parser
Version:
The LaunchQL Proto parser
309 lines (308 loc) • 13.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProtoStore = void 0;
const protobufjs_1 = require("@launchql/protobufjs");
const ast_1 = require("./ast");
const runtime_schema_1 = require("./runtime-schema");
const enums_json_1 = require("./ast/enums/enums-json");
const strfy_js_1 = require("strfy-js");
const fs_1 = require("fs");
const path_1 = require("path");
const options_1 = require("./options");
const utils_1 = require("./utils");
const inline_helpers_1 = require("./inline-helpers");
const constants_1 = require("./constants");
class ProtoStore {
options;
root;
services;
types;
fields;
enums;
namespaces;
_runtimeSchema;
constructor(root, options = {}) {
this.options = (0, options_1.getOptionsWithDefaults)(options);
this.root = root;
this.services = [];
this.types = [];
this.fields = [];
this.enums = [];
this.namespaces = [];
this._parse(this.root);
}
_parse(node, name = '') {
if (node instanceof protobufjs_1.Service) {
this.services.push((0, utils_1.cloneAndNameNode)(node, name));
}
else if (node instanceof protobufjs_1.Type) {
this.types.push((0, utils_1.cloneAndNameNode)(node, name));
node.fieldsArray.forEach(field => this.fields.push(field));
}
else if (node instanceof protobufjs_1.Enum) {
this.enums.push((0, utils_1.cloneAndNameNode)(this._processEnum(node), name));
}
if (node instanceof protobufjs_1.Namespace) {
this.namespaces.push(node);
Object.entries(node.nested || {}).forEach(([key, child]) => {
this._parse(child, key);
});
}
}
_processEnum(enumNode) {
const undefinedKey = (0, utils_1.getUndefinedKey)(enumNode.name);
const clone = (0, utils_1.cloneAndNameNode)(enumNode, enumNode.name);
if (!this.options.enums.removeUndefinedAt0 || !(0, utils_1.hasUndefinedInitialValue)(enumNode)) {
return clone;
}
const newValues = {};
let decrement = 0;
for (const [key, value] of Object.entries(enumNode.values)) {
if (key === undefinedKey && value === 0) {
decrement = 1;
continue;
}
newValues[key] = value - decrement;
}
clone.values = newValues;
return clone;
}
write() {
// Ensure the output directory exists
(0, fs_1.mkdirSync)(this.options.outDir, { recursive: true });
this.writeEnumMaps();
this.writeTypes();
this.writeEnums();
this.writeUtilsEnums();
this.writeAstHelpers();
this.writeWrappedAstHelpers();
this.writeRuntimeSchema();
}
writeEnumMaps() {
if (!this.options.enums.enumMap?.enabled) {
return;
}
const enumsToProcess = this.enumsToProcess();
const enums2int = (0, enums_json_1.generateEnum2IntJSON)(enumsToProcess);
const enums2str = (0, enums_json_1.generateEnum2StrJSON)(enumsToProcess);
const format = this.options.enums.enumMap.format || 'json';
if (format === 'json') {
// Write plain JSON files
if (this.options.enums.enumMap.toIntOutFile) {
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toIntOutFile, '.json');
this.writeFile(filename, JSON.stringify(enums2int, null, 2));
}
if (this.options.enums.enumMap.toStrOutFile) {
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toStrOutFile, '.json');
this.writeFile(filename, JSON.stringify(enums2str, null, 2));
}
}
else if (format === 'ts') {
// Write TypeScript files with exports
if (this.options.enums.enumMap.toIntOutFile) {
const tsContent = this.generateEnumMapTypeScript(enums2int, 'enumToIntMap', 'EnumToIntMap');
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toIntOutFile, '.ts');
this.writeFile(filename, tsContent);
}
if (this.options.enums.enumMap.toStrOutFile) {
const tsContent = this.generateEnumMapTypeScript(enums2str, 'enumToStrMap', 'EnumToStrMap');
const filename = this.ensureCorrectExtension(this.options.enums.enumMap.toStrOutFile, '.ts');
this.writeFile(filename, tsContent);
}
}
}
allTypesExceptNode() {
return this.types.filter(type => type.name !== constants_1.NODE_TYPE);
}
typesToProcess() {
return this.types
.filter(type => type.name !== constants_1.NODE_TYPE)
.filter(type => !this.options.exclude.includes(type.name));
}
enumsToProcess() {
return this.enums
.filter(enm => !this.options.exclude.includes(enm.name));
}
writeTypes() {
if (this.options.types.enabled) {
const typesToProcess = this.typesToProcess();
const enumsToProcess = this.enumsToProcess();
const node = (0, ast_1.generateNodeUnionType)(this.options, typesToProcess);
const enumImports = (0, ast_1.generateEnumImports)(enumsToProcess, this.options.types.enumsSource);
const types = typesToProcess.reduce((m, type) => {
return [...m, (0, ast_1.convertTypeToTsInterface)(type, this.options)];
}, []);
const filename = this.ensureCorrectExtension(this.options.types.filename, '.ts');
this.writeCodeToFile(filename, [
enumImports,
node,
...types
]);
}
}
writeEnums() {
if (this.options.enums.enabled) {
const filename = this.ensureCorrectExtension(this.options.enums.filename, '.ts');
this.writeCodeToFile(filename, this.enumsToProcess().map(enm => this.options.enums.enumsAsTypeUnion ?
(0, ast_1.convertEnumToTsUnionType)(enm) :
(0, ast_1.convertEnumToTsEnumDeclaration)(enm)));
}
}
writeUtilsEnums() {
if (this.options.utils.enums.enabled) {
const enumsToProcess = this.enumsToProcess();
const useNestedObjects = this.options.utils.enums.outputFormat === 'nestedObjects';
if (this.options.utils.enums.unidirectional) {
// Generate separate unidirectional functions
const toIntGenerator = useNestedObjects ? ast_1.generateEnumToIntFunctionsNested : ast_1.generateEnumToIntFunctions;
const toStringGenerator = useNestedObjects ? ast_1.generateEnumToStringFunctionsNested : ast_1.generateEnumToStringFunctions;
const toIntCode = (0, utils_1.convertAstToCode)(toIntGenerator(enumsToProcess));
const toIntFilename = this.ensureCorrectExtension(this.options.utils.enums.toIntFilename, '.ts');
this.writeFile(toIntFilename, toIntCode);
const toStringCode = (0, utils_1.convertAstToCode)(toStringGenerator(enumsToProcess));
const toStringFilename = this.ensureCorrectExtension(this.options.utils.enums.toStringFilename, '.ts');
this.writeFile(toStringFilename, toStringCode);
}
else {
// Generate bidirectional function (original behavior)
// Note: Nested objects format only supported for unidirectional functions
const code = (0, utils_1.convertAstToCode)((0, ast_1.generateEnumValueFunctions)(enumsToProcess));
const filename = this.ensureCorrectExtension(this.options.utils.enums.filename, '.ts');
this.writeFile(filename, code);
}
}
}
writeAstHelpers() {
if (this.options.utils.astHelpers.enabled) {
const imports = this.options.utils.astHelpers.inlineNestedObj ?
(0, utils_1.createDefaultImport)('_o', './' + (0, utils_1.stripExtension)(this.options.utils.astHelpers.nestedObjFile)) :
(0, utils_1.createDefaultImport)('_o', 'nested-obj');
const typesToProcess = this.typesToProcess();
const code = (0, utils_1.convertAstToCode)([
imports,
(0, ast_1.generateTypeImportSpecifiers)(typesToProcess, this.options),
(0, ast_1.generateAstHelperMethods)(typesToProcess)
]);
const filename = this.ensureCorrectExtension(this.options.utils.astHelpers.filename, '.ts');
this.writeFile(filename, code);
if (this.options.utils.astHelpers.inlineNestedObj) {
const nestedObjFilename = this.ensureCorrectExtension(this.options.utils.astHelpers.nestedObjFile, '.ts');
this.writeFile(nestedObjFilename, inline_helpers_1.nestedObjCode);
}
}
}
writeWrappedAstHelpers() {
if (this.options.utils.wrappedAstHelpers?.enabled) {
const imports = (0, utils_1.createDefaultImport)('_o', 'nested-obj');
const typesToProcess = this.typesToProcess();
const code = (0, utils_1.convertAstToCode)([
imports,
(0, ast_1.generateTypeImportSpecifiers)(typesToProcess, this.options),
(0, ast_1.generateWrappedAstHelperMethods)(typesToProcess)
]);
const filename = this.ensureCorrectExtension(this.options.utils.wrappedAstHelpers.filename, '.ts');
this.writeFile(filename, code);
}
}
writeRuntimeSchema() {
if (!this.options.runtimeSchema?.enabled) {
return;
}
const generator = new runtime_schema_1.RuntimeSchemaGenerator(this.root);
const nodeSpecs = generator.generateNodeSpecs();
const format = this.options.runtimeSchema.format || 'json';
const filename = this.options.runtimeSchema.filename || 'runtime-schema';
if (format === 'json') {
const jsonContent = JSON.stringify(nodeSpecs, null, 2);
const correctedFilename = this.ensureCorrectExtension(filename, '.json');
const outFile = (0, path_1.join)(this.options.outDir, correctedFilename);
(0, utils_1.writeFileToDisk)(outFile, jsonContent, this.options);
}
else if (format === 'typescript') {
const tsContent = this.generateRuntimeSchemaTypeScript(nodeSpecs);
const correctedFilename = this.ensureCorrectExtension(filename, '.ts');
const outFile = (0, path_1.join)(this.options.outDir, correctedFilename);
(0, utils_1.writeFileToDisk)(outFile, tsContent, this.options);
}
}
getRuntimeSchema() {
if (!this._runtimeSchema) {
const generator = new runtime_schema_1.RuntimeSchemaGenerator(this.root);
this._runtimeSchema = generator.generateNodeSpecs();
}
return this._runtimeSchema;
}
generateRuntimeSchemaTypeScript(nodeSpecs) {
const interfaceDefinitions = [
'export interface FieldSpec {',
' name: string;',
' type: string;',
' isArray: boolean;',
' optional: boolean;',
'}',
'',
'export interface NodeSpec {',
' name: string;',
' isNode: boolean;',
' fields: FieldSpec[];',
'}',
''
];
const exportStatement = `export const runtimeSchema: NodeSpec[] = ${(0, strfy_js_1.jsStringify)(nodeSpecs, {
space: 2,
camelCase: true,
quotes: 'single'
})};`;
return [
...interfaceDefinitions,
exportStatement
].filter(Boolean).join('\n');
}
generateEnumMapTypeScript(enumMap, varName, typeName) {
const exportStatement = `export const ${varName} = ${(0, strfy_js_1.jsStringify)(enumMap, {
space: 2,
camelCase: false, // Preserve enum casing
quotes: 'single'
})};`;
const typeStatement = `export type ${typeName} = typeof ${varName};`;
return [
exportStatement,
'',
typeStatement
].filter(Boolean).join('\n');
}
ensureCorrectExtension(filename, expectedExt) {
if (!filename || !expectedExt) {
return filename || '';
}
// Ensure expectedExt starts with a dot
if (!expectedExt.startsWith('.')) {
expectedExt = '.' + expectedExt;
}
const extMatch = filename.match(/(\.[^./\\]+)+$/);
const currentExt = extMatch ? extMatch[0] : '';
if (currentExt && currentExt !== expectedExt) {
// Replace the current extension with the expected one
return filename.slice(0, -currentExt.length) + expectedExt;
}
else if (!currentExt) {
// No extension, add the expected one
return filename + expectedExt;
}
// Extension is already correct
return filename;
}
writeFile(filePath, content) {
const fullPath = (0, path_1.join)(this.options.outDir, filePath);
const dir = (0, path_1.dirname)(fullPath);
(0, fs_1.mkdirSync)(dir, { recursive: true });
(0, utils_1.writeFileToDisk)(fullPath, content, this.options);
}
writeCodeToFile(filename, nodes) {
const code = (0, utils_1.convertAstToCode)(nodes);
const correctedFilename = this.ensureCorrectExtension(filename, '.ts');
const filePath = (0, path_1.join)(this.options.outDir, correctedFilename);
(0, utils_1.writeFileToDisk)(filePath, code, this.options);
}
}
exports.ProtoStore = ProtoStore;