feat(infra): singular dockerfile with vite bundling

Using Vite as a bundler to allow easier builds, with shared library

Moved to a single dockerfile that takes an argument to specify which
service to use

moved some file around to faciliate bundling with vite

cried a lot
This commit is contained in:
Maieul BOYER 2025-07-30 21:27:56 +02:00 committed by Maix0
parent bdc4616106
commit 573af0bc4b
21 changed files with 1587 additions and 1062 deletions

View file

@ -6,12 +6,10 @@
# By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ # # By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ # # +#+#+#+#+#+ +#+ #
# Created: 2025/06/11 18:10:26 by maiboyer #+# #+# # # Created: 2025/06/11 18:10:26 by maiboyer #+# #+# #
# Updated: 2025/07/28 18:00:02 by maiboyer ### ########.fr # # Updated: 2025/07/30 19:32:11 by maiboyer ### ########.fr #
# # # #
# **************************************************************************** # # **************************************************************************** #
BUILD_IMAGE = trans_builder
all: build all: build
docker compose up -d docker compose up -d
@ -22,7 +20,6 @@ down:
docker compose down docker compose down
build: build:
docker build -t "$(BUILD_IMAGE)" ./src
docker compose build docker compose build
re: re:
@ -30,7 +27,6 @@ re:
$(MAKE) -f ./Docker.mk all $(MAKE) -f ./Docker.mk all
clean: clean:
-docker rmi "$(BUILD_IMAGE)"
docker compose down docker compose down
prune: clean prune: clean

View file

@ -28,7 +28,10 @@ services:
# - write icons to shared volume allowing nginx to serve them (does it better than if we did it in the service) # - write icons to shared volume allowing nginx to serve them (does it better than if we did it in the service)
icons: icons:
# build is a bit strange: it has two parts # build is a bit strange: it has two parts
build: ./src/icons/ build:
context: ./src/
args:
- SERVICE=icons
container_name: icons container_name: icons
restart: always restart: always
networks: networks:

View file

@ -1,7 +1,7 @@
#forward the post request to the microservice #forward the post request to the microservice
location /api/icons/set/ { location /api/icons/set/ {
rewrite ^/api/icons/set/(.*) $1 break; rewrite ^/api/icons/set/(.*) $1 break;
proxy_pass http://icons/set/$uri; proxy_pass http://icons/$uri;
} }
#handle the get request with nginx, since it does this well #handle the get request with nginx, since it does this well

View file

@ -4,12 +4,7 @@
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"description": "shared utils library", "description": "shared utils library",
"scripts": { "scripts": {},
"embed": "npm run embed:sql",
"embed:sql": "node scripts/embed:sql.js",
"build:ts": "npm run embed:sql && tsc -d",
"watch:ts": "tsc -d -w"
},
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@ -17,14 +12,10 @@
"better-sqlite3": "^11.10.0", "better-sqlite3": "^11.10.0",
"fastify": "^5.0.0", "fastify": "^5.0.0",
"fastify-plugin": "^5.0.1", "fastify-plugin": "^5.0.1",
"typescript-result": "3.1.1",
"uuidv7": "^1.0.2" "uuidv7": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.1.0", "@types/node": "^22.1.0"
"c8": "^10.1.2",
"concurrently": "^9.0.0",
"typescript": "~5.8.2"
} }
} }

View file

@ -1,56 +0,0 @@
// ************************************************************************** //
// //
// ::: :::::::: //
// embed.js :+: :+: :+: //
// +:+ +:+ +:+ //
// By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ //
// +#+#+#+#+#+ +#+ //
// Created: 2025/06/19 23:32:59 by maiboyer #+# #+# //
// Updated: 2025/06/20 00:09:04 by maiboyer ### ########.fr //
// //
// ************************************************************************** //
import { readFile, writeFile, stat } from "node:fs/promises";
/**
* escape a string to be a valid js string literal
* @param {string} input
* @returns {string}
*/
function escape(input) {
return JSON.stringify(input)
.replace('\n', '\\n')
.replace('\t', '\\t')
.replace('\r', '\\r')
.replace('\v', '\\v');
}
/**
* @description Embed {input} inside a default exported string at location {output}
* @param {string} input
* @param {string} output
* @returns void
*/
export default async function embed(input, output) {
const inputData = (await readFile(input)).toString('utf-8');
const inputStat = await stat(input);
const escapedData = escape(inputData);
const fullFile = `\
//! this file was generated automatically.
//! it is just a string literal that is the file ${input}
//! if you want to edit this file, DONT. edit ${input} please
//!
//! this file need to be regenerated on changes to ${input} manually.
//! the \`npm run build:ts\` might regenerate it, but do check.
//! here is the date of the last time it was generated: ${new Date(Date.now())}
//! the file ${input} that is embeded was modified on ${inputStat.mtime}
//! the file ${input} that is embeded was ${inputStat.size} bytes
export default ${escapedData};\
`;
await writeFile(output, fullFile, { flush: true, flag: "w" })
}

