perf(docker): бандл API, brotli-статика, wget-healthcheck, лимиты ресурсов, вендорные чанки web
This commit is contained in:
1 parent
1d7ea35a23
commit
8298454c63
10 files changed
+244
-20
No files matched your search
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/server.ts",
|
||||
"build": "tsup src/server.ts --format esm --dts",
|
||||
"build": "tsup",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"start": "node dist/server.js",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../../..')
|
||||
const read = (p: string) => readFileSync(resolve(root, p), 'utf-8')
|
||||
|
||||
const dockerfile = read('deploy/docker/authportal/Dockerfile')
|
||||
const composes = {
|
||||
'docker-compose.yml': read('docker-compose.yml'),
|
||||
'deploy/docker-compose.traefik.yml': read('deploy/docker-compose.traefik.yml'),
|
||||
}
|
||||
|
||||
describe('Dockerfile', () => {
|
||||
it('graceful stop и ограничение ресурсов Node', () => {
|
||||
expect(dockerfile).toMatch(/^STOPSIGNAL SIGTERM$/m)
|
||||
expect(dockerfile).toMatch(/NODE_OPTIONS=--max-old-space-size=\d+/)
|
||||
expect(dockerfile).toMatch(/UV_THREADPOOL_SIZE=4/)
|
||||
})
|
||||
|
||||
it('healthcheck без запуска node', () => {
|
||||
const hc = dockerfile.split('HEALTHCHECK')[1]!.split('\n').slice(0, 2).join(' ')
|
||||
expect(hc).toContain('wget')
|
||||
expect(hc).not.toContain('"node"')
|
||||
})
|
||||
|
||||
it('запуск не от root, рантайм без тулчейна и pnpm deploy', () => {
|
||||
expect(dockerfile).toMatch(/^USER node$/m)
|
||||
const runtime = dockerfile.split('AS runtime')[1]!
|
||||
expect(runtime).not.toMatch(/apk add/)
|
||||
expect(dockerfile).not.toMatch(/pnpm .*deploy/)
|
||||
})
|
||||
|
||||
it('статика сжимается только brotli', () => {
|
||||
expect(dockerfile).toContain('brotli')
|
||||
expect(dockerfile).not.toMatch(/gzip -k/)
|
||||
})
|
||||
|
||||
it('нативные модули проверяются при сборке', () => {
|
||||
expect(dockerfile).toContain('native modules ok')
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(Object.entries(composes))('%s', (_name, text) => {
|
||||
it('init, grace period и лимиты ресурсов', () => {
|
||||
expect(text).toMatch(/init: true/)
|
||||
expect(text).toMatch(/stop_grace_period: 15s/)
|
||||
expect(text).toMatch(/mem_limit: 256m/)
|
||||
expect(text).toMatch(/pids_limit: \d+/)
|
||||
expect(text).toMatch(/read_only: true/)
|
||||
expect(text).toMatch(/cap_drop:\s*\n\s*- ALL/)
|
||||
expect(text).toMatch(/no-new-privileges:true/)
|
||||
})
|
||||
|
||||
it('healthcheck через wget', () => {
|
||||
expect(text).toMatch(/"wget", "-q"/)
|
||||
expect(text).not.toMatch(/fetch\('http:\/\/127\.0\.0\.1:8080\/health'\)/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('traefik', () => {
|
||||
it('включено сжатие ответов', () => {
|
||||
expect(composes['deploy/docker-compose.traefik.yml']).toContain(
|
||||
'middlewares.auth-portal-compress.compress=true',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
// Нативные модули остаются внешними (ставятся в образ отдельно),
|
||||
// всё остальное (fastify, jose, drizzle, workspace-пакеты) бандлится в один файл.
|
||||
const NATIVE = ['better-sqlite3', '@node-rs/argon2']
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/server.ts'],
|
||||
format: ['esm'],
|
||||
target: 'node22',
|
||||
external: NATIVE,
|
||||
noExternal: [/^(?!better-sqlite3$|@node-rs\/argon2).+/],
|
||||
// CJS-зависимости внутри ESM-бандла используют require/__dirname.
|
||||
banner: {
|
||||
js: [
|
||||
"import { createRequire as __cr } from 'node:module';",
|
||||
"import { fileURLToPath as __fu } from 'node:url';",
|
||||
"import { dirname as __dn } from 'node:path';",
|
||||
'const require = __cr(import.meta.url);',
|
||||
'const __filename = __fu(import.meta.url);',
|
||||
'const __dirname = __dn(__filename);',
|
||||
].join('\n'),
|
||||
},
|
||||
clean: true,
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
// @ts-expect-error — .mjs без типов
|
||||
import { checkBudget } from '../../../scripts/ci/web-budget.mjs'
|
||||
|
||||
function dist(files: Record<string, string>): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'web-budget-'))
|
||||
for (const [name, body] of Object.entries(files)) {
|
||||
writeFileSync(join(dir, name), body)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
describe('web-budget', () => {
|
||||
it('в бюджете → без ошибок', () => {
|
||||
const dir = dist({ 'index-abc.js': 'a'.repeat(100), 'x-1.js': 'b'.repeat(100) })
|
||||
const { errors, sizes } = checkBudget(dir, { entryGz: 1000, totalGz: 2000 })
|
||||
expect(errors).toEqual([])
|
||||
expect(sizes.entryGz).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('превышение entry и total → ошибки', () => {
|
||||
const noise = Array.from({ length: 5000 }, (_, i) => `v${i}=${Math.random()}`).join(';')
|
||||
const dir = dist({ 'index-abc.js': noise })
|
||||
const { errors } = checkBudget(dir, { entryGz: 10, totalGz: 10 })
|
||||
expect(errors).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('нет entry-чанка → ошибка', () => {
|
||||
const dir = dist({ 'other.js': 'x' })
|
||||
expect(checkBudget(dir).errors[0]).toMatch(/entry chunk/)
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,22 @@ export default defineConfig(({ mode }) => ({
|
||||
? [visualizer({ filename: 'dist/stats.html', gzipSize: true })]
|
||||
: []),
|
||||
],
|
||||
build: {
|
||||
// Вендорные чанки меняются реже кода приложения → лучше кэш браузера между релизами.
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
advancedChunks: {
|
||||
groups: [
|
||||
{
|
||||
name: 'vendor-react',
|
||||
test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/,
|
||||
},
|
||||
{ name: 'vendor-tanstack', test: /node_modules[\\/]@tanstack[\\/]/ },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||
Reference in new issue
Block a user