feat: add minimal bun-compiled docker image
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"bundle": "bun build --compile ./src/main.ts --target=bun-linux-x64-baseline --minify --outfile ./dist/codexdash -e @nestjs/platform-socket.io -e amqplib -e amqp-connection-manager -e ioredis -e nats -e mqtt -e @grpc/grpc-js -e @grpc/proto-loader -e kafkajs",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
@@ -21,7 +22,9 @@
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.1",
|
||||
"@nestjs/microservices": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/websockets": "^11.0.1",
|
||||
"@prisma/client": "6.16.2",
|
||||
"argon2": "^0.44.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { AppModule } from './app.module';
|
||||
import { CodexService } from './codex/codex.service';
|
||||
import {
|
||||
buildSpaFallbackPath,
|
||||
resolveCallbackListenHost,
|
||||
resolveWebDistDir,
|
||||
shouldServeSpaFallback,
|
||||
} from './runtime-assets';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
app.enableCors({ origin: true, credentials: true });
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
@@ -15,6 +23,27 @@ async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
|
||||
const webDistDir = resolveWebDistDir({
|
||||
envWebDistDir: process.env.WEB_DIST_DIR,
|
||||
});
|
||||
if (webDistDir) {
|
||||
app.useStaticAssets(webDistDir);
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (
|
||||
shouldServeSpaFallback({
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
acceptHeader: req.headers.accept,
|
||||
})
|
||||
) {
|
||||
res.sendFile(buildSpaFallbackPath(webDistDir));
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
await app.listen(process.env.PORT ?? 3001);
|
||||
|
||||
const codexService = app.get(CodexService);
|
||||
@@ -49,6 +78,9 @@ async function bootstrap() {
|
||||
})();
|
||||
});
|
||||
|
||||
callbackServer.listen(Number(callbackUrl.port || 80), callbackUrl.hostname);
|
||||
callbackServer.listen(
|
||||
Number(callbackUrl.port || 80),
|
||||
resolveCallbackListenHost(callbackUrl.hostname),
|
||||
);
|
||||
}
|
||||
void bootstrap();
|
||||
|
||||
87
apps/api/src/runtime-assets.spec.ts
Normal file
87
apps/api/src/runtime-assets.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
buildSpaFallbackPath,
|
||||
resolveCallbackListenHost,
|
||||
resolveWebDistDir,
|
||||
shouldServeSpaFallback,
|
||||
} from './runtime-assets';
|
||||
|
||||
describe('runtime asset helpers', () => {
|
||||
it('prefers WEB_DIST_DIR when it points to an existing directory', () => {
|
||||
const webDistDir = resolveWebDistDir({
|
||||
envWebDistDir: '/tmp/codexdash-web',
|
||||
execPath: '/app/codexdash',
|
||||
directoryExists: (candidate) => candidate === '/tmp/codexdash-web',
|
||||
});
|
||||
|
||||
expect(webDistDir).toBe('/tmp/codexdash-web');
|
||||
});
|
||||
|
||||
it('falls back to a web directory next to the compiled binary', () => {
|
||||
const expected = join('/app', 'web');
|
||||
|
||||
const webDistDir = resolveWebDistDir({
|
||||
execPath: '/app/codexdash',
|
||||
directoryExists: (candidate) => candidate === expected,
|
||||
});
|
||||
|
||||
expect(webDistDir).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns null when no candidate directory exists', () => {
|
||||
expect(
|
||||
resolveWebDistDir({
|
||||
envWebDistDir: '/missing',
|
||||
execPath: '/app/codexdash',
|
||||
directoryExists: () => false,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('serves SPA fallback only for browser GET requests outside API routes', () => {
|
||||
expect(
|
||||
shouldServeSpaFallback({
|
||||
method: 'GET',
|
||||
path: '/dashboard',
|
||||
acceptHeader: 'text/html,application/xhtml+xml',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldServeSpaFallback({
|
||||
method: 'GET',
|
||||
path: '/health',
|
||||
acceptHeader: 'text/html',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldServeSpaFallback({
|
||||
method: 'GET',
|
||||
path: '/auth/login',
|
||||
acceptHeader: 'text/html',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldServeSpaFallback({
|
||||
method: 'POST',
|
||||
path: '/dashboard',
|
||||
acceptHeader: 'text/html',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldServeSpaFallback({
|
||||
method: 'GET',
|
||||
path: '/app.js',
|
||||
acceptHeader: 'text/html',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('builds the SPA entrypoint path from the web dist directory', () => {
|
||||
expect(buildSpaFallbackPath('/app/web')).toBe('/app/web/index.html');
|
||||
});
|
||||
|
||||
it('prefers an explicit callback bind host override for containers', () => {
|
||||
expect(resolveCallbackListenHost('localhost', '0.0.0.0')).toBe('0.0.0.0');
|
||||
expect(resolveCallbackListenHost('127.0.0.1')).toBe('127.0.0.1');
|
||||
});
|
||||
});
|
||||
71
apps/api/src/runtime-assets.ts
Normal file
71
apps/api/src/runtime-assets.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, extname, join } from 'node:path';
|
||||
|
||||
type ResolveWebDistDirOptions = {
|
||||
envWebDistDir?: string | null;
|
||||
execPath?: string;
|
||||
directoryExists?: (candidate: string) => boolean;
|
||||
};
|
||||
|
||||
type SpaFallbackRequest = {
|
||||
method: string;
|
||||
path: string;
|
||||
acceptHeader?: string | null;
|
||||
};
|
||||
|
||||
const API_PREFIXES = ['/auth', '/codex', '/health'];
|
||||
|
||||
export function resolveWebDistDir({
|
||||
envWebDistDir,
|
||||
execPath = process.execPath,
|
||||
directoryExists = existsSync,
|
||||
}: ResolveWebDistDirOptions = {}): string | null {
|
||||
const candidates = [
|
||||
envWebDistDir,
|
||||
join(dirname(execPath), 'web'),
|
||||
join(process.cwd(), 'apps/web/dist'),
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (directoryExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldServeSpaFallback({
|
||||
method,
|
||||
path,
|
||||
acceptHeader,
|
||||
}: SpaFallbackRequest): boolean {
|
||||
if (method !== 'GET') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(acceptHeader ?? '').includes('text/html')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
API_PREFIXES.some(
|
||||
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return extname(path) === '';
|
||||
}
|
||||
|
||||
export function buildSpaFallbackPath(webDistDir: string): string {
|
||||
return join(webDistDir, 'index.html');
|
||||
}
|
||||
|
||||
export function resolveCallbackListenHost(
|
||||
redirectHostname: string,
|
||||
bindHost = process.env.CODEX_OAUTH_CALLBACK_BIND_HOST,
|
||||
): string {
|
||||
return bindHost || redirectHostname;
|
||||
}
|
||||
Reference in New Issue
Block a user