@hyperfixi/server-bridge 2.0.0

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.
Files changed (77) hide show
  1. package/LICENSE +20 -0
  2. package/dist/cli/config.d.ts +5 -0
  3. package/dist/cli/diff-command.d.ts +9 -0
  4. package/dist/cli/generate-command.d.ts +12 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/scan-command.d.ts +9 -0
  7. package/dist/cli/watch-command.d.ts +30 -0
  8. package/dist/cli.cjs +1853 -0
  9. package/dist/cli.cjs.map +1 -0
  10. package/dist/conventions/convention-engine.d.ts +29 -0
  11. package/dist/conventions/url-patterns.d.ts +37 -0
  12. package/dist/generate.d.ts +21 -0
  13. package/dist/generators/django-generator.d.ts +10 -0
  14. package/dist/generators/express-generator.d.ts +9 -0
  15. package/dist/generators/fastapi-generator.d.ts +10 -0
  16. package/dist/generators/hono-generator.d.ts +9 -0
  17. package/dist/generators/index.d.ts +6 -0
  18. package/dist/generators/manifest.d.ts +43 -0
  19. package/dist/generators/openapi-generator.d.ts +14 -0
  20. package/dist/generators/template-helpers.d.ts +24 -0
  21. package/dist/generators/types.d.ts +11 -0
  22. package/dist/index.cjs +1426 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +1376 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/scanner/conflict-detector.d.ts +10 -0
  28. package/dist/scanner/form-scanner.d.ts +12 -0
  29. package/dist/scanner/htmx-extractor.d.ts +9 -0
  30. package/dist/scanner/hyperscript-extractor.d.ts +9 -0
  31. package/dist/scanner/route-scanner.d.ts +21 -0
  32. package/dist/types.d.ts +123 -0
  33. package/package.json +64 -0
  34. package/src/__tests__/cli.test.ts +30 -0
  35. package/src/__tests__/conflict-detector.test.ts +126 -0
  36. package/src/__tests__/convention-engine.test.ts +58 -0
  37. package/src/__tests__/diff-command.test.ts +69 -0
  38. package/src/__tests__/django-generator.test.ts +129 -0
  39. package/src/__tests__/express-generator.test.ts +105 -0
  40. package/src/__tests__/fastapi-generator.test.ts +125 -0
  41. package/src/__tests__/form-scanner.test.ts +91 -0
  42. package/src/__tests__/generate.test.ts +89 -0
  43. package/src/__tests__/hono-generator.test.ts +90 -0
  44. package/src/__tests__/htmx-extractor.test.ts +84 -0
  45. package/src/__tests__/hyperscript-extractor.test.ts +89 -0
  46. package/src/__tests__/integration.test.ts +102 -0
  47. package/src/__tests__/manifest.test.ts +133 -0
  48. package/src/__tests__/openapi-generator.test.ts +116 -0
  49. package/src/__tests__/query-params.test.ts +101 -0
  50. package/src/__tests__/route-scanner.test.ts +47 -0
  51. package/src/__tests__/url-patterns.test.ts +74 -0
  52. package/src/__tests__/watch-command.test.ts +141 -0
  53. package/src/cli/config.ts +35 -0
  54. package/src/cli/diff-command.ts +96 -0
  55. package/src/cli/generate-command.ts +108 -0
  56. package/src/cli/index.ts +143 -0
  57. package/src/cli/scan-command.ts +72 -0
  58. package/src/cli/watch-command.ts +242 -0
  59. package/src/conventions/convention-engine.ts +68 -0
  60. package/src/conventions/url-patterns.ts +123 -0
  61. package/src/generate.ts +44 -0
  62. package/src/generators/django-generator.ts +210 -0
  63. package/src/generators/express-generator.ts +137 -0
  64. package/src/generators/fastapi-generator.ts +248 -0
  65. package/src/generators/hono-generator.ts +137 -0
  66. package/src/generators/index.ts +25 -0
  67. package/src/generators/manifest.ts +164 -0
  68. package/src/generators/openapi-generator.ts +259 -0
  69. package/src/generators/template-helpers.ts +77 -0
  70. package/src/generators/types.ts +12 -0
  71. package/src/index.ts +42 -0
  72. package/src/scanner/conflict-detector.ts +92 -0
  73. package/src/scanner/form-scanner.ts +126 -0
  74. package/src/scanner/htmx-extractor.ts +174 -0
  75. package/src/scanner/hyperscript-extractor.ts +184 -0
  76. package/src/scanner/route-scanner.ts +130 -0
  77. package/src/types.ts +151 -0
