feat: squelette serveur/client + Docker + CI de build/release
Pose l'infrastructure de base avant l'implémentation des fonctionnalités : - server/ : Fastify + TypeScript (ESM), route /health, sert client/dist - client/ : TypeScript vanilla + esbuild/sass, vue placeholder (shell) - server/Dockerfile : image multi-étapes avec yt-dlp + ffmpeg embarqués - .forgejo/workflows/publish-docker.yml : test puis publication sur le registre de conteneurs Forgejo (nécessite le secret REGISTRY_TOKEN) - biome.json : lint du repo entier (downloader/ exclu) - spec/architecture.md : documentation de ces choix Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
097b367319
commit
29aadd28a4
25 changed files with 4721 additions and 1 deletions
155
.forgejo/workflows/publish-docker.yml
Normal file
155
.forgejo/workflows/publish-docker.yml
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
# Teste, puis build et publie l'image Docker sur le registre de conteneurs Forgejo :
|
||||||
|
# git.dev.kyuno.fr/kyuno/youtubedownloader
|
||||||
|
#
|
||||||
|
# - push sur `main` -> tags `latest` + `<sha court>`
|
||||||
|
# - push d'un tag `vX.Y.Z` -> tags `X.Y.Z`, `X.Y`, `latest`
|
||||||
|
# - déclenchement manuel possible (Actions -> Run workflow)
|
||||||
|
# - PR vers `main` -> job `test` seul (voir le `if` du job `publish`)
|
||||||
|
#
|
||||||
|
# Repris de mediaDrop/watchparty (.forgejo/workflows/publish-docker.yml) -
|
||||||
|
# mêmes prérequis côté instance :
|
||||||
|
# 1. Actions activé pour le dépôt : Settings -> Actions -> Enable.
|
||||||
|
# 2. Un runner enregistré avec le label `ubuntu-latest`, capable de lancer
|
||||||
|
# `docker` dans les jobs conteneurisés (socket hôte monté dans le
|
||||||
|
# runner - voir la config qui fait déjà tourner mediaDrop/watchparty).
|
||||||
|
# 3. Registre : secret REGISTRY_TOKEN à créer sur CE dépôt - un jeton avec
|
||||||
|
# les scopes « write:package » et « read:package » (Forgejo -> Settings
|
||||||
|
# -> Applications -> Generate New Token), le jeton automatique de
|
||||||
|
# l'Action n'a pas ce droit.
|
||||||
|
# Dépôt youtubeDownloader -> Settings -> Actions -> Secrets -> Add Secret,
|
||||||
|
# nom : REGISTRY_TOKEN, valeur : le jeton créé.
|
||||||
|
# Le nom d'utilisateur du login registre est le propriétaire du dépôt.
|
||||||
|
|
||||||
|
name: Publier l'image Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.dev.kyuno.fr
|
||||||
|
IMAGE: git.dev.kyuno.fr/${{ github.repository_owner }}/youtubedownloader
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:22-alpine
|
||||||
|
steps:
|
||||||
|
# node:22-alpine n'a pas git préinstallé - clone manuel du dépôt public.
|
||||||
|
- name: Récupérer les sources
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
apk add --no-cache git
|
||||||
|
git init -q
|
||||||
|
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
||||||
|
git fetch -q --depth 1 origin "${{ github.ref_name }}"
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Racine - installer les dépendances
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Racine - lint (Biome)
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Serveur - installer les dépendances
|
||||||
|
working-directory: server
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Serveur - vérifier les types
|
||||||
|
working-directory: server
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Serveur - lancer les tests
|
||||||
|
working-directory: server
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Serveur - build
|
||||||
|
working-directory: server
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Client - installer les dépendances
|
||||||
|
working-directory: client
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Client - vérifier les types
|
||||||
|
working-directory: client
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Client - build
|
||||||
|
working-directory: client
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: test
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: docker:27-cli
|
||||||
|
steps:
|
||||||
|
# `docker:27-cli` (Alpine) n'a pas Node : pas d'actions/checkout
|
||||||
|
# (action JS) possible ici. Clone Git direct du dépôt, comme mediaDrop.
|
||||||
|
- name: Récupérer les sources
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
apk add --no-cache git
|
||||||
|
git init -q
|
||||||
|
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
||||||
|
git fetch -q --depth 1 origin "${{ github.ref_name }}"
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Calculer les tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
v="${{ github.ref_name }}"
|
||||||
|
v="${v#v}"
|
||||||
|
list="${IMAGE}:${v} ${IMAGE}:${v%.*} ${IMAGE}:latest"
|
||||||
|
else
|
||||||
|
short="$(echo "${{ github.sha }}" | cut -c1-12)"
|
||||||
|
list="${IMAGE}:latest ${IMAGE}:${short}"
|
||||||
|
fi
|
||||||
|
echo "list=${list}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Tags : ${list}"
|
||||||
|
|
||||||
|
- name: Connexion au registre
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
|
echo "Secret REGISTRY_TOKEN manquant (voir l'en-tête du workflow)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | \
|
||||||
|
docker login "$REGISTRY" -u "${{ github.repository_owner }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
args=""
|
||||||
|
for t in ${{ steps.tags.outputs.list }}; do args="$args -t $t"; done
|
||||||
|
# Contexte = racine du dépôt (le Dockerfile copie aussi client/) -
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
docker build $args \
|
||||||
|
-f server/Dockerfile \
|
||||||
|
--label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \
|
||||||
|
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||||
|
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
for t in ${{ steps.tags.outputs.list }}; do docker push "$t"; done
|
||||||
|
|
||||||
|
- name: Déconnexion
|
||||||
|
if: always()
|
||||||
|
run: docker logout "$REGISTRY"
|
||||||
33
README.md
33
README.md
|
|
@ -24,6 +24,37 @@ Ce projet reprend et remplace une série de scripts `.cmd` (téléchargement vid
|
||||||
- **Déploiement** : Docker / docker-compose.
|
- **Déploiement** : Docker / docker-compose.
|
||||||
- **Linting** : Biome.js.
|
- **Linting** : Biome.js.
|
||||||
|
|
||||||
|
## Développement
|
||||||
|
|
||||||
|
Squelette actuel : serveur Fastify (`server/`) + client minimal (`client/`), sans encore la logique de découverte/file d'attente/téléchargement.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Racine : lint (Biome) sur tout le repo
|
||||||
|
npm install && npm run lint
|
||||||
|
|
||||||
|
# Client : build (esbuild + sass) -> client/dist
|
||||||
|
cd client && npm install && npm run build
|
||||||
|
|
||||||
|
# Serveur : typecheck, tests, dev (sert client/dist)
|
||||||
|
cd server && npm install
|
||||||
|
npm run typecheck
|
||||||
|
npm test
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Le serveur écoute par défaut sur `http://localhost:3000` (`/health` pour la vérification de santé) et sert les fichiers statiques de `client/dist` (variable `CLIENT_DIST_PATH`).
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f server/Dockerfile -t youtubedownloader .
|
||||||
|
docker run -p 3000:3000 youtubedownloader
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD
|
||||||
|
|
||||||
|
`.forgejo/workflows/publish-docker.yml` teste (lint + typecheck + tests + build) puis publie l'image sur `git.dev.kyuno.fr/kyuno/youtubedownloader` (push sur `main` -> `latest` + sha court, tag `vX.Y.Z` -> version). Nécessite un secret `REGISTRY_TOKEN` sur le dépôt (voir l'en-tête du workflow).
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
En cours de conception — pas encore de code applicatif. Voir les issues et PR pour le suivi du développement.
|
Squelette d'infrastructure en place (serveur/client minimaux, Docker, CI). Fonctionnalités listées ci-dessus pas encore implémentées. Voir les issues et PR pour le suivi du développement.
|
||||||
|
|
|
||||||
33
biome.json
Normal file
33
biome.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 100
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"semicolons": "always",
|
||||||
|
"trailingCommas": "all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignore": ["**/dist/**", "**/node_modules/**", "downloader/**"]
|
||||||
|
}
|
||||||
|
}
|
||||||
57
client/build.mjs
Normal file
57
client/build.mjs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Build client : TypeScript (+ .html/.scss par vue) -> client/dist, via
|
||||||
|
// esbuild. Script à la main plutôt qu'un outil plus lourd (Vite, webpack) :
|
||||||
|
// le client reste pour l'instant minimal, et esbuild suffit - y compris
|
||||||
|
// pour le Sass (voir le plugin ci-dessous, qui délègue à `sass` puis laisse
|
||||||
|
// esbuild regrouper le CSS résultant, importé depuis le JS, dans un
|
||||||
|
// app.css voisin).
|
||||||
|
//
|
||||||
|
// Usage : node build.mjs [--watch]
|
||||||
|
// npm run build -> build unique (celui lancé par server/Dockerfile)
|
||||||
|
// npm run watch -> reconstruit à chaque sauvegarde, pour le dev local
|
||||||
|
import { copyFileSync, mkdirSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import * as esbuild from 'esbuild';
|
||||||
|
import * as sass from 'sass';
|
||||||
|
|
||||||
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outdir = path.join(here, 'dist');
|
||||||
|
|
||||||
|
mkdirSync(outdir, { recursive: true });
|
||||||
|
copyFileSync(path.join(here, 'src', 'index.html'), path.join(outdir, 'index.html'));
|
||||||
|
|
||||||
|
/** Plugin esbuild : compile les imports `.scss` en CSS via le paquet `sass`. */
|
||||||
|
const sassPlugin = {
|
||||||
|
name: 'sass',
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\.scss$/ }, async (args) => {
|
||||||
|
const result = await sass.compileAsync(args.path);
|
||||||
|
return { contents: result.css, loader: 'css' };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildOptions = {
|
||||||
|
entryPoints: [path.join(here, 'src', 'app.ts')],
|
||||||
|
outfile: path.join(outdir, 'app.js'),
|
||||||
|
bundle: true,
|
||||||
|
format: 'esm',
|
||||||
|
target: 'es2020',
|
||||||
|
sourcemap: true,
|
||||||
|
// Chaque vue importe son propre gabarit comme chaîne brute (`import html
|
||||||
|
// from './Vue.html'` - voir global.d.ts) et son propre style Sass ; les
|
||||||
|
// styles sont regroupés automatiquement par esbuild dans app.css.
|
||||||
|
loader: { '.html': 'text' },
|
||||||
|
plugins: [sassPlugin],
|
||||||
|
};
|
||||||
|
|
||||||
|
const watch = process.argv.includes('--watch');
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
const ctx = await esbuild.context(buildOptions);
|
||||||
|
await ctx.watch();
|
||||||
|
console.log('[youtubedownloader-client] watching for changes...');
|
||||||
|
} else {
|
||||||
|
await esbuild.build(buildOptions);
|
||||||
|
console.log('[youtubedownloader-client] built to client/dist');
|
||||||
|
}
|
||||||
929
client/package-lock.json
generated
Normal file
929
client/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,929 @@
|
||||||
|
{
|
||||||
|
"name": "youtubedownloader-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "youtubedownloader-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.28.2",
|
||||||
|
"sass": "^1.83.4",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3",
|
||||||
|
"is-glob": "^4.0.3",
|
||||||
|
"node-addon-api": "^7.0.0",
|
||||||
|
"picomatch": "^4.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher-android-arm64": "2.6.0",
|
||||||
|
"@parcel/watcher-darwin-arm64": "2.6.0",
|
||||||
|
"@parcel/watcher-darwin-x64": "2.6.0",
|
||||||
|
"@parcel/watcher-freebsd-x64": "2.6.0",
|
||||||
|
"@parcel/watcher-linux-arm-glibc": "2.6.0",
|
||||||
|
"@parcel/watcher-linux-arm-musl": "2.6.0",
|
||||||
|
"@parcel/watcher-linux-arm64-glibc": "2.6.0",
|
||||||
|
"@parcel/watcher-linux-arm64-musl": "2.6.0",
|
||||||
|
"@parcel/watcher-linux-x64-glibc": "2.6.0",
|
||||||
|
"@parcel/watcher-linux-x64-musl": "2.6.0",
|
||||||
|
"@parcel/watcher-win32-arm64": "2.6.0",
|
||||||
|
"@parcel/watcher-win32-x64": "2.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-android-arm64": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-darwin-x64": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-arm64": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-x64": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||||
|
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.28.2",
|
||||||
|
"@esbuild/android-arm": "0.28.2",
|
||||||
|
"@esbuild/android-arm64": "0.28.2",
|
||||||
|
"@esbuild/android-x64": "0.28.2",
|
||||||
|
"@esbuild/darwin-arm64": "0.28.2",
|
||||||
|
"@esbuild/darwin-x64": "0.28.2",
|
||||||
|
"@esbuild/freebsd-arm64": "0.28.2",
|
||||||
|
"@esbuild/freebsd-x64": "0.28.2",
|
||||||
|
"@esbuild/linux-arm": "0.28.2",
|
||||||
|
"@esbuild/linux-arm64": "0.28.2",
|
||||||
|
"@esbuild/linux-ia32": "0.28.2",
|
||||||
|
"@esbuild/linux-loong64": "0.28.2",
|
||||||
|
"@esbuild/linux-mips64el": "0.28.2",
|
||||||
|
"@esbuild/linux-ppc64": "0.28.2",
|
||||||
|
"@esbuild/linux-riscv64": "0.28.2",
|
||||||
|
"@esbuild/linux-s390x": "0.28.2",
|
||||||
|
"@esbuild/linux-x64": "0.28.2",
|
||||||
|
"@esbuild/netbsd-arm64": "0.28.2",
|
||||||
|
"@esbuild/netbsd-x64": "0.28.2",
|
||||||
|
"@esbuild/openbsd-arm64": "0.28.2",
|
||||||
|
"@esbuild/openbsd-x64": "0.28.2",
|
||||||
|
"@esbuild/openharmony-arm64": "0.28.2",
|
||||||
|
"@esbuild/sunos-x64": "0.28.2",
|
||||||
|
"@esbuild/win32-arm64": "0.28.2",
|
||||||
|
"@esbuild/win32-ia32": "0.28.2",
|
||||||
|
"@esbuild/win32-x64": "0.28.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immutable": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-glob": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass": {
|
||||||
|
"version": "1.104.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.104.1.tgz",
|
||||||
|
"integrity": "sha512-yDA+1aIG3EHgN4V/BvuhCvu61FF6hEd4e+9DxikUm9U0CAGuvdIZ/UYy7qbOxjhbbWQraoyLlMuzdRGOSV5Bmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
|
"immutable": "^5.1.5",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sass": "sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
client/package.json
Normal file
15
client/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "youtubedownloader-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.mjs",
|
||||||
|
"watch": "node build.mjs --watch",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.28.2",
|
||||||
|
"sass": "^1.83.4",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
client/src/app.ts
Normal file
15
client/src/app.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { mountShell } from './shell/Shell';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point d'entrée du client : récupère l'élément racine et y monte le shell
|
||||||
|
* initial de l'application.
|
||||||
|
*/
|
||||||
|
function main(): void {
|
||||||
|
const root = document.getElementById('app');
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("Élément racine '#app' introuvable dans le document.");
|
||||||
|
}
|
||||||
|
mountShell(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
8
client/src/global.d.ts
vendored
Normal file
8
client/src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** Gabarit HTML importé comme chaîne brute (voir client/build.mjs). */
|
||||||
|
declare module '*.html' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Feuille de style Sass, compilée et regroupée par client/build.mjs. */
|
||||||
|
declare module '*.scss';
|
||||||
13
client/src/index.html
Normal file
13
client/src/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>youtubeDownloader</title>
|
||||||
|
<link rel="stylesheet" href="./app.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
client/src/shell/Shell.html
Normal file
4
client/src/shell/Shell.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<div class="shell">
|
||||||
|
<h1>youtubeDownloader</h1>
|
||||||
|
<p>L'interface arrive bientôt — pour l'instant, seul le serveur est en place.</p>
|
||||||
|
</div>
|
||||||
19
client/src/shell/Shell.scss
Normal file
19
client/src/shell/Shell.scss
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
.shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
client/src/shell/Shell.ts
Normal file
11
client/src/shell/Shell.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import shellTemplate from './Shell.html';
|
||||||
|
import './Shell.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monte la coquille (shell) de l'application dans l'élément racine fourni.
|
||||||
|
*
|
||||||
|
* @param root - Élément DOM dans lequel injecter le gabarit du shell.
|
||||||
|
*/
|
||||||
|
export function mountShell(root: HTMLElement): void {
|
||||||
|
root.innerHTML = shellTemplate;
|
||||||
|
}
|
||||||
16
client/tsconfig.json
Normal file
16
client/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
179
package-lock.json
generated
Normal file
179
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
{
|
||||||
|
"name": "youtubedownloader",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "youtubedownloader",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/biome": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"biome": "bin/biome"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/biome"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@biomejs/cli-darwin-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-darwin-x64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-arm64-musl": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-x64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-x64-musl": "1.9.4",
|
||||||
|
"@biomejs/cli-win32-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-win32-x64": "1.9.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
package.json
Normal file
13
package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "youtubedownloader",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Application web self-hosted pour extraire des vidéos/audios YouTube (yt-dlp + ffmpeg).",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "biome check .",
|
||||||
|
"lint:fix": "biome check --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
60
server/Dockerfile
Normal file
60
server/Dockerfile
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Contexte de build = racine du dépôt (le Dockerfile copie aussi client/) :
|
||||||
|
# docker build -f server/Dockerfile -t youtubedownloader .
|
||||||
|
#
|
||||||
|
# Trois étapes : build client (esbuild + sass -> client/dist), build
|
||||||
|
# serveur (tsc -> server/dist), puis l'image runtime, qui ne contient que
|
||||||
|
# les deux dossiers dist/ compilés, les dépendances de production du
|
||||||
|
# serveur, et yt-dlp + ffmpeg (moteur d'extraction, utilisé par le futur
|
||||||
|
# service de téléchargement).
|
||||||
|
|
||||||
|
FROM node:22-alpine AS client-build
|
||||||
|
WORKDIR /app/client
|
||||||
|
COPY client/package.json client/package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
COPY client/build.mjs client/tsconfig.json ./
|
||||||
|
COPY client/src ./src
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS server-build
|
||||||
|
WORKDIR /app/server
|
||||||
|
COPY server/package.json server/package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
COPY server/tsconfig.json server/tsconfig.build.json ./
|
||||||
|
COPY server/src ./src
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# yt-dlp (binaire autonome, sans dépendance Python) + ffmpeg : dépendances
|
||||||
|
# d'exécution du futur service de téléchargement.
|
||||||
|
RUN apk add --no-cache ffmpeg curl && \
|
||||||
|
curl -fL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -o /usr/local/bin/yt-dlp && \
|
||||||
|
chmod +x /usr/local/bin/yt-dlp && \
|
||||||
|
apk del curl
|
||||||
|
|
||||||
|
WORKDIR /app/server
|
||||||
|
|
||||||
|
COPY server/package.json server/package-lock.json* ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
|
COPY --from=server-build /app/server/dist ./dist
|
||||||
|
COPY --from=client-build /app/client/dist ../client/dist
|
||||||
|
|
||||||
|
# Dossier de travail temporaire du futur service de téléchargement,
|
||||||
|
# inscriptible par l'utilisateur non-root `node`.
|
||||||
|
RUN mkdir -p /app/server/tmp && chown -R node:node /app/server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PORT=3000 \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
CLIENT_DIST_PATH=/app/client/dist
|
||||||
|
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# GET /health ne demande aucune authentification.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||||
|
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
2919
server/package-lock.json
generated
Normal file
2919
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
server/package.json
Normal file
27
server/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "youtubedownloader-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12"
|
||||||
|
},
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/static": "^10.1.3",
|
||||||
|
"fastify": "^5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
server/src/config.ts
Normal file
31
server/src/config.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration runtime du serveur, dérivée des variables d'environnement.
|
||||||
|
*/
|
||||||
|
export interface ServerConfig {
|
||||||
|
/** Port d'écoute HTTP du serveur. */
|
||||||
|
readonly port: number;
|
||||||
|
/** Adresse d'écoute HTTP du serveur. */
|
||||||
|
readonly host: string;
|
||||||
|
/** Chemin absolu vers les fichiers statiques du client compilé. */
|
||||||
|
readonly clientDistPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit la configuration du serveur à partir des variables d'environnement,
|
||||||
|
* en appliquant des valeurs par défaut adaptées au développement local (le
|
||||||
|
* client compilé est alors supposé être un dossier `client/dist` voisin de
|
||||||
|
* `server/`).
|
||||||
|
*
|
||||||
|
* @returns La configuration runtime résolue.
|
||||||
|
*/
|
||||||
|
export function loadConfig(): ServerConfig {
|
||||||
|
const parsedPort = Number.parseInt(process.env.PORT ?? '3000', 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
port: Number.isNaN(parsedPort) ? 3000 : parsedPort,
|
||||||
|
host: process.env.HOST ?? '0.0.0.0',
|
||||||
|
clientDistPath: process.env.CLIENT_DIST_PATH ?? resolve(process.cwd(), '../client/dist'),
|
||||||
|
};
|
||||||
|
}
|
||||||
79
server/src/index.ts
Normal file
79
server/src/index.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import fastifyStatic from '@fastify/static';
|
||||||
|
import Fastify, { type FastifyInstance } from 'fastify';
|
||||||
|
import { type ServerConfig, loadConfig } from './config.js';
|
||||||
|
import { healthRoutes } from './routes/health.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit et configure l'instance Fastify de l'application : fichiers
|
||||||
|
* statiques du client, et routes applicatives.
|
||||||
|
*
|
||||||
|
* @param config - Configuration runtime résolue depuis l'environnement.
|
||||||
|
* @returns L'instance Fastify prête à être démarrée.
|
||||||
|
*/
|
||||||
|
async function buildServer(config: ServerConfig): Promise<FastifyInstance> {
|
||||||
|
const app = Fastify({ logger: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await app.register(fastifyStatic, {
|
||||||
|
root: config.clientDistPath,
|
||||||
|
index: ['index.html'],
|
||||||
|
});
|
||||||
|
await app.register(healthRoutes);
|
||||||
|
} catch (error) {
|
||||||
|
app.log.error({ err: error }, "Échec de l'enregistrement des plugins Fastify");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installe les gestionnaires d'arrêt propre du serveur (SIGINT/SIGTERM).
|
||||||
|
*
|
||||||
|
* @param app - Instance Fastify à fermer proprement à l'arrêt.
|
||||||
|
*/
|
||||||
|
function installShutdownHandlers(app: FastifyInstance): void {
|
||||||
|
const shutdown = async (signal: string): Promise<void> => {
|
||||||
|
app.log.info(`Signal ${signal} reçu, arrêt du serveur...`);
|
||||||
|
try {
|
||||||
|
await app.close();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
app.log.error({ err: error }, "Échec de l'arrêt propre du serveur");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
||||||
|
process.on(signal, () => {
|
||||||
|
shutdown(signal).catch((error: unknown) => {
|
||||||
|
console.error(`Échec du gestionnaire ${signal}`, error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point d'entrée du serveur : construit l'application, la démarre, et
|
||||||
|
* installe les gestionnaires d'arrêt propre.
|
||||||
|
*/
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const config = loadConfig();
|
||||||
|
const app = await buildServer(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await app.listen({ port: config.port, host: config.host });
|
||||||
|
app.log.info(`Serveur démarré sur http://${config.host}:${config.port}`);
|
||||||
|
} catch (error) {
|
||||||
|
app.log.error({ err: error }, 'Échec du démarrage du serveur');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
installShutdownHandlers(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error: unknown) => {
|
||||||
|
console.error('Erreur fatale au démarrage', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
23
server/src/routes/health.ts
Normal file
23
server/src/routes/health.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { FastifyInstance, FastifyPluginAsync } from 'fastify';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réponse renvoyée par la route de santé.
|
||||||
|
*/
|
||||||
|
export interface HealthResponse {
|
||||||
|
/** Statut du service ; toujours `"ok"` tant que le processus répond. */
|
||||||
|
readonly status: 'ok';
|
||||||
|
/** Durée depuis le démarrage du processus, en secondes. */
|
||||||
|
readonly uptime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Fastify exposant `GET /health`, utilisé par le HEALTHCHECK Docker
|
||||||
|
* et par toute supervision externe.
|
||||||
|
*
|
||||||
|
* @param app - Instance Fastify sur laquelle enregistrer la route.
|
||||||
|
*/
|
||||||
|
export const healthRoutes: FastifyPluginAsync = async (app: FastifyInstance): Promise<void> => {
|
||||||
|
app.get('/health', async (): Promise<HealthResponse> => {
|
||||||
|
return { status: 'ok', uptime: process.uptime() };
|
||||||
|
});
|
||||||
|
};
|
||||||
15
server/test/health.test.ts
Normal file
15
server/test/health.test.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { healthRoutes } from '../src/routes/health.js';
|
||||||
|
|
||||||
|
describe('GET /health', () => {
|
||||||
|
it('répond 200 avec un statut ok', async () => {
|
||||||
|
const app = Fastify();
|
||||||
|
await app.register(healthRoutes);
|
||||||
|
|
||||||
|
const response = await app.inject({ method: 'GET', url: '/health' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.json()).toMatchObject({ status: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
7
server/tsconfig.build.json
Normal file
7
server/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
17
server/tsconfig.json
Normal file
17
server/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||||
|
}
|
||||||
44
spec/architecture.md
Normal file
44
spec/architecture.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Remplace une série de scripts `.cmd` (`./downloader`, non versionné) par une application web : parcourir une chaîne YouTube, mettre des vidéos/audios en file d'attente, les télécharger en parallèle avec suivi en temps réel, et les livrer au navigateur de l'utilisateur. Pas d'intégration Emby, pas d'authentification (usage LAN).
|
||||||
|
|
||||||
|
## Structure du dépôt
|
||||||
|
|
||||||
|
```
|
||||||
|
server/ Backend Fastify (TypeScript, ESM)
|
||||||
|
client/ Frontend TypeScript vanilla (esbuild + sass)
|
||||||
|
.forgejo/workflows/ CI : lint + typecheck + tests + build, puis publication Docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Pas de `docker-compose.yml` pour l'instant (une seule image, pas encore de dépendances externes type base de données).
|
||||||
|
|
||||||
|
## Backend (`server/`)
|
||||||
|
|
||||||
|
- Fastify 5, TypeScript strict, ESM (`"type": "module"`, imports relatifs en `.js`, résolution `NodeNext`).
|
||||||
|
- `src/index.ts` : bootstrap (plugins, écoute, arrêt propre SIGINT/SIGTERM).
|
||||||
|
- `src/config.ts` : configuration runtime depuis l'environnement (`PORT`, `HOST`, `CLIENT_DIST_PATH`).
|
||||||
|
- `src/routes/` : routes Fastify enregistrées comme plugins (`health.ts` pour l'instant). Les futurs domaines métier (découverte de chaîne, file d'attente, téléchargement, notifications temps réel) suivront le même principe de service modulaire autonome, un fichier/dossier par domaine sous `src/`.
|
||||||
|
- Le serveur sert aussi les fichiers statiques de `client/dist` via `@fastify/static`.
|
||||||
|
- Tests : Vitest (`server/test/`), via `app.inject()` (pas de serveur HTTP réel démarré en test).
|
||||||
|
|
||||||
|
## Frontend (`client/`)
|
||||||
|
|
||||||
|
- TypeScript vanilla, pas de framework. Chaque vue = un dossier avec `Vue.ts` / `Vue.html` / `Vue.scss` (gabarit importé comme chaîne brute, style Sass importé et compilé). Voir `src/shell/` pour l'exemple actuel (placeholder).
|
||||||
|
- Build via `build.mjs` (esbuild), pas d'outil de build plus lourd (Vite, webpack) : le script compile `src/app.ts` en bundle ESM, copie `src/index.html`, et compile/regroupe les `.scss` importés en un `app.css` voisin (plugin esbuild maison qui délègue au paquet `sass`).
|
||||||
|
- Pas encore de router : une seule vue (shell) pour l'instant. Un routeur sera introduit avec la première vraie vue (parcours de chaîne).
|
||||||
|
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
Biome.js à la racine (`biome.json`), applique aux deux paquets (`server/src`, `client/src`) - pas d'ESLint/Prettier. `downloader/` (scripts legacy, non versionné) est explicitement exclu.
|
||||||
|
|
||||||
|
## Docker / CI-CD
|
||||||
|
|
||||||
|
- `server/Dockerfile` : image multi-étapes construite avec pour contexte la racine du dépôt (`docker build -f server/Dockerfile .`), car elle a besoin à la fois de `server/` et `client/`. Étapes : build client (esbuild) → build serveur (tsc) → image runtime (Node 22 Alpine + `ffmpeg` + binaire autonome `yt-dlp`, utilisateur non-root `node`).
|
||||||
|
- `yt-dlp` et `ffmpeg` sont installés dans l'image (pas vendorisés dans le dépôt, contrairement aux scripts `.cmd` d'origine qui embarquaient `yt-dlp.exe` et un build ffmpeg complet).
|
||||||
|
- `.forgejo/workflows/publish-docker.yml` : job `test` (lint racine, typecheck/tests/build serveur, typecheck/build client) puis job `publish` (build + push de l'image vers `git.dev.kyuno.fr/kyuno/youtubedownloader`, seulement si `test` passe et hors PR). Tags : `latest` + sha court sur push `main`, `X.Y.Z`/`X.Y`/`latest` sur tag `vX.Y.Z`. Nécessite le secret de dépôt `REGISTRY_TOKEN` (à créer manuellement, voir l'en-tête du workflow - un jeton d'API n'a pas le droit d'écrire dans le registre de paquets).
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
Squelette d'infrastructure seulement : serveur qui répond, client placeholder, Docker, CI. Aucune des fonctionnalités métier (découverte de chaîne, file d'attente, téléchargement yt-dlp, notifications temps réel) n'est encore implémentée - elles arriveront par PR successives.
|
||||||
Loading…
Reference in a new issue