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.
@@ -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
+ }