@@ -0,0 +1,174 @@
1
+ import type { RouteDescriptor, HttpMethod, RouteSource } from '../types.js';
2
+ import {
3
+ extractPathParams,
4
+ extractQueryParams,
5
+ normalizeUrl,
6
+ } from '../conventions/url-patterns.js';
7
+ import { inferConventions } from '../conventions/convention-engine.js';
8
+
9
+ /**
10
+ * Regex patterns forked from packages/vite-plugin/src/scanner.ts (lines 12-21)
11
+ * with URL capture groups added for route extraction.
12
+ */
13
+ const HTMX_REQUEST_PATTERN =
14
+ /\b(hx-get|hx-post|hx-put|hx-patch|hx-delete)\s*=\s*["']([^"']+)["']/gi;
15
+
16
+ const FIXI_ACTION_PATTERN = /\bfx-action\s*=\s*["']([^"']+)["']/gi;
17
+ const FIXI_METHOD_PATTERN = /\bfx-method\s*=\s*["'](GET|POST|PUT|PATCH|DELETE)["']/gi;
18
+
19
+ const HTMX_TARGET_PATTERN = /\b(?:hx-target|fx-target)\s*=\s*["']([^"']+)["']/gi;
20
+ const HTMX_SWAP_PATTERN = /\b(?:hx-swap|fx-swap)\s*=\s*["']([^"']+)["']/gi;
21
+
22
+ /** Map hx-{method} attribute names to HTTP methods */
23
+ const HTMX_METHOD_MAP: Record<string, HttpMethod> = {
24
+ 'hx-get': 'GET',
25
+ 'hx-post': 'POST',
26
+ 'hx-put': 'PUT',
27
+ 'hx-patch': 'PATCH',
28
+ 'hx-delete': 'DELETE',
29
+ };
30
+
31
+ interface ExtractionOptions {
32
+ file: string;
33
+ }
34
+
35
+ /**
36
+ * Extract route descriptors from htmx (hx-*) and fixi (fx-*) attributes.
37
+ */
38
+ export function extractHtmxRoutes(content: string, options: ExtractionOptions): RouteDescriptor[] {
39
+ const routes: RouteDescriptor[] = [];
40
+ const seen = new Set<string>();
41
+
42
+ // --- htmx request attributes: hx-get, hx-post, etc. ---
43
+ HTMX_REQUEST_PATTERN.lastIndex = 0;
44
+ let match;
45
+ while ((match = HTMX_REQUEST_PATTERN.exec(content)) !== null) {
46
+ const attrName = match[1].toLowerCase();
47
+ const rawUrl = match[2];
48
+ const method = HTMX_METHOD_MAP[attrName];
49
+ if (!method) continue;
50
+
51
+ addRoute(routes, seen, rawUrl, {
52
+ file: options.file,
53
+ line: lineNumberAt(content, match.index),
54
+ kind: 'hx-attr',
55
+ raw: `${match[1]}="${match[2]}"`,
56
+ explicitMethod: method,
57
+ // htmx default response is html (server-rendered partials)
58
+ fetchResponseType: 'html',
59
+ });
60
+ }
61
+
62
+ // --- fixi action attributes ---
63
+ // Collect fx-method if present (for context)
64
+ const fxMethods = collectFxMethods(content);
65
+
66
+ FIXI_ACTION_PATTERN.lastIndex = 0;
67
+ while ((match = FIXI_ACTION_PATTERN.exec(content)) !== null) {
68
+ const rawUrl = match[1];
69
+ const line = lineNumberAt(content, match.index);
70
+
71
+ // Try to find a fx-method near this fx-action (within ~200 chars)
72
+ const nearbyMethod = findNearbyFxMethod(content, match.index, fxMethods);
73
+
74
+ addRoute(routes, seen, rawUrl, {
75
+ file: options.file,
76
+ line,
77
+ kind: 'fx-attr',
78
+ raw: `fx-action="${match[1]}"`,
79
+ explicitMethod: nearbyMethod,
80
+ });
81
+ }
82
+
83
+ return routes;
84
+ }
85
+
86
+ function lineNumberAt(content: string, index: number): number {
87
+ let line = 1;
88
+ for (let i = 0; i < index && i < content.length; i++) {
89
+ if (content[i] === '\n') line++;
90
+ }
91
+ return line;
92
+ }
93
+
94
+ interface FxMethodMatch {
95
+ method: string;
96
+ index: number;
97
+ }
98
+
99
+ function collectFxMethods(content: string): FxMethodMatch[] {
100
+ const methods: FxMethodMatch[] = [];
101
+ FIXI_METHOD_PATTERN.lastIndex = 0;
102
+ let match;
103
+ while ((match = FIXI_METHOD_PATTERN.exec(content)) !== null) {
104
+ methods.push({ method: match[1], index: match.index });
105
+ }
106
+ return methods;
107
+ }
108
+
109
+ /**
110
+ * Find an fx-method attribute near an fx-action attribute.
111
+ * "Near" means within the same HTML element (~300 chars).
112
+ */
113
+ function findNearbyFxMethod(
114
+ content: string,
115
+ actionIndex: number,
116
+ fxMethods: FxMethodMatch[]
117
+ ): string | undefined {
118
+ for (const fm of fxMethods) {
119
+ if (Math.abs(fm.index - actionIndex) < 300) {
120
+ return fm.method;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ interface AddRouteOptions {
127
+ file: string;
128
+ line: number;
129
+ kind: RouteSource['kind'];
130
+ raw: string;
131
+ explicitMethod?: string;
132
+ fetchResponseType?: string;
133
+ }
134
+
135
+ function addRoute(
136
+ routes: RouteDescriptor[],
137
+ seen: Set<string>,
138
+ rawUrl: string,
139
+ options: AddRouteOptions
140
+ ): void {
141
+ // Skip external URLs
142
+ if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
143
+ return;
144
+ }
145
+
146
+ // Extract query params BEFORE normalization (which strips them)
147
+ const queryParams = extractQueryParams(rawUrl);
148
+ const path = normalizeUrl(rawUrl);
149
+
150
+ const conventions = inferConventions(path, {
151
+ explicitMethod: options.explicitMethod,
152
+ fetchResponseType: options.fetchResponseType,
153
+ });
154
+
155
+ const key = `${conventions.method}:${path}`;
156
+ if (seen.has(key)) return;
157
+ seen.add(key);
158
+
159
+ routes.push({
160
+ path,
161
+ method: conventions.method,
162
+ responseFormat: conventions.responseFormat,
163
+ source: {
164
+ file: options.file,
165
+ line: options.line,
166
+ kind: options.kind,
167
+ raw: options.raw,
168
+ },
169
+ pathParams: extractPathParams(path),
170
+ ...(queryParams.length > 0 ? { queryParams } : {}),
171
+ handlerName: conventions.handlerName,
172
+ notes: conventions.notes,
173
+ });
174
+ }
@@ -0,0 +1,184 @@
1
+ import type { RouteDescriptor, RouteSource } from '../types.js';
2
+ import {
3
+ extractPathParams,
4
+ extractQueryParams,
5
+ normalizeUrl,
6
+ } from '../conventions/url-patterns.js';
7
+ import { inferConventions } from '../conventions/convention-engine.js';
8
+
9
+ /**
10
+ * Regex patterns for finding hyperscript with fetch commands.
11
+ * Matches _="..." attributes in various quote styles.
12
+ */
13
+ const ATTR_PATTERNS = [
14
+ /_\s*=\s*"([^"]+)"/g,
15
+ /_\s*=\s*'([^']+)'/g,
16
+ /_\s*=\s*`([^`]+)`/g,
17
+ /_=\{`([^`]+)`\}/g, // JSX template literal
18
+ /_=\{['"]([^'"]+)['"]\}/g, // JSX string
19
+ ];
20
+
21
+ const SCRIPT_PATTERN = /<script[^>]*type=["']?text\/hyperscript["']?[^>]*>([\s\S]*?)<\/script>/gi;
22
+
23
+ /**
24
+ * Regex for fetch commands within hyperscript code.
25
+ * Captures: url, optional response type (as json/html/text), optional method (via GET/POST)
26
+ */
27
+ const FETCH_COMMAND = /\bfetch\s+['"]?([^\s'"]+)['"]?(?:\s+as\s+(\w+))?(?:\s+via\s+(\w+))?/gi;
28
+
29
+ /**
30
+ * Regex for "send form to URL" pattern (implies POST).
31
+ */
32
+ const SEND_FORM = /\bsend\s+(?:the\s+)?form\s+to\s+['"]?([^\s'"]+)['"]?(?:\s+as\s+(\w+))?/gi;
33
+
34
+ interface ExtractionOptions {
35
+ file: string;
36
+ }
37
+
38
+ /**
39
+ * Extract route descriptors from hyperscript attributes in file content.
40
+ */
41
+ export function extractHyperscriptRoutes(
42
+ content: string,
43
+ options: ExtractionOptions
44
+ ): RouteDescriptor[] {
45
+ const routes: RouteDescriptor[] = [];
46
+ const seen = new Set<string>();
47
+
48
+ // Collect all hyperscript code regions with their line numbers
49
+ const regions = extractHyperscriptRegions(content);
50
+
51
+ for (const region of regions) {
52
+ // Extract fetch commands
53
+ for (const match of region.code.matchAll(FETCH_COMMAND)) {
54
+ const rawUrl = match[1];
55
+ const responseType = match[2]; // json, html, text
56
+ const method = match[3]; // GET, POST, etc.
57
+
58
+ addRoute(routes, seen, rawUrl, {
59
+ file: options.file,
60
+ line: region.line,
61
+ kind: 'fetch',
62
+ raw: match[0],
63
+ explicitMethod: method,
64
+ fetchResponseType: responseType,
65
+ });
66
+ }
67
+
68
+ // Extract "send form to" commands
69
+ for (const match of region.code.matchAll(SEND_FORM)) {
70
+ const rawUrl = match[1];
71
+ const responseType = match[2];
72
+
73
+ addRoute(routes, seen, rawUrl, {
74
+ file: options.file,
75
+ line: region.line,
76
+ kind: 'fetch',
77
+ raw: match[0],
78
+ explicitMethod: 'POST',
79
+ fetchResponseType: responseType,
80
+ });
81
+ }
82
+ }
83
+
84
+ return routes;
85
+ }
86
+
87
+ interface RegionInfo {
88
+ code: string;
89
+ line: number;
90
+ }
91
+
92
+ function extractHyperscriptRegions(content: string): RegionInfo[] {
93
+ const regions: RegionInfo[] = [];
94
+ const lines = content.split('\n');
95
+
96
+ // Search for _="..." attributes
97
+ for (const pattern of ATTR_PATTERNS) {
98
+ // Reset the regex
99
+ pattern.lastIndex = 0;
100
+ let match;
101
+ while ((match = pattern.exec(content)) !== null) {
102
+ const line = lineNumberAt(content, match.index);
103
+ regions.push({ code: match[1], line });
104
+ }
105
+ }
106
+
107
+ // Search for <script type="text/hyperscript"> blocks
108
+ SCRIPT_PATTERN.lastIndex = 0;
109
+ let match;
110
+ while ((match = SCRIPT_PATTERN.exec(content)) !== null) {
111
+ const line = lineNumberAt(content, match.index);
112
+ regions.push({ code: match[1], line });
113
+ }
114
+
115
+ return regions;
116
+ }
117
+
118
+ function lineNumberAt(content: string, index: number): number {
119
+ let line = 1;
120
+ for (let i = 0; i < index && i < content.length; i++) {
121
+ if (content[i] === '\n') line++;
122
+ }
123
+ return line;
124
+ }
125
+
126
+ interface AddRouteOptions {
127
+ file: string;
128
+ line: number;
129
+ kind: RouteSource['kind'];
130
+ raw: string;
131
+ explicitMethod?: string;
132
+ fetchResponseType?: string;
133
+ }
134
+
135
+ function addRoute(
136
+ routes: RouteDescriptor[],
137
+ seen: Set<string>,
138
+ rawUrl: string,
139
+ options: AddRouteOptions
140
+ ): void {
141
+ // Skip external URLs
142
+ if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
143
+ return;
144
+ }
145
+
146
+ // Skip template-only URLs (e.g., {{baseUrl}})
147
+ if (rawUrl.startsWith('{{') || rawUrl.startsWith('${')) {
148
+ return;
149
+ }
150
+
151
+ // Skip non-URL strings (e.g., CSS selectors, keywords, bare words ending in colon)
152
+ if (!rawUrl.startsWith('/') && !rawUrl.includes('/')) {
153
+ return;
154
+ }
155
+
156
+ // Extract query params BEFORE normalization (which strips them)
157
+ const queryParams = extractQueryParams(rawUrl);
158
+ const path = normalizeUrl(rawUrl);
159
+
160
+ const conventions = inferConventions(path, {
161
+ explicitMethod: options.explicitMethod,
162
+ fetchResponseType: options.fetchResponseType,
163
+ });
164
+
165
+ const key = `${conventions.method}:${path}`;
166
+ if (seen.has(key)) return;
167
+ seen.add(key);
168
+
169
+ routes.push({
170
+ path,
171
+ method: conventions.method,
172
+ responseFormat: conventions.responseFormat,
173
+ source: {
174
+ file: options.file,
175
+ line: options.line,
176
+ kind: options.kind,
177
+ raw: options.raw,
178
+ },
179
+ pathParams: extractPathParams(path),
180
+ ...(queryParams.length > 0 ? { queryParams } : {}),
181
+ handlerName: conventions.handlerName,
182
+ notes: conventions.notes,
183
+ });
184
+ }
@@ -0,0 +1,130 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { glob } from 'glob';
3
+ import type { RouteDescriptor, ScanResult, ScanError } from '../types.js';
4
+ import { extractHyperscriptRoutes } from './hyperscript-extractor.js';
5
+ import { extractHtmxRoutes } from './htmx-extractor.js';
6
+ import { extractFormBodies } from './form-scanner.js';
7
+ import { detectConflicts } from './conflict-detector.js';
8
+ import { normalizeUrl } from '../conventions/url-patterns.js';
9
+
10
+ /**
11
+ * Default file patterns to scan.
12
+ */
13
+ const DEFAULT_INCLUDE = ['**/*.html', '**/*.htm'];
14
+ const DEFAULT_EXCLUDE = ['**/node_modules/**', '**/dist/**', '**/.git/**'];
15
+
16
+ interface ScanOptions {
17
+ include?: string[];
18
+ exclude?: string[];
19
+ /** URL patterns to ignore */
20
+ ignore?: string[];
21
+ }
22
+
23
+ /**
24
+ * Scan a single file's content for routes.
25
+ */
26
+ export function scanRoutes(content: string, file: string): RouteDescriptor[] {
27
+ const hsRoutes = extractHyperscriptRoutes(content, { file });
28
+ const htmxRoutes = extractHtmxRoutes(content, { file });
29
+
30
+ // Merge and deduplicate
31
+ const all = [...hsRoutes, ...htmxRoutes];
32
+ const deduped = deduplicateRoutes(all);
33
+
34
+ // Enrich with form body data
35
+ const formBodies = extractFormBodies(content);
36
+ for (const route of deduped) {
37
+ if (['POST', 'PUT', 'PATCH'].includes(route.method)) {
38
+ const rawUrl = route.path;
39
+ const fields = formBodies.get(rawUrl);
40
+ if (fields && fields.length > 0) {
41
+ route.requestBody = fields;
42
+ }
43
+ }
44
+ }
45
+
46
+ return deduped;
47
+ }
48
+
49
+ /**
50
+ * Scan a directory for all HTML/template files and extract routes.
51
+ */
52
+ export async function scanDirectory(dir: string, options: ScanOptions = {}): Promise<ScanResult> {
53
+ const include = options.include ?? DEFAULT_INCLUDE;
54
+ const exclude = options.exclude ?? DEFAULT_EXCLUDE;
55
+
56
+ const files: string[] = [];
57
+ for (const pattern of include) {
58
+ const matches = await glob(pattern, {
59
+ cwd: dir,
60
+ absolute: true,
61
+ ignore: exclude,
62
+ });
63
+ files.push(...matches);
64
+ }
65
+
66
+ // Deduplicate file paths
67
+ const uniqueFiles = [...new Set(files)];
68
+
69
+ const allRoutes: RouteDescriptor[] = [];
70
+ const errors: ScanError[] = [];
71
+
72
+ for (const file of uniqueFiles) {
73
+ try {
74
+ const content = await readFile(file, 'utf-8');
75
+ const routes = scanRoutes(content, file);
76
+ allRoutes.push(...routes);
77
+ } catch (err) {
78
+ errors.push({
79
+ file,
80
+ message: err instanceof Error ? err.message : String(err),
81
+ });
82
+ }
83
+ }
84
+
85
+ // Detect conflicts BEFORE deduplication (needs all occurrences)
86
+ const conflicts = detectConflicts(allRoutes);
87
+
88
+ // Final deduplication across files
89
+ const deduped = deduplicateRoutes(allRoutes);
90
+
91
+ // Apply ignore patterns
92
+ const ignored = options.ignore ?? [];
93
+ const filtered = deduped.filter(
94
+ route => !ignored.some(pattern => matchIgnorePattern(route.path, pattern))
95
+ );
96
+
97
+ return {
98
+ routes: filtered,
99
+ filesScanned: uniqueFiles,
100
+ errors,
101
+ conflicts,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Deduplicate routes by method + path.
107
+ * First occurrence wins (preserves source from first detection).
108
+ */
109
+ function deduplicateRoutes(routes: RouteDescriptor[]): RouteDescriptor[] {
110
+ const seen = new Map<string, RouteDescriptor>();
111
+ for (const route of routes) {
112
+ const key = `${route.method}:${route.path}`;
113
+ if (!seen.has(key)) {
114
+ seen.set(key, route);
115
+ }
116
+ }
117
+ return [...seen.values()];
118
+ }
119
+
120
+ /**
121
+ * Check if a URL path matches an ignore pattern.
122
+ * Supports simple wildcards: /external/** matches /external/anything
123
+ */
124
+ export function matchIgnorePattern(path: string, pattern: string): boolean {
125
+ if (pattern.endsWith('/**')) {
126
+ const prefix = pattern.slice(0, -3);
127
+ return path.startsWith(prefix);
128
+ }
129
+ return path === pattern;
130
+ }
package/src/types.ts ADDED
@@ -0,0 +1,151 @@
1
+ // =============================================================================
2
+ // Core Data Model
3
+ // =============================================================================
4
+
5
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
6
+
7
+ /**
8
+ * A server route extracted from hyperscript/htmx/fixi attributes.
9
+ */
10
+ export interface RouteDescriptor {
11
+ /** URL path pattern, e.g., "/api/users/:id" */
12
+ path: string;
13
+ /** HTTP method */
14
+ method: HttpMethod;
15
+ /** Inferred response format */
16
+ responseFormat: 'json' | 'text' | 'html';
17
+ /** Where this route was discovered */
18
+ source: RouteSource;
19
+ /** Inferred request body shape (for POST/PUT/PATCH) */
20
+ requestBody?: RequestBodyField[];
21
+ /** Path parameters extracted from URL */
22
+ pathParams: string[];
23
+ /** Query parameters extracted from URL (e.g., ?q=hello&limit=10) */
24
+ queryParams?: QueryParam[];
25
+ /** Suggested handler function name */
26
+ handlerName: string;
27
+ /** Convention-engine notes */
28
+ notes: string[];
29
+ }
30
+
31
+ export interface RouteSource {
32
+ /** Absolute file path */
33
+ file: string;
34
+ /** Line number (1-indexed) */
35
+ line?: number;
36
+ /** How the route was detected */
37
+ kind: 'fetch' | 'hx-attr' | 'fx-attr';
38
+ /** Original source text */
39
+ raw: string;
40
+ }
41
+
42
+ export interface RequestBodyField {
43
+ /** Field name from form input name="" attribute */
44
+ name: string;
45
+ /** Inferred type from input type="" attribute */
46
+ type: 'string' | 'number' | 'boolean' | 'file' | 'unknown';
47
+ /** Whether the field has the required attribute */
48
+ required: boolean;
49
+ }
50
+
51
+ export interface QueryParam {
52
+ /** Parameter name from URL query string */
53
+ name: string;
54
+ /** HTTP query params are always strings */
55
+ type: 'string';
56
+ /** Whether the param appears required (true when explicitly present in URL) */
57
+ required: boolean;
58
+ }
59
+
60
+ // =============================================================================
61
+ // Scan Results
62
+ // =============================================================================
63
+
64
+ export interface ScanResult {
65
+ /** All extracted routes */
66
+ routes: RouteDescriptor[];
67
+ /** Files that were scanned */
68
+ filesScanned: string[];
69
+ /** Errors during scanning */
70
+ errors: ScanError[];
71
+ /** Route conflicts detected (same method+path, different expectations) */
72
+ conflicts: ConflictWarning[];
73
+ }
74
+
75
+ export interface ConflictWarning {
76
+ routeKey: string;
77
+ conflicts: ConflictDetail[];
78
+ sources: RouteSource[];
79
+ }
80
+
81
+ export interface ConflictDetail {
82
+ field: 'responseFormat' | 'requestBody' | 'queryParams';
83
+ values: string[];
84
+ message: string;
85
+ }
86
+
87
+ export interface ScanError {
88
+ file: string;
89
+ line?: number;
90
+ message: string;
91
+ }
92
+
93
+ // =============================================================================
94
+ // Generator Types
95
+ // =============================================================================
96
+
97
+ export interface GeneratorOptions {
98
+ /** Output directory for generated files */
99
+ outputDir: string;
100
+ /** Whether to use TypeScript (default: true) */
101
+ typescript?: boolean;
102
+ /** Overwrite existing user code (default: false) */
103
+ overwrite?: boolean;
104
+ /** Base path prefix to strip from routes */
105
+ basePath?: string;
106
+ }
107
+
108
+ export interface GeneratedFile {
109
+ /** Relative path within outputDir */
110
+ path: string;
111
+ /** File content */
112
+ content: string;
113
+ /** Whether this is a new file or regenerated */
114
+ isNew: boolean;
115
+ }
116
+
117
+ export interface GenerateResult {
118
+ /** Generated files */
119
+ files: GeneratedFile[];
120
+ /** Routes with preserved user code */
121
+ preserved: string[];
122
+ /** Warnings */
123
+ warnings: string[];
124
+ }
125
+
126
+ // =============================================================================
127
+ // Config
128
+ // =============================================================================
129
+
130
+ export interface ServerBridgeConfig {
131
+ /** Target framework */
132
+ framework?: 'express' | 'hono' | 'openapi' | 'django' | 'fastapi';
133
+ /** Output directory */
134
+ output?: string;
135
+ /** TypeScript output */
136
+ typescript?: boolean;
137
+ /** File include patterns */
138
+ include?: string[];
139
+ /** File exclude patterns */
140
+ exclude?: string[];
141
+ /** Base path prefix to strip */
142
+ basePath?: string;
143
+ /** Convention overrides */
144
+ conventions?: {
145
+ defaultResponseFormat?: 'json' | 'html' | 'text';
146
+ fetchMethod?: HttpMethod;
147
+ formMethod?: HttpMethod;
148
+ };
149
+ /** URL patterns to ignore */
150
+ ignore?: string[];
151
+ }