stacked-server-typescript-types 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.mjs +15 -0
- package/generate-index.js +159 -0
- package/generate-typescript-types.sh +508 -0
- package/mock_proto_types/empty.ts +12 -0
- package/mock_proto_types/struct.ts +17 -0
- package/mock_proto_types/timestamp.ts +12 -0
- package/mock_proto_types/wrappers.ts +51 -0
- package/package.json +12 -0
- package/process-openapi-types.js +747 -0
- package/typegen-package.json +20 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import tseslint from "typescript-eslint";
|
|
4
|
+
import { defineConfig } from "eslint/config";
|
|
5
|
+
|
|
6
|
+
export default defineConfig([
|
|
7
|
+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
|
|
8
|
+
tseslint.configs.recommended,
|
|
9
|
+
{
|
|
10
|
+
files: ["**/*.{ts,mts,cts}"],
|
|
11
|
+
rules: {
|
|
12
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate index.ts that exports everything from endpoint files
|
|
5
|
+
* with conflict resolution for duplicate names
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const outputDir = process.argv[2];
|
|
12
|
+
|
|
13
|
+
if (!outputDir) {
|
|
14
|
+
console.error('Usage: generate-index.js <output-dir>');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(outputDir)) {
|
|
19
|
+
console.error(`Output directory not found: ${outputDir}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Find all endpoint files
|
|
24
|
+
const endpointFiles = fs.readdirSync(outputDir)
|
|
25
|
+
.filter(f => f.endsWith('-endpoints.ts'))
|
|
26
|
+
.sort();
|
|
27
|
+
|
|
28
|
+
if (endpointFiles.length === 0) {
|
|
29
|
+
console.log('No endpoint files found');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse each file to extract exported names
|
|
34
|
+
const fileExports = {};
|
|
35
|
+
const allExports = {};
|
|
36
|
+
|
|
37
|
+
for (const file of endpointFiles) {
|
|
38
|
+
const filePath = path.join(outputDir, file);
|
|
39
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
40
|
+
const baseName = file.replace('-endpoints.ts', '');
|
|
41
|
+
|
|
42
|
+
// Convert to PascalCase for prefix (e.g., "post" -> "Post", "creator" -> "Creator")
|
|
43
|
+
const prefix = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
44
|
+
|
|
45
|
+
fileExports[file] = {
|
|
46
|
+
baseName,
|
|
47
|
+
prefix,
|
|
48
|
+
exports: []
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Match "export const ServiceName = {" for service objects
|
|
52
|
+
const constMatch = content.match(/export const (\w+)\s*=/g);
|
|
53
|
+
if (constMatch) {
|
|
54
|
+
for (const match of constMatch) {
|
|
55
|
+
const name = match.match(/export const (\w+)/)[1];
|
|
56
|
+
fileExports[file].exports.push({ name, type: 'const' });
|
|
57
|
+
|
|
58
|
+
if (!allExports[name]) {
|
|
59
|
+
allExports[name] = [];
|
|
60
|
+
}
|
|
61
|
+
allExports[name].push({ file, prefix, type: 'const' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Match "export type { Type1, Type2, ... } from" for re-exported types
|
|
66
|
+
const typeExportMatch = content.match(/export type \{([^}]+)\} from/g);
|
|
67
|
+
if (typeExportMatch) {
|
|
68
|
+
for (const match of typeExportMatch) {
|
|
69
|
+
const typesStr = match.match(/export type \{([^}]+)\}/)[1];
|
|
70
|
+
const types = typesStr.split(',').map(t => t.trim()).filter(t => t);
|
|
71
|
+
|
|
72
|
+
for (const typeName of types) {
|
|
73
|
+
fileExports[file].exports.push({ name: typeName, type: 'type' });
|
|
74
|
+
|
|
75
|
+
if (!allExports[typeName]) {
|
|
76
|
+
allExports[typeName] = [];
|
|
77
|
+
}
|
|
78
|
+
allExports[typeName].push({ file, prefix, type: 'type' });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find conflicts (names exported from multiple files)
|
|
85
|
+
const conflicts = {};
|
|
86
|
+
for (const [name, sources] of Object.entries(allExports)) {
|
|
87
|
+
if (sources.length > 1) {
|
|
88
|
+
conflicts[name] = sources;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Generate index.ts content
|
|
93
|
+
let indexContent = `// Auto-generated index file
|
|
94
|
+
// This file exports all service endpoints and types
|
|
95
|
+
// Conflicting names are prefixed with the service name
|
|
96
|
+
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
// Group exports by file
|
|
100
|
+
for (const file of endpointFiles) {
|
|
101
|
+
const { prefix, exports } = fileExports[file];
|
|
102
|
+
const fileBaseName = file.replace('.ts', '');
|
|
103
|
+
|
|
104
|
+
// Separate conflicting and non-conflicting exports
|
|
105
|
+
const nonConflictingConsts = [];
|
|
106
|
+
const conflictingConsts = [];
|
|
107
|
+
const nonConflictingTypes = [];
|
|
108
|
+
const conflictingTypes = [];
|
|
109
|
+
|
|
110
|
+
for (const exp of exports) {
|
|
111
|
+
if (conflicts[exp.name]) {
|
|
112
|
+
if (exp.type === 'const') {
|
|
113
|
+
conflictingConsts.push(exp.name);
|
|
114
|
+
} else {
|
|
115
|
+
conflictingTypes.push(exp.name);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
if (exp.type === 'const') {
|
|
119
|
+
nonConflictingConsts.push(exp.name);
|
|
120
|
+
} else {
|
|
121
|
+
nonConflictingTypes.push(exp.name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add comment for this service
|
|
127
|
+
indexContent += `// ${prefix} Service\n`;
|
|
128
|
+
|
|
129
|
+
// Export non-conflicting consts directly
|
|
130
|
+
if (nonConflictingConsts.length > 0) {
|
|
131
|
+
indexContent += `export { ${nonConflictingConsts.join(', ')} } from './${fileBaseName}';\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Export non-conflicting types directly
|
|
135
|
+
if (nonConflictingTypes.length > 0) {
|
|
136
|
+
indexContent += `export type { ${nonConflictingTypes.join(', ')} } from './${fileBaseName}';\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Export conflicting consts with prefix
|
|
140
|
+
for (const name of conflictingConsts) {
|
|
141
|
+
// e.g. HealthCheck collisions -> PostServiceHealthCheck etc.
|
|
142
|
+
const prefixedName = `${prefix}${name}`;
|
|
143
|
+
indexContent += `export { ${name} as ${prefixedName} } from './${fileBaseName}';\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Export conflicting types with prefix
|
|
147
|
+
for (const name of conflictingTypes) {
|
|
148
|
+
const prefixedName = `${prefix}${name}`;
|
|
149
|
+
indexContent += `export type { ${name} as ${prefixedName} } from './${fileBaseName}';\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
indexContent += '\n';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Write the index file
|
|
156
|
+
const indexPath = path.join(outputDir, 'index.ts');
|
|
157
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
158
|
+
console.log(`Generated index.ts with ${endpointFiles.length} service files`);
|
|
159
|
+
console.log(`Found ${Object.keys(conflicts).length} naming conflicts that were prefixed`);
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
# Exit immediately on error and undefined variables.
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
# Enable pipefail when supported.
|
|
7
|
+
(set -o pipefail) 2>/dev/null && set -o pipefail || true
|
|
8
|
+
|
|
9
|
+
# Configuration
|
|
10
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
11
|
+
SERVER_DIR=$(dirname "$SCRIPT_DIR")
|
|
12
|
+
OUTPUT_DIR="$SERVER_DIR/generated-types"
|
|
13
|
+
TEMP_DIR="$SCRIPT_DIR/.types-temp"
|
|
14
|
+
|
|
15
|
+
# Colors for output
|
|
16
|
+
RED='\033[0;31m'
|
|
17
|
+
GREEN='\033[0;32m'
|
|
18
|
+
YELLOW='\033[1;33m'
|
|
19
|
+
NC='\033[0m' # No Color
|
|
20
|
+
|
|
21
|
+
print_info() {
|
|
22
|
+
printf "%b[INFO]%b %s\n" "$GREEN" "$NC" "$1"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
print_warn() {
|
|
26
|
+
printf "%b[WARN]%b %s\n" "$YELLOW" "$NC" "$1"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
print_error() {
|
|
30
|
+
printf "%b[ERROR]%b %s\n" "$RED" "$NC" "$1"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Cleanup function
|
|
34
|
+
cleanup() {
|
|
35
|
+
if [ -d "$TEMP_DIR" ]; then
|
|
36
|
+
print_info "Cleaning up temporary files..."
|
|
37
|
+
rm -rf "$TEMP_DIR"
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Set trap to cleanup on exit
|
|
42
|
+
trap cleanup EXIT
|
|
43
|
+
|
|
44
|
+
# Check for required tools
|
|
45
|
+
check_dependencies() {
|
|
46
|
+
print_info "Checking dependencies..."
|
|
47
|
+
|
|
48
|
+
# Check for protoc
|
|
49
|
+
if ! command -v protoc >/dev/null 2>&1; then
|
|
50
|
+
print_error "protoc is not installed. Please install it first."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Check for protoc-gen-openapiv2
|
|
55
|
+
openapi_plugin=""
|
|
56
|
+
if command -v protoc-gen-openapiv2 >/dev/null 2>&1; then
|
|
57
|
+
openapi_plugin="protoc-gen-openapiv2"
|
|
58
|
+
elif [ -f "$HOME/go/bin/protoc-gen-openapiv2" ]; then
|
|
59
|
+
openapi_plugin="$HOME/go/bin/protoc-gen-openapiv2"
|
|
60
|
+
elif [ -n "${GOPATH:-}" ] && [ -f "$GOPATH/bin/protoc-gen-openapiv2" ]; then
|
|
61
|
+
openapi_plugin="$GOPATH/bin/protoc-gen-openapiv2"
|
|
62
|
+
else
|
|
63
|
+
print_warn "protoc-gen-openapiv2 not found. Attempting to install..."
|
|
64
|
+
# Try to install, but don't fail if it doesn't work
|
|
65
|
+
if command -v go >/dev/null 2>&1; then
|
|
66
|
+
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest 2>&1 | grep -v "GOROOT" || true
|
|
67
|
+
if [ -f "$HOME/go/bin/protoc-gen-openapiv2" ]; then
|
|
68
|
+
openapi_plugin="$HOME/go/bin/protoc-gen-openapiv2"
|
|
69
|
+
elif [ -n "${GOPATH:-}" ] && [ -f "$GOPATH/bin/protoc-gen-openapiv2" ]; then
|
|
70
|
+
openapi_plugin="$GOPATH/bin/protoc-gen-openapiv2"
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [ -z "$openapi_plugin" ]; then
|
|
76
|
+
print_warn "protoc-gen-openapiv2 not found. OpenAPI generation will be skipped."
|
|
77
|
+
print_warn "Please install it manually: go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest"
|
|
78
|
+
export OPENAPI_PLUGIN_PATH=""
|
|
79
|
+
else
|
|
80
|
+
export OPENAPI_PLUGIN_PATH="$openapi_plugin"
|
|
81
|
+
print_info "Found protoc-gen-openapiv2 at: $openapi_plugin"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Check for Node.js and npm
|
|
85
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
86
|
+
print_error "Node.js is not installed. Please install it first."
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
91
|
+
print_error "npm is not installed. Please install it first."
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Install npm dependencies if node_modules doesn't exist in server directory
|
|
96
|
+
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
|
|
97
|
+
print_info "Installing npm dependencies..."
|
|
98
|
+
(cd "$SCRIPT_DIR" && npm install)
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
print_info "All dependencies are available"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Find proto files (excluding google protos)
|
|
105
|
+
find_proto_files() {
|
|
106
|
+
find "$SERVER_DIR/services" -name '*.proto' | grep -v '/google/' | sort
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Generate TypeScript message types using ts-proto
|
|
110
|
+
generate_ts_proto_types() {
|
|
111
|
+
print_info "Generating TypeScript message types using ts-proto..."
|
|
112
|
+
|
|
113
|
+
proto_files=$(find_proto_files)
|
|
114
|
+
ts_proto_dir="$TEMP_DIR/ts-proto"
|
|
115
|
+
mkdir -p "$ts_proto_dir"
|
|
116
|
+
|
|
117
|
+
# Find ts-proto plugin once
|
|
118
|
+
plugin_bin=""
|
|
119
|
+
if [ -f "$SCRIPT_DIR/node_modules/.bin/protoc-gen-ts_proto" ]; then
|
|
120
|
+
plugin_bin="$SCRIPT_DIR/node_modules/.bin/protoc-gen-ts_proto"
|
|
121
|
+
elif command -v protoc-gen-ts_proto >/dev/null 2>&1; then
|
|
122
|
+
plugin_bin="$(command -v protoc-gen-ts_proto)"
|
|
123
|
+
else
|
|
124
|
+
print_warn "ts-proto plugin not found, trying to install..."
|
|
125
|
+
(cd "$SCRIPT_DIR" && npm install --save-dev ts-proto 2>&1 | grep -v "npm WARN" || true)
|
|
126
|
+
if [ -f "$SCRIPT_DIR/node_modules/.bin/protoc-gen-ts_proto" ]; then
|
|
127
|
+
plugin_bin="$SCRIPT_DIR/node_modules/.bin/protoc-gen-ts_proto"
|
|
128
|
+
else
|
|
129
|
+
print_error "Failed to find ts-proto plugin"
|
|
130
|
+
return 1
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
print_info "Using ts-proto plugin: $plugin_bin"
|
|
135
|
+
|
|
136
|
+
# Find Google's well-known protos include path
|
|
137
|
+
google_proto_path=""
|
|
138
|
+
if [ -d "/usr/local/include/google/protobuf" ]; then
|
|
139
|
+
google_proto_path="/usr/local/include"
|
|
140
|
+
elif [ -d "/opt/homebrew/include/google/protobuf" ]; then
|
|
141
|
+
google_proto_path="/opt/homebrew/include"
|
|
142
|
+
elif [ -d "$HOME/.local/include/google/protobuf" ]; then
|
|
143
|
+
google_proto_path="$HOME/.local/include"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
google_include=""
|
|
147
|
+
if [ -n "$google_proto_path" ]; then
|
|
148
|
+
google_include="-I$google_proto_path"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Also emit TS for well-known Google protos referenced by our service protos (e.g., wrappers.proto)
|
|
152
|
+
# so downstream imports like BoolValue resolve from generated-types/messages/google/protobuf.
|
|
153
|
+
if [ -n "$google_proto_path" ]; then
|
|
154
|
+
print_info "Generating TS for google/protobuf well-known types (wrappers, timestamp, empty, struct)..."
|
|
155
|
+
|
|
156
|
+
wk_protos="wrappers.proto timestamp.proto empty.proto struct.proto"
|
|
157
|
+
for wk in $wk_protos; do
|
|
158
|
+
wk_file="$google_proto_path/google/protobuf/$wk"
|
|
159
|
+
if [ -f "$wk_file" ]; then
|
|
160
|
+
print_info " - $wk"
|
|
161
|
+
protoc \
|
|
162
|
+
$google_include \
|
|
163
|
+
--plugin=protoc-gen-ts_proto="$plugin_bin" \
|
|
164
|
+
--ts_proto_opt=esModuleInterop=true,forceLong=string,useOptionals=messages \
|
|
165
|
+
--ts_proto_out="$ts_proto_dir" \
|
|
166
|
+
"$wk_file" \
|
|
167
|
+
2>&1 | grep -v "google/protobuf" | grep -v "^$" || true
|
|
168
|
+
else
|
|
169
|
+
print_warn "Well-known proto not found: $wk_file"
|
|
170
|
+
fi
|
|
171
|
+
done
|
|
172
|
+
else
|
|
173
|
+
print_warn "Google well-known protos include path not found; will not generate wrappers/timestamp/empty/struct TS types"
|
|
174
|
+
print_warn "Install protobuf includes (e.g., via Homebrew) so google/protobuf/wrappers.proto can be generated"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
for proto_file in $proto_files; do
|
|
178
|
+
dir=$(dirname "$proto_file")
|
|
179
|
+
base=$(basename "$proto_file" .proto)
|
|
180
|
+
|
|
181
|
+
print_info "Processing $proto_file..."
|
|
182
|
+
|
|
183
|
+
# Find google/api protos (for annotations.proto)
|
|
184
|
+
google_api_path=""
|
|
185
|
+
if [ -d "$SERVER_DIR/services/google" ]; then
|
|
186
|
+
google_api_path="-I$SERVER_DIR/services"
|
|
187
|
+
elif [ -d "$dir/google" ]; then
|
|
188
|
+
google_api_path="-I$dir"
|
|
189
|
+
elif [ -d "$dir/../google" ]; then
|
|
190
|
+
google_api_path="-I$dir/.."
|
|
191
|
+
elif [ -d "$dir/../../google" ]; then
|
|
192
|
+
google_api_path="-I$dir/../.."
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
protoc_exit_code=0
|
|
196
|
+
protoc_output=$(protoc \
|
|
197
|
+
-I"$SERVER_DIR/services" \
|
|
198
|
+
$google_api_path \
|
|
199
|
+
$google_include \
|
|
200
|
+
--plugin=protoc-gen-ts_proto="$plugin_bin" \
|
|
201
|
+
--ts_proto_opt=esModuleInterop=true,forceLong=string,useOptionals=messages \
|
|
202
|
+
--ts_proto_out="$ts_proto_dir" \
|
|
203
|
+
"$proto_file" \
|
|
204
|
+
2>&1) || protoc_exit_code=$?
|
|
205
|
+
|
|
206
|
+
filtered_output=$(echo "$protoc_output" | grep -v "google/protobuf" | grep -v "^$" || true)
|
|
207
|
+
if [ -n "$filtered_output" ]; then
|
|
208
|
+
echo "$filtered_output"
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
if [ $protoc_exit_code -ne 0 ]; then
|
|
212
|
+
exit 1
|
|
213
|
+
fi
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
print_info "TypeScript message types generated"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Generate OpenAPI specs using protoc-gen-openapiv2
|
|
220
|
+
generate_openapi_specs() {
|
|
221
|
+
print_info "Generating OpenAPI specs using protoc-gen-openapiv2..."
|
|
222
|
+
|
|
223
|
+
proto_files=$(find_proto_files)
|
|
224
|
+
openapi_dir="$TEMP_DIR/openapi"
|
|
225
|
+
mkdir -p "$openapi_dir"
|
|
226
|
+
|
|
227
|
+
# Detect whether protoc-gen-openapiv2 supports include_source_info
|
|
228
|
+
include_source_info_opt=""
|
|
229
|
+
if [ -n "${OPENAPI_PLUGIN_PATH:-}" ]; then
|
|
230
|
+
# Help output differs by version; use a conservative grep.
|
|
231
|
+
if "$OPENAPI_PLUGIN_PATH" --help 2>&1 | grep -q "include_source_info"; then
|
|
232
|
+
include_source_info_opt="--openapiv2_opt=include_source_info=true"
|
|
233
|
+
print_info "protoc-gen-openapiv2 supports include_source_info=true"
|
|
234
|
+
else
|
|
235
|
+
print_warn "protoc-gen-openapiv2 does not support include_source_info; skipping that option"
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
for proto_file in $proto_files; do
|
|
240
|
+
dir=$(dirname "$proto_file")
|
|
241
|
+
base=$(basename "$proto_file" .proto)
|
|
242
|
+
service_dir=$(echo "$dir" | sed "s|$SERVER_DIR/services/||" | sed 's|/protos||')
|
|
243
|
+
|
|
244
|
+
print_info "Generating OpenAPI spec for $proto_file..."
|
|
245
|
+
|
|
246
|
+
(
|
|
247
|
+
cd "$dir"
|
|
248
|
+
|
|
249
|
+
# Generate OpenAPI spec
|
|
250
|
+
if [ -z "$OPENAPI_PLUGIN_PATH" ]; then
|
|
251
|
+
print_warn "Skipping OpenAPI generation for $proto_file (plugin not available)"
|
|
252
|
+
continue
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
plugin_arg=""
|
|
256
|
+
if [ -n "$OPENAPI_PLUGIN_PATH" ] && [ "$OPENAPI_PLUGIN_PATH" != "protoc-gen-openapiv2" ]; then
|
|
257
|
+
plugin_arg="--plugin=protoc-gen-openapiv2=$OPENAPI_PLUGIN_PATH"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
protoc -I. -I../../../../ -I./ \
|
|
261
|
+
$plugin_arg \
|
|
262
|
+
--openapiv2_out="$openapi_dir" \
|
|
263
|
+
--openapiv2_opt=logtostderr=true \
|
|
264
|
+
--openapiv2_opt=allow_merge=true \
|
|
265
|
+
--openapiv2_opt=merge_file_name="$base" \
|
|
266
|
+
--openapiv2_opt=openapi_naming_strategy=simple \
|
|
267
|
+
${include_source_info_opt:-} \
|
|
268
|
+
"$base.proto" 2>&1 | grep -v "google/protobuf" || true
|
|
269
|
+
)
|
|
270
|
+
done
|
|
271
|
+
|
|
272
|
+
print_info "OpenAPI specs generated"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Process OpenAPI specs (we'll use the JSON directly, not convert to TS first)
|
|
276
|
+
process_openapi_specs() {
|
|
277
|
+
print_info "Processing OpenAPI specs..."
|
|
278
|
+
|
|
279
|
+
openapi_dir="$TEMP_DIR/openapi"
|
|
280
|
+
|
|
281
|
+
# Find all OpenAPI spec files
|
|
282
|
+
openapi_files=$(find "$openapi_dir" -name "*.swagger.json" -o -name "*.json" | grep -v "google")
|
|
283
|
+
|
|
284
|
+
if [ -z "$openapi_files" ]; then
|
|
285
|
+
print_warn "No OpenAPI spec files found"
|
|
286
|
+
return
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
print_info "Found OpenAPI spec files, will process them in combine_outputs"
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Generate *-endpoints.ts files from all OpenAPI specs using the Node processor.
|
|
293
|
+
generate_endpoints_from_openapi() {
|
|
294
|
+
print_info "Generating *-endpoints.ts files from OpenAPI specs..."
|
|
295
|
+
|
|
296
|
+
openapi_dir="$TEMP_DIR/openapi"
|
|
297
|
+
if [ ! -d "$openapi_dir" ]; then
|
|
298
|
+
print_warn "OpenAPI directory not found: $openapi_dir"
|
|
299
|
+
return
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
openapi_files=$(find "$openapi_dir" -name "*.swagger.json" -o -name "*.json" | grep -v "google" || true)
|
|
303
|
+
if [ -z "${openapi_files:-}" ]; then
|
|
304
|
+
print_warn "No OpenAPI spec files found for endpoint generation"
|
|
305
|
+
return
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
processor="$SCRIPT_DIR/process-openapi-types.js"
|
|
309
|
+
if [ ! -f "$processor" ]; then
|
|
310
|
+
print_error "OpenAPI processor not found: $processor"
|
|
311
|
+
exit 1
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
for spec in $openapi_files; do
|
|
315
|
+
base=$(basename "$spec")
|
|
316
|
+
service=$(echo "$base" | sed 's/\.swagger\.json$//' | sed 's/\.json$//')
|
|
317
|
+
|
|
318
|
+
# outputFile is used only to determine the output directory and some message type inference.
|
|
319
|
+
# We keep it inside OUTPUT_DIR.
|
|
320
|
+
out_stub="$OUTPUT_DIR/${service}-types.ts"
|
|
321
|
+
|
|
322
|
+
print_info "Generating endpoints for $service from $base"
|
|
323
|
+
node "$processor" "$spec" "$out_stub" "$service" "$OUTPUT_DIR/messages" "$SERVER_DIR" || true
|
|
324
|
+
done
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Convert interfaces to types in generated files
|
|
328
|
+
convert_interfaces_to_types() {
|
|
329
|
+
target_dir="$1"
|
|
330
|
+
print_info "Converting interfaces to types..."
|
|
331
|
+
|
|
332
|
+
# The previous sed-based approach corrupted the ts-proto output by removing the interface body.
|
|
333
|
+
# This transform preserves the entire interface block and just changes `export interface X` -> `export type X =`.
|
|
334
|
+
find "$target_dir" -name "*.ts" -type f ! -name "index.ts" | while read -r file; do
|
|
335
|
+
perl -0777 -i -pe 's/^export\s+interface\s+([A-Za-z_][A-ZaZ0-9_]*)\s*\{/export type $1 = { /mg' "$file"
|
|
336
|
+
done
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# Flatten ts-proto output so service protos land directly in generated-types/messages (e.g. messages/user.ts)
|
|
340
|
+
# and rewrite internal imports to valid relative paths in the flattened structure.
|
|
341
|
+
flatten_messages() {
|
|
342
|
+
ts_proto_dir="$1"
|
|
343
|
+
out_messages_dir="$2"
|
|
344
|
+
|
|
345
|
+
print_info "Flattening generated message files into $out_messages_dir ..."
|
|
346
|
+
|
|
347
|
+
rm -rf "$out_messages_dir"
|
|
348
|
+
mkdir -p "$out_messages_dir"
|
|
349
|
+
|
|
350
|
+
# 1) Copy google well-known types (preserve nested path google/protobuf)
|
|
351
|
+
if [ -d "$ts_proto_dir/google" ]; then
|
|
352
|
+
mkdir -p "$out_messages_dir/google"
|
|
353
|
+
cp -R "$ts_proto_dir/google" "$out_messages_dir/" 2>/dev/null || true
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# 2) Copy non-google generated proto outputs (flatten)
|
|
357
|
+
# We take the last path segment as the module name (e.g. config-service/protos/config.ts -> config.ts)
|
|
358
|
+
find "$ts_proto_dir" -type f -name "*.ts" | while read -r f; do
|
|
359
|
+
rel="${f#$ts_proto_dir/}"
|
|
360
|
+
|
|
361
|
+
case "$rel" in
|
|
362
|
+
google/*) continue ;; # already handled
|
|
363
|
+
esac
|
|
364
|
+
|
|
365
|
+
base=$(basename "$f")
|
|
366
|
+
|
|
367
|
+
# Avoid overwriting if there are name collisions; warn and skip.
|
|
368
|
+
if [ -f "$out_messages_dir/$base" ]; then
|
|
369
|
+
print_warn "Flatten skip (name collision): $rel -> $base"
|
|
370
|
+
continue
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
cp "$f" "$out_messages_dir/$base"
|
|
374
|
+
done
|
|
375
|
+
|
|
376
|
+
# 3) Rewrite imports in flattened files so they resolve in the new layout.
|
|
377
|
+
# ts-proto emits imports like: import { Empty } from "../../google/protobuf/empty";
|
|
378
|
+
# After flattening, the correct path is: import { Empty } from "./google/protobuf/empty";
|
|
379
|
+
find "$out_messages_dir" -type f -name "*.ts" | while read -r file; do
|
|
380
|
+
# Only rewrite in top-level flattened files; google/protobuf files keep theirs.
|
|
381
|
+
case "$file" in
|
|
382
|
+
*"/google/protobuf/"*) continue ;;
|
|
383
|
+
esac
|
|
384
|
+
|
|
385
|
+
perl -i -pe 's/from "\.\.\/\.\.\/google\/protobuf\//from "\.\/google\/protobuf\//g' "$file"
|
|
386
|
+
perl -i -pe 's/from "\.\.\/\.\.\/\.\.\/google\/protobuf\//from "\.\/google\/protobuf\//g' "$file"
|
|
387
|
+
perl -i -pe 's/from "\.\.\/google\/protobuf\//from "\.\/google\/protobuf\//g' "$file"
|
|
388
|
+
done
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# Re-export google/protobuf imports from each top-level message file.
|
|
392
|
+
# We ensure the symbols are BOTH imported (so local references compile) and re-exported
|
|
393
|
+
# (so downstream can import them from the message module).
|
|
394
|
+
reexport_google_imports() {
|
|
395
|
+
out_messages_dir="$1"
|
|
396
|
+
|
|
397
|
+
print_info "Ensuring ./google/protobuf symbols are imported + re-exported from flattened message files..."
|
|
398
|
+
|
|
399
|
+
find "$out_messages_dir" -maxdepth 1 -type f -name "*.ts" ! -name "index.ts" | while read -r file; do
|
|
400
|
+
# Skip the google-protobuf generated files themselves
|
|
401
|
+
case "$file" in
|
|
402
|
+
*"/google/"*) continue ;;
|
|
403
|
+
esac
|
|
404
|
+
|
|
405
|
+
# 1) If we previously converted imports to re-exports, restore them to import+export.
|
|
406
|
+
# export { X } from "./google/protobuf/wrappers";
|
|
407
|
+
# -> import { X } from "./google/protobuf/wrappers";
|
|
408
|
+
# export { X };
|
|
409
|
+
perl -0777 -i -pe 's/^export\s+\{\s*([^}]+?)\s*\}\s+from\s+"(\.\/google\/protobuf\/[^\"]+)"\s*;\s*$/import { $1 } from "$2";\nexport { $1 };\n/mg' "$file"
|
|
410
|
+
|
|
411
|
+
# 2) Same for type-only.
|
|
412
|
+
# export type { X } from "./google/protobuf/wrappers";
|
|
413
|
+
# -> import type { X } from "./google/protobuf/wrappers";
|
|
414
|
+
# export type { X };
|
|
415
|
+
perl -0777 -i -pe 's/^export\s+type\s+\{\s*([^}]+?)\s*\}\s+from\s+"(\.\/google\/protobuf\/[^\"]+)"\s*;\s*$/import type { $1 } from "$2";\nexport type { $1 };\n/mg' "$file"
|
|
416
|
+
|
|
417
|
+
# 3) If file still has plain imports, add a matching export line.
|
|
418
|
+
# import { X } from "./google/protobuf/wrappers";
|
|
419
|
+
# -> import { X } from "./google/protobuf/wrappers";
|
|
420
|
+
# export { X };
|
|
421
|
+
perl -0777 -i -pe 's/^import\s+\{\s*([^}]+?)\s*\}\s+from\s+"(\.\/google\/protobuf\/[^\"]+)"\s*;\s*$/import { $1 } from "$2";\nexport { $1 };\n/mg' "$file"
|
|
422
|
+
|
|
423
|
+
perl -0777 -i -pe 's/^import\s+type\s+\{\s*([^}]+?)\s*\}\s+from\s+"(\.\/google\/protobuf\/[^\"]+)"\s*;\s*$/import type { $1 } from "$2";\nexport type { $1 };\n/mg' "$file"
|
|
424
|
+
done
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
# Generate a top-level index.ts that exports per-service endpoints consts and namespaced message types
|
|
428
|
+
generate_generated_types_index() {
|
|
429
|
+
out_dir="$1"
|
|
430
|
+
messages_dir="$out_dir/messages"
|
|
431
|
+
|
|
432
|
+
print_info "Generating $out_dir/index.ts ..."
|
|
433
|
+
|
|
434
|
+
endpoints_files=$(find "$out_dir" -maxdepth 1 -type f -name "*-endpoints.ts" | sort || true)
|
|
435
|
+
|
|
436
|
+
# Helper: kebab-case -> PascalCase (e.g. data-analysis -> DataAnalysis)
|
|
437
|
+
to_pascal() {
|
|
438
|
+
perl -pe '$_=join("", map { ucfirst($_) } split(/-/, $_)); chomp' <<< "$1"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
{
|
|
442
|
+
echo "// Auto-generated barrel file for generated-types"
|
|
443
|
+
echo ""
|
|
444
|
+
echo "export * from './envWrapper';"
|
|
445
|
+
echo ""
|
|
446
|
+
|
|
447
|
+
for f in $endpoints_files; do
|
|
448
|
+
base=$(basename "$f" "-endpoints.ts")
|
|
449
|
+
svc=$(to_pascal "$base")
|
|
450
|
+
service_const="${svc}Service"
|
|
451
|
+
|
|
452
|
+
# Re-export the service const only (keeps endpoint surface simple)
|
|
453
|
+
echo "export { ${service_const} } from './${base}-endpoints';"
|
|
454
|
+
|
|
455
|
+
# Export message types under a namespace to avoid collisions
|
|
456
|
+
msg_guess_a="$messages_dir/${base}.ts"
|
|
457
|
+
msg_guess_b="$messages_dir/$(echo "$base" | tr '-' '_').ts"
|
|
458
|
+
|
|
459
|
+
if [ -f "$msg_guess_a" ]; then
|
|
460
|
+
echo "export * as ${svc}Types from './messages/${base}';"
|
|
461
|
+
elif [ -f "$msg_guess_b" ]; then
|
|
462
|
+
msg_mod=$(basename "$msg_guess_b" .ts)
|
|
463
|
+
echo "export * as ${svc}Types from './messages/${msg_mod}';"
|
|
464
|
+
else
|
|
465
|
+
print_warn "No message module found for ${base}: expected $msg_guess_a or $msg_guess_b" >&2
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
echo ""
|
|
469
|
+
done
|
|
470
|
+
} > "$out_dir/index.ts"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# ---- Main ----
|
|
474
|
+
main() {
|
|
475
|
+
check_dependencies
|
|
476
|
+
|
|
477
|
+
# Fresh temp
|
|
478
|
+
rm -rf "$TEMP_DIR"
|
|
479
|
+
mkdir -p "$TEMP_DIR"
|
|
480
|
+
|
|
481
|
+
# Generate message types and OpenAPI specs
|
|
482
|
+
generate_ts_proto_types
|
|
483
|
+
generate_openapi_specs
|
|
484
|
+
process_openapi_specs
|
|
485
|
+
|
|
486
|
+
# Write/flatten messages
|
|
487
|
+
flatten_messages "$TEMP_DIR/ts-proto" "$OUTPUT_DIR/messages"
|
|
488
|
+
|
|
489
|
+
# Ensure google wrapper imports are re-exported from top-level message files
|
|
490
|
+
reexport_google_imports "$OUTPUT_DIR/messages"
|
|
491
|
+
|
|
492
|
+
# Convert interfaces to types (optional)
|
|
493
|
+
convert_interfaces_to_types "$OUTPUT_DIR/messages"
|
|
494
|
+
|
|
495
|
+
# Generate endpoints from OpenAPI specs
|
|
496
|
+
generate_endpoints_from_openapi
|
|
497
|
+
|
|
498
|
+
# Generate top-level barrel exports
|
|
499
|
+
generate_generated_types_index "$OUTPUT_DIR"
|
|
500
|
+
|
|
501
|
+
# Copy package files
|
|
502
|
+
cp "$SCRIPT_DIR/typegen-package.json" "$OUTPUT_DIR/package.json"
|
|
503
|
+
cp "$SCRIPT_DIR/eslint.config.mjs" "$OUTPUT_DIR/eslint.config.mjs"
|
|
504
|
+
|
|
505
|
+
print_info "Done"
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
main "$@"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Code generated-ish fallback. DO NOT EDIT.
|
|
2
|
+
// Minimal shim for google/protobuf/empty.proto when protoc output isn't available.
|
|
3
|
+
|
|
4
|
+
export class Empty {
|
|
5
|
+
static encode(...args: any): any {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static decode(...args: any): Empty {
|
|
10
|
+
return {} as any;
|
|
11
|
+
}
|
|
12
|
+
}
|