build(api): bundle the api instead of transpiling at boot (#2227)

This commit is contained in:
Hampus
2026-08-31 01:26:30 +02:00
committed by GitHub
parent ba54b61dcf
commit 5f6f9428ac
11 changed files with 197 additions and 30 deletions
+1 -1
View File
@@ -325,7 +325,7 @@ services:
limits: limits:
memory: 1536mb memory: 1536mb
working_dir: /usr/src/app/fluxer_api working_dir: /usr/src/app/fluxer_api
command: ["./node_modules/.bin/tsx", "src/WorkerEntrypoint.ts"] command: ["node", "dist/WorkerEntrypoint.js"]
environment: environment:
<<: *fluxer-env <<: *fluxer-env
FLUXER_API_WORKER_MODE: all_lanes FLUXER_API_WORKER_MODE: all_lanes
+4 -2
View File
@@ -23,6 +23,7 @@ COPY . .
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
RUN pnpm --filter @fluxer/config run --if-present generate RUN pnpm --filter @fluxer/config run --if-present generate
RUN pnpm --filter fluxer_api run build
RUN pnpm deploy --legacy --filter=fluxer_api --prod /out RUN pnpm deploy --legacy --filter=fluxer_api --prod /out
FROM node:24-bookworm-slim FROM node:24-bookworm-slim
@@ -48,6 +49,7 @@ RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' > /etc/apt/s
RUN corepack enable && corepack prepare [email protected] --activate RUN corepack enable && corepack prepare [email protected] --activate
COPY --from=deploy /out ./ COPY --from=deploy /out ./
COPY --from=deploy /usr/src/app/fluxer_api/dist ./dist
COPY --from=deploy /usr/src/app/tsconfigs /usr/src/app/tsconfigs COPY --from=deploy /usr/src/app/tsconfigs /usr/src/app/tsconfigs
RUN rm -rf pkgs && \ RUN rm -rf pkgs && \
@@ -57,7 +59,7 @@ RUN rm -rf pkgs && \
ENV HOME=/usr/src/app ENV HOME=/usr/src/app
ENV COREPACK_HOME=/usr/src/app/.cache/corepack ENV COREPACK_HOME=/usr/src/app/.cache/corepack
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=2048" ENV NODE_OPTIONS="--max-old-space-size=2048 --enable-source-maps"
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
ENV BUILD_VERSION=${BUILD_VERSION} ENV BUILD_VERSION=${BUILD_VERSION}
@@ -65,4 +67,4 @@ USER 65532:65532
EXPOSE 8080 EXPOSE 8080
CMD ["./node_modules/.bin/tsx", "src/AppEntrypoint.ts"] CMD ["node", "dist/AppEntrypoint.js"]
+14
View File
@@ -3,6 +3,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "node scripts/build.mjs",
"test": "vitest run", "test": "vitest run",
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
"dev": "tsx watch --clear-screen=false src/AppEntrypoint.ts", "dev": "tsx watch --clear-screen=false src/AppEntrypoint.ts",
@@ -17,6 +18,7 @@
"@bluesky-social/jwk-jose": "catalog:", "@bluesky-social/jwk-jose": "catalog:",
"@bluesky-social/oauth-client-node": "catalog:", "@bluesky-social/oauth-client-node": "catalog:",
"@bufbuild/protobuf": "^2.12.0", "@bufbuild/protobuf": "^2.12.0",
"@elastic/elasticsearch": "catalog:",
"@fluxer/config": "workspace:*", "@fluxer/config": "workspace:*",
"@fluxer/constants": "workspace:*", "@fluxer/constants": "workspace:*",
"@fluxer/date_utils": "workspace:*", "@fluxer/date_utils": "workspace:*",
@@ -29,6 +31,9 @@
"@fluxer/logger": "workspace:*", "@fluxer/logger": "workspace:*",
"@fluxer/schema": "workspace:*", "@fluxer/schema": "workspace:*",
"@fluxer/snowflake": "workspace:*", "@fluxer/snowflake": "workspace:*",
"@hono/node-server": "catalog:",
"@messageformat/core": "catalog:",
"@messageformat/parser": "catalog:",
"@pkgs/cache": "workspace:*", "@pkgs/cache": "workspace:*",
"@pkgs/captcha": "workspace:*", "@pkgs/captcha": "workspace:*",
"@pkgs/cassandra": "workspace:*", "@pkgs/cassandra": "workspace:*",
@@ -49,28 +54,36 @@
"@pkgs/worker": "workspace:*", "@pkgs/worker": "workspace:*",
"@simplewebauthn/server": "catalog:", "@simplewebauthn/server": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@vvo/tzdb": "catalog:",
"archiver": "catalog:", "archiver": "catalog:",
"argon2": "catalog:", "argon2": "catalog:",
"bowser": "catalog:", "bowser": "catalog:",
"cassandra-driver": "catalog:", "cassandra-driver": "catalog:",
"emoji-regex": "catalog:",
"fast-xml-parser": "catalog:", "fast-xml-parser": "catalog:",
"hi-base32": "catalog:", "hi-base32": "catalog:",
"hono": "catalog:", "hono": "catalog:",
"html-entities": "catalog:", "html-entities": "catalog:",
"idna-uts46-hx": "catalog:", "idna-uts46-hx": "catalog:",
"ioredis": "catalog:",
"itty-time": "catalog:", "itty-time": "catalog:",
"jose": "catalog:", "jose": "catalog:",
"livekit-server-sdk": "catalog:", "livekit-server-sdk": "catalog:",
"lodash": "catalog:", "lodash": "catalog:",
"luxon": "catalog:", "luxon": "catalog:",
"maxmind": "catalog:",
"mime": "catalog:", "mime": "catalog:",
"nats": "catalog:", "nats": "catalog:",
"nodemailer": "catalog:",
"pg": "catalog:",
"pino": "catalog:",
"sharp": "catalog:", "sharp": "catalog:",
"stripe": "catalog:", "stripe": "catalog:",
"tempy": "catalog:", "tempy": "catalog:",
"transliteration": "catalog:", "transliteration": "catalog:",
"tsx": "catalog:", "tsx": "catalog:",
"uint8array-extras": "catalog:", "uint8array-extras": "catalog:",
"validator": "catalog:",
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
@@ -78,6 +91,7 @@
"@types/lodash": "catalog:", "@types/lodash": "catalog:",
"@types/luxon": "catalog:", "@types/luxon": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"esbuild": "catalog:",
"msw": "catalog:", "msw": "catalog:",
"vite-tsconfig-paths": "catalog:", "vite-tsconfig-paths": "catalog:",
"vitest": "catalog:" "vitest": "catalog:"
+87
View File
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {existsSync, readFileSync, rmSync, statSync} from 'node:fs';
import {isBuiltin} from 'node:module';
import {dirname, join, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
import {build} from 'esbuild';
const API_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const REPO_ROOT = resolve(API_ROOT, '..');
const OUT_DIR = join(API_ROOT, 'dist');
const CANDIDATE_SUFFIXES = ['', '.ts', '.tsx', '.js', '.mjs', '/index.ts', '/index.tsx', '/index.js', '/src/index.ts'];
const WORKSPACE_ROOTS = {
'@app/': join(API_ROOT, 'src'),
'@pkgs/': join(API_ROOT, 'pkgs'),
'@fluxer/': join(REPO_ROOT, 'packages'),
};
function resolveWorkspacePath(specifier) {
for (const [prefix, root] of Object.entries(WORKSPACE_ROOTS)) {
if (!specifier.startsWith(prefix)) {
continue;
}
const base = join(root, specifier.slice(prefix.length));
for (const suffix of CANDIDATE_SUFFIXES) {
const candidate = `${base}${suffix}`;
if (existsSync(candidate) && statSync(candidate).isFile()) {
return candidate;
}
}
throw new Error(`Unable to resolve workspace import ${specifier}`);
}
return null;
}
function packageNameOf(specifier) {
const segments = specifier.split('/');
return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
}
const declaredDependencies = new Set(
Object.keys(JSON.parse(readFileSync(join(API_ROOT, 'package.json'), 'utf-8')).dependencies),
);
const undeclaredDependencies = new Set();
const workspacePlugin = {
name: 'fluxer-workspace',
setup(pluginBuild) {
pluginBuild.onResolve({filter: /^[^./]/}, (args) => {
const workspacePath = resolveWorkspacePath(args.path);
if (workspacePath) {
return {path: workspacePath};
}
const packageName = packageNameOf(args.path);
if (!isBuiltin(args.path) && !declaredDependencies.has(packageName)) {
undeclaredDependencies.add(packageName);
}
return {path: args.path, external: true};
});
},
};
rmSync(OUT_DIR, {recursive: true, force: true});
await build({
entryPoints: [join(API_ROOT, 'src/AppEntrypoint.ts'), join(API_ROOT, 'src/WorkerEntrypoint.ts')],
outdir: OUT_DIR,
bundle: true,
platform: 'node',
format: 'esm',
target: 'node24',
charset: 'utf8',
sourcemap: true,
sourcesContent: false,
logLevel: 'info',
banner: {js: '// SPDX-License-Identifier: AGPL-3.0-or-later'},
plugins: [workspacePlugin],
});
if (undeclaredDependencies.size > 0) {
const names = [...undeclaredDependencies].sort().join(', ');
throw new Error(
`The bundle imports packages that fluxer_api does not declare as dependencies: ${names}. ` +
'Workspace packages keep their own node_modules, so the bundle cannot reach them from fluxer_api.',
);
}
@@ -1,14 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path';
import {fileURLToPath} from 'node:url';
import type {Hono} from 'hono'; import type {Hono} from 'hono';
import {RateLimitMiddleware} from '../middleware/RateLimitMiddleware'; import {RateLimitMiddleware} from '../middleware/RateLimitMiddleware';
import {RateLimitConfigs} from '../RateLimitConfig'; import {RateLimitConfigs} from '../RateLimitConfig';
import type {HonoEnv} from '../types/HonoEnv'; import type {HonoEnv} from '../types/HonoEnv';
import {resolveAssetPath} from '../utils/AssetPaths';
const SPEC_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), 'openapi.json'); const SPEC_PATH = resolveAssetPath('openapi', 'openapi.json');
const SPEC_BODY = fs.readFileSync(SPEC_PATH, 'utf-8'); const SPEC_BODY = fs.readFileSync(SPEC_PATH, 'utf-8');
export function OpenAPIController(app: Hono<HonoEnv>): void { export function OpenAPIController(app: Hono<HonoEnv>): void {
+23
View File
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {existsSync} from 'node:fs';
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
function findPackageRoot(startDir: string): string {
let current = startDir;
while (!existsSync(join(current, 'package.json'))) {
const parent = dirname(current);
if (parent === current) {
throw new Error(`Unable to locate the fluxer_api package root from ${startDir}`);
}
current = parent;
}
return current;
}
const PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
export function resolveAssetPath(...segments: Array<string>): string {
return join(PACKAGE_ROOT, 'src', 'api', ...segments);
}
@@ -2,13 +2,11 @@
import {randomInt} from 'node:crypto'; import {randomInt} from 'node:crypto';
import {readFileSync} from 'node:fs'; import {readFileSync} from 'node:fs';
import {dirname, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
import {UsernameType} from '@fluxer/schema/src/primitives/UserValidators'; import {UsernameType} from '@fluxer/schema/src/primitives/UserValidators';
import {resolveAssetPath} from './AssetPaths';
const WORDS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'words'); const scales = readFileSync(resolveAssetPath('words', 'scales.txt'), 'utf-8').trim().split('\n').filter(Boolean);
const scales = readFileSync(resolve(WORDS_DIR, 'scales.txt'), 'utf-8').trim().split('\n').filter(Boolean); const tails = readFileSync(resolveAssetPath('words', 'tails.txt'), 'utf-8').trim().split('\n').filter(Boolean);
const tails = readFileSync(resolve(WORDS_DIR, 'tails.txt'), 'utf-8').trim().split('\n').filter(Boolean);
function capitalize(word: string): string { function capitalize(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1); return word.charAt(0).toUpperCase() + word.slice(1);
+2 -1
View File
@@ -42,6 +42,7 @@ import type {RequestCache} from '../middleware/RequestCacheMiddleware';
import type {Channel} from '../models/Channel'; import type {Channel} from '../models/Channel';
import type {Message} from '../models/Message'; import type {Message} from '../models/Message';
import type {Webhook} from '../models/Webhook'; import type {Webhook} from '../models/Webhook';
import {resolveAssetPath} from '../utils/AssetPaths';
import * as RandomUtils from '../utils/RandomUtils'; import * as RandomUtils from '../utils/RandomUtils';
import type {IWebhookRepository} from './IWebhookRepository'; import type {IWebhookRepository} from './IWebhookRepository';
import {transform as GitHubTransform} from './transformers/GitHubTransformer'; import {transform as GitHubTransform} from './transformers/GitHubTransformer';
@@ -620,7 +621,7 @@ export class WebhookService {
const cacheKey = `webhook:${webhookId}:avatar:${provider}`; const cacheKey = `webhook:${webhookId}:avatar:${provider}`;
const avatarCache = await this.cacheService.get<string | null>(cacheKey); const avatarCache = await this.cacheService.get<string | null>(cacheKey);
if (avatarCache) return avatarCache; if (avatarCache) return avatarCache;
const avatarFile = await fs.readFile(new URL(`../assets/${provider}.webp`, import.meta.url)); const avatarFile = await fs.readFile(resolveAssetPath('assets', `${provider}.webp`));
const avatar = await this.avatarService.uploadAvatar({ const avatar = await this.avatarService.uploadAvatar({
prefix: 'avatars', prefix: 'avatars',
entityId: webhookId, entityId: webhookId,
+3 -8
View File
@@ -2,11 +2,7 @@
import {randomInt} from 'node:crypto'; import {randomInt} from 'node:crypto';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import {resolveAssetPath} from '../utils/AssetPaths';
import {fileURLToPath} from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let tails: Array<string> | undefined; let tails: Array<string> | undefined;
let scales: Array<string> | undefined; let scales: Array<string> | undefined;
@@ -34,9 +30,8 @@ export function generateConnectionId(): string {
} }
function initWords(): void { function initWords(): void {
const wordsDir = path.join(__dirname); tails = parseWordsFile(resolveAssetPath('words', 'tails.txt'));
tails = parseWordsFile(path.join(wordsDir, 'tails.txt')); scales = parseWordsFile(resolveAssetPath('words', 'scales.txt'));
scales = parseWordsFile(path.join(wordsDir, 'scales.txt'));
} }
function parseWordsFile(filePath: string): Array<string> { function parseWordsFile(filePath: string): Array<string> {
+16
View File
@@ -30,6 +30,22 @@
".": { ".": {
"project": ["scripts/**/*.mjs"] "project": ["scripts/**/*.mjs"]
}, },
"fluxer_api": {
"ignoreDependencies": [
"@elastic/elasticsearch",
"@hono/node-server",
"@messageformat/core",
"@messageformat/parser",
"@vvo/tzdb",
"emoji-regex",
"ioredis",
"maxmind",
"nodemailer",
"pg",
"pino",
"validator"
]
},
"fluxer_app": { "fluxer_app": {
"entry": ["lingui.config.js", "scripts/build-sw.mjs", "scripts/Generate*.ts"], "entry": ["lingui.config.js", "scripts/build-sw.mjs", "scripts/Generate*.ts"],
"ignoreDependencies": ["@lingui/swc-plugin"] "ignoreDependencies": ["@lingui/swc-plugin"]
+42 -10
View File
@@ -440,6 +440,9 @@ importers:
'@bufbuild/protobuf': '@bufbuild/protobuf':
specifier: ^2.12.0 specifier: ^2.12.0
version: 2.12.0 version: 2.12.0
'@elastic/elasticsearch':
specifier: 'catalog:'
version: 9.3.2
'@fluxer/config': '@fluxer/config':
specifier: workspace:* specifier: workspace:*
version: link:../packages/config version: link:../packages/config
@@ -476,6 +479,15 @@ importers:
'@fluxer/snowflake': '@fluxer/snowflake':
specifier: workspace:* specifier: workspace:*
version: link:../packages/snowflake version: link:../packages/snowflake
'@hono/node-server':
specifier: 'catalog:'
version: 1.19.9(patch_hash=8736af0d82dad75a667b7daed93f88c00065adc9a809d526353e290b150827ed)([email protected])
'@messageformat/core':
specifier: 'catalog:'
version: 3.4.0
'@messageformat/parser':
specifier: 'catalog:'
version: 5.1.1
'@pkgs/cache': '@pkgs/cache':
specifier: workspace:* specifier: workspace:*
version: link:pkgs/cache version: link:pkgs/cache
@@ -536,6 +548,9 @@ importers:
'@types/node': '@types/node':
specifier: 'catalog:' specifier: 'catalog:'
version: 25.3.0 version: 25.3.0
'@vvo/tzdb':
specifier: 'catalog:'
version: 6.198.0
archiver: archiver:
specifier: 'catalog:' specifier: 'catalog:'
version: 7.0.1 version: 7.0.1
@@ -548,6 +563,9 @@ importers:
cassandra-driver: cassandra-driver:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.8.0 version: 4.8.0
emoji-regex:
specifier: 'catalog:'
version: 10.6.0
fast-xml-parser: fast-xml-parser:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.3.7 version: 5.3.7
@@ -563,6 +581,9 @@ importers:
idna-uts46-hx: idna-uts46-hx:
specifier: 'catalog:' specifier: 'catalog:'
version: 6.1.1 version: 6.1.1
ioredis:
specifier: 'catalog:'
version: 5.9.3
itty-time: itty-time:
specifier: 'catalog:' specifier: 'catalog:'
version: 2.0.2 version: 2.0.2
@@ -578,12 +599,24 @@ importers:
luxon: luxon:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.7.2 version: 3.7.2
maxmind:
specifier: 'catalog:'
version: 5.0.5
mime: mime:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.1.0 version: 4.1.0
nats: nats:
specifier: 'catalog:' specifier: 'catalog:'
version: 2.29.3 version: 2.29.3
nodemailer:
specifier: 'catalog:'
version: 8.0.1
pg:
specifier: 'catalog:'
version: 8.16.3
pino:
specifier: 'catalog:'
version: 10.3.1
sharp: sharp:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.34.5 version: 0.34.5
@@ -602,6 +635,9 @@ importers:
uint8array-extras: uint8array-extras:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.5.0 version: 1.5.0
validator:
specifier: 'catalog:'
version: 13.15.26
zod: zod:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.3.6 version: 4.3.6
@@ -618,6 +654,9 @@ importers:
'@typescript/native-preview': '@typescript/native-preview':
specifier: 'catalog:' specifier: 'catalog:'
version: 7.0.0-dev.20260224.1 version: 7.0.0-dev.20260224.1
esbuild:
specifier: 'catalog:'
version: 0.27.3
msw: msw:
specifier: 'catalog:' specifier: 'catalog:'
version: 2.12.10(@types/[email protected])([email protected]) version: 2.12.10(@types/[email protected])([email protected])
@@ -10874,10 +10913,6 @@ packages:
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
engines: {node: '>=18.17'} engines: {node: '>=18.17'}
[email protected]:
resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
engines: {node: '>=20.18.1'}
[email protected]: [email protected]:
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
engines: {node: '>=20.18.1'} engines: {node: '>=20.18.1'}
@@ -13478,7 +13513,7 @@ snapshots:
ms: 2.1.3 ms: 2.1.3
secure-json-parse: 4.1.0 secure-json-parse: 4.1.0
tslib: 2.8.1 tslib: 2.8.1
undici: 7.22.0 undici: 7.29.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -21182,7 +21217,7 @@ snapshots:
[email protected]: [email protected]:
dependencies: dependencies:
esbuild: 0.27.3 esbuild: 0.27.2
get-tsconfig: 4.13.6 get-tsconfig: 4.13.6
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
@@ -21253,10 +21288,7 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]2.0: {} [email protected]9.0: {}
[email protected]:
optional: true
[email protected]: {} [email protected]: {}