Skip to content

Commit f0f1f4b

Browse files
authored
Add /tidewave and /tidewave/config to vite (#41)
1 parent 92f291f commit f0f1f4b

7 files changed

Lines changed: 178 additions & 100 deletions

File tree

src/core.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ts from 'typescript';
2-
2+
import path from 'path';
3+
import fs from 'fs/promises';
34
export type Program = ts.Program;
45
export type TypeChecker = ts.TypeChecker;
56
export type SourceFile = ts.SourceFile;
@@ -132,8 +133,26 @@ export interface TidewaveConfig {
132133
clientUrl?: string;
133134
allowRemoteAccess?: boolean;
134135
allowedOrigins?: string[];
136+
projectName?: string;
137+
framework?: string;
135138
team?: {
136139
id?: string;
137140
token?: string;
138141
};
139142
}
143+
144+
export async function getProjectName(defaultName = 'app'): Promise<string> {
145+
if (typeof process === 'undefined' || !process.cwd) {
146+
return defaultName;
147+
}
148+
149+
const rootDir = process.cwd();
150+
const packageJsonPath = path.join(rootDir, 'package.json');
151+
try {
152+
const packageJson = await fs.readFile(packageJsonPath, 'utf8');
153+
const { name } = JSON.parse(packageJson);
154+
return name || defaultName;
155+
} catch {
156+
return defaultName;
157+
}
158+
}

src/http/handlers/config.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Request, Response, NextFn, Handler } from '../index';
2+
import type { TidewaveConfig } from '../../core';
3+
import { default as tidewavePackage } from '../../../package.json' with { type: 'json' };
4+
5+
export function createHandleConfig(config: TidewaveConfig): Handler {
6+
return async function handleConfig(_req: Request, res: Response, next: NextFn): Promise<void> {
7+
try {
8+
const tidewaveConfig = {
9+
project_name: config.projectName || 'app',
10+
framework_type: config.framework || 'unknown',
11+
tidewave_version: tidewavePackage.version,
12+
team: config.team || {},
13+
};
14+
15+
res.statusCode = 200;
16+
res.setHeader('Content-Type', 'application/json');
17+
res.end(JSON.stringify(tidewaveConfig));
18+
} catch (err) {
19+
console.error(`[Tidewave] Failed to serve config: ${err}`);
20+
21+
if (!res.headersSent) {
22+
res.statusCode = 500;
23+
res.setHeader('Content-Type', 'application/json');
24+
res.end(
25+
JSON.stringify({
26+
error: 'Internal server error',
27+
message: err instanceof Error ? err.message : String(err),
28+
}),
29+
);
30+
}
31+
32+
next(err);
33+
}
34+
};
35+
}

