fix: properly generate build-time constants

This commit is contained in:
Christian Benincasa
2026-01-22 13:38:08 -05:00
parent ff21b76014
commit 9da132a174
14 changed files with 109 additions and 181 deletions

View File

@@ -11,3 +11,5 @@ catalog:
random-js: 2.1.0
typescript: 5.7.3
zod: ^4.1.5
enablePrePostScripts: true

3
server/.gitignore vendored
View File

@@ -10,6 +10,5 @@ streams/
bin/
src/generated/**
!src/generated/web-imports.d.ts
!src/generated/.gitkeep
!src/generated/env.ts
emby.ts

View File

@@ -15,7 +15,9 @@
"make-bin": "dotenvx run -- tsx scripts/make-bin.ts",
"clean": "rimraf --glob ./build/ ./dist/ ./bin/*",
"debug": "cross-env NODE_ENV=development dotenvx run -f .env.development -- tsx watch --trace-warnings --tsconfig ./tsconfig.build.json --ignore 'src/streams' --inspect-wait ./src",
"predev": "tsx scripts/generateEnvModule.ts",
"dev": "cross-env NODE_ENV=development dotenvx run -f .env.development -- tsx watch --heap-snapshot-on-oom --trace-warnings --tsconfig ./tsconfig.build.json --ignore 'build' --ignore 'src/streams' --ignore 'src/**/*.test.ts' ./src/index.ts",
"generate-env": "tsx scripts/generateEnvModule.ts",
"generate-openapi": "tsx src/index.ts generate-openapi",
"install-meilisearch": "tsx scripts/download-meilisearch.ts",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",

View File

@@ -1,96 +0,0 @@
import 'dotenv/config';
import esbuild from 'esbuild';
import esbuildPluginPino from 'esbuild-plugin-pino';
import fg from 'fast-glob';
import fs from 'node:fs';
import { basename } from 'node:path';
import { rimraf } from 'rimraf';
import { nativeNodeModulesPlugin } from '../esbuild/native-node-module.js';
import { nodeProtocolPlugin } from '../esbuild/node-protocol.js';
const DIST_DIR = 'dist';
if (fs.existsSync(DIST_DIR)) {
console.log('Deleting old build...');
await rimraf(DIST_DIR);
}
fs.mkdirSync(DIST_DIR);
console.log('Copying images...');
fs.cpSync('src/resources/images', `${DIST_DIR}/resources/images`, {
recursive: true,
});
const isEdgeBuild = process.env.TUNARR_EDGE_BUILD === 'true';
console.log('Bundling app...');
const result = await esbuild.build({
entryPoints: {
bundle: 'src/index.ts',
},
outExtension: {
'.js': '.cjs',
},
bundle: true,
minify: true,
outdir: DIST_DIR,
logLevel: 'info',
format: 'cjs',
platform: 'node',
target: 'node22',
inject: [
'./esbuild/bundlerPathsOverrideShim.ts',
'./esbuild/importMetaUrlShim.ts',
],
tsconfig: './tsconfig.build.json',
external: [
'mysql',
'mysql2',
'sqlite3',
'pg',
'tedious',
'pg-query-stream',
'oracledb',
'mariadb',
'libsql',
],
mainFields: ['module', 'main'],
plugins: [
nativeNodeModulesPlugin(),
nodeProtocolPlugin(),
// copy({
// resolveFrom: 'cwd',
// assets: {
// from: ['node_modules/@fastify/swagger-ui/static/*'],
// to: ['build/static'],
// },
// }),
esbuildPluginPino({
transports: ['pino-pretty', 'pino-roll'],
}),
],
keepNames: true, // This is to ensure that Entity class names remain the same
metafile: true,
define: {
'process.env.NODE_ENV': '"production"',
'process.env.TUNARR_VERSION': `"${process.env.TUNARR_VERSION}"`,
'process.env.TUNARR_BUILD': `"${process.env.TUNARR_BUILD}"`,
'process.env.TUNARR_EDGE_BUILD': `"${isEdgeBuild}"`,
'import.meta.url': '__import_meta_url',
'import.meta.dirname': '__import_meta_dirname',
},
});
fs.writeFileSync(`${DIST_DIR}/meta.json`, JSON.stringify(result.metafile));
fs.cpSync('package.json', `${DIST_DIR}/package.json`);
const nativeBindings = await fg('node_modules/better-sqlite3/**/*.node');
for (const binding of nativeBindings) {
console.log(`Copying ${binding} to out dir`);
fs.cpSync(binding, `${DIST_DIR}/build/${basename(binding)}`);
}
console.log('Done bundling!');
process.exit(0);