View file

@ -1,15 +0,0 @@
// ************************************************************************** //
// //
// ::: :::::::: //
// embed:sql.js :+: :+: :+: //
// +:+ +:+ +:+ //
// By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ //
// +#+#+#+#+#+ +#+ //
// Created: 2025/06/19 23:30:39 by maiboyer #+# #+# //
// Updated: 2025/07/28 15:36:11 by maiboyer ### ########.fr //
// //
// ************************************************************************** //
import embed from "./embed.js";
await embed('./src/database/init.sql', './src/database/init.sql.ts')

View file

@ -6,16 +6,15 @@
// By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ // // By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ //
// +#+#+#+#+#+ +#+ // // +#+#+#+#+#+ +#+ //
// Created: 2025/07/28 17:36:22 by maiboyer #+# #+# // // Created: 2025/07/28 17:36:22 by maiboyer #+# #+# //
// Updated: 2025/07/28 17:36:26 by maiboyer ### ########.fr // // Updated: 2025/07/30 21:19:05 by maiboyer ### ########.fr //
// // // //
// ************************************************************************** // // ************************************************************************** //
import fp from 'fastify-plugin' import fp from 'fastify-plugin'
import { FastifyInstance } from 'fastify' import { FastifyInstance } from 'fastify'
import sqlite from 'better-sqlite3' import sqlite from 'better-sqlite3'
import { Result } from 'typescript-result'
import initSql from "./init.sql.js" import initSql from "./init.sql?raw"
import { newUUIDv7, UUIDv7 } from '@shared/uuid' import { newUUIDv7, UUIDv7 } from '@shared/uuid'

View file

@ -1,12 +0,0 @@
//! this file was generated automatically.
//! it is just a string literal that is the file ./src/database/init.sql
//! if you want to edit this file, DONT. edit ./src/database/init.sql please
//!
//! this file need to be regenerated on changes to ./src/database/init.sql manually.
//! the `npm run build:ts` might regenerate it, but do check.
//! here is the date of the last time it was generated: Mon Jul 28 2025 16:40:15 GMT+0200 (Central European Summer Time)
//! the file ./src/database/init.sql that is embeded was modified on Sat Jul 19 2025 15:33:56 GMT+0200 (Central European Summer Time)
//! the file ./src/database/init.sql that is embeded was 436 bytes
export default "-- this file will make sure that the database is always up to date with the correct schema\n-- when editing this file, make sure to always include stuff like `IF NOT EXISTS` such as to not throw error\n-- NEVER DROP ANYTHING IN THIS FILE\nCREATE TABLE IF NOT EXISTS users (\n id STRING UNIQUE PRIMARY KEY, -- UUIDv7 as a string\n name STRING UNIQUE, -- name of the user\n token STRING UNIQUE, -- the token of the user (aka the cookie)\n);\n\n";

View file