src/http/handlers/html.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Request, Response, NextFn, Handler } from '../index';
2+
import type { TidewaveConfig } from '../../core';
3+
import { default as tidewavePackage } from '../../../package.json' with { type: 'json' };
4+
5+
export function createHandleHtml(config: TidewaveConfig): Handler {
6+
return async function handleHtml(req: Request, res: Response, next: NextFn): Promise<void> {
7+
// Only handle exact /tidewave path, not sub-paths
8+
const url = req.url || '/';
9+
const [pathname] = url.split('?');
10+
11+
// vite middleware strips the url, but next.js does not, so we check all three
12+
if (pathname !== '' && pathname !== '/' && pathname !== '/tidewave') {
13+
return next();
14+
}
15+
16+
try {
17+
const clientUrl = config.clientUrl || 'https://tidewave.ai';
18+
const tidewaveConfig = {
19+
project_name: config.projectName || 'app',
20+
framework_type: config.framework || 'unknown',
21+
tidewave_version: tidewavePackage.version,
22+
team: config.team || {},
23+
};
24+
25+
res.statusCode = 200;
26+
res.setHeader('Content-Type', 'text/html');
27+
res.end(`
28+
<html>
29+
<head>
30+
<meta charset="UTF-8" />
31+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
32+
<meta name="tidewave:config" content="${escapeHtml(JSON.stringify(tidewaveConfig))}" />
33+
<script type="module" src="${clientUrl}/tc/tc.js"></script>
34+
</head>
35+
<body></body>
36+
</html>
37+
`);
38+
} catch (err) {
39+
console.error(`[Tidewave] Failed to serve HTML: ${err}`);
40+
41+
if (!res.headersSent) {
42+
res.statusCode = 500;
43+
res.setHeader('Content-Type', 'text/html');
44+
res.end('<html><body>Internal server error</body></html>');
45+
}
46+
47+
next(err);
48+
}
49+
};
50+
}
51+
52+
function escapeHtml(text: string): string {
53+
const map: Record<string, string> = {
54+
'&': '&amp;',
55+
'<': '&lt;',
56+
'>': '&gt;',
57+
'"': '&quot;',
58+
"'": '&#039;',
59+
};
60+
61+
return text.replace(/[&<>"']/g, match => map[match]!);
62+
}

src/http/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import http from 'node:http';
55
import { checkOrigin, checkRemoteIp } from './security';
66
import { handleMcp } from './handlers/mcp';
77
import { handleShell } from './handlers/shell';
8+
import { createHandleHtml } from './handlers/html';
9+
import { createHandleConfig } from './handlers/config';
810
import bodyParser from 'body-parser';
911
import type { TidewaveConfig } from '../core';
1012

@@ -25,10 +27,14 @@ const DEFAULT_OPTIONS: TidewaveConfig = {
2527

2628
export type Handler = (req: Request, res: Response, next: NextFn) => Promise<void>;
2729

28-
export const HANDLERS: Record<string, Handler> = {
29-
mcp: handleMcp,
30-
shell: handleShell,
31-
} as const;
30+
function getHandlers(config: TidewaveConfig): Record<string, Handler> {
31+
return {
32+
'': createHandleHtml(config),
33+
config: createHandleConfig(config),
34+
mcp: handleMcp,
35+
shell: handleShell,
36+
};
37+
}
3238

3339
export function configureServer(
3440
server: Server = connect(),
@@ -39,7 +45,8 @@ export function configureServer(
3945
server.use(`${ENDPOINT}`, securityChecker);
4046
server.use(`${ENDPOINT}`, bodyParser.json());
4147

42-
for (const [path, handler] of Object.entries(HANDLERS)) {
48+
const handlers = getHandlers(config);
49+
for (const [path, handler] of Object.entries(handlers)) {
4350
server.use(ENDPOINT + '/' + path, handler);
4451
}
4552

@@ -64,3 +71,6 @@ export function methodNotAllowed(res: Response): void {
6471
res.end();
6572
return;
6673
}
74+
75+
// Export for use by framework integrations
76+
export { getHandlers };

src/next-js/handler.ts

Lines changed: 9 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import path from 'path';
2-
import fs from 'fs/promises';
31
import type { NextApiRequest, NextApiResponse } from 'next';
4-
import { checkSecurity, methodNotAllowed, type Request, type Response } from '../http';
2+
import { checkSecurity, type Request, type Response } from '../http';
53
import bodyParser from 'body-parser';
64
import type { TidewaveConfig } from '../core';
7-
import { default as tidewavePackage } from '../../package.json' with { type: 'json' };
5+
import { getProjectName } from '../core';
86

97
const DEFAULT_CONFIG: TidewaveConfig = {};
108

@@ -55,6 +53,10 @@ export async function tidewaveHandler(
5553
await import('../logger/instrumentation');
5654
}
5755

56+
// Set framework and projectName upfront
57+
config.framework = 'nextjs';
58+
config.projectName = config.projectName || (await getProjectName('next_app'));
59+
5860
return async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
5961
const origin = req.headers.host;
6062
const url = new URL(req.url ?? '', `http://${origin}`);
@@ -76,81 +78,12 @@ export async function tidewaveHandler(
7678
await connectWrapper(securityMiddleware)(req, res, next);
7779
await connectWrapper(bodyParser.json())(req, res, next);
7880

79-
if (req.method === 'GET' && endpoint === undefined) {
80-
return await respondTidewaveHTML(res, config);
81-
}
82-
83-
if (req.method === 'GET' && endpoint === 'config') {
84-
return await respondTidewaveConfigJSON(res, config);
85-
}
86-
87-
if (req.method !== 'POST') {
88-
return methodNotAllowed(res);
89-
}
81+
const { getHandlers } = await import('../http');
82+
const handlers = getHandlers(config);
9083

91-
const { HANDLERS } = await import('../http');
92-
const handler = HANDLERS[endpoint || ''];
84+
const handler = handlers[endpoint || ''];
9385
if (handler) return await connectWrapper(handler)(req, res, next);
9486

9587
return res.status(404).json({ message: `Route not found: ${req.method} ${req.url}` });
9688
};
9789
}
98-
99-
async function respondTidewaveHTML(res: NextApiResponse, config: TidewaveConfig): Promise<void> {
100-
const clientUrl = config.clientUrl || 'https://tidewave.ai';
101-
102-
const tidewaveConfig = await tidewaveConfigJSON(config);
103-
104-
res.status(200).setHeader('Content-Type', 'text/html').end(`
105-
<html>
106-
<head>
107-
<meta charset="UTF-8" />
108-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
109-
<meta name="tidewave:config" content="${escapeHtml(JSON.stringify(tidewaveConfig))}" />
110-
<script type="module" src="${clientUrl}/tc/tc.js"></script>
111-
</head>
112-
<body></body>
113-
</html>
114-
`);
115-
}
116-
117-
async function respondTidewaveConfigJSON(
118-
res: NextApiResponse,
119-
config: TidewaveConfig,
120-
): Promise<void> {
121-
const tidewaveConfig = await tidewaveConfigJSON(config);
122-
res.status(200).json(tidewaveConfig);
123-
}
124-
125-
async function tidewaveConfigJSON(config: TidewaveConfig): Promise<object> {
126-
return {
127-
project_name: await getProjectName(),
128-
framework_type: 'nextjs',
129-
tidewave_version: tidewavePackage.version,
130-
team: config.team || {},
131-
};
132-
}
133-
134-
async function getProjectName(): Promise<string> {
135-
const rootDir = process.cwd();
136-
const packageJsonPath = path.join(rootDir, 'package.json');
137-
try {
138-
const packageJson = await fs.readFile(packageJsonPath, 'utf8');
139-
const { name } = JSON.parse(packageJson);
140-
return name || 'next_app';
141-
} catch {
142-
return 'next_app';
143-
}
144-
}
145-
146-
function escapeHtml(text: string): string {
147-
const map: Record<string, string> = {
148-
'&': '&amp;',
149-
'<': '&lt;',
150-
'>': '&gt;',
151-
'"': '&quot;',
152-
"'": '&#039;',
153-
};
154-
155-
return text.replace(/[&<>"']/g, match => map[match]!);
156-
}

src/vite-plugin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TidewaveConfig } from './core';
22
import { configureServer } from './http';
3+
import { getProjectName } from './core';
34
import type { Plugin, ViteDevServer } from 'vite';
45
// Import instrumentation to automatically patch console
56
import './logger/instrumentation';
@@ -41,5 +42,9 @@ async function tidewaveServer(
4142
return;
4243
}
4344

44-
server.middlewares = configureServer(server.middlewares, config);
45+
// Set framework and projectName upfront
46+
config.framework = 'vite';
47+
config.projectName = config.projectName || (await getProjectName('vite_app'));
48+
49+
configureServer(server.middlewares, config);
4550
}

0 commit comments

Comments
 (0)