Compare commits
46
Commits
dev
...
c0d6834178
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0d6834178
|
||
|
|
6f77c17003
|
||
|
|
c4f45f712a
|
||
|
|
c66c972a93
|
||
|
|
85ecdf3072
|
||
|
|
291aa54912
|
||
|
|
bc5d0fb498
|
||
|
|
e515944fb9
|
||
|
|
1562ecd706
|
||
|
|
416032942a
|
||
|
|
9fa37c516c
|
||
|
|
d87ab299c4
|
||
|
|
4c13f47156
|
||
|
|
36917d79b4
|
||
|
|
ddb6d3f2f0
|
||
|
|
8b17882d6b
|
||
|
|
f592d4ded7
|
||
|
|
796b4379fd
|
||
|
|
b99b6f2a25
|
||
|
|
d9868a1d5d
|
||
|
|
c62b54fba7
|
||
|
|
34a54eaae9
|
||
|
|
2003948a9a
|
||
|
|
cac88c39b2 | ||
|
|
fcd1e737ab
|
||
|
|
a10d854ee8
|
||
|
|
cf2f4de06c | ||
|
|
36da183404
|
||
|
|
58fd049d03
|
||
|
|
0a0772eea3
|
||
|
|
7fea170597
|
||
|
|
029ba3cc90
|
||
|
|
c8dd6e9c4a
|
||
|
|
afb38a067b
|
||
|
|
2540c04782
|
||
|
|
03e2449be3
|
||
|
|
ad034a9a6f
|
||
|
|
6db371d513
|
||
|
|
ea0f56c202
|
||
|
|
95156de4ca
|
||
|
|
20e701b754
|
||
|
|
615d3fafa0
|
||
|
|
a000af5c8b
|
||
|
|
b565224fcd
|
||
|
|
f7dcad115d
|
||
|
|
4e41518cf4 |
@@ -6,6 +6,9 @@ PORT=1337
|
|||||||
STRAPI_URL=
|
STRAPI_URL=
|
||||||
STRAPI_ADMIN_URL=/admin
|
STRAPI_ADMIN_URL=/admin
|
||||||
|
|
||||||
|
# Branding (affiché dans l'interface admin Strapi — rebuild requis)
|
||||||
|
STRAPI_ADMIN_SITE_NAME=OKI
|
||||||
|
|
||||||
APP_KEYS=
|
APP_KEYS=
|
||||||
API_TOKEN_SALT=
|
API_TOKEN_SALT=
|
||||||
ADMIN_JWT_SECRET=
|
ADMIN_JWT_SECRET=
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ jobs:
|
|||||||
- name: Installer les dépendances
|
- name: Installer les dépendances
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lancer les tests
|
||||||
|
run: yarn test
|
||||||
|
|
||||||
- name: Build Strapi
|
- name: Build Strapi
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ name: Déploiement API BETA
|
|||||||
run-name: ${{ gitea.actor }} déploie API BETA
|
run-name: ${{ gitea.actor }} déploie API BETA
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -39,7 +37,8 @@ jobs:
|
|||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
cd ${{ secrets.DEPLOY_PATH }}
|
cd ${{ secrets.DEPLOY_PATH }}
|
||||||
git pull --ff-only origin dev
|
git fetch origin
|
||||||
|
git checkout -B ${{ gitea.ref_name }} origin/${{ gitea.ref_name }}
|
||||||
corepack enable
|
corepack enable
|
||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
NODE_ENV=production yarn build
|
NODE_ENV=production yarn build
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ $RECYCLE.BIN/
|
|||||||
*.sql
|
*.sql
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
backups/
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -56,6 +56,26 @@ Si la variable JWT_SECRET n'est pas renseignée, elle est générée automatique
|
|||||||
yarn && yarn build && yarn dev
|
yarn && yarn build && yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Tokens API
|
||||||
|
|
||||||
|
Les endpoints marqués ⚙️ **Token requis** nécessitent un token API Strapi.
|
||||||
|
|
||||||
|
**Créer le token** : Administration Strapi → *Settings → API Tokens → Create new API Token*
|
||||||
|
|
||||||
|
| Endpoint | Type de token | Permissions requises |
|
||||||
|
|----------|---------------|----------------------|
|
||||||
|
| `GET /paroles/export` | Custom | Parole → `export` |
|
||||||
|
| `POST /paroles/bulk-translate` | Custom | Parole → `bulkTranslate` |
|
||||||
|
|
||||||
|
Pour un token couvrant les deux endpoints, créer un token de type **Custom** et cocher dans la section *Parole* : `export` et `bulkTranslate`.
|
||||||
|
|
||||||
|
Le token est à passer dans le header HTTP :
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Point d'accès
|
## Point d'accès
|
||||||
|
|
||||||
### `/awtis`
|
### `/awtis`
|
||||||
@@ -77,6 +97,72 @@ ___
|
|||||||
### `/paroles/count`
|
### `/paroles/count`
|
||||||
- `GET` : Récupère le nombre de texte
|
- `GET` : Récupère le nombre de texte
|
||||||
|
|
||||||
|
### `/paroles/bulk-translate` ⚙️ Token requis
|
||||||
|
- `POST` : Traduit automatiquement via DeepL toutes les paroles ayant une source française (`traductions.francais` ou `langueSource: fr`) vers les langues manquantes (EN, ES, DE, IT). Ne modifie pas les traductions déjà existantes.
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{"translated": 42, "skipped": 18, "errors": []}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Champ | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `translated` | Nombre de traductions ajoutées |
|
||||||
|
| `skipped` | Paroles ignorées (pas de source FR ou déjà complètes) |
|
||||||
|
| `errors` | Erreurs DeepL avec `documentId`, `titre` et `lang` |
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: Bearer <token>" \
|
||||||
|
"https://api.pawol.nu/api/paroles/bulk-translate"
|
||||||
|
```
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### `/paroles/export` ⚙️ Token requis
|
||||||
|
- `GET` : Exporter les paroles et traductions au format JSONL ou JSON pour l'entraînement de modèles LLM
|
||||||
|
|
||||||
|
**Paramètres de requête :**
|
||||||
|
|
||||||
|
| Paramètre | Valeurs acceptées | Défaut | Description |
|
||||||
|
|-----------|-------------------|--------|-------------|
|
||||||
|
| `type` | `pairs` \| `instruct` | `pairs` | Format des exemples d'entraînement |
|
||||||
|
| `lang` | `fr,en,es,de,it` | toutes | Langues cibles à inclure (séparées par des virgules) |
|
||||||
|
| `format` | `jsonl` \| `json` | `jsonl` | Format de la réponse |
|
||||||
|
|
||||||
|
**Type `pairs`** — corpus parallèle source/cible, adapté aux modèles de traduction :
|
||||||
|
```json
|
||||||
|
{"source_lang":"ka","target_lang":"fr","source":"Mwen ka palé épi ou…","target":"Je suis en train de te parler…","title":"Titre","artists":["Artiste"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type `instruct`** — format instruction/chat, adapté au fine-tuning de modèles d'instruction :
|
||||||
|
```json
|
||||||
|
{"messages":[{"role":"system","content":"Tu es un expert en langue KA…"},{"role":"user","content":"Tradui an fransé :\n\nMwen ka palé épi ou…"},{"role":"assistant","content":"Je suis en train de te parler…"}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Format `jsonl`** : la première ligne contient les métadonnées (champ `_metadata: true`). Pour les filtrer :
|
||||||
|
```bash
|
||||||
|
jq 'select(._metadata | not)' export.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Métadonnées incluses** (`_metadata: true` en JSONL, clé `metadata` en JSON) :
|
||||||
|
|
||||||
|
| Champ | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `exported_at` | Horodatage de l'export |
|
||||||
|
| `total_paroles` | Nombre de paroles traitées |
|
||||||
|
| `total_pairs` | Nombre d'exemples d'entraînement générés |
|
||||||
|
| `languages` | Nombre de paires par langue |
|
||||||
|
| `missing_translations` | Paroles avec des traductions manquantes, par langue |
|
||||||
|
| `non_ka_transcriptions` | Paroles dont la transcription est suspectée d'être dans une autre langue (ex. français) |
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <token>" \
|
||||||
|
"https://api.pawol.nu/api/paroles/export?type=instruct&lang=fr,en&format=jsonl" \
|
||||||
|
-o dataset.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2024 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
Copyright (C) 2024 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
rest: {
|
rest: {
|
||||||
defaultLimit: 25,
|
defaultLimit: 25,
|
||||||
maxLimit: 100,
|
maxLimit: 200,
|
||||||
withCount: true,
|
withCount: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,19 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const {backupDatabase} = require('../src/utils/backup-database');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
myJob: {
|
myJob: {
|
||||||
task: ({ strapi }) => {console.log('TODO > save db')},
|
task: ({ strapi }) => {
|
||||||
|
const dbPath = path.join(__dirname, '..', process.env.DATABASE_FILENAME || '.tmp/data.db');
|
||||||
|
const backupsDir = path.join(__dirname, '..', 'backups');
|
||||||
|
const backupPath = backupDatabase({dbPath, backupsDir});
|
||||||
|
|
||||||
|
if (backupPath) {
|
||||||
|
strapi.log.info(`Sauvegarde de la base effectuée : ${backupPath}`);
|
||||||
|
} else {
|
||||||
|
strapi.log.warn(`Sauvegarde de la base ignorée : fichier introuvable (${dbPath})`);
|
||||||
|
}
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
rule: '0 0 * * SUN',
|
rule: '0 0 * * SUN',
|
||||||
tz: 'Indian/Reunion',
|
tz: 'Indian/Reunion',
|
||||||
|
|||||||
@@ -16,23 +16,4 @@ module.exports = ({env}) => ({
|
|||||||
defaultReplyTo: env('SMTP_REPLY_TO'),
|
defaultReplyTo: env('SMTP_REPLY_TO'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'rest-cache': {
|
|
||||||
config: {
|
|
||||||
provider: {
|
|
||||||
name: 'memory',
|
|
||||||
options: {
|
|
||||||
max: 32767,
|
|
||||||
maxAge: 3600
|
|
||||||
}
|
|
||||||
},
|
|
||||||
strategy: {
|
|
||||||
contentTypes: [
|
|
||||||
'api::artiste.artiste',
|
|
||||||
'api::parole.parole'
|
|
||||||
],
|
|
||||||
debug: true,
|
|
||||||
hitpass: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
+10
-6
@@ -5,25 +5,29 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Cédric Famibelle-Pronzola",
|
"name": "ORGANSATION KA INTERNATIONALE",
|
||||||
"email": "contact@cedric-pronzola.dev",
|
"email": "kontak@o-k-i.net",
|
||||||
"url": "https://cedric-pronzola.dev"
|
"url": "https://o-k-i.net"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://codeberg.org/OKI/api.pawol.nu.git"
|
"url": "git+https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/api.pawol.nu"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "strapi develop",
|
"dev": "strapi develop",
|
||||||
"start": "strapi start",
|
"start": "strapi start",
|
||||||
"build": "strapi build",
|
"build": "strapi build",
|
||||||
"strapi": "strapi"
|
"strapi": "strapi",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strapi/plugin-users-permissions": "5.44.0",
|
"@strapi/plugin-users-permissions": "5.44.0",
|
||||||
"@strapi/provider-email-nodemailer": "^4.26.1",
|
"@strapi/provider-email-nodemailer": "^4.26.1",
|
||||||
"@strapi/strapi": "5.44.0",
|
"@strapi/strapi": "5.44.0",
|
||||||
|
"axios": "1.15.1",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
|
|||||||
+4
-4
@@ -13,14 +13,14 @@ export default {
|
|||||||
locales: ['fr'],
|
locales: ['fr'],
|
||||||
translations: {
|
translations: {
|
||||||
fr: {
|
fr: {
|
||||||
'Auth.form.welcome.subtitle': 'Connectez-vous à votre compte OKI API',
|
'Auth.form.welcome.subtitle': `Connectez-vous à votre compte ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API`,
|
||||||
'Auth.form.welcome.title': 'Bienvenue sur OKI API !',
|
'Auth.form.welcome.title': `Bienvenue sur ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
|
||||||
'LeftMenu.navbrand.title': 'Tableau de bord',
|
'LeftMenu.navbrand.title': 'Tableau de bord',
|
||||||
'LeftMenu.navbrand.workplace': 'Menu',
|
'LeftMenu.navbrand.workplace': 'Menu',
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
'Auth.form.welcome.subtitle': 'Log in to your OKI API account',
|
'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`,
|
||||||
'Auth.form.welcome.title': 'Welcome to OKI API !',
|
'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
|
||||||
'LeftMenu.navbrand.title': 'Dashboard',
|
'LeftMenu.navbrand.title': 'Dashboard',
|
||||||
'LeftMenu.navbrand.workplace': 'Workplace',
|
'LeftMenu.navbrand.workplace': 'Workplace',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
|
||||||
|
async function loadLifecycles(strapiMock) {
|
||||||
|
vi.resetModules()
|
||||||
|
global.strapi = strapiMock
|
||||||
|
const mod = await import('../lifecycles.js')
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('artiste afterUpdate — cascade de renommage des slugs', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.strapi
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renomme le slug des paroles de l'artiste quand son alias change", async () => {
|
||||||
|
const artisteFindOne = vi.fn(async () => ({
|
||||||
|
id: 5,
|
||||||
|
paroles: [{id: 10}, {id: 11}]
|
||||||
|
}))
|
||||||
|
const paroleFindMany = vi.fn(async () => [
|
||||||
|
{id: 10, titre: 'Titre 1', slug: 'ancien-alias-titre-1', artistes: [{alias: 'nouvel-alias'}]},
|
||||||
|
{id: 11, titre: 'Titre 2', slug: 'nouvel-alias-titre-2', artistes: [{alias: 'nouvel-alias'}]}
|
||||||
|
])
|
||||||
|
const paroleUpdate = vi.fn(async () => {})
|
||||||
|
|
||||||
|
const strapiMock = {
|
||||||
|
db: {
|
||||||
|
query: vi.fn(uid => {
|
||||||
|
if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne}
|
||||||
|
if (uid === 'api::parole.parole') return {findMany: paroleFindMany, update: paroleUpdate}
|
||||||
|
throw new Error(`unexpected uid: ${uid}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {afterUpdate} = await loadLifecycles(strapiMock)
|
||||||
|
|
||||||
|
await afterUpdate({result: {id: 5}})
|
||||||
|
|
||||||
|
expect(artisteFindOne).toHaveBeenCalledWith({where: {id: 5}, populate: ['paroles']})
|
||||||
|
expect(paroleFindMany).toHaveBeenCalledWith({where: {id: {$in: [10, 11]}}, populate: ['artistes']})
|
||||||
|
expect(paroleUpdate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(paroleUpdate).toHaveBeenCalledWith({where: {id: 10}, data: {slug: 'nouvel-alias-titre-1'}})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ne fait rien quand l'artiste n'a aucune parole", async () => {
|
||||||
|
const artisteFindOne = vi.fn(async () => ({id: 5, paroles: []}))
|
||||||
|
const paroleFindMany = vi.fn()
|
||||||
|
|
||||||
|
const strapiMock = {
|
||||||
|
db: {
|
||||||
|
query: vi.fn(uid => {
|
||||||
|
if (uid === 'api::artiste.artiste') return {findOne: artisteFindOne}
|
||||||
|
if (uid === 'api::parole.parole') return {findMany: paroleFindMany}
|
||||||
|
throw new Error(`unexpected uid: ${uid}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {afterUpdate} = await loadLifecycles(strapiMock)
|
||||||
|
|
||||||
|
await afterUpdate({result: {id: 5}})
|
||||||
|
|
||||||
|
expect(paroleFindMany).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,13 +3,19 @@
|
|||||||
const { ApplicationError } = require("@strapi/utils").errors
|
const { ApplicationError } = require("@strapi/utils").errors
|
||||||
const slugify = require('slugify')
|
const slugify = require('slugify')
|
||||||
|
|
||||||
const jwennTeksEpiId = async data => {
|
const jwennTeksEpiId = async ids => {
|
||||||
const paroles = await strapi.db.query('api::parole.parole').find({id_in: data})
|
const paroles = await strapi.db.query('api::parole.parole').findMany({
|
||||||
|
where: {id: {$in: ids}},
|
||||||
|
populate: ['artistes']
|
||||||
|
})
|
||||||
return paroles
|
return paroles
|
||||||
}
|
}
|
||||||
|
|
||||||
const jwennAwtisEpiId = async id => {
|
const jwennAwtisEpiId = async id => {
|
||||||
const artiste = await strapi.db.query('api::artiste.artiste').findOne({id})
|
const artiste = await strapi.db.query('api::artiste.artiste').findOne({
|
||||||
|
where: {id},
|
||||||
|
populate: ['paroles']
|
||||||
|
})
|
||||||
return artiste
|
return artiste
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,15 +45,15 @@ module.exports = {
|
|||||||
data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
data.slug = slugify(data.alias, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||||
},
|
},
|
||||||
afterUpdate: async event => {
|
afterUpdate: async event => {
|
||||||
let {data} = event.params
|
const {result} = event
|
||||||
const {id} = data
|
const artiste = await jwennAwtisEpiId(result.id)
|
||||||
const artiste = await jwennAwtisEpiId(id)
|
|
||||||
|
|
||||||
if (artiste.paroles && artiste.paroles.length >= 1) {
|
if (artiste.paroles && artiste.paroles.length >= 1) {
|
||||||
const paroles = await jwennTeksEpiId(artiste.paroles)
|
const paroleIds = artiste.paroles.map(({id}) => id)
|
||||||
Promise.all(paroles.map(async t => {
|
const paroles = await jwennTeksEpiId(paroleIds)
|
||||||
const {id, titre, slug, artiste} = t
|
await Promise.all(paroles.map(async t => {
|
||||||
const alias = artiste.map(a => a.alias).join('-')
|
const {id, titre, slug, artistes} = t
|
||||||
|
const alias = artistes.map(a => a.alias).join('-')
|
||||||
const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
const slugUpdated = slugify(`${alias}-${titre}`, {lower: true, remove: /[*#+~.()'"!:@]/g})
|
||||||
|
|
||||||
if (slug !== slugUpdated) {
|
if (slug !== slugUpdated) {
|
||||||
|
|||||||
@@ -57,6 +57,20 @@
|
|||||||
},
|
},
|
||||||
"musicBrainzUrl": {
|
"musicBrainzUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isExclusiveArtist": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"titrePhare": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToOne",
|
||||||
|
"target": "api::parole.parole"
|
||||||
|
},
|
||||||
|
"rezoSosyal": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": true,
|
||||||
|
"component": "social.rezo-sosyal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {describe, it, expect, vi} from 'vitest'
|
||||||
|
|
||||||
|
const {default: createController} = await import('../artiste.js')
|
||||||
|
|
||||||
|
function buildStrapi({dbUser, existingArtiste = null}) {
|
||||||
|
const dbQuery = {
|
||||||
|
findOne: vi.fn(async () => existingArtiste)
|
||||||
|
}
|
||||||
|
const artisteDocuments = {
|
||||||
|
create: vi.fn(async ({data}) => ({id: 42, ...data}))
|
||||||
|
}
|
||||||
|
const userDocuments = {
|
||||||
|
findOne: vi.fn(async () => dbUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
const strapi = {
|
||||||
|
contentType: vi.fn(() => ({uid: 'api::artiste.artiste', kind: 'collectionType'})),
|
||||||
|
db: {
|
||||||
|
query: vi.fn(() => dbQuery)
|
||||||
|
},
|
||||||
|
documents: vi.fn(uid => {
|
||||||
|
if (uid === 'plugin::users-permissions.user') return userDocuments
|
||||||
|
if (uid === 'api::artiste.artiste') return artisteDocuments
|
||||||
|
throw new Error(`unexpected uid: ${uid}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {strapi, artisteDocuments}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCtx(data) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
body: {data},
|
||||||
|
header: {authorization: 'Bearer faketoken'}
|
||||||
|
},
|
||||||
|
badRequest: vi.fn(),
|
||||||
|
notFound: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'}
|
||||||
|
|
||||||
|
function buildData(overrides = {}) {
|
||||||
|
return {
|
||||||
|
alias: 'Test Artist',
|
||||||
|
user: {...dbUser},
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('artiste.create', () => {
|
||||||
|
it('crée l\'artiste quand le user existe et que l\'alias est nouveau', async () => {
|
||||||
|
const {strapi, artisteDocuments} = buildStrapi({dbUser})
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData())
|
||||||
|
|
||||||
|
await controller.create(ctx)
|
||||||
|
|
||||||
|
expect(artisteDocuments.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse sans planter quand data.user est absent', async () => {
|
||||||
|
const {strapi} = buildStrapi({dbUser})
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData({user: undefined}))
|
||||||
|
|
||||||
|
await controller.create(ctx)
|
||||||
|
|
||||||
|
expect(ctx.badRequest).toHaveBeenCalled()
|
||||||
|
expect(strapi.documents).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,18 +12,8 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({
|
|||||||
const {body} = ctx.request
|
const {body} = ctx.request
|
||||||
let {data} = body
|
let {data} = body
|
||||||
|
|
||||||
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
|
if (!data?.user?.documentId) {
|
||||||
try {
|
return ctx.badRequest('Informations manquantes.')
|
||||||
const {id} = await strapi.plugins[
|
|
||||||
'users-permissions'
|
|
||||||
].services.jwt.getToken(ctx)
|
|
||||||
|
|
||||||
if (id !== data.user.id) {
|
|
||||||
throw new UnauthorizedError('Opération non autorisée')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new UnauthorizedError(ctx, err, 'Opération non autorisée')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
||||||
@@ -31,11 +21,11 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError('Utilisateur introuvable.')
|
return ctx.notFound('Utilisateur introuvable.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
||||||
ctx.badRequest('Informations non valides.')
|
return ctx.badRequest('Informations non valides.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const artiste = await strapi.db.query('api::artiste.artiste').findOne({
|
const artiste = await strapi.db.query('api::artiste.artiste').findOne({
|
||||||
|
|||||||
@@ -2,4 +2,16 @@
|
|||||||
|
|
||||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||||
|
|
||||||
module.exports = createCoreRouter('api::artiste.artiste')
|
module.exports = createCoreRouter('api::artiste.artiste', {
|
||||||
|
config: {
|
||||||
|
create: {
|
||||||
|
policies: ['global::is-payload-owner']
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}]
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {describe, it, expect, vi} from 'vitest'
|
||||||
|
|
||||||
|
const {default: createController} = await import('../commentaire.js')
|
||||||
|
|
||||||
|
const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'}
|
||||||
|
const dbParole = {id: 7, documentId: 'parole-doc-7'}
|
||||||
|
|
||||||
|
function buildStrapi({existingParole = dbParole} = {}) {
|
||||||
|
const commentaireDocuments = {
|
||||||
|
create: vi.fn(async ({data}) => ({id: 99, ...data}))
|
||||||
|
}
|
||||||
|
const paroleDocuments = {
|
||||||
|
update: vi.fn(async () => {})
|
||||||
|
}
|
||||||
|
const userDbQuery = {
|
||||||
|
findOne: vi.fn(async ({where}) => (where.id === dbUser.id ? dbUser : null))
|
||||||
|
}
|
||||||
|
const paroleDbQuery = {
|
||||||
|
findOne: vi.fn(async ({where}) => (where.id === existingParole?.id ? existingParole : null))
|
||||||
|
}
|
||||||
|
|
||||||
|
const strapi = {
|
||||||
|
contentType: vi.fn(() => ({uid: 'api::commentaire.commentaire', kind: 'collectionType'})),
|
||||||
|
db: {
|
||||||
|
query: vi.fn(uid => {
|
||||||
|
if (uid === 'plugin::users-permissions.user') return userDbQuery
|
||||||
|
if (uid === 'api::parole.parole') return paroleDbQuery
|
||||||
|
throw new Error(`unexpected uid: ${uid}`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
documents: vi.fn(uid => {
|
||||||
|
if (uid === 'api::commentaire.commentaire') return commentaireDocuments
|
||||||
|
if (uid === 'api::parole.parole') return paroleDocuments
|
||||||
|
throw new Error(`unexpected uid: ${uid}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {strapi, commentaireDocuments, paroleDocuments, userDbQuery, paroleDbQuery}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCtx(data) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
body: {data},
|
||||||
|
header: {authorization: 'Bearer faketoken'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildData(overrides = {}) {
|
||||||
|
return {
|
||||||
|
contenu: 'Un commentaire',
|
||||||
|
datePublication: '2026-07-04',
|
||||||
|
parole: dbParole.id,
|
||||||
|
user: {...dbUser},
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('commentaire.create', () => {
|
||||||
|
it('retrouve la parole par son id (pas par le documentId du user) et l\'associe correctement', async () => {
|
||||||
|
const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi()
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData())
|
||||||
|
|
||||||
|
await controller.create(ctx)
|
||||||
|
|
||||||
|
expect(paroleDbQuery.findOne).toHaveBeenCalledWith({where: {id: dbParole.id}})
|
||||||
|
expect(commentaireDocuments.create).toHaveBeenCalled()
|
||||||
|
expect(paroleDocuments.update).toHaveBeenCalledWith({
|
||||||
|
documentId: dbParole.documentId,
|
||||||
|
data: {commentaires: [99]}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejette quand la parole ciblée n\'existe pas', async () => {
|
||||||
|
const {strapi, commentaireDocuments} = buildStrapi({existingParole: null})
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData())
|
||||||
|
|
||||||
|
await expect(controller.create(ctx)).rejects.toThrow('Texte introuvable.')
|
||||||
|
expect(commentaireDocuments.create).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse sans planter quand data.user est absent', async () => {
|
||||||
|
const {strapi, userDbQuery} = buildStrapi()
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData({user: undefined}))
|
||||||
|
|
||||||
|
await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.')
|
||||||
|
expect(userDbQuery.findOne).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse sans planter quand data.parole est absent', async () => {
|
||||||
|
const {strapi, paroleDbQuery} = buildStrapi()
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData({parole: undefined}))
|
||||||
|
|
||||||
|
await expect(controller.create(ctx)).rejects.toThrow('Informations manquantes.')
|
||||||
|
expect(paroleDbQuery.findOne).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,28 +1,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { createCoreController } = require('@strapi/strapi').factories;
|
const { createCoreController } = require('@strapi/strapi').factories;
|
||||||
const { ApplicationError, NotFoundError, UnauthorizedError } = require("@strapi/utils").errors
|
const { ApplicationError, NotFoundError } = require("@strapi/utils").errors
|
||||||
|
|
||||||
module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) => ({
|
module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) => ({
|
||||||
async create(ctx) {
|
async create(ctx) {
|
||||||
const {body} = ctx.request
|
const {body} = ctx.request
|
||||||
let {data} = body
|
let {data} = body
|
||||||
|
|
||||||
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
|
if (!data?.user?.id || !data?.parole) {
|
||||||
try {
|
throw new ApplicationError('Informations manquantes.')
|
||||||
const {id} = await strapi.plugins[
|
}
|
||||||
'users-permissions'
|
|
||||||
].services.jwt.getToken(ctx)
|
|
||||||
|
|
||||||
if (id !== data.user.id) {
|
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
||||||
throw new UnauthorizedError('Opération non autorisée')
|
where: {id: data.user.id}
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new UnauthorizedError(ctx, err, 'Opération non autorisée')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
|
||||||
documentId: data.user.documentId
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -35,9 +26,8 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi})
|
|||||||
|
|
||||||
data.user = user.id
|
data.user = user.id
|
||||||
|
|
||||||
const parole = await strapi.documents('api::parole.parole').findOne({
|
const parole = await strapi.db.query('api::parole.parole').findOne({
|
||||||
documentId: user.documentId,
|
where: {id: data.parole}
|
||||||
fields: ['id']
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!parole) {
|
if (!parole) {
|
||||||
@@ -51,7 +41,7 @@ module.exports = createCoreController('api::commentaire.commentaire', ({strapi})
|
|||||||
})
|
})
|
||||||
|
|
||||||
await strapi.documents('api::parole.parole').update({
|
await strapi.documents('api::parole.parole').update({
|
||||||
documentId: user.documentId,
|
documentId: parole.documentId,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
commentaires: [newCommentaire.id]
|
commentaires: [newCommentaire.id]
|
||||||
|
|||||||
@@ -2,4 +2,16 @@
|
|||||||
|
|
||||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||||
|
|
||||||
module.exports = createCoreRouter('api::commentaire.commentaire')
|
module.exports = createCoreRouter('api::commentaire.commentaire', {
|
||||||
|
config: {
|
||||||
|
create: {
|
||||||
|
policies: ['global::is-payload-owner']
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}]
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
|
||||||
|
async function loadLifecycles(strapiMock) {
|
||||||
|
vi.resetModules()
|
||||||
|
global.strapi = strapiMock
|
||||||
|
const mod = await import('../lifecycles.js')
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('afterCreate — notification au soumetteur', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.strapi
|
||||||
|
})
|
||||||
|
|
||||||
|
it('recherche le user par id (pas par un champ "user" inexistant) et envoie le mail', async () => {
|
||||||
|
const userFindOne = vi.fn(async ({where}) => {
|
||||||
|
if (where.id === 5) return {id: 5, username: 'foo', email: 'foo@bar.com'}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
const emailSend = vi.fn()
|
||||||
|
|
||||||
|
const strapiMock = {
|
||||||
|
db: {
|
||||||
|
query: vi.fn(uid => {
|
||||||
|
if (uid === 'plugin::users-permissions.user') return {findOne: userFindOne}
|
||||||
|
return {findOne: vi.fn(async () => null)}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
email: {services: {email: {send: emailSend}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {afterCreate} = await loadLifecycles(strapiMock)
|
||||||
|
|
||||||
|
await afterCreate({params: {data: {titre: 'Titre', user: {id: 5}}}})
|
||||||
|
|
||||||
|
expect(userFindOne).toHaveBeenCalledWith({where: {id: 5}})
|
||||||
|
expect(emailSend).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('beforeUpdate — notifications Telegram/Revolt', () => {
|
||||||
|
const originalEnv = {...process.env}
|
||||||
|
const axios = require('axios')
|
||||||
|
const originalPost = axios.post
|
||||||
|
|
||||||
|
function buildStrapi(previous) {
|
||||||
|
const dbQuery = {
|
||||||
|
findOne: vi.fn(async () => previous),
|
||||||
|
updateMany: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
db: {query: vi.fn(() => dbQuery)},
|
||||||
|
plugins: {email: {services: {email: {send: vi.fn()}}}},
|
||||||
|
log: {error: vi.fn()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEvent(overrides = {}) {
|
||||||
|
return {
|
||||||
|
state: {},
|
||||||
|
params: {
|
||||||
|
data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = {...originalEnv}
|
||||||
|
axios.post = originalPost
|
||||||
|
delete global.strapi
|
||||||
|
})
|
||||||
|
|
||||||
|
it("n'interrompt pas la publication quand Telegram échoue, et encode le message", async () => {
|
||||||
|
process.env.TELEGRAM_API_TOKEN = 'fake-token'
|
||||||
|
process.env.TELEGRAM_CHAN_ID = 'fake-chan'
|
||||||
|
delete process.env.REVOLT_TOKEN
|
||||||
|
|
||||||
|
axios.post = vi.fn(async () => {
|
||||||
|
throw new Error('boom')
|
||||||
|
})
|
||||||
|
|
||||||
|
const previous = {publishedAt: null, slug: 'foo&bar', titre: 'Mon titre', user: null, userAdmin: null, artistes: []}
|
||||||
|
const strapiMock = buildStrapi(previous)
|
||||||
|
|
||||||
|
const {beforeUpdate} = await loadLifecycles(strapiMock)
|
||||||
|
|
||||||
|
await beforeUpdate(buildEvent())
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledTimes(1)
|
||||||
|
const [calledUrl] = axios.post.mock.calls[0]
|
||||||
|
expect(calledUrl).toContain('text=')
|
||||||
|
expect(calledUrl.split('text=')[1]).not.toContain('&bar')
|
||||||
|
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Telegram'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("n'interrompt pas la publication quand Revolt échoue", async () => {
|
||||||
|
delete process.env.TELEGRAM_API_TOKEN
|
||||||
|
process.env.REVOLT_TOKEN = 'fake-token'
|
||||||
|
process.env.REVOLT_TARGET = 'fake-target'
|
||||||
|
process.env.REVOLT_BOT_ID = 'fake-bot'
|
||||||
|
|
||||||
|
axios.post = vi.fn(async () => {
|
||||||
|
throw new Error('boom')
|
||||||
|
})
|
||||||
|
|
||||||
|
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: []}
|
||||||
|
const strapiMock = buildStrapi(previous)
|
||||||
|
|
||||||
|
const {beforeUpdate} = await loadLifecycles(strapiMock)
|
||||||
|
|
||||||
|
await beforeUpdate(buildEvent())
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledTimes(1)
|
||||||
|
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('Revolt'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('afterUpdate — historique de différence', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.strapi
|
||||||
|
})
|
||||||
|
|
||||||
|
it("n'échoue pas quand updatedBy n'est pas peuplé", async () => {
|
||||||
|
const entityServiceUpdate = vi.fn(async () => {})
|
||||||
|
const strapiMock = {
|
||||||
|
entityService: {update: entityServiceUpdate}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {afterUpdate} = await loadLifecycles(strapiMock)
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
result: {id: 1, difference: [], updatedBy: undefined},
|
||||||
|
state: {diff: {path: 'transcription', jsonDiff: []}}
|
||||||
|
}
|
||||||
|
|
||||||
|
await afterUpdate(event)
|
||||||
|
|
||||||
|
expect(entityServiceUpdate).toHaveBeenCalledWith('api::parole.parole', 1, {
|
||||||
|
data: {
|
||||||
|
difference: [{
|
||||||
|
admin_user: null,
|
||||||
|
paroles: 'transcription',
|
||||||
|
jsonDiff: [],
|
||||||
|
date: expect.any(Date),
|
||||||
|
sources: 'transcription'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -53,7 +53,7 @@ const jwennUserEpiId = async userId => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
||||||
where: {user: userId}
|
where: {id: userId}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -144,6 +144,13 @@ module.exports = {
|
|||||||
let {data} = event.params
|
let {data} = event.params
|
||||||
const {documentId} = data
|
const {documentId} = data
|
||||||
|
|
||||||
|
if (data.isNewRelease === true) {
|
||||||
|
await strapi.db.query('api::parole.parole').updateMany({
|
||||||
|
where: { isNewRelease: true },
|
||||||
|
data: { isNewRelease: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const previousParoles = await strapi.db.query('api::parole.parole').findOne({
|
const previousParoles = await strapi.db.query('api::parole.parole').findOne({
|
||||||
where: {documentId},
|
where: {documentId},
|
||||||
populate: {difference: true, artistes: true}
|
populate: {difference: true, artistes: true}
|
||||||
@@ -186,7 +193,7 @@ module.exports = {
|
|||||||
strapi.plugins['email'].services.email.send({
|
strapi.plugins['email'].services.email.send({
|
||||||
from: process.env.SMTP_FROM,
|
from: process.env.SMTP_FROM,
|
||||||
to: previousData.user.email,
|
to: previousData.user.email,
|
||||||
subject: `Publication de "${previousData.titre}" sur pawol.nu`,
|
subject: `Publication de "${previousData.titre}" sur ${(process.env.WEBSITE_URL || 'https://pawol.nu').replace(/^https?:\/\//, '')}`,
|
||||||
text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.
|
text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.
|
||||||
Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}
|
Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}
|
||||||
Merci pour votre contribution ❤️`,
|
Merci pour votre contribution ❤️`,
|
||||||
@@ -199,7 +206,7 @@ module.exports = {
|
|||||||
strapi.plugins['email'].services.email.send({
|
strapi.plugins['email'].services.email.send({
|
||||||
from: process.env.SMTP_FROM,
|
from: process.env.SMTP_FROM,
|
||||||
to: previousData.userAdmin.email,
|
to: previousData.userAdmin.email,
|
||||||
subject: `Publication de "${previousData.titre}" sur pawol.nu`,
|
subject: `Publication de "${previousData.titre}" sur ${(process.env.WEBSITE_URL || 'https://pawol.nu').replace(/^https?:\/\//, '')}`,
|
||||||
text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.
|
text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.
|
||||||
Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}.
|
Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}.
|
||||||
Merci pour votre contribution ❤️`,
|
Merci pour votre contribution ❤️`,
|
||||||
@@ -209,7 +216,11 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TELEGRAM_API_TOKEN) {
|
if (TELEGRAM_API_TOKEN) {
|
||||||
await axios.post(`${MESSAGE_URL}&text=${message}`)
|
try {
|
||||||
|
await axios.post(`${MESSAGE_URL}&text=${encodeURIComponent(message)}`)
|
||||||
|
} catch (err) {
|
||||||
|
strapi.log.error(`Notification Telegram : ${err.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (REVOLT_TOKEN && REVOLT_TARGET && REVOLT_BOT_ID) {
|
if (REVOLT_TOKEN && REVOLT_TARGET && REVOLT_BOT_ID) {
|
||||||
@@ -227,7 +238,11 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await axios.post(url, {content: revoltMessage}, config)
|
await axios.post(url, {content: revoltMessage}, config)
|
||||||
|
} catch (err) {
|
||||||
|
strapi.log.error(`Notification Revolt : ${err.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +256,7 @@ module.exports = {
|
|||||||
difference: [
|
difference: [
|
||||||
...result.difference,
|
...result.difference,
|
||||||
{
|
{
|
||||||
admin_user: result.updatedBy.id,
|
admin_user: result.updatedBy?.id ?? null,
|
||||||
paroles: state.diff.path,
|
paroles: state.diff.path,
|
||||||
jsonDiff: state.diff.jsonDiff,
|
jsonDiff: state.diff.jsonDiff,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"draftAndPublish": true
|
"draftAndPublish": true,
|
||||||
|
"populateCreatorFields": true
|
||||||
},
|
},
|
||||||
"pluginOptions": {},
|
"pluginOptions": {},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
@@ -65,18 +66,18 @@
|
|||||||
},
|
},
|
||||||
"traductions": {
|
"traductions": {
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"repeatable": false,
|
"component": "trad.traductions",
|
||||||
"component": "trad.traductions"
|
"repeatable": false
|
||||||
},
|
},
|
||||||
"streamVideo": {
|
"streamVideo": {
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"repeatable": true,
|
"component": "url.liens",
|
||||||
"component": "url.liens"
|
"repeatable": true
|
||||||
},
|
},
|
||||||
"streamAudio": {
|
"streamAudio": {
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"repeatable": true,
|
"component": "store.album",
|
||||||
"component": "store.album"
|
"repeatable": true
|
||||||
},
|
},
|
||||||
"commentaires": {
|
"commentaires": {
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
@@ -88,8 +89,8 @@
|
|||||||
},
|
},
|
||||||
"difference": {
|
"difference": {
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"repeatable": true,
|
"component": "difference.paroles-diff",
|
||||||
"component": "difference.paroles-diff"
|
"repeatable": true
|
||||||
},
|
},
|
||||||
"gadeEmbed": {
|
"gadeEmbed": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -118,6 +119,45 @@
|
|||||||
"videos",
|
"videos",
|
||||||
"files"
|
"files"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"pawol": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false,
|
||||||
|
"allowedTypes": [
|
||||||
|
"files"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"langueSource": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": ["ka", "fr", "en", "es", "de", "it"],
|
||||||
|
"default": "ka"
|
||||||
|
},
|
||||||
|
"sourceOriginale": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToOne",
|
||||||
|
"target": "api::parole.parole",
|
||||||
|
"inversedBy": "remixes"
|
||||||
|
},
|
||||||
|
"remixes": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToMany",
|
||||||
|
"target": "api::parole.parole",
|
||||||
|
"mappedBy": "sourceOriginale"
|
||||||
|
},
|
||||||
|
"isNewRelease": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"karaokeUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"karaokeDesktopUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"kits": {
|
||||||
|
"type": "component",
|
||||||
|
"component": "kit.lyen",
|
||||||
|
"repeatable": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import {describe, it, expect, vi} from 'vitest'
|
||||||
|
|
||||||
|
const {default: createController} = await import('../parole.js')
|
||||||
|
|
||||||
|
function buildStrapi({dbUser, artiste}) {
|
||||||
|
const paroleDocuments = {
|
||||||
|
findMany: vi.fn(async () => []),
|
||||||
|
create: vi.fn(async ({data}) => ({id: 42, ...data}))
|
||||||
|
}
|
||||||
|
const userDocuments = {
|
||||||
|
findOne: vi.fn(async () => dbUser),
|
||||||
|
update: vi.fn(async () => {})
|
||||||
|
}
|
||||||
|
const artisteDocuments = {
|
||||||
|
findOne: vi.fn(async () => artiste)
|
||||||
|
}
|
||||||
|
|
||||||
|
const strapi = {
|
||||||
|
contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})),
|
||||||
|
service: vi.fn(() => ({
|
||||||
|
validateParoles: vi.fn(),
|
||||||
|
translateLyrics: vi.fn()
|
||||||
|
})),
|
||||||
|
documents: vi.fn(uid => {
|
||||||
|
if (uid === 'plugin::users-permissions.user') return userDocuments
|
||||||
|
if (uid === 'api::artiste.artiste') return artisteDocuments
|
||||||
|
if (uid === 'api::parole.parole') return paroleDocuments
|
||||||
|
throw new Error(`unexpected uid: ${uid}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {strapi, paroleDocuments, userDocuments, artisteDocuments}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCtx(data) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
body: {data},
|
||||||
|
header: {authorization: 'Bearer faketoken'}
|
||||||
|
},
|
||||||
|
badRequest: vi.fn(),
|
||||||
|
notFound: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbUser = {id: 1, documentId: 'user-doc-1', username: 'foo', email: 'foo@bar.com'}
|
||||||
|
const artiste = {documentId: 'artiste-doc-1'}
|
||||||
|
|
||||||
|
function buildData(overrides = {}) {
|
||||||
|
return {
|
||||||
|
titre: 'Test',
|
||||||
|
transcription: 'Paroles...',
|
||||||
|
user: {...dbUser},
|
||||||
|
artistes: [{documentId: 'artiste-doc-1'}],
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('parole.create', () => {
|
||||||
|
it('crée la parole quand le user et l\'artiste existent', async () => {
|
||||||
|
const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste})
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData())
|
||||||
|
|
||||||
|
await controller.create(ctx)
|
||||||
|
|
||||||
|
expect(paroleDocuments.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse sans planter quand data.user est absent', async () => {
|
||||||
|
const {strapi, userDocuments} = buildStrapi({dbUser, artiste})
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData({user: undefined}))
|
||||||
|
|
||||||
|
await controller.create(ctx)
|
||||||
|
|
||||||
|
expect(ctx.badRequest).toHaveBeenCalled()
|
||||||
|
expect(userDocuments.findOne).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse sans planter quand data.artistes est vide', async () => {
|
||||||
|
const {strapi, artisteDocuments} = buildStrapi({dbUser, artiste})
|
||||||
|
const controller = createController({strapi})
|
||||||
|
const ctx = buildCtx(buildData({artistes: []}))
|
||||||
|
|
||||||
|
await controller.create(ctx)
|
||||||
|
|
||||||
|
expect(ctx.badRequest).toHaveBeenCalled()
|
||||||
|
expect(artisteDocuments.findOne).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,7 +2,48 @@
|
|||||||
|
|
||||||
const { createCoreController } = require('@strapi/strapi').factories;
|
const { createCoreController } = require('@strapi/strapi').factories;
|
||||||
|
|
||||||
|
const VALID_LANGS = new Set(['fr', 'en', 'es', 'de', 'it'])
|
||||||
|
|
||||||
module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
||||||
|
async export(ctx) {
|
||||||
|
const { type = 'pairs', lang, format = 'jsonl' } = ctx.query
|
||||||
|
|
||||||
|
const langs = lang
|
||||||
|
? lang.split(',').map(l => l.trim()).filter(l => VALID_LANGS.has(l))
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (lang && (!langs || langs.length === 0)) {
|
||||||
|
return ctx.badRequest('Langue(s) invalide(s). Valeurs acceptées : fr, en, es, de, it.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['pairs', 'instruct'].includes(type)) {
|
||||||
|
return ctx.badRequest('type invalide. Valeurs acceptées : pairs, instruct.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const paroles = await strapi.service('api::parole.parole').fetchAllParoles()
|
||||||
|
const { metadata, pairs } = strapi.service('api::parole.parole').buildExport(paroles, type, langs)
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
return ctx.send({ metadata, data: pairs })
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONL : première ligne = métadonnées, suivies des exemples d'entraînement.
|
||||||
|
// Pour filtrer la ligne de métadonnées : jq 'select(._metadata | not)'
|
||||||
|
const lines = [
|
||||||
|
JSON.stringify({ _metadata: true, ...metadata }),
|
||||||
|
...pairs.map(p => JSON.stringify(p)),
|
||||||
|
]
|
||||||
|
|
||||||
|
ctx.set('Content-Type', 'application/x-ndjson')
|
||||||
|
ctx.set('Content-Disposition', `attachment; filename="pawol-nu-export-${Date.now()}.jsonl"`)
|
||||||
|
ctx.body = lines.join('\n')
|
||||||
|
},
|
||||||
|
|
||||||
|
async bulkTranslate(ctx) {
|
||||||
|
const result = await strapi.service('api::parole.parole').bulkTranslateMissing()
|
||||||
|
return ctx.send(result)
|
||||||
|
},
|
||||||
|
|
||||||
async findOne(documentId) {
|
async findOne(documentId) {
|
||||||
const parole = await strapi.documents('api::parole.parole').findOne({
|
const parole = await strapi.documents('api::parole.parole').findOne({
|
||||||
documentId,
|
documentId,
|
||||||
@@ -28,32 +69,23 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
|||||||
async create(ctx) {
|
async create(ctx) {
|
||||||
const {body} = ctx.request
|
const {body} = ctx.request
|
||||||
const {data} = body
|
const {data} = body
|
||||||
|
|
||||||
|
if (!data?.user?.documentId || !data?.artistes?.[0]?.documentId) {
|
||||||
|
return ctx.badRequest('Informations manquantes.')
|
||||||
|
}
|
||||||
|
|
||||||
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription)
|
strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription)
|
||||||
|
|
||||||
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
|
|
||||||
try {
|
|
||||||
const {id} = await strapi.plugins[
|
|
||||||
'users-permissions'
|
|
||||||
].services.jwt.getToken(ctx)
|
|
||||||
|
|
||||||
if (id !== data.user.id) {
|
|
||||||
ctx.unauthorized('Opération non autorisée')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
ctx.unauthorized(ctx, err, 'Opération non autorisée')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
const user = await strapi.documents('plugin::users-permissions.user').findOne({
|
||||||
documentId: body.data.user.documentId
|
documentId: body.data.user.documentId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.notFound('Utilisateur introuvable.')
|
return ctx.notFound('Utilisateur introuvable.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
if (user.id !== data.user.id || user.username !== data.user.username || user.email !== data.user.email) {
|
||||||
ctx.badRequest('Informations non valides.')
|
return ctx.badRequest('Informations non valides.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const artiste = await strapi.documents('api::artiste.artiste').findOne({
|
const artiste = await strapi.documents('api::artiste.artiste').findOne({
|
||||||
@@ -61,7 +93,7 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!artiste) {
|
if (!artiste) {
|
||||||
ctx.notFound('Artiste introuvable.')
|
return ctx.notFound('Artiste introuvable.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserParole = await strapi.documents('api::parole.parole').findMany({
|
const currentUserParole = await strapi.documents('api::parole.parole').findMany({
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {describe, it, expect} from 'vitest'
|
||||||
|
import isApiToken from '../is-api-token.js'
|
||||||
|
|
||||||
|
describe('is-api-token policy', () => {
|
||||||
|
it('autorise une requête authentifiée par token API', () => {
|
||||||
|
const ctx = {state: {auth: {strategy: {name: 'api-token'}}}}
|
||||||
|
expect(isApiToken(ctx)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse une requête authentifiée autrement (ex: JWT utilisateur)', () => {
|
||||||
|
const ctx = {state: {auth: {strategy: {name: 'users-permissions'}}}}
|
||||||
|
expect(isApiToken(ctx)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse une requête non authentifiée', () => {
|
||||||
|
const ctx = {state: {}}
|
||||||
|
expect(isApiToken(ctx)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = policyContext => {
|
||||||
|
return policyContext.state?.auth?.strategy?.name === 'api-token'
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
path: '/paroles/export',
|
||||||
|
handler: 'parole.export',
|
||||||
|
config: { policies: [], middlewares: [] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
path: '/paroles/bulk-translate',
|
||||||
|
handler: 'parole.bulkTranslate',
|
||||||
|
config: { policies: ['api::parole.is-api-token'], middlewares: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -2,4 +2,16 @@
|
|||||||
|
|
||||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||||
|
|
||||||
module.exports = createCoreRouter('api::parole.parole')
|
module.exports = createCoreRouter('api::parole.parole', {
|
||||||
|
config: {
|
||||||
|
create: {
|
||||||
|
policies: ['global::is-payload-owner']
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}]
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import {describe, it, expect, vi, afterEach} from 'vitest'
|
||||||
|
|
||||||
|
const {default: createService} = await import('../parole.js')
|
||||||
|
|
||||||
|
function fakeDeeplResponse(text) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({translations: [{text}]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Translator (DeepL)', () => {
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('attache un timeout à la requête DeepL', async () => {
|
||||||
|
global.fetch = vi.fn(async () => fakeDeeplResponse('hello'))
|
||||||
|
|
||||||
|
const strapi = {contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'}))}
|
||||||
|
const service = createService({strapi})
|
||||||
|
await service.translate('FR', 'EN', 'bonjour')
|
||||||
|
|
||||||
|
const [, options] = global.fetch.mock.calls[0]
|
||||||
|
expect(options.signal).toBeInstanceOf(AbortSignal)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('translateLyrics', () => {
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('continue les autres langues quand une traduction DeepL échoue', async () => {
|
||||||
|
global.fetch = vi.fn(async (_url, options) => {
|
||||||
|
const {target_lang: target} = JSON.parse(options.body)
|
||||||
|
if (target === 'ES') {
|
||||||
|
return {ok: false, status: 500, text: async () => 'boom'}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeDeeplResponse(`traduit-${target}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const strapi = {
|
||||||
|
contentType: vi.fn(() => ({uid: 'api::parole.parole', kind: 'collectionType'})),
|
||||||
|
log: {error: vi.fn()}
|
||||||
|
}
|
||||||
|
const service = createService({strapi})
|
||||||
|
const result = await service.translateLyrics('Bonjour le monde')
|
||||||
|
|
||||||
|
expect(result.anglais).toContain('traduit-EN')
|
||||||
|
expect(result.espagnol).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,11 +1,51 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const qs = require('qs')
|
const qs = require('qs')
|
||||||
const axios = require('axios')
|
|
||||||
const Diff = require('diff')
|
const Diff = require('diff')
|
||||||
|
|
||||||
const { createCoreService } = require('@strapi/strapi').factories;
|
const { createCoreService } = require('@strapi/strapi').factories;
|
||||||
const { ApplicationError } = require("@strapi/utils").errors
|
const { ApplicationError } = require("@strapi/utils").errors
|
||||||
|
|
||||||
|
const LANG_MAP = {
|
||||||
|
fr: { field: 'francais', targetLang: 'fr', userPrompt: 'Tradui an fransé' },
|
||||||
|
en: { field: 'anglais', targetLang: 'en', userPrompt: 'Translate to English', deeplTarget: 'EN', suffix: '\n\n (Translated by DeepL)' },
|
||||||
|
es: { field: 'espagnol', targetLang: 'es', userPrompt: 'Traduce al español', deeplTarget: 'ES', suffix: '\n\n (Traducido por DeepL)' },
|
||||||
|
de: { field: 'allemand', targetLang: 'de', userPrompt: 'Übersetze auf Deutsch', deeplTarget: 'DE', suffix: '\n\n (Übersetzt von DeepL)' },
|
||||||
|
it: { field: 'italien', targetLang: 'it', userPrompt: 'Traduci in italiano', deeplTarget: 'IT', suffix: '\n\n (Tradotto da DeepL)' },
|
||||||
|
pt: { field: 'portugais', targetLang: 'pt', userPrompt: 'Traduza para o português', deeplTarget: 'PT-BR', suffix: '\n\n (Traduzido pela DeepL)' },
|
||||||
|
ja: { field: 'japonais', targetLang: 'ja', userPrompt: '日本語に翻訳して', deeplTarget: 'JA', suffix: '\n\n (DeepLによる翻訳)' },
|
||||||
|
ko: { field: 'coreen', targetLang: 'ko', userPrompt: '한국어로 번역해줘', deeplTarget: 'KO', suffix: '\n\n (DeepL 번역)' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_LANGS = Object.keys(LANG_MAP)
|
||||||
|
|
||||||
|
function stripMarkdown(text) {
|
||||||
|
if (!text) return ''
|
||||||
|
return text
|
||||||
|
.replace(/#{1,6}\s+/g, '')
|
||||||
|
.replace(/\*\*(.*?)\*\*/gs, '$1')
|
||||||
|
.replace(/\*(.*?)\*/gs, '$1')
|
||||||
|
.replace(/__(.*?)__/gs, '$1')
|
||||||
|
.replace(/_(.*?)_/gs, '$1')
|
||||||
|
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
|
||||||
|
.replace(/^[>\-\*\+]\s+/gm, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Détecte si une transcription est probablement en français plutôt qu'en KA.
|
||||||
|
// Heuristique : si les pronoms personnels français représentent > 4 % des mots.
|
||||||
|
const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles'])
|
||||||
|
|
||||||
|
function suspectFrench(text) {
|
||||||
|
if (!text) return false
|
||||||
|
const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || []
|
||||||
|
if (words.length < 10) return false
|
||||||
|
const frCount = words.filter(w => FR_PRONOUNS.has(w)).length
|
||||||
|
return frCount / words.length > 0.04
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
class Translator {
|
class Translator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com'
|
this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com'
|
||||||
@@ -14,50 +54,52 @@ class Translator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(origin, target, text) {
|
async get(origin, target, text) {
|
||||||
try {
|
const response = await fetch(this.urlRequest, {
|
||||||
const data = {
|
method: 'POST',
|
||||||
auth_key: this.deeplKey,
|
|
||||||
source_lang: origin,
|
|
||||||
target_lang: target,
|
|
||||||
text
|
|
||||||
}
|
|
||||||
const result = await axios.post(this.urlRequest, {
|
|
||||||
text: Array.isArray(text) ? text : [text],
|
|
||||||
source_lang: origin,
|
|
||||||
target_lang: target,
|
|
||||||
}, {
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `DeepL-Auth-Key ${this.deeplKey}`,
|
Authorization: `DeepL-Auth-Key ${this.deeplKey}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: Array.isArray(text) ? text : [text],
|
||||||
|
source_lang: origin,
|
||||||
|
target_lang: target,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(15_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.data
|
if (!response.ok) {
|
||||||
} catch (error) {
|
const body = await response.text()
|
||||||
console.error('DeepL error:', error?.response?.data || error);
|
console.error('DeepL error:', body)
|
||||||
|
throw new Error(`DeepL ${response.status}: ${body}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const translator = new Translator()
|
||||||
|
|
||||||
module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
||||||
async translate(origin, target, text) {
|
async translate(origin, target, text) {
|
||||||
const translator = new Translator()
|
|
||||||
const data = await translator.get(origin, target, text)
|
const data = await translator.get(origin, target, text)
|
||||||
return data.translations[0].text
|
return data.translations[0].text
|
||||||
},
|
},
|
||||||
async translateLyrics(parolesFR) {
|
async translateLyrics(parolesFR) {
|
||||||
const anglais = await this.translate('FR', 'EN', parolesFR)
|
const result = { francais: parolesFR }
|
||||||
const espagnol = await this.translate('FR', 'ES', parolesFR)
|
|
||||||
const allemand = await this.translate('FR', 'DE', parolesFR)
|
|
||||||
const italien = await this.translate('FR', 'IT', parolesFR)
|
|
||||||
|
|
||||||
return {
|
for (const lang of ALL_LANGS) {
|
||||||
francais: parolesFR,
|
if (lang === 'fr') continue
|
||||||
anglais: anglais + '\n\n (Translated by DeepL)',
|
const { field, deeplTarget, suffix } = LANG_MAP[lang]
|
||||||
espagnol: espagnol + '\n\n (Traducido por DeepL)',
|
try {
|
||||||
allemand: allemand + '\n\n (Übersetzt von DeepL)',
|
const translated = await this.translate('FR', deeplTarget, parolesFR)
|
||||||
italien: italien + '\n\n (Tradotto da DeepL)'
|
result[field] = translated + suffix
|
||||||
|
} catch (err) {
|
||||||
|
strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
validateParoles(titre, transcription) {
|
validateParoles(titre, transcription) {
|
||||||
if (!titre || titre.trim().length === 0) {
|
if (!titre || titre.trim().length === 0) {
|
||||||
@@ -72,6 +114,161 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
|
|||||||
throw new ApplicationError('La transcription doit contenir au moins 10 caractères.')
|
throw new ApplicationError('La transcription doit contenir au moins 10 caractères.')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async fetchAllParoles() {
|
||||||
|
const pageSize = 100
|
||||||
|
let start = 0
|
||||||
|
const all = []
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const batch = await strapi.documents('api::parole.parole').findMany({
|
||||||
|
status: 'published',
|
||||||
|
populate: ['artistes', 'traductions'],
|
||||||
|
fields: ['documentId', 'titre', 'slug', 'transcription', 'annee', 'langueSource'],
|
||||||
|
limit: pageSize,
|
||||||
|
start,
|
||||||
|
})
|
||||||
|
all.push(...batch)
|
||||||
|
if (batch.length < pageSize) break
|
||||||
|
start += pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
},
|
||||||
|
|
||||||
|
buildExport(paroles, type, langs) {
|
||||||
|
const targetLangs = langs && langs.length ? langs : ALL_LANGS
|
||||||
|
const pairs = []
|
||||||
|
const missing = []
|
||||||
|
const nonKa = []
|
||||||
|
const langCounts = {}
|
||||||
|
|
||||||
|
for (const parole of paroles) {
|
||||||
|
const source = stripMarkdown(parole.transcription)
|
||||||
|
const sourceLang = parole.langueSource || 'ka'
|
||||||
|
const artists = (parole.artistes || []).map(a => a.alias)
|
||||||
|
const paroleMeta = { title: parole.titre, artists }
|
||||||
|
|
||||||
|
if (sourceLang !== 'ka') {
|
||||||
|
nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang })
|
||||||
|
} else if (suspectFrench(source)) {
|
||||||
|
nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field])
|
||||||
|
if (missingLangs.length > 0) {
|
||||||
|
missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lang of targetLangs) {
|
||||||
|
const { field, targetLang, userPrompt } = LANG_MAP[lang]
|
||||||
|
if (lang === sourceLang) continue
|
||||||
|
const target = stripMarkdown(parole.traductions?.[field])
|
||||||
|
if (!target) continue
|
||||||
|
|
||||||
|
langCounts[lang] = (langCounts[lang] || 0) + 1
|
||||||
|
|
||||||
|
if (type === 'instruct') {
|
||||||
|
const systemPrompt = sourceLang === 'ka'
|
||||||
|
? 'Tu es un expert en langue KA (créole guadeloupéen/martiniquais). Traduis le texte KA suivant.'
|
||||||
|
: `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).`
|
||||||
|
pairs.push({
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: `${userPrompt} :\n\n${source}` },
|
||||||
|
{ role: 'assistant', content: target },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
pairs.push({
|
||||||
|
source_lang: sourceLang,
|
||||||
|
target_lang: targetLang,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
...paroleMeta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
exported_at: new Date().toISOString(),
|
||||||
|
total_paroles: paroles.length,
|
||||||
|
total_pairs: pairs.length,
|
||||||
|
languages: langCounts,
|
||||||
|
missing_translations: missing,
|
||||||
|
non_ka_transcriptions: nonKa,
|
||||||
|
}
|
||||||
|
|
||||||
|
return { metadata, pairs }
|
||||||
|
},
|
||||||
|
|
||||||
|
async bulkTranslateMissing() {
|
||||||
|
const TARGET_LANGS = ALL_LANGS
|
||||||
|
.filter(lang => lang !== 'fr')
|
||||||
|
.map(lang => ({ lang, ...LANG_MAP[lang] }))
|
||||||
|
|
||||||
|
const pageSize = 100
|
||||||
|
let start = 0
|
||||||
|
const all = []
|
||||||
|
while (true) {
|
||||||
|
const batch = await strapi.documents('api::parole.parole').findMany({
|
||||||
|
status: 'published',
|
||||||
|
populate: ['traductions'],
|
||||||
|
fields: ['documentId', 'slug', 'titre', 'transcription', 'langueSource'],
|
||||||
|
limit: pageSize,
|
||||||
|
start,
|
||||||
|
})
|
||||||
|
all.push(...batch)
|
||||||
|
if (batch.length < pageSize) break
|
||||||
|
start += pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
const translated = []
|
||||||
|
const skipped = []
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (const parole of all) {
|
||||||
|
const sourceFR = parole.traductions?.francais
|
||||||
|
|| (parole.langueSource === 'fr' ? parole.transcription : null)
|
||||||
|
|
||||||
|
if (!sourceFR) { skipped.push(parole.slug); continue }
|
||||||
|
|
||||||
|
const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field])
|
||||||
|
if (missing.length === 0) { skipped.push(parole.slug); continue }
|
||||||
|
|
||||||
|
const { id: _id, ...tradData } = parole.traductions || {}
|
||||||
|
const updatedTrad = { ...tradData }
|
||||||
|
const addedLangs = []
|
||||||
|
|
||||||
|
for (const { lang, field, deeplTarget, suffix } of missing) {
|
||||||
|
try {
|
||||||
|
await sleep(700)
|
||||||
|
const result = await translator.get('FR', deeplTarget, sourceFR)
|
||||||
|
const text = result?.translations?.[0]?.text
|
||||||
|
if (text) {
|
||||||
|
updatedTrad[field] = text + suffix
|
||||||
|
addedLangs.push(lang)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedLangs.length > 0) {
|
||||||
|
await strapi.documents('api::parole.parole').update({
|
||||||
|
documentId: parole.documentId,
|
||||||
|
data: { traductions: updatedTrad },
|
||||||
|
})
|
||||||
|
await strapi.documents('api::parole.parole').publish({
|
||||||
|
documentId: parole.documentId,
|
||||||
|
})
|
||||||
|
translated.push({ slug: parole.slug, langs: addedLangs })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { translated, skipped, errors }
|
||||||
|
},
|
||||||
|
|
||||||
parolesDiff(titre = '', oldString, newString) {
|
parolesDiff(titre = '', oldString, newString) {
|
||||||
const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée')
|
const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée')
|
||||||
const parsePatch = Diff.parsePatch(patch)
|
const parsePatch = Diff.parsePatch(patch)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_kit_lyens",
|
||||||
|
"info": {
|
||||||
|
"displayName": "Kit",
|
||||||
|
"icon": "database",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"plateforme": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": ["kDrive", "NIYAJ"],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": ["Stems", "Acapella + Instru"],
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_social_rezo_sosyal",
|
||||||
|
"info": {
|
||||||
|
"displayName": "Rézo Sosyal",
|
||||||
|
"icon": "earth",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"plateforme": {
|
||||||
|
"type": "enumeration",
|
||||||
|
"enum": [
|
||||||
|
"Mastodon",
|
||||||
|
"Peertube",
|
||||||
|
"Pixelfed",
|
||||||
|
"Funkwhale",
|
||||||
|
"Bluesky",
|
||||||
|
"Instagram",
|
||||||
|
"Youtube",
|
||||||
|
"Tiktok",
|
||||||
|
"Spotify",
|
||||||
|
"Deezer",
|
||||||
|
"Applemusic",
|
||||||
|
"Bandcamp",
|
||||||
|
"Soundcloud",
|
||||||
|
"Facebook",
|
||||||
|
"Twitter",
|
||||||
|
"Linktree",
|
||||||
|
"SiteWeb"
|
||||||
|
],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,15 @@
|
|||||||
},
|
},
|
||||||
"italien": {
|
"italien": {
|
||||||
"type": "richtext"
|
"type": "richtext"
|
||||||
|
},
|
||||||
|
"portugais": {
|
||||||
|
"type": "richtext"
|
||||||
|
},
|
||||||
|
"japonais": {
|
||||||
|
"type": "richtext"
|
||||||
|
},
|
||||||
|
"coreen": {
|
||||||
|
"type": "richtext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const yup = require('yup');
|
|
||||||
|
|
||||||
module.exports = (plugin) => {
|
|
||||||
/**
|
|
||||||
* Validation custom (équivalent Strapi)
|
|
||||||
*/
|
|
||||||
const registerSchema = yup.object({
|
|
||||||
username: yup.string().required(),
|
|
||||||
email: yup.string().email().required(),
|
|
||||||
password: yup.string().min(6).required(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const validateRegisterBody = async (data) => {
|
|
||||||
try {
|
|
||||||
return await registerSchema.validate(data, {
|
|
||||||
abortEarly: false,
|
|
||||||
stripUnknown: true,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
const message = err.errors.join(', ');
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override du controller register
|
|
||||||
*/
|
|
||||||
plugin.controllers.auth.register = async (ctx) => {
|
|
||||||
const { body } = ctx.request;
|
|
||||||
|
|
||||||
// 🔒 1. Validation
|
|
||||||
let params;
|
|
||||||
try {
|
|
||||||
params = await validateRegisterBody(body);
|
|
||||||
} catch (err) {
|
|
||||||
return ctx.badRequest(err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, username, password } = params;
|
|
||||||
|
|
||||||
// 🔎 2. Vérifier si user existe déjà
|
|
||||||
const userService = strapi.service('plugin::users-permissions.user');
|
|
||||||
|
|
||||||
const existingUser = await userService.fetchAll({
|
|
||||||
filters: {
|
|
||||||
$or: [{ email }, { username }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser.length > 0) {
|
|
||||||
return ctx.badRequest('Email or Username already taken');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚙️ 3. Récupérer rôle "authenticated"
|
|
||||||
const role = await strapi
|
|
||||||
.query('plugin::users-permissions.role')
|
|
||||||
.findOne({ where: { type: 'authenticated' } });
|
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
return ctx.badRequest('Default role not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 👤 4. Création user
|
|
||||||
const newUser = await userService.add({
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
role: role.id,
|
|
||||||
confirmed: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🔑 5. Générer JWT
|
|
||||||
const jwtService = strapi.service('plugin::users-permissions.jwt');
|
|
||||||
|
|
||||||
const token = jwtService.issue({ id: newUser.id });
|
|
||||||
|
|
||||||
// 📤 6. Réponse
|
|
||||||
ctx.send({
|
|
||||||
jwt: token,
|
|
||||||
user: newUser,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return plugin;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {describe, it, expect, vi} from 'vitest'
|
||||||
|
|
||||||
|
const {default: isDocumentOwner} = await import('../is-document-owner.js')
|
||||||
|
|
||||||
|
function buildStrapi({jwtUserId, document}) {
|
||||||
|
const dbQuery = {
|
||||||
|
findOne: vi.fn(async () => document)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: {
|
||||||
|
'users-permissions': {
|
||||||
|
services: {
|
||||||
|
jwt: {
|
||||||
|
getToken: vi.fn(async () => ({id: jwtUserId}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
query: vi.fn(() => dbQuery)
|
||||||
|
},
|
||||||
|
dbQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDocumentId} = {}) {
|
||||||
|
return {
|
||||||
|
params: paramId ? {id: paramId} : {},
|
||||||
|
request: {
|
||||||
|
header: authorization ? {authorization} : {},
|
||||||
|
body: {data: {documentId: bodyDocumentId}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('is-document-owner policy', () => {
|
||||||
|
it("refuse quand aucun en-tête d'autorisation n'est présent", async () => {
|
||||||
|
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||||
|
const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'})
|
||||||
|
|
||||||
|
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('autorise quand le user du JWT est le propriétaire du document (id dans les params)', async () => {
|
||||||
|
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||||
|
const policyContext = buildPolicyContext({paramId: 'doc-1'})
|
||||||
|
|
||||||
|
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true)
|
||||||
|
expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('autorise quand le documentId vient du corps de la requête (cas du contrôleur parole.update)', async () => {
|
||||||
|
const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||||
|
const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'})
|
||||||
|
|
||||||
|
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("refuse quand le user du JWT n'est pas le propriétaire du document", async () => {
|
||||||
|
const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}})
|
||||||
|
const policyContext = buildPolicyContext({paramId: 'doc-1'})
|
||||||
|
|
||||||
|
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("refuse quand le document ciblé n'existe pas", async () => {
|
||||||
|
const strapi = buildStrapi({jwtUserId: 1, document: null})
|
||||||
|
const policyContext = buildPolicyContext({paramId: 'doc-inconnu'})
|
||||||
|
|
||||||
|
await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {describe, it, expect, vi} from 'vitest'
|
||||||
|
|
||||||
|
const {default: isPayloadOwner} = await import('../is-payload-owner.js')
|
||||||
|
|
||||||
|
function buildStrapi(jwtUserId) {
|
||||||
|
return {
|
||||||
|
plugins: {
|
||||||
|
'users-permissions': {
|
||||||
|
services: {
|
||||||
|
jwt: {
|
||||||
|
getToken: vi.fn(async () => ({id: jwtUserId}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPolicyContext({authorization, payloadUserId}) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
header: authorization ? {authorization} : {},
|
||||||
|
body: {data: {user: {id: payloadUserId}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('is-payload-owner policy', () => {
|
||||||
|
it("refuse quand aucun en-tête d'autorisation n'est présent", async () => {
|
||||||
|
const strapi = buildStrapi(999)
|
||||||
|
const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1})
|
||||||
|
|
||||||
|
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('autorise quand le user du JWT correspond au user du payload', async () => {
|
||||||
|
const strapi = buildStrapi(1)
|
||||||
|
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1})
|
||||||
|
|
||||||
|
await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse quand le user du JWT ne correspond pas au user du payload', async () => {
|
||||||
|
const strapi = buildStrapi(999)
|
||||||
|
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1})
|
||||||
|
|
||||||
|
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuse quand le token est invalide', async () => {
|
||||||
|
const strapi = {
|
||||||
|
plugins: {
|
||||||
|
'users-permissions': {
|
||||||
|
services: {
|
||||||
|
jwt: {
|
||||||
|
getToken: vi.fn(async () => {
|
||||||
|
throw new Error('Invalid token.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1})
|
||||||
|
|
||||||
|
await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors
|
||||||
|
|
||||||
|
module.exports = async (policyContext, config, {strapi}) => {
|
||||||
|
const {request, params} = policyContext
|
||||||
|
|
||||||
|
if (!request?.header?.authorization) {
|
||||||
|
throw new UnauthorizedError('Opération non autorisée')
|
||||||
|
}
|
||||||
|
|
||||||
|
let jwtUserId
|
||||||
|
try {
|
||||||
|
({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext))
|
||||||
|
} catch (err) {
|
||||||
|
throw new UnauthorizedError('Opération non autorisée')
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentId = params?.id ?? request.body?.data?.documentId
|
||||||
|
|
||||||
|
const document = await strapi.db.query(config.uid).findOne({
|
||||||
|
where: {documentId},
|
||||||
|
populate: {user: true}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new NotFoundError('Ressource introuvable.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.user?.id !== jwtUserId) {
|
||||||
|
throw new UnauthorizedError('Opération non autorisée')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { UnauthorizedError } = require('@strapi/utils').errors
|
||||||
|
|
||||||
|
module.exports = async (policyContext, config, {strapi}) => {
|
||||||
|
const {request} = policyContext
|
||||||
|
|
||||||
|
if (!request?.header?.authorization) {
|
||||||
|
throw new UnauthorizedError('Opération non autorisée')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext)
|
||||||
|
|
||||||
|
if (id !== request.body?.data?.user?.id) {
|
||||||
|
throw new UnauthorizedError('Opération non autorisée')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new UnauthorizedError('Opération non autorisée')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {describe, it, expect, afterEach} from 'vitest'
|
||||||
|
import fs from 'fs'
|
||||||
|
import os from 'os'
|
||||||
|
import path from 'path'
|
||||||
|
import {backupDatabase} from '../backup-database.js'
|
||||||
|
|
||||||
|
const tmpDirs = []
|
||||||
|
|
||||||
|
function makeTmpDir() {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-'))
|
||||||
|
tmpDirs.push(dir)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('backupDatabase', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tmpDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, {recursive: true, force: true})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ne fait rien quand le fichier de base n'existe pas", () => {
|
||||||
|
const root = makeTmpDir()
|
||||||
|
const result = backupDatabase({
|
||||||
|
dbPath: path.join(root, 'inexistant.db'),
|
||||||
|
backupsDir: path.join(root, 'backups')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(fs.existsSync(path.join(root, 'backups'))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('copie le fichier de base dans le dossier de sauvegardes', () => {
|
||||||
|
const root = makeTmpDir()
|
||||||
|
const dbPath = path.join(root, 'data.db')
|
||||||
|
fs.writeFileSync(dbPath, 'contenu-de-la-base')
|
||||||
|
const backupsDir = path.join(root, 'backups')
|
||||||
|
|
||||||
|
const result = backupDatabase({dbPath, backupsDir})
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(fs.existsSync(result)).toBe(true)
|
||||||
|
expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne conserve que les 8 sauvegardes les plus récentes', () => {
|
||||||
|
const root = makeTmpDir()
|
||||||
|
const dbPath = path.join(root, 'data.db')
|
||||||
|
fs.writeFileSync(dbPath, 'contenu')
|
||||||
|
const backupsDir = path.join(root, 'backups')
|
||||||
|
fs.mkdirSync(backupsDir, {recursive: true})
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien')
|
||||||
|
}
|
||||||
|
|
||||||
|
backupDatabase({dbPath, backupsDir})
|
||||||
|
|
||||||
|
const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-'))
|
||||||
|
expect(remaining).toHaveLength(8)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const RETENTION = 8;
|
||||||
|
|
||||||
|
function backupDatabase({dbPath, backupsDir}) {
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(backupsDir, {recursive: true});
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const backupPath = path.join(backupsDir, `data-${timestamp}.db`);
|
||||||
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
|
||||||
|
const backups = fs.readdirSync(backupsDir)
|
||||||
|
.filter(name => name.startsWith('data-') && name.endsWith('.db'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const toDelete = backups.slice(0, Math.max(backups.length - RETENTION, 0));
|
||||||
|
for (const name of toDelete) {
|
||||||
|
fs.unlinkSync(path.join(backupsDir, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {backupDatabase};
|
||||||
Vendored
+55
@@ -25,6 +25,56 @@ export interface DifferenceParolesDiff extends Struct.ComponentSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KitLyen extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_kit_lyens';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Kit';
|
||||||
|
icon: 'database';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
plateforme: Schema.Attribute.Enumeration<['kDrive', 'NIYAJ']> &
|
||||||
|
Schema.Attribute.Required;
|
||||||
|
type: Schema.Attribute.Enumeration<['Stems', 'Acapella + Instru']> &
|
||||||
|
Schema.Attribute.Required;
|
||||||
|
url: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocialRezoSosyal extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_social_rezo_sosyal';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'R\u00E9zo Sosyal';
|
||||||
|
icon: 'earth';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
plateforme: Schema.Attribute.Enumeration<
|
||||||
|
[
|
||||||
|
'Mastodon',
|
||||||
|
'Peertube',
|
||||||
|
'Pixelfed',
|
||||||
|
'Funkwhale',
|
||||||
|
'Bluesky',
|
||||||
|
'Instagram',
|
||||||
|
'Youtube',
|
||||||
|
'Tiktok',
|
||||||
|
'Spotify',
|
||||||
|
'Deezer',
|
||||||
|
'Applemusic',
|
||||||
|
'Bandcamp',
|
||||||
|
'Soundcloud',
|
||||||
|
'Facebook',
|
||||||
|
'Twitter',
|
||||||
|
'Linktree',
|
||||||
|
'SiteWeb',
|
||||||
|
]
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Required;
|
||||||
|
url: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoreAlbum extends Struct.ComponentSchema {
|
export interface StoreAlbum extends Struct.ComponentSchema {
|
||||||
collectionName: 'components_store_albums';
|
collectionName: 'components_store_albums';
|
||||||
info: {
|
info: {
|
||||||
@@ -59,9 +109,12 @@ export interface TradTraductions extends Struct.ComponentSchema {
|
|||||||
attributes: {
|
attributes: {
|
||||||
allemand: Schema.Attribute.RichText;
|
allemand: Schema.Attribute.RichText;
|
||||||
anglais: Schema.Attribute.RichText;
|
anglais: Schema.Attribute.RichText;
|
||||||
|
coreen: Schema.Attribute.RichText;
|
||||||
espagnol: Schema.Attribute.RichText;
|
espagnol: Schema.Attribute.RichText;
|
||||||
francais: Schema.Attribute.RichText;
|
francais: Schema.Attribute.RichText;
|
||||||
italien: Schema.Attribute.RichText;
|
italien: Schema.Attribute.RichText;
|
||||||
|
japonais: Schema.Attribute.RichText;
|
||||||
|
portugais: Schema.Attribute.RichText;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +137,8 @@ declare module '@strapi/strapi' {
|
|||||||
export module Public {
|
export module Public {
|
||||||
export interface ComponentSchemas {
|
export interface ComponentSchemas {
|
||||||
'difference.paroles-diff': DifferenceParolesDiff;
|
'difference.paroles-diff': DifferenceParolesDiff;
|
||||||
|
'kit.lyen': KitLyen;
|
||||||
|
'social.rezo-sosyal': SocialRezoSosyal;
|
||||||
'store.album': StoreAlbum;
|
'store.album': StoreAlbum;
|
||||||
'trad.traductions': TradTraductions;
|
'trad.traductions': TradTraductions;
|
||||||
'url.liens': UrlLiens;
|
'url.liens': UrlLiens;
|
||||||
|
|||||||
Vendored
+18
@@ -448,6 +448,8 @@ export interface ApiArtisteArtiste extends Struct.CollectionTypeSchema {
|
|||||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
dateNaissance: Schema.Attribute.Date;
|
dateNaissance: Schema.Attribute.Date;
|
||||||
|
isExclusiveArtist: Schema.Attribute.Boolean &
|
||||||
|
Schema.Attribute.DefaultTo<false>;
|
||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<
|
localizations: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
@@ -460,7 +462,9 @@ export interface ApiArtisteArtiste extends Struct.CollectionTypeSchema {
|
|||||||
photo: Schema.Attribute.Media<'images'>;
|
photo: Schema.Attribute.Media<'images'>;
|
||||||
prenom: Schema.Attribute.String;
|
prenom: Schema.Attribute.String;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
rezoSosyal: Schema.Attribute.Component<'social.rezo-sosyal', true>;
|
||||||
slug: Schema.Attribute.String;
|
slug: Schema.Attribute.String;
|
||||||
|
titrePhare: Schema.Attribute.Relation<'manyToOne', 'api::parole.parole'>;
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
@@ -540,6 +544,14 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema {
|
|||||||
>;
|
>;
|
||||||
forceSlug: Schema.Attribute.Boolean;
|
forceSlug: Schema.Attribute.Boolean;
|
||||||
gadeEmbed: Schema.Attribute.String;
|
gadeEmbed: Schema.Attribute.String;
|
||||||
|
isNewRelease: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
|
karaokeDesktopUrl: Schema.Attribute.String;
|
||||||
|
karaokeUrl: Schema.Attribute.String;
|
||||||
|
kits: Schema.Attribute.Component<'kit.lyen', true>;
|
||||||
|
langueSource: Schema.Attribute.Enumeration<
|
||||||
|
['ka', 'fr', 'en', 'es', 'de', 'it']
|
||||||
|
> &
|
||||||
|
Schema.Attribute.DefaultTo<'ka'>;
|
||||||
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
localizations: Schema.Attribute.Relation<
|
localizations: Schema.Attribute.Relation<
|
||||||
'oneToMany',
|
'oneToMany',
|
||||||
@@ -548,9 +560,15 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema {
|
|||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
musicBrainzUrl: Schema.Attribute.String;
|
musicBrainzUrl: Schema.Attribute.String;
|
||||||
okiMizikID: Schema.Attribute.Integer;
|
okiMizikID: Schema.Attribute.Integer;
|
||||||
|
pawol: Schema.Attribute.Media<'files'>;
|
||||||
prioriteArtistes: Schema.Attribute.String;
|
prioriteArtistes: Schema.Attribute.String;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
remixes: Schema.Attribute.Relation<'oneToMany', 'api::parole.parole'>;
|
||||||
slug: Schema.Attribute.String & Schema.Attribute.Unique;
|
slug: Schema.Attribute.String & Schema.Attribute.Unique;
|
||||||
|
sourceOriginale: Schema.Attribute.Relation<
|
||||||
|
'manyToOne',
|
||||||
|
'api::parole.parole'
|
||||||
|
>;
|
||||||
streamAudio: Schema.Attribute.Component<'store.album', true>;
|
streamAudio: Schema.Attribute.Component<'store.album', true>;
|
||||||
streamVideo: Schema.Attribute.Component<'url.liens', true>;
|
streamVideo: Schema.Attribute.Component<'url.liens', true>;
|
||||||
titre: Schema.Attribute.String & Schema.Attribute.Required;
|
titre: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {defineConfig} from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -661,6 +661,21 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
"@emnapi/core@1.11.1":
|
||||||
|
version "1.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6"
|
||||||
|
integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==
|
||||||
|
dependencies:
|
||||||
|
"@emnapi/wasi-threads" "1.2.2"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@emnapi/runtime@1.11.1":
|
||||||
|
version "1.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24"
|
||||||
|
integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@emnapi/runtime@^1.2.0":
|
"@emnapi/runtime@^1.2.0":
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c"
|
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c"
|
||||||
@@ -668,6 +683,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads@1.2.2":
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a"
|
||||||
|
integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@emotion/babel-plugin@^11.10.5":
|
"@emotion/babel-plugin@^11.10.5":
|
||||||
version "11.10.5"
|
version "11.10.5"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c"
|
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c"
|
||||||
@@ -1352,7 +1374,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec@^1.5.0":
|
"@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
|
||||||
version "1.5.5"
|
version "1.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
||||||
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
||||||
@@ -1460,6 +1482,13 @@
|
|||||||
hls.js "~1.5.11"
|
hls.js "~1.5.11"
|
||||||
mux-embed "^5.3.1"
|
mux-embed "^5.3.1"
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime@^1.1.6":
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5"
|
||||||
|
integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==
|
||||||
|
dependencies:
|
||||||
|
"@tybys/wasm-util" "^0.10.3"
|
||||||
|
|
||||||
"@noble/hashes@^1.1.5":
|
"@noble/hashes@^1.1.5":
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a"
|
||||||
@@ -1491,6 +1520,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
|
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
|
||||||
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
|
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
|
||||||
|
|
||||||
|
"@oxc-project/types@=0.138.0":
|
||||||
|
version "0.138.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.138.0.tgz#ce9690ec80144b4c38dfed44e76a67b911df9aca"
|
||||||
|
integrity sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==
|
||||||
|
|
||||||
"@paralleldrive/cuid2@2.2.2":
|
"@paralleldrive/cuid2@2.2.2":
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz#7f91364d53b89e2c9cb9e02e8dd0f129e834455f"
|
resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz#7f91364d53b89e2c9cb9e02e8dd0f129e834455f"
|
||||||
@@ -2200,6 +2234,90 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.2.tgz#156c4b481c0bee22a19f7924728a67120de06971"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.2.tgz#156c4b481c0bee22a19f7924728a67120de06971"
|
||||||
integrity sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==
|
integrity sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz#6c31f378f42d5ea75b37970d2add539682264792"
|
||||||
|
integrity sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz#1dbbbbaf2b00d13b32127cbf3a379d6ef69fd647"
|
||||||
|
integrity sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz#3b8f76dfd462e28e8914e7ded63647194866329f"
|
||||||
|
integrity sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz#7f3f82c6a7b9fcec4e24c29e89d8343c9c72d403"
|
||||||
|
integrity sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz#baee435c7addf84e58fee72c70e98acc603aba3e"
|
||||||
|
integrity sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz#cc8c63913bac68bf1f49a0d095634e09008741ce"
|
||||||
|
integrity sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz#aaa1d02f5ee705bd6812ff84ab1f7752ba4e4497"
|
||||||
|
integrity sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz#9e26bb9ba2a846039902270b778d1d3ba0b62033"
|
||||||
|
integrity sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-s390x-gnu@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz#d9ec4d5b1bd6f8029072670da9f0d58e728e8c44"
|
||||||
|
integrity sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz#6cc9cad61ea020ac2191037755a36f8fcfa11434"
|
||||||
|
integrity sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz#d23df39a184be427877da934fc3c30e3f25cd686"
|
||||||
|
integrity sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz#189d1a062624f57f9b0a52f94163d51b6aa692ab"
|
||||||
|
integrity sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz#98c634b91a9abf5de74b8402aa8063fb5044efd4"
|
||||||
|
integrity sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==
|
||||||
|
dependencies:
|
||||||
|
"@emnapi/core" "1.11.1"
|
||||||
|
"@emnapi/runtime" "1.11.1"
|
||||||
|
"@napi-rs/wasm-runtime" "^1.1.6"
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz#1c8c8515c811ae28fc26355079b80f617fdddb0b"
|
||||||
|
integrity sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz#e14b1ef53473f51a4180d9b46c5f0cea9f1713cb"
|
||||||
|
integrity sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==
|
||||||
|
|
||||||
|
"@rolldown/pluginutils@^1.0.0":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
|
||||||
|
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi@4.60.1":
|
"@rollup/rollup-android-arm-eabi@4.60.1":
|
||||||
version "4.60.1"
|
version "4.60.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz#043f145716234529052ef9e1ce1d847ffbe9e674"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz#043f145716234529052ef9e1ce1d847ffbe9e674"
|
||||||
@@ -2361,7 +2479,7 @@
|
|||||||
escape-string-regexp "^2.0.0"
|
escape-string-regexp "^2.0.0"
|
||||||
lodash.deburr "^4.1.0"
|
lodash.deburr "^4.1.0"
|
||||||
|
|
||||||
"@standard-schema/spec@^1.0.0":
|
"@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
|
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
|
||||||
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
||||||
@@ -3179,6 +3297,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
||||||
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
|
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
|
||||||
|
|
||||||
|
"@tybys/wasm-util@^0.10.3":
|
||||||
|
version "0.10.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d"
|
||||||
|
integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@types/accepts@*":
|
"@types/accepts@*":
|
||||||
version "1.3.7"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
|
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
|
||||||
@@ -3214,6 +3339,14 @@
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
"@types/responselike" "*"
|
"@types/responselike" "*"
|
||||||
|
|
||||||
|
"@types/chai@^5.2.2":
|
||||||
|
version "5.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a"
|
||||||
|
integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==
|
||||||
|
dependencies:
|
||||||
|
"@types/deep-eql" "*"
|
||||||
|
assertion-error "^2.0.1"
|
||||||
|
|
||||||
"@types/co-body@^6.1.0":
|
"@types/co-body@^6.1.0":
|
||||||
version "6.1.3"
|
version "6.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.3.tgz#201796c6389066b400cfcb4e1ec5c3db798265a2"
|
resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.3.tgz#201796c6389066b400cfcb4e1ec5c3db798265a2"
|
||||||
@@ -3251,6 +3384,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/ms" "*"
|
"@types/ms" "*"
|
||||||
|
|
||||||
|
"@types/deep-eql@*":
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd"
|
||||||
|
integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
|
||||||
|
|
||||||
"@types/eslint-scope@^3.7.7":
|
"@types/eslint-scope@^3.7.7":
|
||||||
version "3.7.7"
|
version "3.7.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
|
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
|
||||||
@@ -3697,6 +3835,66 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@swc/core" "^1.3.107"
|
"@swc/core" "^1.3.107"
|
||||||
|
|
||||||
|
"@vitest/expect@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.9.tgz#ba1af73ae53262e3dc9b518cb7b76fb614e0ef53"
|
||||||
|
integrity sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==
|
||||||
|
dependencies:
|
||||||
|
"@standard-schema/spec" "^1.1.0"
|
||||||
|
"@types/chai" "^5.2.2"
|
||||||
|
"@vitest/spy" "4.1.9"
|
||||||
|
"@vitest/utils" "4.1.9"
|
||||||
|
chai "^6.2.2"
|
||||||
|
tinyrainbow "^3.1.0"
|
||||||
|
|
||||||
|
"@vitest/mocker@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.9.tgz#a483de79b358aba3dd8f319a0d8ab17c89f5c75d"
|
||||||
|
integrity sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/spy" "4.1.9"
|
||||||
|
estree-walker "^3.0.3"
|
||||||
|
magic-string "^0.30.21"
|
||||||
|
|
||||||
|
"@vitest/pretty-format@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz#885cfe9fcb6ff3df4409ea66192cc1fb23d62fae"
|
||||||
|
integrity sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow "^3.1.0"
|
||||||
|
|
||||||
|
"@vitest/runner@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.9.tgz#bb742947ce4841dfb2d8984a2f9014850be10f51"
|
||||||
|
integrity sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/utils" "4.1.9"
|
||||||
|
pathe "^2.0.3"
|
||||||
|
|
||||||
|
"@vitest/snapshot@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.9.tgz#bdfb670ae5617613ea8776e93d0666a66defeeb7"
|
||||||
|
integrity sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/pretty-format" "4.1.9"
|
||||||
|
"@vitest/utils" "4.1.9"
|
||||||
|
magic-string "^0.30.21"
|
||||||
|
pathe "^2.0.3"
|
||||||
|
|
||||||
|
"@vitest/spy@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.9.tgz#bfc40d48fb9bd1a1228bfbfde7f5555e7f6b3867"
|
||||||
|
integrity sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==
|
||||||
|
|
||||||
|
"@vitest/utils@4.1.9":
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.9.tgz#0184c7e6eb3234739b2b6b3b985f78d1ed823ee1"
|
||||||
|
integrity sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/pretty-format" "4.1.9"
|
||||||
|
convert-source-map "^2.0.0"
|
||||||
|
tinyrainbow "^3.1.0"
|
||||||
|
|
||||||
"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
|
"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6"
|
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6"
|
||||||
@@ -4082,6 +4280,11 @@ asn1.js@^5.3.0:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
safer-buffer "^2.1.0"
|
safer-buffer "^2.1.0"
|
||||||
|
|
||||||
|
assertion-error@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
|
||||||
|
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
|
||||||
|
|
||||||
ast-types@^0.16.1:
|
ast-types@^0.16.1:
|
||||||
version "0.16.1"
|
version "0.16.1"
|
||||||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2"
|
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2"
|
||||||
@@ -4451,6 +4654,11 @@ ce-la-react@^0.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/ce-la-react/-/ce-la-react-0.3.2.tgz#66c1454e024c3b9f65ebac05724d0f50756ff057"
|
resolved "https://registry.yarnpkg.com/ce-la-react/-/ce-la-react-0.3.2.tgz#66c1454e024c3b9f65ebac05724d0f50756ff057"
|
||||||
integrity sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA==
|
integrity sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA==
|
||||||
|
|
||||||
|
chai@^6.2.2:
|
||||||
|
version "6.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
|
||||||
|
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
|
||||||
|
|
||||||
chalk@4.1.2, chalk@^4.1.0, chalk@^4.1.2:
|
chalk@4.1.2, chalk@^4.1.0, chalk@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||||
@@ -5675,6 +5883,13 @@ estree-util-is-identifier-name@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd"
|
resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd"
|
||||||
integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==
|
integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==
|
||||||
|
|
||||||
|
estree-walker@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
|
||||||
|
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "^1.0.0"
|
||||||
|
|
||||||
events@^3.2.0:
|
events@^3.2.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||||
@@ -5717,6 +5932,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
homedir-polyfill "^1.0.1"
|
homedir-polyfill "^1.0.1"
|
||||||
|
|
||||||
|
expect-type@^1.3.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6"
|
||||||
|
integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==
|
||||||
|
|
||||||
extend@^3.0.0, extend@^3.0.2:
|
extend@^3.0.0, extend@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
@@ -7439,6 +7659,80 @@ liftoff@^5.0.1:
|
|||||||
rechoir "^0.8.0"
|
rechoir "^0.8.0"
|
||||||
resolve "^1.20.0"
|
resolve "^1.20.0"
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
|
||||||
|
integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
|
||||||
|
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
|
||||||
|
integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
|
||||||
|
integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
|
||||||
|
integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
|
||||||
|
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
|
||||||
|
integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
|
||||||
|
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
|
||||||
|
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
|
||||||
|
integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
|
||||||
|
integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
|
||||||
|
|
||||||
|
lightningcss@^1.32.0:
|
||||||
|
version "1.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
|
||||||
|
integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
|
||||||
|
dependencies:
|
||||||
|
detect-libc "^2.0.3"
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-android-arm64 "1.32.0"
|
||||||
|
lightningcss-darwin-arm64 "1.32.0"
|
||||||
|
lightningcss-darwin-x64 "1.32.0"
|
||||||
|
lightningcss-freebsd-x64 "1.32.0"
|
||||||
|
lightningcss-linux-arm-gnueabihf "1.32.0"
|
||||||
|
lightningcss-linux-arm64-gnu "1.32.0"
|
||||||
|
lightningcss-linux-arm64-musl "1.32.0"
|
||||||
|
lightningcss-linux-x64-gnu "1.32.0"
|
||||||
|
lightningcss-linux-x64-musl "1.32.0"
|
||||||
|
lightningcss-win32-arm64-msvc "1.32.0"
|
||||||
|
lightningcss-win32-x64-msvc "1.32.0"
|
||||||
|
|
||||||
limiter@^1.1.5:
|
limiter@^1.1.5:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
|
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
|
||||||
@@ -7649,6 +7943,13 @@ lz-string@^1.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||||
|
|
||||||
|
magic-string@^0.30.21:
|
||||||
|
version "0.30.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
|
||||||
|
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.5.5"
|
||||||
|
|
||||||
mailcomposer@3.12.0:
|
mailcomposer@3.12.0:
|
||||||
version "3.12.0"
|
version "3.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-3.12.0.tgz#9c5e1188aa8e1c62ec8b86bd43468102b639e8f9"
|
resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-3.12.0.tgz#9c5e1188aa8e1c62ec8b86bd43468102b639e8f9"
|
||||||
@@ -8345,6 +8646,11 @@ nanoid@^3.3.11:
|
|||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||||
|
|
||||||
|
nanoid@^3.3.12:
|
||||||
|
version "3.3.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316"
|
||||||
|
integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==
|
||||||
|
|
||||||
nanoid@^3.3.4:
|
nanoid@^3.3.4:
|
||||||
version "3.3.4"
|
version "3.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||||
@@ -8553,6 +8859,11 @@ object.pick@^1.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isobject "^3.0.1"
|
isobject "^3.0.1"
|
||||||
|
|
||||||
|
obug@^2.1.1:
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.3.tgz#c02c60f95abd603409330e767db7f2823193331e"
|
||||||
|
integrity sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==
|
||||||
|
|
||||||
on-finished@^2.3.0:
|
on-finished@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
@@ -8874,6 +9185,11 @@ path-type@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||||
|
|
||||||
|
pathe@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
||||||
|
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||||
|
|
||||||
pause@0.0.1:
|
pause@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
|
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
|
||||||
@@ -8904,6 +9220,11 @@ picomatch@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
|
picomatch@^4.0.3:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab"
|
||||||
|
integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==
|
||||||
|
|
||||||
picomatch@^4.0.4:
|
picomatch@^4.0.4:
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||||
@@ -9043,6 +9364,15 @@ postcss@^8.4.43:
|
|||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
postcss@^8.5.16:
|
||||||
|
version "8.5.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28"
|
||||||
|
integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.12"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
prebuild-install@^7.1.1:
|
prebuild-install@^7.1.1:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
|
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
|
||||||
@@ -9818,6 +10148,30 @@ rimraf@6.1.3:
|
|||||||
glob "^13.0.3"
|
glob "^13.0.3"
|
||||||
package-json-from-dist "^1.0.1"
|
package-json-from-dist "^1.0.1"
|
||||||
|
|
||||||
|
rolldown@~1.1.3:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.1.4.tgz#7e15fb26997353808330109ef6bffda274aaa027"
|
||||||
|
integrity sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==
|
||||||
|
dependencies:
|
||||||
|
"@oxc-project/types" "=0.138.0"
|
||||||
|
"@rolldown/pluginutils" "^1.0.0"
|
||||||
|
optionalDependencies:
|
||||||
|
"@rolldown/binding-android-arm64" "1.1.4"
|
||||||
|
"@rolldown/binding-darwin-arm64" "1.1.4"
|
||||||
|
"@rolldown/binding-darwin-x64" "1.1.4"
|
||||||
|
"@rolldown/binding-freebsd-x64" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-arm64-gnu" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-arm64-musl" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-s390x-gnu" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-x64-gnu" "1.1.4"
|
||||||
|
"@rolldown/binding-linux-x64-musl" "1.1.4"
|
||||||
|
"@rolldown/binding-openharmony-arm64" "1.1.4"
|
||||||
|
"@rolldown/binding-wasm32-wasi" "1.1.4"
|
||||||
|
"@rolldown/binding-win32-arm64-msvc" "1.1.4"
|
||||||
|
"@rolldown/binding-win32-x64-msvc" "1.1.4"
|
||||||
|
|
||||||
rollup@^4.20.0:
|
rollup@^4.20.0:
|
||||||
version "4.60.1"
|
version "4.60.1"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.1.tgz#b4aa2bcb3a5e1437b5fad40d43fe42d4bde7a42d"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.1.tgz#b4aa2bcb3a5e1437b5fad40d43fe42d4bde7a42d"
|
||||||
@@ -10146,6 +10500,11 @@ sift@16.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053"
|
resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053"
|
||||||
integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==
|
integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==
|
||||||
|
|
||||||
|
siginfo@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
|
||||||
|
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
|
||||||
|
|
||||||
signal-exit@^3.0.2:
|
signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
@@ -10335,6 +10694,11 @@ stack-trace@0.0.x:
|
|||||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||||
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
|
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
||||||
|
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
|
||||||
|
|
||||||
stackframe@^1.3.4:
|
stackframe@^1.3.4:
|
||||||
version "1.3.4"
|
version "1.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
|
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
|
||||||
@@ -10355,6 +10719,11 @@ statuses@~2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
|
||||||
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
||||||
|
|
||||||
|
std-env@^4.0.0-rc.1:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f"
|
||||||
|
integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==
|
||||||
|
|
||||||
stream-chain@2.2.5, stream-chain@^2.2.5:
|
stream-chain@2.2.5, stream-chain@^2.2.5:
|
||||||
version "2.2.5"
|
version "2.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09"
|
resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09"
|
||||||
@@ -10692,6 +11061,16 @@ tiny-warning@^1.0.2, tiny-warning@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
|
||||||
|
tinybench@^2.9.0:
|
||||||
|
version "2.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
||||||
|
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
|
||||||
|
|
||||||
|
tinyexec@^1.0.2:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71"
|
||||||
|
integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==
|
||||||
|
|
||||||
tinyglobby@^0.2.15:
|
tinyglobby@^0.2.15:
|
||||||
version "0.2.16"
|
version "0.2.16"
|
||||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
|
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
|
||||||
@@ -10700,6 +11079,19 @@ tinyglobby@^0.2.15:
|
|||||||
fdir "^6.5.0"
|
fdir "^6.5.0"
|
||||||
picomatch "^4.0.4"
|
picomatch "^4.0.4"
|
||||||
|
|
||||||
|
tinyglobby@^0.2.17:
|
||||||
|
version "0.2.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
|
||||||
|
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
|
||||||
|
dependencies:
|
||||||
|
fdir "^6.5.0"
|
||||||
|
picomatch "^4.0.4"
|
||||||
|
|
||||||
|
tinyrainbow@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421"
|
||||||
|
integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==
|
||||||
|
|
||||||
title-case@^4.3.2:
|
title-case@^4.3.2:
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/title-case/-/title-case-4.3.2.tgz#498d22ca7083dcfb9add4f4b8d98c000d05fceb3"
|
resolved "https://registry.yarnpkg.com/title-case/-/title-case-4.3.2.tgz#498d22ca7083dcfb9add4f4b8d98c000d05fceb3"
|
||||||
@@ -11139,6 +11531,45 @@ vite@5.4.21:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
|
"vite@^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||||
|
version "8.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/vite/-/vite-8.1.3.tgz#ce05b445b501014a10e5a8073811759c223e8ac2"
|
||||||
|
integrity sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==
|
||||||
|
dependencies:
|
||||||
|
lightningcss "^1.32.0"
|
||||||
|
picomatch "^4.0.4"
|
||||||
|
postcss "^8.5.16"
|
||||||
|
rolldown "~1.1.3"
|
||||||
|
tinyglobby "^0.2.17"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
|
vitest@^4.1.9:
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.9.tgz#98f22fbd70e2a18c4a92bb20624bc92e5dfac5f3"
|
||||||
|
integrity sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==
|
||||||
|
dependencies:
|
||||||
|
"@vitest/expect" "4.1.9"
|
||||||
|
"@vitest/mocker" "4.1.9"
|
||||||
|
"@vitest/pretty-format" "4.1.9"
|
||||||
|
"@vitest/runner" "4.1.9"
|
||||||
|
"@vitest/snapshot" "4.1.9"
|
||||||
|
"@vitest/spy" "4.1.9"
|
||||||
|
"@vitest/utils" "4.1.9"
|
||||||
|
es-module-lexer "^2.0.0"
|
||||||
|
expect-type "^1.3.0"
|
||||||
|
magic-string "^0.30.21"
|
||||||
|
obug "^2.1.1"
|
||||||
|
pathe "^2.0.3"
|
||||||
|
picomatch "^4.0.3"
|
||||||
|
std-env "^4.0.0-rc.1"
|
||||||
|
tinybench "^2.9.0"
|
||||||
|
tinyexec "^1.0.2"
|
||||||
|
tinyglobby "^0.2.15"
|
||||||
|
tinyrainbow "^3.1.0"
|
||||||
|
vite "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
why-is-node-running "^2.3.0"
|
||||||
|
|
||||||
vscode-oniguruma@^1.7.0:
|
vscode-oniguruma@^1.7.0:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b"
|
resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b"
|
||||||
@@ -11279,6 +11710,14 @@ which@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
|
why-is-node-running@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
|
||||||
|
integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
|
||||||
|
dependencies:
|
||||||
|
siginfo "^2.0.0"
|
||||||
|
stackback "0.0.2"
|
||||||
|
|
||||||
widest-line@^3.1.0:
|
widest-line@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
|
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
|
||||||
|
|||||||
Reference in New Issue
Block a user