@ -6,11 +6,10 @@
// By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ // // By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ //
// +#+#+#+#+#+ +#+ // // +#+#+#+#+#+ +#+ //
// Created: 2025/06/20 17:41:01 by maiboyer #+# #+# // // Created: 2025/06/20 17:41:01 by maiboyer #+# #+# //
// Updated: 2025/07/28 15:42:53 by maiboyer ### ########.fr // // Updated: 2025/07/30 16:08:19 by maiboyer ### ########.fr //
// // // //
// ************************************************************************** // // ************************************************************************** //
import { Result } from "typescript-result";
import { uuidv7 } from "uuidv7"; import { uuidv7 } from "uuidv7";
export class InvalidUUID extends Error { export class InvalidUUID extends Error {
@ -38,11 +37,11 @@ export function isUUIDv7(value: string): value is UUIDv7 {
return uuidv7Regex.test(value); return uuidv7Regex.test(value);
} }
export function toUUIDv7(value: string): Result<UUIDv7, InvalidUUID> { //export function toUUIDv7(value: string): Result<UUIDv7, InvalidUUID> {
if (!isUUIDv7(value)) return Result.error(new InvalidUUID()); // if (!isUUIDv7(value)) return Result.error(new InvalidUUID());
//
return Result.ok(value.toLowerCase() as UUIDv7); // return Result.ok(value.toLowerCase() as UUIDv7);
} //}
export function newUUIDv7(): UUIDv7 { export function newUUIDv7(): UUIDv7 {
return uuidv7() as UUIDv7; return uuidv7() as UUIDv7;

View file

@ -1,32 +1,29 @@
# **************************************************************************** # FROM node:24-alpine as builder
# #
# ::: :::::::: #
# Dockerfile :+: :+: :+: #
# +:+ +:+ +:+ #
# By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ #
# Created: 2025/07/28 16:33:09 by maiboyer #+# #+# #
# Updated: 2025/07/29 13:54:54 by maiboyer ### ########.fr #
# #
# **************************************************************************** #
# this file will be used to build everything at once ARG SERVICE
# then all the other Dockerfiles will just copy from this one...
#
# it'll always be tagged as `trans_builder`
# this is VERY UGLY
# I didn't find really a lot of other ways to do this thought...
# so yeah have mercy :P
RUN apk add jq python3 musl-dev gcc g++ make;
# please do not touch this file unless you know what you are doing :) WORKDIR /build
COPY ./@shared/package.json /build/@shared/package.json
COPY ./${SERVICE}/package.json /build/service/package.json
COPY ./tsconfig.base.json /build/tsconfig.base.json
FROM node:24-alpine RUN echo "{\"name\":\"workspace\", \"workspaces\": [\"@shared\", \"${SERVICE}\" ]}" | jq . >/build/package.json;
RUN npm install && npm install --prefix=/build/service;
RUN apk add python3; COPY ./@shared/ /build/@shared/
COPY ./${SERVICE}/ /build/service/
RUN (cd /build/service && npm run build:prod) \
&& jq -s '.[0] * .[1]' /build/@shared/package.json /build/service/package.json >/dist/package.json;
FROM node:24
COPY ./ /src/
WORKDIR /src WORKDIR /src
RUN npm run install-all; COPY --from=builder /dist /src
RUN npm run build;
RUN npm install;
CMD ["node", "/src/run.cjs"]

View file

@ -1,18 +0,0 @@
# **************************************************************************** #
# #
# ::: :::::::: #
# Dockerfile :+: :+: :+: #
# +:+ +:+ +:+ #
# By: maiboyer <maiboyer@student.42.fr> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ #
# Created: 2025/06/16 14:57:11 by maiboyer #+# #+# #
# Updated: 2025/07/29 13:53:21 by maiboyer ### ########.fr #
# #
# **************************************************************************** #
FROM trans_builder
# do extra stuff that is specific for this service here !
CMD ["node", "/src/icons/dist/run.js"]

View file

@ -9,12 +9,9 @@
"test": "test" "test": "test"
}, },
"scripts": { "scripts": {
"test": "npm run build:ts && tsc -p test/tsconfig.json && FASTIFY_AUTOLOAD_TYPESCRIPT=1 node --test --experimental-test-coverage --loader ts-node/esm test/**/*.ts", "start": "npm run build && node dist/run.js",
"start": "npm run build:ts && fastify start -l info dist/app.js", "build": "vite build",
"build:ts": "tsc -d", "build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false"
"watch:ts": "tsc -d -w",
"dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -28,15 +25,13 @@
"fastify": "^5.0.0", "fastify": "^5.0.0",
"fastify-cli": "^7.4.0", "fastify-cli": "^7.4.0",
"fastify-plugin": "^5.0.0", "fastify-plugin": "^5.0.0",
"fastify-raw-body": "^5.0.0", "raw-body": "^3.0.0",
"sharp": "^0.34.2" "sharp": "^0.34.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.1.0", "@types/node": "^22.1.0",
"c8": "^10.1.2", "rollup-plugin-node-externals": "^8.0.1",
"concurrently": "^9.0.0", "vite": "^7.0.6",
"fastify-tsconfig": "^3.0.0", "vite-tsconfig-paths": "^5.1.4"
"ts-node": "^10.4.0",
"typescript": "~5.8.2"
} }
} }