View File

@@ -11,6 +11,7 @@ import { nodeProtocolPlugin } from '../esbuild/node-protocol.ts';
import { trimStart } from 'lodash-es';
import { createRequire } from 'node:module';
import { generateEnvModule } from './generateEnvModule.ts';
const __require = createRequire(import.meta.url);
const esbuildPluginPino = __require('esbuild-plugin-pino');
@@ -28,17 +29,25 @@ fs.cpSync('src/resources/images', `${DIST_DIR}/resources/images`, {
recursive: true,
});
const tunarrKeyVals: Record<string, string> = {};
for (const [key, val] of Object.entries(process.env)) {
if (key.startsWith('TUNARR_') && val) {
tunarrKeyVals[key] = val;
}
}
console.debug(format('Building with Tunarr env: %O', tunarrKeyVals));
const isEdgeBuild = process.env.TUNARR_EDGE_BUILD === 'true';
// TODO: Do we want to hard-code any TUNARR_ prefixed environment variables at build time?
await generateEnvModule([
'NODE_ENV',
'TUNARR_VERSION',
'TUNARR_BUILD',
'TUNARR_EDGE_BUILD',
]);
const define = {
'process.env.NODE_ENV': '"production"',
'process.env.TUNARR_VERSION': `"${trimStart(process.env.TUNARR_VERSION, 'v')}"`,
'process.env.TUNARR_BUILD': `"${process.env.TUNARR_BUILD}"`,
'process.env.TUNARR_EDGE_BUILD': `"${isEdgeBuild}"`,
'import.meta.url': '__import_meta_url',
};
console.debug(format('Building with Tunarr env: %O', define));
console.log('Bundling app...');
const result = await esbuild.build({
entryPoints: {
@@ -87,13 +96,7 @@ const result = await esbuild.build({
],
keepNames: true, // This is to ensure that Entity class names remain the same
metafile: true,
define: {
'process.env.NODE_ENV': '"production"',
'process.env.TUNARR_VERSION': `"${trimStart(process.env.TUNARR_VERSION, 'v')}"`,
'process.env.TUNARR_BUILD': `"${process.env.TUNARR_BUILD}"`,
'process.env.TUNARR_EDGE_BUILD': `"${isEdgeBuild}"`,
'import.meta.url': '__import_meta_url',
},
define,
});
fs.writeFileSync(`${DIST_DIR}/meta.json`, JSON.stringify(result.metafile));

View File

@@ -0,0 +1,31 @@
import fs from 'node:fs/promises';
export async function generateEnvModule(keysToInline: string[]) {
const entries = keysToInline
.map((key) => {
const value = process.env[key];
if (!!value) {
return ` ${key}: ${JSON.stringify(value)},`;
}
return null;
})
.filter(Boolean)
.join('\n');
const moduleContent = `
// AUTO-GENERATED - DO NOT EDIT MANUALLY
// Generated a build time by bundle.ts
export const BUILD_ENV: Record<string, string> = {
${entries}
} as const;
export const BUILD_ENV_KEYS = new Set(Object.keys(BUILD_ENV));
`;
await fs.writeFile('./src/generated/env.ts', moduleContent);
}
if (process.argv[1] === import.meta.filename) {
await generateEnvModule([]);
}

View File

@@ -43,6 +43,7 @@ import {
isPodman,
isRunningInContainer,
} from '../util/containerUtil.ts';
import { getEnvVar, TUNARR_ENV_VARS } from '../util/env.ts';
import { streamFileBackwards } from '../util/fsUtil.ts';
import { take } from '../util/streams.ts';
@@ -457,19 +458,20 @@ export const systemApiRouter: RouterPluginAsyncCallback = async (
},
},
async (_, res) => {
const matching = seq.collect(
Object.entries(process.env),
([key, val]) => {
if (key.startsWith('TUNARR_') && val) {
return [key, val] as const;
} else if ((key === 'NODE_ENV' || key === 'LOG_LEVEL') && val) {
return [key, val] as const;
}
return;
},
);
const matches = seq.collect(Object.values(TUNARR_ENV_VARS), (key) => {
const value = getEnvVar(key);
if (!value) return;
return [key, value] as const;
});
const obj = Object.fromEntries(matches);
if (process.env.NODE_ENV) {
obj['NODE_ENV'] = process.env.NODE_ENV;
}
if (process.env.LOG_LEVEL) {
obj['LOG_LEVEL'] = process.env.LOG_LEVEL;
}
return res.send(Object.fromEntries(matching));
return res.send(obj);
},
);