View file

@ -1,22 +1,12 @@
import * as path from 'node:path'
import AutoLoad, { AutoloadPluginOptions } from '@fastify/autoload'
import { FastifyPluginAsync } from 'fastify' import { FastifyPluginAsync } from 'fastify'
import { fileURLToPath } from 'node:url'
import fastifyFormBody from '@fastify/formbody' import fastifyFormBody from '@fastify/formbody'
import fastifyMultipart from '@fastify/multipart' import fastifyMultipart from '@fastify/multipart'
import { mkdir } from 'node:fs/promises' import { mkdir } from 'node:fs/promises'
import fp from 'fastify-plugin' import fp from 'fastify-plugin'
const __filename = fileURLToPath(import.meta.url) const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
const __dirname = path.dirname(__filename) const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
export type AppOptions = {
// Place your custom options for app below here.
} & Partial<AutoloadPluginOptions>
// Pass --options via CLI arguments in command to enable these options.
const options: AppOptions = {
}
// When using .decorate you have to specify added properties for Typescript // When using .decorate you have to specify added properties for Typescript
declare module 'fastify' { declare module 'fastify' {
@ -25,11 +15,17 @@ declare module 'fastify' {
} }
} }
const app: FastifyPluginAsync<AppOptions> = async ( const app: FastifyPluginAsync = async (
fastify, fastify,
opts opts
): Promise<void> => { ): Promise<void> => {
// Place here your custom code! // Place here your custom code!
for (const plugin of Object.values(plugins)) {
void fastify.register(plugin, {});
}
for (const route of Object.values(routes)) {
void fastify.register(route, {});
}
//void fastify.register(MyPlugin, {}) //void fastify.register(MyPlugin, {})
void fastify.register(fastifyFormBody, {}) void fastify.register(fastifyFormBody, {})
@ -43,30 +39,7 @@ const app: FastifyPluginAsync<AppOptions> = async (
await mkdir(fastify.image_store, { recursive: true }) await mkdir(fastify.image_store, { recursive: true })
})) }))
// Do not touch the following lines
// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
// eslint-disable-next-line no-void
void fastify.register(AutoLoad, {
dir: path.join(__dirname, 'plugins'),
options: opts,
forceESM: true
})
// This loads all plugins defined in routes
// define your routes in one of these
// eslint-disable-next-line no-void
void fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: opts,
forceESM: true
})
} }
export default app export default app
export { app, options } export { app }

View file