View File

View File

@@ -0,0 +1,12 @@
// AUTO-GENERATED - DO NOT EDIT MANUALLY
// Generated a build time by bundle.ts
export const BUILD_ENV: Record<string, string> = {
NODE_ENV: "production",
TUNARR_VERSION: "v1.1.4",
TUNARR_BUILD: "ff21b76",
TUNARR_EDGE_BUILD: "false",
} as const;
export const BUILD_ENV_KEYS = new Set(Object.keys(BUILD_ENV));

View File

@@ -1,3 +1,4 @@
import { BUILD_ENV, BUILD_ENV_KEYS } from '@/generated/env.js';
import { TruthyQueryParam } from '../types/schemas.ts';
import type { Nullable } from '../types/util.ts';
import { isNonEmptyString, parseIntOrNull } from './index.ts';
@@ -52,6 +53,9 @@ export const TUNARR_ENV_VARS = {
type ValidEnvVar = (typeof TUNARR_ENV_VARS)[keyof typeof TUNARR_ENV_VARS];
export function getEnvVar(name: ValidEnvVar): Nullable<string> {
if (BUILD_ENV_KEYS.has(name)) {
return BUILD_ENV[name] ?? null;
}
const val = process.env[name];
return isNonEmptyString(val) ? val : null;
}

View File

@@ -459,7 +459,7 @@ export function flipMap<K extends string | number, V extends string | number>(
export const filename = (path: string) => fileURLToPath(path);
export const currentEnv = once(() => {
const env = process.env['NODE_ENV'];
const env = process.env.NODE_ENV;
return env ?? 'production';
});

View File

@@ -1,9 +1,7 @@
{
"extends": "./tsconfig.json",
"include": [
"./src/**/*.ts",
"mikro-orm.prod.config.ts",
"mikro-orm.base.config.ts"
"./src/**/*",
],
"exclude": [
"./dist/**/*",

View File

@@ -42,8 +42,8 @@
"skipLibCheck": true,
},
"include": [
"./src/**/*.ts",
"./scripts/**/*.ts",
"./src/**/*",
"./scripts/**/*",
],
"exclude": [
"./src/**/*.ignore.ts",

View File

@@ -1,74 +1,45 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": [
"//"
],
"extends": ["//"],
"tasks": {
"build": {},
"build": {
"dependsOn": ["^build", "generate-env"]
},
"typecheck": {
"dependsOn": [
"^build"
]
"dependsOn": ["^build", "generate-env"]
},
"bundle": {
"inputs": [
"./scripts/bundle.ts",
"./src/**"
],
"dependsOn": [
"^build",
"@tunarr/web#bundle"
],
"outputs": [
"dist/**"
]
"inputs": ["./scripts/bundle.ts", "./src/**"],
"dependsOn": ["^build", "@tunarr/web#bundle"],
"outputs": ["dist/**"]
},
"build-dev": {
"dependsOn": [
"^build"
]
"dependsOn": ["^build", "generate-env"]
},
"make-bin": {
"cache": false,
"dependsOn": [
"@tunarr/web#bundle",
"bundle"
],
"outputs": [
"bin/**"
]
"dependsOn": ["@tunarr/web#bundle", "bundle"],
"outputs": ["bin/**"]
},
"lint-staged": {},
"lint": {
"dependsOn": [
"lint-staged"
]
"dependsOn": ["lint-staged"]
},
"install-meilisearch": {
"inputs": [
"./scripts/download-meilisearch.ts"
],
"dependsOn": [
"@tunarr/shared#build"
],
"inputs": ["./scripts/download-meilisearch.ts"],
"dependsOn": ["@tunarr/shared#build"],
"cache": false
},
"dev": {
"dependsOn": [
"install-meilisearch",
"@tunarr/shared#build"
],
"dependsOn": ["install-meilisearch", "@tunarr/shared#build"],
"persistent": true,
"cache": false,
"interruptible": true
},
"generate-openapi": {
"inputs": [
"./src/**"
],
"dependsOn": [
"^build"
]
}
"inputs": ["./src/**"],
"dependsOn": ["^build"]
},
"generate-env": {}
}
}
}