@ -1,29 +0,0 @@
# Routes Folder
Routes define the pathways within your application.
Fastify's structure supports the modular monolith approach, where your
application is organized into distinct, self-contained modules.
This facilitates easier scaling and future transition to a microservice architecture.
In the future you might want to independently deploy some of those.
In this folder you should define all the routes that define the endpoints
of your web application.
Each service is a [Fastify
plugin](https://fastify.dev/docs/latest/Reference/Plugins/), it is
encapsulated (it can have its own independent plugins) and it is
typically stored in a file; be careful to group your routes logically,
e.g. all `/users` routes in a `users.js` file. We have added
a `root.js` file for you with a '/' root added.
If a single file becomes too large, create a folder and add a `index.js` file there:
this file must be a Fastify plugin, and it will be loaded automatically
by the application. You can now add as many files as you want inside that folder.
In this way you can create complex routes within a single monolith,
and eventually extract them.
If you need to share functionality between routes, place that
functionality into the `plugins` folder, and share it via
[decorators](https://fastify.dev/docs/latest/Reference/Decorators/).
If you're a bit confused about using `async/await` to write routes, you would
better take a look at [Promise resolution](https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution) for more details.

View file

@ -1,19 +1,23 @@
import { FastifyPluginAsync } from 'fastify' import { FastifyPluginAsync } from 'fastify'
import { join } from 'node:path' import { join } from 'node:path'
import { open } from 'node:fs/promises' import { open } from 'node:fs/promises'
import fastifyRawBody from 'fastify-raw-body'
import sharp from 'sharp' import sharp from 'sharp'
import { newUUIDv7 } from '@shared/uuid'
import rawBody from 'raw-body'
const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => { const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
// await fastify.register(authMethod, {}); // await fastify.register(authMethod, {});
// here we register plugins that will be active for the current fastify instance (aka everything in this function) // here we register plugins that will be active for the current fastify instance (aka everything in this function)
await fastify.register(fastifyRawBody, { encoding: false });
// we register a route handler for: `/<USERID_HERE>` // we register a route handler for: `/<USERID_HERE>`
// it sets some configuration options, and set the actual function that will handle the request // it sets some configuration options, and set the actual function that will handle the request
fastify.post('/:userid', { config: { rawBody: true, encoding: false } }, async function(request, reply) {
fastify.addContentTypeParser('*', function(request, payload, done) {
done()
});
fastify.post('/:userid', async function(request, reply) {
let buffer = await rawBody(request.raw);
// this is how we get the `:userid` part of things // this is how we get the `:userid` part of things
const userid: string | undefined = (request.params as any)['userid']; const userid: string | undefined = (request.params as any)['userid'];
if (userid === undefined) { if (userid === undefined) {
@ -22,14 +26,11 @@ const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
const image_store: string = fastify.getDecorator('image_store') const image_store: string = fastify.getDecorator('image_store')
const image_path = join(image_store, userid) const image_path = join(image_store, userid)
//let raw_image_file = await open(image_path + ".raw", "w", 0o666)
//await raw_image_file.write(request.rawBody as Buffer);
//await raw_image_file.close()
try { try {
let img = sharp(request.rawBody as Buffer); let img = sharp(buffer);
img.resize({ img.resize({
height: 512, height: 128,
width: 512, width: 128,
fit: 'fill', fit: 'fill',
}) })
const data = await img.png({ compressionLevel: 6 }).toBuffer() const data = await img.png({ compressionLevel: 6 }).toBuffer()
@ -44,5 +45,5 @@ const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
}) })
} }
export default example export default route

View file

@ -4,7 +4,21 @@ import fastify, { FastifyInstance } from "fastify";
import app from './app.js' import app from './app.js'
const start = async () => { const start = async () => {
const f: FastifyInstance = fastify({logger: true}); const envToLogger = {
development: {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
production: true,
test: false,
}
const f: FastifyInstance = fastify({ logger: envToLogger.development });
try { try {
await f.register(app, {}); await f.register(app, {});
await f.listen({ port: 80, host: '0.0.0.0' }) await f.listen({ port: 80, host: '0.0.0.0' })

View file

@ -1,8 +1,5 @@
{ {
"extends": "../tsconfig.base.json", "extends": "../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {},
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }

26
src/icons/vite.config.js Normal file
View file

@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import nodeExternals from 'rollup-plugin-node-externals'
import path from 'node:path'
export default defineConfig({
root: __dirname, // service root
plugins: [tsconfigPaths(), nodeExternals()],
build: {
ssr: true,
outDir: 'dist',
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, 'src/run.ts'), // adjust main entry
formats: ['cjs'], // CommonJS for Node.js
fileName: () => 'index.js',
},
rollupOptions: {
external: [],
},
target: 'node24', // or whatever Node version you use
sourcemap: true,
minify: false, // for easier debugging
}
})

2279
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,17 +3,16 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"./_shared", "./@shared",
"./icons" "./icons"
], ],
"scripts": { "scripts": {
"build": "npm run build:ts --workspaces", "build": "npm run build --workspaces --if-present",
"fclean": "rimraf \"**/dist\"", "fclean": "rimraf \"**/dist\"",
"clean": "rimraf \"**/node_modules\"", "clean": "rimraf \"**/node_modules\"",
"install-all": "npm install" "install-all": "npm install"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.3.0",
"rimraf": "^5.0.1" "rimraf": "^5.0.1"
} }
} }

View file

@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"target": "ES2023", "target": "ES2023",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "node",
"newLine": "lf", "newLine": "lf",
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
@ -16,14 +16,14 @@
"strict": true, "strict": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": ".",
"baseUrl": ".", "baseUrl": ".",
"skipLibCheck": true, "skipLibCheck": true,
"lib": ["ESNext"], "lib": ["ESNext"],
"paths": { "paths": {
"@shared/database": ["./@shared/src/database"], "@shared/database": ["@shared/src/database"],
"@shared/uuid": ["./@shared/src/uuid"] "@shared/uuid": ["@shared/src/uuid"]
} }
} }
} }